Files
edentext/tests/render-parity
Steffen Becker 889f11cffc test: a committed corpus CI can read
Ten documents the docx lib authors directly, plus the ODT twin
LibreOffice converts each into, now live in tests/corpus/ instead of the
gitignored fixtures dir. corpus.test.ts asserts both formats of a
document read the same and that each survives our export and import
either way. The heading styles gained the outline level Word writes —
without it LibreOffice converts a heading to a paragraph that merely
carries the style, and the two formats disagreed.
2026-08-15 11:54:36 +02:00
..
2026-08-15 11:54:36 +02:00
2026-08-15 11:54:36 +02:00

Render parity

Opens the same document in LibreOffice and in the editor, then compares the resulting text layout. Everything else in tests/ compares documents at the model level (JSON → export → import); this leg is the only one that checks what a page actually looks like.

npm run test:parity:fixtures   # (re)generate the baseline .docx corpus
npm run test:parity            # whole corpus
npm run test:parity -- path/to/file.docx --json report.json

Exit code 1 when any document differs. --json also dumps both sides' lines (y, x, x2 in mm, per page) — that dump is what you diagnose from.

How it compares

reference editor
render soffice --convert-to pdf Playwright Chromium, real app, real file input
read pdftotext -bbox-layout (word boxes, pt) Range.getClientRects() (word boxes, px)

Both sides are normalized to mm from the top-left of each page, grouped into lines, and compared as: page count → line count → line text → line position. Comparison is whitespace-insensitive, because the spell-check decorations split text nodes mid-word and the two engines needn't agree on word boundaries — only on what sits on a line.

Reported differences are pageCount, lineCount, lineBreak (same line, different words) and position (same words, off by more than POS_TOL_MM). Comparison of a page stops at the first line-level divergence, since everything after it is noise.

Prerequisites

apt-get install -y --no-install-recommends libreoffice-writer poppler-utils
npm install --no-save playwright-core && npx playwright-core install chromium
cp src/assets/fonts/*.ttf /usr/local/share/fonts/ && fc-cache -f

The font step is not optional. LibreOffice renders with system fonts; without the repo's own TTFs installed it substitutes DejaVu for Liberation Serif and every line breaks differently for a reason that is not a bug. With them, both engines resolve Times New Roman → Liberation Serif, Arial → Liberation Sans, Calibri → Carlito, Cambria → Caladea — the same files the editor bundles.

But install only what LibreOffice does not already ship. A second copy of a family it bundles makes its render non-deterministic: it picks between the two files per run — measured as Carlito against Carlito-Regular in the PDF's font list — and their vertical metrics differ, so a page carrying a large-font heading lands 1.85mm lower on some runs and a fixture's issue count swings by 40. The browser side never needs the install: the app @font-faces its own copies. On a macOS install /Applications/LibreOffice.app/Contents/Resources/fonts/truetype/ already holds Carlito, Caladea and Liberation, so skip the cp there entirely; check any other platform with fc-list | grep -ci carlito before copying.

The runner starts npm run dev on port 5199 itself if nothing answers there.

Fixtures

make-fixtures.mjs authors the baseline corpus with the docx lib directly, not through src/lib/export/docx.ts — otherwise the corpus would test our exporter against itself — and converts each document to its ODT twin with LibreOffice. It writes them to tests/corpus/, which is committed: tests/corpus.test.ts reads them at the model level (import → our own export → import) on every npm test, so CI runs against documents this editor did not write. A run with no argument compares that corpus and fixtures/, which stays gitignored — keep whatever real-world files you test against there and local.

Names stay local

A real-world document's name goes in no file, and a commit message names no document at all — write the rule that changed and what it measured. FINDINGS.md is gitignored for that reason.

Only this harness reads fixtures/. The directory is gitignored, so a tests/unit/ test reading it fails with ENOENT on a fresh clone; it zips its own document instead (docx-onoff.test.ts — both importers take a Uint8Array, not a path), or reads tests/corpus/, which is committed. CI greps for the read.

Prerequisites, part two: Calibri Light

Word's default theme heading font. LibreOffice's substitution table knows Calibri but not Calibri Light, so without an alias it falls to fontconfig's generic — DejaVu Serif, a serif heading nothing else renders. Word substitutes plain Calibri, and so does the editor (global.css), so give LibreOffice the same answer:

<!-- /etc/fonts/conf.d/30-calibri-light.conf, then fc-cache -f -->
<match target="pattern">
  <test name="family"><string>Calibri Light</string></test>
  <edit name="family" mode="prepend" binding="strong"><string>Carlito</string></edit>
</match>