Quick answer
A Playwright assertion is expect(locator).toXxx(). Anything starting with toBe, toHave or toContain on a locator auto-waits and auto-retries until it passes or times out (default 5 s). This is why modern Playwright tests need no sleeps.
1. Why web-first assertions matter
| Old (Selenium-style) | Modern Playwright | Result |
|---|---|---|
sleep(3000); assert el.isDisplayed() | await expect(el).toBeVisible() | No sleeps, no flake |
wait.until(...).click() | await el.click() | Playwright auto-waits actionability |
| Manual retry loops | Built-in retry every ~50 ms | 5× faster, deterministic |
2. Visibility & state assertions
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page.getByText('Loading…')).toBeHidden();
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled();
await expect(page.getByRole('textbox', { name: 'Email' })).toBeEmpty();
await expect(page.getByRole('checkbox')).toBeChecked();
await expect(page.getByRole('button', { name: 'Delete' })).toBeDisabled();
await expect(page.getByRole('textbox')).toBeFocused();
await expect(page.getByRole('textbox')).toBeEditable();3. Text and value assertions
await expect(page.getByTestId('cart-total')).toHaveText('$42.00');
await expect(page.getByTestId('cart-total')).toContainText('42');
await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('a@b.co');
await expect(page.getByRole('combobox')).toHaveValues(['us', 'ca']); // multi-select
await expect(page.locator('img.logo')).toHaveAttribute('alt', 'Acme');
await expect(page.locator('button')).toHaveClass(/primary/);
await expect(page.getByTestId('badge')).toHaveCSS('background-color', 'rgb(0, 130, 251)');4. Count and collections
const rows = page.getByRole('row');
await expect(rows).toHaveCount(10);
// Assert the exact set of texts, in order
await expect(page.getByRole('listitem')).toHaveText(['Milk', 'Eggs', 'Bread']);
// Or contain-in-order
await expect(page.getByRole('listitem')).toContainText([/milk/i, /eggs/i]);5. URL, title & network assertions
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page).toHaveTitle('Dashboard — Acme');
const [resp] = await Promise.all([
page.waitForResponse('**/api/user'),
page.getByRole('button', { name: 'Refresh' }).click(),
]);
expect(resp.status()).toBe(200);
expect(await resp.json()).toMatchObject({ email: expect.any(String) });6. Soft assertions — see every failure at once
test('dashboard renders every widget', async ({ page }) => {
await page.goto('/dashboard');
await expect.soft(page.getByTestId('revenue')).toBeVisible();
await expect.soft(page.getByTestId('users')).toBeVisible();
await expect.soft(page.getByTestId('churn')).toBeVisible();
// Test still fails at the end if any soft assertion failed,
// but you see ALL broken widgets in the report — not just the first.
});7. Custom timeouts & negation
// One-off longer timeout
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 30_000 });
// Negated — wait until element disappears
await expect(page.getByText('Loading…')).not.toBeVisible();
// Custom matcher
expect.extend({
toBeValidEmail(received: string) {
const pass = /.+@.+\..+/.test(received);
return { pass, message: () => `expected ${received} to be a valid email` };
},
});
// usage: expect('a@b.co').toBeValidEmail();8. Common mistakes
await loc.isVisible() resolves once with a boolean — no retry. Always use await expect(loc).toBeVisible().
Assertions should fail loudly. If you catch them the test always passes and you lose the diff.
await page.waitForTimeout(2000) before expect is redundant and slow. Delete it — the assertion already retries.
9. Hands-on task (10 minutes)
- 1
Break a locator
Point one
getByRoleat an element that doesn't exist. Run the test and note the 5 s default timeout and screenshot in the report. - 2
Add three soft assertions
Pick a page with 3+ visible widgets. Assert each with
expect.soft. Break two on purpose and observe the report shows both. - 3
Assert URL after nav
Add
await expect(page).toHaveURL(/dashboard/)after login and delete everywaitForTimeoutyou had.
Next up: refactor these calls behind a Page Object Model.