The committed tests/screenshot-manifest.json is retired. CI now downloads baselines/pcbjam/manifest.json (manifest v3, written only by the morelli review app + its seed script) to the gitignored .baseline-manifest.json, and everything downstream (pull, verify, compare) reads that copy: - config.ts: MANIFEST_VERSION 3, MANIFEST_PATH .baseline-manifest.json, R2_BASELINES_MANIFEST_KEY; ManifestEntry grows opaque provenance - r2-sync.ts: new --manifest mode (atomic fetch; no-creds skip DELETES a stale copy so the gate skips rather than using old baselines); --push gone (bytes enter the CAS only via morelli's promote) - compare.ts: hard-skips when no manifest was fetched — a stale warm cache can never gate - wasm-build.yml: fetch-manifest step before the baselines cache; cache key now hashes the fetched manifest; the gen-manifest --check lint gate goes with the committed manifest - deleted: screenshot-manifest.json, promote.ts, changelog.ts, gen-manifest.ts, screenshot-changelog.yml, promote-screenshots skill - docs (CLAUDE/README/TESTING/WHATWORKS/tools README): promote flow is now https://pcbjam-morelli-staging.pcbjam-staging.workers.dev Validated locally against the real bucket: fetch-manifest (492), cold pull 492 / warm pull cached=492, no-creds skip chain, compare gate skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.2 KiB
Testing rules
Determinism rules for the Playwright specs (tests/e2e, tests/kicad, tests/jspi, tests/web).
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, jspi-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
- No
page.waitForTimeout(n). Wait on a condition:expect.poll(() => predicate), a web-first assertion (expect(locator).toBeVisible()), orwaitUntil(page, fn, desc)(throws loudly on timeout). - App readiness:
waitForWxApp(page)(canvas visible + element registry populated) for widget/editor harnesses;waitForCanvasApp(page)for registry-less canvas apps. - The only allowed
waitForTimeoutis 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
clickMenuItemis single-shot. Before everyclickMenuItem(page, 'X'),await waitForRenderedByLabel(page, 'X', { elementType: 'menuitem' })(same matcher as the click).clickMenuItemByTextalready 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
- No
if (await el.count()) el.click(). Assert the element exists, then act:expect(await clickByLabel(page, 'X'), '...').toBe(true). UseclickMenuItemByText(normalizes&/.../…) instead of try-A-else-A…-else-A fallback chains. - No swallowed
.catch(() => {}). Let it throw, or assert the tolerated outcome. A genuinely best-effort op must carry a marker explaining why.
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 totest-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'stoHaveScreenshot. Rawpage.screenshot/fs writers must route throughshotPath(page, 'name.png')for the same engine scoping. - Comparison is offline and per-engine:
npm run screenshots:checkdiffstest-results/<engine>/against the baselines intests/baseline-screenshots/<engine>/(+ the still-committed3d-regression/,gal-regression/). - Baselines live in the private R2 bucket
pcbjam-ci-screenshots, not git: the R2-hosted manifestbaselines/pcbjam/manifest.jsonpins each{name, engine}to a sha256, andbaseline-screenshots/+.baseline-manifest.jsonare gitignored caches — materialize them withnpm run screenshots:fetch-manifest && npm run screenshots:fetch(needs the R2 credentials; seetools/screenshots/README.md). Nothing screenshot-related is committed to git. - CI's Linux render is the source of truth; baselines are promoted from a CI run in the morelli review app (https://pcbjam-morelli-staging.pcbjam-staging.workers.dev) — CI uploads each run's renders to R2 (30-day retention), morelli shows the diffs and writes the manifest on Promote. A local (Mac) check shows font/render noise and is not the gate.
- A continuously-animating state (timer, mid-slide) can't be a stable baseline — drop the shot.
Retries
retries: 0in both configs (playwright.config.ts— the merged wasm-suite config — andplaywright-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 coveredtestDir, adjust a project'stestMatch, 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 toLOCAL_ONLY_PROJECTSin the lint with a comment saying why.
Where things are
- Per-test logs (JS console + cpp):
tests/logs/{wxwidgets,kicad}/<test-name>/. - Guards:
npm run lint:determinism,npm run lint:ci-coverage. Screenshot gate:npm run screenshots:check.