PLAYWRIGHT UIINTERMEDIATE

Playwright Assertions: The Complete Guide with Real-World Examples

Master Playwright assertions with practical examples including toHaveText(), toBeVisible(), toHaveURL(), toBeEnabled(), toHaveValue(), soft assertions, and enterprise best practices.

iff Solution Academy July 8, 2026 18 min read Updated July 8, 2026
Playwright Assertions expect toHaveText toBeVisible Best Practices

What are Assertions?

An assertion is the moment in a test where you stop performing actions and start verifying outcomes. Without assertions, a Playwright script is just a browser automation — it clicks, types, and navigates, but never says 'this is right' or 'this is wrong'. Assertions are what turn automation into testing.

In Playwright, assertions live on the expect() function imported from @playwright/test. They wrap a locator or a page and compare its live state to an expected value — visible, enabled, containing certain text, at a certain URL. If the condition never becomes true within the timeout, the assertion fails and the test is reported red.

Why Playwright Assertions Are Better

The single biggest reason Playwright assertions are more reliable than the manual expect(await locator.textContent()).toBe('Welcome') pattern from older tools is auto-retry. Playwright assertions poll the DOM until either the condition passes or the assertion timeout expires (5 seconds by default), so a slightly-late render does not fail your test.

  • Auto-waiting eliminates the vast majority of timing-related flakes.
  • Rich diff output shows exactly what was expected vs. what was found.
  • Assertion messages point directly at the failing locator, not a mystery line.
  • Uniform API — every assertion is expect(target).toBeSomething().
  • Works seamlessly with Playwright's Trace Viewer for post-mortem debugging.

Prerequisites

  • Node.js 18+ and a Playwright project (npm init playwright@latest).
  • Basic familiarity with locators (getByRole, getByLabel, getByTestId).
  • Comfort with async/await syntax — every assertion is awaited.
  • A target site to test — the Playwright docs use https://demo.playwright.dev/todomvc.

expect()

Every Playwright assertion starts with expect(), imported from @playwright/test. Pass it a Page or a Locator, then chain a matcher.

example.spec.ts
import { test, expect } from '@playwright/test';

test('Verify title', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

toBeVisible()

Verify an element is present in the DOM AND rendered on screen (non-zero size, not hidden by CSS).

ts
await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();

Pair it with toBeHidden() for the opposite check. Both auto-retry, so you can call them immediately after a click that triggers a modal — no waitFor needed.

toHaveText() vs toContainText()

toHaveText() requires the element's text to match exactly (after whitespace normalization). Use it when the full label is deterministic.

ts
await expect(page.locator('.message')).toHaveText('Welcome');

toContainText() checks for a substring — perfect when the surrounding text includes timestamps, user names, or other dynamic content.

ts
await expect(page.locator('.message')).toContainText('Welcome');

Both matchers accept an array to assert against multiple sibling elements, and both accept regular expressions for pattern matching.

toHaveValue()

For inputs, textareas, and selects — verifies the current form value, not the visible text.

ts
await expect(page.locator('#email')).toHaveValue('admin@test.com');

toBeChecked()

Works on checkboxes and radio buttons. Add { checked: false } to assert the opposite.

ts
await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();

toBeEnabled() / toBeDisabled()

Perfect for validating that a submit button unlocks only after a form is valid.

ts
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();

toHaveURL() and toHaveTitle()

Both target the Page rather than a Locator. toHaveURL is invaluable for asserting a navigation actually happened, and both accept strings or regular expressions.

ts
await expect(page).toHaveURL(/\/dashboard$/);
await expect(page).toHaveTitle(/Dashboard/);

toHaveCount()

For a locator that resolves to multiple elements — search results, table rows, product cards — assert the exact count without a manual .count().

ts
await expect(page.locator('.product')).toHaveCount(12);

Soft Assertions

Sometimes you want to validate several independent facts in one test and report all failures at once instead of stopping at the first. That is what expect.soft() is for.

ts
await expect.soft(page.locator('h1')).toHaveText('Dashboard');
await expect.soft(page.locator('.user')).toContainText('admin');
await expect.soft(page.locator('.count')).toHaveText('42');

Use soft assertions sparingly — they are ideal for a single 'landing page smoke check' but should not replace hard assertions in critical flows, where you genuinely want the test to stop the moment a precondition fails.

Common Pitfalls

  • Forgetting to await the assertion — an un-awaited expect returns a Promise that resolves without failing the test.
  • Reading text with await locator.textContent() and then asserting on the string — you lose auto-retry. Use expect(locator).toHaveText() directly.
  • Using page.waitForTimeout() to 'give the UI time' before an assertion — the assertion already polls; the sleep just slows the suite.
  • Asserting on class names or DOM structure instead of user-visible behaviour — brittle to refactors. Assert what the user sees.
  • Tuning the global expect timeout to 30s to hide flaky waits — fix the wait, do not mask it.

Debugging Tips

  • Enable trace: 'on-first-retry' in playwright.config.ts and open the trace on failure — you can scrub to the exact frame where the assertion failed and inspect the DOM.
  • Run a single failing test with npx playwright test -g 'my test' --headed --debug to step through in the Inspector.
  • Bump the assertion timeout locally with expect(locator).toHaveText('x', { timeout: 10_000 }) to confirm a slow render before making it the default.
  • Log the current text with console.log(await locator.textContent()) only as a last resort — the trace almost always tells you more.

When to Use — and When Not To

Use Playwright assertions for every user-visible outcome you care about: navigation, state changes, form validation, error messages, dynamic counts. They are the reason a test is a test.

Do not use UI assertions to validate business logic that is better tested at the API or unit level. If you are asserting on a computed total, a unit test on the calculation function is cheaper, faster, and more reliable than clicking through the UI to reach it.

Enterprise Best Practices

  • Validate business behaviour, not implementation details — assert on what a user sees, not on CSS class names.
  • Prefer Playwright's expect() matchers over manual if/throw checks — you get retries, diffs, and traces for free.
  • Keep the assertion close to the action it validates — a wall of setup then a wall of assertions makes failures hard to diagnose.
  • Provide meaningful test titles — the title is what appears in CI reports, so 'admin can log in' beats 'test 42'.
  • Never chain more than one hard assertion where a soft-assertion group would give a fuller picture of a broken landing page.

FAQ

Why are Playwright assertions more reliable than manual validation?

Because they automatically retry until the expected condition becomes true or the timeout expires — no arbitrary sleeps required.

Difference between toHaveText() and toContainText()?

toHaveText() requires an exact match after whitespace normalization. toContainText() checks for a substring, which is more forgiving of dynamic content.

When should I use soft assertions?

When you want to validate multiple independent UI elements in one test and see every failure at once instead of stopping at the first — for example a landing-page smoke check.

How do I change the default assertion timeout?

Set expect.timeout in playwright.config.ts (globally) or pass { timeout } to a single assertion. The default is 5000ms.

Can I write my own assertion matchers?

Yes — expect.extend({ toHaveMyThing(received, expected) { ... } }) registers a custom matcher with the same auto-retry semantics as the built-ins.

Summary

Playwright assertions are the layer that turns a browser automation into a test suite you can trust. Their auto-retry behaviour eliminates the timing flakiness that plagued older tools, and their expressive API — toBeVisible, toHaveText, toHaveURL, toHaveCount, expect.soft — covers almost every real-world verification you need.

Use the exact matcher for the job, keep assertions close to the action they verify, and lean on the Trace Viewer whenever an assertion fails. Master these fundamentals and the assertions layer of your framework will quietly do its job for years — while your test count grows into the thousands.

Get Playwright tutorials in your inbox

Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.

Recommended Next Articles