Finding an element is half the job. The other half is verifying it is in the right state. Is it visible? Does it have the correct text? Is the checkbox checked? Is the button disabled? Playwright gives you a full set of assertions that auto-retry until the condition is met or the timeout expires.
import { test, expect } from '@playwright/test';
test('visibility assertions', async ({ page }) => {
await page.goto('/banking');
// Element IS visible
await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
// Element is NOT visible (hidden or not in DOM)
await expect(page.getByRole('alert')).not.toBeVisible();
// Equivalent: element is hidden
await expect(page.getByRole('alert')).toBeHidden();
// Element exists in DOM (even if hidden)
await expect(page.getByTestId('loading-spinner')).toBeAttached();
// Element does NOT exist in DOM at all
await expect(page.getByTestId('error-modal')).not.toBeAttached();
});import { test, expect } from '@playwright/test';
test('text assertions', async ({ page }) => {
await page.goto('/banking');
// Exact text match
await expect(page.getByTestId('balance')).toHaveText('$5,000.00');
// Regex match
await expect(page.getByTestId('balance')).toHaveText(/\$[\d,]+\.\d{2}/);
// Contains text (substring)
await expect(page.getByTestId('balance')).toContainText('5,000');
// Multiple elements -- check each one's text
await expect(page.getByRole('listitem')).toHaveText([
'Savings Account',
'Current Account',
'Fixed Deposit',
]);
});import { test, expect } from '@playwright/test';
test('input and form assertions', async ({ page }) => {
await page.goto('/banking');
// Input has specific value
await page.getByLabel('Username').fill('testuser');
await expect(page.getByLabel('Username')).toHaveValue('testuser');
// Input value matches regex
await expect(page.getByLabel('Email')).toHaveValue(/@/);
// Input is empty
await expect(page.getByLabel('Password')).toHaveValue('');
// Element is enabled
await expect(page.getByRole('button', { name: 'Login' })).toBeEnabled();
// Element is disabled
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
// Checkbox is checked
await page.getByRole('checkbox', { name: 'Remember me' }).check();
await expect(page.getByRole('checkbox', { name: 'Remember me' })).toBeChecked();
// Checkbox is NOT checked
await expect(page.getByRole('checkbox', { name: 'Newsletter' })).not.toBeChecked();
});import { test, expect } from '@playwright/test';
test('count and attribute assertions', async ({ page }) => {
await page.goto('/shopping');
// Exact count of elements
await expect(page.locator('.product-card')).toHaveCount(12);
// Element has a specific CSS class
await expect(page.getByRole('link', { name: 'Home' })).toHaveClass(/active/);
// Element has a specific attribute
await expect(page.getByRole('link', { name: 'Docs' })).toHaveAttribute('href', '/docs');
// Element has specific CSS property
await expect(page.getByRole('alert')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
// Page-level assertions
await expect(page).toHaveTitle(/Shopping/);
await expect(page).toHaveURL(/\/shopping/);
});| Assertion | What It Checks | Auto-Retries? |
|---|---|---|
| toBeVisible() | Element is visible on screen | Yes |
| toBeHidden() | Element is hidden or not in DOM | Yes |
| toBeAttached() | Element exists in DOM | Yes |
| toBeEnabled() | Element is not disabled | Yes |
| toBeDisabled() | Element has disabled attribute | Yes |
| toBeChecked() | Checkbox/radio is checked | Yes |
| toHaveText() | Element text matches exactly | Yes |
| toContainText() | Element text contains substring | Yes |
| toHaveValue() | Input value matches | Yes |
| toHaveCount() | Number of matching elements | Yes |
| toHaveAttribute() | Element has attribute with value | Yes |
| toHaveClass() | Element has CSS class | Yes |
| toHaveCSS() | Element has CSS property value | Yes |
| toHaveTitle() | Page title matches | Yes |
| toHaveURL() | Page URL matches | Yes |
All assertions auto-retry for 5 seconds by default (configurable via expect.timeout in playwright.config.ts). This means if an element takes 2 seconds to appear after a click, toBeVisible() waits and passes. No explicit waits needed.
Do not use expect(await locator.textContent()).toBe("text"). This grabs the text once without retrying. Use expect(locator).toHaveText("text") instead -- it auto-retries. The first approach is a snapshot, the second is a living assertion.
Key Point: Every Playwright assertion auto-retries. Use the built-in assertions (toBeVisible, toHaveText, etc.) instead of manual checks. The .not modifier inverts any assertion.
Key Point: Playwright assertions auto-retry for 5 seconds. Use toBeVisible, toHaveText, toHaveValue, toBeChecked, toHaveCount. Never snapshot text then assert -- use auto-retrying assertions.