Spec: F67 — MSW (Mock Service Worker for Frontend Testing)
Status: shipped
1. Problem & CEO Job
Trust the signal (job 3) — frontend unit and E2E tests depend on a running, seeded Django backend. They cannot run offline; error states (500, 429, timeout, malformed response) require backend changes; network calls slow them; and CI must start the full stack.
MSW intercepts HTTP fetch/XMLHttpRequest at the network level inside the test browser, so frontend tests are fast, deterministic, and independent. This directly improves the CEO’s signal quality because error-handling code actually gets tested.
2. User Stories & Acceptance Criteria
-
As a developer, when I run frontend unit tests, I want them to work without a backend, so that I can test offline and in CI without spinning up Django.
- Given
pnpm test, all frontend unit tests pass withbackendstopped - Given a test that hits
/api/tasks/, MSW intercepts the request and returns fixture data
- Given
-
As a developer, when I write a test for an error state, I want to return a 500 / 429 / timeout from the handler, so that the error UI is verifiable.
- Given a handler returning
HttpResponse.json({ detail: 'Error' }, { status: 500 }), the component shows its error state - Given a handler that never responds (timeout), TanStack Query shows loading then error state
- Given a handler returning
-
As a developer, when I write E2E tests, I want to optionally mock the API for deterministic scenarios, so that I avoid flaky tests caused by seed data drift.
- Given an E2E test with MSW enabled, all API calls go through MSW handlers instead of the real backend
3. Scope Boundaries
- In: Installation (
msw),frontend/src/mocks/handlers.ts— one handler per domain,frontend/src/mocks/server.ts— test setup for Vitest + Playwright, migration of existing unit tests to use MSW - Out: MSW in production builds (it’s dev-only by design), mocking every single endpoint (start with the most-used), integration with Storybook (not used in this project)
4. Data Model
n/a — fixtures are plain TypeScript objects matching src/types/.
5. API Contract
MSW handlers mirror the real API contract. When the API changes, the handler must be updated (same PR — CI catches drift when handler and test disagree).
6. UI / UX
- Handlers:
frontend/src/mocks/handlers.ts— onehttp.get/post/patch/deleteper endpoint, returning fixture data fromfrontend/src/mocks/fixtures/ - Fixtures:
frontend/src/mocks/fixtures/— one file per domain (e.g.tasks.ts,commitments.ts), typed againstsrc/types/ - Test setup:
frontend/src/mocks/server.ts—setupServer(...handlers)for Vitest;setupWorker(...handlers)for Playwright - Inject into tests:
beforeAll(() => server.listen())/afterEach(() => server.resetHandlers())/afterAll(() => server.close()) - Per-test overrides:
server.use(http.get('/api/tasks/', () => HttpResponse.json([], { status: 500 }))) - i18n: n/a
7. Test Plan
| # | Case | Type | Where |
|---|---|---|---|
| 1 | Unit test passes with MSW and backend stopped | frontend | All existing src/test/*.test.ts files |
| 2 | Error state unit test (500, 429, timeout) | frontend | frontend/src/test/ErrorStates.test.tsx |
| 3 | MSW handler matches real API shape (type check) | frontend | tsc --noEmit |
| 4 | Migration: existing E2E tests still pass with MSW disabled | e2e | make e2e |
8. Current Behavior
Shipped in source. frontend/src/mocks/handlers.ts composes domain handler modules, while
browser.ts and server.ts provide browser and Node setup. The shared Vitest setup wires the
server lifecycle and server.use() supports per-test overrides. main.tsx enables the browser
worker only with VITE_ENABLE_MOCKS=true; default E2E remains backend-backed. Runtime execution
and the timeout acceptance criterion remain explicit gaps in the Contract Graph.
9. Error States
- Handler mismatch: TypeScript catches stale fixtures (fixture type ≠ API response type) at compile time
- Unhandled request: MSW logs a warning in dev but doesn’t crash the test — the test may fail with unexpected data
10. Diagrams & Docs Touched
frontend/AGENTS.md— add MSW testing conventionfrontend/DIAGRAMS.md— §4 API client flow: add note that in test mode, MSW intercepts before Axios
11. Implementation Audit
| # | Claim | Status | Evidence |
|---|---|---|---|
| 1 | frontend/src/mocks/handlers.ts composes domain handler modules | ✅ match | handlers.ts |
| 2 | frontend/src/mocks/fixtures/deals.ts — typed Deal[] fixture | ✅ match | fixtures/deals.ts |
| 3 | frontend/src/mocks/server.ts — setupServer(...handlers) for Vitest | ✅ match | server.ts |
| 4 | frontend/src/test/_shared/setup.ts — beforeAll/afterEach/afterAll lifecycle hooks | ✅ match | setup.ts |
| 5 | msw-handlers.test.ts — handler overrides and fixture shape validation | ✅ match | frontend/src/test/_shared/msw-handlers.test.ts |
| 6 | Component tests no longer require live backend (renderWithProviders helper) | ✅ match | Test files using renderWithProviders pattern |
12. Open Questions / Decisions
- MSW v2 (fetch-based) handles
fetchnatively but requires polyfill for Vitest’snode-fetch? Decision: MSW v2 works out of the box with Vitest and Playwright. No polyfill needed — both use modernfetch. - Fixtures: hand-written vs generated from OpenAPI? Decision: Hand-written for now (30 fixtures). If we reach 100+, generate from
openapi/schema.yaml.
13. Bug / Change Log
- 2026-06-17 — Frontend coverage OOM fix.
vitest --coveragewas still using the default fork pool and broad file instrumentation, which let V8 coverage push the process past ~4 GB heap and crash CI withFATAL ERROR: Ineffective mark-compacts near heap limit.frontend/vite.config.tsnow detects coverage mode from argv, disables file-level parallelism for coverage runs, caps coverage runs at one fork, narrows instrumentation tosrc/**/*.{ts,tsx}, excludes tests and mocks from coverage collection, and lowers coverage result processing concurrency so CI surfaces real failures instead of dying in tinypool IPC teardown. - 2026-07-02 — Spec drift fix (batch10). Checked ACs 1–3 (§2) per §11 audit evidence:
server.ts+ lifecycle hooks +renderWithProvidersconfirm unit tests run without backend (rows 3/4/6); handlers + fixtures confirm MSW interception (rows 1/2);server.use()override tests confirm 500-error-state pattern (row 5). Left AC 4 (timeout) and AC 5 (E2E browser worker) unchecked — §11 has no explicit ✅ row for either. - 2026-06-13 (T5-D r04): Playwright E2E MSW worker integration done.
frontend/public/mockServiceWorker.jsgenerated vianpx msw init.frontend/src/mocks/browser.tscreated withsetupWorker(...handlers). Unit testmsw-browser.test.tsadded (4 tests). Fixed missingpositionfield in fixtureDeals to matchDealtype.