Playwright: Modern End-to-End Testing Guide
Master Playwright for browser automation and testing. Learn selectors, assertions, API testing, visual testing, and build reliable E2E test suites.
Moshiour Rahman
Advertisement
What is Playwright?
Playwright is a modern end-to-end testing framework by Microsoft. It supports all major browsers, provides powerful automation APIs, and enables reliable, fast testing.
Playwright Features
| Feature | Description |
|---|---|
| Cross-browser | Chrome, Firefox, Safari |
| Auto-wait | Reliable element detection |
| Network | Intercept and mock |
| Mobile | Device emulation |
| Visual | Screenshot comparison |
Getting Started
Installation
# Install Playwright
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
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 Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Writing Tests
Basic Test Structure
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display welcome message', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
test('should navigate to about page', async ({ page }) => {
await page.click('text=About');
await expect(page).toHaveURL('/about');
});
});
Selectors
import { test, expect } from '@playwright/test';
test('selector examples', async ({ page }) => {
await page.goto('/');
// Role-based selectors (recommended)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Home' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
await page.getByRole('checkbox', { name: 'Accept terms' }).check();
// Text selectors
await page.getByText('Welcome').click();
await page.getByText(/hello/i).click(); // Regex
// Label and placeholder
await page.getByLabel('Username').fill('john');
await page.getByPlaceholder('Enter email').fill('john@example.com');
// Test ID
await page.getByTestId('submit-button').click();
// CSS selectors
await page.locator('.my-class').click();
await page.locator('#my-id').click();
await page.locator('[data-test="value"]').click();
// XPath
await page.locator('xpath=//button').click();
// Chaining
await page.locator('.card').filter({ hasText: 'Product' }).click();
await page.locator('.list').locator('.item').first().click();
});
Assertions
import { test, expect } from '@playwright/test';
test('assertion examples', async ({ page }) => {
await page.goto('/');
// Visibility
await expect(page.getByText('Hello')).toBeVisible();
await expect(page.getByText('Hidden')).toBeHidden();
// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Welc');
// Attributes
await expect(page.locator('input')).toHaveAttribute('type', 'email');
await expect(page.locator('input')).toHaveValue('test@example.com');
// Count
await expect(page.locator('.item')).toHaveCount(5);
// URL and title
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('Dashboard');
// CSS
await expect(page.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
// State
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toBeDisabled();
await expect(page.getByRole('checkbox')).toBeChecked();
// Negation
await expect(page.getByText('Error')).not.toBeVisible();
});
Form Interactions
import { test, expect } from '@playwright/test';
test('form interactions', async ({ page }) => {
await page.goto('/form');
// Text input
await page.getByLabel('Name').fill('John Doe');
await page.getByLabel('Name').clear();
await page.getByLabel('Name').type('Jane Doe', { delay: 100 });
// Select dropdown
await page.getByLabel('Country').selectOption('US');
await page.getByLabel('Country').selectOption({ label: 'United States' });
// Multi-select
await page.getByLabel('Skills').selectOption(['javascript', 'python']);
// Checkbox and radio
await page.getByRole('checkbox', { name: 'Newsletter' }).check();
await page.getByRole('checkbox', { name: 'Newsletter' }).uncheck();
await page.getByRole('radio', { name: 'Male' }).check();
// File upload
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf');
await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf']);
// Submit
await page.getByRole('button', { name: 'Submit' }).click();
// Verify success
await expect(page.getByText('Form submitted')).toBeVisible();
});
Navigation and Waits
import { test, expect } from '@playwright/test';
test('navigation and waits', async ({ page }) => {
// Navigate
await page.goto('https://example.com');
await page.goto('/dashboard', { waitUntil: 'networkidle' });
// Click with navigation
await Promise.all([
page.waitForNavigation(),
page.click('text=Next Page')
]);
// Wait for element
await page.waitForSelector('.loading', { state: 'hidden' });
await page.waitForSelector('.content', { state: 'visible' });
// Wait for response
const response = await page.waitForResponse('**/api/users');
const data = await response.json();
// Wait for request
const request = await page.waitForRequest('**/api/submit');
// Wait for function
await page.waitForFunction(() => {
return document.querySelector('.count')?.textContent === '10';
});
// Wait for timeout (avoid if possible)
await page.waitForTimeout(1000);
// Wait for load state
await page.waitForLoadState('domcontentloaded');
await page.waitForLoadState('networkidle');
});
API Testing
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
test('GET request', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toHaveLength(10);
});
test('POST request', async ({ request }) => {
const response = await request.post('/api/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.name).toBe('John Doe');
});
test('with headers', async ({ request }) => {
const response = await request.get('/api/protected', {
headers: {
Authorization: 'Bearer token123'
}
});
expect(response.ok()).toBeTruthy();
});
});
Mocking Network
import { test, expect } from '@playwright/test';
test('mock API response', async ({ page }) => {
// Mock response
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock User' }
])
});
});
await page.goto('/users');
await expect(page.getByText('Mock User')).toBeVisible();
});
test('modify response', async ({ page }) => {
await page.route('**/api/users', async route => {
const response = await route.fetch();
const json = await response.json();
// Modify response
json.push({ id: 999, name: 'Added User' });
await route.fulfill({
response,
body: JSON.stringify(json)
});
});
await page.goto('/users');
});
test('block requests', async ({ page }) => {
// Block images
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());
// Block analytics
await page.route('**/analytics/**', route => route.abort());
await page.goto('/');
});
Visual Testing
import { test, expect } from '@playwright/test';
test('visual comparison', async ({ page }) => {
await page.goto('/');
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png');
// Element screenshot
await expect(page.locator('.hero')).toHaveScreenshot('hero.png');
// With options
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
threshold: 0.2,
animations: 'disabled'
});
});
test('take screenshot', async ({ page }) => {
await page.goto('/');
// Save screenshot
await page.screenshot({ path: 'screenshot.png' });
// Full page
await page.screenshot({ path: 'full.png', fullPage: true });
// Specific element
await page.locator('.card').screenshot({ path: 'card.png' });
});
Authentication
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
// Save auth state
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
dependencies: ['setup'],
use: {
storageState: 'playwright/.auth/user.json',
},
},
],
});
Page Object Model
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
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();
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('should login successfully', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
Running Tests
# Run all tests
npx playwright test
# Run specific file
npx playwright test login.spec.ts
# Run specific test
npx playwright test -g "should login"
# Run in headed mode
npx playwright test --headed
# Run specific browser
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
# Show report
npx playwright show-report
# Update snapshots
npx playwright test --update-snapshots
Summary
| Feature | Usage |
|---|---|
| Selectors | getByRole(), getByText() |
| Assertions | expect() with matchers |
| Navigation | goto(), waitFor*() |
| API Testing | request.get(), request.post() |
| Mocking | page.route() |
| Visual | toHaveScreenshot() |
| Auth | storageState |
Playwright provides reliable, cross-browser end-to-end testing for modern web applications.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
Turborepo: High-Performance Monorepo Build System
Master Turborepo for monorepo management. Learn workspace setup, caching, pipelines, and build performant multi-package JavaScript projects.
JavaScriptReact Hooks Complete Guide: useState to Custom Hooks
Master all React hooks from basics to advanced. Learn useState, useEffect, useContext, useReducer, useMemo, useCallback, and create custom hooks.
JavaScriptTypeScript Advanced Guide: Types, Generics, and Patterns
Master advanced TypeScript concepts. Learn generics, utility types, conditional types, mapped types, and professional patterns for type-safe code.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.