pcbjam/tests/TESTING.md
Istvan Matejcsok ec7a1b0787 screenshots: R2-hosted manifest becomes the baseline source of truth (morelli cutover)
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>
2026-08-19 14:35:08 +02:00

88 lines
5.2 KiB
Markdown

# 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()`), or `waitUntil(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 `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
- **No `if (await el.count()) el.click()`.** Assert the element exists, then act:
`expect(await clickByLabel(page, 'X'), '...').toBe(true)`. Use `clickMenuItemByText` (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 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 baselines in
`tests/baseline-screenshots/<engine>/` (+ the still-committed `3d-regression/`,
`gal-regression/`).
- **Baselines live in the private R2 bucket `pcbjam-ci-screenshots`, not git**:
the R2-hosted manifest `baselines/pcbjam/manifest.json` pins each
`{name, engine}` to a sha256, and `baseline-screenshots/` +
`.baseline-manifest.json` are gitignored caches — materialize them with
`npm run screenshots:fetch-manifest && npm run screenshots:fetch` (needs the
R2 credentials; see `tools/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: 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>/`.
- Guards: `npm run lint:determinism`, `npm run lint:ci-coverage`.
Screenshot gate: `npm run screenshots:check`.