## Architecture Two cooperating helpers share one threshold, one viewport, and one font/animation discipline so the e2e and Storybook baselines stay comparable. ### Adapter choice - **e2e path:** Playwright's built-in `expect(page).toHaveScreenshot()` — already in the dependency closure, supports per-OS baselines natively, threshold semantics (`maxDiffPixelRatio`, `maxDiffPixels`) are well documented. No `pixelmatch` direct dep needed. - **Storybook path:** `@storybook/test-runner` is the canonical Storybook→Playwright bridge. It exposes `preVisit` / `postVisit` hooks where we capture screenshots using the same Playwright `Page` instance, so the comparator and threshold match the e2e path bit-for-bit. A pure-Node `pixelmatch` helper was rejected: it would fork the comparator, drift in floating-point handling between OSes, and double the surface to maintain. ### Default thresholds - `maxDiffPixelRatio: 0.005` (0.5%). Tighter ratios (e.g. 0.001) produce noise from sub-pixel font rendering on different GPU drivers. - `animations: 'disabled'`, `caret: 'hide'`, `scale: 'css'`. - Viewport: 1280×720 default. Stories pin to the same default unless the story sets an explicit `parameters.viewport`. - `prefers-reduced-motion: reduce` forced via `page.emulateMedia`. ### OS matrix Baselines are stored per platform: `__snapshots__//...`. CI MUST run at least the platforms it intends to gate on. A platform with no baseline directory MUST skip the comparison (no implicit pass) and emit a warning. ### Baseline-update workflow 1. Developer runs `bunx turbo test --filter= -- --update-snapshots` locally. 2. Diff is committed in a **separate, review-tagged commit** prefixed `chore(visual): refresh baselines — `. Squashing into feature commits is forbidden. 3. PR reviewer compares before/after PNGs in the diff view; CI re-runs the comparator on the new baselines to catch accidental cross-platform drift. ### Determinism levers - Fonts: `await document.fonts.ready` after `networkidle`. Web fonts that load lazily are the #1 source of flake. - Color scheme: stories with `parameters.colorScheme === 'dark'` toggle via `page.emulateMedia({ colorScheme: 'dark' })` before capture. - Time: any time-display surfaces under test must already accept an injected `Clock` per `openspec/project.md`; visual specs SHALL pass a `FakeClock` set to a fixed instant. ### Error model `VisualRegressionError extends TestingE2eError` for the e2e path, `StorybookVisualError extends TestingDomError` for the Storybook path. Each carries `{ code, cause?, context: { baselinePath, actualPath, diffPath, pixelRatio } }`. ### Cross-links - Release-readiness gate: `../enterprise-testbed-verification/proposal.md`. - Audit driver: `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` §2 row 2. - Project rules: `/mnt/s/web/genesis/CLAUDE.md` "Web App & Page Verification".