Playwright Locators & Assertions
Most end-to-end test flakiness comes from two things: brittle selectors that break when markup changes, and timing assumptions that fail when the app is a little slower than usual. Playwright addresses both. Locators describe how to find elements the way users perceive them (by role, label, and text) and are re-evaluated every time they're used. Web-first assertions automatically retry until the expected state appears or a timeout passes, with no manual sleeps or waits.
Using locators and assertions the way Playwright intends is the biggest factor in a fast, stable E2E suite. It also nudges your app toward better accessibility, because role- and label-based locators only work when the UI exposes proper semantics.
TL;DR
- Prefer user-facing locators:
getByRole(withname),getByLabel,getByPlaceholder,getByText,getByAltText,getByTitle. - Use
getByTestIdfor elements without a stable accessible identity; avoid CSS and XPath tied to structure or styling. - Locators are lazy and strict: they resolve at action time, and error if they match multiple elements for a single-element action.
- Narrow with chaining (
locator.getByRole(...)),filter({ hasText, has }),nth(), andfirst()/last()(sparingly). - Actions auto-wait for elements to be attached, visible, stable, enabled, and able to receive events.
- Use web-first assertions (
await expect(locator).toBeVisible(),toHaveText,toHaveURL…), never manual timeouts.
Quick Example
Core Concepts
Recommended Locators
getByRole is the top recommendation: it matches how users and assistive technologies perceive the page, and it survives refactors of class names and DOM structure. Options like exact, checked, pressed, expanded, level (headings), and includeHidden refine matches. Text matching is case-insensitive substring by default; use exact: true or regex for precision.
CSS and XPath (page.locator('css=…')) remain available for edge cases, but selectors like .btn-primary > span:nth-child(2) couple tests to implementation details and break often.
Chaining and Filtering
Scoping locators to a container (a dialog, a table row, a card) is the cleanest way to disambiguate repeated elements.
Strictness
Actions that target one element (click, fill, textContent) throw if the locator matches multiple elements. That's intentional: it prevents clicking the wrong "Delete" button. Fix it by making the locator more specific rather than reaching for .first(), which hides ambiguity. For lists, use count(), all(), or assertions like toHaveCount.
Auto-Waiting and Actionability
Before an action, Playwright waits until the element is:
- Attached to the DOM and visible.
- Stable (not animating).
- Enabled (for inputs and buttons) and editable (for
fill). - Receiving events (not covered by another element such as a modal overlay).
That removes most explicit waits. If an action times out, the error lists which check failed, for example "element is not stable" or "another element would receive the click".
Web-First Assertions
expect(locator) assertions retry until they pass or the timeout expires (5s by default):
Use expect.soft() to continue after a failure and report all of them, expect.poll() to retry arbitrary async checks (such as API state), and expect(...).not. for negation, which is also retried.
Best Practices
Test Like a User
Locate by what users see and interact with: roles, labels, visible text. If a locator is hard to write, the UI may lack accessible names, which is worth fixing for real users too.
Add Test IDs Deliberately
For elements without meaningful roles or text (a canvas, a dynamic count badge, repeated icons), add data-testid. Keep test IDs stable and semantic (cart-count), and treat them as part of the component contract.
Never Use Fixed Waits
page.waitForTimeout(2000) makes tests slow and flaky. Assert the condition you're waiting for (await expect(spinner).toBeHidden()), or wait for a specific response with page.waitForResponse.
Generate and Debug Locators With Tooling
npx playwright codegen records actions with recommended locators. The VS Code extension, the UI mode (--ui), and the trace viewer's "pick locator" help build and verify them.
Common Mistakes
Non-Retrying Assertions
Using first() to Silence Strictness Errors
.first() makes the test pass today, and click the wrong element tomorrow when order changes. Scope the locator to the right container instead.
Selectors Tied to Styling
page.locator('.MuiButton-root.css-1x2y3z') breaks on any CSS change or library upgrade. Use roles, labels, or test IDs.
FAQ
What's the best locator strategy in Playwright?
Prefer getByRole with an accessible name, then getByLabel, getByText, and the other user-facing locators, and use getByTestId where no stable user-facing handle exists. Avoid CSS and XPath tied to DOM structure or class names.
Why do I get a "strict mode violation" error?
Your locator matched more than one element, and you tried to perform a single-element action. Make it more specific (add a name, scope it within a container, or use filter) rather than picking an index arbitrarily.
Do I need explicit waits in Playwright?
Rarely. Actions auto-wait for elements to be actionable, and expect assertions retry until conditions are met. Explicit waits are mainly for specific network responses (waitForResponse) or events. Never use fixed sleeps.
How is Playwright different from Cypress for selectors?
Both support user-facing and test-ID selectors. Playwright's locator API emphasizes accessibility-based locators with strictness and built-in auto-waiting for every action, runs across Chromium, Firefox, and WebKit, and supports multiple tabs, origins, and contexts natively. See Cypress.
Related Topics
- Playwright — The testing framework overview
- Playwright Fixtures — Structuring tests and page objects
- Playwright Network Mocking — Controlling backend responses
- Accessibility — Roles and labels that make locators work
- Testing — Testing strategy overview
- Cypress — An alternative E2E framework