Playwright is Microsoft's open-source Node.js library for reliable, cross-browser end-to-end testing and browser automation. A single API drives Chromium, Firefox, and WebKit — the engines behind Chrome, Edge, Firefox, and Safari — so tests written once run everywhere. Auto-wait, network interception, mobile emulation, tracing, and code generation come built in.
This guide covers everything you need to install, configure, write tests, automate browsers, and troubleshoot Playwright v1.60.0-next on Node.js 18+. For architecture internals, security hardening, deployment patterns, and full configuration reference, see the Admin Guide.
1. Quick Start
Get a full Playwright project running in under two minutes.
1.1 Create a New Project
npm init playwright@latest
The initializer prompts for TypeScript or JavaScript, a tests folder name, GitHub Actions setup, and whether to install browsers. Accept the defaults for the fastest path.
Expected output:
Initializing NPM project (npm init -y)…
Installing Playwright Test (npm install -D @playwright/test)…
Writing playwright.config.ts
Creating tests/example.spec.ts
Downloading browsers…
✔ Success! Created a Playwright Test project
1.2 Run the Example Test
npx playwright test
Expected output:
Running 3 tests using 2 workers
✓ [chromium] › tests/example.spec.ts:3:1 › has title (2.1s)
✓ [firefox] › tests/example.spec.ts:3:1 › has title (3.4s)
✓ [webkit] › tests/example.spec.ts:3:1 › has title (2.8s)
3 passed (8.2s)
1.3 View the HTML Report
npx playwright show-report
A browser tab opens with an interactive report. Drill into any test for traces, screenshots, and step timings.
Tip
Run
npx playwright test --uito launch UI Mode — a visual test runner with live-reload, time-travel debugging, and a watch mode that re-runs tests on file change. It is the best way to author and debug tests.
1.4 Generate Tests with Codegen
npx playwright codegen https://example.com
Two windows open: a browser for you to interact with, and an inspector that records every action as test code. Copy the generated code into your test file when done.
1.5 Minimal Test File
// tests/homepage.spec.ts
import { test, expect } from '@playwright/test';
test('homepage loads correctly', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
await expect(page.locator('h1')).toBeVisible();
});
Save this as tests/homepage.spec.ts and run npx playwright test tests/homepage.spec.ts.
2. Installation
2.1 Requirements
| Requirement | Minimum |
|---|---|
| Node.js | 18 or higher |
| OS | Windows 10+, macOS 12+, Ubuntu 20.04+ (Debian 11+, other distros vary) |
| Disk | ~500 MB per browser |
2.2 Install via npm
npm init -D @playwright/test
This installs the @playwright/test package. Then install browsers:
npx playwright install
Expected output:
Downloading Chromium 131.0.6778.33 (playwright build v1154)
Downloading Firefox 133.0 (playwright build v1466)
Downloading WebKit 18.2 (playwright build v2104)
2.3 Install Specific Browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
2.4 Install System Dependencies
On Linux, browsers need system libraries. Install them automatically:
npx playwright install-deps
Or for a specific browser:
npx playwright install-deps chromium
Note
On CI systems (Ubuntu), always run
npx playwright install --with-depsto install both browsers and OS-level libraries in one step. This is the recommended CI installation command.
2.5 Install Options Reference
| Option | Description |
|---|---|
--with-deps |
Install OS dependencies alongside browsers |
--dry-run |
Show what would be installed without doing it |
--list |
List installed and available browsers |
--force |
Force re-download even if already installed |
--only-shell |
Install only the shell/launcher, not the full browser |
--no-shell |
Skip shell/launcher installation |
2.6 Uninstall Browsers
npx playwright uninstall # remove browsers for current version
npx playwright uninstall --all # remove ALL Playwright browsers
2.7 Docker Installation
For CI environments, use the official Playwright Docker image:
FROM mcr.microsoft.com/playwright:v1.60.0-noble
WORKDIR /app
COPY . .
RUN npm ci
CMD ["npx", "playwright", "test"]
2.8 Using Playwright Library (Without Test Runner)
If you only need browser automation without the test framework:
npm install playwright
Or for the minimal core without bundled browsers:
npm install playwright-core
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
await browser.close();
3. Configuration
Playwright uses a configuration file — playwright.config.ts (or .js) — at the project root. Every setting has a sensible default; you only override what you need.
3.1 Minimal Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
});
3.2 Full Working Configuration
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : '50%',
reporter: process.env.CI ? 'dot' : 'list',
fullyParallel: true,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
locale: 'en-US',
colorScheme: 'light',
viewport: { width: 1280, height: 720 },
testIdAttribute: 'data-testid',
actionTimeout: 10_000,
navigationTimeout: 30_000,
},
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 13'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
3.3 Configuration Defaults
| Setting | Default |
|---|---|
timeout |
30000 ms |
workers |
50% of CPU cores |
testMatch |
**/*.@(spec|test).?(c|m)[jt]s?(x) |
viewport |
1280 × 720 |
locale |
en-US |
colorScheme |
light |
screenshot |
off |
video |
off |
trace |
off |
testIdAttribute |
data-testid |
reporter |
list (dot on CI) |
3.4 Projects — Multi-Browser and Multi-Config
Projects let you run the same tests under different configurations:
projects: [
{
name: 'setup',
testMatch: /global\.setup\.ts/,
},
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'mobile',
use: { ...devices['Pixel 5'] },
dependencies: ['setup'],
},
],
Use dependencies to run setup projects (like authentication) before test projects.
3.5 Web Server Configuration
Playwright can start your app before tests and shut it down after:
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
Multiple servers:
webServer: [
{ command: 'npm run start:api', port: 4000 },
{ command: 'npm run start:web', port: 3000 },
],
3.6 Environment Variables
| Variable | Purpose |
|---|---|
PLAYWRIGHT_TEST_BASE_URL |
Override baseURL without changing config |
PWDEBUG |
Set to 1 to launch Playwright Inspector for debugging |
PWPAUSE |
Pause at end of test execution |
CI |
Auto-detected; changes default reporter to dot |
PLAYWRIGHT_HTML_TITLE |
Custom title for the HTML report |
PLAYWRIGHT_DASHBOARD |
Enable dashboard features |
Important
When
PWDEBUG=1is set, Playwright launches in headed mode with the Inspector attached, timeouts are disabled, and the test pauses at eachawaitfor manual stepping. This is the primary interactive debugging workflow.
3.7 Reporters
Configure one or more reporters to control test output:
reporter: [
['list'],
['html', { open: 'never' }],
['junit', { outputFile: 'results.xml' }],
],
| Reporter | Output | Best For |
|---|---|---|
list |
Step-by-step in terminal | Local development |
line |
Single updating line | Compact terminal output |
dot |
Dots for pass/fail | CI (default when CI is set) |
html |
Interactive HTML report | Post-run analysis |
json |
JSON to stdout or file | Programmatic processing |
junit |
JUnit XML | CI systems (Jenkins, GitLab) |
blob |
Binary blob | Sharding, cross-machine merge |
github |
GitHub Actions annotations | GitHub CI |
null |
No output | Suppressing output |
3.8 Global Setup and Teardown
// playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
});
// global-setup.ts
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('#username', 'admin');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await page.context().storageState({ path: './auth.json' });
await browser.close();
}
export default globalSetup;
4. Core Usage
4.1 Running Tests
Run All Tests
npx playwright test
Run a Specific File
npx playwright test tests/login.spec.ts
Run Tests Matching a Name
npx playwright test -g "login"
Run on a Specific Browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit
Run in Headed Mode (See the Browser)
npx playwright test --headed
Run a Single Test in Debug Mode
npx playwright test --debug tests/login.spec.ts
Run with Specific Workers
npx playwright test --workers=4
npx playwright test --workers=1 # serial execution
Run Tests with Tags
npx playwright test --grep @smoke
npx playwright test --grep-invert @slow
4.2 Test Runner CLI Reference
| Flag | Description |
|---|---|
--headed |
Run in headed (visible) mode |
--debug |
Launch Inspector, pause on first action |
--ui |
Open UI Mode for interactive development |
-g, --grep <regex> |
Filter tests by title |
--grep-invert <regex> |
Exclude tests by title |
--project <name> |
Run only the named project(s) |
--workers <count> |
Number of parallel workers |
--retries <count> |
Retry failed tests N times |
--reporter <name> |
Override reporter |
--output <dir> |
Test artifacts directory |
--timeout <ms> |
Override test timeout |
--max-failures <N> |
Stop after N failures |
--shard <current/total> |
Shard tests (e.g., 1/3) |
--repeat-each <N> |
Repeat each test N times |
--forbid-only |
Fail if test.only exists (CI guard) |
--update-snapshots |
Update visual comparison baselines |
--pass-with-no-tests |
Don't fail on empty test suite |
--last-failed |
Re-run only previously failed tests |
-c, --config <file> |
Path to config file |
4.3 Writing Tests
Basic Test Structure
import { test, expect } from '@playwright/test';
test.describe('Feature: Shopping Cart', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/shop');
});
test('adds item to cart', async ({ page }) => {
await page.click('[data-testid="add-to-cart"]');
await expect(page.locator('.cart-count')).toHaveText('1');
});
test('removes item from cart', async ({ page }) => {
await page.click('[data-testid="add-to-cart"]');
await page.click('[data-testid="remove-from-cart"]');
await expect(page.locator('.cart-count')).toHaveText('0');
});
});
Test Annotations
test('feature behind flag', async ({ page }) => {
test.skip(process.env.FEATURE_FLAG !== 'true', 'Feature not enabled');
// ...
});
test('known flaky on webkit', async ({ page, browserName }) => {
test.fixme(browserName === 'webkit', 'WebKit rendering issue');
});
test.slow('large data import', async ({ page }) => {
// timeout tripled automatically
});
test.only('debug this test', async ({ page }) => {
// only this test runs (remove before commit)
});
Tagging Tests
test('login @smoke @auth', async ({ page }) => {
// ...
});
test('complex report @slow', async ({ page }) => {
// ...
});
Run tagged tests: npx playwright test --grep @smoke
4.4 Locators
Locators are the preferred way to find elements. They auto-wait and auto-retry.
Recommended Locators (Resilient to DOM Changes)
// By role (best for accessibility)
page.getByRole('button', { name: 'Submit' });
page.getByRole('heading', { name: 'Welcome' });
page.getByRole('link', { name: 'Sign Up' });
page.getByRole('textbox', { name: 'Email' });
page.getByRole('checkbox', { name: 'Remember me' });
// By label text
page.getByLabel('Username');
page.getByLabel('Password');
// By placeholder
page.getByPlaceholder('Search...');
// By visible text
page.getByText('Welcome back');
page.getByText(/welcome/i); // regex
// By alt text (images)
page.getByAltText('Company Logo');
// By title attribute
page.getByTitle('Close dialog');
// By test ID (configurable via testIdAttribute)
page.getByTestId('submit-button');
CSS and XPath (When Needed)
page.locator('css=button.primary');
page.locator('#login-form input[type="email"]');
page.locator('xpath=//button[@aria-label="Close"]');
Locator Filtering and Chaining
// Filter by text
page.getByRole('listitem').filter({ hasText: 'Product' });
// Filter by child locator
page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'Buy' }),
});
// Nth item
page.getByRole('listitem').nth(2);
page.getByRole('listitem').first();
page.getByRole('listitem').last();
4.5 Actions
All actions auto-wait for the element to be actionable (visible, enabled, stable).
// Click
await page.click('button#submit');
await page.dblclick('.card');
await page.click('button', { button: 'right' });
// Fill form fields (clears first)
await page.fill('#email', '[email removed]');
await page.fill('#password', 's3cure!');
// Type character by character (for autocomplete testing)
await page.type('#search', 'playwright', { delay: 100 });
// Press keyboard keys
await page.press('#search', 'Enter');
await page.keyboard.press('Control+A');
// Select dropdown
await page.selectOption('select#country', 'US');
await page.selectOption('select#colors', ['red', 'blue']);
// Checkbox and radio
await page.check('#agree-terms');
await page.uncheck('#newsletter');
// File upload
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', ['file1.jpg', 'file2.jpg']);
// Hover
await page.hover('.tooltip-trigger');
// Drag and drop
await page.dragAndDrop('#source', '#target');
// Focus
await page.focus('#input-field');
4.6 Assertions
Playwright assertions auto-retry until the condition is met or the timeout expires.
import { expect } from '@playwright/test';
// Page assertions
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL(/\/dashboard/);
// Visibility
await expect(page.locator('.alert')).toBeVisible();
await expect(page.locator('.spinner')).toBeHidden();
await expect(page.locator('.modal')).not.toBeVisible();
// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Welc');
await expect(page.locator('.items')).toHaveCount(5);
// Input values
await expect(page.locator('#email')).toHaveValue('[email removed]');
await expect(page.locator('#name')).toBeEmpty();
// Attributes and CSS
await expect(page.locator('button')).toHaveAttribute('disabled', '');
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('.item')).toHaveClass(/selected/);
await expect(page.locator('.box')).toHaveCSS('color', 'rgb(255, 0, 0)');
// Checked state
await expect(page.locator('#checkbox')).toBeChecked();
await expect(page.locator('#checkbox')).not.toBeChecked();
// Attached to DOM
await expect(page.locator('.dynamic')).toBeAttached();
// Screenshot comparison
await expect(page).toHaveScreenshot('homepage.png');
await expect(page.locator('.chart')).toHaveScreenshot('chart.png', {
maxDiffPixels: 100,
});
Tip
Always prefer Playwright's auto-retrying
expectassertions over manualifchecks or rawtextContent()calls. Auto-retrying assertions handle timing naturally and eliminate flakiness from elements that haven't rendered yet.
4.7 Navigation
// Navigate
await page.goto('https://example.com');
await page.goto('/dashboard'); // uses baseURL from config
// Wait for navigation
await page.waitForURL('**/dashboard');
await page.waitForLoadState('networkidle');
// Back and forward
await page.goBack();
await page.goForward();
// Reload
await page.reload();
4.8 Fixtures
Fixtures provide isolated resources for each test. Playwright's built-in fixtures give you a fresh page and context per test.
Built-in Test Fixtures
| Fixture | Scope | Description |
|---|---|---|
page |
Test | Isolated page instance |
context |
Test | Browser context (cookies, storage) |
browser |
Worker | Shared browser instance |
browserName |
Worker | chromium, firefox, or webkit |
playwright |
Worker | Playwright instance |
request |
Test | API request context |
Custom Fixtures
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
adminPage: Page;
userPage: Page;
};
const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: './admin-auth.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: './user-auth.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
test('admin sees dashboard', async ({ adminPage }) => {
await adminPage.goto('/admin');
await expect(adminPage.locator('h1')).toHaveText('Admin Dashboard');
});
Fixture Configuration via Config
Many fixtures are configurable directly in playwright.config.ts:
use: {
acceptDownloads: true,
bypassCSP: false,
colorScheme: 'dark',
geolocation: { latitude: 40.7128, longitude: -74.0060 },
hasTouch: false,
httpCredentials: { username: 'admin', password: 'pass' },
ignoreHTTPSErrors: true,
isMobile: false,
javaScriptEnabled: true,
locale: 'en-US',
offline: false,
permissions: ['geolocation'],
timezoneId: 'America/New_York',
userAgent: 'Custom UA String',
viewport: { width: 1920, height: 1080 },
serviceWorkers: 'block',
baseURL: 'http://localhost:3000',
actionTimeout: 15_000,
navigationTimeout: 30_000,
testIdAttribute: 'data-testid',
},
4.9 Page Object Model
Organize tests with page objects for maintainability:
// pages/login-page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: 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' });
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).toHaveText(message);
}
}
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test('shows error for bad credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email removed]', 'wrong');
await loginPage.expectError('Invalid credentials');
});
5. Common Tasks
5.1 Authentication and Storage State
Reuse authentication across tests by saving and loading browser storage state.
Save Authentication State (Setup Project)
// 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('[email removed]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: authFile });
});
Use Saved State in Config
// playwright.config.ts
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
Note
Add
playwright/.auth/to your.gitignoreto avoid committing session tokens.
5.2 API Testing
Playwright includes a built-in API testing client — no additional packages needed.
import { test, expect } from '@playwright/test';
test('API: create and fetch user', async ({ request }) => {
// POST
const createResponse = await request.post('/api/users', {
data: {
name: 'Jane Doe',
email: '[email removed]',
},
});
expect(createResponse.ok()).toBeTruthy();
const user = await createResponse.json();
expect(user.name).toBe('Jane Doe');
// GET
const getResponse = await request.get(`/api/users/${user.id}`);
expect(getResponse.ok()).toBeTruthy();
const fetched = await getResponse.json();
expect(fetched.email).toBe('[email removed]');
// DELETE
const deleteResponse = await request.delete(`/api/users/${user.id}`);
expect(deleteResponse.status()).toBe(204);
});
5.3 Network Interception and Mocking
Mock API Responses
test('shows mocked data', async ({ page }) => {
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock Product', price: 9.99 },
]),
});
});
await page.goto('/products');
await expect(page.locator('.product-name')).toHaveText('Mock Product');
});
Modify Responses
test('modify API response', async ({ page }) => {
await page.route('**/api/user/profile', async (route) => {
const response = await route.fetch();
const json = await response.json();
json.name = 'Modified Name';
await route.fulfill({ response, body: JSON.stringify(json) });
});
await page.goto('/profile');
});
Block Resources
test('block images for speed', async ({ page }) => {
await page.route('**/*.{png,jpg,jpeg,gif,svg}', (route) => route.abort());
await page.goto('/gallery');
});
HAR Recording and Replay
# Record network traffic
npx playwright open --save-har=traffic.har https://example.com
test('replay from HAR', async ({ page }) => {
await page.routeFromHAR('traffic.har', { update: false });
await page.goto('https://example.com');
});
5.4 Screenshots and Visual Comparison
Capture Screenshots
// Full page
await page.screenshot({ path: 'screenshots/full.png', fullPage: true });
// Specific element
await page.locator('.chart').screenshot({ path: 'screenshots/chart.png' });
// Clip a region
await page.screenshot({
path: 'screenshots/header.png',
clip: { x: 0, y: 0, width: 1280, height: 200 },
});
Visual Regression Testing
test('homepage visual', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
test('component visual', async ({ page }) => {
await page.goto('/components');
await expect(page.locator('.card')).toHaveScreenshot('card.png', {
maxDiffPixelRatio: 0.01,
});
});
Update baselines after intentional changes:
npx playwright test --update-snapshots
CLI Screenshot
npx playwright screenshot https://example.com screenshot.png --full-page
npx playwright screenshot https://example.com hero.png --wait-for-selector="h1"
5.5 Video Recording
Enable video per project in config:
use: {
video: 'retain-on-failure', // or 'on', 'off', 'on-first-retry'
},
Videos are saved to test-results/ and embedded in the HTML report.
5.6 Tracing
Traces capture a complete timeline of actions, DOM snapshots, network requests, and console logs.
Enable in Config
use: {
trace: 'on-first-retry', // or 'on', 'off', 'retain-on-failure'
},
Capture Manually in a Test
test('traced workflow', async ({ page, context }) => {
await context.tracing.start({ screenshots: true, snapshots: true });
await page.goto('/checkout');
await page.fill('#card', '4242424242424242');
await page.click('#pay');
await context.tracing.stop({ path: 'trace.zip' });
});
View a Trace
npx playwright show-trace trace.zip
The Trace Viewer opens in a browser with a timeline, DOM snapshots at each step, network log, and console output. You can step through actions and inspect the page state at any point.
5.7 PDF Generation (Chromium Only)
const page = await browser.newPage();
await page.goto('https://example.com/report');
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
});
CLI shortcut:
npx playwright pdf https://example.com/report report.pdf --paper-format=A4
5.8 File Downloads
test('download report', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.click('a#download-report');
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('report.csv');
await download.saveAs('./downloads/report.csv');
});
Tip
Set
acceptDownloads: truein config or fixture options to allow downloads. It istrueby default with@playwright/testfixtures.
5.9 File Uploads
test('upload avatar', async ({ page }) => {
await page.goto('/settings');
// Single file
await page.setInputFiles('input[type="file"]', 'fixtures/avatar.png');
// Multiple files
await page.setInputFiles('input[type="file"]', [
'fixtures/doc1.pdf',
'fixtures/doc2.pdf',
]);
// Clear selection
await page.setInputFiles('input[type="file"]', []);
});
5.10 Handling Dialogs (alert, confirm, prompt)
test('handles confirm dialog', async ({ page }) => {
page.on('dialog', async (dialog) => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toBe('Delete this item?');
await dialog.accept();
});
await page.click('#delete-button');
});
test('dismisses prompt', async ({ page }) => {
page.on('dialog', (dialog) => dialog.dismiss());
await page.click('#rename-button');
});
5.11 iFrames
test('interact with iframe', async ({ page }) => {
const frame = page.frameLocator('#payment-iframe');
await frame.locator('#card-number').fill('4242424242424242');
await frame.locator('#submit').click();
});
5.12 Multiple Pages and Tabs
test('handle popup', async ({ page }) => {
const popupPromise = page.waitForEvent('popup');
await page.click('a[target="_blank"]');
const popup = await popupPromise;
await popup.waitForLoadState();
await expect(popup).toHaveTitle(/New Tab/);
});
5.13 Mobile Emulation
import { devices } from '@playwright/test';
// In config
projects: [
{
name: 'mobile-safari',
use: {
...devices['iPhone 13'],
},
},
{
name: 'tablet',
use: {
...devices['iPad Pro 11'],
},
},
],
Or per test:
test('mobile layout', async ({ browser }) => {
const context = await browser.newContext({
...devices['Pixel 5'],
});
const page = await context.newPage();
await page.goto('/');
// test mobile-specific behavior
await context.close();
});
5.14 Geolocation, Timezone, and Locale
// Config-level
use: {
geolocation: { latitude: 48.8566, longitude: 2.3522 },
permissions: ['geolocation'],
timezoneId: 'Europe/Paris',
locale: 'fr-FR',
},
// Per test
test('paris location', async ({ browser }) => {
const context = await browser.newContext({
geolocation: { latitude: 48.8566, longitude: 2.3522 },
permissions: ['geolocation'],
locale: 'fr-FR',
timezoneId: 'Europe/Paris',
});
const page = await context.newPage();
await page.goto('/store-locator');
await context.close();
});
5.15 Dark Mode and Color Scheme
use: {
colorScheme: 'dark',
},
Or toggle mid-test:
test('toggle dark mode', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark' });
await expect(page.locator('body')).toHaveCSS(
'background-color', 'rgb(0, 0, 0)'
);
await page.emulateMedia({ colorScheme: 'light' });
await expect(page.locator('body')).toHaveCSS(
'background-color', 'rgb(255, 255, 255)'
);
});
5.16 Parallel Execution and Sharding
By default, Playwright runs test files in parallel with 50% of CPU cores as workers.
// Make all tests in a file run in parallel (not just across files)
test.describe.configure({ mode: 'parallel' });
For CI sharding across machines:
# Machine 1
npx playwright test --shard=1/3
# Machine 2
npx playwright test --shard=2/3
# Machine 3
npx playwright test --shard=3/3
Merge Sharded Reports
npx playwright merge-reports ./blob-reports --reporter=html
5.17 Test Retries
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
});
In tests, detect retry:
test('may need retry', async ({ page }, testInfo) => {
if (testInfo.retry) {
await page.goto('/clear-cache');
}
// ...
});
5.18 Parameterized Tests
const users = [
{ role: 'admin', expectDashboard: true },
{ role: 'viewer', expectDashboard: false },
{ role: 'editor', expectDashboard: true },
];
for (const { role, expectDashboard } of users) {
test(`${role} sees correct dashboard`, async ({ page }) => {
await page.goto(`/login?role=${role}`);
if (expectDashboard) {
await expect(page.locator('#dashboard')).toBeVisible();
} else {
await expect(page.locator('#dashboard')).not.toBeVisible();
}
});
}
5.19 Code Generation with Codegen
Record interactions as test code automatically:
# Generate TypeScript test code
npx playwright codegen https://example.com
# Save to file
npx playwright codegen -o tests/recorded.spec.ts https://example.com
# Target a different language
npx playwright codegen --target python https://example.com
npx playwright codegen --target java https://example.com
npx playwright codegen --target csharp https://example.com
# Emulate a device
npx playwright codegen --device="iPhone 13" https://example.com
# Set viewport
npx playwright codegen --viewport-size=800,600 https://example.com
# Custom test ID attribute
npx playwright codegen --test-id-attribute=data-cy https://example.com
Available --target languages:
| Target | Output |
|---|---|
javascript |
Playwright library script |
playwright-test |
@playwright/test test file (default) |
python |
Python sync script |
python-async |
Python async script |
python-pytest |
pytest-playwright test |
csharp |
C# script |
csharp-mstest |
C# MSTest |
csharp-nunit |
C# NUnit |
java |
Java script |
java-junit |
Java JUnit test |
5.20 Browser Open and Inspect
Open a browser manually for exploration:
# Default browser (Chromium)
npx playwright open https://example.com
# Specific browser
npx playwright cr https://example.com # Chromium
npx playwright ff https://example.com # Firefox
npx playwright wk https://example.com # WebKit
# With emulation
npx playwright open --device="iPhone 13" https://example.com
npx playwright open --color-scheme=dark https://example.com
npx playwright open --timezone="Asia/Tokyo" https://example.com
npx playwright open --geolocation="35.6762,139.6503" https://example.com
5.21 Waiting Strategies
Playwright auto-waits for elements before acting on them, but sometimes you need explicit waits:
// Wait for a specific element
await page.waitForSelector('.results-loaded');
// Wait for navigation
await page.waitForURL('**/success');
// Wait for load state
await page.waitForLoadState('networkidle');
// Wait for a response
const response = await page.waitForResponse('**/api/data');
// Wait for a request
const request = await page.waitForRequest('**/api/submit');
// Wait for a function to return truthy
await page.waitForFunction(() => document.title.includes('Ready'));
// Explicit timeout
await page.waitForTimeout(1000); // avoid in tests; prefer auto-wait
Warning
Avoid
page.waitForTimeout()in tests. It introduces artificial delays and makes tests slow and fragile. Use locator assertions (await expect(locator).toBeVisible()) or event-based waits instead.
5.22 Console and Error Capture
test('capture console errors', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (error) => {
errors.push(error.message);
});
await page.goto('/');
expect(errors).toEqual([]);
});
5.23 Accessibility Testing
test('page is accessible', async ({ page }) => {
await page.goto('/');
// Check for accessibility violations using @axe-core/playwright
// npm install -D @axe-core/playwright
const { checkA11y } = require('@axe-core/playwright');
const accessibilityScanResults = await new AxeBuilder({ page })
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
5.24 Component Testing
Playwright supports component testing for React and Vue applications.
npm install -D @playwright/ct-react # React 18+
npm install -D @playwright/ct-react17 # React 17
npm install -D @playwright/ct-vue # Vue
// src/Button.spec.tsx
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('renders with text', async ({ mount }) => {
const component = await mount(<Button>Click me</Button>);
await expect(component).toContainText('Click me');
});
test('handles click', async ({ mount }) => {
let clicked = false;
const component = await mount(
<Button onClick={() => (clicked = true)}>Click</Button>
);
await component.click();
expect(clicked).toBeTruthy();
});
5.25 Running a Playwright Server
For remote browser connections:
npx playwright run-server --port=3001 --host=0.0.0.0
| Option | Description |
|---|---|
--port |
Port to listen on |
--host |
Host to bind to |
--path |
WebSocket path prefix |
--max-clients |
Maximum concurrent connections |
--mode |
Server mode |
--artifacts-dir |
Directory for artifacts |
Connect from a client:
import { chromium } from 'playwright';
const browser = await chromium.connect('ws://server-host:3001');
const page = await browser.newPage();
await page.goto('https://example.com');
5.26 Storage State for Multi-User Scenarios
test('admin and user interaction', async ({ browser }) => {
// Admin context
const adminContext = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const adminPage = await adminContext.newPage();
// User context
const userContext = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const userPage = await userContext.newPage();
// Admin creates content
await adminPage.goto('/admin/posts');
await adminPage.click('#create-post');
await adminPage.fill('#title', 'New Announcement');
await adminPage.click('#publish');
// User sees it
await userPage.goto('/posts');
await expect(userPage.locator('.post-title')).toContainText('New Announcement');
await adminContext.close();
await userContext.close();
});
5.27 Handling HTTP Credentials and Proxies
// Basic auth
use: {
httpCredentials: {
username: 'admin',
password: 'secret',
},
},
// Proxy
use: {
proxy: {
server: 'http://proxy.example.com:8080',
username: 'proxyuser',
password: 'proxypass',
},
},
CLI with proxy:
npx playwright open --proxy-server=http://proxy:8080 https://example.com
5.28 Block Service Workers
// In config
use: {
serviceWorkers: 'block',
},
Or via CLI:
npx playwright open --block-service-workers https://example.com
5.29 Custom Test ID Attribute
If your app uses a custom attribute for test selectors (e.g., data-cy for Cypress migration):
// playwright.config.ts
use: {
testIdAttribute: 'data-cy',
},
Now page.getByTestId('submit') looks for [data-cy="submit"] instead of [data-testid="submit"].
5.30 MCP Test Server
Playwright includes an MCP (Model Context Protocol) server for AI-assisted testing:
npx playwright run-test-mcp-server --headless --config=playwright.config.ts
| Option | Description |
|---|---|
--headless |
Run browsers in headless mode |
-c, --config |
Path to config file |
--host |
Host to bind to |
--port |
Port to listen on |
5.31 Test Agents
Initialize test agents for AI-driven testing:
npx playwright init-agents --config=playwright.config.ts --project=chromium
| Option | Description |
|---|---|
-c, --config |
Path to config file |
--project |
Target project |
--prompts |
Agent prompt configuration |
6. Troubleshooting
6.1 Installation Problems
Error: Host system is missing dependencies to run browsers
Cause: Linux system lacks required shared libraries for browser binaries.
Solution:
npx playwright install-deps
Or for a specific browser:
npx playwright install-deps chromium
Prevention: Always use npx playwright install --with-deps on fresh CI images.
Error: browserType.launch: Executable doesn't exist at ...
Cause: Browsers were not installed, or were installed for a different Playwright version.
Solution:
npx playwright install
Prevention: Add npx playwright install to your CI setup step. Pin your Playwright version in package.json.
Error: Cannot find module '@playwright/test'
Cause: Package not installed or not in the correct node_modules.
Solution:
npm install -D @playwright/test
6.2 Test Execution Problems
Error: Timeout 30000ms exceeded
Cause: Action or assertion took longer than the default 30-second timeout. Common with slow pages, network latency, or elements that never appear.
Solution:
- Increase timeout for the specific test:
test('slow operation', async ({ page }) => {
test.setTimeout(60_000);
// ...
});
- Or increase globally in config:
timeout: 60_000,
use: {
actionTimeout: 15_000,
navigationTimeout: 60_000,
},
- Investigate whether the element actually appears — use
--debugmode to watch.
Error: strict mode violation: locator resolved to N elements
Cause: A locator matched multiple elements when only one was expected.
Solution: Make the locator more specific:
// Too broad
await page.click('button');
// Specific
await page.click('button:has-text("Submit")');
// or
await page.getByRole('button', { name: 'Submit' }).click();
Error: Element is not visible
Cause: Attempting to interact with a hidden element.
Solution:
// Wait for visibility first
await expect(page.locator('#modal')).toBeVisible();
await page.locator('#modal button').click();
// Or force the action (use sparingly)
await page.click('#hidden-button', { force: true });
Error: waiting for selector "..." to be visible
Cause: The element exists in DOM but is not rendered (display: none, visibility: hidden, zero size).
Solution: Check that the element becomes visible before interacting. Use the browser DevTools or Trace Viewer to inspect the DOM state at the time of failure.
Error: page.goto: net::ERR_CONNECTION_REFUSED
Cause: The target URL is not reachable. The dev server may not have started yet.
Solution:
- Verify the
webServerconfig starts your app correctly. - Ensure the URL and port match between
webServerandbaseURL. - Add a longer
timeouttowebServer:
webServer: {
command: 'npm start',
url: 'http://localhost:3000',
timeout: 120_000,
},
Playwright stuck at about:blank
Cause: This is a known issue (GitHub #31050, 50+ comments) where the browser opens but never navigates. Often caused by browser extensions, corrupted profiles, or system-level browser interference.
Solution:
- Clear the browser cache:
npx playwright clear-cache - Ensure no other Playwright processes are running
- Try a different browser:
--project=firefox - On Linux, check that
--no-sandboxis not needed (Docker/CI)
Tests pass locally but fail on CI
Cause: Timing differences, missing fonts, different screen resolution, or missing system dependencies.
Solution:
- Use
npx playwright install --with-depsin CI - Set
retries: 2for CI in config - Use
trace: 'on-first-retry'to debug CI failures - Set deterministic viewport and locale in config
- Avoid
networkidlewaits — prefer element-based assertions
6.3 Browser-Specific Issues
WebKit WASM breakage (v1.59)
Cause: Known regression in v1.59 (GitHub #40268) where WebAssembly modules fail in WebKit.
Solution: Upgrade to v1.60+ where this is addressed, or skip WebKit for WASM-dependent tests:
test('wasm feature', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'WebKit WASM issue #40268');
// ...
});
Firefox cannot read response JSON from GET requests
Cause: Known issue with Firefox 146 (GitHub #39160).
Solution: Use response.text() and parse manually as a workaround, or check for Playwright updates that bundle a patched Firefox.
Response.formData() throws TypeError in WebKit
Cause: WebKit 26.4 regression (GitHub #40244).
Solution: Parse the response body manually instead of using formData().
Proxy auth causes redirects to hang in Chromium
Cause: When using proxy authentication without page.route(), Chromium may hang on redirects (GitHub #40194).
Solution: Set proxy credentials in the browser context options rather than relying on HTTP authentication prompts:
const context = await browser.newContext({
proxy: {
server: 'http://proxy:8080',
username: 'user',
password: 'pass',
},
});
6.4 Dialog and Popup Issues
page.on('dialog') not working with external URL handlers
Cause: Known limitation (GitHub #11014, 37 comments) where dialogs triggered by external URL scheme handlers (e.g., mailto:, tel:) are not intercepted by Playwright's dialog handler.
Solution: Mock the navigation instead of handling the dialog:
await page.route('**/*', (route) => {
if (route.request().url().startsWith('mailto:')) {
return route.abort();
}
return route.continue();
});
6.5 TypeScript Issues
Error: Cannot run TypeScript files with "declare"
Cause: TypeScript files using declare keyword (ambient declarations) can conflict with Playwright's test loader (GitHub #38586).
Solution: Move ambient declarations to a separate .d.ts file and ensure tsconfig.json includes it properly:
{
"compilerOptions": {
"strict": true
},
"include": ["tests/**/*.ts", "types/**/*.d.ts"]
}
6.6 VS Code Extension Issues
VS Code extension showing "No tests have been found"
Cause: The extension may not detect tests if the config file is in a subdirectory, the test file pattern does not match, or the extension version is mismatched (GitHub #40226).
Solution:
- Ensure
playwright.config.tsis at the workspace root - Verify
testDirin config points to the correct directory - Check that test files match the pattern
**/*.@(spec|test).?(c|m)[jt]s?(x) - Restart the extension: Ctrl+Shift+P > "Playwright: Reload"
- Ensure extension version matches your installed Playwright version
6.7 Performance Problems
Tests are too slow
Solution:
- Run in parallel (default): ensure
fullyParallel: trueis set - Use more workers:
--workers=8 - Block unnecessary resources:
await page.route('**/*.{png,jpg,css,font}', (route) => route.abort());
- Reuse authentication state instead of logging in per test
- Use
webServer.reuseExistingServerin local development - Avoid
waitForLoadState('networkidle')— it waits for 500ms of network silence
Out of memory in CI
Solution:
- Reduce workers:
--workers=1or--workers=2 - Shard across machines instead of parallelizing on one
- Close contexts explicitly in tests
- Disable video and trace unless needed
6.8 Debugging Techniques
Debug a Specific Test
PWDEBUG=1 npx playwright test tests/login.spec.ts
The Playwright Inspector opens alongside the browser. Step through actions, inspect locators, and evaluate selectors in real time.
Pause Mid-Test
test('debug this', async ({ page }) => {
await page.goto('/');
await page.pause(); // Inspector opens here
await page.click('#next');
});
Verbose Logging
DEBUG=pw:api npx playwright test
Other debug channels: pw:browser, pw:protocol, pw:channel.
Review Traces After Failure
npx playwright show-trace test-results/my-test-chromium/trace.zip
7. FAQ
Q: Which browsers does Playwright support? Playwright supports Chromium (Chrome, Edge), Firefox, and WebKit (Safari) — the three major browser engines. Each Playwright release bundles specific browser versions that are tested and supported.
Q: Can I use Playwright with JavaScript instead of TypeScript?
Yes. Use playwright.config.js instead of .ts and write tests as .spec.js files. TypeScript is optional but recommended for autocompletion and type safety.
Q: How do I run tests on only one browser?
Use --project=chromium, --project=firefox, or --project=webkit. These correspond to project names in your config.
Q: What is the difference between playwright and playwright-core?
playwright bundles browser download logic and defaults. playwright-core is the bare library without auto-download — intended for library authors or custom browser management. For testing, always use @playwright/test which includes everything.
Q: How do I run tests in headed mode to see the browser?
Use npx playwright test --headed. For debugging, npx playwright test --debug opens headed mode with the Inspector.
Q: How do I test multiple viewports or devices?
Define multiple projects in your config, each with different devices settings. Playwright includes device descriptors for dozens of phones and tablets.
Q: Can I test a site that requires login?
Yes. Use a setup project to authenticate once, save storageState to a JSON file, and load it in subsequent projects. See section 5.1.
Q: How do I handle pop-ups and new tabs?
Use page.waitForEvent('popup') to capture the new page object. See section 5.12.
Q: How do I test file downloads?
Set acceptDownloads: true (default in test fixtures), then use page.waitForEvent('download'). See section 5.8.
Q: What is the baseURL setting for?
It allows you to use relative URLs in page.goto('/path') instead of full URLs. Set it in config or override with the PLAYWRIGHT_TEST_BASE_URL environment variable.
Q: How do I debug tests on CI?
Enable trace: 'on-first-retry' in your config. Failed tests produce trace files you can download and view locally with npx playwright show-trace.
Q: How do I test Web Components or Shadow DOM?
Playwright pierces Shadow DOM automatically. Locators like page.locator('my-component >> button') work without extra configuration.
Q: Can I run Playwright tests in parallel?
Yes, parallelism is on by default. Test files run in parallel across workers (default: 50% of CPU cores). Use test.describe.configure({ mode: 'parallel' }) for intra-file parallelism.
Q: How do I shard tests across CI machines?
Use --shard=1/3, --shard=2/3, --shard=3/3 on each machine, then merge results with npx playwright merge-reports.
Q: How do I retry flaky tests?
Set retries: 2 in your config. Playwright re-runs failed tests up to that many times. Use trace: 'on-first-retry' to automatically capture traces for debugging the first failure.
Q: How do I test APIs without a browser?
Use the request fixture which provides an API client. See section 5.2. No browser is launched for API-only tests using request.
Q: Can I intercept and mock network requests?
Yes. Use page.route() to intercept requests and route.fulfill() to return mock responses. See section 5.3.
Q: What file naming convention should I use for tests?
The default pattern matches *.spec.ts, *.spec.js, *.test.ts, *.test.js (and their .mjs, .cjs, .mts, .cts, .tsx, .jsx variants). Use .spec.ts by convention.
Q: How do I update snapshot baselines?
Run npx playwright test --update-snapshots. This regenerates all screenshot baselines.
Q: How do I skip a test on a specific browser? Use conditional skip inside the test:
test('chromium only feature', async ({ browserName }) => {
test.skip(browserName !== 'chromium', 'Chromium-only feature');
// ...
});
Q: Can I use environment variables in tests?
Yes. Access them via process.env in config or test files. Use dotenv for .env file loading, or pass them inline: MY_VAR=value npx playwright test.
Q: How do I generate PDF output from a page?
Use page.pdf() in Chromium only. See section 5.7. Firefox and WebKit do not support PDF generation.
Q: What is UI Mode?
npx playwright test --ui opens an interactive test runner with live-reload, time-travel debugging, and a visual timeline. It is the recommended way to author and debug tests during development.
Q: How do I clear Playwright's cache?
Run npx playwright clear-cache. This removes cached browser binaries and test artifacts.
Q: What is the Trace Viewer?
A web-based tool that visualizes test execution: DOM snapshots, network traffic, console logs, and action timeline. Open with npx playwright show-trace <trace.zip>.
Q: Can I connect to a remote browser?
Yes. Start a server with npx playwright run-server and connect with browser.connect('ws://host:port'). See section 5.25.
Q: How do I test responsive design?
Configure multiple projects with different viewport sizes or use the device descriptors (devices['iPhone 13'], devices['iPad Pro 11'], etc.).