mcpField
U
ui4-convert-tests

ui4-convert-tests

github.com/payloadcms测试与质量45.6k69 次安装今天更新
代码仓库

关于 ui4-convert-tests

ui4-convert-tests 是在 UI 改动后定位并修复受影响端到端测试的技能,面向维护界面和测试套件的开发者。它分析改动类型及测试依赖,再指导更新选择器、交互步骤和断言,并运行受影响的测试验证。

主要功能

  1. 01分析界面差异及其测试影响
  2. 02跨测试目录搜索相关用例和共享辅助函数
  3. 03按选择器、结构、文本或属性变化分类修复
  4. 04优先更新共享辅助函数
  5. 05运行受影响的端到端测试验证修复

适用场景

  1. 01界面改动导致 CI 端到端测试失败时排查
  2. 02组件移动到弹窗后更新测试交互步骤
  3. 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:分析测试依赖

修复之前,先理解测试:

  1. 阅读完整测试 - 理解它实际测试的内容
  2. 检查辅助函数 - 是否有处理此选择器的共享辅助函数?
  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

修复:

  1. 检查 test/<feature>/helpers/ 中是否已存在辅助函数
  2. 如果存在,修复该辅助函数(一次性修复所有测试)
  3. 如果不存在,创建一个并重构测试以使用它
模式:不同测试套件中的测试共享组件

当你更改一个共享组件(如 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
Modaldialog, [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 命令会自动:

  1. 如果端口空闲,启动开发服务器
  2. 如果端口被占用,复用已有的开发服务器
  3. 针对该端口运行 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')

评论

支持 Markdown