1. expect() auto-retries
Playwright's expect() assertions auto-retry. expect(locator).toBeVisible() polls the page until the assertion passes or the timeout (default 5s) fires — no manual waitFor needed.
await expect(page.getByRole('alert')).toBeVisible();
// Polls every ~100ms for up to 5s until the alert appears.2. Common matchers
The matchers you'll use 90% of the time:
toBeVisible— element is rendered and visibletoHaveText— exact text matchtoContainText— substring matchtoHaveValue— input valuetoHaveURL— current page URLtoHaveCount— number of matched elementstoHaveAttribute— DOM attribute value
await expect(page.getByRole('heading')).toHaveText('Welcome');
await expect(page.getByLabel('Email')).toHaveValue('qa@example.com');
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('link', { name: 'Docs' })).toHaveAttribute('href', '/docs');3. Negative matchers
Chain .not to assert the opposite. Negative matchers also auto-retry until the condition becomes false (or the timeout fires).
await expect(page.getByRole('progressbar')).not.toBeVisible();
await expect(page.getByRole('status')).not.toHaveText('Error');4. Soft assertions
Use expect.soft() when you want the test to keep running after a failure and report every problem at the end — perfect for verifying a results page with many fields.
await expect.soft(page.getByTestId('order-id')).toHaveText('ORD-1001');
await expect.soft(page.getByTestId('total')).toHaveText('$42.00');
await expect.soft(page.getByTestId('status')).toHaveText('Confirmed');
// Test continues even if one fails; all failures reported at the end.5. Custom matchers
Extend expect with domain-specific matchers to keep tests readable.
import { expect } from '@playwright/test';
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} to be within ${floor}..${ceiling}`,
};
},
});
// usage
expect(99.5).toBeWithinRange(0, 100);6. Hands-on task
Take 5 manual checks from an existing test and convert them into auto-retrying assertions. Drop every waitForTimeout and if (await locator.isVisible()) pattern in favour of expect().
// Before
await page.waitForTimeout(2000);
if (await page.locator('.toast').isVisible()) {
console.log('saved');
}
// After
await expect(page.getByRole('status')).toHaveText('Saved');7. What's next
Next, move into Module 05: Page Object Model to structure your tests for a real product.
Up next in the learning path
Module 05: Page Object Model