with one click
e2e-studio-tests
// Run e2e tests in the Studio app. Use when asked to run e2e tests, run studio tests, playwright tests, or test the feature.
// Run e2e tests in the Studio app. Use when asked to run e2e tests, run studio tests, playwright tests, or test the feature.
PostHog event tracking standards for Supabase Studio. Use when reviewing PRs for telemetry compliance or implementing new event tracking. Covers event naming, property conventions, approved patterns, and implementation guide.
Testing strategy for Supabase Studio. Use when writing tests, deciding what type of test to write, extracting logic from components into testable utility functions, or reviewing test coverage. Covers unit tests, component tests, and E2E test selection criteria.
| name | e2e-studio-tests |
| description | Run e2e tests in the Studio app. Use when asked to run e2e tests, run studio tests, playwright tests, or test the feature. |
Run Playwright end-to-end tests for the Studio application.
Tests must be run from the e2e/studio directory:
cd e2e/studio && pnpm run e2e
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
cd e2e/studio && pnpm run e2e -- --ui
IS_PLATFORM=false) runs tests in parallel (3 workers)e2e/studio/features/*.spec.tsimport { test } from '../utils/test.js'page, ref, and other helpersWait 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' })
getByRole with accessible name - Most robust, tests accessibility
page.getByRole('button', { name: 'Save' })
page.getByRole('button', { name: 'Configure API privileges' })
getByTestId - Stable, explicit test hooks
page.getByTestId('table-editor-side-panel')
getByText with exact match - Good for unique text
page.getByText('Data API Access', { exact: true })
locator with CSS - Use sparingly, more fragile
page.locator('[data-state="open"]')
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')
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' })
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 })
waitForTimeoutNever 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()
await apiPromise
// GOOD - wait for toast indicating operation complete
await expect(page.getByText('Table created successfully')).toBeVisible({ timeout: 15000 })
force: true on clicksInstead of forcing clicks on hidden elements, make them visible first:
// BAD
await menuButton.click({ force: true })
// GOOD - hover to reveal, then click
await tableRow.hover()
await expect(menuButton).toBeVisible()
await menuButton.click()
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>
cd e2e/studio && pnpm exec playwright show-report
Error context files are saved in the test-results/ directory.
Use Playwright MCP tools to inspect UI when debugging locally.
The key difference is cold start vs warm state:
Tests run from a blank database slate. Each test run resets the database and starts fresh containers. Extensions like pg_cron are NOT enabled by default.
pnpm dev:studio-localWhen debugging with a running dev server, the database may already have state from previous runs (extensions enabled, test data present).
Tests that work locally but fail in CI often have assumptions about existing state.
test.describe.configure({ mode: 'serial' }))The test framework automatically resets the database when running pnpm run e2e. This matches CI behavior.
If using pnpm dev:studio-local for Playwright MCP debugging, remember the state differs from CI.
pnpm run e2e -- features/<file>.spec.ts (cold start)test-results/ directorypnpm dev:studio-local and use Playwright MCP tools