Playwright

Playwright Selector Best Practices: From Fragile to Resilient

The selector strategy you choose determines whether your test suite is a reliable safety net or a constant source of false failures.

80%

Self-healing selectors that update automatically when your UI changes, so tests stay green without manual fixes

Assrt Self-Healing Docs

1. The selector hierarchy: from worst to best

Not all selectors are equally fragile. Understanding the hierarchy helps you make informed tradeoffs between selector stability, readability, and effort to implement.

XPath selectors are at the bottom of the hierarchy. An expression like //div[3]/form/div[2]/button encodes the exact DOM tree structure. Any structural change to the page (adding a wrapper div, reordering siblings) breaks the selector. XPath selectors are also difficult to read, making it hard to understand what element a test targets without inspecting the page.

CSS selectors with class names are slightly better. A selector like .btn-primary is more readable and survives structural changes, but it breaks when developers rename CSS classes during refactoring. With utility-first CSS frameworks like Tailwind, class names change frequently and are not meaningful identifiers.

Data-testid attributes create an explicit contract between the application and the tests. A selector like [data-testid="submit-button"] survives both structural and styling changes. The downside is that developers must add these attributes to every testable element, which requires coordination between development and QA teams. Some teams object to shipping test infrastructure in production markup.

Text-based selectors like page.getByText('Submit Order') target elements by their visible text content. They are resilient to DOM and CSS changes but break when copy changes. For applications with stable UI copy, this is often the best tradeoff.

Role-based locators sit at the top of the hierarchy. page.getByRole('button', { name: 'Submit Order' }) queries the accessibility tree, which reflects the semantic meaning of elements rather than their implementation. This is the most resilient approach and the one Playwright recommends as default.

2. Role-based locators and the accessibility tree

Playwright's getByRole locator queries the browser's accessibility tree rather than the DOM. The accessibility tree is a parallel representation of the page that browsers build for assistive technologies like screen readers. It contains the semantic role, name, and state of each interactive element.

When you write page.getByRole('textbox', { name: 'Email address' }), Playwright finds the element whose accessible role is "textbox" and whose accessible name is "Email address." The accessible name can come from a label element, an aria-label attribute, an aria-labelledby reference, or the element's text content. This means the selector works regardless of whether the input uses a <label> tag, an aria-label, or a placeholder attribute for naming.

The practical benefit is that role-based locators survive almost any refactoring that preserves the user-facing behavior. A developer can replace a <div role="button"> with a native <button>, restructure the component tree, change CSS classes, and move the element to a different position in the DOM. As long as the accessible role and name remain the same, the locator continues to work.

Playwright provides a full set of semantic locators beyond getByRole: getByLabel for form fields, getByPlaceholder for inputs, getByAltText for images, and getByTitle for elements with title attributes. Prefer these over CSS selectors in every new test you write.

There is an important side effect: writing tests with role-based locators forces your application to be accessible. If you cannot find a button with getByRole, it means the element does not have the correct ARIA semantics, which means screen readers cannot find it either. Your test suite becomes an accessibility audit for free.

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

3. Self-healing selectors: how they work

Even the best selectors eventually break. Components get renamed, text changes during localization, and accessibility improvements alter element roles. Self-healing is the idea that when a selector fails to find its target element, the tool automatically identifies the correct element using alternative strategies and updates the selector.

Traditional self-healing tools use a multi-attribute approach. When a test is first recorded or created, the tool stores multiple attributes for each target element: the CSS selector, XPath, text content, nearby element context, element position, and visual appearance. When the primary selector fails, the tool tries each fallback attribute to find the element. If a fallback succeeds, the primary selector is updated automatically.

AI-powered self-healing takes this further. Instead of relying on stored attributes, an AI model examines the current page and determines which element corresponds to the original test intent. Assrt implements this by combining the accessibility tree with visual context. When a selector breaks, the framework captures the page state, identifies the element that best matches the original intent (based on role, label, position, and surrounding context), and generates an updated selector. The updated selector is written back to the test file, so the next run uses the new selector without any manual intervention.

The critical question with any self-healing approach is: how do you distinguish a legitimate selector update from a false heal? If a "Submit" button is removed from a page and the healer targets a different button, the test now passes on the wrong element. Good self-healing tools flag uncertain heals for human review rather than silently applying them.

In practice, self-healing works best for cosmetic changes (class renames, DOM restructuring, minor text edits) and works poorly for functional changes (removed elements, redesigned workflows). Treat self-healing as a maintenance reducer, not a maintenance eliminator.

4. MCP-based healing and token cost considerations

The Model Context Protocol (MCP) enables AI models to interact with browser automation tools directly. In the context of test healing, an MCP-based approach works like this: when a selector fails, the test framework sends the page state (accessibility tree snapshot, DOM excerpt, and optionally a screenshot) to an AI model via MCP, and the model returns an updated selector.

The accessibility tree is the most token-efficient representation of a web page. A complex page with hundreds of DOM elements might produce a 50KB HTML document, but its accessibility tree representation is typically 2 to 5KB. Sending the accessibility tree instead of raw HTML reduces token consumption by 90% or more, which directly reduces the cost of each healing operation.

Here is a rough cost breakdown for MCP-based test healing. Assume an accessibility tree snapshot of 3,000 tokens, a prompt of 500 tokens, and a response of 200 tokens. At typical API pricing ($3 per million input tokens, $15 per million output tokens), each healing operation costs approximately $0.01 to $0.02. For a test suite where 10 selectors break per UI update, the healing cost is $0.10 to $0.20 per update cycle. This is negligible compared to the developer time it replaces.

Including screenshots in the healing context increases accuracy but significantly increases token cost. A 1280x720 screenshot at standard resolution consumes approximately 1,500 tokens in a vision model. For most selector healing, the accessibility tree alone provides sufficient context. Reserve screenshot-based healing for cases where visual position is the primary distinguishing factor between elements (such as two identical buttons in different sections of the page).

When evaluating tools like Assrt that offer self-healing, ask whether the healing happens at test creation time, at test execution time, or both. Creation-time healing updates the test file on disk, which means subsequent runs are fast and free. Execution-time healing runs the AI model on every failure, which adds latency and cost per run but catches issues faster. The ideal approach combines both: execution-time healing with automatic write-back to the test file.

5. Migrating an existing test suite

If you have an existing Playwright test suite using CSS selectors or XPath, migrating to role-based locators does not need to be a big-bang rewrite. A gradual migration strategy reduces risk and lets you validate the approach before committing fully.

Start with the Playwright codegen tool. Run npx playwright codegen and interact with your application. The code generator produces locators using the priority order that Playwright recommends: getByRole, then getByText, then getByTestId, and CSS only as a last resort. Use the generated code as a reference for what role-based locators look like for your specific application.

Adopt a policy for new tests: all new tests must use role-based locators exclusively. Add a lint rule (Playwright has an ESLint plugin) that warns on CSS selector usage in test files. This prevents the problem from growing while you address the existing debt.

For the existing tests, prioritize migration by failure frequency. Run your CI pipeline for a month and track which tests fail most often due to selector issues. Migrate those tests first. You will get the most maintenance reduction from fixing the most fragile selectors.

AI tools can accelerate this migration. Assrt can analyze your existing test files, identify fragile CSS selectors, and suggest role-based replacements. Since it understands both the DOM structure and the accessibility tree, it can determine the correct getByRole locator for each element. You review and apply the suggestions rather than manually inspecting each element in the browser.

Measure the impact. Track selector-related test failures per week before and after migration. Teams that have fully migrated to role-based locators consistently report a 60% to 80% reduction in selector-related maintenance. The upfront effort pays for itself within a few months.

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk