From 6b6b621a5a511df7be0ed8223d0259627e5d2129 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Fri, 4 Sep 2026 19:11:23 +0200 Subject: [PATCH] test: the browser runs take their engine from BROWSER, and CI runs all three openApp() launches chromium, firefox or webkit from playwright-core; the smoke and DOM runs pass in each. CI runs both per engine, WebKit on macOS for Safari's text shaping. build.target is set explicitly to the browsers the bundle promises. CLAUDE.md is back under its 120-line budget. --- .github/workflows/ci.yml | 22 ++++++++++++++++------ CLAUDE.md | 11 +++++------ docs/headless-testing.md | 15 +++++++++++---- tests/browser.mjs | 12 +++++++++--- vite.config.ts | 3 +++ 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b29e14a..85dc38d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,11 +44,20 @@ jobs: - run: npm ci - run: npm run test:lo - # Production build booted in headless Chromium — catches bundling and boot - # regressions the jsdom suite cannot see. - smoke: - runs-on: ubuntu-latest - timeout-minutes: 20 + # Production build booted in the three engines behind the supported browsers + # (vite build.target). WebKit runs on macOS: its text shaping there is Safari's. + browser: + strategy: + fail-fast: false + matrix: + include: + - { browser: chromium, os: ubuntu-latest } + - { browser: firefox, os: ubuntu-latest } + - { browser: webkit, os: macos-latest } + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + env: + BROWSER: ${{ matrix.browser }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -56,6 +65,7 @@ jobs: node-version: 20 cache: npm - run: npm ci - - run: npx playwright-core install chromium + - run: npx playwright-core install --with-deps ${{ matrix.browser }} - run: npm run build - run: npm run test:smoke + - run: npm run test:dom diff --git a/CLAUDE.md b/CLAUDE.md index 9a64c84..bc73c84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,8 @@ npm run check # svelte-check type-check (svelte + ts) npm test # Vitest suite once (tests/**/*.test.ts) npm run test:watch # Vitest in watch mode npm run test:lo # LibreOffice legs: round trip + ODT/DOCX render consistency (needs `soffice`) -npm run test:smoke # boots the dist/ build in headless Chromium (tests/smoke/run.mjs) -npm run test:dom # pagination + editing in the real browser (tests/dom/run.mjs) +npm run test:smoke # boots the dist/ build headless (tests/smoke/run.mjs); BROWSER=firefox|webkit +npm run test:dom # pagination + editing in the real browser (tests/dom/run.mjs); same BROWSER npm run test:coverage # vitest + v8 coverage over src/ → coverage/index.html npm run test:parity # render parity vs LibreOffice (tests/render-parity/README.md) node scripts/make-thesaurus.mjs # re-vendor public/thesaurus/ from LibreOffice's MyThes data @@ -64,10 +64,9 @@ silently lands in every imported document as direct formatting. The defaults fol `components/Editor.svelte` and `styles/editor.css` (`PAGE_HEIGHT` 1123px, `PAGE_GAP` 20px, the `--user-page-*`/`--user-margin-*` custom properties) — details in `docs/architecture/pagination.md`. -**Headless browser testing** — this container is linux **arm64**. Never `puppeteer` / -`@puppeteer/browsers`: they fetch an x86-64 Chrome that cannot run here. Use `playwright-core` + -its Chromium; full recipe in `docs/headless-testing.md`. Driving the live app is the only way to -verify rendering, layout or NodeView behaviour — there is no DOM test suite for it. +**Headless browser testing** — `playwright-core` with its own engines, never `puppeteer` (it +fetches an x86-64 Chrome that cannot run on arm64). Recipe and the three-engine CI matrix in +`docs/headless-testing.md`; driving the live app is the only way to verify rendering or NodeViews. **Naming** — components `PascalCase.svelte`, every `.ts` module `camelCase`; extension files are named by feature (`image.ts`, `indent.ts`), not `XyzExtension.ts`. diff --git a/docs/headless-testing.md b/docs/headless-testing.md index d37c3b4..3069505 100644 --- a/docs/headless-testing.md +++ b/docs/headless-testing.md @@ -12,10 +12,17 @@ For PDF-export repros specifically: replicate `pdf.ts`'s clone + `html2canvas(.. ## The two committed browser runs `npm run test:smoke` and `npm run test:dom` are these probes made permanent: both boot -`dist/` in headless Chromium 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. +`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. + +`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 diff --git a/tests/browser.mjs b/tests/browser.mjs index a7b6125..cad6b7a 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -4,7 +4,7 @@ import { spawn, execSync } from 'node:child_process'; import { existsSync } from 'node:fs'; import { join, resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { chromium } from 'playwright-core'; +import * as playwright from 'playwright-core'; export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); export const MOD = process.platform === 'darwin' ? 'Meta' : 'Control'; @@ -34,9 +34,15 @@ export async function previewServer(port) { throw new Error('preview server did not start'); } -// Fixed locale, so the UI labels a test clicks are deterministic across machines. +// BROWSER=chromium|firefox|webkit picks the engine (Chromium by default). Fixed locale, +// so the UI labels a test clicks are deterministic across machines. export async function openApp(port) { - const browser = await chromium.launch({ executablePath: chromium.executablePath(), args: ['--no-sandbox'] }); + const name = process.env.BROWSER ?? 'chromium'; + const engine = playwright[name]; + if (!engine) throw new Error(`unknown BROWSER "${name}": chromium, firefox or webkit`); + const args = name === 'chromium' ? ['--no-sandbox'] : []; + const browser = await engine.launch({ executablePath: engine.executablePath(), args }); + console.log(`engine: ${name} ${browser.version()}`); const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, locale: 'en-US' }); const pageErrors = []; page.on('pageerror', (err) => pageErrors.push(String(err))); diff --git a/vite.config.ts b/vite.config.ts index 82d30ea..827c2cc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,6 +21,9 @@ export default defineConfig({ // subpath (GitHub Pages project site). sw.js and the manifest are relative too. base: './', plugins: [svelte()], + // The browsers the bundle promises to parse in; the CI browser matrix runs the + // engines behind them (docs/headless-testing.md). + build: { target: 'baseline-widely-available' }, define: { __APP_VERSION__: JSON.stringify(commit ? `${pkg.version} (${commit})` : pkg.version), },