关于 ui-data-testid
这是一个指导 UI 实现者为界面元素添加稳定 data-testid 的技能,帮助测试选择器不受文案翻译和视觉调整影响。它提供命名、覆盖范围、重复列表定位及测试查询的规则。
主要功能
- 01为根容器和交互元素添加标识
- 02使用语义化 kebab-case 命名
- 03为重复列表设置行级标识
- 04优先使用稳定选择器编写测试
- 05保留已有标识并覆盖异步状态
适用场景
- 01为新表单添加可测试的稳定选择器
- 02改造菜单和弹窗而保留既有标识
- 03测试重复列表中的指定行及其操作
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dtyq/ magic --skill ui-data-testid安装后在 Claude Code 中输入/ui-data-testid确认已加载。
附带文件agents/openai.yaml
详细介绍
UI data-testid
概览
在实现过程中为 UI 代码添加可预测的 data-testid 属性,而不是事后补充。
确保选择器不会因 i18n 文本变化或视觉重构而变动。
遵循 .cursor/rules 中的项目测试规则:
- 重构或迁移时保留现有的
data-testid - 在项目测试中采用
data-testid-first查询策略
工作流
- 根据功能或模块上下文确定作用域前缀(例如:
user-menus、organization-switch、settings-profile)。 - 为组件根容器添加
data-testid。 - 为所有主要交互节点添加
data-testid:- 按钮或链接触发器
- 输入框、选择框、复选框和单选框控件
- 标签页、菜单项和子菜单触发器
- 模态框或抽屉的打开和确认操作
- 对于配置驱动的 UI(例如
menu.items),在配置中添加"data-testid",并在渲染器或包装组件中将其传递给实际可点击的 DOM 节点。 - 对于重复列表行或列表项,首先将稳定的
data-testid放在行或列表项容器上。默认情况下,不要为每个子元素添加单独的唯一 ID。 - 在列表行或列表项中,在测试里复用行作用域:通过共享的行 ID 以及文本或业务数据定位行,然后使用
within(row)或角色/标签选择器查询子控件。 - 只有在无法通过行作用域可靠地选择子元素时,才在重复行中添加子级
data-testid;如有需要,让子级 ID 在各行之间共用,而不是追加行 ID。 - 除非用户明确要求重命名,否则保持现有 ID 不变;迁移任务中绝不能移除现有 ID。
- 对于交互变更,在可行的情况下,于同级的
__tests__中添加或更新 Vitest/RTL 测试。
命名规则
- 仅使用小写 kebab-case。
- 使用语义格式:
<scope>-<entity>-<action>。 - ID 不依赖文本内容(不得依赖 i18n 标签)。
- 避免使用动态或随机值(
Date.now、UUID、翻译文本)。 - 不要嵌入密钥、电子邮件地址、电话号码或令牌。
- 适用时使用稳定的后缀:
trigger、content、button、input、option、item、row、loading、empty、error。
最低覆盖检查清单
对于每个新建的 UI 组件,至少包含:
- 一个根容器 test id
- 一个主要 CTA test id
- 每个次要操作按钮的 test id
- 每个表单字段组或控件的 test id
- 存在菜单时,为每个菜单项触发器添加 test id
- 对于重复列表或表格,一个列表容器 ID 和一个行或列表项容器 ID;除非行作用域选择器不够用,否则避免为每个单元格或操作添加动态 ID
- 保留已修改文件中的历史
data-testid - 为异步 UI 添加 loading、empty 和 error test id
查询优先级
编写或更新测试时:
- 在本项目中优先使用
getByTestId作为稳定选择器 - 使用
getByRole、getByLabelText、getByText作为补充断言 - 面向用户行为的测试中,避免使用
container.querySelector(...)选择器
这样可以确保与 .cursor/rules/testing-guide.mdc 保持一致。
场景指南
应用以下模式,以稳定、准确地定位元素:
- 表单
- 为表单容器、输入框、提交/取消按钮和验证错误添加 id
- 列表和表格
- 添加列表容器 id 和行容器 id
- 优先使用共享行 id,再根据文本/业务数据选择目标行
- 仅在确实需要直接查找特定行时,才使用稳定的业务键作为行 id 后缀
- 不要为行内的每个字段/操作单独添加动态 id;使用
within(row)作用域查询子操作 - 如果子操作需要
data-testid,请使用一个共享 id,例如collaborator-remove-button,并在行作用域内查找
- 菜单和下拉菜单
- 为触发器、弹出内容和每个可操作菜单项添加 id
- 如果菜单由配置驱动,请将各项级别的
data-testid传递到渲染后的节点
- 模态框和抽屉
- 为打开触发器、模态框内容、主要操作和关闭/取消操作添加 id
- 异步状态
- 为加载、空状态和错误状态添加 id
稳定性规则
- 如果数组顺序可能变化,绝不要根据数组索引生成 id。
- 绝不要根据随机值或时间戳生成 id。
- 确保单例 id 在页面上唯一。
- 对于重复组件,保留共享的子级 id,并使用
within(...)限定作用域。 - 在重复行中,优先保证行级唯一性,而不是子级唯一性;除非没有可用于限定作用域的行容器,否则子级 id 不应包含行 id。
模式
组件标记
<div data-testid="user-menus-organization-info">
<button type="button" data-testid="user-menus-upgrade-button" />
<button type="button" data-testid="user-menus-recharge-button" />
</div>
重复列表行
<div data-testid="collaborator-list-content">
{collaborators.map((collaborator) => (
<div
key={collaborator.id}
data-testid="collaborator-item"
data-collaborator-id={collaborator.id}
>
<span>{collaborator.name}</span>
<button type="button" data-testid="collaborator-remove-button" />
</div>
))}
</div>
const row = page
.getByTestId("collaborator-list-content")
.locator('[data-testid="collaborator-item"]')
.filter({ hasText: receiverName })
await row.getByTestId("collaborator-remove-button").click()
配置 + 渲染器转发
const items = [
{ key: "logout", label: t("logout"), "data-testid": "user-menus-logout" },
]
<ItemComponent data-testid={menuItem["data-testid"]}>{menuItem.label}</ItemComponent>
完成标准
只有在已触及的 UI 文件中,所有新增的单例交互节点都具有稳定的 data-testid 值,且重复列表/表格行都具有行级选择器,可用于在行作用域内查找子控件时,才算完成。
确认迁移/重构差异中没有意外移除任何现有的 data-testid。
评论
支持 Markdown