🎯 What You Will Learn
Choose resilient role, label, text, and test-id locators, then verify behavior with auto-retrying assertions.
- Choose locators in a reliable priority order
- Narrow ambiguous matches without fragile DOM paths
- Use assertions that retry until the UI reaches the expected state
- Diagnose strictness and timeout failures
Treat a Locator as a Live Query
A locator describes how to find an element at action time. Playwright resolves it again before each action, so it can survive a component re-render.
Use a User-facing Priority Order
Start with role, label, placeholder, text, alt text, or title. Use a test id when the interface needs an explicit testing contract.
| Locator | Typical use |
|---|---|
| getByRole | Buttons, links, headings, dialogs, and landmarks |
| getByLabel | Form controls with visible labels |
| getByText | Visible messages and content |
| getByTestId | Stable explicit contract when user-facing attributes are insufficient |
Locate by Role and Name
Role locators follow accessibility semantics and make the intended control clear.
await page
.getByRole('button', { name: 'Save changes' })
.click();
await expect(
page.getByRole('status')
).toHaveText('Changes saved');Locate Form Controls by Label
Labels remain understandable to users and tests. They are normally more stable than class names generated by styling systems.
await page.getByLabel('Email address').fill('[email protected]');
await page.getByLabel('Password').fill('example-password');
await page.getByRole('button', { name: 'Sign in' }).click();Narrow Ambiguous Matches
Locators are strict for single-element actions. Scope to a card, row, or dialog before selecting the nested control.
const product = page
.getByRole('listitem')
.filter({ hasText: 'Product 2' });
await product.getByRole('button', { name: 'Add to cart' }).click();Use Web-first Assertions
Locator assertions retry until the expected state appears or the assertion timeout expires. Always await them.
await expect(page.getByTestId('status')).toHaveText('Submitted');
await expect(page.getByRole('dialog')).toBeVisible();
await expect(page).toHaveURL(/\/account$/);Avoid Immediate Value Checks
Reading textContent and immediately comparing it does not retry. Prefer toHaveText when the UI changes asynchronously.
// Fragile: one immediate read.
expect(await page.getByTestId('status').textContent()).toBe('Ready');
// Reliable: retries until the expected state.
await expect(page.getByTestId('status')).toHaveText('Ready');Debug Strictness and Timeouts
A strictness error means more than one element matched. A timeout means the expected actionable or asserted state did not arrive.
- Read the error's matching elements.
- Add a role name or scope to a parent region.
- Confirm the UI state in headed or UI mode.
- Fix the application or locator before increasing timeouts.
Avoid long CSS chains: DOM structure and generated class names change easily. Prefer user-facing attributes or an intentional test id.
Recap
- Locators are live queries resolved before actions.
- Prefer role, label, text, and explicit test-id contracts.
- Web-first assertions retry and must be awaited.
- Narrow ambiguous matches by scoping to meaningful UI regions.
Checkpoint: You have completed the workflow and have a repeatable reference for your next Playwright project.