/studio-mock-api-tests
Component tests for Supabase Studio that mock API requests at the
$ npx -y skills add supabase/supabase --skill studio-mock-api-tests --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/studio-mock-api-tests
Context preview
The summary Claude sees to decide when to auto-load this skill.
Component tests for Supabase Studio that mock API requests at the
SKILL.md
studio-mock-api-tests.SKILL.mdname: 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
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<OrganizationResponse[]>([
{
/* ... */
},
]),
})
customRender(<MyComponent />)
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.
// ❌ 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.
// ❌ Mutation onSuccess silently never fires
response: () => new HttpResponse(null, { status: 201 })
// ✅ Works (pass the OpenAPI body shape explicitly — see gotcha #8)
response: () => HttpResponse.json<MyResponse>({}, { status: 201 })3. Submit buttons in Sheets/Modals need `fireEvent.click`
The convention `<Button form={FORM_ID} type="submit" />` (button outside the form, associated by id) doesn't reliably trigger submission under `userEvent.click` in jsdom. Use `fireEvent.click` for the submit button. Continue to use `userEvent.type` for inputs.
await userEvent.type(screen.getByPlaceholderText('value'), 'hello')
fireEvent.click(await screen.findByRole('button', { name: 'Save' }))4. Profile-gated queries need a `profileContext`
Many hooks (`useOrganizationsQuery`, anything in `data/projects/`, anything that calls `useProfile`) refuse to fire until a profile is loaded. Pass one explicitly:
import type { ProfileContextType } from '@/lib/profile'
const PROFILE_CONTEXT: ProfileContextType = {
profile: {
id: 1,
auth0_id: 'auth0|test',
gotrue_id: 'gotrue-test',
username: 'testuser',
primary_email: 'test@example.com',
first_name: null,
last_name: null,
mobile: null,
is_alpha_user: false,
is_sso_user: false,
disabled_features: [],
free_project_limit: null,
},
error: null,
isLoading: false,
isError: false,
isSuccess: true,
}
customRender(<MyComponent />, { profileContext: PROFILE_CONTEXT })5. `useParams` is globally mocked to `{ ref: 'default' }`
You don't need to mock the Next router for project-scoped components. Just use `'default'` as the project ref in your mock paths: `/v1/projects/default/secrets`, `/platform/projects/default/...`. If you need a different ref, override with `routerMock.setCurrentUrl(...)` (see `apps/studio/tests/lib/route-mock.ts`).
6. Unhandled requests fail loudly — mock every endpoint a render triggers
`mswServer.listen({ onUnhandledRequest: 'error' })` is set globally. If a component (or any child it renders) fires an unmocked request, you'll see MSW errors in stderr and likely flaky behavior. Cards, lists, and details panels often fire nested queries (e.g. `OrganizationCard` calls `useOrgProjectsInfiniteQuery`) — read what the rendered subtree does and mock all of it, or stub it with `vi.mock` for nested components only.
7. Don't put query strings in the handler `path`
`addAPIMock` accepts `?foo=bar` suffixes via `TrimQueryParams`, but the helper strips them before matching. MSW v2 doesn't match query params via path strings — read them inside the resolver instead:
addAPIMock({
method: 'get',
path: '/platform/projects',
response: ({ request }) => {
const limit = new URL(request.url).searchParams.get('limit')
// ...
},
})8. Always pass an explicit generic to `HttpResponse.json`
`add
Read more
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
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<OrganizationResponse[]>([
{
/* ... */
},
]),
})
customRender(<MyComponent />)
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.
// ❌ 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.
// ❌ Mutation onSuccess silently never fires
response: () => new HttpResponse(null, { status: 201 })
// ✅ Works (pass the OpenAPI body shape explicitly — see gotcha #8)
response: () => HttpResponse.json<MyResponse>({}, { status: 201 })3. Submit buttons in Sheets/Modals need `fireEvent.click`
The convention `<Button form={FORM_ID} type="submit" />` (button outside the form, associated by id) doesn't reliably trigger submission under `userEvent.click` in jsdom. Use `fireEvent.click` for the submit button. Continue to use `userEvent.type` for inputs.
await userEvent.type(screen.getByPlaceholderText('value'), 'hello')
fireEvent.click(await screen.findByRole('button', { name: 'Save' }))4. Profile-gated queries need a `profileContext`
Many hooks (`useOrganizationsQuery`, anything in `data/projects/`, anything that calls `useProfile`) refuse to fire until a profile is loaded. Pass one explicitly:
import type { ProfileContextType } from '@/lib/profile'
const PROFILE_CONTEXT: ProfileContextType = {
profile: {
id: 1,
auth0_id: 'auth0|test',
gotrue_id: 'gotrue-test',
username: 'testuser',
primary_email: 'test@example.com',
first_name: null,
last_name: null,
mobile: null,
is_alpha_user: false,
is_sso_user: false,
disabled_features: [],
free_project_limit: null,
},
error: null,
isLoading: false,
isError: false,
isSuccess: true,
}
customRender(<MyComponent />, { profileContext: PROFILE_CONTEXT })5. `useParams` is globally mocked to `{ ref: 'default' }`
You don't need to mock the Next router for project-scoped components. Just use `'default'` as the project ref in your mock paths: `/v1/projects/default/secrets`, `/platform/projects/default/...`. If you need a different ref, override with `routerMock.setCurrentUrl(...)` (see `apps/studio/tests/lib/route-mock.ts`).
6. Unhandled requests fail loudly — mock every endpoint a render triggers
`mswServer.listen({ onUnhandledRequest: 'error' })` is set globally. If a component (or any child it renders) fires an unmocked request, you'll see MSW errors in stderr and likely flaky behavior. Cards, lists, and details panels often fire nested queries (e.g. `OrganizationCard` calls `useOrgProjectsInfiniteQuery`) — read what the rendered subtree does and mock all of it, or stub it with `vi.mock` for nested components only.
7. Don't put query strings in the handler `path`
`addAPIMock` accepts `?foo=bar` suffixes via `TrimQueryParams`, but the helper strips them before matching. MSW v2 doesn't match query params via path strings — read them inside the resolver instead:
addAPIMock({
method: 'get',
path: '/platform/projects',
response: ({ request }) => {
const limit = new URL(request.url).searchParams.get('limit')
// ...
},
})8. Always pass an explicit generic to `HttpResponse.json`
`add
Supabase is the Postgres development platform. We're building the features of Firebase using enterprise-grade open source tools. [x] Hosted Postgres Database. Docs [x] Authentication and Authorization. Docs [x] Auto-generated APIs. [x] REST. Docs [x] GraphQL.
Repo: supabase/supabase
Other skills on supabase.
- /clickhouse-logs-queries
Write, review, and migrate Supabase logs queries against the ClickHouse-backed `logs` table (the `logs.all.otel` analytics endpoint). Use this whenever a task involves Logs Explorer SQL, the `log_attributes` map, querying a log `source` (edge_logs, postgres_logs, auth_logs,
Open skill - /copywriting
Write or audit UI copy (buttons, labels, empty states, error messages, tooltips, form text) anywhere in the monorepo. Load it before shipping or reviewing any user-facing text — including when copy is incidental to the task, like a new feature that adds buttons, toasts, dialogs,
Open skill - /dev-toolbar-review
Safety rules for the dev toolbar, PostHog client, and feature flags. Use
Open skill - /docs-content
Write, edit, organize, and review Supabase content anywhere in apps/docs — guides, explainers, tutorials, troubleshooting entries, reference docs, and partials. Use for MDX/TOML authoring, frontmatter, navigation, terminology, links, code samples, content listings, and docs
Open skill - /react-hook-form
Correct React Hook Form usage anywhere in the monorepo — data flow, subscriptions,
Open skill - /safe-sql-execution
Use whenever code will build, return, fetch, or execute SQL that runs against a user's real Postgres database — even when the request reads like an ordinary feature or bug fix and never says "security," "injection," or "SafeSqlFragment." This covers: writing or editing any
Open skill

