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.
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.
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.
Mental model
Asking a person to 'press the green Submit button at the bottom of the form'
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.
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.
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.
Examples
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.
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.
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.
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).
Common issues
Error: strict mode violation: locator('button') resolved to 3 elementsThe locator matches more than one element and Playwright refuses to guess.
Add a filter, scope to a container, use getByRole with { name: ... }, or narrow with .filter({ hasText: ... }).
locator.waitFor: element is not visibleThe element exists in the DOM but is hidden (display:none, zero size, or off-screen).
Assert with toBeVisible and check the element's CSS; you may be targeting the wrong element or an earlier step never completed.
Element not found after a route changeThe locator was captured before React or Vue re-rendered the tree.
Locators are resolved lazily at action time, so re-query them or wait for the expected state with toHaveText before acting.
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.
Interview checkpoint
Hands-on lab
- 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
Quick quiz
Which locator should you prefer to find a text input that has a visible <label>?
Locators resolve their elements immediately when they are created.
A button matching 'Save' appears twice on a page and Playwright throws a strict mode violation. What is the cleanest fix?
Cheat sheet
getByRole('button', { name: 'Save' })ARIA role + accessible namegetByLabel('Email')Label-associated controlgetByText('Total')Visible text matchgetByTestId('card')data-testid attributegetByPlaceholder('Search')placeholder attributegetByTitle('Close')title attributelocator.filter({ hasText: 'Draft' })Keep matching elements that contain textlocator.filter({ has: otherLocator })Keep elements containing another locatorlocator.nth(0)Indexed matchcontainer.locator('button')Scope query to a containerlocator.count()Number of matches