Skip to content

Playwright Cheatsheet

Playwright - Modern Web Testing & Automation

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is fast, reliable and capable.

Table of Contents

Installation

Basic Installation

# Install Playwright
npm init playwright@latest

# Or install manually
npm install -D @playwright/test

# Install browsers
npx playwright install

# Install specific browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit

System Dependencies

# Install system dependencies (Linux)
npx playwright install-deps

# Ubuntu/Debian specific
sudo npx playwright install-deps

# Docker installation
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

Project Setup

# Create new Playwright project
npm init playwright@latest my-tests

# Project structure
my-tests/
├── tests/
   └── example.spec.js
├── playwright.config.js
├── package.json
└── package-lock.json

Configuration File

// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test directory
  testDir: './tests',

  // Run tests in files in parallel
  fullyParallel: true,

  // Fail the build on CI if you accidentally left test.only in the source code
  forbidOnly: !!process.env.CI,

  // Retry on CI only
  retries: process.env.CI ? 2 : 0,

  // Opt out of parallel tests on CI
  workers: process.env.CI ? 1 : undefined,

  // Reporter to use
  reporter: 'html',

  // Shared settings for all the projects below
  use: {
    // Base URL to use in actions like `await page.goto('/')`
    baseURL: 'http://127.0.0.1:3000',

    // Collect trace when retrying the failed test
    trace: 'on-first-retry',

    // Record video on failure
    video: 'retain-on-failure',

    // Take screenshot on failure
    screenshot: 'only-on-failure',
  },

  // Configure projects for major browsers
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  // Run your local dev server before starting the tests
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Getting Started

Basic Test Structure

// tests/example.spec.js
import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {
  // Navigate to page
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring
  await expect(page).toHaveTitle(/Playwright/);

  // Click the get started link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects the URL to contain intro
  await expect(page).toHaveURL(/.*intro/);
});

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Click the get started link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects the URL to contain intro
  await expect(page).toHaveURL(/.*intro/);
});

Running Tests

# Run all tests
npx playwright test

# Run tests in headed mode
npx playwright test --headed

# Run tests in specific browser
npx playwright test --project=chromium

# Run specific test file
npx playwright test tests/example.spec.js

# Run tests matching pattern
npx playwright test --grep "login"

# Run tests in debug mode
npx playwright test --debug

# Run tests with UI mode
npx playwright test --ui

Test Hooks

import { test, expect } from '@playwright/test';

test.describe('User Authentication', () => {
  test.beforeEach(async ({ page }) => {
    // Go to the starting url before each test
    await page.goto('https://example.com/login');
  });

  test.afterEach(async ({ page }) => {
    // Clean up after each test
    await page.evaluate(() => localStorage.clear());
  });

  test('should login successfully', async ({ page }) => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login-button');

    await expect(page).toHaveURL(/dashboard/);
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('#username', 'invalid');
    await page.fill('#password', 'wrong');
    await page.click('#login-button');

    await expect(page.locator('.error-message')).toBeVisible();
  });
});

Browser Management

Browser Contexts

import { test, expect, chromium } from '@playwright/test';

test('multiple contexts', async () => {
  // Launch browser
  const browser = await chromium.launch();

  // Create contexts
  const context1 = await browser.newContext();
  const context2 = await browser.newContext();

  // Create pages in different contexts
  const page1 = await context1.newPage();
  const page2 = await context2.newPage();

  // Navigate pages
  await page1.goto('https://example.com');
  await page2.goto('https://playwright.dev');

  // Contexts are isolated
  await page1.evaluate(() => localStorage.setItem('user', 'context1'));
  await page2.evaluate(() => localStorage.setItem('user', 'context2'));

  // Verify isolation
  const storage1 = await page1.evaluate(() => localStorage.getItem('user'));
  const storage2 = await page2.evaluate(() => localStorage.getItem('user'));

  expect(storage1).toBe('context1');
  expect(storage2).toBe('context2');

  // Cleanup
  await browser.close();
});

Browser Launch Options

import { chromium, firefox, webkit } from '@playwright/test';

test('browser launch options', async () => {
  // Chromium with custom options
  const browser = await chromium.launch({
    headless: false,
    slowMo: 1000,
    devtools: true,
    args: [
      '--start-maximized',
      '--disable-web-security',
      '--disable-features=VizDisplayCompositor'
    ]
  });

  // Context with custom options
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    userAgent: 'Custom User Agent',
    locale: 'en-US',
    timezoneId: 'America/New_York',
    permissions: ['geolocation'],
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
    colorScheme: 'dark',
    reducedMotion: 'reduce'
  });

  const page = await context.newPage();
  await page.goto('https://example.com');

  await browser.close();
});

Multiple Browsers

import { test, chromium, firefox, webkit } from '@playwright/test';

test('cross-browser testing', async () => {
  const browsers = [
    { name: 'Chromium', browser: chromium },
    { name: 'Firefox', browser: firefox },
    { name: 'WebKit', browser: webkit }
  ];

  for (const { name, browser } of browsers) {
    console.log(`Testing with ${name}`);

    const browserInstance = await browser.launch();
    const context = await browserInstance.newContext();
    const page = await context.newPage();

    await page.goto('https://example.com');
    await expect(page).toHaveTitle(/Example/);

    await browserInstance.close();
  }
});

Basic Navigation

test('navigation', async ({ page }) => {
  // Navigate to URL
  await page.goto('https://example.com');

  // Navigate with options
  await page.goto('https://example.com', {
    waitUntil: 'networkidle',
    timeout: 30000
  });

  // Go back
  await page.goBack();

  // Go forward
  await page.goForward();

  // Reload page
  await page.reload();

  // Navigate and wait for specific event
  await Promise.all([
    page.waitForNavigation(),
    page.click('a[href="/next-page"]')
  ]);
});

Wait Strategies

test('wait strategies', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for load event
  await page.goto('https://example.com', { waitUntil: 'load' });

  // Wait for DOM content loaded
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Wait for network idle
  await page.goto('https://example.com', { waitUntil: 'networkidle' });

  // Wait for specific element
  await page.waitForSelector('#content');

  // Wait for element to be visible
  await page.waitForSelector('#modal', { state: 'visible' });

  // Wait for element to be hidden
  await page.waitForSelector('#loading', { state: 'hidden' });

  // Wait for function to return true
  await page.waitForFunction(() => window.jQuery !== undefined);

  // Wait for response
  await page.waitForResponse('**/api/data');

  // Wait for request
  await page.waitForRequest('**/api/submit');
});

URL and Title Assertions

test('URL and title checks', async ({ page }) => {
  await page.goto('https://example.com');

  // Check current URL
  expect(page.url()).toBe('https://example.com/');

  // Check URL contains
  expect(page.url()).toContain('example.com');

  // Check URL matches pattern
  await expect(page).toHaveURL(/example\.com/);

  // Check title
  await expect(page).toHaveTitle('Example Domain');

  // Check title contains
  await expect(page).toHaveTitle(/Example/);

  // Get title programmatically
  const title = await page.title();
  expect(title).toBe('Example Domain');
});

Element Interaction

Clicking Elements

test('clicking elements', async ({ page }) => {
  await page.goto('https://example.com');

  // Click by selector
  await page.click('#submit-button');

  // Click with options
  await page.click('#button', {
    button: 'right',
    clickCount: 2,
    delay: 1000,
    force: true,
    modifiers: ['Shift']
  });

  // Click at position
  await page.click('#element', { position: { x: 10, y: 10 } });

  // Double click
  await page.dblclick('#element');

  // Right click
  await page.click('#element', { button: 'right' });

  // Click and wait for navigation
  await Promise.all([
    page.waitForNavigation(),
    page.click('#link')
  ]);
});

Form Interactions

test('form interactions', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Fill input
  await page.fill('#username', 'testuser');

  // Type with delay
  await page.type('#password', 'password123', { delay: 100 });

  // Clear input
  await page.fill('#field', '');

  // Select option
  await page.selectOption('#country', 'US');

  // Select multiple options
  await page.selectOption('#languages', ['en', 'es', 'fr']);

  // Check checkbox
  await page.check('#agree-terms');

  // Uncheck checkbox
  await page.uncheck('#newsletter');

  // Check radio button
  await page.check('#gender-male');

  // Upload file
  await page.setInputFiles('#file-upload', 'path/to/file.pdf');

  // Upload multiple files
  await page.setInputFiles('#files', ['file1.pdf', 'file2.jpg']);
});

Keyboard and Mouse Actions

test('keyboard and mouse actions', async ({ page }) => {
  await page.goto('https://example.com');

  // Keyboard actions
  await page.keyboard.press('Enter');
  await page.keyboard.press('Control+A');
  await page.keyboard.press('Meta+C'); // Cmd+C on Mac

  // Type text
  await page.keyboard.type('Hello World');

  // Key combinations
  await page.keyboard.down('Shift');
  await page.keyboard.press('ArrowLeft');
  await page.keyboard.up('Shift');

  // Mouse actions
  await page.mouse.move(100, 100);
  await page.mouse.click(100, 100);
  await page.mouse.dblclick(100, 100);

  // Drag and drop
  await page.dragAndDrop('#source', '#target');

  // Hover
  await page.hover('#menu-item');

  // Focus element
  await page.focus('#input-field');

  // Scroll
  await page.mouse.wheel(0, 100);
});

Selectors

CSS Selectors

test('CSS selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Basic selectors
  await page.click('#submit');           // ID
  await page.click('.button');           // Class
  await page.click('button');            // Tag
  await page.click('input[type="text"]'); // Attribute

  // Combinators
  await page.click('div > button');      // Child
  await page.click('div button');        // Descendant
  await page.click('div + button');      // Adjacent sibling
  await page.click('div ~ button');      // General sibling

  // Pseudo-selectors
  await page.click('button:first-child');
  await page.click('button:last-child');
  await page.click('button:nth-child(2)');
  await page.click('input:checked');
  await page.click('button:not(.disabled)');
});

Text Selectors

test('text selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Exact text match
  await page.click('text=Submit');

  // Partial text match
  await page.click('text=Sub');

  // Case insensitive
  await page.click('text=submit >> i');

  // Regular expression
  await page.click('text=/submit/i');

  // Text in specific element
  await page.click('button:has-text("Submit")');

  // Text with quotes
  await page.click('text="Click here"');
});

Role-based Selectors

test('role-based selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Button role
  await page.click('role=button[name="Submit"]');

  // Link role
  await page.click('role=link[name="Home"]');

  // Textbox role
  await page.fill('role=textbox[name="Username"]', 'user');

  // Checkbox role
  await page.check('role=checkbox[name="Agree"]');

  // Common roles
  await page.click('role=button');
  await page.click('role=link');
  await page.fill('role=textbox', 'text');
  await page.selectOption('role=combobox', 'option');
  await page.check('role=checkbox');
  await page.check('role=radio');
});

Playwright Locators

test('playwright locators', async ({ page }) => {
  await page.goto('https://example.com');

  // Get by role
  await page.getByRole('button', { name: 'Submit' }).click();
  await page.getByRole('link', { name: 'Home' }).click();
  await page.getByRole('textbox', { name: 'Username' }).fill('user');

  // Get by text
  await page.getByText('Submit').click();
  await page.getByText(/submit/i).click();

  // Get by label
  await page.getByLabel('Username').fill('user');
  await page.getByLabel('Password').fill('pass');

  // Get by placeholder
  await page.getByPlaceholder('Enter username').fill('user');

  // Get by alt text
  await page.getByAltText('Profile picture').click();

  // Get by title
  await page.getByTitle('Close dialog').click();

  // Get by test id
  await page.getByTestId('submit-button').click();

  // Chaining locators
  const form = page.locator('#login-form');
  await form.getByLabel('Username').fill('user');
  await form.getByLabel('Password').fill('pass');
  await form.getByRole('button', { name: 'Login' }).click();
});

Advanced Selectors

test('advanced selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // XPath
  await page.click('xpath=//button[@id="submit"]');

  // CSS with text
  await page.click('css=button >> text=Submit');

  // Multiple selectors (OR)
  await page.click('#submit, .submit-button, button[type="submit"]');

  // Selector with visibility
  await page.click('button:visible');

  // Selector with state
  await page.click('button:enabled');

  // Has selector
  await page.click('article:has(h2:text("Title"))');

  // Near selector
  await page.click('button:near(:text("Submit"))');

  // Nth match
  await page.click('button >> nth=1');

  // Filter by text
  await page.locator('button').filter({ hasText: 'Submit' }).click();

  // Filter by other locator
  await page.locator('article').filter({ has: page.locator('h2') }).first().click();
});

Waiting Strategies

Element Waiting

test('element waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for element to be attached to DOM
  await page.waitForSelector('#element');

  // Wait for element to be visible
  await page.waitForSelector('#element', { state: 'visible' });

  // Wait for element to be hidden
  await page.waitForSelector('#element', { state: 'hidden' });

  // Wait for element to be detached from DOM
  await page.waitForSelector('#element', { state: 'detached' });

  // Wait with timeout
  await page.waitForSelector('#element', { timeout: 5000 });

  // Wait for multiple elements
  await Promise.all([
    page.waitForSelector('#element1'),
    page.waitForSelector('#element2'),
    page.waitForSelector('#element3')
  ]);
});

Auto-waiting

test('auto-waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Playwright automatically waits for elements to be:
  // - Attached to DOM
  // - Visible
  // - Stable (not animating)
  // - Enabled
  // - Editable (for input actions)

  // These actions auto-wait
  await page.click('#button');           // Waits for button to be clickable
  await page.fill('#input', 'text');     // Waits for input to be editable
  await page.selectOption('#select', 'option'); // Waits for select to be enabled
  await page.check('#checkbox');         // Waits for checkbox to be checkable

  // Assertions also auto-wait
  await expect(page.locator('#element')).toBeVisible();
  await expect(page.locator('#input')).toHaveValue('expected');
  await expect(page.locator('#list li')).toHaveCount(5);
});

Custom Waiting

test('custom waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for function to return truthy value
  await page.waitForFunction(() => {
    return document.querySelector('#dynamic-content') !== null;
  });

  // Wait for function with arguments
  await page.waitForFunction(
    (selector) => document.querySelector(selector) !== null,
    '#dynamic-element'
  );

  // Wait for JavaScript condition
  await page.waitForFunction(() => window.dataLoaded === true);

  // Wait for network idle
  await page.waitForLoadState('networkidle');

  // Wait for DOM content loaded
  await page.waitForLoadState('domcontentloaded');

  // Wait for load event
  await page.waitForLoadState('load');

  // Wait for timeout
  await page.waitForTimeout(1000);

  // Wait for event
  await page.waitForEvent('dialog');
  await page.waitForEvent('download');
  await page.waitForEvent('popup');
});

Form Handling

Input Fields

test('input field handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Text input
  await page.fill('#username', 'testuser');
  await page.fill('#email', 'test@example.com');

  // Password input
  await page.fill('#password', 'secretpassword');

  // Number input
  await page.fill('#age', '25');

  // Date input
  await page.fill('#birthdate', '2000-01-01');

  // Textarea
  await page.fill('#comments', 'This is a long comment text');

  // Clear field
  await page.fill('#field', '');

  // Type with delay (simulates human typing)
  await page.type('#search', 'search term', { delay: 100 });

  // Press keys
  await page.press('#input', 'Control+A');
  await page.press('#input', 'Backspace');
});

Select Dropdowns

test('select dropdown handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Select by value
  await page.selectOption('#country', 'US');

  // Select by label
  await page.selectOption('#country', { label: 'United States' });

  // Select by index
  await page.selectOption('#country', { index: 1 });

  // Select multiple options
  await page.selectOption('#languages', ['en', 'es', 'fr']);

  // Select all options
  const options = await page.locator('#languages option').allTextContents();
  await page.selectOption('#languages', options);

  // Get selected value
  const selectedValue = await page.inputValue('#country');
  expect(selectedValue).toBe('US');

  // Get all selected values
  const selectedValues = await page.evaluate(() => {
    const select = document.querySelector('#languages');
    return Array.from(select.selectedOptions).map(option => option.value);
  });
});

Checkboxes and Radio Buttons

test('checkbox and radio handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Check checkbox
  await page.check('#agree-terms');
  await page.check('#newsletter');

  // Uncheck checkbox
  await page.uncheck('#newsletter');

  // Toggle checkbox
  const isChecked = await page.isChecked('#newsletter');
  if (!isChecked) {
    await page.check('#newsletter');
  }

  // Radio buttons
  await page.check('#gender-male');
  await page.check('#age-25-34');

  // Verify checkbox state
  expect(await page.isChecked('#agree-terms')).toBe(true);
  expect(await page.isChecked('#newsletter')).toBe(false);

  // Verify radio button state
  expect(await page.isChecked('#gender-male')).toBe(true);
  expect(await page.isChecked('#gender-female')).toBe(false);
});

File Uploads

test('file upload handling', async ({ page }) => {
  await page.goto('https://example.com/upload');

  // Single file upload
  await page.setInputFiles('#file-upload', 'tests/fixtures/document.pdf');

  // Multiple file upload
  await page.setInputFiles('#multiple-files', [
    'tests/fixtures/image1.jpg',
    'tests/fixtures/image2.png',
    'tests/fixtures/document.pdf'
  ]);

  // Upload from buffer
  await page.setInputFiles('#file-upload', {
    name: 'test.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('Hello World')
  });

  // Remove files
  await page.setInputFiles('#file-upload', []);

  // Verify file upload
  const fileName = await page.inputValue('#file-upload');
  expect(fileName).toContain('document.pdf');

  // Handle file chooser dialog
  const [fileChooser] = await Promise.all([
    page.waitForEvent('filechooser'),
    page.click('#upload-button')
  ]);

  await fileChooser.setFiles('tests/fixtures/document.pdf');
});

Form Submission

test('form submission', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Fill form
  await page.fill('#username', 'testuser');
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password123');
  await page.check('#agree-terms');

  // Submit form by clicking submit button
  await Promise.all([
    page.waitForNavigation(),
    page.click('#submit-button')
  ]);

  // Submit form by pressing Enter
  await Promise.all([
    page.waitForNavigation(),
    page.press('#username', 'Enter')
  ]);

  // Submit form programmatically
  await page.evaluate(() => {
    document.querySelector('#form').submit();
  });

  // Verify form submission
  await expect(page).toHaveURL(/success/);
  await expect(page.locator('.success-message')).toBeVisible();
});

Screenshots & Videos

Screenshots

test('screenshots', async ({ page }) => {
  await page.goto('https://example.com');

  // Full page screenshot
  await page.screenshot({ path: 'screenshots/fullpage.png' });

  // Screenshot with options
  await page.screenshot({
    path: 'screenshots/page.png',
    fullPage: true,
    clip: { x: 0, y: 0, width: 800, height: 600 },
    quality: 80,
    type: 'jpeg'
  });

  // Element screenshot
  await page.locator('#header').screenshot({ path: 'screenshots/header.png' });

  // Screenshot to buffer
  const buffer = await page.screenshot();

  // Screenshot with mask
  await page.screenshot({
    path: 'screenshots/masked.png',
    mask: [page.locator('.sensitive-data')]
  });

  // Mobile screenshot
  await page.setViewportSize({ width: 375, height: 667 });
  await page.screenshot({ path: 'screenshots/mobile.png' });
});

Video Recording

// playwright.config.js
export default defineConfig({
  use: {
    // Record video for all tests
    video: 'on',

    // Record video only on failure
    video: 'retain-on-failure',

    // Record video on first retry
    video: 'on-first-retry',
  },
});

test('video recording', async ({ page }) => {
  // Video is automatically recorded based on config
  await page.goto('https://example.com');
  await page.click('#button');
  await page.fill('#input', 'test');

  // Video will be saved to test-results folder
});

Visual Comparisons

test('visual comparisons', async ({ page }) => {
  await page.goto('https://example.com');

  // Compare full page
  await expect(page).toHaveScreenshot('homepage.png');

  // Compare element
  await expect(page.locator('#header')).toHaveScreenshot('header.png');

  // Compare with threshold
  await expect(page).toHaveScreenshot('page.png', { threshold: 0.2 });

  // Compare with mask
  await expect(page).toHaveScreenshot('page.png', {
    mask: [page.locator('.dynamic-content')]
  });

  // Update screenshots
  // npx playwright test --update-snapshots
});

Network Interception

Request Interception

test('request interception', async ({ page }) => {
  // Intercept all requests
  await page.route('**/*', route => {
    console.log('Request:', route.request().url());
    route.continue();
  });

  // Intercept specific requests
  await page.route('**/api/users', route => {
    // Block request
    route.abort();

    // Modify request
    route.continue({
      headers: {
        ...route.request().headers(),
        'Authorization': 'Bearer token'
      }
    });

    // Mock response
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ users: [] })
    });
  });

  await page.goto('https://example.com');
});

Response Mocking

test('response mocking', async ({ page }) => {
  // Mock API response
  await page.route('**/api/data', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        success: true,
        data: [
          { id: 1, name: 'Item 1' },
          { id: 2, name: 'Item 2' }
        ]
      })
    });
  });

  // Mock with file
  await page.route('**/api/users', route => {
    route.fulfill({
      path: 'tests/fixtures/users.json'
    });
  });

  // Mock error response
  await page.route('**/api/error', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' })
    });
  });

  await page.goto('https://example.com');
});

Network Monitoring

test('network monitoring', async ({ page }) => {
  const requests = [];
  const responses = [];

  // Monitor requests
  page.on('request', request => {
    requests.push({
      url: request.url(),
      method: request.method(),
      headers: request.headers(),
      postData: request.postData()
    });
  });

  // Monitor responses
  page.on('response', response => {
    responses.push({
      url: response.url(),
      status: response.status(),
      headers: response.headers()
    });
  });

  await page.goto('https://example.com');

  // Wait for specific request
  const [request] = await Promise.all([
    page.waitForRequest('**/api/data'),
    page.click('#load-data')
  ]);

  // Wait for specific response
  const [response] = await Promise.all([
    page.waitForResponse('**/api/submit'),
    page.click('#submit')
  ]);

  expect(response.status()).toBe(200);

  // Analyze network activity
  const apiRequests = requests.filter(req => req.url.includes('/api/'));
  expect(apiRequests).toHaveLength(3);
});

Authentication

Basic Authentication

test('basic authentication', async ({ page }) => {
  // Set basic auth
  await page.setExtraHTTPHeaders({
    'Authorization': 'Basic ' + Buffer.from('username:password').toString('base64')
  });

  await page.goto('https://example.com/protected');

  // Or use context-level auth
  const context = await browser.newContext({
    httpCredentials: {
      username: 'testuser',
      password: 'password123'
    }
  });

  const page2 = await context.newPage();
  await page2.goto('https://example.com/protected');
});

Session Management

test('session management', async ({ page }) => {
  // Login
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login');

  // Save session
  const cookies = await page.context().cookies();
  const localStorage = await page.evaluate(() => {
    return JSON.stringify(window.localStorage);
  });

  // Use session in another test
  await page.context().addCookies(cookies);
  await page.evaluate(storage => {
    const data = JSON.parse(storage);
    for (const [key, value] of Object.entries(data)) {
      window.localStorage.setItem(key, value);
    }
  }, localStorage);

  await page.goto('https://example.com/dashboard');
  await expect(page.locator('.user-info')).toBeVisible();
});

OAuth and Token Authentication

test('token authentication', async ({ page }) => {
  // Set authorization header
  await page.setExtraHTTPHeaders({
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  });

  // Or intercept requests to add token
  await page.route('**/api/**', route => {
    route.continue({
      headers: {
        ...route.request().headers(),
        'Authorization': 'Bearer ' + process.env.API_TOKEN
      }
    });
  });

  await page.goto('https://example.com/app');
});

Multi-factor Authentication

test('multi-factor authentication', async ({ page }) => {
  // Step 1: Username and password
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login');

  // Step 2: Wait for MFA prompt
  await expect(page.locator('#mfa-code')).toBeVisible();

  // Step 3: Enter MFA code (you might need to generate this)
  const mfaCode = '123456'; // In real tests, this might come from a TOTP generator
  await page.fill('#mfa-code', mfaCode);
  await page.click('#verify-mfa');

  // Step 4: Verify successful login
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.locator('.welcome-message')).toBeVisible();
});

Mobile Testing

Device Emulation

import { test, devices } from '@playwright/test';

test('mobile testing', async ({ browser }) => {
  // iPhone 12
  const iPhone = devices['iPhone 12'];
  const context = await browser.newContext({
    ...iPhone
  });
  const page = await context.newPage();

  await page.goto('https://example.com');

  // Test mobile-specific features
  await expect(page.locator('.mobile-menu')).toBeVisible();
  await expect(page.locator('.desktop-menu')).toBeHidden();

  await context.close();
});

// Using project configuration
test.describe('Mobile Tests', () => {
  test.use({ ...devices['iPhone 12'] });

  test('mobile navigation', async ({ page }) => {
    await page.goto('https://example.com');

    // Test hamburger menu
    await page.click('.hamburger-menu');
    await expect(page.locator('.mobile-nav')).toBeVisible();
  });
});

Touch Interactions

test('touch interactions', async ({ page }) => {
  // Set mobile viewport
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('https://example.com');

  // Tap (equivalent to click on mobile)
  await page.tap('#button');

  // Long press
  await page.touchscreen.tap(100, 100, { duration: 1000 });

  // Swipe
  await page.touchscreen.tap(100, 300);
  await page.mouse.move(300, 300);
  await page.mouse.up();

  // Pinch to zoom
  await page.touchscreen.tap(200, 200);
  await page.touchscreen.tap(250, 250);

  // Scroll
  await page.mouse.wheel(0, 100);

  // Test responsive design
  const isMobileMenuVisible = await page.isVisible('.mobile-menu');
  expect(isMobileMenuVisible).toBe(true);
});

Geolocation Testing

test('geolocation testing', async ({ context, page }) => {
  // Grant geolocation permission
  await context.grantPermissions(['geolocation']);

  // Set geolocation
  await context.setGeolocation({ latitude: 40.7128, longitude: -74.0060 });

  await page.goto('https://example.com/map');

  // Test location-based features
  await page.click('#get-location');
  await expect(page.locator('#location-display')).toContainText('New York');

  // Change location
  await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
  await page.reload();
  await page.click('#get-location');
  await expect(page.locator('#location-display')).toContainText('Los Angeles');
});

Testing Framework

Test Organization

import { test, expect } from '@playwright/test';

// Test suite
test.describe('User Authentication', () => {
  test.beforeAll(async () => {
    // Setup before all tests in this describe block
    console.log('Setting up test data');
  });

  test.beforeEach(async ({ page }) => {
    // Setup before each test
    await page.goto('https://example.com/login');
  });

  test.afterEach(async ({ page }) => {
    // Cleanup after each test
    await page.evaluate(() => localStorage.clear());
  });

  test.afterAll(async () => {
    // Cleanup after all tests
    console.log('Cleaning up test data');
  });

  test('should login with valid credentials', async ({ page }) => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login');

    await expect(page).toHaveURL(/dashboard/);
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('#username', 'invalid');
    await page.fill('#password', 'wrong');
    await page.click('#login');

    await expect(page.locator('.error')).toBeVisible();
  });
});

Parameterized Tests

const testData = [
  { username: 'user1', password: 'pass1', expected: 'dashboard' },
  { username: 'user2', password: 'pass2', expected: 'admin' },
  { username: 'user3', password: 'pass3', expected: 'profile' }
];

testData.forEach(({ username, password, expected }) => {
  test(`login with ${username}`, async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.fill('#username', username);
    await page.fill('#password', password);
    await page.click('#login');

    await expect(page).toHaveURL(new RegExp(expected));
  });
});

// Or using test.describe.parallel for parallel execution
test.describe.parallel('Parallel Tests', () => {
  ['chrome', 'firefox', 'safari'].forEach(browser => {
    test(`test on ${browser}`, async ({ page }) => {
      // Test logic here
    });
  });
});

Custom Fixtures

// fixtures.js
import { test as base } from '@playwright/test';

// Custom fixture for authenticated user
const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Login before each test
    await page.goto('https://example.com/login');
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login');
    await page.waitForURL('**/dashboard');

    await use(page);

    // Logout after each test
    await page.click('#logout');
  }
});

// Use custom fixture
test('dashboard test', async ({ authenticatedPage }) => {
  await expect(authenticatedPage.locator('.welcome')).toBeVisible();
});

export { test };

Test Annotations

test('slow test', async ({ page }) => {
  test.slow(); // Mark test as slow (3x timeout)

  await page.goto('https://example.com');
  // Long-running test logic
});

test('flaky test', async ({ page }) => {
  test.fixme(); // Mark test as broken

  await page.goto('https://example.com');
  // Flaky test logic
});

test('skip on mobile', async ({ page, isMobile }) => {
  test.skip(isMobile, 'Not supported on mobile');

  await page.goto('https://example.com');
  // Desktop-only test logic
});

test('conditional test', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Not supported in Safari');

  await page.goto('https://example.com');
  // Browser-specific test logic
});

Debugging

Debug Mode

# Run tests in debug mode
npx playwright test --debug

# Debug specific test
npx playwright test tests/example.spec.js --debug

# Debug with headed browser
npx playwright test --headed --debug

# Debug with slow motion
npx playwright test --headed --slowMo=1000

Debugging in Code

test('debugging example', async ({ page }) => {
  await page.goto('https://example.com');

  // Pause execution
  await page.pause();

  // Add console logs
  console.log('Current URL:', page.url());

  // Take screenshot for debugging
  await page.screenshot({ path: 'debug-screenshot.png' });

  // Evaluate JavaScript in browser context
  const title = await page.evaluate(() => document.title);
  console.log('Page title:', title);

  // Get element information
  const element = page.locator('#button');
  console.log('Element visible:', await element.isVisible());
  console.log('Element text:', await element.textContent());

  await page.click('#button');
});

Trace Viewer

// playwright.config.js
export default defineConfig({
  use: {
    // Collect trace when retrying the failed test
    trace: 'on-first-retry',

    // Or collect trace for all tests
    trace: 'on',
  },
});

// View traces
// npx playwright show-trace test-results/example-chromium/trace.zip

Browser Developer Tools

test('debug with devtools', async ({ page }) => {
  // Launch browser with devtools
  await page.goto('https://example.com');

  // Open devtools programmatically
  await page.evaluate(() => {
    debugger; // This will trigger debugger if devtools is open
  });

  // Inspect element
  await page.locator('#element').highlight();

  // Log to browser console
  await page.evaluate(() => {
    console.log('Debug message from browser');
  });
});

Error Handling

test('error handling', async ({ page }) => {
  try {
    await page.goto('https://example.com');
    await page.click('#non-existent-element', { timeout: 5000 });
  } catch (error) {
    console.log('Error occurred:', error.message);

    // Take screenshot on error
    await page.screenshot({ path: 'error-screenshot.png' });

    // Log page content for debugging
    const content = await page.content();
    console.log('Page content:', content);

    throw error; // Re-throw to fail the test
  }
});

Performance Testing

Performance Metrics

test('performance metrics', async ({ page }) => {
  await page.goto('https://example.com');

  // Get performance metrics
  const metrics = await page.evaluate(() => {
    const navigation = performance.getEntriesByType('navigation')[0];
    return {
      domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
      firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,
      firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime
    };
  });

  console.log('Performance metrics:', metrics);

  // Assert performance thresholds
  expect(metrics.domContentLoaded).toBeLessThan(2000);
  expect(metrics.loadComplete).toBeLessThan(5000);
});

Lighthouse Integration

import { playAudit } from 'playwright-lighthouse';

test('lighthouse audit', async ({ page }) => {
  await page.goto('https://example.com');

  await playAudit({
    page,
    thresholds: {
      performance: 90,
      accessibility: 90,
      'best-practices': 90,
      seo: 90,
    },
    port: 9222,
  });
});

Network Performance

test('network performance', async ({ page }) => {
  const responses = [];

  page.on('response', response => {
    responses.push({
      url: response.url(),
      status: response.status(),
      size: response.headers()['content-length'],
      timing: response.timing()
    });
  });

  await page.goto('https://example.com');

  // Analyze network performance
  const slowRequests = responses.filter(r => r.timing.responseEnd > 1000);
  const largeRequests = responses.filter(r => parseInt(r.size) > 1000000);

  expect(slowRequests.length).toBeLessThan(3);
  expect(largeRequests.length).toBeLessThan(2);

  console.log('Total requests:', responses.length);
  console.log('Slow requests:', slowRequests.length);
  console.log('Large requests:', largeRequests.length);
});

API Testing

REST API Testing

import { test, expect, request } from '@playwright/test';

test('API testing', async ({ request }) => {
  // GET request
  const response = await request.get('https://api.example.com/users');
  expect(response.status()).toBe(200);

  const users = await response.json();
  expect(users).toHaveLength(10);

  // POST request
  const newUser = {
    name: 'John Doe',
    email: 'john@example.com'
  };

  const createResponse = await request.post('https://api.example.com/users', {
    data: newUser
  });

  expect(createResponse.status()).toBe(201);
  const createdUser = await createResponse.json();
  expect(createdUser.name).toBe(newUser.name);

  // PUT request
  const updateResponse = await request.put(`https://api.example.com/users/${createdUser.id}`, {
    data: { name: 'Jane Doe' }
  });

  expect(updateResponse.status()).toBe(200);

  // DELETE request
  const deleteResponse = await request.delete(`https://api.example.com/users/${createdUser.id}`);
  expect(deleteResponse.status()).toBe(204);
});

API Authentication

test('API with authentication', async ({ request }) => {
  // Login to get token
  const loginResponse = await request.post('https://api.example.com/auth/login', {
    data: {
      username: 'testuser',
      password: 'password123'
    }
  });

  const { token } = await loginResponse.json();

  // Use token for authenticated requests
  const response = await request.get('https://api.example.com/protected', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  expect(response.status()).toBe(200);
});

GraphQL Testing

test('GraphQL API testing', async ({ request }) => {
  const query = `
    query GetUsers {
      users {
        id
        name
        email
      }
    }
  `;

  const response = await request.post('https://api.example.com/graphql', {
    data: { query }
  });

  expect(response.status()).toBe(200);

  const result = await response.json();
  expect(result.data.users).toBeDefined();
  expect(result.data.users.length).toBeGreaterThan(0);
});

Visual Testing

Visual Regression Testing

test('visual regression', async ({ page }) => {
  await page.goto('https://example.com');

  // Take baseline screenshot
  await expect(page).toHaveScreenshot('homepage.png');

  // Test different states
  await page.hover('#menu');
  await expect(page).toHaveScreenshot('homepage-menu-hover.png');

  await page.click('#toggle-theme');
  await expect(page).toHaveScreenshot('homepage-dark-theme.png');

  // Test responsive design
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(page).toHaveScreenshot('homepage-tablet.png');

  await page.setViewportSize({ width: 375, height: 667 });
  await expect(page).toHaveScreenshot('homepage-mobile.png');
});

Component Visual Testing

test('component visual testing', async ({ page }) => {
  await page.goto('https://example.com/components');

  // Test individual components
  await expect(page.locator('.button')).toHaveScreenshot('button.png');
  await expect(page.locator('.card')).toHaveScreenshot('card.png');
  await expect(page.locator('.modal')).toHaveScreenshot('modal.png');

  // Test component states
  await page.hover('.button');
  await expect(page.locator('.button')).toHaveScreenshot('button-hover.png');

  await page.click('.button');
  await expect(page.locator('.button')).toHaveScreenshot('button-active.png');
});

Cross-browser Visual Testing

['chromium', 'firefox', 'webkit'].forEach(browserName => {
  test(`visual test on ${browserName}`, async ({ page }) => {
    await page.goto('https://example.com');

    await expect(page).toHaveScreenshot(`homepage-${browserName}.png`);
  });
});

CI/CD Integration

GitHub Actions

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: 18
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Docker Integration

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
# Build and run tests in Docker
docker build -t playwright-tests .
docker run --rm playwright-tests

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
                sh 'npx playwright install --with-deps'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'npx playwright test'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: false,
                        keepAll: true,
                        reportDir: 'playwright-report',
                        reportFiles: 'index.html',
                        reportName: 'Playwright Report'
                    ])
                }
            }
        }
    }
}

Advanced Features

Custom Matchers

// custom-matchers.js
import { expect } from '@playwright/test';

expect.extend({
  async toHaveLoadTime(page, maxTime) {
    const startTime = Date.now();
    await page.waitForLoadState('networkidle');
    const loadTime = Date.now() - startTime;

    const pass = loadTime <= maxTime;

    return {
      message: () => `Expected load time to be <= ${maxTime}ms, but was ${loadTime}ms`,
      pass
    };
  }
});

// Usage
test('custom matcher', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveLoadTime(3000);
});

Page Object Model

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('#username');
    this.passwordInput = page.locator('#password');
    this.loginButton = page.locator('#login');
    this.errorMessage = page.locator('.error');
  }

  async goto() {
    await this.page.goto('https://example.com/login');
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }
}

// Usage in test
import { LoginPage } from './pages/LoginPage';

test('login with page object', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');

  await expect(page).toHaveURL(/dashboard/);
});

Global Setup and Teardown

// global-setup.js
export default async function globalSetup() {
  // Start test server
  console.log('Starting test server...');

  // Create test data
  console.log('Creating test data...');

  // Any other global setup
}

// global-teardown.js
export default async function globalTeardown() {
  // Stop test server
  console.log('Stopping test server...');

  // Clean up test data
  console.log('Cleaning up test data...');
}

// playwright.config.js
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),
});

Best Practices

Test Organization

// Organize tests by feature/page
test.describe('User Management', () => {
  test.describe('User Registration', () => {
    test('should register new user', async ({ page }) => {
      // Test implementation
    });

    test('should validate email format', async ({ page }) => {
      // Test implementation
    });
  });

  test.describe('User Login', () => {
    test('should login with valid credentials', async ({ page }) => {
      // Test implementation
    });
  });
});

Reliable Selectors

// Good: Use data-testid attributes
await page.click('[data-testid="submit-button"]');

// Good: Use role-based selectors
await page.click('role=button[name="Submit"]');

// Good: Use text content
await page.click('text=Submit');

// Avoid: CSS selectors that depend on styling
await page.click('.btn.btn-primary.submit-btn');

// Avoid: XPath with position
await page.click('//div[3]/button[2]');

Error Handling

test('robust error handling', async ({ page }) => {
  try {
    await page.goto('https://example.com');

    // Use soft assertions for non-critical checks
    await expect.soft(page.locator('.optional-element')).toBeVisible();

    // Continue with critical test steps
    await page.click('#important-button');
    await expect(page.locator('#result')).toBeVisible();

  } catch (error) {
    // Take screenshot on failure
    await page.screenshot({ path: 'failure-screenshot.png' });

    // Log additional context
    console.log('Current URL:', page.url());
    console.log('Page title:', await page.title());

    throw error;
  }
});

Performance Best Practices

  • Use auto-waiting: Playwright automatically waits for elements
  • Avoid unnecessary waits: Don't use page.waitForTimeout() unless absolutely necessary
  • Reuse browser contexts: Share contexts between tests when possible
  • Parallelize tests: Use fullyParallel: true in configuration
  • Optimize selectors: Use efficient selectors like data-testid

Maintenance Best Practices

  • Keep tests independent: Each test should be able to run in isolation
  • Use Page Object Model: Encapsulate page interactions in reusable classes
  • Implement proper cleanup: Clear state between tests
  • Use meaningful test names: Describe what the test is verifying
  • Regular updates: Keep Playwright and browsers updated

Summary

Playwright is a powerful, modern web testing framework that provides:

  • Cross-browser Support: Test on Chromium, Firefox, and WebKit
  • Auto-waiting: Intelligent waiting for elements to be ready
  • Powerful Selectors: Multiple selector strategies including role-based
  • Network Interception: Mock and monitor network requests
  • Mobile Testing: Device emulation and touch interactions
  • Visual Testing: Screenshot comparison and visual regression testing
  • Debugging Tools: Trace viewer, debug mode, and developer tools integration
  • CI/CD Ready: Easy integration with popular CI/CD platforms
  • API Testing: Built-in support for REST and GraphQL API testing
  • Performance Testing: Built-in performance metrics and Lighthouse integration

Playwright's modern architecture and comprehensive feature set make it an excellent choice for end-to-end testing, browser automation, and web scraping tasks.