diora-web/e2e/reader.spec.js
mrwnslz ffcb6fba4f
Some checks failed
Test / e2e (push) Waiting to run
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Has been cancelled
Refactoring Schritt 1: Browser-Tests für den Reader, in der CI; Plan in docs/refactoring.md
- e2e/reader.spec.js: ein Test-EPUB (e2e/make_epub.py) hochladen, öffnen ohne
  zu senden, lesen und Lesestand nach Neuladen, Fußnote, Markierung und
  Notizleiste, Hell/Dunkel samt Reader, alle Tabs ohne Seitenfehler.
- e2e/env.js: E2E_PORT und E2E_PYTHON; nie einen laufenden Server
  wiederverwenden (dahinter läge eine echte Datenbank).
- CI: eigener Job e2e (Playwright + Chromium); blockiert den Docker-Build nicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:25:06 +02:00

142 lines
6.2 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('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('/');
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();
}
});