Playwright Complete Guide | E2E Testing, Cross-Browser Automation & CI/CD

Playwright Complete Guide | E2E Testing, Cross-Browser Automation & CI/CD

이 글의 핵심

Playwright is Microsoft’s cross-browser test runner for Chromium, Firefox, and WebKit. Auto-waiting locators, network interception, traces, and first-class parallelism make it a strong choice for modern E2E suites and CI pipelines.

What This Guide Covers

Playwright runs tests in real browsers with a single API across Chromium, Firefox, and WebKit. Unlike older WebDriver stacks, it uses the browser’s own protocols where possible, keeps tests fast, and gives excellent artifacts when things go wrong.

You will learn how to:

  • Install and configure @playwright/test
  • Write stable tests with locators and assertions
  • Mock APIs with page.route()
  • Use traces, screenshots, and video
  • Run multi-project (browser) matrices and parallel workers
  • Wire everything into GitHub Actions

1. Why Playwright?

Strengths

  • Cross-browser: One codebase exercises Chromium, Firefox, and WebKit (Safari engine).
  • Auto-waiting: Locators retry until timeouts — fewer sleep() hacks.
  • Network control: Intercept and fulfill requests for deterministic tests.
  • Parallelism: Workers and sharding are first-class for CI throughput.
  • Tooling: Trace viewer, codegen (npx playwright codegen), HTML report.

Trade-offs

  • Learning curve vs Cypress for teams used to Cypress’s dashboard-centric workflow.
  • Very dynamic SPAs still need discipline: stable data-testid or role-based selectors.

2. Installation & Setup

Scaffold a project

npm create playwright@latest

Pick TypeScript, add a GitHub Actions workflow when prompted, and install browsers:

npx playwright install --with-deps

playwright.config.ts

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 ? 2 : undefined,
  reporter: [['html', { open: 'never' }], ['list']],
  use: {
    baseURL: 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-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 dev',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

webServer boots your app automatically in CI and locally (unless a server is already running).


3. First Tests

// tests/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login', () => {
  test('redirects to dashboard on success', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('correct-horse-battery');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  });

  test('shows validation errors', async ({ page }) => {
    await page.goto('/login');
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByText('Email is required')).toBeVisible();
  });
});

Prefer getByRole, getByLabel, and getByTestId over brittle CSS. They mirror how assistive tech sees the page and survive refactors better than deep selectors.


4. Locators & Assertions

// Chaining and filtering
const card = page.locator('.card').filter({ hasText: 'Pro plan' });
await card.getByRole('button', { name: 'Upgrade' }).click();

// Soft assertions (collect failures, then report)
await expect.soft(page.getByText('Welcome')).toBeVisible();
await expect.soft(page.getByText('Notifications')).toBeVisible();

Use expect.poll for asynchronous UI state that updates after network calls.


5. API Mocking with page.route

test('lists users from mocked API', async ({ page }) => {
  await page.route('**/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([{ id: '1', name: 'Ada' }]),
    });
  });

  await page.goto('/users');
  await expect(page.getByText('Ada')).toBeVisible();
});

For GraphQL, match on URL and POST body, or stub at the service worker layer if your app uses one.


6. Authentication State

Reuse login by saving storage state once:

// global-setup.ts (simplified)
import { chromium, type FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(config.projects[0].use.baseURL + '/login');
  // perform login...
  await page.context().storageState({ path: 'storage/state.json' });
  await browser.close();
}

export default globalSetup;

Point use.storageState at that file in projects that need an authenticated session.


7. Screenshots, Video, and Traces

  • Screenshots: await page.screenshot({ path: 'shot.png', fullPage: true })
  • Video: set video: 'on' in use for debugging (disable in high-volume CI if needed).
  • Trace: keep trace: 'on-first-retry' — open playwright show-trace trace.zip after failures.

8. Parallelism & Sharding

# Four parallel workers locally
npx playwright test --workers=4

# CI matrix: shard 1 of 3
npx playwright test --shard=1/3

Combine sharding with multiple job runners to keep PR feedback under a few minutes.


9. GitHub Actions

# .github/workflows/playwright.yml
name: Playwright
on: [push, pull_request]
jobs:
  test:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

Upload the HTML report so reviewers can open failures without reproducing locally.


10. Playwright vs Cypress (Quick Comparison)

TopicPlaywrightCypress
BrowsersChromium, Firefox, WebKit in one runnerChrome, Edge, Firefox (WebKit limited)
Multi-tab / multi-originStrongMore constraints
ArchitectureOut-of-process driverIn-browser (with trade-offs)
Parallel CIBuilt-in shardingOften Cypress Cloud for scale

11. Best Practices

Do

  • Stabilize selectors with roles and accessible names.
  • Mock third-party APIs you do not own.
  • Keep tests independent — create data in beforeEach or use isolated tenants.
  • Run the smallest project set locally; full matrix in CI.

Avoid

  • Arbitrary page.waitForTimeout(ms) except when simulating real delays in demos.
  • Sharing mutable global state between tests.
  • Coupling tests to pixel-perfect CSS class names.

Summary & Checklist

Core ideas

  • Playwright Test is the official runner — use @playwright/test, not raw playwright for assertions and fixtures.
  • page.route controls the network layer for fast, deterministic suites.
  • Projects map to browsers/devices; sharding scales CI.
  • Traces are your best friend for debugging intermittent failures.

Checklist

  • Add playwright.config.ts with baseURL, traces, and webServer
  • Convert critical user journeys into spec files
  • Introduce API mocks for flaky dependencies
  • Turn on HTML + trace artifacts in CI
  • Add npm run test:e2e to package scripts and document it in README

More career guides (Korean on pkglog.com)

Deep-dive posts in Korean pair well with this English overview: job posting channels, resume and interview delivery, weekly habits, and practical job-hunting tips. Use them if you read Korean or run pages through translation.

Related posts: