diora-web/e2e/reader.spec.js
mrwnslz a7d4d92f9e
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 8m19s
Test / e2e (push) Successful in 1m52s
Reader: offene Notizleiste schiebt den Text nicht mehr aus der Mitte; sw v107
Die Leiste nimmt ihre Breite links weg; dieselbe Breite kommt rechts als
Abstand dazu, soweit Platz ist (die Spalte wird dafür nie schmaler). Beides
gleitet gleichzeitig, der Text bleibt stehen. Neu berechnet beim Öffnen und
Schließen der Leiste, bei Fenstergröße und Zeilenlänge/Schrift.

e2e: Test dafür (breit: Spalte bleibt, schmal: weicht aus, bleibt gleich
breit, liegt nicht unter der Leiste); Service Worker in den Tests aus (sein
Neuladen störte mitten im Test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:43:03 +02:00

169 lines
7.6 KiB
JavaScript

// The reader end to end: a book goes in (encrypted in the browser), opens, is
// read, and its place, a highlight and the theme survive a reload. The safety
// net for refactoring app.js (docs/refactoring.md). One book for the whole file:
// the tests build on each other and run in order.
const path = require('path');
const { test, expect } = require('@playwright/test');
test.use({ storageState: path.join(__dirname, '.auth', 'user.json') });
test.describe.configure({ mode: 'serial' });
const BOOK = path.join(__dirname, '.auth', 'book.epub');
let bookId = null;
// Errors in the page fail the test that caused them.
let pageErrors = [];
test.beforeEach(({ page }) => {
pageErrors = [];
page.on('pageerror', e => pageErrors.push(String(e)));
});
test.afterEach(() => {
expect(pageErrors, 'errors in the page').toEqual([]);
});
async function openTestBook(page) {
await page.goto('/#books');
// A new service worker taking over reloads the page once: let that settle first.
await page.waitForLoadState('networkidle');
await page.waitForFunction(() => typeof openBook === 'function' && typeof _lastBookListData !== 'undefined' && Array.isArray(_lastBookListData));
await page.evaluate(id => openBook(id), bookId);
await page.waitForFunction(() => document.querySelectorAll('#reader-content [data-epub-src]').length === 12
&& typeof _readPos !== 'undefined' && _readPos && _readPos.ready);
}
async function serverBook(page) {
const books = await (await page.request.get('/books/')).json();
return books.find(b => b.id === bookId);
}
test('a book goes in and shows in the list', async ({ page }) => {
await page.goto('/#books');
await page.setInputFiles('#book-file-input', BOOK);
const found = await page.waitForFunction(() => {
const list = typeof _lastBookListData !== 'undefined' ? (_lastBookListData || []) : [];
const b = list.find(x => x.title === 'E2E Testbuch');
return b ? b.id : null;
}, null, { timeout: 20000 });
bookId = await found.jsonValue();
expect(bookId).toBeTruthy();
});
test('opening without reading sends no position', async ({ page }) => {
const posts = [];
page.on('request', r => { if (r.method() === 'POST' && /\/progress\/$/.test(r.url())) posts.push(r.url()); });
await openTestBook(page);
await page.waitForTimeout(3000);
await page.evaluate(() => saveReaderProgress());
await page.evaluate(() => closeReader());
await page.waitForTimeout(500);
expect(posts).toEqual([]);
});
test('reading moves the position, and it comes back after a reload', async ({ page }) => {
await openTestBook(page);
const content = page.locator('#reader-content');
await content.hover();
for (let i = 0; i < 12; i++) await page.mouse.wheel(0, 600);
const saved = page.waitForRequest(r => r.method() === 'POST' && /\/progress\/$/.test(r.url()));
const body = JSON.parse((await saved).postData());
expect(body.device).toBe('Browser');
expect(body.read_at).toBeGreaterThan(1e12);
const anchor = await page.evaluate(() => getPositionAnchor(document.getElementById('reader-content')));
await page.waitForTimeout(500);
const b = await serverBook(page);
expect(b.position_anchor).not.toBe('');
expect(b.device).toBe('Browser');
await page.reload();
await openTestBook(page);
const restored = await page.evaluate(() => getPositionAnchor(document.getElementById('reader-content')));
const block = a => Number(a.split(':')[0]);
expect(Math.abs(block(restored) - block(anchor))).toBeLessThanOrEqual(1);
});
test('a footnote shows its text', async ({ page }) => {
await openTestBook(page);
const ref = page.locator('#reader-content [data-footnote-ref]').first();
await ref.scrollIntoViewIfNeeded();
await ref.click();
await expect(page.locator('.footnote-popover')).toContainText('Der Text der Fußnote');
});
test('a highlight is kept, and shows in the margin', async ({ page }) => {
await openTestBook(page);
// Marker mode on (the ✎ button), select the start of a paragraph on screen
// the way a mouse would, then let go.
await page.evaluate(() => {
if (!markerModeActive) toggleMarkerMode();
const p = document.querySelector('#reader-content #c1p2') || document.querySelector('#reader-content p');
p.scrollIntoView({ block: 'center' });
const text = p.firstChild;
const range = document.createRange();
range.setStart(text, 0);
range.setEnd(text, 20);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
const r = range.getBoundingClientRect();
p.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: r.right, clientY: r.bottom }));
});
const saved = page.waitForRequest(r => r.method() === 'POST' && /\/highlights\/$/.test(r.url()), { timeout: 15000 });
await page.locator('.hl-color-btn[data-hl-color="yellow"]').click();
await page.evaluate(() => saveHighlights && saveHighlights());
await saved;
await page.reload();
await openTestBook(page);
await page.waitForFunction(() => document.querySelectorAll('#reader-content mark.epub-highlight, #reader-content .epub-highlight').length > 0);
// The margin remembers being open; open it if it isn't.
await page.evaluate(() => { if (!readerAnnotationsMarginOpen) toggleAnnotationsMargin(); });
await expect(page.locator('#reader-margin-markers .margin-note')).toHaveCount(1);
});
test('an open margin keeps the text column in the middle when there is room', async ({ page }) => {
const column = () => page.evaluate(() => {
const c = document.getElementById('reader-content');
const p = c.querySelector('p');
const r = p.getBoundingClientRect();
const m = document.getElementById('reader-margin').getBoundingClientRect();
return {left: r.left, right: r.right, width: r.width, marginRight: m.right, window: window.innerWidth};
});
await page.setViewportSize({ width: 1600, height: 900 });
await openTestBook(page);
const closed = await column();
await page.evaluate(() => { if (!readerAnnotationsMarginOpen) toggleAnnotationsMargin(); });
await page.waitForTimeout(400); // the margin and the padding slide together
const open = await column();
expect(Math.abs(open.left - closed.left)).toBeLessThanOrEqual(2);
expect(open.width).toBeCloseTo(closed.width, 0);
// Not enough room: the column moves aside, but keeps its width and clears the margin.
await page.setViewportSize({ width: 1000, height: 900 });
await page.waitForTimeout(400);
const narrow = await column();
expect(narrow.left).toBeGreaterThanOrEqual(narrow.marginRight);
await page.evaluate(() => { toggleAnnotationsMargin(); });
});
test('the theme switch turns the page and the reader light, and stays', async ({ page }) => {
await page.goto('/');
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
await page.locator('#theme-toggle').click();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await page.reload();
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
await openTestBook(page);
await expect(page.locator('#reader-overlay')).toHaveClass(/reader-theme-bright/);
await page.evaluate(() => { closeReader(); __dioraSetTheme('dark'); });
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
});
test('every tab opens without errors', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.waitForFunction(() => typeof showTab === 'function' && typeof loadBookList === 'function');
for (const tab of ['home', 'radio', 'podcasts', 'books', 'feeds', 'focus', 'ambient']) {
await page.evaluate(t => showTab(t), tab);
await expect(page.locator(`#tab-${tab}`)).toBeVisible();
}
});