Choorai
E2E Playwright

E2E Test Guide

Test user scenarios in real browsers with Playwright. Automate cross-browser testing and visual regression testing.

Why Playwright?

  • Cross-browser: Chrome, Firefox, Safari, Edge
  • Auto-wait: Automatically waits until elements are ready
  • Powerful selectors: Based on text, role, and test ID
  • Screenshots/Videos: Automatic capture on failure

Installation & Setup

Project Initialization

Terminal
# Install Playwright
npm init playwright@latest

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

Configuration File

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

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:5173',
    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'] },
    },
    // Mobile testing
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  // Start server before tests
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

webServer option

The webServer config automatically starts the dev server before running tests.

Writing Basic Tests

e2e/todo.spec.ts
// e2e/todo.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Todo App', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('should display the home page', async ({ page }) => {
    // Verify page title
    await expect(page).toHaveTitle(/Todo/);

    // Verify main heading
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
  });

  test('should create a new todo', async ({ page }) => {
    // Type text in the input field
    await page.getByPlaceholder('Enter a todo').fill('New todo item');

    // Click the add button
    await page.getByRole('button', { name: 'Add' }).click();

    // Verify it was added to the list
    await expect(page.getByText('New todo item')).toBeVisible();
  });

  test('should toggle todo completion', async ({ page }) => {
    // Create a todo
    await page.getByPlaceholder('Enter a todo').fill('Test todo');
    await page.getByRole('button', { name: 'Add' }).click();

    // Click the checkbox
    await page.getByRole('checkbox').first().click();

    // Verify completed state (strikethrough style)
    await expect(page.getByText('Test todo')).toHaveClass(/line-through/);
  });

  test('should delete a todo', async ({ page }) => {
    // Create a todo
    await page.getByPlaceholder('Enter a todo').fill('Todo to delete');
    await page.getByRole('button', { name: 'Add' }).click();

    // Click the delete button
    await page.getByRole('button', { name: 'Delete' }).click();

    // Verify it was removed from the list
    await expect(page.getByText('Todo to delete')).not.toBeVisible();
  });
});

Running Tests

Terminal
# Run all tests
npx playwright test

# Run in UI mode (useful for debugging)
npx playwright test --ui

# Specific browser only
npx playwright test --project=chromium

# Specific file only
npx playwright test e2e/todo.spec.ts

# Disable headless mode (show browser)
npx playwright test --headed

# Open test report
npx playwright show-report

Selector Best Practices

Recommended

  • getByRole('button', {name: 'Add'})
  • getByText('Todo List')
  • getByPlaceholder('Enter...')
  • getByTestId('todo-list')

Avoid

  • locator('.btn-primary')
  • locator('#submit-btn')
  • locator('div > button:nth-child(2)')

Priority order

getByRole > getByText > getByTestId > locator

API Mocking

You can test the frontend without a real backend. Use page.route() to mock API responses.

e2e/with-mock.spec.ts
// e2e/with-mock.spec.ts
import { test, expect } from '@playwright/test';

test('should display mocked todos', async ({ page }) => {
  // Mock API response
  await page.route('**/api/v1/todos**', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        items: [
          { id: '1', title: 'Mocked Todo 1', is_completed: false },
          { id: '2', title: 'Mocked Todo 2', is_completed: true },
        ],
        total: 2,
      }),
    });
  });

  await page.goto('/');

  // Verify mocked data
  await expect(page.getByText('Mocked Todo 1')).toBeVisible();
  await expect(page.getByText('Mocked Todo 2')).toBeVisible();
});

test('should handle API error', async ({ page }) => {
  // Mock API error
  await page.route('**/api/v1/todos**', async (route) => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Server error' }),
    });
  });

  await page.goto('/');

  // Verify error message
  await expect(page.getByText(/error|failed/i)).toBeVisible();
});

Visual Regression Testing

Compare screenshots to detect unintended UI changes.

e2e/visual.spec.ts
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test';

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

  // Compare full page screenshot
  await expect(page).toHaveScreenshot('home-page.png');
});

test('component screenshot', async ({ page }) => {
  await page.goto('/');

  // Screenshot of a specific component only
  const todoList = page.locator('[data-testid="todo-list"]');
  await expect(todoList).toHaveScreenshot('todo-list.png');
});

test('full page screenshot with interactions', async ({ page }) => {
  await page.goto('/');

  // Screenshot after adding a todo
  await page.getByPlaceholder('Enter a todo').fill('Screenshot test');
  await page.getByRole('button', { name: 'Add' }).click();

  await expect(page).toHaveScreenshot('with-todo.png');
});
Terminal
# First run: generate baseline screenshots
npx playwright test --update-snapshots

# Subsequent runs: compare against baseline
npx playwright test

OS-specific differences

Screenshots may render differently across operating systems. For consistent results in CI, use Docker or configure a threshold.

CI/CD Integration

.github/workflows/e2e.yml
# .github/workflows/e2e.yml
name: E2E Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run E2E tests
        run: npx playwright test

      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Playwright browser caching

To reduce browser installation time in CI, refer to the caching guide.

Debugging Tips

Terminal
# UI mode (most recommended)
npx playwright test --ui

# Debug mode (breakpoint support)
npx playwright test --debug

# Trace recording (trace.zip)
npx playwright test --trace on

# Codegen: convert browser actions to code
npx playwright codegen localhost:5173

UI Mode

Run tests step by step and inspect the DOM state at each step.

Codegen

Interact with the browser directly and test code is automatically generated.

Best Practices

DO

  • ✅ Test only critical user scenarios
  • ✅ Use meaningful selectors
  • ✅ Ensure stability with API mocking
  • ✅ Capture screenshots on failure
  • ✅ Run in parallel on CI

DON'T

  • ❌ Test everything with E2E
  • ❌ Rely on CSS selectors
  • ❌ Use fixed wait times
  • ❌ Share data between tests
  • ❌ Write too many E2E tests

Testing complete!

You've completed the entire testing guide. Now go back to the Learning Roadmap to explore other topics, or start writing tests in your own projects.

Last updated: February 22, 2026 · Version: v0.0.1

Send Feedback

Opens a new issue page with your message.

Open GitHub Issue