E2E Tests Break When UI Selectors Change
Short answer
A CSS class rename should not break checkout. Prefer data-testid, roles, and labels agreed with frontend; use ai.act / ai.verify sparingly only where copy or layout genuinely varies (i18n, AI chat)—never on stable forms that should have test ids.
Part of Common E2E testing gotchas.
Symptom
CI fails with errors like:
Error: locator.click: Timeout 30000ms exceeded.
waiting for locator('.btn-primary.checkout-v2')
The UI works manually. A designer or agent refactor renamed classes or reordered components—your spec still targets the old tree.
Root cause
Brittle selectors bind tests to implementation detail:
- CSS modules (
Button_button__x7f2a) - Deep XPath (
div/div/span[2]) - Text that changes with marketing copy or locale
Record-replay tools encode the same brittle paths—every regen becomes maintenance (record-replay vs TestChimp).
Fix (recommended): stable locator contract
| Priority | Locator | When |
|---|---|---|
| 1 | getByRole('button', { name: 'Pay now' }) | Accessible name is stable |
| 2 | getByTestId('checkout-submit') | Eng adds data-testid on critical actions |
| 3 | getByLabel('Email') | Forms |
| Last resort | ai.act('Complete checkout') | Volatile marketing hero or AI-generated layout only |
// Prefer test id on actions that must not break silently
await page.getByTestId('checkout-submit').click();
// Role + name when label is product-stable
await page.getByRole('button', { name: /pay now/i }).click();
Team contract: PRs that remove or rename data-testid on checkout/auth paths require updating linked SmartTests—// @Scenario: links make that visible in review (requirement traceability).
When ai.act is appropriate
Use semantic steps only for genuine variance:
- Rotating hero headlines
- AI chat bubbles and chip labels
- i18n copy you refuse to freeze in CI
Do not use ai.act for payment buttons, login, or admin tables that should have test ids—see when to use ai.act.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| CSS class from design system | Renamed every sprint | data-testid on contract elements |
text=Submit on i18n app | Locale changes | getByRole + probe Assert |
ai.act on every click | Cost, flake, no precision | Stable ids + probes for outcomes |
| Ignoring failure | Coverage rots | /testchimp test on PR repairs with scenario context |
TestChimp workflow
On the next PR that breaks locators, /testchimp test reads markdown scenarios and repairs SmartTests in Git—faster than re-recording. Stable selectors still win: less agent churn, clearer reviews.
Related
Frequently asked questions
Should we use data-testid on every element?
No—prioritize critical paths: checkout, auth, admin mutations. Use roles/labels elsewhere. Over-tagging creates noise; under-tagging creates refactor pain.
Our eng team refuses test ids—what now?
Use getByRole and getByLabel where accessible names are stable. For volatile regions, ai.act with probe Assert on backend outcomes—not toast text alone.
Can /testchimp test fix broken selectors automatically?
Yes on PRs with scenario context—agents update SmartTests linked via // @Scenario: rather than one-off chat scripts. Stable ids reduce how often you need repair.
Is XPath ever OK?
Rarely—for legacy DOM with no hooks. Prefer adding test ids in the same PR as the UI change.
Do record-replay tools solve selector drift?
They re-capture brittle paths—they do not establish a contract. Refactors still break recordings unless you re-record every time.
How do we prevent regressions when removing test ids?
Link SmartTests to markdown scenarios; code review treats test id removal like API breakage. Requirement traceability shows which specs cover the element.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.