diff --git a/.claude/skills/studio-mock-api-tests/SKILL.md b/.claude/skills/studio-mock-api-tests/SKILL.md new file mode 100644 index 00000000000..bd09ea125f6 --- /dev/null +++ b/.claude/skills/studio-mock-api-tests/SKILL.md @@ -0,0 +1,294 @@ +--- +name: studio-mock-api-tests +description: Component tests for Supabase Studio that mock API requests at the + network layer with MSW. Use when writing or reviewing a component test that + exercises a React Query hook or mutation, or when migrating an existing + test away from vi.mock('@/data/...'). Covers the customRender + addAPIMock + template and the jsdom/MSW gotchas that cost real debugging time. +--- + +# Studio MSW component tests + +Mount a Studio component, intercept its network calls with MSW, assert +what renders and what gets sent. The infrastructure is already wired up — +this skill is the working template plus the gotchas. + +## When to use + +- The component (or any descendant it renders) calls a React Query hook + or mutation that hits `/platform/...`, `/v1/...`, or another endpoint + in `apps/studio/data/api.d.ts`. +- You'd otherwise be tempted to write `vi.mock('@/data/some-query', ...)`. + **Don't.** Mock the network instead — see "Why not vi.mock" below. + +If the component is purely presentational with no data fetching, you +don't need MSW; render and assert directly. + +## The template + +```tsx +import { fireEvent, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { describe, expect, test, vi } from 'vitest' + +import { MyComponent } from './MyComponent' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +// Needed if the component renders inside a Sheet, Modal, Popover, or +// anything else built on Radix that uses Web Animations. +mockAnimationsApi() + +describe('MyComponent', () => { + test('renders rows from the API', async () => { + addAPIMock({ + method: 'get', + path: '/platform/organizations', + response: () => + HttpResponse.json([ + { + /* ... */ + }, + ]), + }) + + customRender() + + expect(await screen.findByText('Acme')).toBeInTheDocument() + }) +}) +``` + +That's the whole pattern. Server lifecycle (`listen`/`resetHandlers`/ +`close`) is handled by `apps/studio/tests/vitestSetup.ts` — handlers +registered via `addAPIMock` are scoped to the current test. + +## Gotchas that will eat your afternoon + +### 1. Path params use `:slug`, not `{slug}` + +`addAPIMock` is typed from the OpenAPI `paths`, but path params are +remapped to MSW's `:param` format. Autocomplete will guide you, but if +typecheck reports the path isn't assignable, you're using the OpenAPI +`{slug}` form. + +```ts +// ❌ TypeScript error, MSW won't match +path: '/platform/organizations/{slug}/projects' + +// ✅ Correct +path: '/platform/organizations/:slug/projects' +``` + +### 2. Use `HttpResponse.json`, not `new HttpResponse` + +For success responses, always go through `HttpResponse.json` — even for +204/201-no-content endpoints. A raw `new HttpResponse(null, { status: 201 })` +returns no content-type, and `openapi-fetch` can hang the mutation flow, +which silently breaks `onSuccess` callbacks. + +```ts +// ❌ Mutation onSuccess silently never fires +response: () => new HttpResponse(null, { status: 201 }) + +// ✅ Works (pass the OpenAPI body shape explicitly — see gotcha #8) +response: () => HttpResponse.json({}, { status: 201 }) +``` + +### 3. Submit buttons in Sheets/Modals need `fireEvent.click` + +The convention `