Introduction
Assertions are how your tests decide pass or fail. In Playwright, they do more than compare values — web-first assertions automatically wait for a condition to become true, eliminating most timing-related flakiness that plagues Selenium and Cypress tests.
This guide walks through every kind of assertion Playwright supports: web-first, generic, negative, soft, API, and custom matchers — with real code you can drop into a spec file today.
What Are Assertions?
An assertion is a check that throws an error when a condition is false. Playwright's expect() comes from @playwright/test and works on both DOM elements and plain values.
import { test, expect } from '@playwright/test';
test('basic assertion', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
await expect(page.locator('h1')).toHaveText('Example Domain');
});Web-First Assertions
Web-first assertions retry automatically until the expected condition is met or the timeout expires. You never need manual waits or sleeps.
- toBeVisible / toBeHidden — element is (or isn't) in the DOM and visible.
- toBeEnabled / toBeDisabled — form control state.
- toBeChecked — checkbox or radio state.
- toHaveText / toContainText — element text content.
- toHaveValue — input value.
- toHaveAttribute — attribute value.
- toHaveClass — CSS class list.
- toHaveCount — number of matching elements.
- toHaveURL / toHaveTitle — page URL and title.
- toBeFocused — element has keyboard focus.
import { test, expect } from '@playwright/test';
test('login form', async ({ page }) => {
await page.goto('/login');
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeEnabled();
await page.getByLabel('Email').fill('qa@example.com');
await page.getByLabel('Password').fill('secret');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
await expect(page.locator('.notification')).toHaveCount(3);
});Generic Assertions
For plain JavaScript values — strings, numbers, arrays, objects — use the standard Jest-style matchers. These do not retry.
import { test, expect } from '@playwright/test';
test('value assertions', () => {
expect(2 + 2).toBe(4);
expect({ name: 'QA' }).toEqual({ name: 'QA' });
expect([1, 2, 3]).toContain(2);
expect('Playwright').toMatch(/play/i);
expect(undefined).toBeUndefined();
expect(null).toBeNull();
expect(true).toBeTruthy();
});Negative Assertions
Use .not to invert any matcher. It still auto-retries for web-first matchers.
import { test, expect } from '@playwright/test';
test('negative checks', async ({ page }) => {
await page.goto('/');
await expect(page.locator('.error-banner')).not.toBeVisible();
await expect(page.getByRole('button', { name: 'Submit' })).not.toBeDisabled();
await expect(page).not.toHaveURL(/login/);
});Soft Assertions
Soft assertions record failures but let the test keep running, so one spec can report multiple problems in a single run instead of stopping at the first failure.
import { test, expect } from '@playwright/test';
test('checkout page details', async ({ page }) => {
await page.goto('/checkout');
await expect.soft(page.getByText('Subtotal')).toBeVisible();
await expect.soft(page.getByText('Shipping')).toBeVisible();
await expect.soft(page.getByText('Tax')).toBeVisible();
await expect.soft(page.getByText('Total')).toBeVisible();
// If any soft assertion above failed, the test is marked failed
// but this final hard assertion still runs.
await expect(page.getByRole('button', { name: 'Place order' })).toBeEnabled();
});API Response Assertions
Playwright's APIResponse works with the same expect() so you can assert on HTTP status, headers, and JSON bodies without leaving the test framework.
import { test, expect } from '@playwright/test';
test('user API', async ({ request }) => {
const res = await request.get('https://api.example.com/users/1');
await expect(res).toBeOK();
expect(res.status()).toBe(200);
expect(res.headers()['content-type']).toContain('application/json');
const body = await res.json();
expect(body).toMatchObject({ id: 1, active: true });
expect(body.roles).toEqual(expect.arrayContaining(['user']));
});Custom Timeouts
Every web-first assertion accepts a timeout in milliseconds. Use it sparingly — the global expect timeout (default 5s) covers most cases.
import { test, expect } from '@playwright/test';
test('slow report generation', async ({ page }) => {
await page.getByRole('button', { name: 'Generate report' }).click();
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 30_000 });
});
// Or configure globally in playwright.config.ts:
// expect: { timeout: 10_000 }Custom Matchers
Extend expect with domain-specific matchers to keep tests expressive and DRY.
import { expect as baseExpect } from '@playwright/test';
export const expect = baseExpect.extend({
async toHaveValidPrice(locator: import('@playwright/test').Locator) {
const text = await locator.textContent();
const pass = /^\$\d+\.\d{2}$/.test(text ?? '');
return {
pass,
message: () =>
pass
? `expected ${text} not to be a valid price`
: `expected ${text} to match $X.XX format`,
};
},
});import { test } from '@playwright/test';
import { expect } from '../utils/matchers';
test('product price format', async ({ page }) => {
await page.goto('/products/1');
await expect(page.locator('.price')).toHaveValidPrice();
});Best Practices
- Prefer web-first assertions (toBeVisible, toHaveText) over manual waits + generic checks — they auto-retry and are far less flaky.
- Always await async assertions. Missing await is the #1 cause of silently passing tests.
- Use role-based locators (getByRole, getByLabel) as targets — they mirror how real users and screen readers see the page.
- Reach for soft assertions when validating groups of related fields; use hard assertions for critical path checks.
- Set a sensible global expect timeout in playwright.config.ts instead of adding { timeout } everywhere.
- Wrap repeated domain checks in custom matchers to keep specs readable.
Common Mistakes
- Missing await on expect(locator).toBeVisible() — the assertion never actually runs.
- Using expect(await locator.textContent()).toBe(...) instead of expect(locator).toHaveText(...) — you lose auto-retry and get flake.
- Using page.waitForTimeout(2000) before assertions — web-first assertions already wait; explicit sleeps only slow the suite.
- Chaining many hard assertions where soft would be more useful — you learn about one failure at a time.
- Asserting on brittle CSS selectors like .css-1abc23 instead of accessible roles or test ids.
Summary
Playwright's assertion library is one of the biggest reasons tests written with it are more stable than those written with older frameworks. Web-first matchers remove the need for manual waits, soft assertions surface every problem in one run, API assertions unify UI and service testing, and custom matchers keep large suites readable.
Master these patterns and you'll spend far less time debugging flaky tests and far more time delivering reliable automation.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.
Playwright Framework Series
View all →- 1Getting Started with Playwright: Installation, Setup, and Your First Test
- 2Playwright Locators: The Complete Guide with Real-World Examples
- 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
- 4Playwright Assertions: The Complete Guide with Real-World Examples
- 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
- 6Playwright Fixtures: The Complete Guide with Real-World Examples
- 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
- 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
- 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
- 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
- 11Page Object Model with Playwright: A Practical Guide
- 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
- 13Building an Enterprise Playwright Framework from Scratch
API Testing Series
View all →- 1Playwright API Testing: The Complete Guide with Real-World Examples
- 2Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
- 3Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
- 4Part 3: Mastering CRUD Operations in Playwright API Testing
- 5Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
- 6Part 5: Building an Enterprise-Level Playwright API Automation Framework
- 7Part 6: API Models, Schema Validation & Test Data Management in Playwright
- 8Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
- 9Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
- 10Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
- 11Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
- 12API Testing with Playwright: Request Context, Auth, and Assertions