require-data-testid
Require stable data-testid attributes on interactive elements for end-to-end test reliability
Require stable data-testid attributes on interactive elements and custom
components for end-to-end test reliability.
Why
E2E test selectors based on class names break on every styling refactor.
Selectors based on visible text break with copy edits and i18n. Selectors
based on data-testid are invisible at runtime, untouchable by Tailwind
churn, and stable across refactors.
This rule pairs with the a11y self-test philosophy — Layer 3: edit-time enforcement of conventions that survive past Layer 1 (axe).
Rule details
By default, the rule flags:
- Native interactive elements:
<button>,<input>,<select>,<textarea>,<form> <a>withhreforonClick(in-page anchors without those are ignored)- Custom React components (PascalCase identifiers)
If data-testid is present but its value is computed (e.g. a function call,
Math.random(), Date.now()), it's flagged separately as
dynamicDataTestId — those values change between renders and produce
flaky tests.
If the element receives {...props}, the rule assumes the parent supplies
data-testid and skips it (avoids double-flagging in pure render-forward
components).
Options
{
"conventions/require-data-testid": [
"warn",
{
// Add element / component names that must carry data-testid.
"requireOn": ["thead", "tbody"],
// Skip these even when the rule would default-flag them.
"ignore": ["VisuallyHidden", "Slot"],
// Regex string — names matching this are treated as components.
"componentPattern": "^[A-Z]",
// Reject computed `data-testid` values. Default: true.
"enforceStableValues": true
}
]
}Examples
✅ Correct:
<button data-testid="submit">Submit</button>
<a data-testid="docs-link" href="/docs">Docs</a>
<MyButton data-testid="primary-cta">Go</MyButton>
<button data-testid={`page-${pageNumber}`}>Next</button>
// Spreading parent props — parent owns the testid.
function ForwardingButton(props) {
return <button {...props} />;
}❌ Incorrect:
<button>Submit</button>
<a href="/docs">Docs</a>
<MyButton>Go</MyButton>
<button data-testid={Math.random()}>Unstable</button>Naming convention
While this rule enforces presence and stability, it does not enforce naming. By repo convention:
- Lower-kebab-case (
clear-search, notclearSearch) - Hierarchical when nested:
pagination→pagination-prev,pagination-next,pagination-page-1 - Match the visible action verb where possible:
clear-search,toggle-theme,submit-form
When not to use
If you don't run E2E tests, this rule has no benefit and adds noise.