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
1
.github/workflows/screenshot-changelog.yml
vendored
|
|
@ -12,7 +12,6 @@ on:
|
|||
branches: [main]
|
||||
paths:
|
||||
- 'tests/baseline-screenshots/**.png'
|
||||
- 'tests/e2e/baseline-screenshots/**.png'
|
||||
|
||||
concurrency:
|
||||
group: screenshot-changelog-${{ github.ref }}
|
||||
|
|
|
|||
65
.github/workflows/wasm-build.yml
vendored
|
|
@ -372,6 +372,21 @@ jobs:
|
|||
working-directory: tests
|
||||
run: npm ci
|
||||
|
||||
# Cheap hygiene gates (no build needed): the determinism lint keeps the
|
||||
# banned flake patterns out of the specs, the manifest check keeps
|
||||
# screenshot-manifest.json in lockstep with the committed baselines
|
||||
# (a stale manifest silently disables removed-screenshot detection), and
|
||||
# the CI-coverage lint proves every spec file on disk is reachable from
|
||||
# the npm scripts THIS workflow invokes (a spec/project that CI never
|
||||
# runs is how the web suite once rotted unnoticed).
|
||||
- name: Lint test determinism + screenshot manifest + CI coverage
|
||||
if: inputs.run_tests
|
||||
working-directory: tests
|
||||
run: |
|
||||
npm run lint:determinism
|
||||
npm run screenshots:manifest -- --check
|
||||
npm run lint:ci-coverage
|
||||
|
||||
- name: Install web workspace deps (collab bundle)
|
||||
if: inputs.run_tests
|
||||
working-directory: web
|
||||
|
|
@ -413,37 +428,36 @@ jobs:
|
|||
working-directory: tests
|
||||
run: npm run setup:kicad
|
||||
|
||||
# The e2e suites run as separate steps so one suite's failure never skips
|
||||
# the others — every suite still renders its screenshots, so the screenshot
|
||||
# report below can show exactly what broke/went missing. A failing step
|
||||
# still fails the JOB (no continue-on-error); the later steps run anyway
|
||||
# via `!cancelled()` + "previous stage wasn't skipped" guards
|
||||
# (steps.wx_e2e.outcome != 'skipped' ⇔ the build reached the tests — on a
|
||||
# build failure everything below stays skipped, as before).
|
||||
- name: wxWidgets e2e (npm run test:wx)
|
||||
id: wx_e2e
|
||||
# ONE merged Playwright invocation for every apps-server suite (wx,
|
||||
# kicad×2 engines, asyncify, coroutine — see playwright.config.ts). One
|
||||
# invocation = one start-of-run outputDir wipe BEFORE anything rendered,
|
||||
# so the engine-scoped screenshots in test-results/{chromium,firefox}/
|
||||
# accumulate naturally for the offline compare. A failure fails the JOB
|
||||
# (no continue-on-error); the report below still runs and shows exactly
|
||||
# what broke or went missing. xvfb: the Firefox projects run headed on CI
|
||||
# (GPU-less VMs have no headless GL).
|
||||
- name: e2e (npm run test:e2e — all suites, both engines)
|
||||
id: e2e
|
||||
if: inputs.run_tests
|
||||
working-directory: tests
|
||||
run: npm run test:wx
|
||||
run: xvfb-run -a npm run test:e2e
|
||||
|
||||
- name: Asyncify e2e (npm run test:asyncify:firefox)
|
||||
id: asyncify_e2e
|
||||
if: inputs.run_tests && !cancelled() && steps.wx_e2e.outcome != 'skipped'
|
||||
# The React web-app suite (web/) against the pnpm dev stack (frontend
|
||||
# :3048 + backend :3060, cold-started by Playwright's webServer on CI).
|
||||
# Runs even if the main e2e failed (`!cancelled()` + not-skipped guard) so
|
||||
# its screenshots still reach the report.
|
||||
- name: web e2e (npm run test:web:ci)
|
||||
id: web_e2e
|
||||
if: inputs.run_tests && !cancelled() && steps.e2e.outcome != 'skipped'
|
||||
working-directory: tests
|
||||
run: npm run test:asyncify:firefox
|
||||
|
||||
- name: KiCad e2e (npm run test:kicad:ci)
|
||||
id: kicad_e2e
|
||||
if: inputs.run_tests && !cancelled() && steps.wx_e2e.outcome != 'skipped'
|
||||
working-directory: tests
|
||||
run: xvfb-run -a npm run test:kicad:ci
|
||||
run: xvfb-run -a npm run test:web:ci
|
||||
|
||||
# Runtime-perf E2E (eeschema + pcbnew): measures the current build's
|
||||
# load / open+render / FPS and writes tests/test-results/perf-*.json.
|
||||
# Track-only — never gates the build (continue-on-error). CI is
|
||||
# headless/SwiftShader so FPS is CPU-bound + noisy; openMs is the stable number.
|
||||
- name: KiCad runtime perf (track-only, non-gating)
|
||||
if: inputs.run_tests && !cancelled() && steps.kicad_e2e.outcome != 'skipped'
|
||||
if: inputs.run_tests && !cancelled() && steps.e2e.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
working-directory: tests
|
||||
run: xvfb-run -a npm run test:perf
|
||||
|
|
@ -458,7 +472,7 @@ jobs:
|
|||
# baseline-webgl/ and flip this step gating. 3d:review writes the full
|
||||
# 47-pair triptych gallery for the artifact upload below.
|
||||
- name: 3D renderer parity (report-only)
|
||||
if: inputs.run_tests && !cancelled() && steps.wx_e2e.outcome != 'skipped'
|
||||
if: inputs.run_tests && !cancelled() && steps.e2e.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
working-directory: tests
|
||||
run: |
|
||||
|
|
@ -479,7 +493,7 @@ jobs:
|
|||
# the already-produced test-results (screenshots + perf-*.json).
|
||||
- name: Screenshot report + perf
|
||||
id: report
|
||||
if: inputs.run_tests && !cancelled() && steps.kicad_e2e.outcome != 'skipped'
|
||||
if: inputs.run_tests && !cancelled() && steps.e2e.outcome != 'skipped'
|
||||
continue-on-error: true
|
||||
working-directory: tests
|
||||
env:
|
||||
|
|
@ -490,9 +504,8 @@ jobs:
|
|||
# without --fail-on-change; continue-on-error also shields transient Discord hiccups.
|
||||
run: |
|
||||
E2E=pass
|
||||
{ [ "${{ steps.wx_e2e.outcome }}" = "success" ] \
|
||||
&& [ "${{ steps.asyncify_e2e.outcome }}" = "success" ] \
|
||||
&& [ "${{ steps.kicad_e2e.outcome }}" = "success" ]; } || E2E=fail
|
||||
{ [ "${{ steps.e2e.outcome }}" = "success" ] \
|
||||
&& [ "${{ steps.web_e2e.outcome }}" = "success" ]; } || E2E=fail
|
||||
npm run screenshots:check
|
||||
npm run screenshots:report -- --e2e "$E2E"
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ The e2e tests are in /tests, with a README and WHATWORKS md files
|
|||
Test determinism rules (no blind sleeps/ifs, `stableShot` screenshots, retries:0) are in tests/TESTING.md, enforced by `npm run lint:determinism`.
|
||||
The e2e tests are separated per feature
|
||||
Wxwidgets wasm port has hooks for finding positions of UI elements, tests use that
|
||||
The test screenshots are tracked with git; CI's Linux render is the source of truth (tooling: tests/tools/screenshots/, see its README).
|
||||
The test screenshots are tracked with git, per engine (tests/baseline-screenshots/{chromium,firefox}/); CI's Linux render is the source of truth (tooling: tests/tools/screenshots/, see its README).
|
||||
To update baselines, promote a CI run's render (churn-free — only meaningfully-changed images restage): `cd tests && npm run screenshots:promote -- --run <ci-run-id>`, then commit. `npm run screenshots:check` is the local gate; on each main push CI posts a screenshot-diff + runtime-perf report to Discord.
|
||||
The tests have log files in tests/logs/{wxwidgets/kicad}/{test-name} after each run where the js console and cpp logs are visible
|
||||
Always check screenshots for validating tests
|
||||
Run e2e tests from /tests folder: `npm run test:kicad` or `npm run test:e2e` (not playwright directly)
|
||||
Run e2e tests from /tests folder: `npm run test:e2e` (full CI project set, one merged playwright.config.ts) or `npm run test:kicad` (firefox shortcut) — not playwright directly. One spec/engine: `npx playwright test --project=kicad-firefox kicad/pcbnew.spec.ts`. Web-app suite: `npm run test:web`.
|
||||
|
||||
Build kicad with docker/build.sh (includes wxwidgets build, runs in docker)
|
||||
Build wxwidgets standalone with scripts/build-wx-wasm.sh (runs on machine, for wxwidgets-only changes)
|
||||
|
|
|
|||
57
docs/features/web-e2e-rot/01-editor-lib-bridge-flows.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Web-suite rot: footprint/symbol editor library-bridge flows (5 specs)
|
||||
|
||||
Surfaced 2026-07-15 by wiring the web suite into CI for the first time
|
||||
(branch `experiment/ff-big-modules`, runs 29414003275 / 29415027412). These
|
||||
specs were green when written and have silently rotted since — the web suite
|
||||
never ran in CI, so nothing caught it.
|
||||
|
||||
## Affected specs (marked `test.fixme` — flip back when fixed)
|
||||
|
||||
| spec | first-broken symptom (local, web-chromium) | CI symptom |
|
||||
|---|---|---|
|
||||
| `web/footprint-browse-remote.spec.ts` | boots, lib tree lists libs, but expanding `Resistor_SMD` never produces child rows (FootprintEnumerate yields nothing) | `#canvas` never visible in 180 s |
|
||||
| `web/footprint-write-remote.spec.ts` | boots; New Footprint + Ctrl+S never lands an item in the backend (`/api/scopes/default/libs/<lib>/items` stays empty 30 s) | same boot timeout |
|
||||
| `web/footprint-write-spike.spec.ts` | boots (route fixed); `window.__pcbjamSaved` never captures a body after New Footprint + save (`?fpwrite=1` spike provider silent) | same boot timeout |
|
||||
| `web/symbol-write-remote.spec.ts` | same family as footprint-write-remote, symbol domain | same boot timeout |
|
||||
| `web/symbol-write-spike.spec.ts` | same family as footprint-write-spike (`?libwrite=1`) | same boot timeout |
|
||||
|
||||
## Evidence pointing at one root cause
|
||||
|
||||
All five live in the same domain: `window.kicadLibs.request(...)` traffic from
|
||||
the footprint/symbol EDITOR tools (enumerate / save). Probing a booted
|
||||
`/default/projects/demo/-/footprint_editor` locally:
|
||||
|
||||
- the lib tree lists the origin libs (Capacitor_SMD, Diode_SMD, LED_SMD,
|
||||
My Symbols, Resistor_SMD) — the *list* path works (pre-sync/IDB);
|
||||
- `__libsCalls` (a wrapper capturing every `kicadLibs.request`) records ZERO
|
||||
calls during boot and ZERO on expanding a lib — the per-lib
|
||||
enumerate/get/save traffic never happens;
|
||||
- one `[pageerror] __name is not defined` fires at boot — esbuild's
|
||||
keep-names helper missing in whatever context executes a bundled callback;
|
||||
prime suspect for the provider dying silently on first use.
|
||||
|
||||
By contrast the SCHEMATIC editor's bridge works (eeschema-fp-selector records
|
||||
`index` calls), and eeschema/pcbnew tool pages boot and pass their specs — the
|
||||
rot is specific to the fp/sym editor lib flows, not the bridge as a whole.
|
||||
|
||||
Separately fixed while triaging (NOT part of this bug): dead `/p/<slug>/<tool>/`
|
||||
routes in 3 of these specs (router grammar is `/:scope/projects/:name/-/:tool`
|
||||
since the scope/kind/name change), the tool-switch overlay race + `/p/` URL
|
||||
asserts, and read-only-editor's cross-tab SelectFirst-order assumption.
|
||||
|
||||
## Why fixme and not expected-fail
|
||||
|
||||
The ysync convention (expected-fail repros) is right for fast unit-level
|
||||
repros. These five die in 180 s boot timeouts on CI — as `test.fail()` they
|
||||
would burn ~30 min of CI per run across two engines for zero extra signal.
|
||||
`test.fixme` keeps them visible in every report as skipped-with-reason;
|
||||
remove the marker (and delete this table row) when the bridge flow is fixed.
|
||||
|
||||
## CI-only sibling (kept RUNNING, conditional expected-fail)
|
||||
|
||||
`web/eeschema-fp-selector.spec.ts` completes its flow but records a
|
||||
`[pageerror] index out of bounds` wasm trap on CI (BOTH engines,
|
||||
llvmpipe/SwiftShader) that does not reproduce on a Mac with real GL — see
|
||||
`test.fail(!!process.env.CI, …)` in the spec. Likely the same software-GL
|
||||
render-path family as the presence ghost-wipe (SwiftShader pass-boundary
|
||||
flush, webgl_gal.cpp). Needs its own investigation on a CI-like rig.
|
||||
119
docs/features/wx-parity-bugs/occ-export-context-eviction.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# occ-export flake: SwiftShader congestion → GAL recovery cascade (fixed by GL-stack swap)
|
||||
|
||||
Status 2026-07-16: fully root-caused with page-level canvas/context telemetry +
|
||||
temp C++ instrumentation in a local docker rig. **Final remedy: CI Chromium
|
||||
now runs WebGL on ANGLE → Mesa llvmpipe instead of SwiftShader**
|
||||
(`tests/playwright.config.ts`, `CHROMIUM_CI_ARGS`) — the same software-GL
|
||||
stack the Firefox projects use, where none of this ever happened. Four small
|
||||
product fixes found along the way were kept (below); everything else was
|
||||
deliberately **not** hardened — the trigger cannot occur outside congested
|
||||
SwiftShader.
|
||||
|
||||
## Symptom
|
||||
|
||||
`chromium/occ-export-{dialog,done}.png` bistable on CI only: the board behind
|
||||
the modal STEP-export dialog flips between runs (~1.2% changedRatio both
|
||||
directions). Firefox (llvmpipe) never affected.
|
||||
|
||||
## The causal chain (every step observed in the rig)
|
||||
|
||||
1. **Transient SwiftShader failure at the first post-board-load draw**
|
||||
(~10-15% of instances at `REPEAT=40-80 WORKERS=4`): `DoRePaint threw:
|
||||
Requested render buffer size is not supported`, plus LRU context evictions
|
||||
the moment a 3rd context appears (21/80 runs; the usual victim is a leaked
|
||||
hidden 881×159 startup GAL canvas, so the `CONTEXT_LOST_WEBGL` line alone
|
||||
was harmless). No flag stops it — `--max-active-webgl-contexts=64`,
|
||||
`--force-gpu-mem-available-mb=4096`, `--disable-low-end-device-mode`,
|
||||
`--disable-gpu-driver-bug-workarounds` all verified present and useless.
|
||||
|
||||
2. **`recoverFromGalError` attempt 1** (`common/draw_panel_gal.cpp:205`)
|
||||
destroys + recreates the WebGL GAL (board canvas `glcanvas-2` →
|
||||
`glcanvas-3`).
|
||||
|
||||
3. **Attempt 1 then always failed** on stale cross-context GL state:
|
||||
- static `g_fontTexture` bound on the new context → `INVALID_OPERATION:
|
||||
bindTexture: object does not belong to this context` → `checkGlError`
|
||||
throw (**fixed — kept**: per-instance `m_fontTexture`,
|
||||
`common/gal/webgl/webgl_gal.{cpp,h}`);
|
||||
- sticky GL error flags left by the old GAL's teardown (its `LockCtx`
|
||||
targets a lost context, so the deletes land on the current one) →
|
||||
the fresh GAL's first `checkGlError` ("generating vertices buffer:
|
||||
invalid enum") misattributed the leftover and threw (**fixed — kept**:
|
||||
drain `glGetError()` before first-frame `init()` in
|
||||
`WEBGL_GAL::BeginDrawing`). With both fixes, reinit-on-WebGL succeeds
|
||||
(validated: 0 Cairo fallbacks / 60 after, vs 100% of recoveries before).
|
||||
|
||||
4. **Attempt 2 = silent permanent Cairo fallback** (`GAL_FALLBACK ==
|
||||
GAL_TYPE_CAIRO` on emscripten, dialog suppressed). Cairo renders the board
|
||||
through the wx software path — shots LOOK painted but are a different
|
||||
engine → subtle whole-board diffs vs the WebGL baseline. **This engine
|
||||
flip IS the original "~1.2% changedRatio both directions" flake.**
|
||||
`FULLSCREEN_QUAD` (singleton VAOs) is the same static-GL-state disease and
|
||||
produced `bindVertexArray: object does not belong` + one wasm abort in
|
||||
rare runs — NOT fixed (unreachable once recovery never triggers; noted
|
||||
here in case in-app recovery robustness is ever wanted).
|
||||
|
||||
5. **wx paint-flag races turned it into a stuck-blank board** (both
|
||||
**fixed — kept**, `wxwidgets/src/wasm/window.cpp`):
|
||||
- `Invalidate` early-out skipped the upward walk while the sweep can
|
||||
strand a window "flags set, ancestors clear" → every later `Refresh()`
|
||||
swallowed. Fix: always propagate to the top.
|
||||
- `PaintSelf`/`PaintChildren` cleared the dirty flags AFTER dispatching
|
||||
paint, so an `Invalidate` issued reentrantly from inside a paint handler
|
||||
(canvas recreated mid-frame, board-load refresh mid-sweep) was clobbered
|
||||
with nothing re-issuing it. Fix: `DoPaint` snapshots + clears both flags
|
||||
BEFORE dispatching. (Pre-fix rigs showed stuck half-painted toolbars —
|
||||
this is a real production-reachable bug, not a CI artifact.)
|
||||
|
||||
One rare mode ("grid-mode": board data loaded, UI painted, canvas alive,
|
||||
items never drawn, zoom-to-fit doesn't help; VIEW had all 464 items and
|
||||
the same Clear/DisplayBoard ordering as healthy runs) was still being
|
||||
diagnosed when the GL-stack swap made it — and everything above —
|
||||
unobservable: **80/80 runs clean on llvmpipe**, all dialog shots pixel-
|
||||
identical (single 55.65% ratio value across all 80; SwiftShader scattered
|
||||
over 55.5–56.8%).
|
||||
|
||||
## The fix that ships
|
||||
|
||||
`CHROMIUM_CI_ARGS` = `--use-gl=angle --use-angle=gl --ignore-gpu-blocklist`
|
||||
(CI-gated). Requirements, all already present on CI: an X display
|
||||
(`xvfb-run -a` wraps the whole e2e step) and Mesa (installed by
|
||||
`playwright install --with-deps`). `--ignore-gpu-blocklist` is mandatory —
|
||||
llvmpipe is on Chromium's software-GL blocklist and WebGL silently reports
|
||||
unavailable without it (the exact analog of the Firefox projects'
|
||||
`webgl.force-enabled`). Headless works; headed is NOT needed.
|
||||
Renderer-string ground truth (Chrome falls back silently — always verify):
|
||||
`gl.getExtension('WEBGL_debug_renderer_info')` must report
|
||||
`ANGLE (Mesa, llvmpipe …)`, not SwiftShader.
|
||||
|
||||
Consequence: chromium renders shift slightly → the chromium baseline set
|
||||
must be re-promoted once from the first green CI run
|
||||
(`cd tests && npm run screenshots:promote -- --run <id>`).
|
||||
|
||||
## Test-side hardening (kept, independent)
|
||||
|
||||
Popup menu items register in the element registry progressively while the
|
||||
menu paints; a coarse ">3 menuitems" gate + single-shot `clickMenuItem` races
|
||||
(4–8/20 under contention, pre-existing). Rule (tests/TESTING.md): wait for the
|
||||
specific item via `waitForRenderedByLabel(page, label, { elementType:
|
||||
'menuitem' })` before every `clickMenuItem`. Applied to `occ-export`,
|
||||
`occ-export-models`, `3d-viewer-models`, `load-pcb`; `pl_editor` already did
|
||||
this; `clickMenuItemByText` waits internally.
|
||||
|
||||
## Open leads (not blocking)
|
||||
|
||||
- The leaked hidden 881×159 startup GAL canvas: destroying it would free a
|
||||
context + memory.
|
||||
- If in-app GL-error recovery ever matters on real GPUs: fix the
|
||||
`FULLSCREEN_QUAD` singleton VAOs (per-instance like the font texture) and
|
||||
audit remaining cross-context statics.
|
||||
|
||||
## Repro rig (for revalidation)
|
||||
|
||||
Docker `mcr.microsoft.com/playwright:v1.57.0-jammy`, worktree bind-mount,
|
||||
container-local node_modules volume, `CI=1`, `xvfb-run -a npx playwright test
|
||||
--project=kicad-chromium --repeat-each=N --workers=4` with a temp spec that
|
||||
loads `pic_programmer`, screenshots board-load / settled / export-dialog /
|
||||
post-poke, and logs the WebGL renderer string + canvas/context telemetry per
|
||||
instance. Canaries: `CONTEXT_LOST_WEBGL`, `does not belong to this context`,
|
||||
board-strip pixel ratio of the shots.
|
||||
2
kicad
|
|
@ -1 +1 @@
|
|||
Subproject commit 261621e87174886d6fd4633e8bfb7f138908d56b
|
||||
Subproject commit 3dcfea5e458418c1d6cc03b1baecd07424211ff7
|
||||
|
|
@ -19,17 +19,21 @@ This builds `apps/minimal_test.{html,js,wasm}` and standalone test apps.
|
|||
|
||||
```bash
|
||||
npm install
|
||||
npm test # wx e2e specs + the asyncify race harness
|
||||
npm test # setup:kicad + the full merged run (same projects as CI)
|
||||
```
|
||||
|
||||
KiCad application tests are a separate, heavier suite (they need the
|
||||
docker-built KiCad WASM): `npm run test:kicad`. Run only the wx e2e specs with
|
||||
`npm run test:wx`.
|
||||
One merged config (`playwright.config.ts`) drives every wasm suite as
|
||||
Playwright *projects*; `npm run test:e2e` runs the CI set: `wx-chromium`,
|
||||
`kicad-firefox`, `kicad-chromium`, `asyncify-firefox`, `coroutine-firefox`.
|
||||
The KiCad specs (heavier — they need the docker-built KiCad WASM) run on BOTH
|
||||
engines; `npm run test:kicad` is the firefox-only shortcut. The React web app
|
||||
suite is separate: `npm run test:web` (see `playwright-web.config.ts`).
|
||||
|
||||
To run specific tests:
|
||||
To run a subset, pick a project (and optionally a spec):
|
||||
```bash
|
||||
npx playwright test menu.spec.ts # Run menu tests only
|
||||
npx playwright test --grep "wxTimer" # Run tests matching pattern
|
||||
npx playwright test --project=wx-chromium menu.spec.ts # wx menu tests only
|
||||
npx playwright test --project=kicad-firefox kicad/pcbnew.spec.ts
|
||||
npx playwright test --project=wx-chromium --grep "wxTimer"
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
|
@ -49,12 +53,14 @@ tests/
|
|||
│ ├── wxwidgets.spec.ts # Comprehensive UI interaction tests
|
||||
│ └── ...
|
||||
├── logs/ # Test logs (auto-generated)
|
||||
├── test-results/ # Screenshots (auto-generated)
|
||||
├── baseline-screenshots/ # Reference screenshots for comparison
|
||||
├── test-results/{chromium,firefox}/ # Screenshots per engine (auto-generated)
|
||||
├── baseline-screenshots/{chromium,firefox}/ # Committed reference screenshots per engine
|
||||
├── screenshot-manifest.json # Authoritative {name, engine} list (npm run screenshots:manifest)
|
||||
├── apps/ # Built WASM test applications
|
||||
│ ├── minimal_test.html # Main test app
|
||||
│ └── standalone/ # Individual component test apps
|
||||
└── playwright.config.ts # Playwright configuration
|
||||
├── playwright.config.ts # THE merged config (wx / kicad / asyncify / coroutine / perf projects)
|
||||
└── playwright-web.config.ts # React web-app suite (own server stack)
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
|
@ -76,10 +82,15 @@ Example log format:
|
|||
|
||||
## Screenshots
|
||||
|
||||
Tests capture screenshots to `test-results/`. Compare against baselines:
|
||||
Tests capture raw PNGs to `test-results/<engine>/` (engine-scoped via
|
||||
`stableShot`/`shotPath`). The offline gate compares them against the committed
|
||||
per-engine baselines:
|
||||
```bash
|
||||
../scripts/compare-screenshots.sh
|
||||
npm run screenshots:check
|
||||
```
|
||||
CI's Linux render is the source of truth — update baselines by promoting a CI
|
||||
run (`npm run screenshots:promote -- --run <ci-run-id>`), never by copying
|
||||
local renders. Rules and details: `TESTING.md`.
|
||||
|
||||
## Viewing the App Directly
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Testing rules
|
||||
|
||||
Determinism rules for the Playwright specs (`tests/e2e`, `tests/kicad`, `tests/web`).
|
||||
Enforced by `npm run lint:determinism` (`tools/lint-determinism.ts`). Run specs from `tests/`
|
||||
via `npm run test:kicad` (firefox) / `npm run test:e2e` (chromium) — not playwright directly.
|
||||
Enforced by `npm run lint:determinism` (`tools/lint-determinism.ts`, gating in CI). Run specs
|
||||
from `tests/` via `npm run test:e2e` (the full CI project set: wx-chromium, kicad-firefox,
|
||||
kicad-chromium, asyncify-firefox, coroutine-firefox) or `npm run test:kicad` (kicad-firefox
|
||||
only) — not playwright directly. One spec on one engine:
|
||||
`npx playwright test --project=kicad-firefox kicad/pcbnew.spec.ts`.
|
||||
|
||||
## Waits — never blind
|
||||
|
||||
|
|
@ -14,6 +17,13 @@ via `npm run test:kicad` (firefox) / `npm run test:e2e` (chromium) — not playw
|
|||
- The **only** allowed `waitForTimeout` is an irreducible interaction dwell — a canvas/keyboard
|
||||
commit with no JS-observable signal — and it MUST carry a same-line marker:
|
||||
`// eslint-disable-line -- documented interaction dwell: <why>`.
|
||||
- **Menu clicks: wait for the specific item, not a count.** Popup items register in the element
|
||||
registry progressively as they paint, so a coarse gate ("N menuitems rendered") can pass before
|
||||
the item you're about to click exists — and `clickMenuItem` is single-shot. Before every
|
||||
`clickMenuItem(page, 'X')`, `await waitForRenderedByLabel(page, 'X', { elementType: 'menuitem' })`
|
||||
(same matcher as the click). `clickMenuItemByText` already waits internally and needs no guard.
|
||||
A submenu click needs its own wait: the parent menu's still-rendered items satisfy any count gate
|
||||
before the submenu paints.
|
||||
|
||||
## No defensive branches
|
||||
|
||||
|
|
@ -26,10 +36,16 @@ via `npm run test:kicad` (firefox) / `npm run test:e2e` (chromium) — not playw
|
|||
## Screenshots — `stableShot`, compared offline
|
||||
|
||||
- Capture with **`stableShot(page, 'name.png', { fullPage })`** — it settles the render (in-page
|
||||
canvas-hash over animation frames) then writes a raw PNG to `test-results/`. It does **not**
|
||||
assert. Never use Playwright's `toHaveScreenshot`.
|
||||
- Comparison is offline: `npm run screenshots:check` diffs `test-results/` against the committed
|
||||
baselines in `tests/baseline-screenshots/` (+ `3d-regression/`, `gal-regression/`).
|
||||
canvas-hash over animation frames) then writes a raw PNG to
|
||||
`test-results/<engine>/` (`chromium`/`firefox`, derived from the running browser — the same
|
||||
spec on two engines writes two files). It does **not** assert. Never use Playwright's
|
||||
`toHaveScreenshot`. Raw `page.screenshot`/fs writers must route through
|
||||
`shotPath(page, 'name.png')` for the same engine scoping.
|
||||
- Comparison is offline and per-engine: `npm run screenshots:check` diffs
|
||||
`test-results/<engine>/` against the committed baselines in
|
||||
`tests/baseline-screenshots/<engine>/` (+ `3d-regression/`, `gal-regression/`).
|
||||
`tests/screenshot-manifest.json` (regen: `npm run screenshots:manifest`) is the authoritative
|
||||
{name, engine} list — CI fails the lint step if it drifts from the baseline tree.
|
||||
- **CI's Linux render is the source of truth**; baselines are promoted from CI
|
||||
(`npm run screenshots:promote -- --run <ci-run-id>`). A local (Mac) check shows font/render
|
||||
noise and is not the gate.
|
||||
|
|
@ -37,9 +53,27 @@ via `npm run test:kicad` (firefox) / `npm run test:e2e` (chromium) — not playw
|
|||
|
||||
## Retries
|
||||
|
||||
- **`retries: 0`** in both configs. A failure is real; don't mask it with a retry.
|
||||
- **`retries: 0`** in both configs (`playwright.config.ts` — the merged wasm-suite config —
|
||||
and `playwright-web.config.ts`). A failure is real; don't mask it with a retry.
|
||||
|
||||
## Every spec runs in CI — `lint:ci-coverage`
|
||||
|
||||
`npm run lint:ci-coverage` (`tools/lint-ci-coverage.ts`, gating in CI next to the
|
||||
determinism lint) proves every `*.spec.ts` under `tests/` is actually executed by CI:
|
||||
it scrapes the `npm run test:…` invocations from `.github/workflows/`, resolves them
|
||||
through `package.json` to their `playwright test --config/--project` flags, and asks
|
||||
Playwright itself (`--list`) which files those runs cover. No hand-maintained lists —
|
||||
adding a spec in a brand-new directory is exactly what it catches.
|
||||
|
||||
When it fires:
|
||||
- `uncovered-spec` — your new spec matches no CI-run project. Put it in a covered
|
||||
`testDir`, adjust a project's `testMatch`, or add the project to a CI npm script.
|
||||
- `orphan-project` — you added a config project no CI script selects. Wire it into a
|
||||
CI script, or (for deliberately-local system-browser projects) add it to
|
||||
`LOCAL_ONLY_PROJECTS` in the lint with a comment saying why.
|
||||
|
||||
## Where things are
|
||||
|
||||
- Per-test logs (JS console + cpp): `tests/logs/{wxwidgets,kicad}/<test-name>/`.
|
||||
- Guard: `npm run lint:determinism`. Screenshot gate: `npm run screenshots:check`.
|
||||
- Guards: `npm run lint:determinism`, `npm run lint:ci-coverage`.
|
||||
Screenshot gate: `npm run screenshots:check`.
|
||||
|
|
|
|||
|
|
@ -387,73 +387,56 @@ cd tests/apps && ./build-test-apps.sh
|
|||
|
||||
## Baseline Screenshots
|
||||
|
||||
The test suite captures screenshots during test runs for visual regression testing. These screenshots are compared against a baseline to detect unexpected visual changes.
|
||||
The test suite captures raw PNGs during test runs for visual regression testing,
|
||||
compared offline against committed per-engine baselines (the calibrated gate in
|
||||
`tools/screenshots/`).
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── baseline-screenshots/ # Known-good reference screenshots (146+ files)
|
||||
├── test-results/ # Screenshots from latest test run
|
||||
└── apps/
|
||||
└── e2e/ # Playwright test specs
|
||||
├── baseline-screenshots/
|
||||
│ ├── chromium/ # Known-good references, Chromium render
|
||||
│ └── firefox/ # Known-good references, Firefox render
|
||||
├── test-results/
|
||||
│ ├── chromium/ # Latest run's captures per engine
|
||||
│ └── firefox/
|
||||
└── screenshot-manifest.json # Authoritative {name, engine} list
|
||||
```
|
||||
|
||||
Specs write via `stableShot(page, 'name.png')` / `shotPath(page, 'name.png')`
|
||||
(`e2e/utils/element-tracker.ts`) — the engine subdir is derived from the running
|
||||
browser, so the same spec on two engines produces two independent captures.
|
||||
|
||||
### Comparing Screenshots
|
||||
|
||||
Use the comparison script to check for visual regressions:
|
||||
|
||||
```bash
|
||||
./scripts/compare-screenshots.sh
|
||||
cd tests
|
||||
npm run screenshots:check
|
||||
```
|
||||
|
||||
This will:
|
||||
- Compare all baseline screenshots with current test results
|
||||
- Report identical, different, and missing screenshots
|
||||
- Show file size differences (useful for detecting changes)
|
||||
|
||||
Example output:
|
||||
```
|
||||
=== Screenshot Comparison ===
|
||||
Baseline: /path/to/tests/baseline-screenshots
|
||||
Test Results: /path/to/tests/test-results
|
||||
|
||||
=== Comparing Baseline Screenshots ===
|
||||
DIFFERENT: dialogs-msgbox-info-open.png (baseline: 47468B, current: 47478B, diff: 10B / .02%)
|
||||
|
||||
=== Summary ===
|
||||
Total baseline screenshots: 121
|
||||
Identical: 54
|
||||
Different: 67
|
||||
Missing from test results: 0
|
||||
```
|
||||
|
||||
### Understanding Differences
|
||||
|
||||
Small byte differences (<2%) are typically caused by:
|
||||
- **Timestamps** in event logs (e.g., `[19:45:35]` vs `[18:49:35]`)
|
||||
- **PNG compression** variations between runs
|
||||
- **Anti-aliasing** differences
|
||||
|
||||
These are **not** visual regressions. Visually inspect screenshots if you see larger differences.
|
||||
This diffs `test-results/<engine>/` against `baseline-screenshots/<engine>/` with
|
||||
per-engine calibrated noise floors and reports CHANGED / ADDED / REMOVED (removed
|
||||
detection is driven by the manifest). On each main push, CI posts the same report
|
||||
(triptych images labeled `[chromium]`/`[firefox]`) to Discord.
|
||||
|
||||
### Updating Baseline Screenshots
|
||||
|
||||
After verifying changes are intentional, update the baseline:
|
||||
CI's Linux render is the source of truth — never copy local (Mac) renders into
|
||||
the baselines. Promote from a CI run instead (churn-free: only meaningfully
|
||||
changed images restage, and the manifest regenerates automatically):
|
||||
|
||||
```bash
|
||||
# Copy current screenshots to baseline
|
||||
cp tests/test-results/*.png tests/baseline-screenshots/
|
||||
|
||||
# Or selectively update specific screenshots
|
||||
cp tests/test-results/dialog-*.png tests/baseline-screenshots/
|
||||
cd tests
|
||||
npm run screenshots:promote -- --run <ci-run-id>
|
||||
git commit
|
||||
```
|
||||
|
||||
### Running Tests with Screenshots
|
||||
|
||||
```bash
|
||||
cd tests
|
||||
npm test # wx e2e specs + asyncify harness (kicad: npm run test:kicad)
|
||||
npm test # setup + the full merged run (all CI projects)
|
||||
npx playwright test --ui # Interactive mode with screenshot preview
|
||||
```
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
BIN
tests/baseline-screenshots/chromium/calculator-before-switch.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
tests/baseline-screenshots/chromium/calculator-color-code.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
tests/baseline-screenshots/chromium/calculator-loaded.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |