2.9 KiB
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. Nopixelmatchdirect dep needed. - Storybook path:
@storybook/test-runneris the canonical Storybook→Playwright bridge. It exposespreVisit/postVisithooks where we capture screenshots using the same PlaywrightPageinstance, 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: reduceforced viapage.emulateMedia.
OS matrix
Baselines are stored per platform: __snapshots__/<linux|darwin|win32>/.... 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
- Developer runs
bunx turbo test --filter=<pkg> -- --update-snapshotslocally. - Diff is committed in a separate, review-tagged commit prefixed
chore(visual): refresh baselines — <reason>. Squashing into feature commits is forbidden. - 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.readyafternetworkidle. Web fonts that load lazily are the #1 source of flake. - Color scheme: stories with
parameters.colorScheme === 'dark'toggle viapage.emulateMedia({ colorScheme: 'dark' })before capture. - Time: any time-display surfaces under test must already accept an injected
Clockperopenspec/project.md; visual specs SHALL pass aFakeClockset 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".