test(ui): clear localStorage between component tests #219

Merged
logan merged 1 commits from fix/138-ui-test-storage-leak into main 2026-08-25 16:37:42 +00:00
Collaborator

What the issue was

test/components/play-in-context.test.ts fails about one full make ui-test
run in three, on a clean main, with

AssertionError: expected [ 12, '/music/track-11.mp3' ]
              to deeply equal [ 12, '/music/track-3.mp3' ]

and passes every time it is run on its own. It has now failed three
scheduled runs, including PR #218, whose diff was entirely backend/riff,
backend/metadata and backend/tagwriter — no frontend file touched. That
is the expensive part: make ui-test is a required check, so this sends
authors looking at their own diff.

What it actually is

A test file does not get its own origin. @vitest/browser-playwright
opens one BrowserContext per session (createContext(sessionId)
browser.newContext()) and runs several files in it one after another, so
localStorage survives from file to file. track-list restores its sort in
connectedCallback; aria-tail.test.ts:100 activates the Title column
header from the keyboard, which persists track-list-sort-field. Any file
that mounts a track list afterwards in that tab opens sorted by title —
where track-11 precedes track-3.

Nothing about it is specific to that pair. A temporary probe (a root
beforeEach that throws when localStorage is not empty) failed 24
test-starts in a single full run on main — 11 with
["track-list-sort-field","track-list-sort-direction"], 8 with
cover-grid-size, 5 with track-list-column-widths — and cascaded into 248
failures. What varies run to run is only which files share a tab and in what
order, which is the whole of why this reads as a 1-in-3 flake.

That also explains why the reporter's sequential subset did not reproduce
it
: two files named on the command line rarely land in the tab, or the
order, that matters. The full suite has ~99 chances to.

What changed, and why there

Both halves of the issue's own Direction, and nothing else.

  • frontend/test/setup.tslocalStorage.clear() in the root
    beforeEach, beside resetHarness(). It belongs here rather than in the
    specs that write, because the spec that reads is never the one that
    knows. It is safe for exactly the reason the leak exists: files within a
    session are sequential, so it cannot wipe storage a concurrently-running
    file is in the middle of using.
  • frontend/test/components/play-in-context.test.ts — the spec states
    the order it asserts (sortField = null after mount) instead of
    inheriting a default, and checks the row it is about to double-click
    carries the path it expects, so a stray sort fails by naming itself rather
    than as an off-by-eight file path.
  • CLAUDE.md — the trap and the reasoning, in the make ui-test
    section.

No production code is touched. That was also this run's constraint:
eight PRs are open against an unmoved main and between them already edit
most of frontend/src.

Verification

Tier Result
make ui-test 1092 passed, eight consecutive full runs
The probe above, re-run on top of the fix 0 polluted test-starts, against 24 before
npx tsc --noEmit clean
make ui-visual 2 screenshot cases fail — identically on origin/main with the change stashed (see below)
make commit-check well-formed

Proved non-vacuous. With the two sort keys injected into setup.ts and
both halves of the fix defeated, the spec fails at the new first
assertion: expected '/music/track-11.mp3' to be '/music/track-3.mp3'. With
the pollution injected and only the setup clear defeated, it passes; with
only the spec's own statement defeated, it passes. Each half stands alone.

Pre-existing and not this. make ui-visual fails chrome.test.ts <app-sidebar> and now-playing.test.ts <now-playing> — byte-identical on
origin/main with the change stashed. These are the machine-specific
screenshot baselines CLAUDE.md describes; CI runs make ui-test, not
make ui-visual.

Tiers deliberately not run. No Go, no .sql, no .templ, no bound
signature and no CSS, so make lint, make test, make generate,
make bindings and make css-check do not apply. No user-visible flow
changed, so make e2e was not run.

Deliberately not done

The four specs that clear one key by hand (play-in-context,
track-list-width, track-list-phone, library-store) keep doing so. The
lines are redundant now but harmless, and deleting them would widen the diff
across four files for nothing.

Closes #138

## What the issue was `test/components/play-in-context.test.ts` fails about one full `make ui-test` run in three, on a clean `main`, with ``` AssertionError: expected [ 12, '/music/track-11.mp3' ] to deeply equal [ 12, '/music/track-3.mp3' ] ``` and passes every time it is run on its own. It has now failed three scheduled runs, including PR #218, whose diff was entirely `backend/riff`, `backend/metadata` and `backend/tagwriter` — no frontend file touched. That is the expensive part: `make ui-test` is a required check, so this sends authors looking at their own diff. ## What it actually is **A test file does not get its own origin.** `@vitest/browser-playwright` opens one `BrowserContext` per session (`createContext(sessionId)` → `browser.newContext()`) and runs several files in it one after another, so `localStorage` survives from file to file. `track-list` restores its sort in `connectedCallback`; `aria-tail.test.ts:100` activates the Title column header from the keyboard, which persists `track-list-sort-field`. Any file that mounts a track list afterwards **in that tab** opens sorted by title — where `track-11` precedes `track-3`. Nothing about it is specific to that pair. A temporary probe (a root `beforeEach` that throws when `localStorage` is not empty) failed **24** test-starts in a single full run on `main` — 11 with `["track-list-sort-field","track-list-sort-direction"]`, 8 with `cover-grid-size`, 5 with `track-list-column-widths` — and cascaded into 248 failures. What varies run to run is only which files share a tab and in what order, which is the whole of why this reads as a 1-in-3 flake. That also explains **why the reporter's sequential subset did not reproduce it**: two files named on the command line rarely land in the tab, or the order, that matters. The full suite has ~99 chances to. ## What changed, and why there Both halves of the issue's own Direction, and nothing else. - **`frontend/test/setup.ts`** — `localStorage.clear()` in the root `beforeEach`, beside `resetHarness()`. It belongs here rather than in the specs that write, because the spec that *reads* is never the one that knows. It is safe for exactly the reason the leak exists: files within a session are sequential, so it cannot wipe storage a concurrently-running file is in the middle of using. - **`frontend/test/components/play-in-context.test.ts`** — the spec states the order it asserts (`sortField = null` after mount) instead of inheriting a default, and checks the row it is about to double-click carries the path it expects, so a stray sort fails by naming itself rather than as an off-by-eight file path. - **`CLAUDE.md`** — the trap and the reasoning, in the `make ui-test` section. **No production code is touched.** That was also this run's constraint: eight PRs are open against an unmoved `main` and between them already edit most of `frontend/src`. ## Verification | Tier | Result | | --- | --- | | `make ui-test` | **1092 passed, eight consecutive full runs** | | The probe above, re-run on top of the fix | **0** polluted test-starts, against 24 before | | `npx tsc --noEmit` | clean | | `make ui-visual` | 2 screenshot cases fail — **identically on `origin/main` with the change stashed** (see below) | | `make commit-check` | well-formed | **Proved non-vacuous.** With the two sort keys injected into `setup.ts` and *both* halves of the fix defeated, the spec fails at the new first assertion: `expected '/music/track-11.mp3' to be '/music/track-3.mp3'`. With the pollution injected and only the setup clear defeated, it passes; with only the spec's own statement defeated, it passes. Each half stands alone. **Pre-existing and not this.** `make ui-visual` fails `chrome.test.ts <app-sidebar>` and `now-playing.test.ts <now-playing>` — byte-identical on `origin/main` with the change stashed. These are the machine-specific screenshot baselines `CLAUDE.md` describes; CI runs `make ui-test`, not `make ui-visual`. **Tiers deliberately not run.** No Go, no `.sql`, no `.templ`, no bound signature and no CSS, so `make lint`, `make test`, `make generate`, `make bindings` and `make css-check` do not apply. No user-visible flow changed, so `make e2e` was not run. ## Deliberately not done The four specs that clear one key by hand (`play-in-context`, `track-list-width`, `track-list-phone`, `library-store`) keep doing so. The lines are redundant now but harmless, and deleting them would widen the diff across four files for nothing. Closes #138
logan added 1 commit 2026-08-24 10:47:51 +00:00
test(ui): clear localStorage between component tests
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m29s
CI / e2e (pull_request) Successful in 9m41s
772c71c49f
A test file does not get its own origin. `@vitest/browser-playwright`
opens one BrowserContext per session and runs several files in it, one
after another, so everything a component persists survives from file to
file. `track-list` restores its sort in `connectedCallback` and
`aria-tail.test.ts` activates the Title column header, so any file that
mounts a track list later in that tab opens sorted by title — where
`track-11` precedes `track-3`, which is #138's failure exactly.

Nothing about it is specific to that pair: a probe that throws when a
test starts with a non-empty `localStorage` failed 24 test-starts in one
full run (11 with the two sort keys, 8 with `cover-grid-size`, 5 with
`track-list-column-widths`) and cascaded into 248 failures. Which files
share a tab, and in what order, changes run to run, which is the whole
of why this reads as a 1-in-3 flake and passes in isolation.

The clear belongs in `setup.ts` rather than in the specs that write,
because the spec that reads is never the one that knows — and it is
safe for the same reason the leak exists: files within a session are
sequential, so it cannot wipe storage a concurrent file is using.

The spec now also states the order it asserts rather than inheriting a
default, and checks the row it is about to double-click carries the path
it expects, so a stray sort fails by naming itself instead of as an
off-by-eight file path.

Closes #138
Author
Collaborator

CI green. Run 554: check success, e2e success (chromium and WebKit).

Worth noting for this PR specifically — the check job'''s Component and store suite step is the one the issue is about, and it passed first attempt. That is one sample, not a proof; the eight consecutive local full runs in the description are the evidence, and the probe result (0 polluted test-starts against 24 before) is the one that does not depend on how many times anything was run.

**CI green.** Run 554: `check` success, `e2e` success (chromium and WebKit). Worth noting for this PR specifically — the `check` job'''s `Component and store suite` step is the one the issue is about, and it passed first attempt. That is one sample, not a proof; the eight consecutive local full runs in the description are the evidence, and the probe result (0 polluted test-starts against 24 before) is the one that does not depend on how many times anything was run.
logan merged commit f3207e8bf9 into main 2026-08-25 16:37:42 +00:00
Sign in to join this conversation.