/studio-e2e-tests
Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
$ npx -y skills add supabase/supabase --skill studio-e2e-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-e2e-tests
Context preview
The summary Claude sees to decide when to auto-load this skill.
Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
SKILL.md
studio-e2e-tests.SKILL.mdname: studio-e2e-tests
description: Write and run Playwright E2E tests for Supabase Studio (e2e/studio).
Use when asked to run e2e tests, write new E2E tests, or debug flaky or failing
Playwright tests. Covers running commands, avoiding race conditions, waiting
strategies, selectors, helper functions, and CI vs local differences.
E2E Studio Tests
Run Playwright end-to-end tests for the Studio application.
Running Tests
Tests must be run from the `e2e/studio` directory:
cd e2e/studio && pnpm run e2e
Run specific file
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
Run with grep filter
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
UI mode for debugging
cd e2e/studio && pnpm run e2e -- --ui
Environment Setup
- Tests auto-start Supabase local containers via web server config
- Self-hosted mode (`IS_PLATFORM=false`) runs tests in parallel (3 workers)
- No manual setup needed for self-hosted tests
Test File Structure
- Tests are in `e2e/studio/features/*.spec.ts`
- Use custom test utility: `import { test } from '../utils/test.js'`
- Test fixtures provide `page`, `ref`, and other helpers
Common Patterns
Wait for elements with generous timeouts:
await expect(locator).toBeVisible({ timeout: 30000 })Add messages to expects for debugging:
await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')Use serial mode for tests sharing database state:
test.describe.configure({ mode: 'serial' })Writing Robust Selectors
Selector priority (best to worst)
1. **`getByRole` with accessible name** - Most robust, tests accessibility
page.getByRole('button', { name: 'Save' })
page.getByRole('button', { name: 'Configure API privileges' })2. **`getByTestId`** - Stable, explicit test hooks
page.getByTestId('table-editor-side-panel')3. **`getByText` with exact match** - Good for unique text
page.getByText('Data API access', { exact: true })4. **`locator` with CSS** - Use sparingly, more fragile
page.locator('[data-state="open"]')Patterns to avoid
- **XPath selectors** - Fragile to DOM changes
// BAD
locator('xpath=ancestor::div[contains(@class, "space-y")]')- **Parent traversal with `locator('..')`** - Breaks when structure changes
// BAD
element.locator('..').getByRole('button')- **Broad `filter({ hasText })` on generic elements** - May match multiple elements
// BAD - popover may have more than one combobox
// Could consider scoping down the container or filtering the combobox more specifically
popover.getByRole('combobox')Add accessible labels to components
When a component lacks a good accessible name, add one in the source code:
// In the React component
<Button aria-label="Configure API privileges">
<Settings />
</Button>
Then use it in tests:
page.getByRole('button', { name: 'Configure API privileges' })Narrowing search scope
Scope selectors to specific containers to avoid matching wrong elements:
// Good - scoped to side panel
const sidePanel = page.getByTestId('table-editor-side-panel')
const toggle = sidePanel.getByRole('switch')
// Good - find unique element, then scope from there
const popover = page.locator('[data-radix-popper-content-wrapper]')
const roleSection = popover.getByText('Anonymous (anon)', { exact: true })Avoiding Race Conditions
**Set up API waiters BEFORE triggering actions.** This is the most common source of flaky tests.
// ❌ Race condition — response may complete before waiter is set up
await page.getByRole('button', { name: 'Save' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
// ✅ Waiter is ready before the action
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await page.getByRole('button', { name: 'Save' }).click()
await apiPromiseSame rule applies before navigation:
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromiseWhen an action triggers multiple API calls, wait for all of them:
const createTablePromise = waitForApiResponseWithTimeout(page, (r) =>
r.url().includes('query?key=table-create')
)
const tablesPromise = waitForApiResponseWithTimeout(page, (r) =>
r.url().includes('tables?include_columns=true')
)
await page.getByRole('button', { name: 'Save' }).click()
await Promise.all([createTablePromise, tablesPromise])Waiting Strategies
Playwright auto-waits for elements to be actionable — prefer this over manual timeouts.
Use `expect.poll` for dynamic state changes:
await expect.poll(async () => await page.getByLabel(`View ${tableName}`).count()).toBe(0)Use `waitForSelector` with state for element lifecycle:
await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })Avoid `networkidle` — use specific API waits instead:
// ❌ Unreliable and slow
await page.waitForLoadState('networkidle')
// ✅ Specific API response
await waitForApiResponse(page, 'pg-meta', ref, 'tables')Timeouts are acceptable only for client-side debounces:
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // allow debounceAvoiding `waitForTimeout`
Never use `waitForTimeout` - always wait for something specific:
// BAD
await page.waitForTimeout(1000)
// GOOD - wait for UI element
await expect(page.getByText('Success')).toBeVisible()
// GOOD - wait for API response
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await saveButton.click()
awRead more
name: studio-e2e-tests description: Write and run Playwright E2E tests for Supabase Studio (e2e/studio). Use when asked to run e2e tests, write new E2E tests, or debug flaky or failing Playwright tests. Covers running commands, avoiding race conditions, waiting strategies, selectors, helper functions, and CI vs local differences.
E2E Studio Tests
Run Playwright end-to-end tests for the Studio application.
Running Tests
Tests must be run from the `e2e/studio` directory:
cd e2e/studio && pnpm run e2e
Run specific file
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
Run with grep filter
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
UI mode for debugging
cd e2e/studio && pnpm run e2e -- --ui
Environment Setup
- Tests auto-start Supabase local containers via web server config
- Self-hosted mode (`IS_PLATFORM=false`) runs tests in parallel (3 workers)
- No manual setup needed for self-hosted tests
Test File Structure
- Tests are in `e2e/studio/features/*.spec.ts`
- Use custom test utility: `import { test } from '../utils/test.js'`
- Test fixtures provide `page`, `ref`, and other helpers
Common Patterns
Wait for elements with generous timeouts:
await expect(locator).toBeVisible({ timeout: 30000 })Add messages to expects for debugging:
await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')Use serial mode for tests sharing database state:
test.describe.configure({ mode: 'serial' })Writing Robust Selectors
Selector priority (best to worst)
1. **`getByRole` with accessible name** - Most robust, tests accessibility
page.getByRole('button', { name: 'Save' })
page.getByRole('button', { name: 'Configure API privileges' })2. **`getByTestId`** - Stable, explicit test hooks
page.getByTestId('table-editor-side-panel')3. **`getByText` with exact match** - Good for unique text
page.getByText('Data API access', { exact: true })4. **`locator` with CSS** - Use sparingly, more fragile
page.locator('[data-state="open"]')Patterns to avoid
- **XPath selectors** - Fragile to DOM changes
// BAD
locator('xpath=ancestor::div[contains(@class, "space-y")]')- **Parent traversal with `locator('..')`** - Breaks when structure changes
// BAD
element.locator('..').getByRole('button')- **Broad `filter({ hasText })` on generic elements** - May match multiple elements
// BAD - popover may have more than one combobox
// Could consider scoping down the container or filtering the combobox more specifically
popover.getByRole('combobox')Add accessible labels to components
When a component lacks a good accessible name, add one in the source code:
// In the React component <Button aria-label="Configure API privileges"> <Settings /> </Button>
Then use it in tests:
page.getByRole('button', { name: 'Configure API privileges' })Narrowing search scope
Scope selectors to specific containers to avoid matching wrong elements:
// Good - scoped to side panel
const sidePanel = page.getByTestId('table-editor-side-panel')
const toggle = sidePanel.getByRole('switch')
// Good - find unique element, then scope from there
const popover = page.locator('[data-radix-popper-content-wrapper]')
const roleSection = popover.getByText('Anonymous (anon)', { exact: true })Avoiding Race Conditions
**Set up API waiters BEFORE triggering actions.** This is the most common source of flaky tests.
// ❌ Race condition — response may complete before waiter is set up
await page.getByRole('button', { name: 'Save' }).click()
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
// ✅ Waiter is ready before the action
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await page.getByRole('button', { name: 'Save' }).click()
await apiPromiseSame rule applies before navigation:
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromiseWhen an action triggers multiple API calls, wait for all of them:
const createTablePromise = waitForApiResponseWithTimeout(page, (r) =>
r.url().includes('query?key=table-create')
)
const tablesPromise = waitForApiResponseWithTimeout(page, (r) =>
r.url().includes('tables?include_columns=true')
)
await page.getByRole('button', { name: 'Save' }).click()
await Promise.all([createTablePromise, tablesPromise])Waiting Strategies
Playwright auto-waits for elements to be actionable — prefer this over manual timeouts.
Use `expect.poll` for dynamic state changes:
await expect.poll(async () => await page.getByLabel(`View ${tableName}`).count()).toBe(0)Use `waitForSelector` with state for element lifecycle:
await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })Avoid `networkidle` — use specific API waits instead:
// ❌ Unreliable and slow
await page.waitForLoadState('networkidle')
// ✅ Specific API response
await waitForApiResponse(page, 'pg-meta', ref, 'tables')Timeouts are acceptable only for client-side debounces:
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // allow debounceAvoiding `waitForTimeout`
Never use `waitForTimeout` - always wait for something specific:
// BAD
await page.waitForTimeout(1000)
// GOOD - wait for UI element
await expect(page.getByText('Success')).toBeVisible()
// GOOD - wait for API response
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await saveButton.click()
awSupabase 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

