BEST PRACTICES
Choosing test selectors that don't break
Updated June 2026 · 7 min read
Most "flaky" E2E failures aren't timing flakiness at all — they're selectors that quietly stopped matching after a UI change. The fix is to pick hooks the app is contractually committed to keeping stable.
Why CSS and XPath selectors rot
A selector like .btn-primary.mt-4 > span:nth-child(2) encodes styling and layout — the two things designers and developers change most often. Restyle the button, wrap it in a flex container, swap a utility class, and the selector breaks even though the feature works perfectly. XPath that walks the DOM tree is worse: any structural edit upstream invalidates it.
These selectors fail for reasons that have nothing to do with the behavior under test. That's the definition of a brittle test.
The hierarchy of selector stability
- Dedicated test ids —
data-testid,data-cy,data-test. They exist only for tests, so nobody changes them for styling reasons. Most stable. - Roles & accessible names —
getByRole('button', { name: 'Submit' }). Stable and they assert accessibility. Playwright and Testing Library both recommend these. - Visible text — durable until copy changes; good for content, riskier for frequently-reworded UI.
- Ids and names — okay if they're semantic and intentional, fragile if auto-generated.
- CSS classes / structural XPath — avoid for selection. They're styling, not contract.
Make test ids a contract
A data-testid only helps if everyone treats it as load-bearing. Two practices make that real:
- Name by intent, not appearance:
data-testid="checkout-submit", notdata-testid="orange-button". - Flag renames at review time. A test id is a contract between app and suite — and like any contract, the dangerous moment is when one side changes it without telling the other.
The gap stable selectors don't close
Even perfect data-testid hygiene doesn't help when someone renames or removes one. The selector is stable right up until the PR that changes it — and that PR's reviewer usually has no idea a spec depends on it. This is exactly the blind spot test impact analysis at PR time exists to cover.
Testward recognizes the full family of stable hooks — data-testid, data-cy, getByRole/getByTestId, aria-labels, ids — and flags the moment a PR changes one a spec depends on, including across repos. Good selectors make your suite durable; Testward tells you when someone's about to break the contract anyway.
Install Testward and get flagged the moment a PR touches a selector your specs depend on.
Install free on GitHub