Core Concepts August 15, 2026 12 min read

Playwright Assertions Guide: expect, toBeVisible & Every Assertion Method (2026)

Assertions are the backbone of every Playwright test — they determine whether your test passes or fails. This guide covers every assertion method with real code examples, from basic visibility checks to pixel-perfect visual comparisons.

You can write the most elegant locators and the most clever navigation logic, but without the right assertion your test proves nothing. Playwright ships with a powerful expect API that auto-retries, supports negative matching, and provides soft assertions — and most teams only use a fraction of it.

This guide walks through every assertion category with production-ready examples. By the end, you will know exactly which assertion to reach for in any scenario and how to avoid the common mistakes that cause flaky failures. If you are still learning how to use Playwright, start there first and come back here for the deep dive on assertions.


How Assertions Work in Playwright

Playwright assertions are fundamentally different from the assert module in Node.js or the expect in Jest. The critical difference is auto-retrying.

When you write await expect(locator).toBeVisible(), Playwright does not check once and fail. Instead, it repeatedly polls the DOM — roughly every 100ms — until the element becomes visible or the assertion timeout expires (default: 5 seconds). This single behavior eliminates an entire class of flaky tests caused by timing issues.

Auto-retrying vs. immediate assertion
// Playwright assertion — auto-retries for 5 seconds
await expect(page.getByRole('alert')).toBeVisible();

// Node.js assert — runs ONCE, fails immediately
const el = await page.$('.alert');
assert(el !== null); // fragile — element may not exist yet

You can override the timeout per assertion or globally in playwright.config.ts:

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

Tip: Only web-first assertions (those that take a locator or page) auto-retry. Generic value assertions like expect(myVariable).toBe(5) run once and are identical to Jest assertions. See the best practices guide for more on avoiding timing traps.


Page Assertions

Page-level assertions verify the current URL and document title. They are typically the first assertions in a test — confirming the page has navigated to the expected destination.

toHaveURL()

Checks that the page URL matches a string or regular expression. Auto-retries, so it naturally waits for client-side routing to complete.

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

// Regex — ignore query params
await expect(page).toHaveURL(/\/dashboard/);

// With custom timeout for slow redirects
await expect(page).toHaveURL('https://example.com/login', {
  timeout: 15_000,
});

toHaveTitle()

Validates the page <title> tag. Useful for SEO tests and verifying navigation landed on the right page.

toHaveTitle examples
// Exact title
await expect(page).toHaveTitle('Dashboard | My App');

// Regex — partial match
await expect(page).toHaveTitle(/Dashboard/);

Locator Assertions

Locator assertions are the most commonly used group. They check the state of an element — visible, hidden, enabled, disabled, checked, or editable. Every one of these auto-retries. For a deep dive on how to target elements before asserting on them, see the Playwright locators guide.

toBeVisible() / toBeHidden()

toBeVisible() passes when the element is attached to the DOM, is not hidden by CSS (display:none, visibility:hidden, opacity:0), and has a non-zero bounding box. toBeHidden() is the inverse.

Visibility assertions
// Wait for modal to appear
await expect(page.getByRole('dialog')).toBeVisible();

// Wait for loading spinner to disappear
await expect(page.getByRole('progressbar')).toBeHidden();

// Assert error banner shows after form submit
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('alert')).toBeVisible();

toBeEnabled() / toBeDisabled()

Checks the disabled attribute on form controls. Essential for multi-step forms where the "Next" button should be disabled until required fields are filled.

Enabled / disabled assertions
// Button disabled until form is valid
await expect(page.getByRole('button', { name: 'Next' })).toBeDisabled();

// Fill required fields, then verify button becomes enabled
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('SecurePass123');
await expect(page.getByRole('button', { name: 'Next' })).toBeEnabled();

toBeChecked()

Validates checkboxes and radio buttons.

Checked assertion
await page.getByLabel('I agree to the terms').check();
await expect(page.getByLabel('I agree to the terms')).toBeChecked();

// Verify unchecked state
await expect(page.getByLabel('Subscribe to newsletter')).toBeChecked({ checked: false });

toBeEditable()

Passes when an input is neither disabled nor readonly. Useful for verifying that a profile form switches from view mode to edit mode.

Editable assertion
// Click edit, then verify fields are editable
await page.getByRole('button', { name: 'Edit Profile' }).click();
await expect(page.getByLabel('Full Name')).toBeEditable();

Text Assertions

Text assertions verify what the user actually reads on the page. Playwright normalizes whitespace automatically, so extra spaces and newlines in the DOM will not cause false failures.

toHaveText() vs. toContainText()

toHaveText() matches the full text content of an element (after whitespace normalization). toContainText() checks for a substring. Both accept strings and regular expressions.

Text assertion examples
// Exact full text
await expect(page.getByRole('heading', { level: 1 }))
  .toHaveText('Welcome to Your Dashboard');

// Substring
await expect(page.getByTestId('status-badge'))
  .toContainText('Active');

// Regex for dynamic content
await expect(page.getByTestId('order-total'))
  .toHaveText(/Total: \$[\d,.]+/);

Array matching with toHaveText()

When a locator matches multiple elements, you can pass an array to assert the text of all matched elements at once. This is powerful for verifying lists, table rows, or navigation items.

Array text matching
// Verify all nav links in order
await expect(page.getByRole('navigation').getByRole('link'))
  .toHaveText(['Home', 'Products', 'Pricing', 'Contact']);

// Regex array — each element matches its own pattern
await expect(page.locator('.price'))
  .toHaveText([/\$\d+/, /\$\d+/, /\$\d+/]);

Tip: Use the { ignoreCase: true } option when text casing is unreliable — for example, when the backend may return "ACTIVE" or "Active" depending on the locale.


Attribute & CSS Assertions

These assertions inspect HTML attributes and computed CSS properties. They are essential for verifying styling changes, ARIA states, and dynamic class toggles.

toHaveAttribute()

Attribute assertions
// Verify link opens in new tab
await expect(page.getByRole('link', { name: 'Docs' }))
  .toHaveAttribute('target', '_blank');

// Verify image src (regex for cache-busted URLs)
await expect(page.getByAltText('Profile photo'))
  .toHaveAttribute('src', /\/avatars\/user-42/);

// ARIA attribute
await expect(page.getByRole('button', { name: 'Menu' }))
  .toHaveAttribute('aria-expanded', 'true');

toHaveClass()

Checks the element's class attribute. Accepts a string (full class list or substring) or regex.

Class assertions
// Verify active state is applied
await expect(page.getByRole('tab', { name: 'Settings' }))
  .toHaveClass(/active/);

// Exact class list
await expect(page.locator('.card').first())
  .toHaveClass('card card-featured');

toHaveCSS()

Asserts on computed CSS values. Useful for verifying color changes, visibility transitions, and responsive layout behavior.

CSS assertions
// Verify error state turns border red
await expect(page.getByLabel('Email'))
  .toHaveCSS('border-color', 'rgb(239, 68, 68)');

// Verify element is hidden via opacity
await expect(page.locator('.tooltip'))
  .toHaveCSS('opacity', '0');

toHaveId()

A convenience assertion for the id attribute.

ID assertion
await expect(page.getByRole('region'))
  .toHaveId('main-content');

Count Assertions

toHaveCount() verifies the number of elements matching a locator. It auto-retries, so it naturally waits for items to load.

Count assertion examples
// Verify search returns exactly 5 results
await expect(page.getByRole('listitem')).toHaveCount(5);

// Verify cart is empty
await expect(page.locator('.cart-item')).toHaveCount(0);

// Verify table rows after filtering
await page.getByPlaceholder('Filter by name').fill('Smith');
await expect(page.locator('tbody tr')).toHaveCount(3);

Tip: Prefer toHaveCount(0) over not.toBeVisible() when you want to assert that elements were fully removed from the DOM, not just hidden.


Value Assertions

toHaveValue() checks the current value of <input>, <textarea>, and <select> elements. For multi-select elements, use toHaveValues().

Value assertions
// Verify input value after programmatic fill
await page.getByLabel('Quantity').fill('3');
await expect(page.getByLabel('Quantity')).toHaveValue('3');

// Verify dropdown selection
await page.getByLabel('Country').selectOption('US');
await expect(page.getByLabel('Country')).toHaveValue('US');

// Multi-select — verify all selected options
await page.getByLabel('Languages').selectOption(['en', 'fr', 'de']);
await expect(page.getByLabel('Languages'))
  .toHaveValues(['en', 'fr', 'de']);

Visual Assertions

toHaveScreenshot() captures a screenshot and compares it pixel-by-pixel against a baseline image. On the first run it creates the baseline; on subsequent runs it diffs against it. For a comprehensive walkthrough, see the visual regression testing guide.

Visual assertion examples
// Full-page screenshot comparison
await expect(page).toHaveScreenshot('dashboard.png');

// Element-level screenshot
await expect(page.locator('.chart-container'))
  .toHaveScreenshot('revenue-chart.png');

// Allow up to 100 pixel differences (animations, fonts)
await expect(page).toHaveScreenshot('homepage.png', {
  maxDiffPixels: 100,
});

// Mask dynamic elements (timestamps, ads, avatars)
await expect(page).toHaveScreenshot('profile.png', {
  mask: [
    page.locator('.timestamp'),
    page.locator('.user-avatar'),
  ],
});

Tip: Use maxDiffPixelRatio instead of maxDiffPixels when the page size varies across test runs (responsive tests). A ratio of 0.01 allows 1% of pixels to differ.


Soft Assertions

Regular assertions stop the test immediately on failure. Soft assertions record the failure but allow the test to continue. All failures are reported together at the end.

Soft assertions
// Check multiple dashboard elements — report ALL failures
await expect.soft(page.getByTestId('total-revenue'))
  .toContainText('$');

await expect.soft(page.getByTestId('active-users'))
  .toBeVisible();

await expect.soft(page.getByTestId('chart-container'))
  .toBeVisible();

await expect.soft(page.getByRole('navigation'))
  .toContainText('Dashboard');

When to use soft assertions:

  • Verifying multiple independent elements on the same page (dashboards, product cards)
  • Smoke tests that check many sections at once
  • Form validation where you want to see all broken fields, not just the first one

When NOT to use soft assertions:

  • When the next step depends on the current assertion (login must succeed before dashboard check)
  • Critical security checks that should halt the test immediately

Custom Assertion Messages

Playwright lets you add a custom message as the second argument to expect(). This message appears in the test report when the assertion fails, making debugging significantly faster.

Custom assertion messages
// Without message: "Expected: visible, Received: hidden"
// With message: "Login error banner should appear after invalid credentials"

await expect(
  page.getByRole('alert'),
  'Login error banner should appear after invalid credentials'
).toBeVisible();

// Useful in loops
for (const item of expectedItems) {
  await expect(
    page.getByText(item),
    `Item "${item}" should be visible in the list`
  ).toBeVisible();
}

Tip: Custom messages are especially valuable in data-driven tests where the same assertion runs for many values. Without them, a failure like "expected visible" tells you nothing about which iteration failed.


Negative Assertions

The .not modifier inverts any assertion. It also auto-retries — Playwright waits until the condition is not true (or times out).

Negative assertion examples
// Toast notification should disappear
await expect(page.getByRole('alert')).not.toBeVisible();

// URL should NOT contain /login after successful auth
await expect(page).not.toHaveURL(/\/login/);

// Input should NOT have the error class
await expect(page.getByLabel('Email'))
  .not.toHaveClass(/error/);

// Button should NOT be disabled after form is complete
await expect(page.getByRole('button', { name: 'Submit' }))
  .not.toBeDisabled();

Common Assertion Mistakes

These are the patterns that cause the most flaky failures in real-world test suites — and how to fix each one.

Mistake 1: Not awaiting assertions

Forgetting await is the single most common assertion bug. The assertion fires but the test does not wait for it, so it passes even when it should fail.

Do this
await expect(locator).toBeVisible();
Not this
expect(locator).toBeVisible();

Mistake 2: Using the wrong assertion type

Using a generic Jest-style assertion on a DOM value fetched with .textContent() defeats auto-retry.

Do this
await expect(locator).toHaveText('Hello');
Not this
const text = await locator.textContent(); expect(text).toBe('Hello');

Mistake 3: Exact string matching on dynamic content

Hard-coding timestamps, counts, or user-specific content in assertions guarantees failures in different environments.

Do this
await expect(el).toHaveText(/Welcome, .+/);
Not this
await expect(el).toHaveText('Welcome, John');

Mistake 4: Asserting before the action completes

Do not insert manual waits. Playwright auto-retrying assertions already handle timing. If you find yourself writing page.waitForTimeout(2000) before an assertion, the assertion itself should be enough.

Do this
await button.click(); await expect(modal).toBeVisible();
Not this
await button.click(); await page.waitForTimeout(2000); await expect(modal).toBeVisible();

Generate Assertions with Claude AI

Writing assertions manually for every element is tedious. In the Playwright + Claude AI & MCP Server course, you will learn how to use Claude AI to automatically generate assertion blocks from page screenshots and DOM snapshots. Claude analyzes the visible UI and produces the correct expect() calls — including the right locator, the right assertion method, and sensible custom messages.

The course covers how to connect Claude AI to your Playwright project via the MCP Server, enabling a workflow where you describe what to verify and AI writes the assertions for you. You will also learn how to review and refine AI-generated assertions to ensure they are stable and meaningful.


Frequently Asked Questions

What is the difference between Playwright assertions and Node.js assert?

Playwright's expect() auto-retries until a condition is met or the timeout expires, making it ideal for dynamic web pages. Node.js assert checks once and fails immediately. Always use Playwright's built-in expect() for web testing to avoid timing-related flakiness.

How long do Playwright assertions wait before failing?

The default auto-retry timeout is 5 seconds (5000ms). You can change it globally in playwright.config.ts with expect: { timeout: 10000 } or per-assertion with expect(locator).toBeVisible({ timeout: 10000 }).

What is the difference between toHaveText and toContainText?

toHaveText() checks that the element's full text content matches exactly (with whitespace normalization). toContainText() checks for a substring. Use toHaveText for strict validation and toContainText when you only need a partial match. Both support regex.

How do soft assertions work in Playwright?

Soft assertions (expect.soft()) record a failure but let the test continue. All failures are reported together at the end. Use them for verifying multiple independent conditions — like checking all widgets on a dashboard — without stopping at the first failure.

Can Playwright assertions match regular expressions?

Yes. Most text and URL assertions accept a RegExp: expect(page).toHaveURL(/\/dashboard/), expect(locator).toHaveText(/Welcome, .+/). Regex is preferred when content includes dynamic values like dates, counts, or user names.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn