Best for
- Use when asked to run e2e tests, write new E2E tests, or debug flaky or failing Playwright tests.
supabase/supabase/.claude/skills/studio-e2e-tests/SKILL.md
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.
Decision brief
Run Playwright end-to-end tests for the Studio application.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/supabase/supabase --skill ".claude/skills/studio-e2e-tests"Inspect the Agent Skill "studio-e2e-tests" from https://github.com/supabase/supabase/blob/9b3e0a9060854e13dba6b123f3c67af1ae618547/.claude/skills/studio-e2e-tests/SKILL.md at commit 9b3e0a9060854e13dba6b123f3c67af1ae618547. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Tests auto-start Supabase local containers via web server config
1. First, run the test locally with pnpm run e2e -- features/.spec.ts (cold start) 2. Check error context in test-results/ directory 3. If you need to inspect UI state, start pnpm dev:studio-local and use Playwright MCP tools 4. Remember: what you see in the dev server may have…
Tests must be run from the e2e/studio directory:
Review the “Run specific file” section in the pinned source before continuing.
Review the “Run with grep filter” section in the pinned source before continuing.
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 88/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 107,562 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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 })
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])
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 debounce
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()
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()
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 })
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' })
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,
})
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)
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 toolsAlternatives
mgiovani/cc-arsenal
Multi-agent review team: architecture, security, performance, testing, style, docs/UX, plus an adversary that cross-examines the other 6, for security-sensitive, architectural, or large PRs (15+ files) where a single-agent pass risks missing cross-cutting issues. Use for auth/payments/PII changes, schema/pattern changes, compliance sign-off, or when asked to 'get the review team on this' / 'multi-agent review' / 'thorough review before merge'. For a standard PR or a quick pre-merge check, use /r
OutlineDriven/odin-claude-plugin
Test and debug browser code with Chrome DevTools MCP. Use when building or debugging browser UI, inspecting the DOM, capturing console errors, analyzing network requests, or verifying visual output.
addyosmani/agent-skills
Tests in real browsers via Chrome DevTools MCP. Use when building or debugging anything that runs in a browser. Use when you need to inspect the DOM, capture console errors, analyze network requests, profile performance, or verify visual output with real runtime data. Requires the chrome-devtools MCP server to be configured.
vercel-labs/agent-browser
Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to "dogfood", "QA", "exploratory test", "find issues", "bug hunt", "test this app/site/platform", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.