shipped

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 with backend stopped
    • Given a test that hits /api/tasks/, MSW intercepts the request and returns fixture data
  • 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
  • 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 — one http.get/post/patch/delete per endpoint, returning fixture data from frontend/src/mocks/fixtures/
  • Fixtures: frontend/src/mocks/fixtures/ — one file per domain (e.g. tasks.ts, commitments.ts), typed against src/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

#CaseTypeWhere
1Unit test passes with MSW and backend stoppedfrontendAll existing src/test/*.test.ts files
2Error state unit test (500, 429, timeout)frontendfrontend/src/test/ErrorStates.test.tsx
3MSW handler matches real API shape (type check)frontendtsc --noEmit
4Migration: existing E2E tests still pass with MSW disablede2emake 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 convention
  • frontend/DIAGRAMS.md — §4 API client flow: add note that in test mode, MSW intercepts before Axios

11. Implementation Audit

#ClaimStatusEvidence
1frontend/src/mocks/handlers.ts composes domain handler modules✅ matchhandlers.ts
2frontend/src/mocks/fixtures/deals.ts — typed Deal[] fixture✅ matchfixtures/deals.ts
3frontend/src/mocks/server.ts — setupServer(...handlers) for Vitest✅ matchserver.ts
4frontend/src/test/_shared/setup.ts — beforeAll/afterEach/afterAll lifecycle hooks✅ matchsetup.ts
5msw-handlers.test.ts — handler overrides and fixture shape validation✅ matchfrontend/src/test/_shared/msw-handlers.test.ts
6Component tests no longer require live backend (renderWithProviders helper)✅ matchTest files using renderWithProviders pattern

12. Open Questions / Decisions

  • MSW v2 (fetch-based) handles fetch natively but requires polyfill for Vitest’s node-fetch? Decision: MSW v2 works out of the box with Vitest and Playwright. No polyfill needed — both use modern fetch.
  • 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 --coverage was still using the default fork pool and broad file instrumentation, which let V8 coverage push the process past ~4 GB heap and crash CI with FATAL ERROR: Ineffective mark-compacts near heap limit. frontend/vite.config.ts now detects coverage mode from argv, disables file-level parallelism for coverage runs, caps coverage runs at one fork, narrows instrumentation to src/**/*.{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 + renderWithProviders confirm 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.js generated via npx msw init. frontend/src/mocks/browser.ts created with setupWorker(...handlers). Unit test msw-browser.test.ts added (4 tests). Fixed missing position field in fixtureDeals to match Deal type.