Locators and assertions
Semantic locators, filtering and chaining, and web-first assertions that wait instead of racing the page.
Locators that survive a redesign
A locator is lazy: it describes how to find an element and is resolved again on each use. That is exactly what lets automatic waiting retry it until the element is ready.
// preferred: semantics a user would recognise
page.getByRole('button', { name: 'Sign in' });
page.getByLabel('Email address');
page.getByPlaceholder('Search');
// scope to a container, then filter
const row = page.getByRole('row').filter({ hasText: 'Ada Lovelace' });
await row.getByRole('button', { name: 'Edit' }).click();
// escape hatch when nothing semantic exists in the markup
page.getByTestId('checkout-total');| Locator | Matches | Notes |
|---|---|---|
| getByRole | An ARIA role plus its accessible name | Best default, and it aligns with accessibility |
| getByLabel | A form control by its label | Ideal for inputs and selects |
| getByText | Visible text, substring by default | Pass { exact: true } to tighten it |
| getByTestId | The data-testid attribute | Configure the attribute name if yours differs |
| locator('css=...') | Any CSS selector | Last resort; breaks when the markup moves |
Web-first assertions
expect(...) retries until the condition holds or the timeout expires. That removes almost all manual waiting: you assert the end state and the framework polls for it.
await expect(page.getByRole('alert')).toHaveText('Saved');
await expect(page.getByRole('listitem')).toHaveCount(3);
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page.getByTestId('spinner')).toBeHidden();
// negate with .not
await expect(page.getByText('Error')).not.toBeVisible();⚠️
A generic
expect(await locator.textContent()).toBe('Saved') resolves the value once and never retries - the classic flaky assertion. Always assert on the locator itself so the retry applies.FAQ
When is a fixed timeout acceptable?
Almost never.
page.waitForTimeout is a sleep: assert on the condition you are actually waiting for, or the test stays slow and remains flaky.How do I debug a locator that matches nothing?
Run with
--ui or --debug and use the locator picker; it shows what each selector resolves to against the live DOM.Related
Install and first test Runner, fixtures and CI
Last refreshed 2026-09-18.