ADR-017: Frontend component testing — Vitest Browser Mode with `vitest-browser-svelte`
ADR-017: Frontend component testing — Vitest Browser Mode with vitest-browser-svelte
Status
Accepted
Context
frontend-architecture.md §13 (as written before this ADR) specified component
and integration tests with @testing-library/svelte + @testing-library/jest-dom
running under jsdom. Two things changed since that line was written:
- Svelte 5 runes (ADR-013) made module-scope
.svelte.tsstate the canonical state layer. Rune reactivity is correct in a real browser, but flaky under jsdom: DOM updates driven by universal*.svelte.tsstate do not reliably flush in the simulated DOM, which makes component tests that assert on rendered output brittle and slow to debug. - Vitest 4 stabilized Browser Mode (4.0, then 4.1): real-browser tests
with a stable
browserconfig option, headless chromium, auto-retrying DOM assertions, and a dedicated Svelte renderer (vitest-browser-svelte) that supports runes in test files (.svelte.test.ts) and in components.
The state layer (*.svelte.ts runes) remains unit-testable in plain Node under
Vitest with the IPC bridge mocked — that part of §13 is unchanged. The decision
is about the component and integration rows.
Decision
Component and integration tests run in Vitest Browser Mode using
vitest-browser-svelte against real chromium:
- Test files use the
.svelte.test.tsextension beside the code they cover (traceability rule), which enables the Svelte rune transform in tests. - Assertions use real-DOM locators (
page.getByRole,page.getByTestId) andexpect.element(...), which auto-retry until the UI settles. - Universal
*.svelte.tsstate requiresflushSync(): a test that mutates external module state wraps the mutating call influshSync(() => ...)(fromsvelte) before asserting on the DOM. @tauri-apps/apiis mocked at the module boundary; component tests never touch real IPC.@testing-library/svelte+ jsdom is not the primary path anymore. It may be used only for a rare non-rune component where browser mode adds no value; it is not the default and does not supersede this policy.- Node-project Vitest remains the runner for the state layer (
*.svelte.ts), exactly as before.
The full layer table, FR traceability matrix, and vitest.config.ts shape live
in docs/internal/testing/strategy.md.
Alternatives considered
- Keep jsdom +
@testing-library/svelte+ jest-dom. The previously documented path. Still functional for simple components, but runes-driven DOM updates from universal state are unreliable under jsdom, and the ecosystem’s Svelte 5 guidance has moved to browser mode. - Playwright Component Testing (
@playwright/test). Real browser, but the Svelte 5 rune story is less complete thanvitest-browser-svelte, it splits component tests into a second framework alongside Vitest, and it does not share the state-layer runner. - WebdriverIO component tests. Same objection (separate framework, no runes-first Svelte renderer) plus it is the E2E tool reserved for the deferred WebDriver path.
Consequences
Positive:
- Rune-correct DOM assertions — tests exercise the real reactivity model the app ships with (ADR-013), catching timing/reactivity bugs jsdom misses.
- One runner (Vitest) covers state (node project) and components (browser
project); CI is one command (
pnpm test). - Auto-retrying assertions and real-browser rendering reduce flaky, sleep-based component tests.
Negative:
- Heavier CI: a chromium download/install and a browser runtime per run; slower than jsdom for trivial cases.
flushSync()discipline is required around universal-state mutations — forgetting it yields a test that passes locally after a retry but is timing dependent. Enforced by convention instrategy.mdand caught in review.vitest-browser-svelteis newer and smaller than the jsdom stack; API details may shift between Vitest minors. Thestrategy.mdconfig shape is pinned at first scaffold, not locked here.
References
frontend-architecture.md§13 (test layers + traceability, superseded tooling rows),docs/internal/testing/strategy.md(this policy’s detail)- ADR-013 (state management — runes; the reason browser mode is needed)
- ADR-012 (room lifecycle — state hoisting consumed by tests), ADR-014 (typed
IPC bridge — the mocked seam), ADR-015 (design tokens — what
ui/primitives render against)