Skip to main content
ESLint Interlace
Plugin: conventionsRules

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> with href or onClick (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, not clearSearch)
  • Hierarchical when nested: paginationpagination-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.