Playwright Cheatsheet¶
Playwright - Modern Web Testing & Automation
Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is fast, reliable and capable.
Table of Contents¶
- Installation
- Getting Started
- Browser Management
- Page Navigation
- Element Interaction
- Selectors
- Waiting Strategies
- Form Handling
- Screenshots & Videos
- Network Interception
- Authentication
- Mobile Testing
- Testing Framework
- Debugging
- Performance Testing
- API Testing
- Visual Testing
- CI/CD Integration
- Advanced Features
- Best Practices
Installation¶
Basic Installation¶
# Install Playwright
npm init playwright@latest
# Or install manually
npm install -D @playwright/test
# Install browsers
npx playwright install
# Install specific browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
System Dependencies¶
# Install system dependencies (Linux)
npx playwright install-deps
# Ubuntu/Debian specific
sudo npx playwright install-deps
# Docker installation
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
Project Setup¶
# Create new Playwright project
npm init playwright@latest my-tests
# Project structure
my-tests/
├── tests/
│ └── example.spec.js
├── playwright.config.js
├── package.json
└── package-lock.json
Configuration File¶
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Test directory
testDir: './tests',
// Run tests in files in parallel
fullyParallel: true,
// Fail the build on CI if you accidentally left test.only in the source code
forbidOnly: !!process.env.CI,
// Retry on CI only
retries: process.env.CI ? 2 : 0,
// Opt out of parallel tests on CI
workers: process.env.CI ? 1 : undefined,
// Reporter to use
reporter: 'html',
// Shared settings for all the projects below
use: {
// Base URL to use in actions like `await page.goto('/')`
baseURL: 'http://127.0.0.1:3000',
// Collect trace when retrying the failed test
trace: 'on-first-retry',
// Record video on failure
video: 'retain-on-failure',
// Take screenshot on failure
screenshot: 'only-on-failure',
},
// Configure projects for major browsers
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'] },
},
],
// Run your local dev server before starting the tests
webServer: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
},
});
Getting Started¶
Basic Test Structure¶
// tests/example.spec.js
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
// Navigate to page
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring
await expect(page).toHaveTitle(/Playwright/);
// Click the get started link
await page.getByRole('link', { name: 'Get started' }).click();
// Expects the URL to contain intro
await expect(page).toHaveURL(/.*intro/);
});
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link
await page.getByRole('link', { name: 'Get started' }).click();
// Expects the URL to contain intro
await expect(page).toHaveURL(/.*intro/);
});
Running Tests¶
# Run all tests
npx playwright test
# Run tests in headed mode
npx playwright test --headed
# Run tests in specific browser
npx playwright test --project=chromium
# Run specific test file
npx playwright test tests/example.spec.js
# Run tests matching pattern
npx playwright test --grep "login"
# Run tests in debug mode
npx playwright test --debug
# Run tests with UI mode
npx playwright test --ui
Test Hooks¶
import { test, expect } from '@playwright/test';
test.describe('User Authentication', () => {
test.beforeEach(async ({ page }) => {
// Go to the starting url before each test
await page.goto('https://example.com/login');
});
test.afterEach(async ({ page }) => {
// Clean up after each test
await page.evaluate(() => localStorage.clear());
});
test('should login successfully', async ({ page }) => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#login-button');
await expect(page).toHaveURL(/dashboard/);
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('#username', 'invalid');
await page.fill('#password', 'wrong');
await page.click('#login-button');
await expect(page.locator('.error-message')).toBeVisible();
});
});
Browser Management¶
Browser Contexts¶
import { test, expect, chromium } from '@playwright/test';
test('multiple contexts', async () => {
// Launch browser
const browser = await chromium.launch();
// Create contexts
const context1 = await browser.newContext();
const context2 = await browser.newContext();
// Create pages in different contexts
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// Navigate pages
await page1.goto('https://example.com');
await page2.goto('https://playwright.dev');
// Contexts are isolated
await page1.evaluate(() => localStorage.setItem('user', 'context1'));
await page2.evaluate(() => localStorage.setItem('user', 'context2'));
// Verify isolation
const storage1 = await page1.evaluate(() => localStorage.getItem('user'));
const storage2 = await page2.evaluate(() => localStorage.getItem('user'));
expect(storage1).toBe('context1');
expect(storage2).toBe('context2');
// Cleanup
await browser.close();
});
Browser Launch Options¶
import { chromium, firefox, webkit } from '@playwright/test';
test('browser launch options', async () => {
// Chromium with custom options
const browser = await chromium.launch({
headless: false,
slowMo: 1000,
devtools: true,
args: [
'--start-maximized',
'--disable-web-security',
'--disable-features=VizDisplayCompositor'
]
});
// Context with custom options
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Custom User Agent',
locale: 'en-US',
timezoneId: 'America/New_York',
permissions: ['geolocation'],
geolocation: { latitude: 40.7128, longitude: -74.0060 },
colorScheme: 'dark',
reducedMotion: 'reduce'
});
const page = await context.newPage();
await page.goto('https://example.com');
await browser.close();
});
Multiple Browsers¶
import { test, chromium, firefox, webkit } from '@playwright/test';
test('cross-browser testing', async () => {
const browsers = [
{ name: 'Chromium', browser: chromium },
{ name: 'Firefox', browser: firefox },
{ name: 'WebKit', browser: webkit }
];
for (const { name, browser } of browsers) {
console.log(`Testing with ${name}`);
const browserInstance = await browser.launch();
const context = await browserInstance.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
await browserInstance.close();
}
});
Page Navigation¶
Basic Navigation¶
test('navigation', async ({ page }) => {
// Navigate to URL
await page.goto('https://example.com');
// Navigate with options
await page.goto('https://example.com', {
waitUntil: 'networkidle',
timeout: 30000
});
// Go back
await page.goBack();
// Go forward
await page.goForward();
// Reload page
await page.reload();
// Navigate and wait for specific event
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/next-page"]')
]);
});
Wait Strategies¶
test('wait strategies', async ({ page }) => {
await page.goto('https://example.com');
// Wait for load event
await page.goto('https://example.com', { waitUntil: 'load' });
// Wait for DOM content loaded
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Wait for network idle
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Wait for specific element
await page.waitForSelector('#content');
// Wait for element to be visible
await page.waitForSelector('#modal', { state: 'visible' });
// Wait for element to be hidden
await page.waitForSelector('#loading', { state: 'hidden' });
// Wait for function to return true
await page.waitForFunction(() => window.jQuery !== undefined);
// Wait for response
await page.waitForResponse('**/api/data');
// Wait for request
await page.waitForRequest('**/api/submit');
});
URL and Title Assertions¶
test('URL and title checks', async ({ page }) => {
await page.goto('https://example.com');
// Check current URL
expect(page.url()).toBe('https://example.com/');
// Check URL contains
expect(page.url()).toContain('example.com');
// Check URL matches pattern
await expect(page).toHaveURL(/example\.com/);
// Check title
await expect(page).toHaveTitle('Example Domain');
// Check title contains
await expect(page).toHaveTitle(/Example/);
// Get title programmatically
const title = await page.title();
expect(title).toBe('Example Domain');
});
Element Interaction¶
Clicking Elements¶
test('clicking elements', async ({ page }) => {
await page.goto('https://example.com');
// Click by selector
await page.click('#submit-button');
// Click with options
await page.click('#button', {
button: 'right',
clickCount: 2,
delay: 1000,
force: true,
modifiers: ['Shift']
});
// Click at position
await page.click('#element', { position: { x: 10, y: 10 } });
// Double click
await page.dblclick('#element');
// Right click
await page.click('#element', { button: 'right' });
// Click and wait for navigation
await Promise.all([
page.waitForNavigation(),
page.click('#link')
]);
});
Form Interactions¶
test('form interactions', async ({ page }) => {
await page.goto('https://example.com/form');
// Fill input
await page.fill('#username', 'testuser');
// Type with delay
await page.type('#password', 'password123', { delay: 100 });
// Clear input
await page.fill('#field', '');
// Select option
await page.selectOption('#country', 'US');
// Select multiple options
await page.selectOption('#languages', ['en', 'es', 'fr']);
// Check checkbox
await page.check('#agree-terms');
// Uncheck checkbox
await page.uncheck('#newsletter');
// Check radio button
await page.check('#gender-male');
// Upload file
await page.setInputFiles('#file-upload', 'path/to/file.pdf');
// Upload multiple files
await page.setInputFiles('#files', ['file1.pdf', 'file2.jpg']);
});
Keyboard and Mouse Actions¶
test('keyboard and mouse actions', async ({ page }) => {
await page.goto('https://example.com');
// Keyboard actions
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');
await page.keyboard.press('Meta+C'); // Cmd+C on Mac
// Type text
await page.keyboard.type('Hello World');
// Key combinations
await page.keyboard.down('Shift');
await page.keyboard.press('ArrowLeft');
await page.keyboard.up('Shift');
// Mouse actions
await page.mouse.move(100, 100);
await page.mouse.click(100, 100);
await page.mouse.dblclick(100, 100);
// Drag and drop
await page.dragAndDrop('#source', '#target');
// Hover
await page.hover('#menu-item');
// Focus element
await page.focus('#input-field');
// Scroll
await page.mouse.wheel(0, 100);
});
Selectors¶
CSS Selectors¶
test('CSS selectors', async ({ page }) => {
await page.goto('https://example.com');
// Basic selectors
await page.click('#submit'); // ID
await page.click('.button'); // Class
await page.click('button'); // Tag
await page.click('input[type="text"]'); // Attribute
// Combinators
await page.click('div > button'); // Child
await page.click('div button'); // Descendant
await page.click('div + button'); // Adjacent sibling
await page.click('div ~ button'); // General sibling
// Pseudo-selectors
await page.click('button:first-child');
await page.click('button:last-child');
await page.click('button:nth-child(2)');
await page.click('input:checked');
await page.click('button:not(.disabled)');
});
Text Selectors¶
test('text selectors', async ({ page }) => {
await page.goto('https://example.com');
// Exact text match
await page.click('text=Submit');
// Partial text match
await page.click('text=Sub');
// Case insensitive
await page.click('text=submit >> i');
// Regular expression
await page.click('text=/submit/i');
// Text in specific element
await page.click('button:has-text("Submit")');
// Text with quotes
await page.click('text="Click here"');
});
Role-based Selectors¶
test('role-based selectors', async ({ page }) => {
await page.goto('https://example.com');
// Button role
await page.click('role=button[name="Submit"]');
// Link role
await page.click('role=link[name="Home"]');
// Textbox role
await page.fill('role=textbox[name="Username"]', 'user');
// Checkbox role
await page.check('role=checkbox[name="Agree"]');
// Common roles
await page.click('role=button');
await page.click('role=link');
await page.fill('role=textbox', 'text');
await page.selectOption('role=combobox', 'option');
await page.check('role=checkbox');
await page.check('role=radio');
});
Playwright Locators¶
test('playwright locators', async ({ page }) => {
await page.goto('https://example.com');
// Get by role
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Home' }).click();
await page.getByRole('textbox', { name: 'Username' }).fill('user');
// Get by text
await page.getByText('Submit').click();
await page.getByText(/submit/i).click();
// Get by label
await page.getByLabel('Username').fill('user');
await page.getByLabel('Password').fill('pass');
// Get by placeholder
await page.getByPlaceholder('Enter username').fill('user');
// Get by alt text
await page.getByAltText('Profile picture').click();
// Get by title
await page.getByTitle('Close dialog').click();
// Get by test id
await page.getByTestId('submit-button').click();
// Chaining locators
const form = page.locator('#login-form');
await form.getByLabel('Username').fill('user');
await form.getByLabel('Password').fill('pass');
await form.getByRole('button', { name: 'Login' }).click();
});
Advanced Selectors¶
test('advanced selectors', async ({ page }) => {
await page.goto('https://example.com');
// XPath
await page.click('xpath=//button[@id="submit"]');
// CSS with text
await page.click('css=button >> text=Submit');
// Multiple selectors (OR)
await page.click('#submit, .submit-button, button[type="submit"]');
// Selector with visibility
await page.click('button:visible');
// Selector with state
await page.click('button:enabled');
// Has selector
await page.click('article:has(h2:text("Title"))');
// Near selector
await page.click('button:near(:text("Submit"))');
// Nth match
await page.click('button >> nth=1');
// Filter by text
await page.locator('button').filter({ hasText: 'Submit' }).click();
// Filter by other locator
await page.locator('article').filter({ has: page.locator('h2') }).first().click();
});
Waiting Strategies¶
Element Waiting¶
test('element waiting', async ({ page }) => {
await page.goto('https://example.com');
// Wait for element to be attached to DOM
await page.waitForSelector('#element');
// Wait for element to be visible
await page.waitForSelector('#element', { state: 'visible' });
// Wait for element to be hidden
await page.waitForSelector('#element', { state: 'hidden' });
// Wait for element to be detached from DOM
await page.waitForSelector('#element', { state: 'detached' });
// Wait with timeout
await page.waitForSelector('#element', { timeout: 5000 });
// Wait for multiple elements
await Promise.all([
page.waitForSelector('#element1'),
page.waitForSelector('#element2'),
page.waitForSelector('#element3')
]);
});
Auto-waiting¶
test('auto-waiting', async ({ page }) => {
await page.goto('https://example.com');
// Playwright automatically waits for elements to be:
// - Attached to DOM
// - Visible
// - Stable (not animating)
// - Enabled
// - Editable (for input actions)
// These actions auto-wait
await page.click('#button'); // Waits for button to be clickable
await page.fill('#input', 'text'); // Waits for input to be editable
await page.selectOption('#select', 'option'); // Waits for select to be enabled
await page.check('#checkbox'); // Waits for checkbox to be checkable
// Assertions also auto-wait
await expect(page.locator('#element')).toBeVisible();
await expect(page.locator('#input')).toHaveValue('expected');
await expect(page.locator('#list li')).toHaveCount(5);
});
Custom Waiting¶
test('custom waiting', async ({ page }) => {
await page.goto('https://example.com');
// Wait for function to return truthy value
await page.waitForFunction(() => {
return document.querySelector('#dynamic-content') !== null;
});
// Wait for function with arguments
await page.waitForFunction(
(selector) => document.querySelector(selector) !== null,
'#dynamic-element'
);
// Wait for JavaScript condition
await page.waitForFunction(() => window.dataLoaded === true);
// Wait for network idle
await page.waitForLoadState('networkidle');
// Wait for DOM content loaded
await page.waitForLoadState('domcontentloaded');
// Wait for load event
await page.waitForLoadState('load');
// Wait for timeout
await page.waitForTimeout(1000);
// Wait for event
await page.waitForEvent('dialog');
await page.waitForEvent('download');
await page.waitForEvent('popup');
});
Form Handling¶
Input Fields¶
test('input field handling', async ({ page }) => {
await page.goto('https://example.com/form');
// Text input
await page.fill('#username', 'testuser');
await page.fill('#email', 'test@example.com');
// Password input
await page.fill('#password', 'secretpassword');
// Number input
await page.fill('#age', '25');
// Date input
await page.fill('#birthdate', '2000-01-01');
// Textarea
await page.fill('#comments', 'This is a long comment text');
// Clear field
await page.fill('#field', '');
// Type with delay (simulates human typing)
await page.type('#search', 'search term', { delay: 100 });
// Press keys
await page.press('#input', 'Control+A');
await page.press('#input', 'Backspace');
});
Select Dropdowns¶
test('select dropdown handling', async ({ page }) => {
await page.goto('https://example.com/form');
// Select by value
await page.selectOption('#country', 'US');
// Select by label
await page.selectOption('#country', { label: 'United States' });
// Select by index
await page.selectOption('#country', { index: 1 });
// Select multiple options
await page.selectOption('#languages', ['en', 'es', 'fr']);
// Select all options
const options = await page.locator('#languages option').allTextContents();
await page.selectOption('#languages', options);
// Get selected value
const selectedValue = await page.inputValue('#country');
expect(selectedValue).toBe('US');
// Get all selected values
const selectedValues = await page.evaluate(() => {
const select = document.querySelector('#languages');
return Array.from(select.selectedOptions).map(option => option.value);
});
});
Checkboxes and Radio Buttons¶
test('checkbox and radio handling', async ({ page }) => {
await page.goto('https://example.com/form');
// Check checkbox
await page.check('#agree-terms');
await page.check('#newsletter');
// Uncheck checkbox
await page.uncheck('#newsletter');
// Toggle checkbox
const isChecked = await page.isChecked('#newsletter');
if (!isChecked) {
await page.check('#newsletter');
}
// Radio buttons
await page.check('#gender-male');
await page.check('#age-25-34');
// Verify checkbox state
expect(await page.isChecked('#agree-terms')).toBe(true);
expect(await page.isChecked('#newsletter')).toBe(false);
// Verify radio button state
expect(await page.isChecked('#gender-male')).toBe(true);
expect(await page.isChecked('#gender-female')).toBe(false);
});
File Uploads¶
test('file upload handling', async ({ page }) => {
await page.goto('https://example.com/upload');
// Single file upload
await page.setInputFiles('#file-upload', 'tests/fixtures/document.pdf');
// Multiple file upload
await page.setInputFiles('#multiple-files', [
'tests/fixtures/image1.jpg',
'tests/fixtures/image2.png',
'tests/fixtures/document.pdf'
]);
// Upload from buffer
await page.setInputFiles('#file-upload', {
name: 'test.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Hello World')
});
// Remove files
await page.setInputFiles('#file-upload', []);
// Verify file upload
const fileName = await page.inputValue('#file-upload');
expect(fileName).toContain('document.pdf');
// Handle file chooser dialog
const [fileChooser] = await Promise.all([
page.waitForEvent('filechooser'),
page.click('#upload-button')
]);
await fileChooser.setFiles('tests/fixtures/document.pdf');
});
Form Submission¶
test('form submission', async ({ page }) => {
await page.goto('https://example.com/form');
// Fill form
await page.fill('#username', 'testuser');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password123');
await page.check('#agree-terms');
// Submit form by clicking submit button
await Promise.all([
page.waitForNavigation(),
page.click('#submit-button')
]);
// Submit form by pressing Enter
await Promise.all([
page.waitForNavigation(),
page.press('#username', 'Enter')
]);
// Submit form programmatically
await page.evaluate(() => {
document.querySelector('#form').submit();
});
// Verify form submission
await expect(page).toHaveURL(/success/);
await expect(page.locator('.success-message')).toBeVisible();
});
Screenshots & Videos¶
Screenshots¶
test('screenshots', async ({ page }) => {
await page.goto('https://example.com');
// Full page screenshot
await page.screenshot({ path: 'screenshots/fullpage.png' });
// Screenshot with options
await page.screenshot({
path: 'screenshots/page.png',
fullPage: true,
clip: { x: 0, y: 0, width: 800, height: 600 },
quality: 80,
type: 'jpeg'
});
// Element screenshot
await page.locator('#header').screenshot({ path: 'screenshots/header.png' });
// Screenshot to buffer
const buffer = await page.screenshot();
// Screenshot with mask
await page.screenshot({
path: 'screenshots/masked.png',
mask: [page.locator('.sensitive-data')]
});
// Mobile screenshot
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: 'screenshots/mobile.png' });
});
Video Recording¶
// playwright.config.js
export default defineConfig({
use: {
// Record video for all tests
video: 'on',
// Record video only on failure
video: 'retain-on-failure',
// Record video on first retry
video: 'on-first-retry',
},
});
test('video recording', async ({ page }) => {
// Video is automatically recorded based on config
await page.goto('https://example.com');
await page.click('#button');
await page.fill('#input', 'test');
// Video will be saved to test-results folder
});
Visual Comparisons¶
test('visual comparisons', async ({ page }) => {
await page.goto('https://example.com');
// Compare full page
await expect(page).toHaveScreenshot('homepage.png');
// Compare element
await expect(page.locator('#header')).toHaveScreenshot('header.png');
// Compare with threshold
await expect(page).toHaveScreenshot('page.png', { threshold: 0.2 });
// Compare with mask
await expect(page).toHaveScreenshot('page.png', {
mask: [page.locator('.dynamic-content')]
});
// Update screenshots
// npx playwright test --update-snapshots
});
Network Interception¶
Request Interception¶
test('request interception', async ({ page }) => {
// Intercept all requests
await page.route('**/*', route => {
console.log('Request:', route.request().url());
route.continue();
});
// Intercept specific requests
await page.route('**/api/users', route => {
// Block request
route.abort();
// Modify request
route.continue({
headers: {
...route.request().headers(),
'Authorization': 'Bearer token'
}
});
// Mock response
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ users: [] })
});
});
await page.goto('https://example.com');
});
Response Mocking¶
test('response mocking', async ({ page }) => {
// Mock API response
await page.route('**/api/data', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
data: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
})
});
});
// Mock with file
await page.route('**/api/users', route => {
route.fulfill({
path: 'tests/fixtures/users.json'
});
});
// Mock error response
await page.route('**/api/error', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.goto('https://example.com');
});
Network Monitoring¶
test('network monitoring', async ({ page }) => {
const requests = [];
const responses = [];
// Monitor requests
page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
headers: request.headers(),
postData: request.postData()
});
});
// Monitor responses
page.on('response', response => {
responses.push({
url: response.url(),
status: response.status(),
headers: response.headers()
});
});
await page.goto('https://example.com');
// Wait for specific request
const [request] = await Promise.all([
page.waitForRequest('**/api/data'),
page.click('#load-data')
]);
// Wait for specific response
const [response] = await Promise.all([
page.waitForResponse('**/api/submit'),
page.click('#submit')
]);
expect(response.status()).toBe(200);
// Analyze network activity
const apiRequests = requests.filter(req => req.url.includes('/api/'));
expect(apiRequests).toHaveLength(3);
});
Authentication¶
Basic Authentication¶
test('basic authentication', async ({ page }) => {
// Set basic auth
await page.setExtraHTTPHeaders({
'Authorization': 'Basic ' + Buffer.from('username:password').toString('base64')
});
await page.goto('https://example.com/protected');
// Or use context-level auth
const context = await browser.newContext({
httpCredentials: {
username: 'testuser',
password: 'password123'
}
});
const page2 = await context.newPage();
await page2.goto('https://example.com/protected');
});
Session Management¶
test('session management', async ({ page }) => {
// Login
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#login');
// Save session
const cookies = await page.context().cookies();
const localStorage = await page.evaluate(() => {
return JSON.stringify(window.localStorage);
});
// Use session in another test
await page.context().addCookies(cookies);
await page.evaluate(storage => {
const data = JSON.parse(storage);
for (const [key, value] of Object.entries(data)) {
window.localStorage.setItem(key, value);
}
}, localStorage);
await page.goto('https://example.com/dashboard');
await expect(page.locator('.user-info')).toBeVisible();
});
OAuth and Token Authentication¶
test('token authentication', async ({ page }) => {
// Set authorization header
await page.setExtraHTTPHeaders({
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
});
// Or intercept requests to add token
await page.route('**/api/**', route => {
route.continue({
headers: {
...route.request().headers(),
'Authorization': 'Bearer ' + process.env.API_TOKEN
}
});
});
await page.goto('https://example.com/app');
});
Multi-factor Authentication¶
test('multi-factor authentication', async ({ page }) => {
// Step 1: Username and password
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#login');
// Step 2: Wait for MFA prompt
await expect(page.locator('#mfa-code')).toBeVisible();
// Step 3: Enter MFA code (you might need to generate this)
const mfaCode = '123456'; // In real tests, this might come from a TOTP generator
await page.fill('#mfa-code', mfaCode);
await page.click('#verify-mfa');
// Step 4: Verify successful login
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator('.welcome-message')).toBeVisible();
});
Mobile Testing¶
Device Emulation¶
import { test, devices } from '@playwright/test';
test('mobile testing', async ({ browser }) => {
// iPhone 12
const iPhone = devices['iPhone 12'];
const context = await browser.newContext({
...iPhone
});
const page = await context.newPage();
await page.goto('https://example.com');
// Test mobile-specific features
await expect(page.locator('.mobile-menu')).toBeVisible();
await expect(page.locator('.desktop-menu')).toBeHidden();
await context.close();
});
// Using project configuration
test.describe('Mobile Tests', () => {
test.use({ ...devices['iPhone 12'] });
test('mobile navigation', async ({ page }) => {
await page.goto('https://example.com');
// Test hamburger menu
await page.click('.hamburger-menu');
await expect(page.locator('.mobile-nav')).toBeVisible();
});
});
Touch Interactions¶
test('touch interactions', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('https://example.com');
// Tap (equivalent to click on mobile)
await page.tap('#button');
// Long press
await page.touchscreen.tap(100, 100, { duration: 1000 });
// Swipe
await page.touchscreen.tap(100, 300);
await page.mouse.move(300, 300);
await page.mouse.up();
// Pinch to zoom
await page.touchscreen.tap(200, 200);
await page.touchscreen.tap(250, 250);
// Scroll
await page.mouse.wheel(0, 100);
// Test responsive design
const isMobileMenuVisible = await page.isVisible('.mobile-menu');
expect(isMobileMenuVisible).toBe(true);
});
Geolocation Testing¶
test('geolocation testing', async ({ context, page }) => {
// Grant geolocation permission
await context.grantPermissions(['geolocation']);
// Set geolocation
await context.setGeolocation({ latitude: 40.7128, longitude: -74.0060 });
await page.goto('https://example.com/map');
// Test location-based features
await page.click('#get-location');
await expect(page.locator('#location-display')).toContainText('New York');
// Change location
await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
await page.reload();
await page.click('#get-location');
await expect(page.locator('#location-display')).toContainText('Los Angeles');
});
Testing Framework¶
Test Organization¶
import { test, expect } from '@playwright/test';
// Test suite
test.describe('User Authentication', () => {
test.beforeAll(async () => {
// Setup before all tests in this describe block
console.log('Setting up test data');
});
test.beforeEach(async ({ page }) => {
// Setup before each test
await page.goto('https://example.com/login');
});
test.afterEach(async ({ page }) => {
// Cleanup after each test
await page.evaluate(() => localStorage.clear());
});
test.afterAll(async () => {
// Cleanup after all tests
console.log('Cleaning up test data');
});
test('should login with valid credentials', async ({ page }) => {
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#login');
await expect(page).toHaveURL(/dashboard/);
});
test('should show error for invalid credentials', async ({ page }) => {
await page.fill('#username', 'invalid');
await page.fill('#password', 'wrong');
await page.click('#login');
await expect(page.locator('.error')).toBeVisible();
});
});
Parameterized Tests¶
const testData = [
{ username: 'user1', password: 'pass1', expected: 'dashboard' },
{ username: 'user2', password: 'pass2', expected: 'admin' },
{ username: 'user3', password: 'pass3', expected: 'profile' }
];
testData.forEach(({ username, password, expected }) => {
test(`login with ${username}`, async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#username', username);
await page.fill('#password', password);
await page.click('#login');
await expect(page).toHaveURL(new RegExp(expected));
});
});
// Or using test.describe.parallel for parallel execution
test.describe.parallel('Parallel Tests', () => {
['chrome', 'firefox', 'safari'].forEach(browser => {
test(`test on ${browser}`, async ({ page }) => {
// Test logic here
});
});
});
Custom Fixtures¶
// fixtures.js
import { test as base } from '@playwright/test';
// Custom fixture for authenticated user
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Login before each test
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password123');
await page.click('#login');
await page.waitForURL('**/dashboard');
await use(page);
// Logout after each test
await page.click('#logout');
}
});
// Use custom fixture
test('dashboard test', async ({ authenticatedPage }) => {
await expect(authenticatedPage.locator('.welcome')).toBeVisible();
});
export { test };
Test Annotations¶
test('slow test', async ({ page }) => {
test.slow(); // Mark test as slow (3x timeout)
await page.goto('https://example.com');
// Long-running test logic
});
test('flaky test', async ({ page }) => {
test.fixme(); // Mark test as broken
await page.goto('https://example.com');
// Flaky test logic
});
test('skip on mobile', async ({ page, isMobile }) => {
test.skip(isMobile, 'Not supported on mobile');
await page.goto('https://example.com');
// Desktop-only test logic
});
test('conditional test', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'Not supported in Safari');
await page.goto('https://example.com');
// Browser-specific test logic
});
Debugging¶
Debug Mode¶
# Run tests in debug mode
npx playwright test --debug
# Debug specific test
npx playwright test tests/example.spec.js --debug
# Debug with headed browser
npx playwright test --headed --debug
# Debug with slow motion
npx playwright test --headed --slowMo=1000
Debugging in Code¶
test('debugging example', async ({ page }) => {
await page.goto('https://example.com');
// Pause execution
await page.pause();
// Add console logs
console.log('Current URL:', page.url());
// Take screenshot for debugging
await page.screenshot({ path: 'debug-screenshot.png' });
// Evaluate JavaScript in browser context
const title = await page.evaluate(() => document.title);
console.log('Page title:', title);
// Get element information
const element = page.locator('#button');
console.log('Element visible:', await element.isVisible());
console.log('Element text:', await element.textContent());
await page.click('#button');
});
Trace Viewer¶
// playwright.config.js
export default defineConfig({
use: {
// Collect trace when retrying the failed test
trace: 'on-first-retry',
// Or collect trace for all tests
trace: 'on',
},
});
// View traces
// npx playwright show-trace test-results/example-chromium/trace.zip
Browser Developer Tools¶
test('debug with devtools', async ({ page }) => {
// Launch browser with devtools
await page.goto('https://example.com');
// Open devtools programmatically
await page.evaluate(() => {
debugger; // This will trigger debugger if devtools is open
});
// Inspect element
await page.locator('#element').highlight();
// Log to browser console
await page.evaluate(() => {
console.log('Debug message from browser');
});
});
Error Handling¶
test('error handling', async ({ page }) => {
try {
await page.goto('https://example.com');
await page.click('#non-existent-element', { timeout: 5000 });
} catch (error) {
console.log('Error occurred:', error.message);
// Take screenshot on error
await page.screenshot({ path: 'error-screenshot.png' });
// Log page content for debugging
const content = await page.content();
console.log('Page content:', content);
throw error; // Re-throw to fail the test
}
});
Performance Testing¶
Performance Metrics¶
test('performance metrics', async ({ page }) => {
await page.goto('https://example.com');
// Get performance metrics
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0];
return {
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime
};
});
console.log('Performance metrics:', metrics);
// Assert performance thresholds
expect(metrics.domContentLoaded).toBeLessThan(2000);
expect(metrics.loadComplete).toBeLessThan(5000);
});
Lighthouse Integration¶
import { playAudit } from 'playwright-lighthouse';
test('lighthouse audit', async ({ page }) => {
await page.goto('https://example.com');
await playAudit({
page,
thresholds: {
performance: 90,
accessibility: 90,
'best-practices': 90,
seo: 90,
},
port: 9222,
});
});
Network Performance¶
test('network performance', async ({ page }) => {
const responses = [];
page.on('response', response => {
responses.push({
url: response.url(),
status: response.status(),
size: response.headers()['content-length'],
timing: response.timing()
});
});
await page.goto('https://example.com');
// Analyze network performance
const slowRequests = responses.filter(r => r.timing.responseEnd > 1000);
const largeRequests = responses.filter(r => parseInt(r.size) > 1000000);
expect(slowRequests.length).toBeLessThan(3);
expect(largeRequests.length).toBeLessThan(2);
console.log('Total requests:', responses.length);
console.log('Slow requests:', slowRequests.length);
console.log('Large requests:', largeRequests.length);
});
API Testing¶
REST API Testing¶
import { test, expect, request } from '@playwright/test';
test('API testing', async ({ request }) => {
// GET request
const response = await request.get('https://api.example.com/users');
expect(response.status()).toBe(200);
const users = await response.json();
expect(users).toHaveLength(10);
// POST request
const newUser = {
name: 'John Doe',
email: 'john@example.com'
};
const createResponse = await request.post('https://api.example.com/users', {
data: newUser
});
expect(createResponse.status()).toBe(201);
const createdUser = await createResponse.json();
expect(createdUser.name).toBe(newUser.name);
// PUT request
const updateResponse = await request.put(`https://api.example.com/users/${createdUser.id}`, {
data: { name: 'Jane Doe' }
});
expect(updateResponse.status()).toBe(200);
// DELETE request
const deleteResponse = await request.delete(`https://api.example.com/users/${createdUser.id}`);
expect(deleteResponse.status()).toBe(204);
});
API Authentication¶
test('API with authentication', async ({ request }) => {
// Login to get token
const loginResponse = await request.post('https://api.example.com/auth/login', {
data: {
username: 'testuser',
password: 'password123'
}
});
const { token } = await loginResponse.json();
// Use token for authenticated requests
const response = await request.get('https://api.example.com/protected', {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.status()).toBe(200);
});
GraphQL Testing¶
test('GraphQL API testing', async ({ request }) => {
const query = `
query GetUsers {
users {
id
name
email
}
}
`;
const response = await request.post('https://api.example.com/graphql', {
data: { query }
});
expect(response.status()).toBe(200);
const result = await response.json();
expect(result.data.users).toBeDefined();
expect(result.data.users.length).toBeGreaterThan(0);
});
Visual Testing¶
Visual Regression Testing¶
test('visual regression', async ({ page }) => {
await page.goto('https://example.com');
// Take baseline screenshot
await expect(page).toHaveScreenshot('homepage.png');
// Test different states
await page.hover('#menu');
await expect(page).toHaveScreenshot('homepage-menu-hover.png');
await page.click('#toggle-theme');
await expect(page).toHaveScreenshot('homepage-dark-theme.png');
// Test responsive design
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page).toHaveScreenshot('homepage-tablet.png');
await page.setViewportSize({ width: 375, height: 667 });
await expect(page).toHaveScreenshot('homepage-mobile.png');
});
Component Visual Testing¶
test('component visual testing', async ({ page }) => {
await page.goto('https://example.com/components');
// Test individual components
await expect(page.locator('.button')).toHaveScreenshot('button.png');
await expect(page.locator('.card')).toHaveScreenshot('card.png');
await expect(page.locator('.modal')).toHaveScreenshot('modal.png');
// Test component states
await page.hover('.button');
await expect(page.locator('.button')).toHaveScreenshot('button-hover.png');
await page.click('.button');
await expect(page.locator('.button')).toHaveScreenshot('button-active.png');
});
Cross-browser Visual Testing¶
['chromium', 'firefox', 'webkit'].forEach(browserName => {
test(`visual test on ${browserName}`, async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot(`homepage-${browserName}.png`);
});
});
CI/CD Integration¶
GitHub Actions¶
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Docker Integration¶
# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
Jenkins Pipeline¶
pipeline {
agent any
stages {
stage('Install Dependencies') {
steps {
sh 'npm ci'
sh 'npx playwright install --with-deps'
}
}
stage('Run Tests') {
steps {
sh 'npx playwright test'
}
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: 'playwright-report',
reportFiles: 'index.html',
reportName: 'Playwright Report'
])
}
}
}
}
}
Advanced Features¶
Custom Matchers¶
// custom-matchers.js
import { expect } from '@playwright/test';
expect.extend({
async toHaveLoadTime(page, maxTime) {
const startTime = Date.now();
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
const pass = loadTime <= maxTime;
return {
message: () => `Expected load time to be <= ${maxTime}ms, but was ${loadTime}ms`,
pass
};
}
});
// Usage
test('custom matcher', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveLoadTime(3000);
});
Page Object Model¶
// pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('#login');
this.errorMessage = page.locator('.error');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage() {
return await this.errorMessage.textContent();
}
}
// Usage in test
import { LoginPage } from './pages/LoginPage';
test('login with page object', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await expect(page).toHaveURL(/dashboard/);
});
Global Setup and Teardown¶
// global-setup.js
export default async function globalSetup() {
// Start test server
console.log('Starting test server...');
// Create test data
console.log('Creating test data...');
// Any other global setup
}
// global-teardown.js
export default async function globalTeardown() {
// Stop test server
console.log('Stopping test server...');
// Clean up test data
console.log('Cleaning up test data...');
}
// playwright.config.js
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
Best Practices¶
Test Organization¶
// Organize tests by feature/page
test.describe('User Management', () => {
test.describe('User Registration', () => {
test('should register new user', async ({ page }) => {
// Test implementation
});
test('should validate email format', async ({ page }) => {
// Test implementation
});
});
test.describe('User Login', () => {
test('should login with valid credentials', async ({ page }) => {
// Test implementation
});
});
});
Reliable Selectors¶
// Good: Use data-testid attributes
await page.click('[data-testid="submit-button"]');
// Good: Use role-based selectors
await page.click('role=button[name="Submit"]');
// Good: Use text content
await page.click('text=Submit');
// Avoid: CSS selectors that depend on styling
await page.click('.btn.btn-primary.submit-btn');
// Avoid: XPath with position
await page.click('//div[3]/button[2]');
Error Handling¶
test('robust error handling', async ({ page }) => {
try {
await page.goto('https://example.com');
// Use soft assertions for non-critical checks
await expect.soft(page.locator('.optional-element')).toBeVisible();
// Continue with critical test steps
await page.click('#important-button');
await expect(page.locator('#result')).toBeVisible();
} catch (error) {
// Take screenshot on failure
await page.screenshot({ path: 'failure-screenshot.png' });
// Log additional context
console.log('Current URL:', page.url());
console.log('Page title:', await page.title());
throw error;
}
});
Performance Best Practices¶
- Use auto-waiting: Playwright automatically waits for elements
- Avoid unnecessary waits: Don't use
page.waitForTimeout()
unless absolutely necessary - Reuse browser contexts: Share contexts between tests when possible
- Parallelize tests: Use
fullyParallel: true
in configuration - Optimize selectors: Use efficient selectors like data-testid
Maintenance Best Practices¶
- Keep tests independent: Each test should be able to run in isolation
- Use Page Object Model: Encapsulate page interactions in reusable classes
- Implement proper cleanup: Clear state between tests
- Use meaningful test names: Describe what the test is verifying
- Regular updates: Keep Playwright and browsers updated
Summary¶
Playwright is a powerful, modern web testing framework that provides:
- Cross-browser Support: Test on Chromium, Firefox, and WebKit
- Auto-waiting: Intelligent waiting for elements to be ready
- Powerful Selectors: Multiple selector strategies including role-based
- Network Interception: Mock and monitor network requests
- Mobile Testing: Device emulation and touch interactions
- Visual Testing: Screenshot comparison and visual regression testing
- Debugging Tools: Trace viewer, debug mode, and developer tools integration
- CI/CD Ready: Easy integration with popular CI/CD platforms
- API Testing: Built-in support for REST and GraphQL API testing
- Performance Testing: Built-in performance metrics and Lighthouse integration
Playwright's modern architecture and comprehensive feature set make it an excellent choice for end-to-end testing, browser automation, and web scraping tasks.