
studio-e2e-tests
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.
- Installs
- 221
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/supabase/supabase --skill studio-e2e-tests- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads studio-e2e-tests automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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 })
Use serial mode for tests sharing database state:
test.describe.configure({ mode: 'serial' })
Writing Robust Selectors
Selector priority (best to worst)
-
getByRolewith accessible name - Most robust, tests accessibilitypage.getByRole('button', { name: 'Save' }) page.getByRole('button', { name: 'Configure API privileges' }) -
getByTestId- Stable, explicit test hookspage.getByTestId('table-editor-side-panel') -
getByTextwith exact match - Good for unique textpage.getByText('Data API access', { exact: true }) -
locatorwith CSS - Use sparingly, more fragilepage.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 apiPromise
Same rule applies before navigation:
const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise
When 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')
The only acceptable use of waitForTimeout is a client-side debounce:
await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // allow debounce
Avoiding waitForTimeout
Never use waitForTimeout to wait for UI or network — always wait for something specific (the debounce case above is the sole exception):
// 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 })
Avoiding force: true on clicks
Instead 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()
Test Structure
Always import from the custom test utility:
import { test } from '../utils/test.js'
Use withFileOnceSetup for expensive setup that should run once per file:
test.beforeAll(async ({ browser, ref }) => {
await withFileOnceSetup(import.meta.url, async () => {
const ctx = await browser.newContext()
const page = await ctx.newPage()
await deleteTestTables(page, ref)
})
})
test.afterAll(async () => {
await releaseFileOnceCleanup(import.meta.url)
})
Dismiss toasts before interacting — they can overlay buttons:
const dismissToastsIfAny = async (page: Page) => {
const closeButtons = page.getByRole('button', { name: 'Close toast' })
const count = await closeButtons.count()
for (let i = 0; i < count; i++) {
await closeButtons.nth(i).click()
}
}
await dismissToastsIfAny(page)
await page.getByRole('button', { name: 'New table' }).click()
Assertions
Always include descriptive messages for easier debugging:
// ❌ No context on failure
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible()
// ✅ Clear message on failure
await expect(
page.getByRole('button', { name: 'Save' }),
'Save button should be visible after form is filled'
).toBeVisible()
Use explicit timeouts for slow operations:
await expect(
page.getByText(`Table ${tableName} is good to go!`),
'Success toast should be visible after table creation'
).toBeVisible({ timeout: 50000 })
Helper Functions
Extract reusable operations into domain helpers (e.g. e2e/studio/utils/storage-helpers.ts).
Use the existing wait utilities:
import {
createApiResponseWaiter,
waitForApiResponse,
waitForGridDataToLoad,
waitForTableToLoad,
} from '../utils/wait-for-response.js'
Use expectClipboardValue instead of manual clipboard reads with hardcoded timeouts:
// ❌ Brittle
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)
// ✅ Uses Playwright auto-retries
await expectClipboardValue({ page, value: 'expectedValue' })
API Mocking
await page.route('*/**/logs.all*', async (route) => {
await route.fulfill({ body: JSON.stringify(mockAPILogs) })
})
Use soft waits for optional API calls:
await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', {
soft: true,
fallbackWaitMs: 1000,
})
Cleanup
Clean up test data in beforeAll/beforeEach. Check before deleting to handle existing state gracefully:
const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
if ((await bucketRow.count()) === 0) return
// proceed with deletion
Reset local storage after tests that modify it:
import { resetLocalStorage } from '../utils/reset-local-storage.js'
await resetLocalStorage(page, ref)
Debugging
View trace
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>
View HTML report
cd e2e/studio && pnpm exec playwright show-report
Error context
Error context files are saved in the test-results/ directory.
Playwright MCP tools
Use Playwright MCP tools to inspect UI when debugging locally.
CI vs Local Development
The key difference is cold start vs warm state:
CI (cold start)
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.
Local dev with pnpm dev:studio-local
When debugging with a running dev server, the database may already have state from previous runs (extensions enabled, test data present).
Handling Cold Start Bugs
Tests that work locally but fail in CI often have assumptions about existing state.
Common issues
- Extension not enabled (must enable in test setup)
- Race conditions when parallel tests try to modify shared state (use
test.describe.configure({ mode: 'serial' })) - Locators matching wrong elements because the page structure differs when state isn't set up
Reproducing CI behavior locally
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.
Debugging Workflow for CI Failures
- First, run the test locally with
pnpm run e2e -- features/<file>.spec.ts(cold start) - Check error context in
test-results/directory - If you need to inspect UI state, start
pnpm dev:studio-localand use Playwright MCP tools - Remember: what you see in the dev server may have state that doesn't exist in CI
FAQ
What does the studio-e2e-tests skill do?
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.
How do I install studio-e2e-tests?
Run npx skills add https://github.com/supabase/supabase --skill studio-e2e-tests in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/studio-e2e-tests/ (personal) or .claude/skills/studio-e2e-tests/ (project).
Which AI agents can use studio-e2e-tests?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is studio-e2e-tests safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains studio-e2e-tests?
It is published by supabase in supabase/supabase, and it is an official skill from the maker of the technology it covers. It has 221 recorded installs.
More from supabase/supabase
vitest
supabase/supabase
Vitest API and config reference (Jest-compatible) — mocking with vi., spies, fake timers, coverage configuration, fixtures, snapshots, and test filtering. Use for Vitest API and configuration questions anywhere in the monorepo; for Studio-specific test strategy and component-test setup, start with studio-testing and studio-mock-api-tests.
vercel-composition-patterns
supabase/supabase
React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.
studio-testing
supabase/supabase
Open the skill to read its SKILL.md instructions.
telemetry-standards
supabase/supabase
Open the skill to read its SKILL.md instructions.
studio-error-handling
supabase/supabase
Open the skill to read its SKILL.md instructions.
studio-ui-patterns
supabase/supabase
Open the skill to read its SKILL.md instructions.
Popular testing & qa skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
grilling
mattpocock/skills
Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
migrate-to-shoehorn
mattpocock/skills
Migrate test files from as type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace as in tests, or needs partial test data.
run-train
lllllllama/rigorpilot-skills
Rigor Train skill for deep learning research repositories. Use when a documented or selected training command should be run conservatively for startup verification, short-run verification, full kickoff, or resume, with command, config, seed, log, checkpoint, status, and metric evidence written to standardized trainoutputs/. Do not use for environment setup, exploratory sweeps, speculative idea implementation, or end-to-end orchestration.
test-driven-development
obra/superpowers
Use when implementing any feature or bugfix, before writing implementation code
finishing-a-development-branch
obra/superpowers
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work