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.

Advertisement

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 }).

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.