The Only Playwright Reference You Need
Every API, pattern, and trick โ organized, searchable, and copy-ready across 5 languages.
๐ Multi-Language Support 5 LANGUAGES
BeginnerInstallation & Basic Setup
Click any language tab to see the code in that language. All 5 languages are available!
// Install Playwright
npm init playwright@latest
npm install -D @playwright/test
// Import
const { test, expect } = require('@playwright/test')
// Basic test
test('my test', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
})
// Install Playwright
npm init playwright@latest
// Import with types
import { test, expect, Page } from '@playwright/test'
// Basic test
test('my test', async ({ page }: { page: Page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
})
# Install Playwright
pip install pytest-playwright
playwright install
# Import
from playwright.sync_api import Page, expect
# Basic test
def test_my_test(page: Page):
page.goto("https://example.com")
expect(page).to_have_title("Example")
// Install via NuGet
dotnet add package Microsoft.Playwright.NUnit
pwsh bin/Debug/net8.0/playwright.ps1 install
// Import
using Microsoft.Playwright.NUnit;
// Basic test
[Test]
public async Task MyTest()
{
await Page.GotoAsync("https://example.com");
await Expect(Page).ToHaveTitleAsync("Example");
}
// Add Maven dependency
// com.microsoft.playwright:playwright:1.40.0
// Import
import com.microsoft.playwright.*;
// Basic test
@Test
void myTest() {
page.navigate("https://example.com");
assertThat(page).hasTitle("Example");
}
Getting Started & Installation
BeginnerInstallation
Install Playwright with a single command. This includes browsers, test runner, and all dependencies.
// Install Playwright with browsers
npm init playwright@latest
// Or add to existing project
npm install -D @playwright/test
npx playwright install
// Install Playwright with browsers (TypeScript included)
npm init playwright@latest
// Or add to existing project
npm install -D @playwright/test
npx playwright install
# Install Playwright
pip install pytest-playwright
# Install browsers
playwright install
# Install with system dependencies (Linux)
playwright install --with-deps
// Install via NuGet
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit
// Build and install browsers
dotnet build
pwsh bin/Debug/net8.0/playwright.ps1 install
// Add to pom.xml (Maven)
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.40.0</version>
</dependency>
// Or build.gradle (Gradle)
implementation 'com.microsoft.playwright:playwright:1.40.0'
Project Initialization Options
# TypeScript project
npm init playwright@latest -- --typescript
# JavaScript project
npm init playwright@latest -- --javascript
# Install specific browsers only
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit
# Install with dependencies(for CI)
npx playwright install --with-deps
Basic Browser Launch
const { chromium, firefox, webkit } = require('playwright');
// Launch Chromium
const browser = await chromium.launch();
const browser = await chromium.launch({ headless: false }); // Headed mode
const browser = await chromium.launch({
headless: true,
slowMo: 100, // Slow down by 100ms
devtools: true // Open devtools
});
// Launch Firefox
const browser = await firefox.launch();
// Launch WebKit(Safari)
const browser = await webkit.launch();
// Close browser
await browser.close();
Browser Contexts
Browser contexts are isolated environments - like incognito windows. Each context has its own cookies, storage, and cache.
// Create a new context
const context = await browser.newContext();
// Create context with 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: 59.95, longitude: 30.31667 },
colorScheme: 'dark',
deviceScaleFactor: 2
});
// Create page from context
const page = await context.newPage();
// Close context(closes all pages)
await context.close();
Locators & Selectors CRITICAL
BeginnerModern Locators (Recommended)
Playwright's locator API provides auto-waiting and retry-ability. Always prefer locators over deprecated selectors.
// By role (MOST RECOMMENDED)
page.getByRole('button', { name: 'Submit' })
page.getByRole('link', { name: 'Contact Us' })
page.getByRole('textbox', { name: 'Email' })
// By text
page.getByText('Welcome')
page.getByText('Welcome', { exact: true })
page.getByText(/welcome/i)
// By label (for form inputs)
page.getByLabel('Email address')
page.getByLabel('Password', { exact: true })
// By placeholder, alt text, title
page.getByPlaceholder('Enter your email')
page.getByAltText('Company logo')
page.getByTitle('Close dialog')
// By test ID (recommended for stable automation)
page.getByTestId('submit-button')
// By role (MOST RECOMMENDED)
page.getByRole('button', { name: 'Submit' })
page.getByRole('link', { name: 'Contact Us' })
page.getByRole('textbox', { name: 'Email' })
// By text
page.getByText('Welcome')
page.getByText('Welcome', { exact: true })
page.getByText(/welcome/i)
// By label (for form inputs)
page.getByLabel('Email address')
page.getByLabel('Password', { exact: true })
// By placeholder, alt text, title
page.getByPlaceholder('Enter your email')
page.getByAltText('Company logo')
page.getByTitle('Close dialog')
// By test ID (recommended for stable automation)
page.getByTestId('submit-button')
# By role (MOST RECOMMENDED)
page.get_by_role("button", name="Submit")
page.get_by_role("link", name="Contact Us")
page.get_by_role("textbox", name="Email")
# By text
page.get_by_text("Welcome")
page.get_by_text("Welcome", exact=True)
page.get_by_text(re.compile("welcome", re.IGNORECASE))
# By label (for form inputs)
page.get_by_label("Email address")
page.get_by_label("Password", exact=True)
# By placeholder, alt text, title
page.get_by_placeholder("Enter your email")
page.get_by_alt_text("Company logo")
page.get_by_title("Close dialog")
# By test ID (recommended for stable automation)
page.get_by_test_id("submit-button")
// By role (MOST RECOMMENDED)
Page.GetByRole(AriaRole.Button, new() { Name = "Submit" })
Page.GetByRole(AriaRole.Link, new() { Name = "Contact Us" })
Page.GetByRole(AriaRole.Textbox, new() { Name = "Email" })
// By text
Page.GetByText("Welcome")
Page.GetByText("Welcome", new() { Exact = true })
Page.GetByText(new Regex("welcome", RegexOptions.IgnoreCase))
// By label (for form inputs)
Page.GetByLabel("Email address")
Page.GetByLabel("Password", new() { Exact = true })
// By placeholder, alt text, title
Page.GetByPlaceholder("Enter your email")
Page.GetByAltText("Company logo")
Page.GetByTitle("Close dialog")
// By test ID (recommended for stable automation)
Page.GetByTestId("submit-button")
// By role (MOST RECOMMENDED)
page.getByRole(AriaRole.BUTTON, new GetByRoleOptions().setName("Submit"))
page.getByRole(AriaRole.LINK, new GetByRoleOptions().setName("Contact Us"))
page.getByRole(AriaRole.TEXTBOX, new GetByRoleOptions().setName("Email"))
// By text
page.getByText("Welcome")
page.getByText("Welcome", new GetByTextOptions().setExact(true))
page.getByText(Pattern.compile("welcome", Pattern.CASE_INSENSITIVE))
// By label (for form inputs)
page.getByLabel("Email address")
page.getByLabel("Password", new GetByLabelOptions().setExact(true))
// By placeholder, alt text, title
page.getByPlaceholder("Enter your email")
page.getByAltText("Company logo")
page.getByTitle("Close dialog")
// By test ID (recommended for stable automation)
page.getByTestId("submit-button")
CSS & XPath Selectors
// CSS selectors
page.locator('button')
page.locator('.submit-btn')
page.locator('#login-form')
page.locator('button.primary')
page.locator('[data-testid="submit"]')
page.locator('div > button')
page.locator('input[type="email"]')
page.locator('button:has-text("Submit")')
page.locator('div.card:has(button)')
// XPath selectors
page.locator('xpath=//button[@type="submit"]')
page.locator('//div[@class="container"]//button')
page.locator('//button[contains(text(), "Submit")]')
Locator Chaining & Filtering
// Filter by text
page.locator('button').filter({ hasText: 'Submit' })
// Filter by another locator
page.locator('div').filter({ has: page.locator('button') })
// Get by index(use sparingly)
page.locator('button').nth(0)
page.locator('button').first()
page.locator('button').last()
// Count locators
await page.locator('button').count()
// Chain locators
page.locator('.product-card').locator('button.add-to-cart')
page.locator('form').getByRole('button', { name: 'Submit' })
// Get all matching elements
const elements = await page.locator('.item').all()
for (const element of elements) {
await element.click()
}
Role-Based Locators Reference
| Role | HTML Elements | Example |
|---|---|---|
button |
<button>, <input type="button"> | getByRole('button', { name: 'Submit' }) |
link |
<a href> | getByRole('link', { name: 'Home' }) |
textbox |
<input type="text"> | getByRole('textbox', { name: 'Email' }) |
checkbox |
<input type="checkbox"> | getByRole('checkbox', { name: 'Accept' }) |
radio |
<input type="radio"> | getByRole('radio', { name: 'Male' }) |
combobox |
<select> | getByRole('combobox', { name: 'Country' }) |
heading |
<h1> to <h6> | getByRole('heading', { name: 'Welcome' }) |
img |
<img> | getByRole('img', { name: 'Logo' }) |
Locator Best Practices
1.
getByRole() - Most resilient and accessible2.
getByLabel() - Great for form fields3.
getByTestId() - Stable for automation4.
getByText() - Good for unique text5.
CSS/XPath - Use as last resort
// โ BAD - Brittle, will break easily
page.locator('div > div > button:nth-child(3)')
page.locator('body > div.container > div.row > button')
// โ
GOOD - Resilient and meaningful
page.getByRole('button', { name: 'Submit Order' })
page.getByTestId('submit-order-button')
page.locator('button[aria-label="Submit Order"]')
// โ BAD - Too generic
page.locator('button').nth(2)
page.locator('.btn').first()
// โ
GOOD - Specific and stable
page.locator('form[name="checkout"]').getByRole('button', { name: 'Pay Now' })
page.getByTestId('checkout-pay-button')
Actions & Interactions
BeginnerNavigation
// Navigate to URL
await page.goto('https://example.com')
await page.goto('https://example.com', { waitUntil: 'networkidle' })
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' })
// Navigation with timeout
await page.goto('https://example.com', { timeout: 30000 })
// Navigate back/forward
await page.goBack()
await page.goForward()
await page.reload()
// Get current URL
const url = page.url()
// Get title
const title = await page.title()
Click Actions
// Simple click(auto-waits for element)
await page.getByRole('button', { name: 'Submit' }).click()
// Click with options
await page.locator('button').click({
button: 'right', // 'left', 'right', 'middle'
clickCount: 2, // Double click
delay: 100, // Delay between mousedown and mouseup
force: true, // Skip actionability checks
modifiers: ['Shift'], // 'Alt', 'Control', 'Meta', 'Shift'
position: { x: 10, y: 10 }, // Click at specific position
timeout: 5000 // Custom timeout
})
// Double click
await page.locator('text').dblclick()
// Hover then click
await page.locator('menu').hover()
await page.locator('menu-item').click()
// Click and wait for navigation
await Promise.all([
page.waitForNavigation(),
page.getByRole('link').click()
])
Form Interactions
// Fill, type, clear
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Email').type('text', { delay: 100 })
await page.getByLabel('Email').clear()
// Checkbox & Radio
await page.getByRole('checkbox').check()
await page.getByRole('checkbox').uncheck()
await page.getByRole('checkbox').setChecked(true)
// Select dropdown
await page.getByRole('combobox').selectOption('option-value')
await page.getByRole('combobox').selectOption({ label: 'Label' })
await page.getByRole('combobox').selectOption({ index: 2 })
// File upload
await page.getByLabel('Upload').setInputFiles('file.pdf')
await page.getByLabel('Upload').setInputFiles(['f1.pdf', 'f2.pdf'])
// Fill, type, clear
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Email').type('text', { delay: 100 })
await page.getByLabel('Email').clear()
// Checkbox & Radio
await page.getByRole('checkbox').check()
await page.getByRole('checkbox').uncheck()
await page.getByRole('checkbox').setChecked(true)
// Select dropdown
await page.getByRole('combobox').selectOption('option-value')
await page.getByRole('combobox').selectOption({ label: 'Label' })
await page.getByRole('combobox').selectOption({ index: 2 })
// File upload
await page.getByLabel('Upload').setInputFiles('file.pdf')
await page.getByLabel('Upload').setInputFiles(['f1.pdf', 'f2.pdf'])
# Fill, type, clear
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Email").type("text", delay=100)
page.get_by_label("Email").clear()
# Checkbox & Radio
page.get_by_role("checkbox").check()
page.get_by_role("checkbox").uncheck()
page.get_by_role("checkbox").set_checked(True)
# Select dropdown
page.get_by_role("combobox").select_option("option-value")
page.get_by_role("combobox").select_option(label="Label")
page.get_by_role("combobox").select_option(index=2)
# File upload
page.get_by_label("Upload").set_input_files("file.pdf")
page.get_by_label("Upload").set_input_files(["f1.pdf", "f2.pdf"])
// Fill, type, clear
await Page.GetByLabel("Email").FillAsync("user@example.com");
await Page.GetByLabel("Email").TypeAsync("text", new() { Delay = 100 });
await Page.GetByLabel("Email").ClearAsync();
// Checkbox & Radio
await Page.GetByRole(AriaRole.Checkbox).CheckAsync();
await Page.GetByRole(AriaRole.Checkbox).UncheckAsync();
await Page.GetByRole(AriaRole.Checkbox).SetCheckedAsync(true);
// Select dropdown
await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync("option-value");
await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new() { Label = "Label" });
await Page.GetByRole(AriaRole.Combobox).SelectOptionAsync(new() { Index = 2 });
// File upload
await Page.GetByLabel("Upload").SetInputFilesAsync("file.pdf");
await Page.GetByLabel("Upload").SetInputFilesAsync(new[] { "f1.pdf", "f2.pdf" });
// Fill, type, clear
page.getByLabel("Email").fill("user@example.com");
page.getByLabel("Email").type("text", new TypeOptions().setDelay(100));
page.getByLabel("Email").clear();
// Checkbox & Radio
page.getByRole(AriaRole.CHECKBOX).check();
page.getByRole(AriaRole.CHECKBOX).uncheck();
page.getByRole(AriaRole.CHECKBOX).setChecked(true);
// Select dropdown
page.getByRole(AriaRole.COMBOBOX).selectOption("option-value");
page.getByRole(AriaRole.COMBOBOX).selectOption(new SelectOption().setLabel("Label"));
page.getByRole(AriaRole.COMBOBOX).selectOption(new SelectOption().setIndex(2));
// File upload
page.getByLabel("Upload").setInputFiles(Paths.get("file.pdf"));
page.getByLabel("Upload").setInputFiles(new Path[] {Paths.get("f1.pdf"), Paths.get("f2.pdf")});
// Fill input field(clears first)
await page.getByLabel('Email').fill('user@example.com')
// Type with delay(simulates human typing)
await page.getByLabel('Email').type('user@example.com', { delay: 100 })
// Press sequences
await page.getByLabel('Search').pressSequentially('Hello World', { delay: 50 })
// Clear input
await page.getByLabel('Email').clear()
// Check/uncheck
await page.getByRole('checkbox').check()
await page.getByRole('checkbox').uncheck()
await page.getByRole('checkbox').setChecked(true)
// Select dropdown
await page.getByRole('combobox').selectOption('option-value')
await page.getByRole('combobox').selectOption({ label: 'Option Label' })
await page.getByRole('combobox').selectOption({ index: 2 })
// Multiple select
await page.locator('select').selectOption(['option1', 'option2'])
// File upload
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf'])
// Remove file
await page.getByLabel('Upload').setInputFiles([])
Keyboard Actions
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowDown')
await page.getByLabel('Input').press('Control+A')
await page.keyboard.type('Hello World', { delay: 100 })
await page.keyboard.down('Shift')
await page.keyboard.up('Shift')
page.keyboard.press("Enter")
page.keyboard.press("ArrowDown")
page.get_by_label("Input").press("Control+A")
page.keyboard.type("Hello World", delay=100)
page.keyboard.down("Shift")
page.keyboard.up("Shift")
await Page.Keyboard.PressAsync("Enter");
await Page.Keyboard.PressAsync("ArrowDown");
await Page.GetByLabel("Input").PressAsync("Control+A");
await Page.Keyboard.TypeAsync("Hello World", new() { Delay = 100 });
await Page.Keyboard.DownAsync("Shift");
await Page.Keyboard.UpAsync("Shift");
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowDown')
await page.getByLabel('Input').press('Control+A')
await page.keyboard.type('Hello World', { delay: 100 })
await page.keyboard.down('Shift')
await page.keyboard.up('Shift')
page.keyboard().press("Enter");
page.keyboard().press("ArrowDown");
page.getByLabel("Input").press("Control+A");
page.keyboard().type("Hello World", new Keyboard.TypeOptions().setDelay(100));
page.keyboard().down("Shift");
page.keyboard().up("Shift");
// Press single key
await page.keyboard.press('Enter')
await page.keyboard.press('ArrowDown')
await page.keyboard.press('Tab')
// Press key on element
await page.getByLabel('Search').press('Enter')
// Type text
await page.keyboard.type('Hello World')
// Key combinations
await page.keyboard.press('Control+A')
await page.keyboard.press('Meta+C') // Cmd+C on Mac
await page.keyboard.press('Shift+ArrowDown')
// Hold and release
await page.keyboard.down('Shift')
await page.keyboard.press('A')
await page.keyboard.up('Shift')
// Insert text(without triggering keyboard events)
await page.keyboard.insertText('Paste this text')
Mouse Actions
// Hover
await page.locator('button').hover()
// Mouse move
await page.mouse.move(100, 200)
// Mouse click at position
await page.mouse.click(100, 200)
await page.mouse.dblclick(100, 200)
// Drag and drop
await page.locator('.source').dragTo(page.locator('.target'))
// Manual drag and drop
await page.locator('.source').hover()
await page.mouse.down()
await page.locator('.target').hover()
await page.mouse.up()
// Scroll
await page.mouse.wheel(0, 100) // Scroll down
// Scroll element into view
await page.locator('.footer').scrollIntoViewIfNeeded()
Focus & Blur
// Focus element
await page.getByLabel('Email').focus()
// Blur element
await page.getByLabel('Email').blur()
// Tab navigation
await page.keyboard.press('Tab')
await page.keyboard.press('Shift+Tab')
Assertions & Expectations CRITICAL
BeginnerElement State Assertions
Playwright's expect includes auto-waiting and retry logic. These assertions will wait until the condition is met or timeout.
await expect(page.getByRole('button')).toBeVisible()
await expect(page.getByRole('button')).toBeHidden()
await expect(page.getByRole('button')).toBeEnabled()
await expect(page.getByRole('button')).toBeDisabled()
await expect(page.getByRole('checkbox')).toBeChecked()
await expect(page.getByLabel('Email')).toBeEditable()
await expect(page.getByLabel('Email')).toBeFocused()
await expect(page.getByLabel('Search')).toBeEmpty()
await expect(page.getByRole('button')).toBeVisible()
await expect(page.getByRole('button')).toBeHidden()
await expect(page.getByRole('button')).toBeEnabled()
await expect(page.getByRole('button')).toBeDisabled()
await expect(page.getByRole('checkbox')).toBeChecked()
await expect(page.getByLabel('Email')).toBeEditable()
await expect(page.getByLabel('Email')).toBeFocused()
await expect(page.getByLabel('Search')).toBeEmpty()
expect(page.get_by_role("button")).to_be_visible()
expect(page.get_by_role("button")).to_be_hidden()
expect(page.get_by_role("button")).to_be_enabled()
expect(page.get_by_role("button")).to_be_disabled()
expect(page.get_by_role("checkbox")).to_be_checked()
expect(page.get_by_label("Email")).to_be_editable()
expect(page.get_by_label("Email")).to_be_focused()
expect(page.get_by_label("Search")).to_be_empty()
await Expect(Page.GetByRole(AriaRole.Button)).ToBeVisibleAsync();
await Expect(Page.GetByRole(AriaRole.Button)).ToBeHiddenAsync();
await Expect(Page.GetByRole(AriaRole.Button)).ToBeEnabledAsync();
await Expect(Page.GetByRole(AriaRole.Button)).ToBeDisabledAsync();
await Expect(Page.GetByRole(AriaRole.Checkbox)).ToBeCheckedAsync();
await Expect(Page.GetByLabel("Email")).ToBeEditableAsync();
await Expect(Page.GetByLabel("Email")).ToBeFocusedAsync();
await Expect(Page.GetByLabel("Search")).ToBeEmptyAsync();
assertThat(page.getByRole(AriaRole.BUTTON)).isVisible();
assertThat(page.getByRole(AriaRole.BUTTON)).isHidden();
assertThat(page.getByRole(AriaRole.BUTTON)).isEnabled();
assertThat(page.getByRole(AriaRole.BUTTON)).isDisabled();
assertThat(page.getByRole(AriaRole.CHECKBOX)).isChecked();
assertThat(page.getByLabel("Email")).isEditable();
assertThat(page.getByLabel("Email")).isFocused();
assertThat(page.getByLabel("Search")).isEmpty();
const { expect } = require('@playwright/test')
// Visibility
await expect(page.getByRole('button')).toBeVisible()
await expect(page.getByRole('button')).toBeHidden()
await expect(page.getByRole('button')).not.toBeVisible()
// Enabled/Disabled
await expect(page.getByRole('button')).toBeEnabled()
await expect(page.getByRole('button')).toBeDisabled()
// Checked(checkboxes/radio)
await expect(page.getByRole('checkbox')).toBeChecked()
await expect(page.getByRole('checkbox')).not.toBeChecked()
// Editable
await expect(page.getByLabel('Email')).toBeEditable()
await expect(page.getByLabel('Email')).not.toBeEditable()
// Focused
await expect(page.getByLabel('Email')).toBeFocused()
// Empty
await expect(page.getByLabel('Search')).toBeEmpty()
await expect(page.getByLabel('Search')).not.toBeEmpty()
Content Assertions
await expect(page.locator('.title')).toHaveText('Welcome')
await expect(page.locator('.title')).toContainText('Wel')
await expect(page.getByLabel('Email')).toHaveValue('user@example.com')
await expect(page.locator('li')).toHaveCount(5)
await expect(page.getByRole('button')).toHaveAttribute('disabled')
await expect(page.getByRole('button')).toHaveClass('btn-primary')
await expect(page.locator('.box')).toHaveCSS('color', 'rgb(255, 0, 0)')
await expect(page.locator('.title')).toHaveText('Welcome')
await expect(page.locator('.title')).toContainText('Wel')
await expect(page.getByLabel('Email')).toHaveValue('user@example.com')
await expect(page.locator('li')).toHaveCount(5)
await expect(page.getByRole('button')).toHaveAttribute('disabled')
await expect(page.getByRole('button')).toHaveClass('btn-primary')
await expect(page.locator('.box')).toHaveCSS('color', 'rgb(255, 0, 0)')
expect(page.locator(".title")).to_have_text("Welcome")
expect(page.locator(".title")).to_contain_text("Wel")
expect(page.get_by_label("Email")).to_have_value("user@example.com")
expect(page.locator("li")).to_have_count(5)
expect(page.get_by_role("button")).to_have_attribute("disabled")
expect(page.get_by_role("button")).to_have_class("btn-primary")
expect(page.locator(".box")).to_have_css("color", "rgb(255, 0, 0)")
await Expect(Page.Locator(".title")).ToHaveTextAsync("Welcome");
await Expect(Page.Locator(".title")).ToContainTextAsync("Wel");
await Expect(Page.GetByLabel("Email")).ToHaveValueAsync("user@example.com");
await Expect(Page.Locator("li")).ToHaveCountAsync(5);
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttributeAsync("disabled");
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveClassAsync("btn-primary");
await Expect(Page.Locator(".box")).ToHaveCSSAsync("color", "rgb(255, 0, 0)");
assertThat(page.locator(".title")).hasText("Welcome");
assertThat(page.locator(".title")).containsText("Wel");
assertThat(page.getByLabel("Email")).hasValue("user@example.com");
assertThat(page.locator("li")).hasCount(5);
assertThat(page.getByRole(AriaRole.BUTTON)).hasAttribute("disabled");
assertThat(page.getByRole(AriaRole.BUTTON)).hasClass("btn-primary");
assertThat(page.locator(".box")).hasCSS("color", "rgb(255, 0, 0)");
// Text content
await expect(page.locator('.title')).toHaveText('Welcome')
await expect(page.locator('.title')).toHaveText(/welcome/i) // Regex
await expect(page.locator('.title')).toContainText('Wel')
// Multiple elements text
await expect(page.locator('li')).toHaveText(['Item 1', 'Item 2', 'Item 3'])
// Input value
await expect(page.getByLabel('Email')).toHaveValue('user@example.com')
await expect(page.getByLabel('Email')).toHaveValue(/user@/)
// Count
await expect(page.locator('li')).toHaveCount(5)
// Attribute
await expect(page.getByRole('button')).toHaveAttribute('disabled', '')
await expect(page.locator('img')).toHaveAttribute('src', /logo\.png/)
// CSS class
await expect(page.getByRole('button')).toHaveClass('btn-primary')
await expect(page.getByRole('button')).toHaveClass(/btn-/)
// CSS property
await expect(page.locator('.box')).toHaveCSS('color', 'rgb(255, 0, 0)')
// Has ID
await expect(page.locator('div')).toHaveId('container')
Page Assertions
await expect(page).toHaveURL('https://example.com')
await expect(page).toHaveURL(/example\.com/)
await expect(page).toHaveTitle('Home Page')
await expect(page).toHaveTitle(/Home/)
await expect(page).toHaveScreenshot()
await expect(page.locator('.hero')).toHaveScreenshot('hero.png')
await expect(page).toHaveURL('https://example.com')
await expect(page).toHaveURL(/example\.com/)
await expect(page).toHaveTitle('Home Page')
await expect(page).toHaveTitle(/Home/)
await expect(page).toHaveScreenshot()
await expect(page.locator('.hero')).toHaveScreenshot('hero.png')
expect(page).to_have_url("https://example.com")
expect(page).to_have_url(re.compile(r"example\.com"))
expect(page).to_have_title("Home Page")
expect(page).to_have_title(re.compile("Home"))
expect(page).to_have_screenshot()
expect(page.locator(".hero")).to_have_screenshot("hero.png")
await Expect(Page).ToHaveURLAsync("https://example.com");
await Expect(Page).ToHaveURLAsync(new Regex("example\\.com"));
await Expect(Page).ToHaveTitleAsync("Home Page");
await Expect(Page).ToHaveTitleAsync(new Regex("Home"));
await Expect(Page).ToHaveScreenshotAsync();
await Expect(Page.Locator(".hero")).ToHaveScreenshotAsync("hero.png");
assertThat(page).hasURL("https://example.com");
assertThat(page).hasURL(Pattern.compile("example\\.com"));
assertThat(page).hasTitle("Home Page");
assertThat(page).hasTitle(Pattern.compile("Home"));
assertThat(page).hasScreenshot();
assertThat(page.locator(".hero")).hasScreenshot("hero.png");
// URL
await expect(page).toHaveURL('https://example.com')
await expect(page).toHaveURL(/example\.com/)
// Title
await expect(page).toHaveTitle('Home Page')
await expect(page).toHaveTitle(/Home/)
// Screenshot comparison(visual regression)
await expect(page).toHaveScreenshot()
await expect(page.locator('.hero')).toHaveScreenshot('hero-section.png')
Custom Timeouts & Soft Assertions
// Custom timeout
await expect(page.getByRole('button')).toBeVisible({ timeout: 10000 })
// Soft assertions(continue on failure)
await expect.soft(page.locator('.title')).toHaveText('Welcome')
await expect.soft(page.locator('.subtitle')).toBeVisible()
// Test continues even if above fail
// Poll until condition
await expect.poll(async () => {
const response = await page.request.get('https://api.example.com/status')
return response.status()
}).toBe(200)
// Negative assertions
await expect(page.locator('.error')).not.toBeVisible()
await expect(page.locator('button')).not.toBeDisabled()
JavaScript Assertions
// For non-locator values
const text = await page.locator('.title').textContent()
expect(text).toBe('Welcome')
expect(text).toContain('Wel')
const count = await page.locator('li').count()
expect(count).toBeGreaterThan(5)
expect(count).toBeLessThanOrEqual(10)
const isVisible = await page.locator('button').isVisible()
expect(isVisible).toBeTruthy()
// Array assertions
const items = await page.locator('li').allTextContents()
expect(items).toHaveLength(3)
expect(items).toContain('Item 1')
expect(items).toEqual(['Item 1', 'Item 2', 'Item 3'])
Test Framework Structure
IntermediateBasic Test Structure
import { test, expect } from '@playwright/test'
test('basic test', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
})
test.describe('Login Tests', () => {
test('should login', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Login' }).click()
await expect(page).toHaveURL('/dashboard')
})
})
import { test, expect } from '@playwright/test'
test('basic test', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
})
test.describe('Login Tests', () => {
test('should login', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Login' }).click()
await expect(page).toHaveURL('/dashboard')
})
})
import pytest
from playwright.sync_api import Page, expect
def test_basic(page: Page):
page.goto("https://example.com")
expect(page).to_have_title(re.compile("Example"))
class TestLogin:
def test_should_login(self, page: Page):
page.goto("/login")
page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("password123")
page.get_by_role("button", name="Login").click()
expect(page).to_have_url("/dashboard")
using Microsoft.Playwright.NUnit;
using static Microsoft.Playwright.Assertions;
[TestFixture]
public class Tests : PageTest
{
[Test]
public async Task BasicTest()
{
await Page.GotoAsync("https://example.com");
await Expect(Page).ToHaveTitleAsync(new Regex("Example"));
}
[Test]
public async Task ShouldLogin()
{
await Page.GotoAsync("/login");
await Page.GetByLabel("Email").FillAsync("user@example.com");
await Page.GetByLabel("Password").FillAsync("password123");
await Page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
await Expect(Page).ToHaveURLAsync("/dashboard");
}
}
import com.microsoft.playwright.*;
import org.junit.jupiter.api.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;
public class TestExample {
@Test
void basicTest() {
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().launch();
Page page = browser.newPage();
page.navigate("https://example.com");
assertThat(page).hasTitle(Pattern.compile("Example"));
}
}
@Test
void shouldLogin() {
page.navigate("/login");
page.getByLabel("Email").fill("user@example.com");
page.getByLabel("Password").fill("password123");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Login")).click();
assertThat(page).hasURL("/dashboard");
}
}
import { test, expect } from '@playwright/test'
test('basic test', async ({ page }) => {
await page.goto('https://example.com')
await expect(page).toHaveTitle(/Example/)
})
// Test with description
test.describe('Login Tests', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Login' }).click()
await expect(page).toHaveURL('/dashboard')
})
})
// Multiple tests
test.describe('Shopping Cart', () => {
test('add item to cart', async ({ page }) => {
// Test code
})
test('remove item from cart', async ({ page }) => {
// Test code
})
test('checkout process', async ({ page }) => {
// Test code
})
})
Hooks & Setup
test.describe('Test Suite', () => {
// Runs once before all tests in describe
test.beforeAll(async ({ browser }) => {
console.log('Setup before all tests')
})
// Runs before each test
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com')
// Login or common setup
})
// Runs after each test
test.afterEach(async ({ page }) => {
// Cleanup after each test
await page.close()
})
// Runs once after all tests
test.afterAll(async ({ browser }) => {
console.log('Cleanup after all tests')
})
test('test 1', async ({ page }) => {
// Test code
})
test('test 2', async ({ page }) => {
// Test code
})
})
Test Annotations
// Run only this test
test.only('important test', async ({ page }) => {
// Only this test will run
})
// Skip test
test.skip('broken test', async ({ page }) => {
// This test will be skipped
})
// Conditional skip
test('mobile test', async ({ page, isMobile }) => {
test.skip(!isMobile, 'Test only for mobile')
// Test code
})
// Mark as failing(known issue)
test.fail('known bug', async ({ page }) => {
// Test is expected to fail
})
// Slow test (3x timeout)
test.slow('long running test', async ({ page }) => {
// This test gets 3x default timeout
})
// Fixme(will be skipped in CI)
test.fixme('needs fixing', async ({ page }) => {
// Skipped in CI, runs locally
})
// Tags
test('payment test @smoke @payment', async ({ page }) => {
// Can filter by tags: npx playwright test --grep @smoke
})
Fixtures
Fixtures provide test isolation and encapsulate setup/teardown logic. They're the recommended way to share state between tests.
// fixtures.ts
import { test as base } from '@playwright/test'
type MyFixtures = {
authenticatedPage: Page
apiClient: APIClient
}
export const test = base.extend<MyFixtures>({
authenticatedPage: async ({ page }, use) => {
// Setup
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Login' }).click()
// Use fixture
await use(page)
// Teardown
await page.goto('/logout')
},
apiClient: async ({ request }, use) => {
const client = new APIClient(request)
await use(client)
}
})
export { expect } from '@playwright/test'
// Use in test
import { test, expect } from './fixtures'
test('user dashboard', async ({ authenticatedPage }) => {
await expect(authenticatedPage).toHaveURL('/dashboard')
})
Parallel Execution
// In playwright.config.ts
export default defineConfig({
workers: 4, // Number of parallel workers
fullyParallel: true, // Run tests in parallel
})
// Serial execution for specific describe
test.describe.serial('checkout flow', () => {
test('step 1', async ({ page }) => {})
test('step 2', async ({ page }) => {})
// These run in order
})
// Configure in test file
test.describe.configure({ mode: 'parallel' })
test.describe.configure({ mode: 'serial' })
// Limit workers for specific describe
test.describe.configure({ workers: 1 })
Retries & Timeouts
// In playwright.config.ts
export default defineConfig({
timeout: 30000, // 30 seconds per test
retries: 2, // Retry failed tests twice
expect: {
timeout: 5000 // 5 seconds for assertions
}
})
// In test file
test.describe('Flaky tests', () => {
test.describe.configure({ retries: 3 })
test('might fail', async ({ page }) => {
test.setTimeout(60000) // 60 seconds for this test
// Test code
})
})
Configuration & Setup ESSENTIAL
IntermediatePlaywright Config File
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
// Test directory
testDir: './tests',
// Maximum time one test can run
timeout: 30000,
// Expect timeout
expect: {
timeout: 5000
},
// Run tests in parallel
fullyParallel: true,
// Fail build on CI if you accidentally left test.only
forbidOnly: !!process.env.CI,
// Retry failed tests
retries: process.env.CI ? 2 : 0,
// Number of parallel workers
workers: process.env.CI ? 1 : undefined,
// Reporter configuration
reporter: [
['html'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'results.xml' }],
['list']
],
// Shared settings for all projects
use: {
// Base URL
baseURL: 'http://localhost:3000',
// Browser context options
viewport: { width: 1280, height: 720 },
// Collect trace on first retry
trace: 'on-first-retry',
// Screenshot on failure
screenshot: 'only-on-failure',
// Video on failure
video: 'retain-on-failure',
// Default timeout for actions
actionTimeout: 10000,
// Default navigation timeout
navigationTimeout: 30000,
},
// Project configurations
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'] }
}
],
// Web server for local development
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000
}
})
Environment Variables
// .env file
BASE_URL=https://staging.example.com
API_TOKEN=your_api_token_here
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=password123
// Usage in tests
import * as dotenv from 'dotenv'
dotenv.config()
test('use environment variables', async ({ page }) => {
await page.goto(process.env.BASE_URL)
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL)
})
// In playwright.config.ts
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000'
}
Global Setup & 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()
// Login once and save authentication state
await page.goto('https://example.com/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Login' }).click()
// Save storage state
await page.context().storageState({
path: 'auth.json'
})
await browser.close()
}
export default globalSetup
// In playwright.config.ts
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
use: {
storageState: 'auth.json' // Use saved auth state
}
})
Network Handling & Mocking
IntermediateNetwork Interception
// Listen to all requests
page.on('request', request => {
console.log('>>', request.method(), request.url())
})
// Listen to all responses
page.on('response', response => {
console.log('<<', response.status(), response.url())
})
// Wait for specific request
const request = await page.waitForRequest('**/api/data')
const request = await page.waitForRequest(
request => request.url().includes('api') && request.method() === 'POST'
)
// Wait for specific response
const response = await page.waitForResponse('**/api/data')
const response = await page.waitForResponse(
response => response.url().includes('api') && response.status() === 200
)
// Wait for navigation and request together
await Promise.all([
page.waitForNavigation(),
page.waitForRequest('**/api/user'),
page.getByRole('button').click()
])
Mock API Responses
// Mock API response
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
})
})
// Mock with custom headers
await page.route('**/api/data', route => {
route.fulfill({
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({ success: true })
})
})
// Abort specific requests
await page.route('**/*.{png,jpg,jpeg}', route => route.abort())
await page.route('**/analytics/**', route => route.abort())
// Modify request before sending
await page.route('**/api/submit', async route => {
const request = route.request()
await route.continue({
headers: {
...request.headers(),
'Authorization': 'Bearer token123'
}
})
})
// Mock from file
await page.route('**/api/users', async route => {
await route.fulfill({
path: './mocks/users.json'
})
})
HAR Files
// Record HAR file
const context = await browser.newContext({
recordHar: {
path: 'network.har',
content: 'embed' // Include response bodies
}
})
const page = await context.newPage()
await page.goto('https://example.com')
await context.close()
// Replay from HAR
await page.routeFromHAR('network.har', {
url: '**/api/**',
update: false // Use recorded responses
})
// Update HAR(record missing requests)
await page.routeFromHAR('network.har', {
update: true,
updateContent: 'embed',
updateMode: 'minimal' // Only update missing
})
Request/Response Analysis
// Get request details
const response = await page.goto('https://example.com')
console.log(response.status())
console.log(response.headers())
console.log(await response.text())
console.log(await response.json())
// Analyze POST data
page.on('request', request => {
if (request.method() === 'POST') {
console.log(request.postData())
console.log(request.postDataJSON())
}
})
// Check specific header
const response = await page.goto('https://example.com')
const contentType = response.headers()['content-type']
expect(contentType).toContain('application/json')
// Get all network events
const requests = []
page.on('request', request => requests.push(request))
await page.goto('https://example.com')
console.log(`Total requests: ${requests.length}`)
Authentication Patterns
IntermediateStorage State (Recommended)
Save authentication state once and reuse across tests. Most efficient method.
// auth.setup.ts - Run once
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Login' }).click()
// Wait for auth to complete
await page.waitForURL('/dashboard')
// Save storage state
await page.context().storageState({
path: 'playwright/.auth/user.json'
})
})
// playwright.config.ts
export default defineConfig({
projects: [
// Setup project
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
// Test project that depends on setup
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json'
},
dependencies: ['setup']
}
]
})
// Now all tests start authenticated!
test('user dashboard', async ({ page }) => {
await page.goto('/dashboard')
// Already logged in!
})
Cookie-Based Authentication
// Add cookies
await context.addCookies([
{
name: 'session_token',
value: 'abc123',
domain: 'example.com',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Lax'
}
])
// Get cookies
const cookies = await context.cookies()
const sessionCookie = cookies.find(c => c.name === 'session_token')
// Clear cookies
await context.clearCookies()
// Clear specific cookies
await context.clearCookies({
name: 'session_token',
domain: 'example.com'
})
Token-Based Authentication
// Add auth header to all requests
const context = await browser.newContext({
extraHTTPHeaders: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
})
// Or set dynamically
await page.setExtraHTTPHeaders({
'Authorization': `Bearer ${token}`
})
// Store token in localStorage
await page.addInitScript(token => {
localStorage.setItem('auth_token', token)
}, 'your-token-here')
// Get token from API first
test('with API token', async ({ page, request }) => {
// Get token from API
const response = await request.post('/api/login', {
data: {
email: 'user@example.com',
password: 'password'
}
})
const { token } = await response.json()
// Use token in page
await page.setExtraHTTPHeaders({
'Authorization': `Bearer ${token}`
})
await page.goto('/dashboard')
})
HTTP Basic Authentication
// Set credentials for context
const context = await browser.newContext({
httpCredentials: {
username: 'admin',
password: 'secret123'
}
})
// Or in config
use: {
httpCredentials: {
username: process.env.BASIC_AUTH_USER,
password: process.env.BASIC_AUTH_PASSWORD
}
}
Multi-User Authentication
// Create fixture for multiple users
import { test as base } from '@playwright/test'
type MyFixtures = {
adminPage: Page
userPage: Page
}
export const test = base.extend<MyFixtures>({
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'auth/admin.json'
})
const page = await context.newPage()
await use(page)
await context.close()
},
userPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'auth/user.json'
})
const page = await context.newPage()
await use(page)
await context.close()
}
})
// Use in test
test('admin and user interaction', async ({ adminPage, userPage }) => {
await adminPage.goto('/admin/dashboard')
await userPage.goto('/user/profile')
// Test interaction between admin and user
})
API Testing POWERFUL
IntermediateAPI Request Context
import { test, expect } from '@playwright/test'
test('API GET request', async ({ request }) => {
const response = await request.get('https://api.example.com/users')
expect(response.ok()).toBeTruthy()
expect(response.status()).toBe(200)
const data = await response.json()
expect(data).toHaveLength(10)
})
test('API POST request', async ({ request }) => {
const response = await request.post('https://api.example.com/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
})
expect(response.status()).toBe(201)
const user = await response.json()
expect(user.name).toBe('John Doe')
})
test('API PUT request', async ({ request }) => {
const response = await request.put('https://api.example.com/users/1', {
data: {
name: 'Jane Doe'
}
})
expect(response.status()).toBe(200)
})
test('API DELETE request', async ({ request }) => {
const response = await request.delete('https://api.example.com/users/1')
expect(response.status()).toBe(204)
})
API Headers & Authentication
// Create request context with default headers
const context = await request.newContext({
baseURL: 'https://api.example.com',
extraHTTPHeaders: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
}
})
// Use in test
test('authenticated API call', async ({ request }) => {
const response = await request.get('/protected-endpoint', {
headers: {
'Authorization': 'Bearer token123'
}
})
expect(response.ok()).toBeTruthy()
})
// Configure in playwright.config.ts
use: {
baseURL: 'https://api.example.com',
extraHTTPHeaders: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
}
Response Validation
test('validate response', async ({ request }) => {
const response = await request.get('/api/users/1')
// Status
expect(response.ok()).toBeTruthy()
expect(response.status()).toBe(200)
// Headers
expect(response.headers()['content-type']).toContain('application/json')
// Body
const data = await response.json()
expect(data).toHaveProperty('id')
expect(data).toHaveProperty('email')
expect(data.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
// Schema validation
expect(data).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.any(String)
})
})
API + UI Testing
test('create user via API, verify in UI', async ({ request, page }) => {
// Create user via API
const response = await request.post('/api/users', {
data: {
name: 'Test User',
email: 'test@example.com'
}
})
const user = await response.json()
// Verify in UI
await page.goto(`/users/${user.id}`)
await expect(page.getByText('Test User')).toBeVisible()
await expect(page.getByText('test@example.com')).toBeVisible()
})
test('setup data via API, test UI flow', async ({ request, page }) => {
// Setup test data via API
await request.post('/api/products', {
data: { name: 'Product 1', price: 99.99 }
})
// Test UI
await page.goto('/products')
await expect(page.getByText('Product 1')).toBeVisible()
await page.getByText('Product 1').click()
await page.getByRole('button', { name: 'Add to Cart' }).click()
// Verify via API
const cartResponse = await request.get('/api/cart')
const cart = await cartResponse.json()
expect(cart.items).toHaveLength(1)
})
Page Object Model (POM)
IntermediateBasic Page Object
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly page: Page
readonly emailInput: Locator
readonly passwordInput: Locator
readonly loginButton: Locator
readonly errorMessage: Locator
constructor(page: Page) {
this.page = page
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.loginButton = page.getByRole('button', { name: 'Login' })
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.loginButton.click()
}
async getErrorMessage() {
return await this.errorMessage.textContent()
}
}
// Use in test
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'
test('login test', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
Advanced Page Object
export class DashboardPage {
readonly page: Page
constructor(page: Page) {
this.page = page
}
// Locators as methods(lazy evaluation)
get welcomeMessage() {
return this.page.getByRole('heading', { name: 'Welcome' })
}
get profileLink() {
return this.page.getByRole('link', { name: 'Profile' })
}
getStatCard(title: string) {
return this.page.locator(`.stat-card:has-text("${title}")`)
}
// Actions
async goto() {
await this.page.goto('/dashboard')
await this.page.waitForLoadState('networkidle')
}
async goToProfile() {
await this.profileLink.click()
}
async getStatValue(title: string): Promise {
const card = this.getStatCard(title)
return await card.locator('.value').textContent() || ''
}
// Assertions
async expectToBeVisible() {
await expect(this.welcomeMessage).toBeVisible()
}
}
Component-Based POM
// components/NavigationBar.ts
export class NavigationBar {
readonly page: Page
constructor(page: Page) {
this.page = page
}
get homeLink() {
return this.page.getByRole('link', { name: 'Home' })
}
get profileMenu() {
return this.page.getByRole('button', { name: 'Profile' })
}
async navigateToHome() {
await this.homeLink.click()
}
async logout() {
await this.profileMenu.click()
await this.page.getByRole('menuitem', { name: 'Logout' }).click()
}
}
// pages/BasePage.ts
export class BasePage {
readonly page: Page
readonly navigation: NavigationBar
constructor(page: Page) {
this.page = page
this.navigation = new NavigationBar(page)
}
}
// pages/ProfilePage.ts extends BasePage
export class ProfilePage extends BasePage {
get nameInput() {
return this.page.getByLabel('Name')
}
async updateName(name: string) {
await this.nameInput.fill(name)
await this.page.getByRole('button', { name: 'Save' }).click()
}
}
// Use in test
test('update profile', async ({ page }) => {
const profile = new ProfilePage(page)
await profile.page.goto('/profile')
await profile.updateName('John Doe')
await profile.navigation.logout()
})
Page Object with Fixtures
// fixtures/pages.ts
import { test as base } from '@playwright/test'
import { LoginPage } from '../pages/LoginPage'
import { DashboardPage } from '../pages/DashboardPage'
type Pages = {
loginPage: LoginPage
dashboardPage: DashboardPage
}
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page))
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page))
}
})
export { expect } from '@playwright/test'
// Use in test - much cleaner!
import { test, expect } from './fixtures/pages'
test('login flow', async ({ loginPage, dashboardPage }) => {
await loginPage.goto()
await loginPage.login('user@example.com', 'password')
await dashboardPage.expectToBeVisible()
})
Mobile & Responsive Testing
IntermediateDevice Emulation
import { devices } from '@playwright/test'
// Use predefined device
const iPhone = devices['iPhone 13']
const context = await browser.newContext({
...iPhone
})
// Common devices
devices['Desktop Chrome']
devices['Desktop Firefox']
devices['Desktop Safari']
devices['iPhone 13']
devices['iPhone 13 Pro']
devices['iPhone 13 Pro Max']
devices['iPhone 14']
devices['iPhone 14 Pro']
devices['iPhone 15']
devices['iPhone 15 Pro']
devices['Pixel 5']
devices['Pixel 7']
devices['Galaxy S9+']
devices['Galaxy S20']
devices['iPad Pro']
devices['iPad Mini']
// Custom device
const context = await browser.newContext({
viewport: { width: 375, height: 667 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)',
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true
})
Viewport Testing
// Set viewport
await page.setViewportSize({ width: 1280, height: 720 })
// Test responsive design
test('responsive design', async ({ page }) => {
await page.goto('https://example.com')
// Desktop
await page.setViewportSize({ width: 1920, height: 1080 })
await expect(page.locator('.desktop-menu')).toBeVisible()
await expect(page.locator('.mobile-menu')).toBeHidden()
// Tablet
await page.setViewportSize({ width: 768, height: 1024 })
await expect(page.locator('.tablet-menu')).toBeVisible()
// Mobile
await page.setViewportSize({ width: 375, height: 667 })
await expect(page.locator('.mobile-menu')).toBeVisible()
await expect(page.locator('.desktop-menu')).toBeHidden()
})
// Test multiple viewports
const viewports = [
{ width: 1920, height: 1080, name: 'Desktop' },
{ width: 1366, height: 768, name: 'Laptop' },
{ width: 768, height: 1024, name: 'Tablet' },
{ width: 375, height: 667, name: 'Mobile' }
]
for (const viewport of viewports) {
test(`test on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport)
await page.goto('/')
// Your tests
})
}
Touch Gestures
// Tap
await page.locator('button').tap()
// Tap with position
await page.locator('.element').tap({ position: { x: 10, y: 10 } })
// Swipe gesture
await page.locator('.carousel').evaluate(element => {
element.scrollLeft += 300
})
// Pinch zoom(touchscreen)
await page.touchscreen.tap(100, 100)
// Long press
await page.locator('button').tap({ timeout: 1000 })
// Geolocation
const context = await browser.newContext({
geolocation: { latitude: 59.95, longitude: 30.31667 },
permissions: ['geolocation']
})
// Change geolocation
await context.setGeolocation({ latitude: 37.7749, longitude: -122.4194 })
Mobile-Specific Testing
// In playwright.config.ts
projects: [
{
name: 'Mobile Safari',
use: {
...devices['iPhone 13'],
locale: 'en-US',
timezoneId: 'America/New_York',
permissions: ['geolocation'],
geolocation: { latitude: 40.7128, longitude: -74.0060 }
}
},
{
name: 'Mobile Chrome',
use: {
...devices['Pixel 5'],
viewport: { width: 393, height: 851 }
}
}
]
// Conditional tests
test('mobile only feature', async ({ page, isMobile }) => {
test.skip(!isMobile, 'Mobile only test')
await page.goto('/')
await expect(page.locator('.mobile-feature')).toBeVisible()
})
Debugging & Tracing ESSENTIAL
IntermediateDebug Mode
// Debug from command line
npx playwright test --debug
npx playwright test --debug specific-test.spec.ts
npx playwright test --debug --headed
// Debug in code
await page.pause() // Pauses execution, opens inspector
// Debug with headed browser
test('debug test', async ({ page }) => {
await page.goto('/')
await page.pause() // Inspector opens here
await page.getByRole('button').click()
})
// Run with specific browser
npx playwright test --debug --project=chromium
Tracing
// Configure in playwright.config.ts
use: {
trace: 'on-first-retry', // 'on-first-retry', 'on', 'off', 'retain-on-failure'
video: 'retain-on-failure',
screenshot: 'only-on-failure'
}
// Manual trace
await context.tracing.start({
screenshots: true,
snapshots: true,
sources: true
})
await page.goto('https://example.com')
await page.getByRole('button').click()
await context.tracing.stop({ path: 'trace.zip' })
// View trace
npx playwright show-trace trace.zip
// In test
test('with tracing', async ({ page }) => {
await page.context().tracing.start({
screenshots: true,
snapshots: true
})
// Your test actions
await page.goto('/')
await page.context().tracing.stop({
path: 'traces/test-trace.zip'
})
})
Screenshots & Videos
// Screenshot
await page.screenshot({ path: 'screenshot.png' })
await page.screenshot({ path: 'screenshot.png', fullPage: true })
// Screenshot of element
await page.locator('.hero').screenshot({ path: 'hero.png' })
// Screenshot to buffer
const buffer = await page.screenshot()
// With options
await page.screenshot({
path: 'screenshot.png',
fullPage: true,
clip: { x: 0, y: 0, width: 800, height: 600 },
quality: 100, // For JPEG
type: 'png' // 'png' or 'jpeg'
})
// Videos(automatic)
const context = await browser.newContext({
recordVideo: {
dir: 'videos/',
size: { width: 1280, height: 720 }
}
})
// Or in config
use: {
video: 'on', // 'on', 'off', 'retain-on-failure', 'on-first-retry'
}
Console & Network Logs
// Console logs
page.on('console', msg => {
console.log(`Browser console [${msg.type()}]:`, msg.text())
})
// Errors
page.on('pageerror', error => {
console.log('Page error:', error)
})
// Request failures
page.on('requestfailed', request => {
console.log('Request failed:', request.url(), request.failure())
})
// Dialog events
page.on('dialog', dialog => {
console.log('Dialog:', dialog.message())
dialog.accept()
})
// Download events
page.on('download', download => {
console.log('Downloaded:', download.suggestedFilename())
})
// Verbose logging
DEBUG=pw:api npx playwright test
Slow Motion & Wait
// Launch with slow motion
const browser = await chromium.launch({
headless: false,
slowMo: 1000 // 1 second delay between actions
})
// Wait for timeout
await page.waitForTimeout(3000)
// Wait for console message
await page.waitForEvent('console', msg => msg.text().includes('loaded'))
// Wait for function
await page.waitForFunction(() => {
return document.querySelector('.loading') === null
})
UI Mode
// Run in UI mode
npx playwright test --ui
// Features:
// - Watch mode with live reloading
// - Time travel debugging
// - Pick locator tool
// - View traces inline
// - Edit tests and see results
// - Filter and search tests
CI/CD Integration
AdvancedGitHub Actions
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@v4
- uses: actions/setup-node@v4
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@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
path: test-results/
retention-days: 30
Docker Integration
# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "">test"]
# Build and run
docker build -t playwright-tests .
docker run --rm playwright-tests
# With docker-compose
version: '3.8'
services:
playwright:
image: mcr.microsoft.com/playwright:v1.40.0-jammy
working_dir: /app
volumes:
- ./:/app
command: npx playwright test
Jenkins Pipeline
pipeline {
agent {
docker {
image 'mcr.microsoft.com/playwright:v1.40.0-jammy'
}
}
stages {
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Test') {
steps {
sh 'npx playwright test'
}
}
}
post {
always {
publishHTML([
reportDir: 'playwright-report',
reportFiles: 'index.html',
reportName: 'Playwright Test Report'
])
junit 'test-results/junit.xml'
}
}
}
GitLab CI
image: mcr.microsoft.com/playwright:v1.40.0-jammy
stages:
- test
playwright:
stage: test
script:
- npm ci
- npx playwright install --with-deps
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 1 week
cache:
paths:
- node_modules/
Sharding for Parallel Execution
# GitHub Actions with sharding
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
# Manual sharding
npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4
# Merge reports
npx playwright merge-reports --reporter html ./all-blob-reports
Best Practices & Patterns MUST READ
AdvancedLocator Strategies
โข Use role-based locators:
getByRole('button', { name: 'Submit' })โข Use test IDs for stable automation:
getByTestId('submit-btn')โข Use semantic selectors:
getByLabel('Email')โข Chain locators for specificity:
page.locator('form').getByRole('button')โ DON'T:
โข Rely on CSS classes:
.btn-primary-dark-themeโข Use nth selectors:
button:nth-child(3)โข Use XPath unless absolutely necessary
โข Use overly specific selectors:
div > div > div > button
Auto-Waiting Best Practices
// โ
GOOD - Auto-waits for element
await page.getByRole('button').click()
// โ BAD - Unnecessary explicit wait
await page.waitForSelector('button')
await page.click('button')
// โ
GOOD - Wait for specific state
await page.getByRole('button').waitFor({ state: 'visible' })
// โ
GOOD - Wait for network
await Promise.all([
page.waitForResponse('**/api/data'),
page.getByRole('button').click()
])
// โ BAD - Arbitrary timeout
await page.waitForTimeout(3000) // Flaky!
Test Independence
โข Tests should not depend on execution order
โข Clean up test data after each test
โข Don't share state between tests
โข Use fixtures for common setup
โข Reset to known state before each test
// โ BAD - Tests depend on each other
test('create user', async ({ page }) => {
// Creates user with ID 1
})
test('update user', async ({ page }) => {
// Assumes user ID 1 exists
})
// โ
GOOD - Each test is independent
test('update user', async ({ page, request }) => {
// Create test data
const user = await request.post('/api/users', {
data: { name: 'Test User' }
})
// Test update
await page.goto(`/users/${user.id}`)
// ... test logic
// Cleanup
await request.delete(`/api/users/${user.id}`)
})
Test Data Management
// Test data in separate file
// testData.ts
export const users = {
admin: {
email: 'admin@example.com',
password: 'admin123'
},
regular: {
email: 'user@example.com',
password: 'user123'
}
}
// Use in test
import { users } from './testData'
test('login as admin', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill(users.admin.email)
await page.getByLabel('Password').fill(users.admin.password)
})
// Dynamic test data
const generateUser = () => ({
email: `user_${Date.now()}@example.com`,
name: `Test User ${Date.now()}`,
password: 'Test123!'
})
Error Handling
// Soft assertions for multiple checks
test('multiple validations', async ({ page }) => {
await expect.soft(page.locator('.title')).toHaveText('Welcome')
await expect.soft(page.locator('.subtitle')).toBeVisible()
await expect.soft(page.locator('.footer')).toContainText('2024')
// All assertions run even if some fail
})
// Try-catch for expected failures
test('handle optional element', async ({ page }) => {
await page.goto('/')
try {
await page.getByRole('button', { name: 'Accept Cookies' })
.click({ timeout: 5000 })
} catch {
console.log('No cookie banner found')
}
// Continue with test
})
// Custom error messages
await expect(page.locator('.price'), 'Price should be $99.99')
.toHaveText('$99.99')
Performance Optimization
โข Use API calls for test setup instead of UI
โข Reuse authentication state across tests
โข Run tests in parallel
โข Block unnecessary resources (ads, analytics)
โข Use appropriate wait strategies
โข Cache test data when possible
// Block unnecessary resources
await page.route('**/*', route => {
const url = route.request().url()
if (url.includes('google-analytics') ||
url.includes('facebook') ||
url.endsWith('.jpg') ||
url.endsWith('.png')) {
route.abort()
} else {
route.continue()
}
})
// Setup via API(faster)
test('check dashboard', async ({ page, request }) => {
// โ
Fast - Create data via API
await request.post('/api/orders', { data: testOrder })
// โ Slow - Would navigate through UI to create order
// await page.goto('/orders/new')
// await page.fill(...)
await page.goto('/dashboard')
await expect(page.getByText('1 order')).toBeVisible()
})
Common Issues & Solutions
IntermediateFlaky Tests
// โ FLAKY - Race condition
await page.click('button')
await page.click('input') // Might click before button action completes
// โ
FIXED - Wait for stability
await page.click('button')
await page.waitForLoadState('networkidle')
await page.click('input')
// โ FLAKY - Timing dependent
await page.goto('/')
await page.click('.dynamic-element') // Might not be loaded yet
// โ
FIXED - Wait for element
await page.goto('/')
await page.waitForSelector('.dynamic-element')
await page.click('.dynamic-element')
// โ
BETTER - Auto-waiting with locator
await page.goto('/')
await page.locator('.dynamic-element').click() // Auto-waits
// โ FLAKY - Arbitrary timeout
await page.waitForTimeout(5000)
// โ
FIXED - Wait for specific condition
await page.waitForResponse('**/api/data')
await page.waitForSelector('.data-loaded')
Timeouts
// Increase timeout for slow operations
await page.goto('/', { timeout: 60000 })
await page.getByRole('button').click({ timeout: 10000 })
// Set default timeout
page.setDefaultTimeout(30000)
// Action-specific timeout
await expect(page.locator('.slow-element')).toBeVisible({
timeout: 15000
})
// In config
export default defineConfig({
timeout: 60000,
expect: { timeout: 10000 },
use: {
actionTimeout: 15000,
navigationTimeout: 30000
}
})
Element Not Found
// Check if element exists
const count = await page.locator('button').count()
if (count > 0) {
await page.locator('button').click()
}
// Wait for element to be attached
await page.locator('button').waitFor({ state: 'attached' })
// Use soft assertion to continue on failure
await expect.soft(page.locator('.optional')).toBeVisible()
// Debug with pause
await page.pause() // Opens inspector
// Check page content
console.log(await page.content())
// Get all matching elements
const elements = await page.locator('button').all()
console.log(`Found ${elements.length} buttons`)
Shadow DOM & iframes
// Shadow DOM(automatic in Playwright!)
await page.locator('button').click() // Works even in shadow DOM
// iframe
const frame = page.frameLocator('iframe[name="myframe"]')
await frame.locator('button').click()
// Or by URL
const frame = page.frameLocator('iframe[src*="example.com"]')
// Nested iframes
const parent = page.frameLocator('iframe.parent')
const child = parent.frameLocator('iframe.child')
await child.locator('button').click()
// Get frame by name
const frame = page.frame({ name: 'myframe' })
await frame.locator('button').click()
Network Errors
// Retry on network error
test('handle network errors', async ({ page }) => {
let retries = 3
while (retries > 0) {
try {
await page.goto('https://example.com')
break
} catch(error) {
retries--
if (retries === 0) throw error
await page.waitForTimeout(1000)
}
}
})
// Wait for network to be idle
await page.goto('/', { waitUntil: 'networkidle' })
// Continue on navigation failure
try {
await page.goto('/', { timeout: 10000 })
} catch {
console.log('Page load timeout, continuing...')
}
Advanced Techniques PRO
AdvancedWeb Workers & Service Workers
// Listen for workers
page.on('worker', worker => {
console.log('Worker created:', worker.url())
worker.on('close', () => console.log('Worker destroyed'))
})
// Get service workers
const serviceWorkers = await context.serviceWorkers()
console.log('Active service workers:', serviceWorkers.length)
Browser Context Cookies & Storage
// Set localStorage before page load
await context.addInitScript(() => {
localStorage.setItem('theme', 'dark')
localStorage.setItem('lang', 'en')
})
// Get localStorage
const theme = await page.evaluate(() => localStorage.getItem('theme'))
// Clear all storage
await context.clearCookies()
await page.evaluate(() => {
localStorage.clear()
sessionStorage.clear()
})
// IndexedDB
await page.evaluate(() => {
return new Promise(resolve => {
const request = indexedDB.open('myDB')
request.onsuccess = () => resolve('DB opened')
})
})
Custom Reporters
// custom-reporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter'
class MyReporter implements Reporter {
onBegin(config, suite) {
console.log(`Starting test run with ${suite.allTests().length} tests`)
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`Finished ${test.title}: ${result.status}`)
}
onEnd(result) {
console.log(`Finished test run: ${result.status}`)
}
}
export default MyReporter
// In playwright.config.ts
reporter: [
['./custom-reporter.ts'],
['html']
]
Visual Regression Testing
// Basic screenshot comparison
test('visual regression', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveScreenshot()
})
// With options
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
threshold: 0.2
})
// Element screenshot
await expect(page.locator('.hero')).toHaveScreenshot('hero.png')
// Full page
await expect(page).toHaveScreenshot('full-page.png', {
fullPage: true
})
// Mask dynamic content
await expect(page).toHaveScreenshot({
mask: [page.locator('.dynamic-content')]
})
// Update snapshots
npx playwright test --update-snapshots
Browser Contexts for Isolation
test('multi-user collaboration', async ({ browser }) => {
// User 1
const context1 = await browser.newContext()
const page1 = await context1.newPage()
await page1.goto('/login')
// ... login as user1
// User 2
const context2 = await browser.newContext()
const page2 = await context2.newPage()
await page2.goto('/login')
// ... login as user2
// Test collaboration
await page1.getByRole('button', { name: 'Send Message' }).click()
await expect(page2.getByText('New message')).toBeVisible()
await context1.close()
await context2.close()
})
Performance Metrics
// Get performance metrics
test('page performance', async ({ page }) => {
await page.goto('/')
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0]
return {
domContentLoaded: navigation.domContentLoadedEventEnd,
loadComplete: navigation.loadEventEnd,
firstPaint: performance.getEntriesByType('paint')[0]?.startTime,
totalSize: performance.getEntriesByType('resource')
.reduce((sum, r) => sum + r.transferSize, 0)
}
})
console.log('Performance metrics:', metrics)
expect(metrics.loadComplete).toBeLessThan(3000)
})
// Core Web Vitals
const vitals = await page.evaluate(() => ({
lcp: performance.getEntriesByType('largest-contentful-paint')[0],
fid: performance.getEntriesByType('first-input')[0],
cls: performance.getEntriesByType('layout-shift')
}))
Test Parameterization
// Test with multiple data sets
const testData = [
{ email: 'user1@example.com', expectedName: 'User 1' },
{ email: 'user2@example.com', expectedName: 'User 2' },
{ email: 'user3@example.com', expectedName: 'User 3' }
]
for (const data of testData) {
test(`login as ${data.email}`, async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill(data.email)
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Login' }).click()
await expect(page.getByText(data.expectedName)).toBeVisible()
})
}
// With test.describe
testData.forEach(data => {
test.describe(`Testing ${data.email}`, () => {
test('can login', async ({ page }) => {
// Test code
})
test('can logout', async ({ page }) => {
// Test code
})
})
})
File Operations (Upload/Download)
IntermediateFile Uploads
// Single file upload
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf')
await page.getByLabel('Upload file').setInputFiles('document.pdf')
// Multiple files
await page.setInputFiles('input[type="file"]', [
'file1.pdf',
'file2.jpg',
'file3.docx'
])
// Upload from buffer
await page.setInputFiles('input[type="file"]', {
name: 'file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('File content here')
})
// Remove files
await page.setInputFiles('input[type="file"]', [])
// Upload and wait for processing
await page.setInputFiles('#file-input', 'large-file.zip')
await page.waitForResponse('**/upload/complete')
// Handle drag and drop file upload
const dataTransfer = await page.evaluateHandle(() => new DataTransfer())
await page.dispatchEvent('#drop-zone', 'drop', { dataTransfer })
File Downloads
// Wait for download
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download' }).click()
const download = await downloadPromise
// Get download info
console.log(download.suggestedFilename())
console.log(download.url())
// Save to specific path
await download.saveAs('/path/to/save/file.pdf')
// Get download as stream
const stream = await download.createReadStream()
// Delete downloaded file
await download.delete()
// Handle multiple downloads
test('download multiple files', async ({ page }) => {
const downloads = []
page.on('download', download => downloads.push(download))
await page.getByRole('button', { name: 'Download All' }).click()
// Wait for all downloads
await page.waitForTimeout(2000)
for (const download of downloads) {
console.log('Downloaded:', await download.suggestedFilename())
await download.saveAs(`downloads/${await download.suggestedFilename()}`)
}
})
// Download with authentication
const context = await browser.newContext({
acceptDownloads: true,
extraHTTPHeaders: {
'Authorization': 'Bearer token123'
}
})
// Verify download content
const download = await downloadPromise
const path = await download.path()
const content = await fs.readFile(path, 'utf-8')
expect(content).toContain('Expected text')
PDF Operations
// Generate PDF from page
await page.goto('https://example.com')
await page.pdf({
path: 'page.pdf',
format: 'A4',
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
})
// PDF with custom options
await page.pdf({
path: 'invoice.pdf',
format: 'Letter',
landscape: true,
scale: 0.8,
displayHeaderFooter: true,
headerTemplate: 'Header',
footerTemplate: 'Page class="pageNumber">'
})
// PDF with specific page ranges
await page.pdf({
path: 'partial.pdf',
pageRanges: '1-5, 8, 11-13'
})
// Open and test PDF content(requires pdf-parse)
import pdf from 'pdf-parse'
test('verify PDF content', async ({ page }) => {
const downloadPromise = page.waitForEvent('download')
await page.getByRole('link', { name: 'Download Report' }).click()
const download = await downloadPromise
const path = await download.path()
const dataBuffer = await fs.readFile(path)
const data = await pdf(dataBuffer)
expect(data.text).toContain('Annual Report 2024')
expect(data.numpages).toBe(10)
})
Read Files from Disk
import fs from 'fs'
import path from 'path'
// Read JSON test data
const testData = JSON.parse(
fs.readFileSync('testdata/users.json', 'utf-8')
)
// Read CSV
import { parse } from 'csv-parse/sync'
const csvContent = fs.readFileSync('testdata/products.csv', 'utf-8')
const products = parse(csvContent, { columns: true })
// Read and upload file
const fileBuffer = fs.readFileSync('files/document.pdf')
await page.setInputFiles('input[type="file"]', {
name: 'document.pdf',
mimeType: 'application/pdf',
buffer: fileBuffer
})
Advanced Browser Features
AdvancedPermissions
// Grant permissions at context creation
const context = await browser.newContext({
permissions: ['geolocation', 'notifications', 'camera', 'microphone']
})
// Available permissions:
// 'geolocation', 'midi', 'midi-sysex', 'notifications', 'camera',
// 'microphone', 'background-sync', 'ambient-light-sensor',
// 'accelerometer', 'gyroscope', 'magnetometer', 'accessibility-events',
// 'clipboard-read', 'clipboard-write', 'payment-handler'
// Grant permissions after context creation
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
// Grant for specific origin
await context.grantPermissions(['clipboard-read'], {
origin: 'https://example.com'
})
// Clear permissions
await context.clearPermissions()
// Test geolocation permission
test('geolocation', async ({ context, page }) => {
await context.grantPermissions(['geolocation'])
await context.setGeolocation({ latitude: 59.95, longitude: 30.31667 })
await page.goto('https://maps.google.com')
await page.getByRole('button', { name: 'My Location' }).click()
// Verify location on map
})
Clipboard Operations
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
// Copy text to clipboard(via keyboard)
await page.getByRole('textbox').fill('Text to copy')
await page.getByRole('textbox').press('Control+A')
await page.getByRole('textbox').press('Control+C')
// Read from clipboard
const clipboardText = await page.evaluate(async () => {
return await navigator.clipboard.readText()
})
expect(clipboardText).toBe('Text to copy')
// Write to clipboard
await page.evaluate(async (text) => {
await navigator.clipboard.writeText(text)
}, 'Clipboard content')
// Paste from clipboard
await page.getByRole('textbox').press('Control+V')
// Test copy button functionality
test('copy to clipboard', async ({ page, context }) => {
await context.grantPermissions(['clipboard-write', 'clipboard-read'])
await page.goto('/')
await page.getByRole('button', { name: 'Copy Code' }).click()
const clipboard = await page.evaluate(() =>
navigator.clipboard.readText()
)
expect(clipboard).toContain('const example = true')
})
Color Scheme & Themes
// Set color scheme
const context = await browser.newContext({
colorScheme: 'dark' // 'light', 'dark', or 'no-preference'
})
// Test both themes
for (const theme of ['light', 'dark']) {
test(`test in ${theme} mode`, async ({ browser }) => {
const context = await browser.newContext({
colorScheme: theme
})
const page = await context.newPage()
await page.goto('/')
if (theme === 'dark') {
await expect(page.locator('body')).toHaveCSS(
'background-color',
'rgb(0, 0, 0)'
)
}
await context.close()
})
}
// Change color scheme dynamically
await page.emulateMedia({ colorScheme: 'dark' })
// Reduced motion
await page.emulateMedia({ reducedMotion: 'reduce' })
// Media type
await page.emulateMedia({ media: 'print' }) // 'screen' or 'print'
Timezone & Locale
// Set timezone and locale
const context = await browser.newContext({
locale: 'de-DE',
timezoneId: 'Europe/Berlin'
})
// Common locales and timezones
// en-US, en-GB, de-DE, fr-FR, es-ES, ja-JP, zh-CN
// America/New_York, Europe/London, Asia/Tokyo, etc.
// Test date formatting
test('date localization', async ({ browser }) => {
const context = await browser.newContext({
locale: 'fr-FR',
timezoneId: 'Europe/Paris'
})
const page = await context.newPage()
await page.goto('/events')
// Verify date format is French
await expect(page.getByText(/\d{2}\/\d{2}\/\d{4}/)).toBeVisible()
})
// Test multiple locales
const locales = ['en-US', 'de-DE', 'ja-JP']
for (const locale of locales) {
test(`test in ${locale}`, async ({ browser }) => {
const context = await browser.newContext({ locale })
const page = await context.newPage()
await page.goto('/')
// Your tests
await context.close()
})
}
Offline Mode & Network Throttling
// Enable offline mode
await context.setOffline(true)
// Test offline functionality
test('offline mode', async ({ context, page }) => {
await page.goto('/')
// Go offline
await context.setOffline(true)
await page.reload()
await expect(page.getByText('You are offline')).toBeVisible()
// Go back online
await context.setOffline(false)
await page.reload()
await expect(page.getByText('You are offline')).toBeHidden()
})
// Note: Network throttling is not directly supported in Playwright
// Use route to simulate slow responses
await page.route('**/*', async route => {
await new Promise(resolve => setTimeout(resolve, 1000)) // 1s delay
await route.continue()
})
// Simulate slow images
await page.route('**/*.{png,jpg,jpeg}', async route => {
await new Promise(resolve => setTimeout(resolve, 2000))
await route.continue()
})
Browser Extensions
// Launch browser with extension
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
`--disable-extensions-except=/path/to/extension`,
`--load-extension=/path/to/extension`
]
})
// Get extension background page
const backgroundPage = context.backgroundPages()[0]
await backgroundPage.waitForLoadState()
// Test extension popup
test('test extension', async () => {
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
'--disable-extensions-except=./my-extension',
'--load-extension=./my-extension'
]
})
const page = await context.newPage()
await page.goto('https://example.com')
// Interact with extension
// ...
await context.close()
})
Multiple Windows & Tabs
AdvancedHandle New Windows/Tabs
// Wait for popup
const popupPromise = page.waitForEvent('popup')
await page.getByRole('button', { name: 'Open Popup' }).click()
const popup = await popupPromise
// Work with popup
await popup.waitForLoadState()
await expect(popup).toHaveTitle('Popup Window')
await popup.getByRole('button').click()
// Close popup
await popup.close()
// Handle multiple popups
test('multiple popups', async ({ page }) => {
const popups = []
page.on('popup', async popup => {
await popup.waitForLoadState()
popups.push(popup)
})
await page.getByRole('button', { name: 'Open Windows' }).click()
// Wait for all popups
await page.waitForTimeout(1000)
// Work with each popup
for (const popup of popups) {
console.log('Popup URL:', popup.url())
await popup.close()
}
})
// Get all pages/tabs
const pages = context.pages()
console.log(`Open pages: ${pages.length}`)
// Switch between tabs
const [page1] = context.pages()
const page2 = await context.newPage()
await page1.goto('https://example.com')
await page2.goto('https://google.com')
// Do something on page1
await page1.getByRole('button').click()
// Switch to page2
await page2.bringToFront()
await page2.getByRole('textbox').fill('search term')
Test Target="_blank" Links
// Click link that opens new tab
test('external link', async ({ page, context }) => {
await page.goto('/')
const newPagePromise = context.waitForEvent('page')
await page.getByRole('link', { name: 'Terms' }).click()
const newPage = await newPagePromise
await newPage.waitForLoadState()
await expect(newPage).toHaveURL(/terms/)
await expect(newPage).toHaveTitle(/Terms/)
await newPage.close()
})
// Alternative: modify link behavior
await page.addInitScript(() => {
// Remove target="_blank" from all links
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('a[target="_blank"]').forEach(a => {
a.removeAttribute('target')
})
})
})
// Or handle with keyboard modifier
await page.getByRole('link', { name: 'External' }).click({
modifiers: ['Control'] // Opens in new tab
})
Window Management
// Open new page/tab
const newPage = await context.newPage()
await newPage.goto('https://example.com')
// Get window size
const viewportSize = page.viewportSize()
console.log(viewportSize) // { width: 1280, height: 720 }
// Close all pages except one
const pages = context.pages()
for (let i = 1; i < pages.length; i++) {
await pages[i].close()
}
// Test multi-window workflow
test('multi-window workflow', async ({ context }) => {
// Open dashboard
const dashboard = await context.newPage()
await dashboard.goto('/dashboard')
// Open reports in new window
const reports = await context.newPage()
await reports.goto('/reports')
// Open settings in third window
const settings = await context.newPage()
await settings.goto('/settings')
// Change setting
await settings.getByRole('checkbox', { name: 'Email' }).check()
await settings.getByRole('button', { name: 'Save' }).click()
// Verify dashboard updated
await dashboard.reload()
await expect(dashboard.getByText('Email notifications: On')).toBeVisible()
// Cleanup
await reports.close()
await settings.close()
})
Cross-Window Communication
// Test postMessage between windows
test('cross-window messaging', async ({ context }) => {
const page1 = await context.newPage()
const page2 = await context.newPage()
await page1.goto('https://sender.example.com')
await page2.goto('https://receiver.example.com')
// Listen for message in page2
const messagePromise = page2.evaluate(() => {
return new Promise(resolve => {
window.addEventListener('message', event => {
resolve(event.data)
})
})
})
// Send message from page1
await page1.evaluate(() => {
const target = window.open('https://receiver.example.com')
target.postMessage({ type: 'greeting', text: 'Hello!' }, '*')
})
const message = await messagePromise
expect(message.text).toBe('Hello!')
})
Accessibility Testing A11Y
AdvancedARIA Roles & Labels
// Check ARIA roles
await expect(page.getByRole('button')).toBeVisible()
await expect(page.getByRole('navigation')).toBeVisible()
await expect(page.getByRole('main')).toBeVisible()
// Check ARIA labels
await expect(page.getByRole('button', { name: 'Submit' }))
.toHaveAttribute('aria-label', 'Submit form')
// Check accessible names
const button = page.getByRole('button', { name: 'Close' })
await expect(button).toBeVisible()
// Verify label associations
await expect(page.getByLabel('Email address')).toBeVisible()
// Check alt text on images
await expect(page.getByAltText('Company logo')).toBeVisible()
// Test form accessibility
test('form accessibility', async ({ page }) => {
await page.goto('/contact')
// Every input should have a label
const inputs = await page.locator('input').all()
for (const input of inputs) {
const id = await input.getAttribute('id')
const hasLabel = await page.locator(`label[for="${id}"]`).count()
expect(hasLabel).toBeGreaterThan(0)
}
})
Keyboard Navigation
// Tab navigation
test('keyboard navigation', async ({ page }) => {
await page.goto('/')
// Tab through all interactive elements
await page.keyboard.press('Tab')
await expect(page.locator(':focus')).toHaveAttribute('href', '/')
await page.keyboard.press('Tab')
await expect(page.locator(':focus')).toHaveRole('button')
// Enter to activate
await page.keyboard.press('Enter')
})
// Test focus trap in modal
test('modal focus trap', async ({ page }) => {
await page.goto('/')
await page.getByRole('button', { name: 'Open Modal' }).click()
const modal = page.getByRole('dialog')
await expect(modal).toBeVisible()
// Tab through modal elements
const firstButton = modal.getByRole('button').first()
const lastButton = modal.getByRole('button').last()
await firstButton.focus()
// Tab to last element
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
// Next tab should loop back to first
await page.keyboard.press('Tab')
await expect(firstButton).toBeFocused()
// Shift+Tab should go to last
await page.keyboard.press('Shift+Tab')
await expect(lastButton).toBeFocused()
})
// Test skip links
test('skip to main content', async ({ page }) => {
await page.goto('/')
// First tab should be skip link
await page.keyboard.press('Tab')
await expect(page.locator(':focus')).toHaveText('Skip to main content')
// Activate skip link
await page.keyboard.press('Enter')
await expect(page.locator('main')).toBeFocused()
})
Screen Reader Testing
// Check live regions
await expect(page.locator('[aria-live="polite"]')).toBeVisible()
await expect(page.locator('[role="status"]')).toHaveText('Loading...')
// Verify ARIA attributes
test('ARIA attributes', async ({ page }) => {
await page.goto('/dashboard')
// Check expanded state
const menu = page.getByRole('button', { name: 'Menu' })
await expect(menu).toHaveAttribute('aria-expanded', 'false')
await menu.click()
await expect(menu).toHaveAttribute('aria-expanded', 'true')
})
// Test dynamic content announcements
test('live region announcements', async ({ page }) => {
await page.goto('/')
const liveRegion = page.locator('[aria-live="assertive"]')
await page.getByRole('button', { name: 'Submit' }).click()
await expect(liveRegion).toHaveText('Form submitted successfully')
})
// Verify heading hierarchy
test('heading structure', async ({ page }) => {
await page.goto('/')
const h1Count = await page.locator('h1').count()
expect(h1Count).toBe(1) // Only one H1
// Check heading order
const headings = await page.locator('h1, h2, h3, h4, h5, h6').all()
let previousLevel = 0
for (const heading of headings) {
const tagName = await heading.evaluate(el => el.tagName)
const level = parseInt(tagName[1])
// Headings should not skip levels
expect(level - previousLevel).toBeLessThanOrEqual(1)
previousLevel = level
}
})
Color Contrast & Visual
// Test focus indicators
test('focus visible', async ({ page }) => {
await page.goto('/')
const button = page.getByRole('button', { name: 'Submit' })
await button.focus()
// Check focus outline exists
const outline = await button.evaluate(el =>
window.getComputedStyle(el).outline
)
expect(outline).not.toBe('none')
})
// Test with high contrast mode
test('high contrast mode', async ({ browser }) => {
const context = await browser.newContext({
forcedColors: 'active'
})
const page = await context.newPage()
await page.goto('/')
// Verify UI still works with forced colors
await context.close()
})
// Test text resize
test('text scaling', async ({ page }) => {
await page.goto('/')
// Zoom to 200%
await page.evaluate(() => {
document.body.style.zoom = '2'
})
// Verify content is still readable and functional
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible()
})
// Check for color-only indicators
test('no color-only indicators', async ({ page }) => {
await page.goto('/form')
// Required fields should have more than just red color
const requiredInput = page.getByLabel('Email *')
await expect(requiredInput).toHaveAttribute('required')
await expect(requiredInput).toHaveAttribute('aria-required', 'true')
})
Automated A11y Testing with Axe
// Install: npm install -D @axe-core/playwright
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test('automated accessibility test', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page })
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
// Test specific component
test('header accessibility', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.include('header')
.analyze()
expect(results.violations).toEqual([])
})
// Exclude certain elements
test('skip known issues', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.exclude('#legacy-component')
.analyze()
expect(results.violations).toEqual([])
})
// Test specific rules
test('color contrast only', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page })
.withTags(['wcag2aa', 'wcag21aa'])
.analyze()
expect(results.violations).toEqual([])
})
// Generate detailed report
test('a11y report', async ({ page }) => {
await page.goto('/')
const results = await new AxeBuilder({ page }).analyze()
if (results.violations.length > 0) {
console.log('Accessibility violations found:')
results.violations.forEach(violation => {
console.log(`- ${violation.id}: ${violation.description}`)
console.log(` Impact: ${violation.impact}`)
console.log(` Help: ${violation.helpUrl}`)
})
}
expect(results.violations.length).toBe(0)
})
Quick Command Reference
BeginnerEssential Commands
| Command | Description |
|---|---|
npx playwright test |
Run all tests |
npx playwright test --headed |
Run in headed mode |
npx playwright test --debug |
Run in debug mode |
npx playwright test --ui |
Run in UI mode |
npx playwright test --project=chromium |
Run specific browser |
npx playwright test tests/login.spec.ts |
Run specific file |
npx playwright test --grep @smoke |
Run tests with tag |
npx playwright test --workers=1 |
Run serially |
npx playwright codegen |
Generate test code |
npx playwright show-report |
Show HTML report |
npx playwright show-trace trace.zip |
View trace file |
npx playwright install |
Install browsers |
Playwright Cheatsheet โ The Complete Reference
Created by Anuraj SL ยท v1.0 ยท GitHub ยท Report Issue ยท ๐ฌ Feedback ยท โ Email