Skip to main content

Playwright Assertions for React: Verify Component State

An assertion is a test statement that verifies expected behavior. Playwright's assertion library is built on Expect.js and provides over 20 matchers specifically designed for web testing—checking visibility, text content, attributes, form values, and more. Unlike generic assertions, Playwright assertions auto-wait and retry, so a flaky race condition between your React state update and your assertion won't cause a false negative.

What Are Assertions and Why Auto-Retry Matters

An assertion verifies that your application behaves as expected. In Playwright, assertions are not simple synchronous checks—they retry automatically for up to 5 seconds (by default) while waiting for elements to appear or change. This eliminates a huge source of test flakiness:

// This assertion retries for up to 5 seconds until the text appears
// (perfect for React state updates that happen asynchronously)
await expect(page.locator('[data-testid="success-message"]'))
.toContainText('Account created!');

If the message hasn't appeared after 5 seconds, the assertion fails with a clear error showing the actual state. This is superior to older test frameworks where you'd have to manually await page.waitForSelector() before asserting.

Essential Assertions for React Components

Visibility Assertions

Check whether elements are visible or hidden (CSS visibility, display, and opacity):

// Element is visible (not display:none, not visibility:hidden, not opacity:0)
await expect(page.locator('[data-testid="user-panel"]')).toBeVisible();

// Element is NOT visible
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible();

// Element is visible within a timeout
await expect(page.locator('[data-testid="modal"]')).toBeVisible({ timeout: 10000 });

// Element exists in the DOM but may be hidden
await expect(page.locator('[data-testid="hidden-content"]')).toBeInViewport();

Use toBeVisible() for elements that should be rendered and interactive. Use toBeInViewport() for elements that exist but might be scrolled off-screen.

Text Content Assertions

Verify that elements contain expected text:

// Exact text match
await expect(page.locator('h1')).toHaveText('Welcome to React');

// Partial text match (substring)
await expect(page.locator('.error-message')).toContainText('Email is invalid');

// Text with regex (case-insensitive)
await expect(page.locator('button')).toContainText(/submit/i);

// Multiple elements containing text
const items = page.locator('[data-testid="todo-item"]');
await expect(items).toContainText(['Buy milk', 'Pay bills']);

toHaveText() is strict (exact match); use toContainText() for partial or flexible matching. Both auto-retry, so they wait for React to finish rendering before checking.

Form Value Assertions

Verify form inputs have the expected values:

// Text input value
await expect(page.locator('[data-testid="email-input"]')).toHaveValue('[email protected]');

// Checkbox is checked
await expect(page.locator('[data-testid="agree-checkbox"]')).toBeChecked();

// Radio button is selected
await expect(page.locator('input[name="plan"][value="pro"]')).toBeChecked();

// Select dropdown has the right option selected
await expect(page.locator('select[name="country"]')).toHaveValue('US');

// Textarea contains text
await expect(page.locator('textarea')).toHaveValue('My comment...');

These assertions are essential for testing form interactions and ensuring React's form state management is working correctly.

Attribute Assertions

Check HTML attributes:

// Element has a specific attribute
await expect(page.locator('button')).toHaveAttribute('disabled');

// Attribute has a specific value
await expect(page.locator('a')).toHaveAttribute('href', '/about');

// Attribute contains a substring
await expect(page.locator('img')).toHaveAttribute('src', /logo/);

// Attribute does NOT exist
await expect(page.locator('input')).not.toHaveAttribute('readonly');

Useful for verifying links go to the right places, images load from the correct URLs, and buttons have the right aria-labels.

Class and Style Assertions

Check CSS classes and inline styles:

// Element has a CSS class
await expect(page.locator('[data-testid="status"]')).toHaveClass('active');

// Element has multiple classes
await expect(page.locator('button')).toHaveClass(/btn-primary/);

// Element does NOT have a class
await expect(page.locator('[data-testid="error"]')).not.toHaveClass('hidden');

CSS class assertions are useful when your React component toggles classes for styling (e.g., adding error class when validation fails).

Count Assertions

Verify the number of elements:

// Exactly 5 items
await expect(page.locator('[data-testid="todo-item"]')).toHaveCount(5);

// At least 1 button
const buttons = page.locator('button');
expect(await buttons.count()).toBeGreaterThan(0);

// No error messages
await expect(page.locator('.error')).toHaveCount(0);

Count assertions are perfect for verifying list rendering in React—after adding an item to a todo list, you'd check that the count increased.

Advanced Assertions for Complex Scenarios

Box Model Assertions

Check element positioning and dimensions:

// Element is in the viewport
await expect(page.locator('[data-testid="banner"]')).toBeInViewport();

// Element has specific dimensions
const box = await page.locator('img').boundingBox();
expect(box?.width).toBe(300);
expect(box?.height).toBe(200);

// Element is above/below another
const submitButton = page.locator('button[type="submit"]');
const cancelButton = page.locator('button[type="button"]');
const submitBox = await submitButton.boundingBox();
const cancelBox = await cancelButton.boundingBox();
expect(submitBox!.y).toBeGreaterThan(cancelBox!.y);

Bounding box assertions help verify responsive layouts and visual order.

Enabled/Disabled State

Check if form controls are interactive:

// Button is enabled and clickable
await expect(page.locator('button[data-testid="submit"]')).toBeEnabled();

// Button is disabled
await expect(page.locator('button[data-testid="delete"]')).toBeDisabled();

// Input field is editable
const input = page.locator('[data-testid="email-input"]');
await expect(input).toBeEditable();

Essential for testing conditional logic—e.g., "Submit button is disabled until all required fields are filled."

Screenshot Comparison (Visual Assertions)

Compare a screenshot to a baseline:

// Compare full page
await expect(page).toHaveScreenshot('homepage.png');

// Compare a specific element
await expect(page.locator('[data-testid="card"]')).toHaveScreenshot('card.png');

// Update baseline (after intentional changes)
// npx playwright test --update-snapshots

Visual regression testing is covered in depth later in this series, but the assertion is simple: Playwright compares pixel-perfect screenshots and fails if they differ beyond a small threshold.

Writing Custom Assertions and Matchers

For complex validations, you can extend Playwright's expect with custom matchers:

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

test.extend({
expect: async (actual, customMatchers) => {
return expect(actual).extend({
async toHaveAriaLabel(expected: string) {
const label = await actual.getAttribute('aria-label');
return {
pass: label === expected,
message: () => `Expected aria-label "${expected}" but got "${label}"`,
};
},
});
},
});

test('button has correct aria-label', async ({ page }) => {
await page.goto('/');
const button = page.locator('button[data-testid="menu"]');
// @ts-ignore
await expect(button).toHaveAriaLabel('Open navigation menu');
});

Custom matchers are powerful for domain-specific validations, but most tests can be written with built-in assertions and a clean page-object pattern.

Assertion Best Practices and Common Pitfalls

Do:

  • Assert on user-visible behavior (text, visibility) rather than implementation details (React state, props).
  • Use specific locators (data-testid) to avoid brittle assertions.
  • Combine multiple assertions into logical groups—don't assert on every property separately.
  • Use expect(...).not for negations rather than checking absence separately.

Don't:

  • Assert on implementation details like component props or internal state (use React Testing Library if you need that level of granularity).
  • Use generic assertions like toEqual() on HTML—Playwright's purpose-built matchers are more reliable.
  • Ignore flaky assertions—if a test passes sometimes and fails other times, the assertion probably needs a longer timeout.
// Good: assert on visible behavior
await expect(page.locator('[data-testid="status"]')).toHaveText('Logged in as John');

// Bad: attempting to assert on React state (impossible with E2E tests)
// expect(componentState.user.name).toBe('John'); // Can't do this in E2E!

Key Takeaways

  • Playwright assertions auto-retry for 5 seconds by default, eliminating flakiness from race conditions.
  • Use toBeVisible(), toHaveText(), toHaveValue(), and toHaveAttribute() to verify component behavior.
  • Count assertions (toHaveCount()) are essential for testing list rendering in React.
  • Bounding box and screenshot assertions support advanced visual testing (covered later).
  • Extract assertions into helpers or Page Objects to reduce repetition and improve readability.

Frequently Asked Questions

What's the difference between toHaveText and toContainText?

toHaveText() requires an exact match (after trimming whitespace). toContainText() matches substrings and is more flexible. Use toContainText() for dynamic content (e.g., "You have 5 new messages").

Why does my assertion timeout instead of fail immediately?

Playwright waits up to 5 seconds for the assertion condition to become true. If the element never appears or the text never matches, it fails after the timeout. This is a feature—it absorbs timing issues. If the timeout is too short, extend it: await expect(element).toBeVisible({ timeout: 10000 }).

How do I assert on element count after a list renders?

Use toHaveCount() after an action that modifies the list:

await page.locator('[data-testid="add-item"]').click();
await expect(page.locator('[data-testid="todo-item"]')).toHaveCount(6);

Can I assert on CSS properties like color or font-size?

Not directly with assertions, but you can read the computed style and check it:

const element = page.locator('[data-testid="button"]');
const color = await element.evaluate(el => window.getComputedStyle(el).color);
expect(color).toBe('rgb(255, 0, 0)');

Should I use toBeVisible() or toBeInViewport() for hidden elements?

toBeVisible() checks that the element is displayed (not hidden by CSS). toBeInViewport() checks that it's in the currently visible part of the page (not scrolled off-screen). Use toBeVisible() for most tests; use toBeInViewport() only when scroll position matters.

Further Reading