tests/layout/baseline.json records where each page starts and what the load cost, per engine and platform; a page starting elsewhere is a layout change the run prints page by page, a load over three times its recorded time a regression, and LAYOUT_UPDATE=1 records it again. The monkey draws frame sizes, wraps, alignment and column counts too, holds the history against the edits rather than the caches the editor recomputes, and leaves it unchecked where the columns flow rewrote the document outside it. The differences both importers make by design are named in the comparison; unhoist moved to normalize.ts.
6.8 KiB
Headless browser testing
With no test suite, the way to verify rendering, layout, or interaction is to drive the live app in a headless browser. This container is linux arm64 (uname -m → aarch64) — the one gotcha. Use it for any check: pagination, list-marker/float layout, table sizing, header/footer, theme colors, image drag/resize, ODF round-trips, etc. PDF export is just one example.
- Don't use
puppeteer/npx @puppeteer/browsers install chrome-headless-shell: they fetch an x86-64 Chrome that can't run here (rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2). No arm64 chrome-headless-shell build exists. - Use Playwright's Chromium (native arm64):
npm install --no-save playwright-core && npx playwright-core install chromium→ binary at~/.cache/ms-playwright/chromium-*/chrome-linux/chrome. Playwright'sinstall-depshost check errors out, but the libs install via apt:apt-get install -y libdbus-1-3 libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 libatspi2.0-0 libpango-1.0-0 libcairo2. Launch with playwright-core,executablePathat that binary,args: ['--no-sandbox'].
Then load npm run dev, inject a document into localStorage['edentext-doc'], reload, and read live DOM geometry (getBoundingClientRect, Range.getClientRects) or screenshot. This is the only way to exercise the editor's live ProseMirror NodeViews (e.g. the image node), which generateHTML can't reproduce. The debug/pagebreak-debug-*.json snapshots carry the live doc JSON, handy to inject.
For PDF-export repros specifically: replicate pdf.ts's clone + html2canvas(...) inside page.evaluate and read the canvas back as a PNG — capturing the real jsPDF doc.save() download tends to hang in headless. Inspect output PDFs with poppler-utils (apt-get install -y poppler-utils: pdftoppm, pdfimages, pdftotext).
The four committed browser runs
npm run test:smoke and npm run test:dom are these probes made permanent: both boot
dist/ headless through tests/browser.mjs (preview server, checklist, pageerror
collector) and fail on any uncaught page error. The DOM run keeps the pagination
invariants of pass 4 below — one page count through a settle, a reload, a zoom, a page
break and its undo — which is the only test src/lib/components/** has.
npm run test:layout and npm run test:monkey run against the dev server (Vite serves
TypeScript, so a page imports src/ and tests/ modules directly). The layout run
opens every corpus and showcase document plus LAYOUT_SEEDS fuzz seeds, holds the editor's
page count against LibreOffice's PDF (soffice + pdfinfo, a page or a tenth of slack)
and checks the rendered lines: none overlap, none sit in the page gap or past the sheet,
none cross a single-section document's margins, no heading ends a page or column, nothing
is wider than the page. A tab's box, a frame, a formula and the decor are not lines.
It then holds the document against tests/layout/baseline.json, which records where each
page starts and what the load cost: a page starting elsewhere is a layout change the run
prints page by page, and a load over three times its recorded time is a regression. The
baseline is keyed by engine and platform (line breaking is theirs) and LAYOUT_UPDATE=1
records it again — the answer to a deliberate change, never to a surprise.
The monkey run replays MONKEY_OPS random keys and commands per seed
(MONKEY_SEED, MONKEY_RUNS, MONKEY_DOC) on a corpus document — typing, formatting,
lists, tables, notes, frames and columns — and checks after each:
no uncaught error, a document its own schema accepts; at the end undo back to the opened
file and redo forward, then Save As both formats, xmllint each against the schemas and read
it back through the app's importer. A failing seed prints its last ops and keeps the file
and both documents under the OS temp dir. Found so far: a note inserted in a text box or a
text box in a note breaks the ODT's XML, and a table in a cell is dropped on save — both
commands now refuse there; a page break inside a columns section, which the editor never
shows and only DOCX writes, is now cleared as the blocks are wrapped; an .odt dropped
every block of a list item after the first, and a header row that the table did not ask to
repeat came back as ordinary cells.
BROWSER=chromium|firefox|webkit picks the engine (Chromium by default; the others via
npx playwright-core install firefox webkit). CI runs both on all three, one per matrix
leg, WebKit on macOS because only there its text shaping is Safari's. The engines are
the ones behind build.target in vite.config.ts; a browser older than that target
fails to parse the bundle and no run reaches it. Playwright's WebKit is the engine, not
Safari: storage eviction, the install prompt and iOS input are outside the matrix.
Hunting for bugs the suite cannot see
npm run test:coverage says where to look: the logic modules are dense with tests,
src/lib/components/** and App.svelte are at 0% — every bug found this way so far
lived there. Give each probe a pageerror + console.error collector; that alone reports
faults nobody predicted (two of six were found by clicking controls and reading the console).
Four passes, cheapest first:
- Edges — junk in every
edentext-*key, truncated/renamed/empty archives, a full localStorage, no IndexedDB, page-tall images, 400-section documents. - Breadth — click every control of every tab, watch only for uncaught errors.
- Semantics — not "does it crash" but "is the answer right": reject a tracked change and compare against the original, TOC page numbers against the measured page of each heading, a numeric sort against 3/10/25. The worst bug found (a reject that ate the original text) crashes nothing.
- Invariants — snapshot → act → undo → compare; page count stable over reloads; save → reopen;
.docxand.odtof one corpus fixture giving identical counts.
Then repro minimally and take a stack trace off the dev server (sourcemaps) with
Error.stackTraceLimit = 300 — the default ten frames stop above the cause.
Budget for false alarms. Three of four striking signals were the probe's own fault:
Range.getClientRects also returns container rects (a layout checker built on it is noise),
a stale button index reads as a dead control, and clicking .tiptap > p by index hits a
different block once an edit has reflowed the document. Two more from the DOM run: TipTap's
focus() lands on the next animation frame, so a key sent right after it is lost unless
document.activeElement is awaited; and a page break before the empty last paragraph yields
no page. Reproduce deterministically before touching any code.