e2e-studio-tests

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

E2E 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
e2e/studio
directory:
bash
cd e2e/studio && pnpm run e2e
测试必须在
e2e/studio
目录下运行:
bash
cd e2e/studio && pnpm run e2e

Run specific file

运行指定文件

bash
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
bash
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts

Run 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 -- --ui
bash
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
  • 测试会通过Web服务器配置自动启动Supabase本地容器
  • 自托管模式(
    IS_PLATFORM=false
    )下测试会并行运行(3个工作进程)
  • 自托管测试无需手动配置

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
  • 测试文件位于
    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)

选择器优先级(从优到劣)

  1. getByRole
    with accessible name
    - Most robust, tests accessibility
    typescript
    page.getByRole('button', { name: 'Save' })
    page.getByRole('button', { name: 'Configure API privileges' })
  2. getByTestId
    - Stable, explicit test hooks
    typescript
    page.getByTestId('table-editor-side-panel')
  3. getByText
    with exact match
    - Good for unique text
    typescript
    page.getByText('Data API Access', { exact: true })
  4. locator
    with CSS
    - Use sparingly, more fragile
    typescript
    page.locator('[data-state="open"]')
  1. 带可访问名称的
    getByRole
    - 最健壮,同时测试可访问性
    typescript
    page.getByRole('button', { name: 'Save' })
    page.getByRole('button', { name: 'Configure API privileges' })
  2. getByTestId
    - 稳定、明确的测试钩子
    typescript
    page.getByTestId('table-editor-side-panel')
  3. 精确匹配文本的
    getByText
    - 适用于唯一文本
    typescript
    page.getByText('Data API Access', { exact: true })
  4. 带CSS的
    locator
    - 谨慎使用,易受DOM变更影响
    typescript
    page.locator('[data-state="open"]')

Patterns to avoid

需要避免的模式

  • XPath selectors - Fragile to DOM changes
    typescript
    // BAD
    locator('xpath=ancestor::div[contains(@class, "space-y")]')
  • Parent traversal with
    locator('..')
    - Breaks when structure changes
    typescript
    // BAD
    element.locator('..').getByRole('button')
  • Broad
    filter({ hasText })
    on generic elements
    - May match multiple elements
    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")]')
  • 使用
    locator('..')
    遍历父元素
    - DOM结构变更时会失效
    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

Never use
waitForTimeout
- always wait for something specific:
typescript
// 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 })
永远不要使用
waitForTimeout
- 始终等待特定的内容:
typescript
// 不推荐
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

Instead 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-report
bash
cd e2e/studio && pnpm exec playwright show-report

Error context

错误上下文

Error context files are saved in the
test-results/
directory.
错误上下文文件保存在
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
的本地开发

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.
在本地运行正常但在CI中失败的测试,通常是因为对已有状态存在假设。

Common issues

常见问题

  1. Extension not enabled (must enable in test setup)
  2. Race conditions when parallel tests try to modify shared state (use
    test.describe.configure({ mode: 'serial' })
    )
  3. Locators matching wrong elements because the page structure differs when state isn't set up
  1. 扩展未启用(必须在测试设置中启用)
  2. 并行测试尝试修改共享状态时出现竞争条件(使用
    test.describe.configure({ mode: 'serial' })
  3. 由于未设置状态导致页面结构不同,选择器匹配错误元素

Reproducing CI behavior locally

本地重现CI环境行为

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
时,测试框架会自动重置数据库,与CI环境行为一致。
如果使用
pnpm dev:studio-local
进行Playwright MCP调试,请记住其状态与CI环境不同。

Debugging Workflow for CI Failures

CI失败的调试流程

  1. First, run the test locally with
    pnpm run e2e -- features/<file>.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 state that doesn't exist in CI
  1. 首先,使用
    pnpm run e2e -- features/<file>.spec.ts
    在本地运行测试(冷启动模式)
  2. 检查
    test-results/
    目录中的错误上下文
  3. 如果需要检查UI状态,启动
    pnpm dev:studio-local
    并使用Playwright MCP工具
  4. 注意:开发服务器中的状态可能与CI环境中的状态不同