Layered
Three layers, not one
Unit tests cannot catch routing bugs; e2e cannot catch every hook edge case.
UI Test Strategy
Three test layers, each earning its keep: Vitest for pure logic, Testing Library for component rendering, Playwright for end-to-end against the real Compose stack. No mock service workers. Anything HTTP-shaped is e2e.
3
test layers
No MSW
no mock drift
Chromium + WebKit
e2e browsers
flowchart LR unit["Unit<br/>Vitest<br/>no backend"] component["Component<br/>Testing Library<br/>no backend"] e2e["e2e<br/>Playwright<br/>real stack"] visual["Visual<br/>Playwright<br/>per-platform"] unit --> component --> e2e --> visual
Four test layers in order of cost and scope: Vitest units (no backend) feed into Testing Library component tests (no backend), which give way to Playwright end-to-end against the real Compose stack, and finally per-platform Playwright visual snapshots.
Layered
Unit tests cannot catch routing bugs; e2e cannot catch every hook edge case.
No drift
Hand-written mocks drift from the real backend; HTTP-shaped tests run e2e instead.
Real stack
Catches API-shape drift and integration regressions without any extra scaffolding.
Safari behavior bugs surface in CI, not from a user report.
macOS vs Linux font rendering differs; baselines committed per OS.
Numbers only count files you can meaningfully test.
Location: src/**/*.test.ts colocated with source. Run: bun run test.
Location: e2e/*.spec.ts. Run: bun run e2e (needs dev stack up).
Location: e2e/visual.spec.ts-snapshots/. Run: bun run e2e:visual:update to
refresh.
bun run test:ci.Unit and component tests focus on pure logic; anything HTTP-shaped is e2e against the real backend. Three reasons:
Component test: render the component, assert on the rendered output. Don’t reach into hook internals; hooks have their own test if they’re complex enough to need one.
Hook test: renderHook from Testing Library; assert on the returned view object (the IXxxView shape). Standard pattern for testing a component’s logic without rendering the UI.
E2E test: navigate, interact, assert. Use Playwright’s page-object pattern under e2e/pages/ for anything reused across specs. Baseline visual diffs live in e2e/visual.spec.ts-snapshots/.
Usually one of three things:
bun run e2e:visual:update on a matching machine, or regenerate baselines in CI itself.setTimeout-based assumptions.bun run generate:api wasn’t run. CI catches this via the schema diff check.@boring-stack-pkg/eslint-plugin-test-conventions enforces tests/ mirrors src/ and that every test file has a real source file behind it. No orphan tests, no source files without tests for the things that need them.
vitest.config.ts · playwright.config.ts · e2e/ on GitHub.