Playwright Assertions: expect(), toBeVisible, toHaveText & Screenshots
Assertions decide whether a test passes. Playwright's expect() has a feature most assertion libraries don't: web-first assertions retry automatically until the page reaches the expected state or the timeout expires. This guide covers the assertions you'll use daily — page, element, form, dropdown, API — plus soft assertions, polling, and screenshots. The API, soft and polling examples were run for real; the output is shown.
Two Kinds of expect()
| Web-first (async) assertions | Generic (value) assertions | |
|---|---|---|
| Checks | A locator, page or API response | A plain value you already have |
| Retries? | ✅ Yes — until it passes or times out (5 s default) | ❌ No — checked once |
| Syntax | await expect(locator).toHaveText('Paid') |
expect(total).toBe(43.18) |
| Examples | toBeVisible, toHaveText, toHaveURL, toHaveCount, toBeOK |
toBe, toEqual, toContain, toMatch, toBeGreaterThan |
The most common source of flaky Playwright tests is using a generic assertion on page data:
expect(await locator.textContent()).toBe('Paid')
This reads the text once, so it fails if the update arrives a moment later. Assert on the locator instead:
await expect(locator).toHaveText('Paid')
More: Playwright Auto-Wait.
Page Assertions
await expect(page).toHaveURL(/\/dashboard$/); // regex — robust to domains and query strings
await expect(page).toHaveURL('https://shop.example.com/dashboard');
await expect(page).toHaveTitle(/Dashboard/);
toHaveURL keeps retrying while redirects happen, so it's the right check after login or form submission.
Element Assertions
| Assertion | Checks |
|---|---|
toBeVisible() / toBeHidden() |
Shown / not shown (or absent) |
toHaveText('…') |
Full text (exact; whitespace normalized). Accepts a regex or an array for lists |
toContainText('…') |
Text includes the substring |
toHaveCount(n) |
Number of matching elements |
toBeEnabled() / toBeDisabled() |
Interactive or not |
toBeChecked() |
Checkbox or radio selected |
toHaveValue('…') |
An input's current value |
toHaveAttribute('href', /terms/) |
An attribute's value |
toHaveClass(/active/) |
CSS class |
toBeFocused(), toBeEditable(), toBeInViewport() |
Focus, editability, on-screen |
await expect(page.getByRole('heading', { name: 'Welcome, Naveed' })).toBeVisible();
await expect(page.getByTestId('message')).toContainText('successfully');
await expect(page.getByTestId('cart-item')).toHaveCount(3);
await expect(page.getByRole('button', { name: 'Pay' })).toBeDisabled();
await expect(page.getByLabel('Email')).toHaveValue('asha@example.com');
// Lists: check every item in order with one assertion
await expect(page.getByTestId('cart-item-name')).toHaveText(['Backpack', 'Bike Light', 'T-Shirt']);
Every assertion can be negated with .not (await expect(spinner).not.toBeVisible()) and given a longer timeout for one slow step: toBeVisible({ timeout: 15_000 }).
Dropdowns
const country = page.getByLabel('Country');
await country.selectOption({ label: 'India' });
await expect(country).toHaveValue('in'); // the selected option's value
await expect(country.locator('option')).toHaveText(
['India', 'Japan', 'United States']
); // all options, in order
await expect(page.getByLabel('Skills')).toHaveValues(['java', 'sql']); // multi-select
Assert the selected value with toHaveValue, and the list of options with toHaveText([...]) on the option locator — that also catches missing, extra or reordered options. For custom (non-select) dropdowns, assert the text shown in the combobox.
More: Playwright Locators & Actions.
API Assertions
These use Playwright's request fixture — no browser needed. This test was run against a local API:
test('API assertions and soft assertions', async ({ request }) => {
const res = await request.post('/auth', {
data: {
username: 'admin',
password: 'password123'
}
});
await expect(res).toBeOK(); // status 200–299
const body = await res.json();
expect(body).toHaveProperty('token');
expect(
body.token,
'token should be a non-empty string'
).toMatch(/^\S+$/);
const missing = await request.get('/bookings/999999');
expect(missing.status()).toBe(404);
await expect(missing).not.toBeOK();
expect.soft(
body.token.length,
'token length'
).toBeGreaterThan(100); // fails, but the test continues
expect.soft(
missing.headers()['content-type']
).toContain('application/json');
expect(test.info().errors.length).toBe(1); // one soft failure so far
});
Soft Assertions and Custom Messages
A normal assertion stops the test at the first failure. expect.soft() records the failure and carries on, so one run reports every problem on a page — useful for checking many fields of a form or a response. The test is still marked failed at the end.
In the run above:
✘ API assertions and soft assertions
Error: token length
expect(received).toBeGreaterThan(expected)
Expected: > 100
Received: 8
The lines after the soft failure still ran (the Content-Type check passed and the error count was 1). The second argument to expect() — 'token length' — became the headline of the error, which makes CI failures readable.
Use soft assertions for independent checks; use normal ones when later steps depend on the earlier check.
Polling Non-UI Values: expect.poll
await expect.poll(async () => {
const res = await request.get(`/api/orders/${orderId}`);
return (await res.json()).status;
}, {
intervals: [500, 1000, 2000],
timeout: 15_000
}).toBe('SHIPPED');
expect.poll re-runs a function until its result passes — ideal for background jobs, emails and eventually-consistent APIs. In our run, a counter polled every 100 ms passed on the third call. toPass() does the same for a whole block of steps.
Screenshots
await page.screenshot({
path: 'screenshots/checkout.png',
fullPage: true
}); // whole page
await page.getByTestId('order-summary').screenshot({
path: 'screenshots/summary.png'
}); // one element
// playwright.config.ts — automatic evidence on failure
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry'
}
Visual comparison
await expect(page).toHaveScreenshot(
'checkout.png',
{ maxDiffPixelRatio: 0.01 }
);
await expect(page.getByTestId('order-summary')).toHaveScreenshot();
The first run saves a baseline; later runs compare against it and fail on visual differences, attaching expected, actual and diff images to the report. Update baselines deliberately with:
npx playwright test --update-snapshots
Keep visual tests to stable components, and mask dynamic content (mask: [page.getByTestId('date')]) — and run them on one fixed OS/browser, because fonts render differently across machines.
More: Reports, Trace & Debugging.
Try assertions interactively — pick an element state and see which matcher fits — in the Assertion Explorer.
From Real Projects
My own automation experience is with Selenium WebDriver, Java and TestNG in a POM-based hybrid framework, running in batch, parallel and cross-browser mode. The concepts on this page carry straight over to Playwright — page objects, independent tests, parallel execution and reliable synchronization — which makes it a natural next tool for Selenium testers. The biggest difference you'll notice is how much waiting Playwright handles for you. Testing Testsigma — a platform where users write automated tests in plain English — meant thinking like its users, who are testers themselves. Use web-first assertions for anything that changes over time.
📚 Official documentation: Playwright documentation
FAQs
What assertion library does Playwright use?
Its own expect from @playwright/test — Jest-style generic matchers plus web-first matchers that retry for locators, pages and API responses.
Why do Playwright assertions need await?
Web-first assertions are asynchronous because they retry until the condition passes; without await the test moves on before the check finishes.
toHaveText or toContainText?
toHaveText for the complete text (or a regex), toContainText for a substring.
How do you validate a dropdown?
toHaveValue() for the selected option, toHaveText([...]) on the options for the full list, and toHaveValues() for multi-selects.
What are soft assertions?
expect.soft() records a failure without stopping the test, so all failures are reported together; the test still fails at the end.
How do you take a screenshot?
page.screenshot({ path, fullPage: true }) or locator.screenshot(); for automatic failure screenshots set screenshot: 'only-on-failure' in the config.