Free tool · no signup

Which of your selectors will break next?

Paste selectors, or a whole Playwright or Cypress spec, and get a score per selector with the reason it is fragile. Everything runs in your browser; nothing is uploaded.

A selector is brittle when it depends on something that changes for reasons unrelated to the feature: a build-generated class name, a per-render id, a position among siblings, or a chain of layout wrappers. Stable selectors depend on identity instead — a test id, or a role plus an accessible name.

One per line.
0
.css-1x2y3z4 > div:nth-child(3) button
  • Build-generated class name. Classes like css-1a2b3c or Button_root__x7f are emitted by the styling toolchain and change whenever it recompiles.
  • Positional. nth-child, nth-of-type and XPath indexes break as soon as a sibling is added above the target.

getByRole('button', { name: '<visible text>' }) — or add data-testid to the element

0
/html/body/div[2]/div/form/button
  • Absolute XPath. An XPath anchored at /html breaks on any DOM restructure, including one that does not change what the user sees.
  • Positional. nth-child, nth-of-type and XPath indexes break as soon as a sibling is added above the target.

getByRole('button', { name: '<visible text>' }) — or add data-testid to the element

10
div[id=":r7:"] input
  • Per-render id. Ids like radix-:r0:, headlessui-… or react-aria-… are generated per render, not per element. They change when the tree does.

getByRole('textbox', { name: '<visible text>' }) — or add data-testid to the element

25
button.flex.items-center.gap-2
  • Utility-class chain. Selecting on flex, items-center or gap-2 selects on styling rather than identity. Restyling the element breaks the test.

getByRole('button', { name: '<visible text>' }) — or add data-testid to the element

43
.MuiButton-root:has-text("Save")
  • Coupled to copy. Matching on visible text breaks when the wording changes or the page is translated. Fine for a smoke test, risky as a primary locator.

Add a stable hook to the element and select on it: [data-testid="<name>"]

75
#email
  • Plain id. A hand-written id is reasonably stable, as long as it is not generated.
95
[role="button"][aria-label="Close"]
  • Role or accessible name. Roles and accessible names are stable and accessible at the same time: if this selector breaks, a screen reader user probably noticed first.
100
[data-testid="submit"]
  • Test id. data-testid, data-test and data-cy are an explicit contract between the markup and the test. Nothing else should change them.

This is a heuristic, not a verdict

The checker never sees your DOM. It reads the selector string and nothing else, so it can tell you that.css-1x2y3z4 looks build-generated, but it cannot tell you whether that class is actually stable in your particular setup. Treat a low score as a smell worth a second look.

Why ZeusQA does not store selectors at all

ZeusQA still uses Playwright locators inside each step — it is a real browser, driven the normal way. The difference is that it re-derives the locator every run from the screenshot and the ARIA snapshot, rather than saving one that then has to be maintained. There is no selector file to rot.

That is a trade, not a free win: a re-derived locator is not deterministic the way a stored one is, and it costs a model call per step. It suits flows that change often more than it suits a locked-down regression suite. See what a run actually checks →

Describe the test instead of the selectors

Give ZeusQA a URL and a plain-English flow. It finds the elements each run, and reports a verdict with screenshots and a session video.

Related