ui-data-testid
GitHub为 React/TSX UI 组件默认添加稳定 data-testid,确保选择器在 i18n 和重构中保持一致。规范命名、覆盖范围及测试查询策略,支持单元/E2E 测试。
Trigger Scenarios
Install
npx skills add dtyq/magic --skill ui-data-testid -g -y
SKILL.md
Frontmatter
{
"name": "ui-data-testid",
"description": "Add stable `data-testid` attributes by default for new or refactored UI components. Use when implementing React\/TSX views, shadcn\/antd-style components, dropdown\/menu configs, or interactive UI flows that need reliable selectors for unit\/E2E tests."
}
UI Data-testid
Overview
Add predictable data-testid attributes to UI code as part of implementation, not as a later patch.
Keep selectors stable across i18n text changes and visual refactors.
Follow project testing rules in .cursor/rules:
- preserve existing
data-testidduring refactor/migration - use
data-testid-firstquery strategy in project tests
Workflow
- Determine the scope prefix from feature/module context (for example:
user-menus,organization-switch,settings-profile). - Add
data-testidto the component root container. - Add
data-testidto all primary interactive nodes:- button/link triggers
- input/select/checkbox/radio controls
- tabs/menu items/submenu triggers
- modal/drawer open and confirm actions
- For config-driven UI (for example
menu.items), add"data-testid"in config and forward it to the real clickable DOM node in renderer/wrapper components. - For repeated list rows/items, put the stable
data-testidon the row/item container first. Do not add separate unique ids to every child element by default. - Inside a list row/item, reuse the row scope in tests: locate the row by shared row id plus text/business data, then query child controls with
within(row)or role/label selectors. - Add child-level
data-testidinside repeated rows only when the child cannot be reliably selected from the row scope; if needed, keep the child id shared across rows instead of appending row ids. - Keep existing ids unchanged unless user explicitly asks to rename; never remove existing ids in migration tasks.
- For interaction changes, add or update Vitest/RTL tests in colocated
__tests__where feasible.
Naming Rules
- Use lowercase kebab-case only.
- Use semantic format:
<scope>-<entity>-<action>. - Keep IDs text-agnostic (do not depend on i18n labels).
- Avoid dynamic/random values (
Date.now, UUID, translated text). - Do not embed secrets, emails, phone numbers, or tokens.
- Use stable suffixes when applicable:
trigger,content,button,input,option,item,row,loading,empty,error.
Minimum Coverage Checklist
For every newly created UI component, include at least:
- one root container test id
- one primary CTA test id
- test ids for each secondary action button
- test ids for each form field group/control
- test ids for menu item triggers when menus are present
- for repeated lists/tables, one list container id and one row/item container id; avoid per-cell/per-action dynamic ids unless row-scoped selectors are insufficient
- preserved historical
data-testidin touched files - loading/empty/error test ids for async UIs
Query Priority
When writing or updating tests:
- prefer
getByTestIdfor stable selectors in this project - use
getByRole,getByLabelText,getByTextas complementary assertions - avoid
container.querySelector(...)selectors for user-facing behavior tests
This keeps alignment with .cursor/rules/testing-guide.mdc.
Scenario Playbook
Apply these patterns for stable and accurate element targeting:
- Forms
- add ids for form container, inputs, submit/cancel buttons, and validation errors
- Lists and tables
- add list container id and row container id
- prefer a shared row id, then select the intended row by text/business data
- use stable business key for row id suffix only when there is a concrete need for direct row lookup
- do not add separate dynamic ids to every field/action inside the row; query child actions with
within(row)scope - if a child action needs a
data-testid, use one shared id such ascollaborator-remove-buttonand resolve it from the row scope
- Menus and dropdowns
- add ids for trigger, popup content, and each actionable menu item
- if menu is config-driven, forward item-level
data-testidto rendered node
- Modal and drawer
- add ids for open trigger, modal content, primary action, and close/cancel action
- Async states
- add ids for loading, empty, and error states
Stability Rules
- Never generate ids from array index if order may change.
- Never generate ids from random values or timestamps.
- Keep singleton ids unique on a page.
- For repeated components, keep shared child ids and scope with
within(...). - Prefer row-level uniqueness over child-level uniqueness in repeated rows; child ids should not encode row ids unless there is no row container to scope from.
Patterns
Component markup
<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>
Repeated list row
<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()
Config + renderer forwarding
const items = [
{ key: "logout", label: t("logout"), "data-testid": "user-menus-logout" },
]
<ItemComponent data-testid={menuItem["data-testid"]}>{menuItem.label}</ItemComponent>
Done Criteria
Complete only when all new singleton interactive nodes in touched UI files have stable data-testid values and repeated list/table rows have row-level selectors that allow child controls to be found from the row scope.
Confirm no existing data-testid was removed unintentionally in migration/refactor diffs.
Version History
-
f9973c5
Current 2026-08-20 03:38
优化列表行 ID 处理策略,优先使用共享 ID 配合 row-scoped 查询;细化子元素 ID 添加条件,避免不必要的动态 ID。
- 41d7ef4 2026-07-25 09:30


