Skip to main content

Introduction

Playwright Test includes assertion methods optimized for the web with automatic retrying and detailed error messages.

Basic Assertions

From src/matchers/expect.ts:206-243:
import { test, expect } from '@playwright/test';

test('basic assertions', async ({ page }) => {
  // Generic assertions
  expect(1 + 1).toBe(2);
  expect('hello').toContain('ell');
  expect([1, 2, 3]).toHaveLength(3);
  expect({ name: 'John' }).toEqual({ name: 'John' });
});

Web-First Assertions

Playwright provides auto-retrying assertions for web elements from src/matchers/matchers.ts:

Visibility Assertions

toBeVisible
async
Assert element is visibleFrom src/matchers/matchers.ts:160-171:
await expect(page.locator('button')).toBeVisible();
await expect(page.locator('button')).toBeVisible({ timeout: 5000 });

// Negated
await expect(page.locator('.loading')).not.toBeVisible();

// With visible option
await expect(page.locator('button')).toBeVisible({ visible: true });
await expect(page.locator('.hidden')).toBeVisible({ visible: false });
toBeHidden
async
Assert element is hiddenFrom src/matchers/matchers.ts:150-158:
await expect(page.locator('.modal')).toBeHidden();

Attachment State

toBeAttached
async
Assert element is attached to DOMFrom src/matchers/matchers.ts:56-67:
await expect(page.locator('#element')).toBeAttached();
await expect(page.locator('#removed')).toBeAttached({ attached: false });

Element State Assertions

toBeEnabled
async
Assert element is enabledFrom src/matchers/matchers.ts:127-138:
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeEnabled({ enabled: false });
toBeDisabled
async
Assert element is disabledFrom src/matchers/matchers.ts:94-102:
await expect(page.locator('button:disabled')).toBeDisabled();
toBeEditable
async
Assert element is editableFrom src/matchers/matchers.ts:104-115:
await expect(page.locator('input')).toBeEditable();
await expect(page.locator('input[readonly]')).toBeEditable({ editable: false });
toBeEmpty
async
Assert element is emptyFrom src/matchers/matchers.ts:117-125:
await expect(page.locator('.list')).toBeEmpty();
toBeFocused
async
Assert element is focusedFrom src/matchers/matchers.ts:140-148:
await expect(page.locator('input[name=username]')).toBeFocused();
toBeChecked
async
Assert checkbox is checkedFrom src/matchers/matchers.ts:69-92:
await expect(page.locator('input[type=checkbox]')).toBeChecked();
await expect(page.locator('input[type=checkbox]')).toBeChecked({ checked: false });

// Check for indeterminate state
await expect(page.locator('input[type=checkbox]')).toBeChecked({ indeterminate: true });
toBeInViewport
async
Assert element is in viewportFrom src/matchers/matchers.ts:173-181:
await expect(page.locator('.hero')).toBeInViewport();

// At least 50% visible
await expect(page.locator('.banner')).toBeInViewport({ ratio: 0.5 });

Text Content Assertions

toHaveText
async
Assert element has exact textFrom src/matchers/matchers.ts:380-397:
// String match
await expect(page.locator('.title')).toHaveText('Welcome');

// Regex match
await expect(page.locator('.title')).toHaveText(/Welcome/);

// Array match (for multiple elements)
await expect(page.locator('li')).toHaveText([
  'Item 1',
  'Item 2',
  'Item 3',
]);

// Options
await expect(page.locator('.title')).toHaveText('welcome', { 
  ignoreCase: true,
  useInnerText: true,
});
toContainText
async
Assert element contains text substringFrom src/matchers/matchers.ts:183-200:
await expect(page.locator('.description')).toContainText('product');
await expect(page.locator('.description')).toContainText(/product/i);

// Multiple elements
await expect(page.locator('li')).toContainText(['Apple', 'Banana']);

Attribute Assertions

toHaveAttribute
async
Assert element has attributeFrom src/matchers/matchers.ts:238-261:
// Check attribute exists
await expect(page.locator('button')).toHaveAttribute('disabled');

// Check attribute value
await expect(page.locator('a')).toHaveAttribute('href', '/about');
await expect(page.locator('a')).toHaveAttribute('href', /\/about/);

// Case insensitive
await expect(page.locator('a')).toHaveAttribute('href', '/ABOUT', { 
  ignoreCase: true 
});
toHaveClass
async
Assert element has CSS classFrom src/matchers/matchers.ts:263-280:
// Single class
await expect(page.locator('button')).toHaveClass('btn-primary');
await expect(page.locator('button')).toHaveClass(/btn-/);

// Multiple elements
await expect(page.locator('.item')).toHaveClass([
  'item active',
  'item',
  'item disabled',
]);
toContainClass
async
Assert element contains CSS classFrom src/matchers/matchers.ts:282-303:
await expect(page.locator('button')).toContainClass('active');

// Multiple elements contain class
await expect(page.locator('.item')).toContainClass(['visible', 'enabled']);
toHaveId
async
Assert element has IDFrom src/matchers/matchers.ts:342-352:
await expect(page.locator('input')).toHaveId('username');
await expect(page.locator('input')).toHaveId(/user/);
toHaveValue
async
Assert input has valueFrom src/matchers/matchers.ts:399-409:
await expect(page.locator('input')).toHaveValue('John');
await expect(page.locator('input')).toHaveValue(/john/i);
toHaveValues
async
Assert select has valuesFrom src/matchers/matchers.ts:411-421:
await expect(page.locator('select')).toHaveValues(['option1', 'option2']);

CSS Assertions

toHaveCSS
async
Assert element has CSS propertyFrom src/matchers/matchers.ts:316-340:
// Single property
await expect(page.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(page.locator('button')).toHaveCSS('display', /flex/);

// Multiple properties
await expect(page.locator('button')).toHaveCSS({
  'color': 'rgb(255, 0, 0)',
  'background-color': 'rgb(0, 0, 255)',
});

JavaScript Property Assertions

toHaveJSProperty
async
Assert element has JavaScript propertyFrom src/matchers/matchers.ts:354-364:
await expect(page.locator('input')).toHaveJSProperty('value', 'text');
await expect(page.locator('input')).toHaveJSProperty('disabled', true);

Count Assertion

toHaveCount
async
Assert locator matches count of elementsFrom src/matchers/matchers.ts:305-314:
await expect(page.locator('li')).toHaveCount(3);
await expect(page.locator('.item')).toHaveCount(0);

Accessibility Assertions

toHaveAccessibleName
async
Assert element has accessible nameFrom src/matchers/matchers.ts:214-224:
await expect(page.locator('button')).toHaveAccessibleName('Submit');
await expect(page.locator('button')).toHaveAccessibleName(/submit/i);
toHaveAccessibleDescription
async
Assert element has accessible descriptionFrom src/matchers/matchers.ts:202-212:
await expect(page.locator('button')).toHaveAccessibleDescription('Click to submit');
toHaveAccessibleErrorMessage
async
Assert element has accessible error messageFrom src/matchers/matchers.ts:226-236:
await expect(page.locator('input')).toHaveAccessibleErrorMessage('Invalid email');
toHaveRole
async
Assert element has ARIA roleFrom src/matchers/matchers.ts:366-378:
await expect(page.locator('button')).toHaveRole('button');
await expect(page.locator('nav')).toHaveRole('navigation');

Page Assertions

toHaveTitle
async
Assert page has titleFrom src/matchers/matchers.ts:423-433:
await expect(page).toHaveTitle('Welcome');
await expect(page).toHaveTitle(/Welcome/);
toHaveURL
async
Assert page has URLFrom src/matchers/matchers.ts:435-454:
await expect(page).toHaveURL('https://example.com/about');
await expect(page).toHaveURL(/about/);

// Case insensitive
await expect(page).toHaveURL('https://example.com/About', { 
  ignoreCase: true 
});

API Response Assertions

toBeOK
async
Assert API response is successful (status 200-299)From src/matchers/matchers.ts:456-481:
const response = await page.request.get('https://api.example.com/data');
await expect(response).toBeOK();

Snapshot Assertions

toHaveScreenshot
async
Assert screenshot matches baseline
await expect(page).toHaveScreenshot();
await expect(page).toHaveScreenshot('homepage.png');
await expect(page).toHaveScreenshot({
  maxDiffPixels: 100,
  threshold: 0.2,
});
toMatchSnapshot
sync
Assert value matches snapshot
expect(data).toMatchSnapshot('user-data.json');

Polling Assertions

From src/matchers/expect.ts:127-131,367-403:
expect.poll
async
Poll until condition is met
await expect.poll(async () => {
  const response = await page.request.get('/api/status');
  return response.status();
}).toBe(200);

// With timeout and intervals
await expect.poll(async () => {
  return await page.locator('.count').textContent();
}, {
  timeout: 10000,
  intervals: [100, 250, 500],
}).toBe('5');
toPass
async
Retry assertion until it passesFrom src/matchers/matchers.ts:483-517:
await expect(async () => {
  const response = await page.request.get('/api/status');
  expect(response.status()).toBe(200);
}).toPass({
  timeout: 10000,
  intervals: [100, 500, 1000],
});

Soft Assertions

From src/matchers/expect.ts:117-121:
test('soft assertions', async ({ page }) => {
  // Test continues even if these fail
  await expect.soft(page.locator('.title')).toHaveText('Welcome');
  await expect.soft(page.locator('.subtitle')).toBeVisible();
  
  // All failures reported at the end
});

Custom Assertions

From src/matchers/expect.ts:101-114:
import { expect as base } from '@playwright/test';

export const expect = base.extend({
  async toHaveValidEmail(locator: Locator) {
    const text = await locator.textContent();
    const isValid = /^[^@]+@[^@]+\.[^@]+$/.test(text || '');
    
    return {
      pass: isValid,
      message: () => `Expected ${text} to be a valid email`,
    };
  },
});

// Usage
await expect(page.locator('.email')).toHaveValidEmail();

Assertion Options

All web-first assertions support timeout:
await expect(page.locator('.loading')).not.toBeVisible({ 
  timeout: 5000 
});
From src/matchers/expect.ts:136-153,181-198:
// Configure timeout globally
export default {
  expect: {
    timeout: 10000,
  },
};

// Configure per test
test('with custom timeout', async ({ page }) => {
  await expect.configure({ timeout: 15000 })(page.locator('.slow'))
    .toBeVisible();
});

Negation

All assertions can be negated with .not:
await expect(page.locator('.hidden')).not.toBeVisible();
await expect(page.locator('button')).not.toBeDisabled();
await expect(page).not.toHaveURL(/admin/);

Custom Messages

From src/matchers/expect.ts:86-88,305-309:
await expect(page.locator('.count'), 'Item count should be 5')
  .toHaveText('5');

await expect.configure({ 
  message: 'Custom failure message' 
})(page.locator('.title')).toBeVisible();

Best Practices

  1. Use web-first assertions: They auto-retry and wait for conditions
  2. Prefer specific assertions: Use toHaveText instead of toBe
  3. Use soft assertions for multiple checks: Continue test execution
  4. Set appropriate timeouts: Based on your application’s behavior
  5. Add custom messages: For better debugging

Next Steps

Test Fixtures

Learn about test fixtures

Test Hooks

Set up test environment with hooks

Build docs developers (and LLMs) love