Assertions in Playwright

By Syncfusion®, Inc. · Last updated

expect() and web-first assertions that retry, why assertions must be awaited, locator assertions for state, content, attributes and count, page assertions, generic and asymmetric matchers, .not, custom messages and timeouts, soft assertions, expect.poll and toPass, and custom matchers with expect.configure and expect.extend.

Assertions

  • An assertion is a check: "this must be true".
  • Actions make a test do things. Assertions make it prove things.
  • Playwright's assertions wait and retry, so they work on pages that change.
  • In this lesson, you will learn every kind of assertion, and when to use each.

Simple lesson: assertions are the checks that decide if a test passes.

A Real-World Example

  • Priya adds two products to the cart, then opens the cart.
  • She checks the cart count, the items, the total and the Checkout button.

What You Will Learn

  1. expect(), and why Playwright's assertions retry.
  2. Assertions for elements: state, text, values, attributes and count.
  3. Assertions for the page: URL and title.
  4. Generic matchers for plain values.
  5. Options: .not, custom messages and timeouts.
  6. Soft assertions, expect.poll() and toPass().
  7. Custom assertions (advanced).

Assertion Fundamentals

expect() and Web-First Assertions

  • expect(value) starts an assertion. A matcher such as toHaveText() finishes it.
  • When you pass a locator or the page, you get a web-first assertion.
  • Web-first = it is made for web pages that change. It keeps checking until it passes.

Auto-Retrying Assertions

  • A web-first assertion checks, waits a little, and checks again.
  • It passes as soon as the check is true. It fails only when the time runs out: 5 seconds by default.

Animation: toHaveText checks the status again and again. The first checks see "Placing your order", then the text changes to "Order confirmed" and the next check passes

0 s timeout 5 s

Placing your order… Order confirmed ✓ passed at 1.2 s toHaveText('Order confirmed') Red: checked, not yet true. Green: true, so the assertion passes and the test moves on.

A web-first assertion checks until the page catches up. It does not wait the full 5 seconds when it can pass sooner.

Why Assertions Must Be Awaited

  • Web-first assertions return a promise. Without await, the test does not wait for the check.
  • The test can end before the check runs. Then a false check does not fail the test.
  • This test checks for text that never appears. Run it: it passes, which is wrong.
  • Add await in front of expect and run it again. Now it fails, as it should.
  • The VS Code extension and TypeScript lint rules can warn you about missing await.

Avoiding Manual Assertions

  • isVisible() and textContent() read the page once, right now. They do not wait.
  • expect(await locator.isVisible()).toBe(true) fails if the element appears a moment later.
  • await expect(locator).toBeVisible() waits for it. Always prefer the web-first version.

Simple lesson: await expect(locator) waits and retries. Reading once with isVisible() or count() does not.

Locator Assertions

State

MatcherPasses when the element is
toBeVisible()visible
toBeHidden()hidden or gone
toBeEnabled() / toBeDisabled()enabled / disabled
toBeChecked()ticked ({ checked: false } for unticked)
toBeEditable()a field you can type in
toBeFocused()the one with the cursor
toBeAttached()in the page, visible or not
toBeEmpty()an empty field or an element with no text

Content

MatcherChecks
toHaveText('Yoga Mat')the whole text (spaces trimmed). A regex or a list also work
toContainText('Yoga')that the text contains this part
toHaveValue('Kochi')the value of an input
toHaveValues(['red', 'blue'])the chosen options of a multi-select

Attributes, Count and Properties

MatcherChecks
toHaveAttribute('href', '/cart')an attribute's value
toHaveClass(/selected/) / toContainClass('selected')the class attribute
toHaveCSS('display', 'flex')a computed style
toHaveId('search')the id
toHaveCount(6)how many elements the locator finds
toHaveJSProperty('checked', true)a JavaScript property of the element

Simple lesson: choose the matcher that says exactly what the user should see.

Page Assertions

  • await expect(page).toHaveURL('/account') checks the address. With baseURL, a short path works.
  • await expect(page).toHaveTitle(/Yoga Mat/) checks the browser tab title.
  • Both accept a string (exact match) or a regular expression (pattern).

With baseURL in the config, toHaveURL('/cart') compares against the full address, https://practice.devtutorials.ai/cart. A regular expression such as /\/cart$/ checks just the end.

Simple lesson: toHaveURL() and toHaveTitle() check the page itself.

Generic Assertions

Generic Matchers

  • For plain values (numbers, strings, objects), use generic matchers. They check once and do not retry.
MatcherExample
toBe()expect(status).toBe(200) (same value)
toEqual()expect(cart).toEqual([{ id: 2, qty: 1 }]) (same content)
toContain()expect(names).toContain('Yoga Mat')
toMatchObject()expect(user).toMatchObject({ name: 'Asha' }) (these keys match)

Asymmetric Matchers

  • Asymmetric matchers describe a value loosely, inside toEqual() or toMatchObject().
MatcherMatches
expect.any(Number)any number
expect.objectContaining({ id: 2 })an object with at least these keys
expect.stringContaining('@')a string containing @
expect.arrayContaining([2, 3])an array holding at least these items

Web-First vs Generic Assertions

  • Web-first (locator or page): asynchronous, retries, needs await.
  • Generic (plain value): checks once. Get the value first, then check it.

Simple lesson: web-first matchers for the page, generic matchers for values you already have.

Assertion Options

Negating with .not

  • .not flips any matcher: await expect(button).not.toBeVisible().
  • Web-first .not assertions also wait: until the element is not visible.

Custom Assertion Messages

  • Pass a message as the second argument. It appears at the top of the error:
await expect(page.getByTestId('cart-count'), 'cart should be empty after checkout').toHaveText('0');

Per-Assertion timeout

  • toHaveText('Ready', { timeout: 10_000 }) waits up to 10 seconds for this one check.
  • To change the default for all assertions, set expect: { timeout } in the config.

Raise the timeout only for checks that are slow on purpose, like a report that takes time to build. A bigger timeout everywhere hides slow pages.

Simple lesson: .not flips a check, a message explains it, a timeout gives it more time.

Soft Assertions

expect.soft()

  • A normal assertion stops the test at the first failure.
  • expect.soft() records the failure and keeps going. The test still fails at the end.
  • Use it to see several problems in one run, for example on a page with many fields.

How Soft Assertions Affect Test Results

  • Both soft failures appear in the report, and the test is marked failed.
  • After a soft failure, later steps still run. Make sure they still make sense.

Simple lesson: soft assertions collect failures instead of stopping at the first one.

Polling and Retrying

expect.poll()

  • expect.poll(fn) turns any value into a retrying check. It calls fn again and again until the matcher passes.
  • Use it for values that do not live on the page, such as data read with page.evaluate() or an API.

expect().toPass()

  • expect(async () => { ... }).toPass() retries a whole block until every check inside passes.
  • Give it a timeout. Its default timeout is 0, which means it would keep trying until the test times out.

Polling Intervals

  • Both accept intervals, the waits between tries: { intervals: [500, 1000, 2000] }.
  • The sync page fails on the first click. toPass() runs the block again, the second click works, and the test passes.

Simple lesson: expect.poll() retries a value, toPass() retries a block.

Custom Assertions (Advanced)

expect.configure()

  • expect.configure({ timeout: 10_000 }) makes a new expect with other defaults.
  • expect.configure({ soft: true }) makes every assertion from it soft.

expect.extend() for Custom Matchers

  • expect.extend() adds your own matchers. Each one returns pass and a message:

Merging Expects

  • Big projects keep matchers in several files. mergeExpects(expectA, expectB) from @playwright/test combines them into one expect.

Custom matchers are worth it when the same check appears in many tests. For a one-off check, a normal assertion is clearer.

Simple lesson: configure() changes defaults, extend() adds your own matchers.

Try It in Your Project

  • These steps take the lesson from the lab to your own app.
  • Make sure every test ends with at least one await expect(...) that would fail if the feature broke.
  • Turn one manual check like expect(await x.textContent()) into a web-first assertion.
  • Use expect.soft() in a test that checks many fields of one page.

Simple lesson: use one idea from this lesson on your own app this week.

TL;DR

  • await expect(locator or page) gives web-first assertions that retry for 5 seconds.
  • Always await web-first assertions.
  • Prefer toBeVisible(), toHaveText(), toHaveCount() over reading values once.
  • Use generic matchers (toBe, toEqual) for plain values.
  • .not, messages and timeout adjust a check. expect.soft() keeps going after a failure.
  • expect.poll() and toPass() retry values and blocks.

Common mistakes

MistakeWhat happensFix
No await before expect(locator)The test can pass when the check is falseAlways await web-first assertions
expect(await el.isVisible()).toBe(true)Fails if the element appears a moment laterawait expect(el).toBeVisible()
toHaveText() when you mean "contains"Fails because of extra wordsUse toContainText()
toPass() without a timeoutIt retries until the whole test times outPass { timeout }
Soft assertions before an action that needs themLater steps fail in confusing waysUse a normal assertion for checks the next steps depend on

Check Yourself

Exercises

What to Remember

  • Web-first assertions retry. Always await them.
  • Pick the matcher that matches what the user sees.
  • Generic matchers check plain values once.
  • expect.soft() collects failures. expect.poll() and toPass() retry.
  • Custom matchers are for checks you repeat often.

Next: Organizing Tests