Files
yonluandClaude Opus 5 a4ada725a2 feat(wails): rebuild the Vitest fake on v3's transport seam
v2 installed two globals and the fake replaced both. v3 has neither —
the runtime is an npm module and the generated bindings call into it.
What it has instead is better: setTransport() is a public seam for
replacing the IPC transport, and *every* runtime call goes through it,
so the fake is smaller than v2's and covers strictly more.

The event dispatcher is no longer mirrored at all. v2's fake
reimplemented desktop/events.js — the listener list, maxCallbacks
expiry, the reverse iteration — because there was no way to reach the
real one; emit() now goes through window._wails.dispatchWailsEvent,
which is the entry point the backend's own push uses. What is mirrored
instead is one line of Go: how EventManager.Emit packs variadic data
into an event's single data field. Registration and unregistration are
the public Events API. The one non-public thing left is the listener
registry, aliased in vitest.config.mts and used only by
listenerNames() — a test asks whether importing a store subscribed it,
which nothing public can answer.

A binding carries a method ID, not a name, so the fake derives the
ID -> path map from the generated tree: FNV-1a over the FQN, with the
Go type's casing recovered from each package's index.ts, which is the
only place it survives (library/library.ts cannot tell you it is
FrontendUtil). The map has to be complete rather than lazy because 21
assertions read calls() with no argument and compare the whole list.

Two things had to move that are not the fake.

fixture() drains microtasks between two renders: a v3 binding settles
several hops later than v2's, and tests were already written as though
fixture() meant "mounted and loaded". Microtasks and not a timer,
which would hang under the suites that install fake ones.

tracklist-store keeps its defaults on an empty answer instead of
emptying the column list. GetTrackListColumns substitutes
DefaultColumns only when the whole config section is missing; a section
that exists with no columns returns nothing. Until now this was
accidental — the binding was typed Column[], an absent answer arrived
as undefined, and .map threw into the catch.

757 tests pass across all 63 files. They are run in batches: a single
browser session dies partway through the 58 it queues, which reproduces
unchanged at the pre-migration commit and is a resource limit on this
machine rather than anything here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
2026-08-14 19:46:14 -04:00

99 lines
4.5 KiB
Markdown

# The component and store tier (`make ui-test`)
757 tests in a real Chromium with no Wails, no backend, no seeded
library and no virtual display. This is the cheapest coverage available
and where the bulk of UI regression belongs.
```bash
make ui-setup # once: the Vitest provider's own Chromium
make ui-test # behaviour only
make ui-watch
make ui-visual # + toMatchScreenshot baselines (YJ_VISUAL=1)
make ui-visual-update # re-record them
make ui-test UI_ARGS='store/queue' # filter
```
## How it works
Wails v3 routes every runtime call — bindings, event emits, window,
dialogs, clipboard — through one IPC transport, and `setTransport()` is
a public seam for replacing it. So
`frontend/test/support/wails-fake.ts` replaces **that and nothing
else**, and the tests then exercise the *real* generated bindings, the
*real* runtime and the *real* store code. No module mocking, and no
second description of the Wails layer.
A binding call carries a *method ID* (an FNV-1a hash of the Go method's
fully-qualified name), not a name, so the fake derives the ID → path
map from the generated tree at setup: each package's `index.ts`
re-exports its service under the Go type's real name, which is the one
place that casing survives. A path that never maps records as `#<id>`
and fails the assertion naming it.
```ts
emit(Events.QueueChanged, payload); // push a backend event
stub('queue.Queue.GetState', state); // a value, or a function of the args
stubFailure('queue.Queue.SetQueue'); // reject, as a Go error does
calls('queue.Queue.SetQueue'); // what the frontend called back with
lastArgs('queue.Queue.SetQueue');
const el = await fixture('now-playing'); // mount; shadow()/text() query it
```
Delivery is not mirrored — `emit()` goes through the runtime's own
`window._wails.dispatchWailsEvent`, which is the entry point the
backend's push uses, so listener expiry and ordering are the runtime's
real code. What *is* mirrored is one line of Go: how
`EventManager.Emit` packs variadic data into an event's single `data`
field (none is null, one is the value, more is the slice).
A frontend `Events.Emit` no longer notifies local listeners before Go —
v3 calls the backend, which sends the event back out to every window.
The page still sees its own emit, one round trip later rather than
synchronously.
## Five things that will cost you time
- **Store singletons are constructed at module import**, before any test
can stub. `test/setup.ts` therefore carries import-time defaults for
the stores that read config in their constructor. Without one, a store
caches `undefined` where Go would have sent `[]`, and components crash
on `.length` — which reads exactly like a component bug and is not.
Adding a store that reads config on construction means adding its
default there.
- **`vitest.config.mts`, not `.ts`** — it `mergeConfig`s the repo's
`vite.config.mts` to reuse the `@go`/`@store`/`@components` aliases,
and a `.ts` sibling cannot import it.
- **Screenshots need the theme.** The setup file imports
`@store/theme-store` for its side effect (it applies the `--yj-*`
ramp to `:root`); without it a component renders white-on-white and
the baseline is blank.
- **`@lit-labs/virtualizer` never produces two identical frames**, so
`toMatchScreenshot` on `<queue-panel>` fails with "could not capture a
stable screenshot" rather than a diff. Assert on its rows instead.
- **A v3 binding settles several microtasks after a v2 one did** — it
goes through `Call()`, an async `runtimeCallWithID`, the transport and
a `CancellablePromise`, where v2's `window.go` proxy resolved one
promise. `fixture()` drains microtasks between two renders so a
component that loads in `firstUpdated` is loaded when it returns.
Microtasks and not a timer, deliberately: a timer hangs forever under
the suites that install fake ones.
Visual baselines are font-hinting and compositing sensitive, which is
why they are opt-in: they only mean anything on the machine that
recorded them.
## Bindings
`frontend/bindings/` is generated by `wails3`, **not** by `go generate`,
so the pre-commit codegen check does not cover it — a renamed Go bound
method first shows up at runtime, as a call that never settles.
```bash
make bindings-check # ~3.5 s warm, also a pre-commit hook
make bindings # regenerate for real
```
No build tags are passed: the generator is a static analyser that sees
only the configuration it is told about, and the one that matters is
the one users run, which is the default tag set.