Files
sendou.ink/tooling/differ/README.md
2026-08-16 19:14:22 +03:00

89 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```sh
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) failed
- `census.json` — the generated route census
- `artifacts/<route>/<variant>/` — `left.png` / `right.png` / `diff.png`,
normalized head HTML, ARIA snapshots — written only for rows that differ
## How a run works
1. `scripts/ensure-e2e-build.ts` — the same production build the e2e suite
serves (`VITE_E2E_TEST_RUN=true` baked in), reused when fresh.
2. Migrate + seed `db-differ-seed.sqlite3` with the clock frozen at `SEED_NOW`
(`scripts/seed.ts` installs `installSeedClock`), then copy it for each side.
The differ seeds with `SEED_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.
3. `scripts/route-census.ts` — walks `app/routes.ts` and 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 row
`skipped` and the report keeps it visible (seed-coverage gap).
4. Two servers start on ports 6873/6874 (heap-capped), one database copy each,
both with the same baked `VITE_SITE_DOMAIN` so absolute URLs are identical.
5. Per browser context (side × theme): log in as the seeded admin via
`/auth/impersonate`, set the theme cookie via `/theme`, freeze the client
clock at `SEED_NOW` (context-level Playwright clock), reseed `Math.random`
per document, and abort every request leaving localhost (fonts and
third-party images fail identically on both sides).
6. 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, carrying
`data-router-idle="true"` when no navigation/mutation is in flight
- `data-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-testid` vocabulary used by `e2e/pages/**` (no CSS-module
class or icon-class selectors — enforced by keeping `class*=`/`lucide-` out
of `e2e/`)