Swacblooms🦋

Making the Moves
Menu
  • Home
  • Motivation
  • Education
  • Programming
  • About
  • Contact
  • Privacy Policy
Home
Programming
Part 4 — Assertions: Verifying What You See
Programming

Part 4 — Assertions: Verifying What You See

Samson Amaugo April 20, 2026

Introduction

In Part 3, you learned how to interact with the page using actions like click(), fill(), and check(). Now you need to prove those actions produced the right result.

That is where assertions come in.

An assertion is a check that confirms something is true. In Playwright, assertions are built on expect() and are designed for the web. Instead of checking once and failing immediately, they retry for a short period while the UI catches up.

This is one of the biggest reasons Playwright tests are reliable.


The expect() API at a Glance

You already saw this pattern:

await expect(page).toHaveTitle(/Playwright/);

The structure is always the same:

await expect(target).matcher(value)

  • target is what you are asserting on (page, a locator, a response, etc.)
  • matcher is the condition (toBeVisible, toHaveText, toHaveURL, …)
  • value is the expected value when required

For UI tests, your most common target is a locator:

await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();


Web-First Assertions (Auto-Retry by Default)

Playwright assertions are web-first. That means they keep re-checking until either:

  1. The assertion passes, or
  2. The assertion timeout is reached

So this:

await expect(page.getByText('Saved')).toBeVisible();

does not mean “check once right now.” It means “wait for this to become visible within the assertion timeout.”

Note: auto-retry applies to Playwright’s async web matchers (like toBeVisible and toHaveText). Generic value matchers such as expect(value).toBe(...) do not auto-retry.

By default, Playwright uses a 5 second assertion timeout unless you configure it.

You can override per assertion:

await expect(page.getByText('Saved')).toBeVisible({ timeout: 10_000 });

or globally in config:

// playwright.config.js
export default defineConfig({
  expect: {
    timeout: 10_000,
  },
});


Core Assertions You Will Use Most

toHaveTitle()

Asserts the page title.

await expect(page).toHaveTitle('Settings');
await expect(page).toHaveTitle(/Settings/);

toHaveURL()

Asserts the current URL.

await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page).toHaveURL(/\/dashboard$/);

toBeVisible()

Checks that an element is visible to the user.

await expect(page.getByRole('alert')).toBeVisible();

toHaveText()

Checks exact or regex text content.

await expect(page.getByTestId('status')).toHaveText('Saved');
await expect(page.getByTestId('status')).toHaveText(/saved/i);

toBeEnabled()

Checks that an element is enabled and can be interacted with.

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


A Realistic End-to-End Example

import { test, expect } from '@playwright/test';

test('user can save profile changes', async ({ page }) => {
  await page.goto('/profile');

  await page.getByLabel('Display name').fill('swacblooms');
  await page.getByRole('button', { name: 'Save changes' }).click();

  await expect(page).toHaveURL(/\/profile$/);
  await expect(page.getByRole('status')).toHaveText('Changes saved');
  await expect(page.getByRole('button', { name: 'Save changes' })).toBeEnabled();
});

Notice there are no manual sleeps. Assertions do the waiting.


Negative Assertions

You can assert that something is not true using .not:

await expect(page.getByText('Loading...')).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Delete' })).not.toBeEnabled();

Use negative assertions carefully. They are useful, but positive checks are often clearer when possible.


Soft Assertions

A regular assertion fails the test immediately. A soft assertion records the failure and allows the test to continue.

await expect.soft(page.getByTestId('first-name')).toHaveValue('Ada');
await expect.soft(page.getByTestId('last-name')).toHaveValue('Lovelace');
await expect.soft(page.getByTestId('email')).toHaveValue('ada@example.com');

// test continues even if one soft check fails
await page.getByRole('button', { name: 'Continue' }).click();

Soft assertions are useful when you want to collect multiple mismatches in one run, for example in long forms or summary pages.

Use them intentionally. For critical checks, prefer normal assertions so failures stop immediately.


Polling for Non-UI Conditions

Sometimes you need to verify something that is not a locator state, such as a value that changes over time.

Use expect.poll():

await expect
  .poll(async () => {
    const response = await page.request.get('https://example.com/api/jobs/123');
    const data = await response.json();
    return data.status;
  })
  .toBe('completed');

Playwright keeps polling until the value becomes 'completed' or times out.

This gives you retry behavior for computed values, API state, and other async conditions.


Common Mistakes to Avoid

  1. Using manual sleeps instead of assertions
// avoid
await page.waitForTimeout(2000);

// prefer
await expect(page.getByText('Saved')).toBeVisible();

  1. Asserting raw values too early
// fragile
const text = await page.getByTestId('status').textContent();
expect(text).toBe('Saved');

Prefer Playwright locator assertions because they auto-retry:

await expect(page.getByTestId('status')).toHaveText('Saved');

  1. Overusing soft assertions

If everything is soft, failures can be easy to miss. Keep core checks hard.


Under the Hood: Why Retry-ability Matters

Modern UIs are asynchronous.

  • React/Vue/Angular re-render after network calls
  • buttons enable after validation
  • toast messages appear after state updates

If assertions checked only once, tests would be flaky.

Playwright’s retry model aligns your checks with real browser timing. Instead of adding arbitrary sleeps, you describe the desired end state and let Playwright wait for it.

That is the foundation of stable E2E tests.


What’s Next

In Part 5 you will learn playwright.config.js in depth. You will configure retries, workers, projects, baseURL, traces, screenshots, and other settings that shape how your whole suite runs locally and in CI.

Prev Article
Next Article

Related Articles

reactive variables(apollo client)
Hello guys, in this tutorial, I would be talking about …

Reactive Variables

chatbot
My team and I participated in the Facebook DevC 2020 …

Making a Wikipedia chatbot with WIT.AI

About The Author

Samson Amaugo

I am Samson Amaugo. I am a full-stack developer and I specialize in DotNet and the MERN stack.

Search Site

Recent Posts

  • Cloudflare Tunnel to Postgres, plus Swarm’s static IP problem
  • The Topological Sort
  • Building a Perceptron in .NET
  • Part 4 — Assertions: Verifying What You See
  • Part 3 — Actions and Auto-Waiting: Putting Locators to Work

Categories

  • EDUCATION
  • Motivation
  • Programming
  • Uncategorized

Get more stuff

Subscribe to our mailing list and get interesting stuff and updates to your email inbox.

Thank you for subscribing.

Something went wrong.

we respect your privacy and take protecting it seriously

RSS feed: Swacblooms Swacblooms

Swacblooms🦋

Making the Moves
Copyright © 2026 Swacblooms🦋
Swacblooms - Making the Moves