← All articles

Selector Rot: Why Your Playwright Agent Broke After the Site Redesign

TL;DR

Selectors break because they encode where an element sits or how it is styled, and both change on every redesign. Prefer test ids, then accessible roles and names, then ids; treat text and CSS-class selectors as brittle and XPath as the most fragile of all. When a site does break the agent, record what a person did as intent rather than as coordinates, so the procedure survives the next redesign too.

Your selectors did not fail. They did exactly what they always did: point at a place in a page that no longer exists. The fix is choosing selectors that describe what an element is rather than where it is, and having a fallback for when even that changes.

Which selectors survive a redesign

Selector kindSurvives a restyleSurvives a relayoutSurvives rewording
Test id, data-testidYesYesYes
Role and name, getByRoleYesYesNo
Element idYesUsuallyYes
Visible textYesYesNo
CSS classesNoSometimesYes
XPath by positionNoNoYes

Classes are the first thing a redesign changes, because they are how designers restyle. Positional XPath breaks when one wrapper div is added above the element. Test ids are put there to be selected and almost never move.

Write for intent

// Brittle: encodes styling and position.
await page.click("div.css-1x7f2 > button.btn-primary:nth-child(2)");

// Resilient: encodes what the thing is and what it says.
await page.getByRole("button", { name: "Accept all" }).click();

Plan for the break you cannot prevent

Some redesigns change the words, the roles and the layout at once. No selector survives that. What survives is intent: a person can still look at the new page and click the right thing in seconds. Record that fix as a procedure written in terms of intent, and expose the concrete selectors alongside it as an export rather than as the source of truth. The agent follows the intent; the developer copies the selector when they want to patch code directly.

Questions people ask

Why did my Playwright script break after a website update?

Its selectors encoded the old page's classes or layout. A redesign changes both. Switch to test ids or role-and-name selectors, which describe what an element is rather than where it sits.

What is the most reliable selector in Playwright?

A data-testid when the site provides one, then getByRole with the accessible name, then an element id. Visible text and CSS classes are brittle, and positional XPath is the most fragile.

How do I stop rewriting selectors after every redesign?

Describe elements by role and name, and keep a human-in-the-loop fallback: when the page changes beyond what selectors survive, a person completes the step once and the procedure is recorded as intent.

Is XPath bad?

It always works and always breaks first. It encodes an element's position in the tree, so inserting one wrapper above it is enough. Use it as an export for automation already written that way, not as a primary selector.

DoubleOh is the reliability layer for AI agents. When one gets stuck, a person fixes it once in a live browser, and the fix becomes a skill the whole fleet follows from then on.

Start free