Glossary · UI automation
Web-first assertion
Also called: web-first assertions, auto-retrying assertion, auto-retrying assertions
Definition
A Playwright assertion that takes a locator and retries until the expected condition is met or the timeout expires, instead of checking a value once.
A web-first assertion is an assertion that understands the page can still be changing. You pass it a locator and an expectation, and Playwright keeps re-reading the element and re-checking the expectation until it passes or the expect timeout (5 seconds by default) runs out. The name comes from Playwright’s documentation, which recommends these assertions over reading a value and comparing it yourself.
Example
import { test, expect } from '@playwright/test';
test('shows the saved message', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('button', { name: 'Save' }).click();
// Web-first: retries until the text matches
await expect(page.getByRole('status')).toHaveText('Settings saved');
await expect(page.getByRole('progressbar')).toBeHidden();
await expect(page.getByRole('listitem')).toHaveCount(3);
await expect(page).toHaveURL(/\/settings$/);
});
How it differs from a plain assertion
// Not web-first: reads the text once, then compares
expect(await page.getByRole('status').textContent()).toBe('Settings saved');
// Not web-first: isVisible() returns immediately
expect(await page.getByRole('dialog').isVisible()).toBe(true);
Both lines check the page at a single instant. If the status message renders 200 ms later, the test fails, and that kind of failure is intermittent, so you have a flaky test. The web-first versions, toHaveText and toBeVisible, wait for the condition.
A quick way to tell them apart: web-first assertions are awaited and receive a Locator or Page. Generic matchers such as toBe and toEqual receive a plain value and never retry.
Useful details
- Negation retries too.
await expect(locator).not.toBeVisible()waits for the element to disappear. - Per-assertion timeouts.
toHaveText('Done', { timeout: 15_000 })for one slow step, rather than raising the global timeout. - Soft assertions.
expect.soft(...)records the failure and lets the test continue, which is handy for checking several fields on one screen. - Custom polling.
expect.poll(() => fetchStatus()).toBe('ready')applies the same retry behaviour to non-UI values, such as an API response.