Skip to content
📚 5 min read

Playwright Guide

Playwright is a powerful testing framework by Microsoft that enables reliable end-to-end testing for modern web apps. It supports multiple browser engines including Chromium, Firefox, and WebKit.

Key Features

  • Cross-browser support
  • Auto-wait capabilities
  • Network interception
  • Mobile device emulation
  • Test parallelization
  • Visual comparisons
  • API testing support
  • Codegen tool

Getting Started

bash
# Install Playwright
npm init playwright@latest

Basic Test Structure

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

test.describe('authentication flows', () => {
  test('successful login', async ({ page }) => {
    await page.goto('/login');

    await page.fill('[name="username"]', 'testuser');
    await page.fill('[name="password"]', 'password123');
    await page.click('button[type="submit"]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('.welcome')).toContainText('Welcome back');
  });
});

Common Actions

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

// Interactions
await page.click('button');
await page.fill('input', 'text');
await page.selectOption('select', 'option1');

// Assertions
await expect(page).toHaveTitle(/My Website/);
await expect(page.locator('.count')).toHaveText('5');
await expect(page.locator('button')).toBeEnabled();

// Network
await page.route('**/api/users', (route) => {
  route.fulfill({
    status: 200,
    body: JSON.stringify({ users: [] }),
  });
});

// Screenshots
await page.screenshot({ path: 'screenshot.png' });

Test Configuration

typescript
// playwright.config.ts
import { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
  testDir: './tests',
  timeout: 30000,
  retries: 2,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
    {
      name: 'firefox',
      use: { browserName: 'firefox' },
    },
    {
      name: 'webkit',
      use: { browserName: 'webkit' },
    },
  ],
};

export default config;

Advanced Features

API Testing

typescript
test('API endpoints', async ({ request }) => {
  // GET request
  const response = await request.get('/api/users');
  expect(response.ok()).toBeTruthy();
  expect(await response.json()).toEqual(
    expect.arrayContaining([expect.objectContaining({ name: 'John' })])
  );

  // POST request with body
  const createResponse = await request.post('/api/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com',
    },
  });
  expect(createResponse.status()).toBe(201);
});

Network Interception

typescript
test('mock API calls', async ({ page }) => {
  // Mock response
  await page.route('/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
    });
  });

  // Mock error response
  await page.route('/api/error', (route) =>
    route.fulfill({
      status: 500,
      body: 'Server Error',
    })
  );

  // Modify request
  await page.route('/api/data', async (route) => {
    const request = route.request();
    const postData = request.postData();
    await route.continue({
      postData: postData?.replace('old', 'new'),
    });
  });
});

Visual Comparison

typescript
test('visual regression', async ({ page }) => {
  await page.goto('/dashboard');

  // Full page screenshot
  expect(await page.screenshot()).toMatchSnapshot('dashboard.png');

  // Element screenshot
  const logo = page.locator('.logo');
  expect(await logo.screenshot()).toMatchSnapshot('logo.png');

  // With options
  expect(
    await page.screenshot({
      fullPage: true,
      mask: [page.locator('.dynamic-content')],
    })
  ).toMatchSnapshot('masked-page.png');
});

Testing Patterns

Page Objects

typescript
// models/LoginPage.ts
class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.page.fill('input[name="email"]', email);
    await this.page.fill('input[name="password"]', password);
    await this.page.click('button[type="submit"]');
  }

  async getErrorMessage() {
    return this.page.textContent('.error-message');
  }
}

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

Component Testing

typescript
test('button component', async ({ mount }) => {
  const component = await mount(
    <Button onClick={() => console.log('clicked')}>
      Click me
    </Button>
  )

  await expect(component).toContainText('Click me')
  await component.click()
  await expect(component).toHaveClass(/active/)
})

Mobile Testing

Device Emulation

typescript
test('mobile viewport', async ({ browser }) => {
  const pixel5 = playwright.devices['Pixel 5'];
  const context = await browser.newContext({
    ...pixel5,
    locale: 'en-US',
    geolocation: { longitude: 12.492507, latitude: 41.889938 },
    permissions: ['geolocation'],
  });
  const page = await context.newPage();

  await page.goto('/mobile');
  await expect(page.locator('.mobile-menu')).toBeVisible();
});

Touch Interactions

typescript
test('touch gestures', async ({ page }) => {
  // Tap
  await page.tap('.button');

  // Double tap
  await page.dblclick('.zoom-area');

  // Swipe
  await page.locator('.slider').dragTo(page.locator('.target'), {
    sourcePosition: { x: 0, y: 0 },
    targetPosition: { x: 100, y: 0 },
  });
});

Performance Testing

Metrics Collection

typescript
test('performance metrics', async ({ page }) => {
  // Enable performance monitoring
  await page.coverage.startJSCoverage();

  const startTime = Date.now();
  await page.goto('/');

  // Get metrics
  const metrics = await page.metrics();
  const timing = await page.evaluate(() =>
    JSON.stringify(window.performance.timing)
  );
  const jsCoverage = await page.coverage.stopJSCoverage();

  // Assertions
  expect(Date.now() - startTime).toBeLessThan(3000);
  expect(metrics.TaskDuration).toBeLessThan(100);
  expect(jsCoverage[0].unusedBytes).toBeLessThan(1024);
});

Resource Monitoring

typescript
test('resource loading', async ({ page }) => {
  const [request] = await Promise.all([
    page.waitForRequest('**/*.js'),
    page.goto('/'),
  ]);

  const responses = await Promise.all([
    page.waitForResponse('**/*.css'),
    page.click('.load-more'),
  ]);

  expect(request.resourceType()).toBe('script');
  expect(responses[0].status()).toBe(200);
});

Debugging

Trace Viewer

typescript
test('record trace', async ({ page }) => {
  // Start tracing
  await context.tracing.start({
    screenshots: true,
    snapshots: true,
  });

  await page.goto('/');
  await page.click('.button');

  // Stop and save trace
  await context.tracing.stop({
    path: 'trace.zip',
  });
});

Debug Mode

typescript
test('debug test', async ({ page }) => {
  // Launch debugger
  await page.pause();

  // Console output
  page.on('console', (msg) => console.log(msg.text()));

  // Screenshot on failure
  test.afterEach(async ({ page }, testInfo) => {
    if (testInfo.status !== testInfo.expectedStatus) {
      await page.screenshot({ path: `failure-${testInfo.title}.png` });
    }
  });
});

Best Practices

1. Selectors

typescript
// ❌ Avoid
page.click('.submit-button');
page.fill('#email', 'user@example.com');

// ✅ Prefer
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email');
page.getByTestId('submit-form');

2. Waiting

typescript
// ❌ Avoid
await page.waitForTimeout(1000);

// ✅ Prefer
await expect(page.getByText('Loading')).toBeHidden();
await expect(page.getByRole('alert')).toBeVisible();
await page.waitForResponse('**/api/data');

3. Assertions

typescript
// State assertions
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('textbox')).toHaveValue('text');
await expect(page.getByRole('heading')).toContainText('Welcome');

// Multiple assertions
await expect(async () => {
  const count = await page.getByTestId('item').count();
  expect(count).toBeGreaterThan(0);
  expect(count).toBeLessThan(10);
}).toPass();

4. Error Handling

typescript
test.beforeEach(async ({ page }) => {
  page.on('pageerror', (exception) => {
    console.error(`Page error: ${exception.message}`);
  });

  page.on('requestfailed', (request) => {
    console.error(`Failed request: ${request.url()}`);
  });
});