---
name: playground-msw-tests
description: >
REQUIRED and PRIMARY testing approach for packages/playground and packages/playground-ui.
Triggers on: adding or modifying hooks, pages, route components, data-fetching code,
React Query interactions, or any test work in these packages. Generates Vitest tests
that drive the real @mastra/client-js + React Query stack through MSW handlers and
typed fixtures derived from @mastra/client-js response types. This is the #1 way to
test the playground packages — ABOVE Playwright E2E. Use Playwright only for
cross-page user journeys that MSW cannot model.
---
# MSW + client-js Fixtures: Primary Testing Strategy
## Core Principle
**Drive the real transport, mock the network.**
Tests in `packages/playground` and `packages/playground-ui` MUST be written as
Vitest tests that exercise the real `@mastra/client-js` SDK, the real React Query
cache, and the real component/hook code paths. The only seam we mock is the
network boundary, via [MSW](https://mswjs.io/).
This catches contract drift between the playground and `@mastra/client-js` at
typecheck time and at test time — something `vi.mock('@/hooks/...')` style tests
cannot do.
## Priority Order
When you write or refactor a test for these packages, choose in this order:
1. **MSW + typed client-js fixtures (THIS SKILL)** — for hooks, pages, routes,
data-fetching, gating, redirect logic, query/mutation flows, error paths.
2. **Playwright E2E** (`e2e-tests-studio` skill) — only for genuine cross-page
user journeys, real browser concerns (focus/keyboard/viewport), or anything
that requires a real running Mastra server.
3. **Pure unit tests** — only for self-contained utilities/services with no
network, no React Query, no router involvement.
If the same behavior can be covered by both #1 and #2, **prefer #1**. MSW tests
are faster, deterministic, run in CI without browsers, and assert the real wire
contract.
## What NOT to do
- ❌ `vi.mock('@/domains/.../hooks/use-agents')` — mocking our own hooks hides
cache, gating and transport bugs.
- ❌ Inline TypeScript types in tests (`type AgentLite = { id: string }`) —
these drift silently from the real SDK.
- ❌ `as any` / `as unknown as ListAgentsResponse` on fixture data.
- ❌ Returning bespoke shapes from MSW handlers that don't match the real
`@mastra/client-js` response. If a field is optional, include it as optional
in the fixture, don't omit the type.
## What TO do
- ✅ Put fixtures in a `__tests__/fixtures/` folder next to the test file.
- ✅ Type every fixture with a response type re-exported from `@mastra/client-js`
(e.g. `ListStoredAgentsResponse`, `GetAgentResponse`, `BuilderSettingsResponse`,
`GetToolResponse`, `GetWorkflowResponse`, `ListStoredSkillsResponse`).
- ✅ Register MSW handlers per test with `server.use(...)` so handlers reset
between tests via the global `afterEach`.
- ✅ Render through `MastraReactProvider` + `QueryClientProvider` + `MemoryRouter`
so the real client SDK is the transport.
- ✅ Use `vi.fn()` wrappers inside MSW handlers to assert which endpoints were
hit (great for testing `enabled: ...` gating without mocking hooks).
## Standard Test Skeleton
```tsx
// @vitest-environment jsdom
import { MastraReactProvider } from '@mastra/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { cleanup, render, screen } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { MemoryRouter } from 'react-router';
import { afterEach, describe, expect, it } from 'vitest';
import { server } from '@/test/msw-server';
import { Subject } from '../subject';
import { happyPathResponse } from './fixtures/subject';
const BASE_URL = 'http://localhost:4111';
const renderSubject = () => {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
,
);
};
afterEach(() => cleanup());
describe('Subject', () => {
it('renders the happy path', async () => {
server.use(http.get(`${BASE_URL}/api/agents`, () => HttpResponse.json(happyPathResponse)));
renderSubject();
expect(await screen.findByText('Expected behavior')).not.toBeNull();
});
});
```
## Standard Fixture File
```ts
// packages/playground/src/.../__tests__/fixtures/subject.ts
import type { ListStoredAgentsResponse } from '@mastra/client-js';
export const emptyStoredAgents: ListStoredAgentsResponse = {
agents: [],
total: 0,
page: 1,
perPage: 50,
hasMore: false,
};
export const oneDraftAgent: ListStoredAgentsResponse = {
...emptyStoredAgents,
agents: [
{
id: 'agent-1',
name: 'Draft Agent',
instructions: '',
model: { provider: 'openai', name: 'gpt-4o-mini' },
status: 'draft',
// ...other required fields from StoredAgentResponse
},
],
total: 1,
};
```
**If a required field on the SDK response type is missing from your fixture,
that's a real test failure — fix the fixture, never `as any` it.**
## Existing Infrastructure to Reuse
- `packages/playground/src/test/msw-server.ts` — exports `server`. The global
`vitest.setup.ts` calls `server.listen({ onUnhandledRequest: 'error' })`,
`server.resetHandlers()` after each test, and `server.close()` at the end.
This means **unhandled requests fail tests loudly** — that's the contract.
- `packages/playground/vitest.setup.ts` — already wires MSW lifecycle, jsdom
polyfills (`matchMedia`, `Element.prototype.scrollTo`), so test files just
add their own `server.use(...)` per case.
## Recipes
### Test loading state without mocking hooks
Defer the MSW handler's resolution with a promise gate:
```ts
const gate = (() => {
let resolve: () => void = () => {};
const promise = new Promise(r => {
resolve = r;
});
return { promise, resolve };
})();
server.use(
http.get(`${BASE_URL}/api/stored/agents`, async () => {
await gate.promise;
return HttpResponse.json(emptyStoredAgents);
}),
);
renderSubject();
expect(screen.getByTestId('spinner')).not.toBeNull();
gate.resolve();
await waitFor(() => expect(screen.queryByTestId('spinner')).toBeNull());
```
### Assert that a query is gated (`enabled: false`)
Wrap the handler in a `vi.fn` and assert it was never called:
```ts
const onAgents = vi.fn<() => void>();
server.use(
http.get(`${BASE_URL}/api/agents`, () => {
onAgents();
return HttpResponse.json(emptyAgents);
}),
);
renderSubject(); // user has no write access → hook should not fire
await new Promise(resolve => setTimeout(resolve, 50));
expect(onAgents).not.toHaveBeenCalled();
```
### Assert per-feature-flag gating
Use a single MSW handler per endpoint and one `vi.fn` per endpoint, then
toggle the builder-settings response to flip features on/off and assert which
handlers were hit.
### Vary response by query string
Read `request.url` inside the handler:
```ts
server.use(
http.get(`${BASE_URL}/api/stored/agents`, ({ request }) => {
const status = new URL(request.url).searchParams.get('status');
return HttpResponse.json(status === 'draft' ? oneDraftAgent : emptyStoredAgents);
}),
);
```
### Thin component seams that ARE acceptable to mock
You may replace these with very thin stubs:
- A heavy child component that has its OWN dedicated test (e.g. mock
`AgentBuilderStarter` to ``).
- `react-router`'s `Navigate` so you can assert the redirect target instead
of letting it actually navigate.
- `Button` / `Spinner` style atoms from `@mastra/playground-ui` only when the
real component requires more global context than the test needs.
**Never** mock our own data hooks, services, or auth gating logic — drive those
through their real implementations against MSW.
## Verification Checklist
Before considering a test done:
- [ ] All fixtures import a type from `@mastra/client-js` and have no `as any`
/ `as unknown as` casts.
- [ ] No `vi.mock` of `@/domains/.../hooks/*`, `@/domains/.../services/*`,
`@mastra/client-js`, or `@mastra/react`.
- [ ] `pnpm --filter ./packages/playground typecheck` passes — proves fixtures
conform to the live SDK shape.
- [ ] The new test runs in isolation AND as part of the package suite without
`onUnhandledRequest: 'error'` failures.
- [ ] Coverage for the file under test is at the target level (usually 100%
for hooks/pages, since MSW makes every branch reachable).
## When to Reach for Playwright Instead
Reach for `e2e-tests-studio` only when at least one is true:
- The behavior spans multiple pages with real navigation/history.
- The behavior requires a real Mastra server (streaming, workflow execution,
real model providers).
- The behavior is fundamentally a browser concern (drag-drop, focus traps,
viewport, file uploads).
Everything else — fetching, caching, redirects, gating, optimistic updates,
error states, empty states, pagination, search params — belongs in this skill.