Files
Vincent BourdonandClaude Opus 5.5 ccb230d514 fix: fondu du repli par masque CSS, indépendant du thème
Le dégradé vers --frss-background-color laissait une bande visible en thème
sombre (fond de l'article différent). Durcit aussi reset.php des tests e2e
(écriture atomique, attente de la revalidation opcache).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:10:49 +01:00

128 lines
5.3 KiB
JavaScript

// @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();
});