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');
LocatorMatchesNotes
getByRoleAn ARIA role plus its accessible nameBest default, and it aligns with accessibility
getByLabelA form control by its labelIdeal for inputs and selects
getByTextVisible text, substring by defaultPass { exact: true } to tighten it
getByTestIdThe data-testid attributeConfigure the attribute name if yours differs
locator('css=...')Any CSS selectorLast 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.

Install and first test Runner, fixtures and CI

Last refreshed 2026-09-18.