Playwright selector strategy

Self-healing vs static Playwright selectors, and the third option nobody mentions

Static selectors break the moment a class name changes. Self-healing selectors keep the test green even when they should not. Both sides of this argument quietly assume the same thing: that a selector is stored in your repo in the first place.

M
Matthew Diakonov
6 min read

Direct answer (verified 2026-05-21)

Default to stable semantic locators (getByRole, getByLabel) because they survive most DOM churn. Reserve self-healing for high-churn UI, and never let a heal auto-pass unsupervised in CI: a selector that “fixes itself” can hide a real regression. The cleanest way out of the tradeoff is to store no selector at all and resolve the element fresh on every run, which is what Assrt does. Stability behavior for Playwright’s own locators is documented at playwright.dev/docs/locators.

Three ways a Playwright test finds an element

The vs framing online is really a fight between the first two rows below. The third row changes the question.

Static selector

A locator string frozen at write time. Explicit and fast.

Fails on

Any DOM change that touches the matched attribute.

Self-healing selector

Starts from a stored string, then patches it with fallbacks when it fails.

Fails quietly on

A heal that re-points to the wrong element and stays green.

No stored selector

The step is natural language. The element is resolved live from the accessibility tree each run.

Nothing to heal

No frozen locator exists, so none can rot.

What “self-healing” usually means in practice

Read almost any guide on this and the mechanism is the same. You still write a selector. When it throws, a layer kicks in and tries other strategies: a different attribute, a nearby label, a path it learned over previous runs. When one matches, it rewrites your selector and the run continues. Healenium-style plugins, the community Healwright approach, and the newer agent-driven healers all share this shape: heal a stored selector after it breaks.

That is genuinely useful for cutting maintenance hours. It also moves the failure mode rather than removing it. The classic rule-based version is literally “if selector fails, try X path, then Y.” If X or Y matches an element that looks right but is not the one the test meant, the suite reports green while the flow is broken. A test that no longer fails when the product breaks has stopped doing its job.

Playwright’s own answer to brittleness is not healing at all, it is better locators plus built-in retries. Its semantic locators (getByRole, getByText) target elements the way a user perceives them and stay stable as markup shifts, and actions auto-retry until the element is actionable. That is the right default, and it is still a stored selector you maintain.

What I found reading the runner: there is no selector to store

I went into the Assrt source to check whether “self-healing selectors” was marketing or architecture. It is architecture, and it is a different one than the heal-after-break model. A test step is not a selector. In src/core/types.ts, a TestStep is just { action, description } in plain language. There is no CSS string, no XPath, no locator field anywhere in the stored scenario.

The resolution happens at run time. The agent’s Selector Strategy in src/core/agent.ts reads, almost verbatim: call snapshot to get the accessibility tree, find the element you want, use its ref to act, and “if a ref is stale (action fails), call snapshot again to get fresh refs.” The element is re-derived from the live tree on every step. There is no frozen locator sitting in your repo waiting to rot, which is why there is nothing to heal in the first place.

How a step resolves at run time (no stored selector)

1

Read the live accessibility tree

browser_snapshot returns the current page as roles and accessible names, not raw DOM.

2

Match the step's intent

The natural-language step ('click Verify') maps to the element by role and name, the way a user would find it.

3

Act on a fresh ref

The action uses the ref ID from this snapshot, valid for this exact page state.

4

Stale ref? Re-snapshot

If the page changed and the ref no longer resolves, the agent re-snapshots and finds the element again. No fallback selector chain to maintain.

What lives in your repo

The practical difference is what you are on the hook to maintain when the design team ships a redesign on Friday afternoon.

Selector layer you maintain

A layer of locator strings, plus (for self-healing) the fallback rules and learned patterns that patch them.

  • page.locator('.btn-primary') breaks when the class changes
  • Self-healing adds a fallback chain you also have to trust and audit
  • Heals can pass silently, so green no longer means working
  • Closed self-healing SaaS keeps the logic behind an API you cannot inspect

Where each approach actually wins

No approach is free. Here is the honest split, including the cases where a plain static selector is the right call.

Reach for static selectors when

  • You own the markup and can add stable test ids
  • The surface is small and rarely changes
  • You want maximum explicitness and easy debugging
  • The UI churns every sprint
  • You do not control the DOM you are testing

Skip stored selectors when

  • The UI changes constantly and selectors rot fast
  • You test apps whose markup you do not own
  • You want green to keep meaning working
  • You need an auditable test id on every element anyway
  • Your team prefers hand-written locators for clarity

For a deeper look at how the healing-after-break model behaves on real apps, see our self-healing Playwright tests guide, and for the manual-Playwright comparison see Assrt vs manual Playwright.

Not sure your selector strategy is the bottleneck?

Bring your flakiest spec file and we will talk through whether healing, better locators, or no stored selector fits your suite.

Frequently asked questions

Should I use self-healing or static selectors in Playwright?

Default to stable semantic locators (getByRole, getByLabel, getByText) because they survive most DOM churn without any extra machinery. Reserve self-healing for genuinely high-churn UI where selectors change every sprint, and never let a heal auto-pass unsupervised in CI: a selector that 'fixes itself' can mask a real regression. If you want to skip the dilemma entirely, store no selector and resolve the element fresh on every run.

What is the actual difference between a static and a self-healing selector?

A static selector is a string you freeze at write time, for example page.locator('.btn-primary'). When the class changes, the test throws. A self-healing selector starts from that same stored string, but when it fails the framework tries fallback strategies (other attributes, nearby text, a learned pattern) and rewrites the selector to whatever still matches. The heal keeps the test green, which is the point and also the danger.

Why is self-healing risky if it keeps my tests passing?

Because a passing test is only useful if it fails when the product breaks. If a button's purpose changed but a heal re-pointed the selector to a different element that happens to match, the test passes while the user-facing flow is broken. Self-healing trades brittleness for the risk of false confidence. The mitigation is to surface every heal as a reviewable event, not to let it run silently in CI.

How does Assrt avoid both problems?

Assrt does not store a CSS or XPath selector at all. A scenario step is saved as natural language. In the source, a TestStep is { action, description } (src/core/types.ts), never a locator string. At run time the agent calls browser_snapshot to read the live accessibility tree, acts on a ref ID, and if a ref is stale it re-snapshots to get fresh refs (agent.ts, Selector Strategy section). There is no frozen locator to break, so there is nothing to heal.

Does that mean Assrt can never produce a false pass?

No tool can promise that. Resolving from the accessibility tree by role and accessible name is closer to how a user finds an element than a CSS class is, so it tracks intent better than a static string. But a step described as 'click Submit' will still find a Submit-shaped element. You keep assertions doing the real verification work. The difference is that you are not also maintaining a separate brittle layer of selectors on top.

Do I still get real Playwright tests, or a proprietary format?

Assrt generates standard Playwright files you can inspect, modify, and run in any CI pipeline. It is fully open source with zero vendor lock-in, so the output lives in your repo and runs on your runners. That is the opposite of closed self-healing SaaS where the selector logic lives behind someone else's API.

When are static selectors still the right call?

When you control the markup and can add stable test ids, static getByTestId selectors are explicit, fast, and trivially debuggable. For a small, stable surface that rarely changes, a hand-written locator is perfectly fine and arguably clearer than any healing layer. The cost only shows up at scale, on high-churn UI, or on apps where you do not own the DOM.

assrtOpen-source AI testing framework
© 2026 Assrt. MIT License.

How did this page land for you?

React to reveal totals

Comments ()

Leave a comment to see what others are saying.

Public and anonymous. No signup.