# Assertions

Course: [Playwright](https://www.devtutorials.ai/learn/playwright) · Section: Core Test Writing
Source: https://www.devtutorials.ai/learn/playwright/core-test-writing/assertions

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

<figure className="plw-fig">
<svg viewBox="0 0 640 190" role="img" aria-labelledby="fig-plw-retry" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-plw-retry">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</title>
  <line x1="40" y1="120" x2="600" y2="120" style={{ stroke: "var(--plw-slate)", strokeWidth: 2 }} />
  <text x="40" y="145" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 11 }}>0 s</text>
  <text x="590" y="145" textAnchor="end" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 11 }}>timeout 5 s</text>
  <line x1="600" y1="105" x2="600" y2="135" style={{ stroke: "var(--plw-red)", strokeWidth: 2, strokeDasharray: "4 3" }} />
  <g style={{ fill: "var(--plw-red)" }}>
    <circle cx="60" cy="120" r="7" opacity="0"><animate attributeName="opacity" values="0;1;1;0" keyTimes="0;0.05;0.95;1" dur="7s" repeatCount="indefinite" /></circle>
    <circle cx="100" cy="120" r="7" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.12;0.15;0.95;1" dur="7s" repeatCount="indefinite" /></circle>
    <circle cx="140" cy="120" r="7" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.22;0.25;0.95;1" dur="7s" repeatCount="indefinite" /></circle>
    <circle cx="180" cy="120" r="7" opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.32;0.35;0.95;1" dur="7s" repeatCount="indefinite" /></circle>
  </g>
  <circle cx="230" cy="120" r="9" style={{ fill: "var(--plw-green)" }} opacity="0"><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.48;0.95;1" dur="7s" repeatCount="indefinite" /></circle>
  <rect x="40" y="25" width="320" height="50" rx="10" style={{ fill: "var(--plw-win)", stroke: "var(--plw-slate)" }} />
  <text x="200" y="56" textAnchor="middle" style={{ fill: "var(--ink2)", fontFamily: "var(--sans)", fontSize: 14 }}>Placing your order…<animate attributeName="opacity" values="1;1;0;0" keyTimes="0;0.42;0.43;1" dur="7s" repeatCount="indefinite" /></text>
  <text x="200" y="56" textAnchor="middle" opacity="0" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>Order confirmed<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.42;0.43;0.95;1" dur="7s" repeatCount="indefinite" /></text>
  <text x="250" y="105" opacity="0" style={{ fill: "var(--plw-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>✓ passed at 1.2 s<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.48;0.5;0.95;1" dur="7s" repeatCount="indefinite" /></text>
  <text x="400" y="56" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>toHaveText('Order confirmed')</text>
  <text x="320" y="175" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>Red: checked, not yet true. Green: true, so the assertion passes and the test moves on.</text>
</svg>
<figcaption>A web-first assertion checks until the page catches up. It does not wait the full 5 seconds when it can pass sooner.</figcaption>
</figure>

### 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

| Matcher | Passes 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

| Matcher | Checks |
|---|---|
| `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

| Matcher | Checks |
|---|---|
| `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.

| Matcher | Example |
|---|---|
| `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()`.

| Matcher | Matches |
|---|---|
| `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:

```ts
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

| Mistake | What happens | Fix |
|---|---|---|
| No `await` before `expect(locator)` | The test can pass when the check is false | Always `await` web-first assertions |
| `expect(await el.isVisible()).toBe(true)` | Fails if the element appears a moment later | `await expect(el).toBeVisible()` |
| `toHaveText()` when you mean "contains" | Fails because of extra words | Use `toContainText()` |
| `toPass()` without a timeout | It retries until the whole test times out | Pass `{ timeout }` |
| Soft assertions before an action that needs them | Later steps fail in confusing ways | Use 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**
