In Challenge 21 you wrote unit tests for synchronous components. Real apps also need tests that cover the full async lifecycle: loading states, success renders, error states, and mutations that refetch data.
In this challenge you will wire up MSW (Mock Service Worker) for Node so that the
Vitest test environment intercepts fetch calls, then write async integration tests
for the pages and hooks that depend on those API calls.
- Configure MSW for the Node/jsdom environment (
setupServer, notsetupWorker) - Test the loading state – the spinner is visible while the fetch is in flight
- Test the success state – projects render after MSW responds
- Test the error state – override a handler to return 500 and verify the error UI plus retry button
- Test mutation + cache invalidation – create a task, verify it appears optimistically, verify the server data is eventually loaded
cd start/
npm install
npm testThe tests in src/components/__tests__/ from Challenge 21 still pass.
Your job is to make the new async tests in src/pages/__tests__/ and
src/components/__tests__/TaskMutations.test.tsx pass by completing the TODOs.
| File | What to do |
|---|---|
src/mocks/server.ts |
Create the MSW Node server with setupServer |
src/test/setup.ts |
Add beforeAll, afterEach, afterAll lifecycle hooks |
src/test/utils.tsx |
Update renderWithProviders to return queryClient and support route params |
src/pages/__tests__/ProjectsLayout.test.tsx |
Write async tests (loading / success / error / retry) |
src/pages/__tests__/ProjectDetailPanel.test.tsx |
Write async tests for project detail |
src/components/__tests__/TaskMutations.test.tsx |
Write mutation + optimistic-update test |
setupWorker |
setupServer |
|
|---|---|---|
| Where | Browser (Service Worker) | Node (http-interceptor) |
| Used in | main.tsx (dev mode) |
Test files via setup.ts |
| Import | msw/browser |
msw/node |
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers()) // clear per-test overrides
afterAll(() => server.close())server.use(
http.get('/api/projects', () => new HttpResponse(null, { status: 500 }))
)server.resetHandlers() in afterEach removes these overrides automatically.
| Helper | When to use |
|---|---|
findBy* |
Single element, waits up to timeout |
waitFor |
Arbitrary assertion, polls until truthy |
queryBy* |
Checking absence (returns null, never throws) |
Each call to renderWithProviders creates a new QueryClient with
retry: false and gcTime: 0 so:
- Failed queries fail immediately (no retries)
- Cache entries are never shared between tests
queryClientis returned so tests can inspect/seed the cache
ProjectsLayoutrenders<Outlet />— in tests you may render the component directly inside a<Routes>with a path, or use thepathoption inrenderWithProviders.- The loading spinner has
role="status"— usegetByRole('status'). ErrorMessagerenders "Something went wrong" and a Retry button — usefindByText(/something went wrong/i)andgetByRole('button', { name: /retry/i }).- For the mutation test, seed the QueryClient cache with the project data before rendering so the detail panel loads instantly without a network round-trip.