// @ts-check // Requires `bash tests/e2e/setup.sh` (FreshRSS 1.30.4 on http://localhost:8080). const { test, expect } = require('@playwright/test'); const { execFileSync } = require('node:child_process'); const path = require('node:path'); const COMPOSE = path.join(__dirname, 'docker-compose.yml'); const READER = '/i/?a=reader&state=2'; // unread only function reset(height = 250) { execFileSync('docker', ['compose', '-f', COMPOSE, 'exec', '-T', '--user', 'www-data', 'freshrss', 'php', '/var/www/FreshRSS/extensions/freshrss-read-more/tests/e2e/reset.php', String(height)]); } // Entries are sorted newest first: "Article 1" is the first .flux. const fluxByTitle = (page, n) => page.locator('.flux', { hasText: `Article ${n}.` }).first(); async function scrollPast(page, flux) { await flux.evaluate((el) => { const r = el.getBoundingClientRect(); window.scrollBy(0, r.bottom + 20); }); await page.waitForTimeout(1000); // FreshRSS throttles onScroll at 500 ms await page.evaluate(() => window.dispatchEvent(new Event('scroll'))); } test.beforeEach(() => reset()); test('long entries collapsed', async ({ page }) => { await page.goto(READER); for (const n of [1, 2, 3, 4, 5, 6, 8]) { const flux = fluxByTitle(page, n); await expect(flux).toHaveAttribute('data-read-more', 'collapsed'); await expect(flux.locator('.read-more-btn')).toHaveCount(1); await expect(flux.locator('.read-more-btn')).toHaveText('Lire plus'); const text = flux.locator('.text'); expect(await text.evaluate((el) => getComputedStyle(el).maxHeight)).toBe('250px'); expect(await text.evaluate((el) => el.getBoundingClientRect().height)).toBeLessThanOrEqual(250); } }); test('short entry not collapsed', async ({ page }) => { await page.goto(READER); const flux = fluxByTitle(page, 7); await expect(flux).toHaveAttribute('data-read-more', 'short'); await expect(flux.locator('.read-more-btn')).toHaveCount(0); }); test('collapsed entry marked read after scrolling past', async ({ page }) => { await page.goto(READER); const flux = fluxByTitle(page, 1); await expect(flux).toHaveAttribute('data-read-more', 'collapsed'); await expect(flux).toHaveClass(/not_read/); await scrollPast(page, flux); await expect(flux).not.toHaveClass(/not_read/, { timeout: 5000 }); await page.waitForTimeout(500); // let the mark-read request land await page.goto(READER); await expect(page.locator('.flux', { hasText: 'Article 2.' }).first()).toBeVisible(); await expect(page.locator('.flux', { hasText: 'Article 1.' })).toHaveCount(0); }); test('read more expands without changing read state or scroll', async ({ page }) => { await page.goto(READER); const flux = fluxByTitle(page, 1); const button = flux.locator('.read-more-btn'); await button.scrollIntoViewIfNeeded(); const before = await page.evaluate(() => window.scrollY); await button.click(); await expect(flux).toHaveAttribute('data-read-more', 'expanded'); expect(await flux.locator('.text').evaluate((el) => getComputedStyle(el).maxHeight)).toBe('none'); await expect(button).toHaveCount(0); await expect(flux).toHaveClass(/not_read/); const after = await page.evaluate(() => window.scrollY); expect(Math.abs(after - before)).toBeLessThan(5); }); test('expanded entry marked read after scrolling past', async ({ page }) => { await page.goto(READER); const flux = fluxByTitle(page, 2); await flux.locator('.read-more-btn').click(); await expect(flux).toHaveAttribute('data-read-more', 'expanded'); const height = await flux.evaluate((el) => el.getBoundingClientRect().height); expect(height).toBeGreaterThan(375); await scrollPast(page, flux); await expect(flux).not.toHaveClass(/not_read/, { timeout: 5000 }); }); test('normal view untouched', async ({ page }) => { await page.goto('/i/?a=normal&state=2'); await expect(page.locator('.flux').first()).toBeAttached(); await expect(page.locator('.read-more-btn')).toHaveCount(0); await expect(page.locator('.flux[data-read-more]')).toHaveCount(0); }); test('config height is clamped', async ({ page }) => { const configUrl = '/i/?c=extension&a=configure&e=' + encodeURIComponent('Read More'); const submit = async (value) => { await page.goto(configUrl); const csrf = await page.locator('input[name="_csrf"]').first().inputValue(); const res = await page.request.post(configUrl, { form: { _csrf: csrf, collapsed_height: String(value) }, }); expect(res.ok()).toBeTruthy(); // FreshRSS answers 304 within the same second: bust the cache. await page.goto(configUrl + '&t=' + Date.now()); return page.locator('#collapsed_height').inputValue(); }; expect(await submit(5)).toBe('100'); expect(await submit(99999)).toBe('2000'); expect(await submit(400)).toBe('400'); await page.goto(READER); const text = fluxByTitle(page, 1).locator('.text'); expect(await text.evaluate((el) => getComputedStyle(el).maxHeight)).toBe('400px'); expect(await text.evaluate((el) => el.getBoundingClientRect().height)).toBeLessThanOrEqual(400); }); test('fade is a theme-independent mask', async ({ page }) => { await page.goto(READER); const text = fluxByTitle(page, 1).locator('.text'); const mask = await text.evaluate((el) => { const cs = getComputedStyle(el); return cs.maskImage || cs.webkitMaskImage; }); expect(mask).toContain('linear-gradient'); const overlay = await text.evaluate((el) => getComputedStyle(el, '::after').content); expect(overlay === 'none' || overlay === 'normal').toBeTruthy(); });