关于 ui4-convert-tests
ui4-convert-tests 是在 UI 改动后定位并修复受影响端到端测试的技能,面向维护界面和测试套件的开发者。它分析改动类型及测试依赖,再指导更新选择器、交互步骤和断言,并运行受影响的测试验证。
主要功能
- 01分析界面差异及其测试影响
- 02跨测试目录搜索相关用例和共享辅助函数
- 03按选择器、结构、文本或属性变化分类修复
- 04优先更新共享辅助函数
- 05运行受影响的端到端测试验证修复
适用场景
- 01界面改动导致 CI 端到端测试失败时排查
- 02组件移动到弹窗后更新测试交互步骤
- 03选择器或标签文本更改后修正测试断言
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ payloadcms/ payload --skill ui4-convert-tests安装后在 Claude Code 中输入/ui4-convert-tests确认已加载。
详细介绍
UI4 转换测试
概述
完成 UI 更改后,本技能会系统地识别并修复受影响的 e2e 测试。它会分析 diff 以了解所做更改的_种类_(而不仅仅是哪些文件),然后找到需要更新的测试。
何时使用
- UI 更改已最终确定,并准备好进行测试修复
- 由于你的 UI 更改,CI 在测试上失败
- 在打开 PR 之前,确保测试通过
流程
步骤 1:分析更改内容
目标: 了解更改的_性质_,以预测对测试的影响。
# Get changed UI files
git diff main --name-only -- 'packages/ui/src/**/*.tsx' 'packages/ui/src/**/*.css'
对每个更改的文件,对更改进行分类:
A. 选择器更改(ID、class)
git diff main -- <file> | grep -E '^\-.*className|^\-.*id=|^\+.*className|^\+.*id='
B. 结构更改(元素被移动)
查找被以下方式处理的组件:
- 被移入弹出层、抽屉或下拉菜单
- 被包裹在新的父元素中
- 变为条件渲染
git diff main -- <file> | grep -E 'Popup|PopupList|Drawer|Dropdown'
C. 文本/标签更改
# Translation keys
git diff main -- <file> | grep -E "t\('|i18n\.t\("
# Hardcoded text
git diff main -- <file> | grep -E 'placeholder=|aria-label='
构建更改摘要:
| 更改类型 | 更改内容 | 测试影响 |
|---|---|---|
| 选择器 | .btn:has-text("Create") → #create-new-doc | 更新定位器 |
| 结构 | 按钮被移入弹出层 | 添加打开弹出层步骤 |
| 文本 | "Search by ID" → "Search" | 更新断言 |
步骤 2:找到受影响的测试
搜索策略: 先广泛撒网,再逐步缩小范围。
# Search for component name references (not just selectors)
grep -rn "QueryPreset\|query-preset\|preset" test/**/*.ts --include="*.spec.ts" --include="*.ts"
# Search for specific selectors from Step 1
grep -rn "\.list-header\|Create New\|#create-new" test/**/*.ts
关键测试位置:
| 模式 | 查找位置 |
|---|---|
| 特定组件 | test/<feature>/e2e.spec.ts |
| 共享辅助函数 | test/<feature>/helpers/*.ts |
| 跨领域 | test/__helpers/e2e/*.ts |
| 使用同一组件的多个功能 | 搜索所有测试目录 |
不要只搜索精确的选择器! 还要搜索:
- 组件名称(例如
QueryPreset、ListHeader) - 功能名称(例如
preset、filter、search) - 发生更改的文本内容(例如
"Create New"、"Search by")
步骤 3:分析测试依赖
修复之前,先理解测试:
- 阅读完整测试 - 理解它实际测试的内容
- 检查辅助函数 - 是否有处理此选择器的共享辅助函数?
- 查找模式 - 是否有多个测试在做同样的事?
如果多个测试使用同一选择器,创建/更新一个辅助函数:
// test/<feature>/helpers/togglePreset.ts
export async function openCreatePreset(page: Page) {
await page.click('#select-preset') // Open popup first
await page.click('#create-new-preset')
}
这样可以集中修复,并防止未来重复。
步骤 4:对所需修复进行分类
| 更改类型 | 修复策略 |
|---|---|
| 选择器重命名 | 直接替换字符串 |
| 元素被移入弹出层 | 在点击元素前添加点击以打开弹出层 |
| 元素被移入抽屉 | 添加抽屉打开/关闭处理 |
| 文本被简化 | 更新断言以匹配新文本 |
| 元素被移除 | 重做测试逻辑或删除测试 |
| Props 更改 | 更新属性断言 |
| 条件渲染 | 可能需要在元素出现前设置状态 |
步骤 5:运行受影响的测试
在修复之前运行测试,以确认它们确实失败:
# Use isolated port to avoid conflicts
PORT=3150 pnpm test:e2e <suite> --max-failures=1
# Run specific test by name
PORT=3150 pnpm test:e2e <suite> -g "test name" --max-failures=1
记录失败模式:
Timeout waiting for locator('.old-selector')→ 选择器已更改locator resolved to 0 elements→ 元素已被移动或移除expected "New Text" received "Old Text"→ 文本内容已更改
步骤 6:应用修复
优先级:先修复辅助函数,再修复单个测试。
模式 1:选择器重命名
// Before
await page.click('.list-header .btn:has-text("Create")')
// After - prefer IDs when available
await page.click('#create-new-doc')
模式 2:元素被移入弹出层
// Before - direct click
await page.click('#edit-preset')
// After - open popup first
await page.click('#select-preset') // Opens the popup
await page.click('#edit-preset') // Now visible in popup
模式 3:文本内容被简化
// Before - specific placeholder text
await expect(input).toHaveAttribute('placeholder', /(Search by ID)/)
// After - simplified text
await expect(input).toHaveAttribute('placeholder', 'Search')
模式 4:创建可复用的辅助函数
当多个测试中需要相同的交互时:
// test/<feature>/helpers/interactions.ts
export async function openEditPreset(page: Page) {
await page.click('#select-preset')
await page.click('#edit-preset')
}
// In tests - import and use
import { openEditPreset } from './helpers/interactions.js'
await openEditPreset(page)
第 7 步:验证修复
# Run same tests that failed
PORT=3150 pnpm test:e2e <suite> --max-failures=1
只有在测试通过后才提交。
常见模式(真实示例)
模式:按钮被移入弹出菜单
症状: 测试超时,等待一个原本直接可见的按钮。
检测: 检查按钮是否被包裹在 <Popup> 或 <PopupList> 中:
git diff main -- <file> | grep -E 'PopupList|Popup'
修复: 在点击按钮之前,先添加弹出触发器的点击:
// Before: Button was directly in toolbar
await page.click('#edit-preset')
// After: Button is now inside a popup
await page.click('#select-preset') // Opens popup
await page.click('#edit-preset') // Now visible
额外好处: 如果多个测试需要此操作,创建一个辅助函数。
模式:基于类的选择器 → ID 选择器
症状: .some-class 或 :has-text("Button Text") 不再能找到元素。
检测: 组件添加了 id= 属性:
git diff main -- <file> | grep -E '^\+.*id='
修复: 使用更稳定的 ID:
// Before: Fragile class + text selector
await page.click('.list-header .btn:has-text("Create New")')
// After: Stable ID selector
await page.click('#create-new-doc')
模式:占位符/标签文本被简化
症状: 断言失败,显示 expected "New Text" received "Old Text"。
检测: 翻译键或硬编码文本发生了变化:
git diff main -- <file> | grep -E 'placeholder=|t\('
修复: 更新断言以匹配新文本:
// Before: Verbose placeholder
await expect(input).toHaveAttribute('placeholder', /(Search by ID, Title)/)
// After: Simplified
await expect(input).toHaveAttribute('placeholder', 'Search')
模式:多个测试需要相同的修复
症状: 不同测试套件中的多个测试因类似的选择器问题而失败。
检测:
# Find all tests using the old selector
grep -rn "old-selector\|.old-class" test/**/*.ts
修复:
- 检查
test/<feature>/helpers/中是否已存在辅助函数 - 如果存在,修复该辅助函数(一次性修复所有测试)
- 如果不存在,创建一个并重构测试以使用它
模式:不同测试套件中的测试共享组件
当你更改一个共享组件(如 ListControls、QueryPresetBar)时,多个测试套件可能会受到影响。
检测:
# Find component name references across all tests
grep -rn "QueryPreset\|ListControl" test/**/*.ts | cut -d: -f1 | sort -u
常见的跨套件组件:
ListControls→ 影响所有列表视图测试QueryPresetBar→ 查询预设、分组、管理测试Search→ i18n、管理、大多数集合测试Button→ 几乎影响所有内容
快速参考:常见的 Payload 测试选择器
| 组件 | 常见选择器 |
|---|---|
| Search | .search-filter__input, #search-filter-input |
| 列表视图 | .collection-list, tbody tr, .table-row |
| Popup | .popup__content, .popup-button-list__button |
| Modal | dialog, [id^=doc-drawer_], [id^=list-drawer_] |
| 按钮 | .btn, button[type="button"] |
| 查询预设 | #select-preset, .query-preset-bar__* |
测试命令参考
# Run all e2e tests for a test suite (auto-starts dev server)
PORT=3150 pnpm test:e2e <suite-name>
# Run specific test file
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts
# Run with headed browser (see what's happening)
PORT=3150 pnpm test:e2e:headed test/<suite>/e2e.spec.ts
# Run in debug mode (step through)
PORT=3150 pnpm test:e2e:debug test/<suite>/e2e.spec.ts
# Run specific test by name pattern
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts -g "test name pattern"
# Stop on first failure (useful during debugging)
PORT=3150 pnpm test:e2e test/<suite>/e2e.spec.ts --max-failures=1
注意: pnpm test:e2e 命令会自动:
- 如果端口空闲,启动开发服务器
- 如果端口被占用,复用已有的开发服务器
- 针对该端口运行 Playwright 测试
使用隔离运行测试
快速开始:隔离测试运行
在 3100-3199 范围内选择一个唯一端口:
# Run tests on an isolated port (MongoDB auto-starts its own in-memory server)
PORT=3150 pnpm test:e2e query-presets --max-failures=1
对于 MongoDB(默认)来说就这些。每次测试运行都会启动自己的内存 MongoDB 服务器,因此不会发生数据库冲突。
Postgres 隔离
对于 Postgres 测试,为每个 worktree/仓库使用唯一的数据库:
# Create a unique database for this worktree
PGPASSWORD=payload psql -h localhost -p 5433 -U payload -c "CREATE DATABASE payload_worktree1;"
# Run tests against that database
POSTGRES_URL="postgres://payload:payload@localhost:5433/payload_worktree1" \
PORT=3150 pnpm test:e2e query-presets --max-failures=1
或者使用自定义 schema 方法(无需单独的数据库):
# Tests will use a separate schema within the same database
PAYLOAD_DATABASE=postgres-custom-schema PORT=3150 pnpm test:e2e query-presets
为什么隔离很重要
| 场景 | 端口冲突? | 数据库冲突? |
|---|---|---|
| MongoDB 内存模式 | 是(相同端口) | 否(每次运行都有自己的服务器) |
| Postgres | 是(相同端口) | 是(相同的表) |
| 多个 worktree | 是 | 是(仅 Postgres) |
解决方案: 始终设置 PORT 以避免端口冲突。对于 Postgres,还需隔离数据库。
常见错误
开发服务器未运行或端口错误:
测试会读取 PORT 环境变量(默认 3000)。有两种方式:
方式 A:使用独立端口(并行测试套件的首选方式)
# Run tests on custom port - script handles everything
PORT=3105 pnpm test:e2e test/query-presets/e2e.spec.ts
方式 B:终止现有端口并使用默认端口
# Kill all dev server ports
lsof -ti:3000,3001,3002,3003,3004,3005,3006,3007,3008,3009 | xargs kill -9 2>/dev/null
# Tests use 3000 by default
pnpm test:e2e test/query-presets/e2e.spec.ts
运行了错误的测试套件: 每个测试套件(fields、query-presets、localization 等)都有自己的 Payload 配置。如果运行了错误的开发服务器,测试会失败或出现意外行为。
在修复前未运行测试: 在做更改前,务必先验证测试确实失败。如果测试通过,就不要更改它。
未检查辅助文件:
test/*/helpers/ 中的测试辅助文件通常包含会影响多个测试的共享选择器。
遗漏弹窗交互: 当元素移入弹窗时,测试需要先打开弹窗。
忘记确认对话框: 删除操作通常会添加确认模态框 - 测试需要处理确认步骤。
占位文本发生变化: 搜索占位符、按钮标签和其他文本内容可能会发生变化。
模态框 slug 不匹配:
在执行删除/确认操作时,模态框的 slug 可能会发生变化。请检查组件代码中传递给 <Modal> 或抽屉组件的实际 slug 属性。
示例:QueryPresetBar 的变更
旧结构(chips):
// Direct buttons visible
await page.click('#create-new-preset')
await page.click('#edit-preset')
await page.click('#delete-preset')
await page.click('.chip__remove') // clear
新结构(弹窗下拉菜单):
// Open popup first
await page.click('#select-preset')
// Then click menu items
await page.click('.popup-button-list__button:has-text("Create New")')
await page.click('.popup-button-list__button:has-text("Edit")')
await page.click('.popup-button-list__button:has-text("Delete")')
// Clear uses dedicated button
await page.click('.query-preset-bar__clear')