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-testid during refactor/migration
- use
data-testid-first query strategy in project tests
Workflow
- Determine the scope prefix from feature/module context (for example:
user-menus, organization-switch, settings-profile).
- Add
data-testid to the component root container.
- Add
data-testid to 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-testid on 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-testid inside 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-testid in touched files
- loading/empty/error test ids for async UIs
Query Priority
When writing or updating tests:
- prefer
getByTestId for stable selectors in this project
- use
getByRole, getByLabelText, getByText as 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 as collaborator-remove-button and 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-testid to 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.
1---2name: ui-data-testid3description: 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.4---5
6# UI Data-testid
7
8## Overview
9
10Add predictable `data-testid` attributes to UI code as part of implementation, not as a later patch.
11Keep selectors stable across i18n text changes and visual refactors.
12Follow project testing rules in `.cursor/rules`:
13- preserve existing `data-testid` during refactor/migration
14- use `data-testid-first` query strategy in project tests
15
16## Workflow
17
181. Determine the scope prefix from feature/module context (for example: `user-menus`, `organization-switch`, `settings-profile`).
192. Add `data-testid` to the component root container.
203. Add `data-testid` to all primary interactive nodes:
21 - button/link triggers
22 - input/select/checkbox/radio controls
23 - tabs/menu items/submenu triggers
24 - modal/drawer open and confirm actions
254. 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.
265. For repeated list rows/items, put the stable `data-testid` on the row/item container first. Do not add separate unique ids to every child element by default.
276. 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.
287. Add child-level `data-testid` inside 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.
298. Keep existing ids unchanged unless user explicitly asks to rename; never remove existing ids in migration tasks.
309. For interaction changes, add or update Vitest/RTL tests in colocated `__tests__` where feasible.
31
32## Naming Rules
33
34- Use lowercase kebab-case only.
35- Use semantic format: `<scope>-<entity>-<action>`.
36- Keep IDs text-agnostic (do not depend on i18n labels).
37- Avoid dynamic/random values (`Date.now`, UUID, translated text).
38- Do not embed secrets, emails, phone numbers, or tokens.
39- Use stable suffixes when applicable: `trigger`, `content`, `button`, `input`, `option`, `item`, `row`, `loading`, `empty`, `error`.
40
41## Minimum Coverage Checklist
42
43For every newly created UI component, include at least:
44
45- one root container test id
46- one primary CTA test id
47- test ids for each secondary action button
48- test ids for each form field group/control
49- test ids for menu item triggers when menus are present
50- 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
51- preserved historical `data-testid` in touched files
52- loading/empty/error test ids for async UIs
53
54## Query Priority
55
56When writing or updating tests:
57
58- prefer `getByTestId` for stable selectors in this project
59- use `getByRole`, `getByLabelText`, `getByText` as complementary assertions
60- avoid `container.querySelector(...)` selectors for user-facing behavior tests
61
62This keeps alignment with `.cursor/rules/testing-guide.mdc`.
63
64## Scenario Playbook
65
66Apply these patterns for stable and accurate element targeting:
67
681. Forms
69 - add ids for form container, inputs, submit/cancel buttons, and validation errors
702. Lists and tables
71 - add list container id and row container id
72 - prefer a shared row id, then select the intended row by text/business data
73 - use stable business key for row id suffix only when there is a concrete need for direct row lookup
74 - do not add separate dynamic ids to every field/action inside the row; query child actions with `within(row)` scope
75 - if a child action needs a `data-testid`, use one shared id such as `collaborator-remove-button` and resolve it from the row scope
763. Menus and dropdowns
77 - add ids for trigger, popup content, and each actionable menu item
78 - if menu is config-driven, forward item-level `data-testid` to rendered node
794. Modal and drawer
80 - add ids for open trigger, modal content, primary action, and close/cancel action
815. Async states
82 - add ids for loading, empty, and error states
83
84## Stability Rules
85
86- Never generate ids from array index if order may change.
87- Never generate ids from random values or timestamps.
88- Keep singleton ids unique on a page.
89- For repeated components, keep shared child ids and scope with `within(...)`.
90- 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.
91
92## Patterns
93
94### Component markup
95
96```tsx
97<div data-testid="user-menus-organization-info">
98 <button type="button" data-testid="user-menus-upgrade-button" />
99 <button type="button" data-testid="user-menus-recharge-button" />
100</div>
101```
102
103### Repeated list row
104
105```tsx
106<div data-testid="collaborator-list-content">
107 {collaborators.map((collaborator) => (
108 <div
109 key={collaborator.id}
110 data-testid="collaborator-item"
111 data-collaborator-id={collaborator.id}
112 >
113 <span>{collaborator.name}</span>
114 <button type="button" data-testid="collaborator-remove-button" />
115 </div>
116 ))}
117</div>
118
119const row = page
120 .getByTestId("collaborator-list-content")
121 .locator('[data-testid="collaborator-item"]')
122 .filter({ hasText: receiverName })
123
124await row.getByTestId("collaborator-remove-button").click()
125```
126
127### Config + renderer forwarding
128
129```tsx
130const items = [
131 { key: "logout", label: t("logout"), "data-testid": "user-menus-logout" },
132]
133
134<ItemComponent data-testid={menuItem["data-testid"]}>{menuItem.label}</ItemComponent>
135```
136
137## Done Criteria
138
139Complete 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.
140Confirm no existing `data-testid` was removed unintentionally in migration/refactor diffs.