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), },