# The e2e suite ```sh pnpm --filter umtool e2e # everything pnpm --filter umtool e2e projects.spec.ts # one file ``` A **"waiting for the e2e queue"** banner is normal, not a hang: the queue is machine-global and one suite runs at a time. ## The fixture `e2e/fixtures/make-fixture.mjs`, rebuilt on every run. The suite **never** runs against the real song directory or the real reports tree — those hold thousands of real human verdicts and six finished videos, and a spec that judged a clip or rendered over a deliverable would be indistinguishable from a person doing it. **Every fixture item has a true answer.** That is the principle; the specifics: | | | |---|---| | `bg.mp4` | 220 Hz then 3000 Hz at the same loudness — only the brightness curve can see the change. A cue at 3.00 s. | | `song.mp4` | 2 s of silence then a tone. `firstSound` at 2.00 s. | | `vid1` cues | punctuated, with a run-on cue at 3–6 s | | `vid2` cues | **no terminator anywhere** — the real degradation in this corpus | | `gone1` cues | readable, but the stub yt-dlp reports the upload removed | | `vid1_0.00-9.00.mp4` | tone/silence/tone with silences centred on 3.0 s and 6.0 s (verified in the file: 2.90–3.11, 5.92–6.11) | ## The projects, and why each exists | | | |---|---| | `report-fixture` | read-only. 4 clips: c01 ends mid-sentence, c04 does too but sets `lockEnd`, c03's source has no punctuation | | `bench-fixture` | the clip bench **writes** — windows, locks, attribution and corrections | | `build-fixture` | the build **writes** | | `onscreen-fixture` | the On-screen section and the bench's on-screen fields **write** — 1920×1080 with the deck on, never built (the preview's estimate) | | `onscreen-build-fixture` | built with the deck, then re-rendered on-screen (`--chrome-only`) | | `gone-fixture` | its source is gone — the preflight must block it | | `no-origin-fixture` / `localhost-fixture` | the two defects that shipped | | `takes-fixture` | takes **write** `takes/verdicts.json` — two groups, a take with no preview, one skipped take.json, a work dir that is not a take | | `bike-fixture` | the third kind | | `find/` | shadowed by a tool page | | `deep/nested/solo-fixture` | a pass-through chain, for the collapse | **Three copies of one manifest is not duplication.** Sharing one project between the read-only specs and the writing ones made the suite pass or fail depending on which file playwright ran first — and the failure named the wrong thing entirely. ## Stubs `YTDLP_BIN`, `QRENCODE_BIN` and `HYPERFRAMES_BIN` point at node scripts the fixture writes, so the whole build chain runs **offline and deterministically**. The HyperFrames stub writes the exact frame sequence the build checks, alternating RGB and RGBA PNGs (the mix a real render produces, and the one `-reinit_filter 0` exists for), and logs every argv to `bin/hyperframes.invocations`. The deck's true still is the system chromium (`CHROME`), not a stub. They are node, not bash: the yt-dlp stub does fractional arithmetic on `--download-sections *FROM-TO`, and doing that in bash means awk, which means three layers of quoting inside a generated file. It got mangled once. ## Writing a spec here - Assert **relationships, not magic numbers**. "the chip's number equals the number of cards" survives a new fixture project; `toHaveCount(6)` does not. - If your spec writes, give it its own project. - A project id is a **path**: `[data-project$='/alpha']`, not `[data-song=alpha]`. - Some assertions read the **source**, not a page — that a kind id is not special-cased outside the registry, that `RESERVED_BROWSE` matches the real directory listing. Those are the ones that fail when the design is broken in a way no rendering can show. ## Known flake `undo.spec.ts:93` reds under load and passes in isolation. It is the documented waveform-drag capture race, not a regression. ## Gotchas **Run e2e in dev mode** (the default). `E2E_MODE=start` serves the last build, which is stale for uncommitted source changes. **`pnpm build` is a separate check.** The suite runs in dev, which never prerenders — a layout or client-component change can pass every spec and 500 in production. Two real bugs in this feature were found that way and one only by a page render. **`fill("")` on a controlled input is two round trips, not one.** Playwright selects the text, then presses Delete separately. A React re-render in between (hydration, right after a `goto`) reassigns the value and collapses the selection to offset 0 — Delete then removes ONE character and the field saves `value.slice(1)`. It reds under load and passes in isolation, and it blames the component, which saved exactly what the box held. Assert the value after the fill and retry (`expect(async () => {…}).toPass()`) before triggering whatever the edit commits on. **Kill stray dev servers by port**, not with `pkill -f`: the pattern matches your own shell's command line.