# 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.
