Playwright End-to-End Testing Guide | Generated by AI

Home 2025.10

What is Playwright?

Playwright is a modern end-to-end testing framework developed by Microsoft that enables reliable testing of web applications across multiple browsers (Chromium, Firefox, and WebKit). It provides a unified API for automating web pages and is designed to handle modern web app challenges like single-page applications, dynamic content, and complex user interactions.

Key Features

Installation and Setup

Basic Installation

# Install Playwright
npm init playwright@latest

# Or add to existing project
npm install -D @playwright/test

# Install browsers
npx playwright install

Project Structure

tests/
  example.spec.js
playwright.config.js
package.json

Configuration (playwright.config.js)

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Writing Your First Test

Basic Test Structure

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

test('basic test example', async ({ page }) => {
  // Navigate to page
  await page.goto('https://example.com');
  
  // Interact with elements
  await page.click('button');
  await page.fill('input[name="username"]', 'testuser');
  
  // Assertions
  await expect(page.locator('h1')).toHaveText('Welcome');
  await expect(page).toHaveURL(/dashboard/);
});

Common Actions

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

Element Interactions

// Click elements
await page.click('button');
await page.click('text=Submit');
await page.click('#login-btn');

// Fill forms
await page.fill('input[name="email"]', 'user@example.com');
await page.type('textarea', 'Hello world');
await page.selectOption('select', 'option-value');

// Check/uncheck
await page.check('input[type="checkbox"]');
await page.uncheck('input[type="checkbox"]');

Waiting and Timeouts

// Wait for elements
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
await page.waitForURL('**/dashboard');
await page.waitForResponse('**/api/users');

// Wait for custom conditions
await page.waitForFunction(() => window.myApp.isReady);

Advanced Testing Patterns

Page Object Model

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.emailInput = page.locator('input[name="email"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.loginButton = page.locator('button[type="submit"]');
  }

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

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

test('user can login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await page.goto('/login');
  await loginPage.login('user@example.com', 'password123');
  await expect(page).toHaveURL('/dashboard');
});

API Testing

test('API testing', async ({ request }) => {
  // POST request
  const response = await request.post('/api/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com'
    }
  });
  
  expect(response.ok()).toBeTruthy();
  const userData = await response.json();
  expect(userData.name).toBe('John Doe');
});

Network Mocking

test('mock API responses', async ({ page }) => {
  // Mock API response
  await page.route('**/api/users', async route => {
    const json = [{ id: 1, name: 'Mock User' }];
    await route.fulfill({ json });
  });
  
  await page.goto('/users');
  await expect(page.locator('.user-name')).toHaveText('Mock User');
});

Visual Testing

test('visual comparison', async ({ page }) => {
  await page.goto('/dashboard');
  
  // Full page screenshot
  await expect(page).toHaveScreenshot('dashboard.png');
  
  // Element screenshot
  await expect(page.locator('.header')).toHaveScreenshot('header.png');
});

Test Organization and Best Practices

Test Hooks

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

test.describe('User Management', () => {
  test.beforeEach(async ({ page }) => {
    // Run before each test
    await page.goto('/login');
    await page.fill('[name="email"]', 'admin@example.com');
    await page.fill('[name="password"]', 'password');
    await page.click('button[type="submit"]');
  });

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

  test('should create new user', async ({ page }) => {
    // Test implementation
  });
});

Fixtures and Test Context

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

export const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Login before test
    await page.goto('/login');
    await page.fill('[name="email"]', 'user@example.com');
    await page.fill('[name="password"]', 'password');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');
    
    await use(page);
    
    // Cleanup after test
    await page.goto('/logout');
  },
});

// In test file
import { test, expect } from '../fixtures/auth';

test('authenticated user actions', async ({ authenticatedPage }) => {
  await expect(authenticatedPage.locator('.welcome')).toBeVisible();
});

Running Tests

Command Line Options

# Run all tests
npx playwright test

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

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

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

# Run tests with debugging
npx playwright test --debug

# Run tests in parallel
npx playwright test --workers=4

Test Reports

# Generate HTML report
npx playwright show-report

# View trace files
npx playwright show-trace trace.zip

Playwright vs Selenium

Architecture Differences

Feature Playwright Selenium
Architecture Direct browser communication WebDriver protocol
Browser Support Chromium, Firefox, WebKit Chrome, Firefox, Safari, Edge, IE
Installation Single package with browsers Separate driver downloads
Language Support JavaScript, Python, Java, C# Most programming languages

Performance Comparison

Playwright Advantages:

Selenium Advantages:

Feature Comparison

Test Reliability

// Playwright - Auto-wait built-in
await page.click('button'); // Waits for element to be clickable

// Selenium - Manual waits required
await driver.wait(until.elementIsVisible(button));
await driver.wait(until.elementToBeClickable(button));
await button.click();

Mobile Testing

// Playwright - Built-in mobile emulation
const context = await browser.newContext({
  ...devices['iPhone 13']
});

// Selenium - Requires additional setup
const options = new chrome.Options();
options.addArguments('--user-agent=iPhone...');

Network Handling

// Playwright - Native network interception
await page.route('**/api/**', route => route.abort());

// Selenium - Requires proxy setup
const proxy = new Proxy();
proxy.setHttpProxy('localhost:8080');

Migration Considerations

When to Choose Playwright:

When to Stick with Selenium:

Code Comparison Example

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

test('login flow', async ({ page }) => {
  await page.goto('/login');
  await page.fill('[name="email"]', 'user@example.com');
  await page.fill('[name="password"]', 'password');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
});

// Selenium (JavaScript)
const { Builder, By, until } = require('selenium-webdriver');

describe('login flow', () => {
  let driver;
  
  beforeEach(async () => {
    driver = await new Builder().forBrowser('chrome').build();
  });
  
  afterEach(async () => {
    await driver.quit();
  });
  
  it('should login successfully', async () => {
    await driver.get('http://localhost:3000/login');
    await driver.findElement(By.name('email')).sendKeys('user@example.com');
    await driver.findElement(By.name('password')).sendKeys('password');
    await driver.findElement(By.css('button[type="submit"]')).click();
    await driver.wait(until.urlContains('/dashboard'));
  });
});

Conclusion

Playwright represents a modern approach to end-to-end testing with significant advantages in speed, reliability, and developer experience. While Selenium remains a solid choice for established projects and specific use cases, Playwright’s architecture and feature set make it particularly well-suited for testing modern web applications. The choice between them should be based on your specific project requirements, existing infrastructure, and team expertise.


Back

anthropic/claude-sonnet-4

Donate