Skip to content

UI testing: Vitest browser mode, Playwright E2E, fake TTS backend - #1015

Draft
jamiepine wants to merge 10 commits into
release/v0.6.0from
ui-testing
Draft

UI testing: Vitest browser mode, Playwright E2E, fake TTS backend#1015
jamiepine wants to merge 10 commits into
release/v0.6.0from
ui-testing

Conversation

@jamiepine

Copy link
Copy Markdown
Owner

UI testing for the app: Vitest browser mode for components, Playwright E2E against a real backend, CI jobs to run it all. 29 vitest tests + 4 E2E specs, all green.

Bug fixes shaken out by making things testable

  • CapturesTab exports (wav/txt/md) called plugin-dialog/plugin-fs directly — broken in the web build. Now routed through platform.filesystem.saveFile, which returns the saved path (null on cancel) so the toast logic stays honest:
const dest = await platform.filesystem.saveFile(name, blob, [{ name: 'Audio', extensions: ['wav'] }]);
if (dest) exportToastSuccess(dest);
  • Duplicate useAutoUpdater.ts/.tsx: Vite resolves the .ts, which accepts showToast but never implements it — the update-available toast never rendered. The .ts is gone, so this PR enables that toast for the first time.
  • Both entries built their own QueryClient while serverStore invalidates the singleton from lib/queryClient — switching server URL never refreshed data. Both entries now render with the singleton.
  • i18n init only worked because lib/utils/format.ts happens to import it; both entries now import @/i18n explicitly.
  • Dead code removed: orphaned generated OpenAPI client, unrouted AudioTab (675 lines), app/'s standalone entry. app/ is now purely a shared library.

Component tests (Vitest 4, browser mode)

Two projects in one root config: unit (happy-dom) for stores/utils, browser (real Chromium via the Playwright provider) for components. Real EventSource/AudioContext/canvas — no jsdom polyfills. Naming: *.test.ts runs in node, *.browser.test.tsx runs in Chromium.

Harness under app/src/test/: renderWithProviders/renderRoute (real route tree over memory history), createMockPlatform (every platform method a spy), typed MSW fixtures + per-domain handler factories, and an SSE controller that streams real text/event-stream bodies through MSW:

const sse = sseController();
worker.use(http.get('*/generate/:id/status', () => sse.response()));
sse.push({ data: { status: 'completed' } });

Suites: App startup state machine, ChordPicker, HistoryTable row actions, FloatingGenerateBox mounted through the real root layout (submit → pending → SSE completion → history refetch is the actual wiring), uiStore/serverStore.

E2E (Playwright + real backend + fake TTS)

e2e/ drives the built web/ bundle against a per-worker uvicorn — each worker gets its own port and temp data dir, so SQLite is parallel-safe. VOICEBOX_FAKE_TTS=1 short-circuits get_tts_backend_for_engine to a backend that synthesizes a sine tone sized to the text: the real task queue, SSE progress, history rows, and audio serving all run, only inference is fake. The generate spec goes from typing text to a completed row serving a playable WAV.

backend/requirements-ci.txt is the slim CPU-only set validated to boot the app on a runner.

CI

quality (typecheck + web build) and unit (vitest) block PRs. e2e and backend-tests (the 30 existing pytest files, first time in CI) run with continue-on-error until they prove stable, then flip to blocking.

Running locally

bun run test                    # vitest, needs node >= 20 (.node-version added)
bunx playwright test -c e2e     # needs a python env: uv venv .venv-ci && uv pip install -r backend/requirements-ci.txt

Notes: biome is not in CI — pre-existing format drift repo-wide would fail it immediately; needs a one-time check:fix commit first. Tauri-only surfaces (updater install, GPU switching, global shortcut dictation) stay manual — WKWebView has no WebDriver, so true in-app E2E on macOS isn't possible.

The wav/txt/md export handlers called plugin-dialog and plugin-fs
directly, which breaks in the web build. saveFile now returns the
saved path (null on cancel) so callers can toast accurately, and the
cross-window capture listeners only register under Tauri.
Both useAutoUpdater.ts and .tsx existed; Vite resolves .ts first,
which accepts showToast but never implements it — so the update toast
App.tsx asks for never rendered. The .tsx variant is the full
implementation and has the same signature.
Each entry built its own QueryClient while serverStore invalidates the
singleton from lib/queryClient — so switching server URL never
refreshed data. Both entries now render with the singleton. i18n init
was only reachable transitively through lib/utils/format.ts; import it
directly in both entries.
Removes the orphaned generated OpenAPI client (superseded by the
hand-written client.ts), the unrouted AudioTab, and app/'s unused
standalone entry (main.tsx, index.html, vite.config) — app/ is a
shared library consumed by web/ and tauri/. routeTree is exported so
tests can build routers over memory history.
Two projects in one root config: 'unit' (happy-dom) for stores, hooks,
and utils, and 'browser' (real Chromium via the playwright provider)
for component tests — no jsdom polyfills for EventSource, AudioContext,
or canvas. Harness pieces: createMockPlatform (spy-able Platform with
an updater emit handle), renderWithProviders (fresh QueryClient with
polling disabled + PlatformProvider), MSW worker with baseline health
handler, and a store-reset teardown covering all eight zustand stores.
Seed tests cover uiStore theme, serverStore invalidation, and an
AboutPage render in Chromium.

Requires node >= 20 (.node-version added); run with bun run test.
…te render

Fixture builders match the hand-written API types with deterministic
ids. Handlers are per-domain factories tests compose via worker.use;
unstubbed requests fail loudly. sseController streams real
text/event-stream bodies through MSW into EventSource, so generation
and download progress are testable without touching the transport.
renderRoute mounts the real route tree over memory history. Query
clients drain on teardown so in-flight fetches can't leak past handler
reset, and the browser project pre-bundles app deps so a cold dep-
optimizer cache can't reload mid-run.
e2e/ drives the web build (vite preview) against a per-worker uvicorn:
each Playwright worker boots its own backend on its own port with a
temp cwd, so SQLite and audio files are fully isolated and parallel-
safe. The page fixture seeds the persisted voicebox-server store with
the worker's URL before any script runs.

VOICEBOX_FAKE_TTS=1 short-circuits get_tts_backend_for_engine to a
backend that synthesizes a sine tone sized to the text — the real task
queue, SSE progress, history rows, and audio serving all run, only
inference is fake. backend/requirements-ci.txt is the slim dependency
set validated to boot the app on a CPU-only runner.

Specs: startup, settings layout, seeded profile in /voices, and
generate-to-completed-audio through the full pipeline.
quality keeps typecheck + web build and uploads the bundle. unit runs
vitest (node + Chromium browser projects) and blocks PRs. e2e boots
the CPU backend with fake TTS and runs Playwright against the preview
build — informational until it has a sustained green run, then flip
continue-on-error off. backend-tests runs the existing pytest suite
for the first time in CI, also informational.
App: non-Tauri skips the startup gate, Tauri dev reaches the router
with close-handler/updater wiring, ?view=dictate renders the pill
without booting the main app (branches behind import.meta.env.PROD are
unreachable under vitest and stay uncovered). ChordPicker: chord
capture via raw KeyboardEvents with explicit codes, peak-set
semantics, save/cancel. HistoryTable: rows, empty state, player-store
autoplay intent, favorite/delete round trips, export through
platform.filesystem.saveFile. FloatingGenerateBox: mounted via the
real index route so submit → pending → SSE completion → history
refetch runs the actual RootLayout wiring.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b76c24d2-7eeb-41b4-9372-1c8c226727cc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant