beginner19 min

Locators: Finding Elements the Way a User Would

Why accessible, role-based locators are Playwright's recommended default, and how to rank a set of candidate locators by real-world stability.

What you'll learn

  • Use role-based, accessible locators as the default choice for finding elements
  • Explain why a locator is a lazy, re-evaluated query rather than a one-time element reference
  • Rank competing locator strategies by stability and honest user-facing intent

Prerequisites

Explanation

A Playwright locator (page.getByRole("button", { name: "Submit" })) is not a reference to a specific DOM element captured at the moment you write the line — it's a lazy, re-evaluated query: every time an action or assertion uses that locator, Playwright re-runs the query against the current page. This is the mechanism behind auto-waiting (covered next lesson): the locator doesn't fail immediately if the element isn't there yet, it keeps re-querying until the element appears (or a timeout elapses), which is fundamentally different from most older automation tools' "find the element once, fail immediately if it isn't there yet" model.

Playwright's recommended default is a role-based, accessible locator: page.getByRole("button", { name: "Submit" }), page.getByLabel("Email address"), page.getByText("Welcome back"). These target the page the way a real user (or an assistive-technology user) actually perceives it — by role and visible/accessible label — rather than by internal implementation details like a CSS class name or a DOM structure. This has a genuine double benefit: the tests are more resilient to internal refactors (a CSS class renamed for styling reasons doesn't break a role-based locator), and writing tests this way tends to surface real accessibility gaps in the application under test, since an element with no discoverable role or accessible name is exactly as hard for getByRole to find as it is for a screen reader user to identify.

Locator stability is a genuine, orderable spectrum, worth reasoning about explicitly rather than reaching for whatever "just works" first: role/label/text-based locators (most stable — tied to what users actually perceive) > a dedicated data-testid attribute (stable, but requires deliberately adding test-only markup) > a specific, meaningful CSS selector (.submit-button, moderately stable — breaks if the class is renamed for styling reasons) > a deep, structural CSS or XPath selector (div > div:nth-child(3) > button, least stable — breaks on almost any layout change, and describes where an element sits rather than what it is). Reaching for the least-stable option first is a common, understandable mistake under time pressure that reliably produces the flakiest, most maintenance-heavy tests in a suite.

Example

Ranking candidate locator strategies by stability -- the actual reasoning behind Playwright's recommended locator priority.

function locatorStabilityScore(strategy) {
  const scores = {
    role: 4,       // page.getByRole(...) -- tied to user-perceivable semantics
    testId: 3,     // page.getByTestId(...) -- stable, but test-only markup
    cssClass: 2,   // page.locator(".submit-button") -- breaks on style refactors
    structural: 1, // page.locator("div > div:nth-child(3) > button") -- breaks on almost any layout change
  };
  return scores[strategy] ?? 0;
}

const candidates = ["structural", "role", "cssClass", "testId"];
const ranked = [...candidates].sort((a, b) => locatorStabilityScore(b) - locatorStabilityScore(a));
console.log(ranked); // ["role", "testId", "cssClass", "structural"] -- most to least stable

Try it yourself

Add a fifth strategy 'label' (page.getByLabel) with a score matching role's stability, and re-rank the list.

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

Write rankLocators(candidates) that sorts an array of locator-strategy strings ('role','testId','cssClass','structural') from MOST to LEAST stable, using the stability scores from this lesson's explanation.

Checks: correctly ranks all four strategies by stability · does not mutate the original input array

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

Write isAccessibleLocatorStrategy(strategy) returning true only for 'role', 'label', or 'text' (Playwright's user-facing, accessible locator strategies), false for anything else (including 'testId', 'cssClass', 'structural', or any unrecognized string).

Checks: recognizes role as accessible · recognizes label as accessible · correctly rejects a non-accessible strategy · correctly rejects an unrecognized strategy

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Reaching for a deep structural CSS or XPath selector first, because it 'just works' in the moment -- these are the most fragile locators and break on almost any unrelated layout change.
  • Treating a locator as a one-time snapshot of an element rather than a re-evaluated query -- this misunderstanding is exactly what makes Playwright's auto-waiting (next lesson) seem confusing at first.
  • Adding data-testid attributes everywhere by default instead of first checking whether a real accessible role/label already exists -- role-based locators are both more stable AND double as an accessibility check; testId is a reasonable fallback, not a default.

Knowledge check

Knowledge check

1. Why is `page.getByRole("button", { name: "Submit" })` Playwright's recommended default over a CSS class selector?
2. Is a Playwright locator a reference to one specific element captured when the line is written?
3. Which locator strategy is generally the LEAST stable, most likely to break on an unrelated change?

Takeaway

A locator is a lazy, re-evaluated query, not a one-time reference — and role-based, accessible locators are Playwright's recommended default because they're both the most stable strategy across refactors and a genuine, incidental accessibility check.

Summary

Locators (getByRole, getByLabel, getByText, getByTestId, CSS/XPath) are re-evaluated every time they're used, not captured once. Role-based/accessible locators are the most stable and are Playwright's recommended default. Structural CSS/XPath selectors are the least stable, breaking on unrelated layout changes.

References

Your notes

Notes save automatically.

Finished this lesson?

Mark it complete to track your progress and schedule a future review.