Playwright
Beginner 13 min read Playwright

Locators & Selectors

Locators are the query system of Playwright. getByRole, getByText, getByTestId and getByLabel find elements the way users perceive them, while strict mode and chaining keep selectors precise and resilient.

locatorsgetByRolegetByTestIdgetByTextstrict modecss selectorslocator chaining
Introduction

What is this?

A locator is a pointer to one or more elements that Playwright resolves lazily at action time. Playwright prefers user-facing queries such as getByRole and getByText because they survive CSS changes, and it enforces strict mode so a query matching several elements throws instead of guessing. Locators can be chained and filtered for deep precision.

Motivation

Why do you need it?

  • getByRole and getByLabel mirror how assistive technology and users actually find controls, so tests verify accessibility as a side effect.
  • User-facing locators are robust: renaming a CSS class or reshuffling markup does not break the test.
  • Strict mode fails fast when a selector is ambiguous, exposing real usability problems instead of silent wrong clicks.
  • Locators resolve lazily and retry automatically, so they pair naturally with dynamic apps.
  • Chaining lets you scope a locator inside a section, card or row, eliminating brittle mega-selectors.
Think about it

Mental model

Imagine

Asking a person to 'press the green Submit button at the bottom of the form'

Think of

getByRole('button', { name: 'Submit' })

Both use role and accessible name rather than pixel coordinates or markup position. If the button moves or restyles, the instruction still works.

Mechanics

How it works

  • You declare a locator with a query engine such as getByRole, getByText or page.locator(css) — no elements are fetched yet.
  • When an action or assertion runs, Playwright evaluates the query against the current DOM and checks strict mode.
  • If several elements match, strict mode throws and tells you how many matched and offers filter() or first().
  • Auto-waiting then applies: Playwright polls until the matched element is attached and actionable.
  • The resolved element performs the requested action or feeds the assertion's value.

How a locator goes from query string to a clicked element.

Declare locator
Lazy resolution
Query the DOM
Strict mode check
Auto-wait
Act on element
Reference

Key commands

npx playwright codegen <url>

Open the inspector to click through the app and generate robust locators automatically.

clicking '#submit' → locator: page.getByRole('button', { name: 'Sign in' })
page.getByRole('button', { name: 'Save' })

Find an element by its ARIA role and accessible name.

page.getByText('Order total')

Match elements by visible text content, case-insensitively by default.

page.getByTestId('user-card')

Match elements by data-testid attribute — the stable hook teams add intentionally.

page.locator('ul.items li').filter({ hasText: 'In stock' })

Chain a CSS locator with a text filter to narrow to a specific row.

page.locator('input').nth(1)

Pick a specific match by index when filters are impractical.

Hands-on

Examples

Locator types and chaining

Locator types and chaining
import { test, expect } from "@playwright/test";
test("uses user-facing locators", async ({ page }) => {
await page.goto("https://example.com/login");
const email = page.getByLabel("Email address");
await email.fill("alice@example.com");
const password = page.locator("input[name=password]");
await password.fill("hunter2");
const form = page.locator("form#signin");
const submit = form.getByRole("button", { name: "Sign in" });
await submit.click();
const toast = page.getByTestId("toast-message");
await expect(toast).toHaveText("Welcome back, Alice!");
});

getByLabel finds the input via its associated <label>, which also validates the form is accessible. Chaining scopes getByRole('button') inside the sign-in form so it can't collide with other buttons. getByTestId reads a data-testid attribute, a stable hook that survives styling and copy changes.

Real world

In production

  • Form suites rely on getByLabel to catch missing <label> elements, turning every test into an a11y check.
  • Tables filter and scope locators per row so sorting or pagination changes don't break row assertions.
  • data-testid is applied to dynamic widgets (toasts, drag handles, virtualized lists) where roles collide.
  • Locator .getByRole with exact: true matches user-facing names, keeping tests readable in code review.
Guidelines

Best practices

  • Prefer user-facing queries — role, label, placeholder, text — over raw CSS selectors.
  • Add data-testid only for genuinely dynamic elements; don't sprinkle it everywhere.
  • Always scope chained locators to a container to keep strict mode happy and intent clear.
  • Prefer the visible text over attribute values; assert on what users see.
  • Use getByRole with name matching over xpath — xpath is brittle and hard to review.
Watch out

Common mistakes

  • Using absolute CSS like div:nth-child(3) > span:nth-child(2) that breaks on the first layout tweak.
  • Reaching for page.locator('text=Login') when getByRole('link', { name: 'Login' }) is precise.
  • Ignoring strict-mode errors and 'fixing' them with .first(), which hides ambiguity.
  • Copy-pasting element IDs that are re-generated on every render (e.g., dynamically hashed ids).
Root cause analysis

Common issues

Error
Error: strict mode violation: locator('button') resolved to 3 elements
Root cause

The locator matches more than one element and Playwright refuses to guess.

Fix

Add a filter, scope to a container, use getByRole with { name: ... }, or narrow with .filter({ hasText: ... }).

Error
locator.waitFor: element is not visible
Root cause

The element exists in the DOM but is hidden (display:none, zero size, or off-screen).

Fix

Assert with toBeVisible and check the element's CSS; you may be targeting the wrong element or an earlier step never completed.

Error
Element not found after a route change
Root cause

The locator was captured before React or Vue re-rendered the tree.

Fix

Locators are resolved lazily at action time, so re-query them or wait for the expected state with toHaveText before acting.

When things break

Debugging tips

  • Re-run the failing test with npx playwright test --debug and inspect the locator inspector's picker.
  • Run npx playwright codegen <url> and use the point-and-click picker to verify a locator matches what you expect.
  • Log the count: await console.log(await locator.count()) to see how many elements a query matches.
  • Use locator.allTextContents() in a temporary test to dump what a broad locator actually resolves to.
Interviews

Interview checkpoint

Practice

Hands-on lab

Locator Lab: Find, Scope and Refine
30 min · beginner
In progress
Objectives
  • Query a page with getByRole, getByLabel, getByText and getByTestId
  • Refine ambiguous locators with filters and scoping
  • Use codegen to generate robust locators for a real form
  • Resolve a strict mode violation with a narrowed locator

Locator Lab: Find, Scope and Refine

30 min · beginner

0/4 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/3
1

Which locator should you prefer to find a text input that has a visible <label>?

2

Locators resolve their elements immediately when they are created.

3

A button matching 'Save' appears twice on a page and Playwright throws a strict mode violation. What is the cleanest fix?

Quick reference

Cheat sheet

Locator engines
getByRole('button', { name: 'Save' })ARIA role + accessible name
getByLabel('Email')Label-associated control
getByText('Total')Visible text match
getByTestId('card')data-testid attribute
getByPlaceholder('Search')placeholder attribute
getByTitle('Close')title attribute
Refining matches
locator.filter({ hasText: 'Draft' })Keep matching elements that contain text
locator.filter({ has: otherLocator })Keep elements containing another locator
locator.nth(0)Indexed match
container.locator('button')Scope query to a container
locator.count()Number of matches
Go deeper

Official documentation