make e2e is green on chromium: 92 passed. The harness is rebuilt on
what v3 actually offers, and three of the four things it replaced turn
out to be better than what they replaced.
The headless launch is v3's own server mode. scripts/dev-headless.sh
ran a `-tags dev` binary whose app_dev.go parsed -devserver/-assetdir
out of os.Args; that file went with v2, so the harness had no server at
all. `-tags dev,server` is a first-class mode and needs no display, so
Xvfb is gone from the script and from CI.
The bridge hooks two places, neither of them EventsOn. Inbound is
window._wails.dispatchWailsEvent, wrapped by pre-creating the object
the runtime keeps and putting an accessor on the one property.
Outbound is fetch: v3 routes every runtime call through one POST, so
the bridge sees binding calls and event emits from any module, needs no
walk of an object graph, and cannot miss a call made before it looked.
__yjEvents.call posts to that endpoint by method name, so it depends on
nothing in the app's bundle and works on a page with no init script.
That is what lets seed-sandbox.sh drop playwright-cli entirely — it
drove AddLibrary through a browser only because window.go was v2's one
way in — and with it a global npm install and a second Chromium in CI.
measure.mjs and one spec lose their window.go walks and read the
bridge's log instead; e2e/support/method-ids.mjs derives id -> name
from frontend/bindings/ (phase 6b option 1, so it cannot go stale
silently). Plain .mjs because measure.mjs runs under bare node and one
derivation beats two that can disagree.
Four bugs surfaced, and the migration is how.
The cross-service wiring never ran headless. It hung off
Common.ApplicationStarted, which server mode never emits —
setupCommonEvents is an explicit no-op there — so the queue had no
TrackLoader and playing a track changed the queue and then silently did
nothing. It is a service registered last now (backend/startup.go):
services start in registration order, which is the ordering the wiring
needs, in every mode.
Six specs called SetQueue with 3 of its 4 arguments. v2 accepted that
and filled the gap; v3 answers "expects 4 arguments, got 3".
requested-badge's cleanup read window.go and returned early on
`if (!svc)` — the silent cleanup its own comment was written to
prevent, one migration later. It posts to the runtime endpoint now,
which any page can do.
SearchIndex.Search trusted a startup latch, so rows a spec staged
afterwards were unsearchable and three specs passed only when an
earlier one happened to flip it. shelves.go fixed exactly this and left
hasCatalogRows behind; the search path now uses it as the fallback,
with the latch still the fast path.
Two spec edits are deletions of assertions about v2. harness.spec
checked Object.keys(window.go) and that a bad call *hung*; it now
checks the real runtime is loaded and that the backend rejects with a
TypeError naming the argument. album-actions asserted a tracklist
legend that dcc40b1 deleted on main — that spec has been failing since,
and what replaced it is covered in frontend/test/components.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
4.3 KiB
The harness: event bridge and control surface
Two things ride on top of the headless app. Both exist only in dev builds; neither is reachable from a shipped binary.
The event bridge (.playwright/init-events.js)
Loaded as an initScript by .playwright/cli.config.json and by
e2e/support/fixtures.ts, so an exploratory session and a committed
spec see an identical page. It records every backend event by wrapping
window.wails.EventsNotify — the single choke point all 46 events pass
through, whether or not the app subscribes to them.
window.__yjEvents.wait('LibraryScanComplete', { timeoutMs: 60000 })
window.__yjEvents.names() // name -> count; use this to find out
// what actually fired before asserting
window.__yjEvents.last('QueueChanged')
window.__yjEvents.since(seq)
window.__yjEvents.reset() // drop the buffer
window.__yjEvents.ready(20000) // resolves when a binding round-trips,
// which is later than DOM-ready and true
window.__yjEvents.call('queue.Queue.GetState', [], 5000)
waitresolves against already-buffered events as well as future ones, so there is no race between doing the thing and listening.- Install exactly one recorder. Listeners survive across
evalcalls; a second recorder double-counts. Callreset(), never re-register. calltimes out on purpose. A binding with wrong argument types never fires its callback. A 5 s rejection naming.dev/app.logbeats an infinite hang.
In specs, use the wrappers rather than page.evaluate:
waitForEvent, resetEvents, eventNames, callBinding, and the
app fixture (a page with the bridge installed and the backend
actually answering) from e2e/support/fixtures.ts.
The control surface (backend/testctl, mounted at /__test/)
Gated twice: behind the dev build tag (with a no-op !dev twin) and
behind YJ_TESTCTL=1, which scripts/dev-headless.sh sets and
make dev does not.
| Endpoint | Use |
|---|---|
GET /__test/health |
is this a seeded dev build, and which library |
POST /__test/db/snapshot?name=X |
save the SQLite state |
POST /__test/db/restore?name=X |
put it back (see below) |
POST /__test/emit {name, data} |
force any backend event |
POST /__test/sql {sql, args} |
read rows, or a write count |
TestCtl in e2e/support/fixtures.ts is the typed client.
emitis the fast way to render a push-driven view without staging the work that would produce it — job progress, download progress, scan progress. It callsevents.Deliver, which errors when the event reaches nobody, so a200means it really arrived.restoreis slow (~40 s in the suite) because it copies every table. Prefer snapshotting once and restoring only when a spec genuinely mutates state.
Traps in the config
- The two path keys in
.playwright/cli.config.jsonresolve differently.initScriptis relative to the config file's directory ("init-events.js", not".playwright/init-events.js");outputDiris relative to the shell's cwd. SetoutputDirto".playwright-cli"and runplaywright-clifrom the repo root, or snapshots land somewhere neither.gitignorenor your nextlswill find, and you will read a stale one from a previous session and think a component regressed. snapshotwrites a file, it does not print the tree. The command prints a path underoutputDir; read that. Only the tail is echoed.- Two separate browser caches.
@playwright/test(make e2e-setup) and the Vitest provider (make ui-setup) each download their own Chromium. One working is no guarantee for the other. There used to be a third:playwright-cliwas a required dependency becausescripts/seed-sandbox.shdroveAddLibrarythrough a real page,window.gobeing v2's only way in. v3 answers the same call over HTTP, so the seed iscurlnow and the CLI is only an exploratory convenience. getByRole('button', { name })matches substrings. "Play" also matches "Add queue to playlist"; transport controls needexact: true.e2e/is its own npm package with"type": "module". Without that, Playwright transpiles the specs to CJS and everyimport.metathrows — reported, unhelpfully, as "No tests found".