Skip to content

fix(capture): declare prefers-reduced-motion before navigation#316

Merged
BenSheridanEdwards merged 1 commit into
mainfrom
fix/reduced-motion-capture
Jul 18, 2026
Merged

fix(capture): declare prefers-reduced-motion before navigation#316
BenSheridanEdwards merged 1 commit into
mainfrom
fix/reduced-motion-capture

Conversation

@BenSheridanEdwards

Copy link
Copy Markdown
Owner

Why does this feature exist?

Validating 4.6.0 against a real consumer surfaced a determinism gap in the capture itself: a JS-driven entrance animation (framer-motion, blur(10px) → blur(0px)) raced the settle, and the self-check correctly failed the surface as "non-deterministic — 1 computed-style difference(s) between two captures of the same commit" — on every cold capture, across multiple PRs. FREEZE_CSS cannot reach this class of motion: those libraries write inline styles from rAF loops that no stylesheet overrides. But they all honour prefers-reduced-motion, and they read it at mount — the capture just never declared it. Out-of-the-box determinism shouldn't depend on each consumer wiring reduced-motion emulation into their own Playwright config.

What changed?

  1. src/runner.tscaptureSurface emulates reducedMotion: 'reduce' before go(). Declared before the first navigation because mount-time is when JS animation libraries read the media query; page.emulateMedia persists across every later go()/reset in the same flow (state resets, popup captures, the determinism self-check's re-navigation).
  2. src/capture-url.tscaptureUrlToDir emulates it before its first goto, covering the ad-hoc URL-capture path and the crawl.
  3. src/capture.tscaptureStyleMap declares it defensively for direct library users, beside the existing FREEZE_CSS injection, with a comment explaining why both exist (CSS-declared motion vs rAF-driven inline styles).

Consumers whose components branch on reduced motion will see a one-time baseline shift to the motionless final state — the deterministic state a style gate should certify; the CHANGELOG entry says so explicitly.

Behavioural Proof (with video and screenshots)

Not applicable — capture-determinism logic with no rendered surface of its own. The new e2e fixture pins the mechanism executably: a page whose mount script colours an element by the mount-time value of prefers-reduced-motion, captured through captureUrlToDir. It passes with the pre-navigation emulation and fails without it (verified both ways locally). The first fixture attempt asserted on the mid-flight race itself and could not bite (the settle's volatile-region detection masked it) — the committed fixture pins the declared media state instead, deterministic in both directions.

Verification Summary

  • Full unit suite: npm test — 587/587 pass.
  • Full e2e suite: npx playwright test — 115/115 pass, including the new reduced-motion fixture and all existing capture/crawl/dogfood specs (no baseline pins shifted).
  • Negative control: reverting the three emulation call sites and rerunning npx playwright test -g "reduced-motion" fails as expected.
  • Pre-commit gate green: tsc build + typecheck, eslint, prettier, gitleaks, fallow, commitlint.

🤖 Generated with Claude Code

FREEZE_CSS only reaches CSS-declared motion. JS animation libraries
(framer-motion, react-spring...) drive inline styles from rAF loops no
stylesheet can override — but they honour prefers-reduced-motion, and they
read it at mount. A short entrance animation racing the settle produced the
'surface is non-deterministic' self-check failure on real consumer apps.

captureSurface and captureUrlToDir now emulate reduced motion BEFORE the
first navigation (emulateMedia persists across later navigations in the
flow); captureStyleMap declares it defensively for direct library users.
The new e2e fixture pins the mount-time media read and fails without the
pre-navigation emulation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown

🗺️ StyleProof report

📊 View the side-by-side visual report →


A navigable affordance was removed without acknowledgement — repair or acknowledge it; visual approval cannot clear this failure.

@BenSheridanEdwards
BenSheridanEdwards merged commit 5e1d3c3 into main Jul 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant