e2e-studio-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseE2E Studio Tests
Studio应用端到端(E2E)测试
Run Playwright end-to-end tests for the Studio application.
为Studio应用运行Playwright端到端(E2E)测试。
Running Tests
运行测试
Tests must be run from the directory:
e2e/studiobash
cd e2e/studio && pnpm run e2e测试必须在目录下运行:
e2e/studiobash
cd e2e/studio && pnpm run e2eRun specific file
运行指定文件
bash
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.tsbash
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.tsRun with grep filter
使用grep筛选运行
bash
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"bash
cd e2e/studio && pnpm run e2e -- --grep "测试名称匹配模式"UI mode for debugging
调试用UI模式
bash
cd e2e/studio && pnpm run e2e -- --uibash
cd e2e/studio && pnpm run e2e -- --uiEnvironment Setup
环境配置
- Tests auto-start Supabase local containers via web server config
- Self-hosted mode () runs tests in parallel (3 workers)
IS_PLATFORM=false - No manual setup needed for self-hosted tests
- 测试会通过Web服务器配置自动启动Supabase本地容器
- 自托管模式()下测试会并行运行(3个工作进程)
IS_PLATFORM=false - 自托管测试无需手动配置
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, and other helpersref
- 测试文件位于
e2e/studio/features/*.spec.ts - 使用自定义测试工具:
import { test } from '../utils/test.js' - 测试夹具提供、
page等辅助工具ref
Common Patterns
通用模式
Wait for elements with generous timeouts:
typescript
await expect(locator).toBeVisible({ timeout: 30000 })Add messages to expects for debugging:
typescript
await expect(locator).toBeVisible({ timeout: 30000 }, 'Element should be visible after page load')Use serial mode for tests sharing database state:
typescript
test.describe.configure({ mode: 'serial' })为元素设置充足的超时时间等待:
typescript
await expect(locator).toBeVisible({ timeout: 30000 })为断言添加调试信息:
typescript
await expect(locator).toBeVisible({ timeout: 30000 }, '页面加载后元素应可见')对于共享数据库状态的测试,使用串行模式:
typescript
test.describe.configure({ mode: 'serial' })Writing Robust Selectors
编写健壮的选择器
Selector priority (best to worst)
选择器优先级(从优到劣)
-
with accessible name - Most robust, tests accessibility
getByRoletypescriptpage.getByRole('button', { name: 'Save' }) page.getByRole('button', { name: 'Configure API privileges' }) -
- Stable, explicit test hooks
getByTestIdtypescriptpage.getByTestId('table-editor-side-panel') -
with exact match - Good for unique text
getByTexttypescriptpage.getByText('Data API Access', { exact: true }) -
with CSS - Use sparingly, more fragile
locatortypescriptpage.locator('[data-state="open"]')
-
带可访问名称的- 最健壮,同时测试可访问性
getByRoletypescriptpage.getByRole('button', { name: 'Save' }) page.getByRole('button', { name: 'Configure API privileges' }) -
- 稳定、明确的测试钩子
getByTestIdtypescriptpage.getByTestId('table-editor-side-panel') -
精确匹配文本的- 适用于唯一文本
getByTexttypescriptpage.getByText('Data API Access', { exact: true }) -
带CSS的- 谨慎使用,易受DOM变更影响
locatortypescriptpage.locator('[data-state="open"]')
Patterns to avoid
需要避免的模式
-
XPath selectors - Fragile to DOM changestypescript
// BAD locator('xpath=ancestor::div[contains(@class, "space-y")]') -
Parent traversal with- Breaks when structure changes
locator('..')typescript// BAD element.locator('..').getByRole('button') -
Broadon generic elements - May match multiple elements
filter({ hasText })typescript// BAD - popover may have more than one combobox // Could consider scoping down the container or filtering the combobox more specifically popover.getByRole('combobox')
-
XPath选择器 - 易受DOM变更影响typescript
// 不推荐 locator('xpath=ancestor::div[contains(@class, "space-y")]') -
使用遍历父元素 - DOM结构变更时会失效
locator('..')typescript// 不推荐 element.locator('..').getByRole('button') -
对通用元素使用宽泛的- 可能匹配多个元素
filter({ hasText })typescript// 不推荐 - 弹出层可能包含多个下拉框 // 可以考虑缩小容器范围或更精确地筛选下拉框 popover.getByRole('combobox')
Add accessible labels to components
为组件添加可访问标签
When a component lacks a good accessible name, add one in the source code:
tsx
// In the React component
<Button aria-label="Configure API privileges">
<Settings />
</Button>Then use it in tests:
typescript
page.getByRole('button', { name: 'Configure API privileges' })当组件缺少合适的可访问名称时,在源代码中添加:
tsx
// 在React组件中
<Button aria-label="Configure API privileges">
<Settings />
</Button>然后在测试中使用:
typescript
page.getByRole('button', { name: 'Configure API privileges' })Narrowing search scope
缩小搜索范围
Scope selectors to specific containers to avoid matching wrong elements:
typescript
// 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 })将选择器限定到特定容器,避免匹配错误元素:
typescript
// 推荐 - 限定到侧边面板
const sidePanel = page.getByTestId('table-editor-side-panel')
const toggle = sidePanel.getByRole('switch')
// 推荐 - 先找到唯一元素,再缩小范围
const popover = page.locator('[data-radix-popper-content-wrapper]')
const roleSection = popover.getByText('Anonymous (anon)', { exact: true })Avoiding waitForTimeout
waitForTimeout避免使用waitForTimeout
waitForTimeoutNever use - always wait for something specific:
waitForTimeouttypescript
// 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 })永远不要使用 - 始终等待特定的内容:
waitForTimeouttypescript
// 不推荐
await page.waitForTimeout(1000)
// 推荐 - 等待UI元素
await expect(page.getByText('Success')).toBeVisible()
// 推荐 - 等待API响应
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-create')
await saveButton.click()
await apiPromise
// 推荐 - 等待提示框显示操作完成
await expect(page.getByText('Table created successfully')).toBeVisible({ timeout: 15000 })Avoiding force: true
on clicks
force: true避免在点击时使用force: true
force: trueInstead of forcing clicks on hidden elements, make them visible first:
typescript
// BAD
await menuButton.click({ force: true })
// GOOD - hover to reveal, then click
await tableRow.hover()
await expect(menuButton).toBeVisible()
await menuButton.click()不要强制点击隐藏元素,先让元素可见再点击:
typescript
// 不推荐
await menuButton.click({ force: true })
// 推荐 - 悬停显示元素后再点击
await tableRow.hover()
await expect(menuButton).toBeVisible()
await menuButton.click()Debugging
调试
View trace
查看追踪信息
bash
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>bash
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>View HTML report
查看HTML报告
bash
cd e2e/studio && pnpm exec playwright show-reportbash
cd e2e/studio && pnpm exec playwright show-reportError context
错误上下文
Error context files are saved in the directory.
test-results/错误上下文文件保存在目录中。
test-results/Playwright MCP tools
Playwright MCP工具
Use Playwright MCP tools to inspect UI when debugging locally.
本地调试时,使用Playwright MCP工具检查UI。
CI vs Local Development
CI环境与本地开发的区别
The key difference is cold start vs warm state:
核心区别是冷启动 vs 已有状态:
CI (cold start)
CI环境(冷启动)
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.
测试从空白数据库开始。每次测试运行都会重置数据库并启动新容器。默认情况下,pg_cron等扩展未启用。
Local dev with pnpm dev:studio-local
pnpm dev:studio-local使用pnpm dev:studio-local
的本地开发
pnpm dev:studio-localWhen 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.
在本地运行正常但在CI中失败的测试,通常是因为对已有状态存在假设。
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
- 扩展未启用(必须在测试设置中启用)
- 并行测试尝试修改共享状态时出现竞争条件(使用)
test.describe.configure({ mode: 'serial' }) - 由于未设置状态导致页面结构不同,选择器匹配错误元素
Reproducing CI behavior locally
本地重现CI环境行为
The test framework automatically resets the database when running . This matches CI behavior.
pnpm run e2eIf using for Playwright MCP debugging, remember the state differs from CI.
pnpm dev:studio-local运行时,测试框架会自动重置数据库,与CI环境行为一致。
pnpm run e2e如果使用进行Playwright MCP调试,请记住其状态与CI环境不同。
pnpm dev:studio-localDebugging Workflow for CI Failures
CI失败的调试流程
- First, run the test locally with (cold start)
pnpm run e2e -- features/<file>.spec.ts - Check error context in directory
test-results/ - If you need to inspect UI state, start and use Playwright MCP tools
pnpm dev:studio-local - Remember: what you see in the dev server may have state that doesn't exist in CI
- 首先,使用在本地运行测试(冷启动模式)
pnpm run e2e -- features/<file>.spec.ts - 检查目录中的错误上下文
test-results/ - 如果需要检查UI状态,启动并使用Playwright MCP工具
pnpm dev:studio-local - 注意:开发服务器中的状态可能与CI环境中的状态不同