Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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:

  1. Svelte 5 runes (ADR-013) made module-scope .svelte.ts state the canonical state layer. Rune reactivity is correct in a real browser, but flaky under jsdom: DOM updates driven by universal *.svelte.ts state do not reliably flush in the simulated DOM, which makes component tests that assert on rendered output brittle and slow to debug.
  2. Vitest 4 stabilized Browser Mode (4.0, then 4.1): real-browser tests with a stable browser config 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.ts extension beside the code they cover (traceability rule), which enables the Svelte rune transform in tests.
  • Assertions use real-DOM locators (page.getByRole, page.getByTestId) and expect.element(...), which auto-retry until the UI settles.
  • Universal *.svelte.ts state requires flushSync(): a test that mutates external module state wraps the mutating call in flushSync(() => ...) (from svelte) before asserting on the DOM.
  • @tauri-apps/api is 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 than vitest-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 in strategy.md and caught in review.
  • vitest-browser-svelte is newer and smaller than the jsdom stack; API details may shift between Vitest minors. The strategy.md config 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)

Was this page helpful?