pcbjam/tests/tools/screenshots/README.md
Viktor Vaczi 63ed1f3c1f e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate
Squash of experiment/ff-big-modules vs main.

Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.

Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.

One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.

SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.

Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.

CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.

Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:21:54 +02:00

3.7 KiB

Screenshot regression + Discord review tooling

One comparison engine + a churn-free updater + a Discord reporter for the e2e screenshots. Design and rationale: ~/.claude/plans/…snowglobe.md (or ask).

Source of truth = CI's Linux render. The dev never authors baselines on the Mac (Mac fonts/GL ≠ CI). Instead, CI renders on every push; when a render change is intentional you promote CI's artifact into the committed baselines. The environment isn't pinned — if the host's Mesa/fonts drift, the gate lights up in Discord and you just re-promote (broad + low-intensity change ⇒ likely drift).

Everything is per-engine. Specs write test-results/<engine>/<name>.png (engine derived from the running browser via stableShot/shotPath); baselines live in baseline-screenshots/<engine>/; the canonical key everywhere in this tooling is <engine>/<name> and captions/attachments carry the engine label. The same spec on chromium + firefox is two independent gated screenshots.

Files

  • config.ts — thresholds, baseline dirs, per-engine floors (calibrate!), clustering knobs.
  • image-ops.ts — PNG load/save, pixelmatch diff (AA-excluded), connected-component boxes, triptych compositing, size-cap resize.
  • compare.ts — the comparison engine: classify per-engine baselines vs test-results/<engine>/test-results/screenshot-diff/report.json + triptych/heatmap PNGs. --pair diffs two files.
  • promote.ts — churn-free updater: pull a CI run's shots (gh run download) or --from DIR; overwrite a baseline only when pixels differ beyond the floor (verbatim bytes, no re-encode) → no git churn.
  • perf-report.ts — renders the track-only runtime-perf table (loadMs/openMs/FPS) with Δ vs the previous main run (fetched via gh).
  • post-discord.ts — the always-on CI-on-main report: SHA + e2e status + perf table, then screenshot triptychs (batched, size-capped, flood-collapsed).
  • changelog.ts — Discord trigger B: git-history diff of committed baseline PNGs (no build/GPU).
  • noise.ts — calibration: diff two identical-input renders → per-engine noise floor.
  • gen-manifest.ts — regenerate screenshot-manifest.json (canonical {name, engine} list) from the committed per-engine baseline tree; --check (gating in CI) fails if it drifts.
  • spec-map.ts — best-effort screenshot-name → spec-file attribution for captions (scans stableShot/shotPath literals).

npm scripts (run from tests/)

npm run screenshots:check      # gate: baselines vs test-results → report.json (exit 0; add --fail-on-change to gate)
npm run screenshots:promote -- --run <ci-run-id>   # churn-free re-baseline from a CI run (or --from DIR)
npm run screenshots:report -- --e2e pass           # post the CI report to Discord (main+push only; needs DISCORD_WEBHOOK_URL)
npm run screenshots:changelog                       # post the baseline changelog (main+push only)
npm run screenshots:noise -- run1/ run2/            # calibrate floors
npm run screenshots:manifest                        # regenerate the manifest (--check to verify it's fresh)

Activation checklist

  • screenshot-manifest.json generated ({name, engine} authoritative — derived from the per-engine baseline tree).
  • scale:'device''css' normalized (no-op at CI's DSF=1).
  1. Add the DISCORD_WEBHOOK_URL repo secret — until then everything is inert.
  2. Calibrate: run the suite twice in CI, screenshots:noise the two dirs, set FLOORS in config.ts.
  3. First re-baseline: promote a clean CI run's render, commit (expect a big, one-time chrome-font diff vs the Mac baselines).
  4. Delete the old scripts/{compare,update-baseline}-screenshots.sh.
  5. Once floors are proven stable, flip the gate to --fail-on-change.