Web-First Assertions
When to Use
Use
expect(locator)for all E2E assertions — it auto-retries until either the condition holds orexpect.timeout(default 5000ms) elapses. Useexpect.poll()for conditions outside the DOM (network state, external service).
Decision
| Assertion | Passes when |
|---|---|
toBeVisible() |
Element attached and non-empty bounding box, not display:none/visibility:hidden |
toBeHidden() |
Element detached or invisible |
toBeAttached() |
Element in the DOM (may be invisible) — for elements that exist before they animate in |
toBeEnabled() / toBeDisabled() |
Element's enabled state |
toBeChecked({ checked: false }) |
Checkbox/radio state |
toBeFocused() |
Element is document.activeElement |
toHaveText('exact') / toHaveText(/regex/) |
Text content — array form asserts multiple elements in order |
toContainText('substr') |
Substring match — most common in real tests |
toHaveValue('foo') |
Input / select value |
toHaveAttribute('name', 'value') |
DOM attribute |
toHaveCount(n) |
Number of matched elements |
toHaveCSS('background-color', 'rgb(0,0,0)') |
Computed style |
toHaveRole('button') |
Accessibility role (since 1.44) |
expect(page).toHaveURL(/foo$/) |
Auto-retries until URL matches |
expect(page).toHaveTitle('Welcome') |
Auto-retries until <title> matches |
expect(response).toBeOK() |
HTTP status 200–299 |
Pattern
// Standard web-first assertion
await expect(page.getByRole('heading')).toHaveText('Welcome');
// Negation — every matcher is negatable
await expect(page.getByText('Loading...')).not.toBeVisible();
// Soft assertion — records failure but continues test
await expect.soft(page.getByRole('heading')).toHaveText('Welcome');
await expect.soft(page.getByRole('navigation')).toBeVisible();
// Per-test timeout override
const slowExpect = expect.configure({ timeout: 30_000 });
await slowExpect(page.getByText('Long-running job complete')).toBeVisible();
Generic polling
// Poll a function until it returns the expected value
await expect.poll(async () => {
const res = await request.get('/api/health');
return res.status();
}, {
message: 'Health endpoint should become 200',
intervals: [1_000, 2_000, 5_000],
timeout: 30_000,
}).toBe(200);
// Block polling — for async chains (queue worker, search reindex, cron)
await expect(async () => {
const response = await page.request.get('/api/orders');
expect(response.status()).toBe(200);
expect((await response.json()).count).toBeGreaterThan(0);
}).toPass({ timeout: 30_000 });
Common Mistakes
- Wrong:
await locator.isVisible()for assertions — returns immediately, no auto-retry; flake source. Right:await expect(locator).toBeVisible() - Wrong:
waitForTimeout(2000)— bypasses auto-retry; replace withexpect()orwaitForResponse - Wrong: Bumping
expect.timeoutto silence flake — masks real bugs. Right: Investigate the underlying race - Wrong:
toHaveScreenshot()in functional E2E tests — that's VR, covered in the VR guide. Keep them separate
See Also
- Locators — how to target elements
- Drupal & DDEV Patterns — Drupal-specific waits (Big Pipe, AJAX, search reindex)
- Reference: Playwright Assertions