Files
yellowjacket/.pi/skills/yellowjacket-dev/references/ui-tier.md
T
logan 5ca6cad45a
Build & publish Arch package / arch-package (push) Successful in 2m8s
CI / check (push) Failing after 1m56s
CI / e2e (push) Skipped
Search index maintenance / maintain-index (push) Successful in 13s
feat(harness): agent-drivable dev harness and CI that gates
A coding agent could develop this repo's Go packages and could not
develop the application: every path to running YellowJacket ended in a
blocking GTK window, so 265 bound methods, 46 events, 33 component
directories and 13 stores had exactly one form of verification
available — `tsc --noEmit`.

The unlock is that `wails dev`'s dev server on :34115 serves the real
frontend with the real generated bindings against the same Go backend a
desktop window attaches to, so a plain Chromium under Xvfb gets a fully
functional app. Four test tiers now exist, cheapest first:

- `make ui-test` — 313 Vitest tests in a real browser in ~2 s, no app,
  no backend, no display. Works because `frontend/wailsjs/` is a pure
  passthrough to `window.go`/`window.runtime`, so faking just those two
  globals runs the real bindings and the real store code.
- `make test` — services in-process, asserting on the payload the
  frontend would receive, via a new `events.Emit` wrapper.
- `make dev-headless` + `playwright-cli` — the real app, driven
  interactively, with an event bridge on `window.__yjEvents` and a
  dev-only control surface at `/__test/`.
- `make e2e` — 19 of those flows frozen as Playwright specs.

`events.Emit(ctx, …)` replaces all 35 direct `runtime.EventsEmit` call
sites: wails' `getEvents` `log.Fatalf`s on any context without its
runtime, so those paths could not run under test and a background
worker could take the app down. Four packages had each hand-rolled the
same guard; nine more guarded on `ctx != nil`, which does not help.
`TestNoDirectRuntimeEmits` fails the build on a new one.

Fixtures are generated, not committed (`make testdata`), and seeds are
built by *running the app* — never by hand-writing config and DB rows,
which would be a second description of a valid YJ_HOME.

`.gitea/workflows/ci.yml` is the first workflow here that tests
anything; the other three only package, so `gitea_ci` reported only
packaging jobs and misled anyone asking whether a push was healthy.
Both jobs were prototyped to green in a bare ubuntu:24.04 container
before the YAML was written, which immediately caught `make lint`
linting three configurations that nothing builds: all three passes
omitted `webkit2_41`, so wails resolved webkit2gtk-4.0 — which Arch
still ships and Ubuntu 24.04 dropped.

Operational instructions live in `.pi/skills/yellowjacket-dev/`,
measured discoveries in `.planning/NOTES.md`, and architecture in
`CLAUDE.md` — split by tense, not by topic, because a topical split
gives every new fact two plausible homes. `make skill-check` fails a
commit if the skill cites a make target that does not exist.
2026-08-10 23:20:42 -04:00

3.1 KiB

The component and store tier (make ui-test)

313 tests in a real Chromium in ~2 s 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.

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

frontend/wailsjs/ is a pure passthrough — every binding is window.go[svc][Type][Method](args), every runtime call is window.runtime.X(...). So frontend/test/support/wails-fake.ts replaces those two globals and nothing else, and the tests then exercise the real generated bindings and the real store code. No module mocking, and no second description of the Wails layer.

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

The dispatcher mirrors wails' own desktop/events.js, including maxCallbacks expiry and the fact that a frontend EventsEmit notifies local listeners before Go.

Four 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 mergeConfigs 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.

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/wailsjs/ is generated by wails, 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.

make bindings-check   # ~1.5 s, also a pre-commit hook
make bindings         # regenerate for real

The generator rewrites wailsjs/runtime/* as mode 755 every run; that is churn, not drift, and the check ignores it.