Playwright is the leading E2E testing framework for web apps. It runs tests in Chromium, Firefox, and WebKit — in parallel, headlessly, and on CI — with built-in auto-waiting, network interception, and powerful tracing. This cheat sheet covers the full Playwright API.
Quick reference
| Task | Playwright API |
|---|---|
| Navigate to URL | await page.goto('https://example.com') |
| Click element | await page.click('button') |
| Type text | await page.fill('input', 'hello') |
| Get element | page.locator('h1') |
| Assert text | await expect(locator).toHaveText('Title') |
| Assert visible | await expect(locator).toBeVisible() |
| Wait for URL | await page.waitForURL('**/dashboard') |
| Screenshot | await page.screenshot({ path: 'out.png' }) |
| Intercept network | await page.route('**/api/**', handler) |
| New tab/page | const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a')]) |
| Evaluate JS | await page.evaluate(() => document.title) |
| Keyboard press | await page.keyboard.press('Enter') |
| Hover | await page.hover('.menu-item') |
| Select option | await page.selectOption('select', 'value') |
| Upload file | await page.setInputFiles('input[type=file]', 'file.pdf') |
| Check checkbox | await page.check('input[type=checkbox]') |
| Get attribute | await page.getAttribute('a', 'href') |
| Count elements | await page.locator('li').count() |
| Wait for element | await page.locator('.spinner').waitFor({ state: 'hidden' }) |
| Mock API response | await page.route('/api/users', r => r.fulfill({ json: [] })) |
| Run API request | await request.get('https://api.example.com/users') |
| Set cookie | await context.addCookies([{ name: 'token', value: '123', ... }]) |
| Clear storage | await context.clearCookies() |
Installation and setup
npm init playwright@latest
# Installs Playwright + browsers + generates playwright.config.ts
# Manual install
npm install -D @playwright/test
npx playwright install # install browsers
npx playwright install chromium # one browser only
playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000, // per-test timeout
expect: { timeout: 5_000 },// assertion timeout
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // record traces on failures
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['Pixel 5'] } },
],
// Spin up dev server before tests
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Writing tests
import { test, expect } from '@playwright/test';
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My App/);
await expect(page.locator('h1')).toBeVisible();
});
test.describe('Auth flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('logs in successfully', async ({ page }) => {
await page.fill('[name=email]', 'user@example.com');
await page.fill('[name=password]', 'secret');
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard');
await expect(page.locator('h1')).toHaveText('Dashboard');
});
test('shows error on bad creds', async ({ page }) => {
await page.fill('[name=email]', 'bad@example.com');
await page.fill('[name=password]', 'wrong');
await page.click('button[type=submit]');
await expect(page.locator('.error')).toContainText('Invalid credentials');
});
});
Locators (selectors)
Playwright recommends role-based and user-visible selectors over CSS/XPath.
// Role-based (recommended — resilient)
page.getByRole('button', { name: 'Submit' })
page.getByRole('heading', { level: 1 })
page.getByRole('link', { name: 'Sign up' })
page.getByRole('checkbox', { name: 'Remember me' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('combobox', { name: 'Country' })
page.getByRole('listitem')
// Text
page.getByText('Welcome back')
page.getByText(/error/i) // regex
// Label
page.getByLabel('Password')
// Placeholder
page.getByPlaceholder('Search...')
// Test ID (recommended for stable selectors)
page.getByTestId('submit-btn') // data-testid="submit-btn"
// Alt text (images)
page.getByAltText('Company logo')
// Title attribute
page.getByTitle('Close dialog')
// CSS (fallback)
page.locator('.card > h2')
page.locator('#main-content')
page.locator('input[type=email]')
// XPath (last resort)
page.locator('xpath=//button[@class="primary"]')
// Chaining locators
page.locator('.card').filter({ hasText: 'Premium' }).getByRole('button')
page.locator('ul').locator('li').nth(2)
// Multiple elements
const items = page.locator('li');
const count = await items.count();
const first = items.first();
const last = items.last();
const third = items.nth(2); // 0-indexed
Assertions (expect)
All Playwright assertions auto-retry until the condition is met or timeout is reached.
// Page assertions
await expect(page).toHaveTitle('My App');
await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page).toHaveURL(/\/dashboard/);
// Locator assertions
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeEmpty(); // input is empty
await expect(locator).toBeFocused();
await expect(locator).toHaveText('Hello');
await expect(locator).toHaveText(/hello/i);
await expect(locator).toContainText('ell');
await expect(locator).toHaveValue('input value');
await expect(locator).toHaveAttribute('href', '/about');
await expect(locator).toHaveClass('active');
await expect(locator).toHaveClass(/btn-/);
await expect(locator).toHaveCount(5);
await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');
// Negation
await expect(locator).not.toBeVisible();
await expect(locator).not.toHaveText('Error');
// Custom timeout
await expect(locator).toBeVisible({ timeout: 10_000 });
// API response assertions
await expect(response).toBeOK(); // status 200-299
Actions
// Navigation
await page.goto('https://example.com');
await page.goto('/about'); // relative to baseURL
await page.goBack();
await page.goForward();
await page.reload();
// Clicking
await page.click('button');
await page.dblclick('.item');
await page.click('text=Submit');
await page.click('button', { button: 'right' }); // right-click
await page.click('.link', { modifiers: ['Shift'] });
// Typing
await page.fill('input[name=email]', 'user@example.com'); // clear + type
await page.type('textarea', 'Hello', { delay: 100 }); // character by character
await page.press('input', 'Enter');
await page.keyboard.type('Hello World');
await page.keyboard.press('Control+A');
await page.keyboard.press('Backspace');
// Form controls
await page.check('input[type=checkbox]');
await page.uncheck('input[type=checkbox]');
await page.selectOption('select', 'value');
await page.selectOption('select', { label: 'Option Label' });
await page.selectOption('select', ['val1', 'val2']); // multi-select
// File upload
await page.setInputFiles('input[type=file]', 'path/to/file.pdf');
await page.setInputFiles('input[type=file]', ['file1.jpg', 'file2.jpg']);
await page.setInputFiles('input[type=file]', []); // clear selection
// Hover and focus
await page.hover('.tooltip-trigger');
await page.focus('input[name=email]');
// Drag and drop
await page.dragAndDrop('#source', '#target');
Waiting strategies
Playwright auto-waits for elements to be actionable. Use explicit waits only when needed.
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForURL(/\/profile\/\d+/);
// Wait for element state
await page.locator('.spinner').waitFor({ state: 'hidden' });
await page.locator('.result').waitFor({ state: 'visible' });
await page.locator('#chart').waitFor({ state: 'attached' });
// Wait for network
await page.waitForResponse('**/api/users');
await page.waitForRequest('**/api/**');
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// Wait with expect (preferred)
await expect(page.locator('.toast')).toBeVisible();
await expect(page.locator('.toast')).toBeHidden();
// Wait for function
await page.waitForFunction(() => window.__READY__ === true);
Network interception and mocking
// Mock API response
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Alice' }]),
});
});
// Block requests (ads, analytics)
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
await page.route('**/analytics/**', route => route.abort());
// Modify request
await page.route('**/api/**', async route => {
const request = route.request();
await route.continue({
headers: { ...request.headers(), 'x-custom': 'value' },
});
});
// Capture response
const responsePromise = page.waitForResponse('**/api/data');
await page.click('#load-btn');
const response = await responsePromise;
const body = await response.json();
// Assert request was made
const requestPromise = page.waitForRequest(req =>
req.url().includes('/api/login') && req.method() === 'POST'
);
await page.click('button[type=submit]');
await requestPromise;
API testing (without browser)
import { test, expect } from '@playwright/test';
test('GET /users returns list', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toHaveLength(3);
expect(body[0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) });
});
test('POST /users creates user', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: 'alice@example.com' },
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.id).toBeDefined();
});
test('DELETE /users/:id', async ({ request }) => {
const response = await request.delete('/api/users/42');
expect(response.ok()).toBeTruthy();
});
Page Object Model (POM)
// pages/login-page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test('login success', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'secret');
await expect(page).toHaveURL(/\/dashboard/);
});
test('login failure', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('bad@example.com', 'wrong');
await loginPage.expectError('Invalid credentials');
});
Fixtures (custom test context)
// fixtures.ts
import { test as base, Page } from '@playwright/test';
import { LoginPage } from './pages/login-page';
import { DashboardPage } from './pages/dashboard-page';
type Fixtures = {
loginPage: LoginPage;
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
// Fixture that auto-logs in
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('[name=email]', 'user@example.com');
await page.fill('[name=password]', 'secret');
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard');
await use(page);
},
});
// Usage in test
import { test } from './fixtures';
test('dashboard shows user name', async ({ authenticatedPage: page }) => {
await expect(page.locator('.user-name')).toHaveText('Alice');
});
Screenshots and visual testing
// Capture screenshots
await page.screenshot({ path: 'screenshot.png' });
await page.screenshot({ path: 'fullpage.png', fullPage: true });
await page.locator('.card').screenshot({ path: 'card.png' });
// Visual comparison (snapshot testing)
await expect(page).toHaveScreenshot('homepage.png');
await expect(page.locator('.chart')).toHaveScreenshot('chart.png');
// Update snapshots
// npx playwright test --update-snapshots
// Configure threshold
await expect(page).toHaveScreenshot('page.png', {
maxDiffPixels: 100,
threshold: 0.2, // 0-1, percentage of pixels that can differ
});
Multiple tabs, popups, and iframes
// Handle popup window
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.click('a[target=_blank]'),
]);
await popup.waitForLoadState();
await expect(popup).toHaveTitle('New Window');
// New tab via context
const newPage = await context.newPage();
await newPage.goto('https://other.com');
// Iframe
const frame = page.frameLocator('iframe[title="Payment"]');
await frame.locator('input[name=card]').fill('4242 4242 4242 4242');
await frame.getByRole('button', { name: 'Pay' }).click();
// All frames
for (const frame of page.frames()) {
console.log(frame.url());
}
Authentication state (reuse login)
// global-setup.ts — log in once and save session
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/login');
await page.fill('[name=email]', 'user@example.com');
await page.fill('[name=password]', 'secret');
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard');
// Save session (cookies + localStorage)
await page.context().storageState({ path: 'auth-state.json' });
await browser.close();
}
export default globalSetup;
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup.ts',
use: {
storageState: 'auth-state.json', // reuse session for all tests
},
// Override per project:
projects: [
{
name: 'authenticated',
use: { storageState: 'auth-state.json' },
},
{
name: 'unauthenticated',
use: { storageState: undefined },
},
],
});
CLI commands
| Command | Description |
|---|---|
npx playwright test |
Run all tests |
npx playwright test login |
Run tests matching "login" |
npx playwright test --project=chromium |
One browser only |
npx playwright test --headed |
Show browser window |
npx playwright test --debug |
Debug mode (step through) |
npx playwright test --ui |
Open interactive UI mode |
npx playwright test --trace on |
Always record traces |
npx playwright show-report |
Open HTML report |
npx playwright show-trace trace.zip |
Open trace viewer |
npx playwright codegen https://example.com |
Record tests interactively |
npx playwright test --update-snapshots |
Update visual snapshots |
npx playwright install |
Install all browsers |
npx playwright install chromium |
Install one browser |
CI/CD setup
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps # browsers + system deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
page.click() on hidden element |
Fails with "not visible" | await locator.waitFor({ state: 'visible' }) or use force: true carefully |
await page.locator('li').click() with multiple matches |
Strict mode error | Use .first(), .nth(n), or .filter() to target one element |
Using page.waitForTimeout(2000) |
Flaky, slow tests | Use expect(locator).toBeVisible() — auto-waits |
Forgetting await on assertions |
Assert passes immediately (false positive) | Always await expect(...) |
Hardcoded localhost:3000 URLs |
Breaks CI config | Use baseURL in config and relative paths |
page.$() / page.$$() |
Old Playwright API | Use page.locator() — it auto-waits |
| Not resetting state between tests | Test pollution | Use beforeEach or test.use({ storageState: undefined }) |
| XPath and CSS selectors | Brittle, break on refactor | Prefer getByRole, getByLabel, getByTestId |
FAQ
Q: Playwright vs Cypress — which should I use?
Playwright supports multiple browsers (Chromium/Firefox/WebKit), runs tests in parallel by default, and supports multiple tabs/iframes natively. Cypress has better DevTools integration but is Chromium-only (Firefox/WebKit are experimental). Playwright is now the recommended choice for new projects.
Q: How do I run tests in parallel?
Tests run in parallel by default when fullyParallel: true is set. Each worker gets its own browser context. Use test.describe.configure({ mode: 'serial' }) to run a suite sequentially.
Q: How do I debug a failing test?
Run npx playwright test --debug to pause on each step, or npx playwright test --ui for the interactive UI. Add await page.pause() anywhere to pause execution. After a CI failure, open the HTML report (npx playwright show-report) and view the trace.
Q: How do I test authenticated pages without logging in every test?
Use globalSetup to log in once and save session with storageState. Apply storageState: 'auth-state.json' in use config to reuse cookies across all tests.
Q: How do I handle flaky tests?
Set retries: 2 in config for CI. Use expect assertions instead of waitForTimeout. Avoid page.$() (no auto-wait). Use { timeout } option to increase timeout for slow operations. Enable trace: 'on-first-retry' to diagnose failures.
Q: Can Playwright test APIs without a browser?
Yes. Use the request fixture in tests — it gives you an APIRequestContext that supports GET/POST/PUT/DELETE. No browser is launched. Useful for testing REST APIs or seeding data before UI tests.