Hardware FixRecommendedDevice not working? Your driver may be the problemCheck updates for common hardware issues.Fix DriversHalloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix Now×
FREEDOM251

Freedom251 › Blog › End-to-End Testing

How to Choose Playwright Locators for Reliable Tests

By Freedom251 Team · · 16 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reliable Playwright tests start with locators that express what a user, assistive technology, or an explicit test contract would recognize. Prefer a unique role and accessible name, scope repeated controls to their own component, and finish actions with awaited web-first assertions that prove the expected UI state. Playwright’s auto-waiting handles timing and element readiness; it cannot make an ambiguous selector safe or prove that the business outcome occurred.

This guide shows how to choose, compose, verify, and troubleshoot locators so ordinary copy, layout, and re-rendering changes do not turn into intermittent failures.

As an Amazon Associate I earn from qualifying purchases.

What makes a Playwright locator resilient?

A locator is a query that Playwright resolves when an action or assertion runs. Unlike an old-style element handle, it is not a one-time reference to a particular DOM node. If a page re-renders between two actions, the locator can resolve the current matching element for the next operation. Playwright describes locators as the central piece of its auto-waiting and retryability.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

That behavior helps with dynamic applications, but it does not fix a selector that matches the wrong thing. Resilience comes from four properties:

  • User-facing meaning: the selector follows a role, accessible name, label, or other interface concept that the product intends to expose.
  • An explicit contract: where visible meaning is unstable or unavailable, a deliberate test ID defines an interface between application and test code.
  • Uniqueness in context: the locator identifies exactly the target required by the scenario, either on the page or inside a meaningful parent.
  • Outcome synchronization: after an action, an awaited assertion retries until the expected state is true or the assertion timeout is reached.

No single locator strategy wins in every application. Choose the most meaningful strategy that is unique, maintainable, and consistent with your team’s testing contract.

How do I write reliable Playwright locators?

Start by describing the element in the same terms as the requirement. “Submit the order” suggests a button with the accessible name Submit order; “edit invoice INV-1042” suggests an invoice row containing that identifier and an Edit control inside it. This wording-first approach prevents a selector from becoming an accidental description of today’s HTML.

Then check the locator against the page as a whole and against the smallest meaningful region. A locator that is unique only because a test happens to run on one page may become ambiguous when a banner, dialog, or second panel is added. The intended scope should be visible in the test code.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Finally, separate targeting from synchronization. A carefully chosen locator finds the intended element; a web-first assertion verifies that the application reached the expected state after the action. Neither responsibility should be hidden in a timeout or a positional shortcut.

Choose locators in a deliberate order

Use the following decision guide when more than one selector could identify an element.

Situation Preferred approach Reason
Button, link, checkbox, heading, or another semantic control getByRole(role, { name }) Matches the role and accessible name exposed to users and assistive technology.
Form field with a visible or programmatic label getByLabel(label) Tracks the field’s accessible label rather than its DOM arrangement.
Element with a deliberate test hook getByTestId(id) Makes the testing contract explicit and independent of visible copy.
Text that is itself the interface contract getByText(text) Useful when the visible wording is the behavior being verified.
Input identified by placeholder, image, or title text getByPlaceholder(), getByAltText(), or getByTitle() Uses a user-visible attribute when no better semantic target exists.
Repeated card or row with a child action Locate the parent, filter it, then locate the child Encodes the relationship between the data item and its control.
Changing list that must be enumerated Wait for a list-ready condition, then call all() all() returns current matches immediately and does not wait for a dynamic list.

Playwright recommends prioritizing user-facing attributes and explicit contracts. CSS or XPath can still be appropriate when no semantic locator or test contract expresses the requirement, but they should be a fallback rather than a reflex.

Compare the choices on four axes

When two strategies appear plausible, compare them instead of applying a universal ranking. User-facing meaning asks whether a person or assistive technology would recognize the role, name, label, or text. Contract stability asks whether a test ID is intentionally maintained as an interface between the application and its tests. Uniqueness asks whether exactly the intended element is found in the relevant region. Maintenance cost asks how ordinary copy, layout, localization, or component changes will affect the selector.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For example, a checkout button whose wording is a key acceptance criterion is well represented by getByRole('button', { name: 'Place order' }). A decorative chart with no meaningful accessible control may need a deliberate test ID. A text locator is appropriate when the test is specifically checking that a notice is rendered, but it is less suitable when the notice’s wording is expected to change and the behavior has a stable test hook.

Use roles and accessible names for controls

Why getByRole is usually the first choice

Role locators model the interface as a user or assistive technology experiences it. Add an accessible name whenever the page contains more than one control with that role:

const saveButton = page.getByRole('button', { name: 'Save changes' });
await saveButton.click();

A broad query such as page.getByRole('button') is strict. An operation that requires one element throws when several buttons match. The failure is useful feedback: the test has not stated which button it intends to activate.

Make the accessible name match the product contract

The accessible name may come from visible text, an associated label, or an accessible-name attribute. Match the wording your application intentionally exposes, and use a regular expression only when the test truly accepts a range of names:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.getByRole('button', { name: /save changes/i }).click();

Do not use a loose regular expression merely to silence a strictness error. If two controls still match, add a meaningful parent scope or refine the name. A case-insensitive expression can tolerate capitalization that is not relevant to the scenario, but it should not hide two different actions that happen to share a word.

Use the role that the interface actually exposes

Buttons, links, headings, checkboxes, dialogs, list items, rows, and status regions have distinct semantics. Choose the role that corresponds to the exposed element, not the tag name you happen to remember from the markup. If a custom widget does not expose the intended semantics, the locator failure can reveal an accessibility problem as well as a test problem. Correcting the widget’s role and name often improves both the test and the user experience.

Use labels and explicit test IDs intentionally

Form fields

getByLabel follows the field’s accessible label and remains readable when the surrounding markup changes:

await page.getByLabel('Email address').fill('[email protected]');
await page.getByLabel('Password').fill('correct-horse-battery-staple');

A placeholder can be useful when it is the product’s deliberate cue, but placeholders often change for copy or localization reasons. Prefer a stable label when one exists. The same principle applies to images and titles: use getByAltText or getByTitle when that attribute is the meaningful interface contract, not simply because it is convenient to query.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When a test ID is the right contract

Use getByTestId when a visible label is not the behavior under test, when copy is expected to change, or when a component needs a stable hook for a non-semantic element. The default attribute is data-testid; Playwright can be configured to use another attribute if your project has an established convention.

await page.getByTestId('checkout-summary').isVisible();

A test ID is not automatically better than a role. It is an explicit engineering contract: the application team must preserve the identifier wherever tests depend on it. Give IDs meaningful names tied to the component or behavior, not to a CSS class or implementation detail. An identifier such as checkout-summary communicates intent; an identifier copied from a generated class name does not.

Should I use getByRole or getByTestId?

Use getByRole when the role and accessible name represent the behavior a user should perceive. Use getByTestId when a stable, deliberate hook is the better contract, especially for non-semantic regions or text that changes independently of behavior. The decision is not a contest between APIs. Ask which choice remains unique and meaningful under the changes your team expects, then apply that choice consistently.

Scope repeated content before selecting its child

Lists, tables, product cards, and navigation menus commonly contain identical buttons. First identify the correct object, then find the control inside that object. Chaining and filtering keep the relationship visible in the test.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const product = page.getByRole('listitem').filter({ hasText: 'Product 2' });
const addToCart = product.getByRole('button', { name: 'Add to cart' });

await expect(product).toHaveCount(1);
await addToCart.click();
await expect(page.getByRole('status')).toHaveText('Added to cart');

The parent locator captures the intended product, the child role and name capture its action, and the status assertion waits for the user-visible result. The exact list semantics and status wording must match your application.

Filter by text or by a child locator

filter({ hasText }) is useful when the identifying content is visible in the component. filter({ has }) lets you identify a parent by a descendant locator, which is valuable when text is duplicated or not the best contract:

const row = page.getByRole('row').filter({
  has: page.getByRole('cell', { name: 'INV-1042' })
});
await row.getByRole('button', { name: 'Download' }).click();

Keep the filter inside the same component scope. A locator passed to has should describe a descendant of the candidate parent, not an unrelated page element. If several rows can legitimately contain the same visible text, combine a distinctive child, a parent region, or a test contract until the intended row is unique.

Verify uniqueness deliberately

An assertion such as toHaveCount(1) documents an invariant and fails close to the cause when duplicate content appears. It is especially useful for a product, invoice, or dialog that the scenario assumes is unique. Do not add count assertions mechanically to every locator; use them where uniqueness is part of the requirement or where a repeated component is likely to grow.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why is my Playwright locator strict?

Playwright locators are strict for operations that act on one target. A message saying a locator resolved to multiple elements means the test’s intent is underspecified, not that strictness should be disabled. Fix the selector in this order:

  1. Give the role an accessible name.
  2. Locate a meaningful region, card, row, dialog, or list item.
  3. Filter that region by distinctive text or a child locator.
  4. Assert the expected count when uniqueness is part of the scenario.

first(), last(), and nth() can bypass strictness, but page changes can shift their targets. Use positional selection only when order is itself the requirement, such as “activate the second result in the ranked list.” Otherwise, a new banner or inserted row can make the test click a different element without producing an obvious failure.

// Intentional only when the scenario specifies list order:
await page.getByRole('listitem').nth(1).getByRole('link').click();

If the order is not a business rule, do not turn a duplicate match into an index. A precise parent and child relationship usually communicates more intent and survives insertion of unrelated controls.

Synchronize with web-first assertions, not sleeps

Actions such as click and fill include auto-waiting for actionability. They do not, however, prove that an asynchronous business operation completed. After an action, assert the state the user should see:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.getByRole('button', { name: 'Submit order' }).click();
await expect(page.getByRole('status')).toHaveText('Order submitted');

Web-first assertions such as toBeVisible(), toHaveText(), and toHaveCount() retry until the condition passes or the assertion timeout is reached. The current Playwright Assertions guide states a default assertion timeout of five seconds; project configuration can override it. Carry that qualification into your own troubleshooting: a timeout may reflect a slow environment, a wrong expected state, or an application defect.

Assert the business outcome, not just an intermediate action

A click can succeed while a request fails, a dialog remains open, or a confirmation is never rendered. Assert the resulting heading, status message, URL, row count, or enabled state that represents success for the user. Avoid replacing that assertion with a fixed delay such as waitForTimeout; a sleep is either too short on a slow run or unnecessarily long on a fast one.

For a dialog, assert that the dialog is visible and contains the expected title or confirmation. For a deletion, assert that the targeted row disappears or that the empty state appears. For a form, assert the validation message or successful navigation that defines completion. Choose the state that proves the requirement rather than a low-level event that merely precedes it.

Use one-time reads only when a snapshot is intended

Immediate state reads and enumeration methods return what is true at that instant. They are appropriate for diagnostics or for data that is already known to be stable, but they do not wait for a future value. If synchronization matters, use an awaited web-first assertion first. An immediate read followed by a manual loop can create a race in which the test observes a loading state and proceeds with incomplete data.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle dynamic lists safely

locator.all() returns the matches currently present and does not wait for a list to finish rendering. On a list that is still receiving rows, the result can vary from run to run. Establish a meaningful ready condition before enumerating:

const results = page.getByRole('listitem');
await expect(results).toHaveCount(10);
const loadedResults = await results.all();

Use a count, a “loaded” status, or another application-specific condition that signals stability. If the number is intentionally variable, assert a minimum, a visible empty state, or the disappearance of a loading indicator before calling all(). The condition must describe what “ready” means for that page; a generic delay does not.

When possible, interact with a locator directly instead of converting the list to an array. Direct locator operations preserve Playwright’s retry and re-resolution behavior. If you need to inspect every item, make the list’s expected state explicit first, then enumerate the stable result.

Understand what auto-waiting does—and does not do

Locator actions wait for conditions such as attachment, visibility, and actionability. Locator re-resolution also helps when frameworks replace DOM nodes during rendering. These features remove many explicit waits, but they cannot:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Choose between two matching buttons.
  • Infer which of several similar cards contains the intended item.
  • Know that a server-side operation produced the business result your test expects.
  • Make a stale or accidental CSS relationship into a product contract.

Design the locator to identify the right element, then design the assertion to verify the right outcome. Treat these as separate responsibilities. If a test still needs an explicit wait, first ask whether it can be replaced by a locator action or a web-first assertion tied to a visible state.

Know when lower-level selectors are justified

CSS and XPath selectors are sometimes necessary for third-party widgets, SVG structures, or elements with no accessible semantics and no test hook. Keep them as local and intention-revealing as possible:

const canvas = page.locator('canvas[data-chart="revenue"]');
await expect(canvas).toBeVisible();

Do not choose a long chain of classes merely because it is easy to copy from developer tools. Such a chain describes implementation structure and can break after a harmless refactor. If a component is important to the product flow, consider adding an accessible name or an explicit test ID instead. When a lower-level selector is unavoidable, scope it to a stable parent and follow the interaction with an assertion that proves the visible result.

Migration and maintenance patterns

Older tests often retain element handles, one-time reads, and explicit waits from an earlier synchronization model. Prefer Locator objects throughout the flow:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const submit = page.getByRole('button', { name: 'Submit order' });
await submit.click();
await expect(page.getByRole('status')).toHaveText('Order submitted');

Keeping the locator object and resolving it at action time lets Playwright work with the current DOM after a render. Replace manual polling and fixed sleeps with assertions that describe the expected state. This makes a timeout actionable: the failure points to a missing or incorrect state instead of an arbitrary elapsed interval.

Review locator code when a component changes. If a strictness failure appears, improve meaning and scope rather than immediately adding first(). If a test ID changes, treat that as a contract change and update the owning component and its tests together. If copy changes frequently, use a stable label, role, or test ID where that better represents the behavior.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A practical locator review checklist

  1. Can the target be described by a user-facing role and accessible name?
  2. If it is a field, does a stable accessible label identify it?
  3. If visible wording is volatile, is there an intentional test ID?
  4. Does the locator resolve to exactly one element in the relevant region?
  5. For a repeated component, is the parent selected before its child action?
  6. Is any first(), last(), or nth() justified by an explicit order requirement?
  7. After the action, does an awaited assertion verify the user-visible or business outcome?
  8. If all() is used, has the list reached a defined ready state first?
  9. Would a normal copy, layout, or re-rendering change break the locator unnecessarily?
  10. Does the code rely on a Locator rather than an old element handle?

Common failure patterns and their fixes

Broad locator plus click

getByRole('button').click() fails when several buttons match. Add the accessible name, then scope to the relevant dialog, card, or region.

Positional locator as a shortcut

first() or nth() can silently select a different element after a new control is inserted. Reserve positional methods for scenarios in which order is intentional and asserted by the requirement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Selector tied to markup without need

CSS and XPath can be valid fallbacks, but a role, label, text locator, or explicit test ID usually communicates intent more clearly. Chaining locators is preferable to one opaque selector when a relationship matters.

One-time state check used for synchronization

An immediate state read does not retry to a future value. Use an awaited web-first assertion when the test must wait for visibility, text, count, or another supported condition.

Calling all() while the list is changing

Because all() does not wait, it can capture a partial list. Wait for a count, loading indicator, empty state, or other application-specific ready signal first.

ElementHandle-first migration code

Locator objects and web-first assertions are the recommended direction for modern Playwright tests. Many explicit waits are unnecessary when actions and assertions already wait for the relevant conditions.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently asked questions

How do I avoid flaky Playwright tests?

Use meaningful, unique locators; scope repeated controls to their parent component; avoid positional shortcuts unless order is part of the requirement; and await assertions for the resulting UI state. Auto-waiting helps with readiness, but it cannot repair ambiguity or verify a failed business operation.

Why does a locator become strict after a harmless UI change?

The change likely introduced another element with the same role, text, or test ID. Treat the error as feedback that the selector is underspecified. Add an accessible name, a meaningful parent scope, or a distinctive filter, and assert uniqueness when it matters.

Is a test ID more stable than a role locator?

It can be more stable when visible copy changes or when the element has no useful semantics, but it is only stable if the application team maintains the contract. A role and accessible name is usually more meaningful when that is the behavior users should perceive.

When should I use getByText?

Use it when the visible wording itself is the interface contract you need to verify or interact with. If the wording changes independently of behavior, prefer a stable label, role/name combination, or deliberate test ID.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use nth() in a reliable test?

Yes, when the scenario explicitly depends on order, such as selecting the second ranked result. Otherwise, index-based selection can shift when a row, banner, or control is inserted. Prefer a parent filter that identifies the intended item.

Why is locator.all() flaky?

It returns the matches present immediately and does not wait for a dynamic list to settle. First await a meaningful condition such as an expected count, loaded status, empty state, or disappearance of a loading indicator.

Is a five-second assertion timeout guaranteed?

Playwright’s current Assertions guide states a default assertion timeout of five seconds, but project configuration can override it. A timeout should prompt you to check the expected state, environment speed, locator uniqueness, and application behavior rather than simply increasing the delay.

Do locator actions prove that a request succeeded?

No. An action proves that Playwright performed an actionable interaction. Assert the resulting status, heading, URL, row count, dialog state, or other user-visible outcome that represents success for the scenario.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

What is the best first locator for a button?

Use getByRole('button', { name: '...' }) when the role and accessible name identify the intended action uniquely.

How should I fix a strict locator error?

Refine the role and accessible name, scope to a meaningful parent, filter by distinctive content or a child locator, and use positional methods only when order is an explicit requirement.

Should I use getByRole or getByTestId?

Choose the strategy that best expresses the contract. Prefer role and accessible name for user-facing behavior; use a maintained test ID for stable hooks where visible semantics are unavailable or intentionally variable.

How do I wait for a dynamic list?

Await a meaningful ready condition, such as an expected count, loaded status, empty state, or removal of a loading indicator, before calling all().

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What should I assert after clicking a control?

Assert the user-visible or business outcome, such as a status message, heading, URL, row count, enabled state, or dialog transition, with an awaited web-first assertion.

The Bottom Line

Build flake-resistant Playwright tests around intent: choose a meaningful role, name, label, text, or deliberate test ID; make the target unique through scope and filtering; and verify the resulting UI state with an awaited assertion. Locators can re-resolve through re-renders and actions can auto-wait for readiness, but only precise targeting and outcome assertions demonstrate that the intended workflow succeeded.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

End-to-End TestinglocatorsPlaywrightsoftware testingtest automation

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.