Chapter 5 – Assertions and Expectations with the Playwright Test Runner

In the previous chapter, you learned how to perform user interactions like clicking, typing, selecting, and navigating. Now it’s time to learn how to verify that your web application behaves as expected.

This is where assertions and expectations come in. Assertions are the checkpoints in your automation, they confirm that something is correct (like a title, a message, or a URL). Without them, automation is just a series of actions without meaning.

In this chapter, we’ll use the Playwright Test Runner to make our tests smart, reliable, and meaningful.

What is the Playwright Test Runner?

Playwright includes a built-in testing framework called the Playwright Test Runner. You don’t need to install Jest or Mocha, it’s already part of Playwright.

It provides:

  • A simple structure for writing tests.
  • Built-in assertions (via expect).
  • Test fixtures (like page, browser, context).
  • Parallel execution.
  • HTML reports and trace debugging.

Here’s a simple example:

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

test('homepage has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

This test opens the Playwright website and checks that the title contains the word “Playwright.”

Structure of a Playwright test

A typical Playwright test file has three main parts:

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

test('description', async ({ page }) => {
  // 1. Setup or navigation
  await page.goto('https://example.com');

  // 2. Action
  await page.click('text=Login');

  // 3. Assertion
  await expect(page).toHaveURL(/login/);
});

Let’s break it down:

SectionPurpose
test()Defines a test case.
{ page }Fixture automatically provided by Playwright (represents a browser tab).
expect()Built-in assertion library.
async/awaitEnsures each step completes before the next one.

Common Playwright assertions

Playwright provides many built-in assertions for verifying the page state.

1. Checking the title

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

Checks that the page title matches a string or regex.

2. Checking the current URL

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

3. Checking element visibility

const welcomeText = page.locator('h1');
await expect(welcomeText).toBeVisible();

4. Checking element text

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

You can also check partial text with regex:

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

5. Checking input values

await expect(page.locator('#username')).toHaveValue('john_doe');

6. Checking element attributes

await expect(page.locator('#submit')).toHaveAttribute('type', 'submit');

7. Checking element count

await expect(page.locator('ul > li')).toHaveCount(5);

8. Checking if an element is enabled or disabled

await expect(page.locator('#submit')).toBeEnabled();
await expect(page.locator('#cancel')).toBeDisabled();

9. Checking screenshots (visual regression)

You can even compare screenshots to detect visual changes:

await expect(page).toHaveScreenshot('homepage.png');

How Playwright handles retries automatically

Assertions in Playwright are smart. They automatically retry until the condition is true or a timeout is reached.

For example:

await expect(page.locator('#status')).toHaveText('Success');

If the text takes 2 seconds to appear, Playwright waits by default, no need for explicit wait() calls.

Default timeout for assertions is 5 seconds, but you can customize it:

await expect(page.locator('#status')).toHaveText('Success', { timeout: 10000 });

Grouping tests

You can group related tests using test.describe():

test.describe('Login tests', () => {
  test('valid login', async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.fill('#username', 'user1');
    await page.fill('#password', 'pass123');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL(/dashboard/);
  });

  test('invalid login shows error', async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.fill('#username', 'wrong');
    await page.fill('#password', 'wrongpass');
    await page.click('button[type="submit"]');
    await expect(page.locator('.error')).toHaveText('Invalid credentials');
  });
});

Grouping helps organize tests logically (like login, signup, checkout, etc.).

Test hooks: beforeEach and afterEach

Playwright supports setup and teardown hooks to prepare and clean up before or after tests.

test.beforeEach(async ({ page }) => {
  await page.goto('https://example.com');
});

test.afterEach(async ({ page }) => {
  console.log('Test completed!');
});

Expect inside loops or conditionals

Playwright’s expect works seamlessly inside loops:

for (const item of await page.locator('ul > li').all()) {
  await expect(item).toBeVisible();
}

Handling flaky assertions

Sometimes, tests may fail intermittently (due to animations, delays, etc.). Here are tips to reduce flakiness:

  • Use toBeVisible() before clicking elements.
  • Avoid arbitrary waits like waitForTimeout(), use expectations instead.
  • Increase assertion timeout only when necessary.
  • Use stable selectors like data-testid.

Full example: login assertion test

Here’s a full example that combines everything:

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

test.describe('Login functionality', () => {
  test('should login successfully and show dashboard', async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');

    await page.fill('#user-name', 'standard_user');
    await page.fill('#password', 'secret_sauce');
    await page.click('#login-button');

    await expect(page).toHaveURL(/inventory/);
    await expect(page.locator('.title')).toHaveText('Products');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('https://www.saucedemo.com/');

    await page.fill('#user-name', 'wrong_user');
    await page.fill('#password', 'wrong_pass');
    await page.click('#login-button');

    const errorMessage = page.locator('[data-test="error"]');
    await expect(errorMessage).toBeVisible();
    await expect(errorMessage).toHaveText(/Username and password do not match/);
  });
});

Exercise

Task 1: Write a test that opens a website and asserts that the title, URL, and a header element are correct.

Task 2: Add another test that verifies an element’s visibility and text.

Bonus: Try adding a screenshot assertion using toHaveScreenshot().

Cheat Sheet

AssertionExample
Check titleawait expect(page).toHaveTitle(/Home/)
Check URLawait expect(page).toHaveURL(/dashboard/)
Check visibilityawait expect(locator).toBeVisible()
Check textawait expect(locator).toHaveText('Success')
Check countawait expect(locator).toHaveCount(5)
Check attributeawait expect(locator).toHaveAttribute('href', '/home')
Check enabledawait expect(locator).toBeEnabled()
Screenshotawait expect(page).toHaveScreenshot('page.png')

Summary

In this chapter, you learned how to:

  • Use the Playwright Test Runner and expect for assertions.
  • Verify page titles, URLs, text, visibility, and attributes.
  • Write grouped and structured tests.
  • Handle flaky conditions with built-in retries.

Assertions turn actions into meaningful automation. They tell you not just what happened, but whether it happened correctly.