4.5 KiB
differ
Differential test harness for the React → Svelte migration (built in Phase 1 of
svelte-big-bang.md). Runs apps/web-react (the oracle) and apps/web against
the same seeded database with a frozen clock and diffs them directly:
- Pixel diff (gate) — screenshot every route in the generated census (route × theme × viewport), strict threshold.
- SSR HTML diff (gate) — normalized
<head>comparison per route to protect SEO through the cutover. - Resource diff (gate) — non-HTML routes (api-public, resource routes, redirects) compared on status, content type, location and body.
- ARIA structure diff (advisory) — Playwright ARIA snapshots as a report to eyeball, not a 1:1 gate.
Until apps/web exists both sides serve apps/web-react: the React-vs-React
run must report zero diffs before the harness judges anything (Phase 1 exit
gate).
Running
pnpm run differ # full census, both themes and viewports
pnpm run differ -- --filter /leaderboards
pnpm run differ -- --max-rows 20 --themes light --viewports desktop
pnpm run differ -- --seed-now 2026-08-16T12:00:00Z --concurrency 2
pnpm run differ -- --skip-prepare # reuse the previous build + seeded dbs
pnpm run differ -- --right-app web --filter /leaderboards # React vs Svelte
--right-app web serves apps/web (the SvelteKit app) on the right side;
the default (web-react) is the React-vs-React control run. The left side is
always the React oracle.
Heads-up: a run holds two production servers plus a Chromium instance — a
couple of GB of memory. Run it from a plain terminal, not nested under other
memory-heavy tooling, and leave --concurrency at its default (1) on smaller
machines.
The run writes output/<timestamp>/:
report.json— every row's results plus a summary; exit code 1 when a gate (pixel / head / resource) failedcensus.json— the generated route censusartifacts/<route>/<variant>/—left.png/right.png/diff.png, normalized head HTML, ARIA snapshots — written only for rows that differ
How a run works
scripts/ensure-e2e-build.ts— the same production build the e2e suite serves (VITE_E2E_TEST_RUN=truebaked in), reused when fresh.- Migrate + seed
db-differ-seed.sqlite3with the clock frozen atSEED_NOW(scripts/seed.tsinstallsinstallSeedClock), then copy it for each side. The differ seeds withSEED_QUIET_WORLD=true: nobody is live on Twitch, so the sidebar streams chrome (which depends on the not-yet-migrated tournament cluster) renders identically empty on both sides. Drop the flag when the tournament wave migrates. scripts/route-census.ts— walksapp/routes.tsand resolves every dynamic param from the seeded database. Never hand-listed: a new route without a resolver is a hard error; a resolver finding no seed data marks the rowskippedand the report keeps it visible (seed-coverage gap).- Two servers start on ports 6873/6874 (heap-capped), one database copy each,
both with the same baked
VITE_SITE_DOMAINso absolute URLs are identical. - Per browser context (side × theme): log in as the seeded admin via
/auth/impersonate, set the theme cookie via/theme, freeze the client clock atSEED_NOW(context-level Playwright clock), reseedMath.randomper document, and abort every request leaving localhost (fonts and third-party images fail identically on both sides). - Per page row: wait for hydration, kill animations, screenshot until two consecutive shots are identical (canvas charts settle), then compare. Pages taller than the capture cap are clipped — decoded RGBA of unbounded pages is what OOMs the process. Decoded-pixel comparison is serialized for the same reason; identical screenshots (the common case) skip decoding.
The app contract
What a served app must provide for the differ (and the e2e suite) to work against it — the Svelte app has to implement the same surface:
data-testid="hydrated"element once client JS is ready, carryingdata-router-idle="true"when no navigation/mutation is in flightdata-testid="error-page"on the error boundary page- dev-controls endpoints under the e2e build flag:
POST /auth/impersonate?id=,POST /theme(theme=light|dark),POST /refresh-caches,POST /end-season,POST /run-routine - the shared
data-testidvocabulary used bye2e/pages/**(no CSS-module class or icon-class selectors — enforced by keepingclass*=/lucide-out ofe2e/)