mcpField
S
studio-e2e-tests

studio-e2e-tests

github.com/supabase测试与质量114.3k221 次安装今天更新
代码仓库

关于 studio-e2e-tests

一份用于运行和维护 Studio 应用 Playwright 端到端测试的技能指南,重点介绍可靠的选择器、针对性等待,以及复现 CI 冷启动状态。

主要功能

  1. 01运行完整或筛选后的 E2E 测试
  2. 02使用 Playwright UI 模式调试
  3. 03采用可访问且范围明确的选择器
  4. 04在操作前设置 API 等待
  5. 05复现 CI 冷启动状态

适用场景

  1. 01本地运行 Studio 功能测试
  2. 02排查不稳定的 UI 或 API 测试
  3. 03编写可靠的选择器和断言
  4. 04复现仅在 CI 出现的故障

MCP 相关

指南提及使用 Playwright MCP 工具,在本地调试时检查 UI。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/supabase/supabase --skill studio-e2e-tests

安装后在 Claude Code 中输入/studio-e2e-tests确认已加载。

详细介绍

E2E Studio 测试

运行 Studio 应用的 Playwright 端到端测试。

运行测试

必须在 e2e/studio 目录中运行测试:

cd e2e/studio && pnpm run e2e
运行指定文件
cd e2e/studio && pnpm run e2e -- features/cron-jobs.spec.ts
使用 grep 筛选器运行
cd e2e/studio && pnpm run e2e -- --grep "test name pattern"
使用 UI 模式进行调试
cd e2e/studio && pnpm run e2e -- --ui

环境设置

  • 测试会通过 web server 配置自动启动 Supabase 本地容器
  • 可自托管模式(IS_PLATFORM=false)下,测试会并行运行(3 个 worker)
  • 可自托管测试无需手动设置

测试文件结构

  • 测试位于 e2e/studio/features/*.spec.ts
  • 使用自定义测试工具:import { test } from '../utils/test.js'
  • 测试夹具提供 page、ref 和其他辅助工具

常见模式

使用较长的超时时间等待元素:

await expect(locator).toBeVisible({ timeout: 30000 })

共享数据库状态的测试使用串行模式:

test.describe.configure({ mode: 'serial' })

编写稳健的选择器

选择器优先级(从优到劣)
  1. 带可访问名称的 getByRole - 最稳健,可测试无障碍性

    page.getByRole('button', { name: 'Save' })
    page.getByRole('button', { name: 'Configure API privileges' })
    
  2. getByTestId - 稳定、明确的测试钩子

    page.getByTestId('table-editor-side-panel')
    
  3. 精确匹配的 getByText - 适用于唯一文本

    page.getByText('Data API access', { exact: true })
    
  4. 使用 CSS 的 locator - 谨慎使用,比较脆弱

    page.locator('[data-state="open"]')
    
应避免的模式
  • XPath 选择器 - 容易因 DOM 变更而失效

    // BAD
    locator('xpath=ancestor::div[contains(@class, "space-y")]')
    
  • 使用 locator('..') 遍历父级 - 结构变更时会失效

    // BAD
    element.locator('..').getByRole('button')
    
  • 在通用元素上使用范围过宽的 filter({ hasText }) - 可能匹配多个元素

    // BAD - popover may have more than one combobox
    // Could consider scoping down the container or filtering the combobox more specifically
    popover.getByRole('combobox')
    
为组件添加无障碍标签

当组件缺少合适的无障碍名称时,请在源代码中添加:

// In the React component
<Button aria-label="Configure API privileges">
  <Settings />
</Button>

然后在测试中使用:

page.getByRole('button', { name: 'Configure API privileges' })
缩小搜索范围

将选择器限定在特定容器内,以避免匹配到错误的元素:

// 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 })

避免竞态条件

在触发操作之前设置 API 等待器。 这是测试不稳定最常见的原因。

// ❌ 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

导航之前也适用同一规则:

const loadPromise = waitForTableToLoad(page, ref)
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
await loadPromise

当某个操作会触发多个 API 调用时,请等待它们全部完成:

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 会自动等待元素变为可操作状态 — 优先使用此功能,而不是手动设置超时。

使用 expect.poll 处理动态状态变化:

await expect.poll(async () => await page.getByLabel(`View ${tableName}`).count()).toBe(0)

使用带状态参数的 waitForSelector 处理元素生命周期:

await page.waitForSelector('[data-testid="side-panel"]', { state: 'detached' })

避免使用 networkidle — 改用特定的 API 等待:

// ❌ Unreliable and slow
await page.waitForLoadState('networkidle')

// ✅ Specific API response
await waitForApiResponse(page, 'pg-meta', ref, 'tables')

waitForTimeout 唯一可接受的用途是客户端防抖:

await page.getByRole('textbox').fill('search term')
await page.waitForTimeout(300) // allow debounce

避免使用 waitForTimeout

绝不要使用 waitForTimeout 等待 UI 或网络 — 始终等待特定事件(上文的防抖情况是唯一例外):

// 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

不要强行点击隐藏元素,而应先让它们显示出来:

// BAD
await menuButton.click({ force: true })

// GOOD - hover to reveal, then click
await tableRow.hover()
await expect(menuButton).toBeVisible()
await menuButton.click()

测试结构

始终从自定义测试工具导入:

import { test } from '../utils/test.js'

对于每个文件只需运行一次的开销较大的设置,请使用 withFileOnceSetup:

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)
})

交互前先关闭提示消息 — 它们可能会遮挡按钮:

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()

断言

始终附上描述性消息,以便更轻松地调试:

// ❌ 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()

对于耗时较长的操作,请明确指定超时时间:

await expect(
  page.getByText(`Table ${tableName} is good to go!`),
  'Success toast should be visible after table creation'
).toBeVisible({ timeout: 50000 })

辅助函数

将可重复使用的操作提取为领域辅助函数(例如 e2e/studio/utils/storage-helpers.ts)。 使用现有的等待工具:

import {
  createApiResponseWaiter,
  waitForApiResponse,
  waitForGridDataToLoad,
  waitForTableToLoad,
} from '../utils/wait-for-response.js'

使用 expectClipboardValue,不要手动读取剪贴板并使用硬编码的超时时间:

// ❌ Brittle
await page.evaluate(() => navigator.clipboard.readText())
await page.waitForTimeout(500)

// ✅ Uses Playwright auto-retries
await expectClipboardValue({ page, value: 'expectedValue' })

API 模拟

await page.route('*/**/logs.all*', async (route) => {
  await route.fulfill({ body: JSON.stringify(mockAPILogs) })
})

对于可选的 API 调用,请使用宽松等待:

await waitForApiResponse(page, 'pg-meta', ref, 'optional-endpoint', {
  soft: true,
  fallbackWaitMs: 1000,
})

清理

在 beforeAll/beforeEach 中清理测试数据。删除前先检查,以便妥善处理已有状态:

const bucketRow = page.getByRole('row').filter({ hasText: bucketName })
if ((await bucketRow.count()) === 0) return
// proceed with deletion

修改本地存储的测试完成后,请重置本地存储:

import { resetLocalStorage } from '../utils/reset-local-storage.js'

await resetLocalStorage(page, ref)

调试

查看跟踪记录
cd e2e/studio && pnpm exec playwright show-trace <path-to-trace.zip>
查看 HTML 报告
cd e2e/studio && pnpm exec playwright show-report
错误上下文

错误上下文文件保存在 test-results/ 目录中。

Playwright MCP 工具

在本地调试时,使用 Playwright MCP 工具检查 UI。

CI 与本地开发

主要区别在于冷启动与预热状态:

CI(冷启动)

测试从空白数据库开始运行。每次测试运行都会重置数据库,并启动全新的容器。默认情况下不会启用 pg_cron 等扩展。

使用 pnpm dev:studio-local 进行本地开发

使用运行中的开发服务器进行调试时,数据库可能已保留之前运行的状态(已启用扩展、存在测试数据)。

处理冷启动问题

本地运行正常但在 CI 中失败的测试,通常是因为对已有状态存在假设。

常见问题
  1. 未启用扩展(必须在测试设置中启用)
  2. 并行测试尝试修改共享状态时发生竞态条件(使用 test.describe.configure({ mode: 'serial' }))
  3. 页面结构因状态未设置而不同,导致定位器匹配到错误的元素
在本地复现 CI 行为

运行 pnpm run e2e 时,测试框架会自动重置数据库。这与 CI 的行为一致。

如果使用 pnpm dev:studio-local 进行 Playwright MCP 调试,请记住其状态与 CI 不同。

CI 故障的调试工作流

  1. 首先,使用 pnpm run e2e -- features/<file>.spec.ts 在本地运行测试(冷启动)
  2. 查看 test-results/ 目录中的错误上下文
  3. 如果需要检查 UI 状态,请启动 pnpm dev:studio-local 并使用 Playwright MCP 工具
  4. 请记住:你在开发服务器中看到的内容可能包含 CI 中不存在的状态

评论

支持 Markdown