본문으로 건너뛰기
Previous
Next
Playwright Complete Guide: End-to-End Testing, Automation...

Playwright Complete Guide: End-to-End Testing, Automation & Cross-Browser Testing

Playwright Complete Guide: End-to-End Testing, Automation & Cross-Browser Testing

이 글의 핵심

Playwright is a Microsoft-developed E2E testing framework with unified API for Chromium, Firefox, and WebKit. Features parallel execution, auto-wait, powerful selectors, and built-in debugging tools. Learn installation, selectors, API mocking, CI/CD integration, and production troubleshooting.

What is Playwright?

Playwright is an end-to-end testing framework developed by Microsoft for modern web applications. It provides a unified API for testing across Chromium, Firefox, and WebKit browsers.

Real-world experience: Migrating from Cypress to Playwright achieved 3x faster test execution and simplified cross-browser testing with a single API.

Why Playwright?

Scenario 1: Cross-Browser Testing is Hard

Selenium requires different setups for each browser. Playwright uses one API for all.

Scenario 2: Slow Test Execution

Parallel execution needed. Playwright supports it by default.

Scenario 3: Difficult Debugging

Error tracking is painful. Playwright provides powerful debugging tools built-in.


Key Features

  • Cross-Browser: Chromium, Firefox, WebKit with single API
  • Fast Execution: Built-in parallel test execution
  • Auto-Wait: Automatically waits for elements
  • Powerful Selectors: CSS, XPath, Text, Role-based
  • Screenshots & Videos: Automatic capture on failures
  • API Mocking: Built-in request interception
  • Trace Viewer: Time-travel debugging
  • Mobile Emulation: Test responsive designs

Installation & Setup

Initialize Project

npm init playwright@latest

This creates:

  • playwright.config.ts - Configuration file
  • tests/ - Test directory
  • .github/workflows/playwright.yml - CI workflow

Configuration

// 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 ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost: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://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Basic Testing

Login Test Example

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

test('successful login', async ({ page }) => {
  await page.goto('/login');
  
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Dashboard');
});

test('failed login shows error', async ({ page }) => {
  await page.goto('/login');
  
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'wrongpass');
  await page.click('button[type="submit"]');
  
  await expect(page.locator('.error')).toContainText('Invalid credentials');
});

Run Tests

# Run all tests
npx playwright test

# Run specific test file
npx playwright test login.spec.ts

# Run in headed mode
npx playwright test --headed

# Debug mode
npx playwright test --debug

Selectors

CSS Selectors

await page.click('button.submit');
await page.fill('#email', '[email protected]');
await page.locator('.card > h2').click();

Text Selectors

await page.click('text=Login');
await page.click('button:has-text("Submit")');
await page.locator('h2:has-text("Welcome")').isVisible();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByRole('heading', { name: 'Dashboard' }).isVisible();

Data-TestId

// HTML: <button data-testid="submit-btn">Submit</button>
await page.getByTestId('submit-btn').click();

Chaining Selectors

await page
  .locator('.card')
  .filter({ hasText: 'Premium' })
  .getByRole('button', { name: 'Buy Now' })
  .click();

Interactions

Click Actions

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

// Double click
await page.dblclick('button');

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

// With modifiers
await page.click('button', { modifiers: ['Shift'] });

Form Inputs

// Text input
await page.fill('input[name="email"]', '[email protected]');

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

// Radio button
await page.check('input[value="male"]');

// Select dropdown
await page.selectOption('select[name="country"]', 'USA');

// File upload
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');

Keyboard & Mouse

// Type with delay
await page.type('input', 'Hello', { delay: 100 });

// Press keys
await page.press('input', 'Enter');
await page.keyboard.press('Control+A');

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

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

Assertions

Page Assertions

await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page).toHaveTitle('Dashboard');

Element Assertions

// Visibility
await expect(page.locator('.error')).toBeVisible();
await expect(page.locator('.success')).toBeHidden();

// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Wel');

// Count
await expect(page.locator('.item')).toHaveCount(5);

// Attributes
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('input')).toHaveAttribute('placeholder', 'Email');

// CSS
await expect(page.locator('.active')).toHaveCSS('color', 'rgb(255, 0, 0)');

API Mocking

Mock Responses

test('mock API response', async ({ page }) => {
  await page.route('**/api/users', (route) => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ]),
    });
  });

  await page.goto('/users');
  await expect(page.locator('.user')).toHaveCount(2);
});

Intercept & Modify

test('modify API response', async ({ page }) => {
  await page.route('**/api/products', async (route) => {
    const response = await route.fetch();
    const json = await response.json();
    
    // Modify data
    json.items = json.items.slice(0, 5);
    
    route.fulfill({ response, json });
  });

  await page.goto('/products');
});

Abort Requests

test('block analytics', async ({ page }) => {
  await page.route('**/analytics/**', (route) => route.abort());
  await page.goto('/');
});

Fixtures & Hooks

Test Hooks

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

test.beforeEach(async ({ page }) => {
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
});

test.afterEach(async ({ page }) => {
  await page.click('button.logout');
});

test('user dashboard', async ({ page }) => {
  await expect(page.locator('h1')).toContainText('Dashboard');
});

Custom Fixtures

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

type MyFixtures = {
  authenticatedPage: Page;
};

const test = base.extend<MyFixtures>({
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('input[name="email"]', '[email protected]');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await use(page);
  },
});

test('authenticated test', async ({ authenticatedPage }) => {
  await expect(authenticatedPage.locator('h1')).toContainText('Dashboard');
});

Screenshots & Videos

Screenshots

// Full page screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });

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

// In config
use: {
  screenshot: 'only-on-failure', // or 'on', 'off'
}

Videos

// In config
use: {
  video: 'retain-on-failure', // or 'on', 'off', 'on-first-retry'
}

Debugging

UI Mode

npx playwright test --ui

Interactive mode with time-travel debugging.

Debug Mode

npx playwright test --debug

Opens Playwright Inspector with step-by-step execution.

Trace Viewer

// In config
use: {
  trace: 'on-first-retry', // or 'on', 'off', 'retain-on-failure'
}
# View trace
npx playwright show-trace trace.zip

Pause Test

test('debug test', async ({ page }) => {
  await page.goto('/');
  await page.pause(); // Execution pauses here
  await page.click('button');
});

CI/CD Integration

GitHub Actions

name: Playwright Tests

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

jobs:
  test:
    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 Playwright tests
        run: npx playwright test
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

Docker

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]

Mobile Testing

Device Emulation

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

const iPhone = devices['iPhone 13'];
const pixel = devices['Pixel 5'];

test.use(iPhone);

test('mobile test', async ({ page }) => {
  await page.goto('/');
  // Test mobile UI
});

Responsive Testing

test('responsive design', async ({ page }) => {
  // Desktop
  await page.setViewportSize({ width: 1920, height: 1080 });
  await page.goto('/');
  
  // Tablet
  await page.setViewportSize({ width: 768, height: 1024 });
  await page.reload();
  
  // Mobile
  await page.setViewportSize({ width: 375, height: 667 });
  await page.reload();
});

Advanced Patterns

Page Object Model

// pages/LoginPage.ts
export 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.locator('.error').textContent();
  }
}

// tests/login.spec.ts
import { LoginPage } from '../pages/LoginPage';

test('login with POM', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('[email protected]', 'password123');
  await expect(page).toHaveURL('/dashboard');
});

Parallel Execution

// playwright.config.ts
export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 2 : undefined,
});

Test Retry

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
});

Comparison: Playwright vs Cypress vs Selenium

FeaturePlaywrightCypressSelenium
Cross-BrowserYes (all)LimitedYes (all)
Parallel ExecutionBuilt-inPaidManual
Auto-WaitYesYesNo
Multi-TabYesNoYes
Network ControlFullLimitedNo
Mobile TestingYesNoYes
Language SupportJS/TS/Python/JavaJS/TS onlyAll
Learning CurveLowLowHigh
CI IntegrationExcellentExcellentGood

When to use:

  • Playwright: Modern apps, cross-browser, parallel execution
  • Cypress: React/Vue apps, developer-friendly
  • Selenium: Legacy apps, multi-language teams

Production Best Practices

Stable Selectors

// ❌ Bad: Fragile selectors
await page.click('div > div > button:nth-child(3)');

// ✅ Good: Semantic selectors
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByTestId('submit-btn').click();

Error Handling

test('handle network errors', async ({ page }) => {
  page.on('response', (response) => {
    if (!response.ok()) {
      console.error(`Failed: ${response.url()} - ${response.status()}`);
    }
  });

  await page.goto('/');
});

Performance Monitoring

test('page load performance', async ({ page }) => {
  const startTime = Date.now();
  await page.goto('/');
  const loadTime = Date.now() - startTime;
  
  expect(loadTime).toBeLessThan(3000); // 3 seconds
});

Troubleshooting

IssueCauseSolution
Flaky testsRace conditionsUse auto-wait, avoid setTimeout
Slow executionSerial testsEnable parallel execution
Element not foundWrong selectorUse Playwright Inspector
Timeout errorsSlow page loadIncrease timeout, optimize app
CI failuresMissing dependenciesUse —with-deps flag
Video too largeLong testsUse retain-on-failure

Summary & Checklist

Key Points

  • Cross-Browser: One API for Chromium, Firefox, WebKit
  • Fast: Built-in parallel execution
  • Reliable: Auto-wait and retry mechanisms
  • Powerful: API mocking, tracing, debugging
  • CI-Ready: GitHub Actions, Docker support

Implementation Checklist

  • Install Playwright and configure projects
  • Write tests with semantic selectors
  • Set up CI/CD pipeline
  • Configure screenshots and videos
  • Implement Page Object Model
  • Enable parallel execution
  • Add retry logic for CI
  • Monitor test performance

  • [Cypress Complete Guide](/en/blog/cypress-complete-guide/
  • [Vitest Complete Guide](/en/blog/vitest-complete-guide/
  • [Jest Complete Guide](/en/blog/jest-complete-guide/

Keywords

Playwright, E2E Testing, Automation, Cross-Browser, CI/CD, Quality Assurance, Testing

Frequently Asked Questions (FAQ)

Q. Can Playwright test mobile apps?

A. No, Playwright tests web applications only. For mobile apps, use Appium or native testing frameworks. However, Playwright can emulate mobile browsers.

Q. How to handle authentication?

A. Use storage state to save authenticated sessions and reuse them across tests, avoiding repeated login flows.

Q. Does Playwright support TypeScript?

A. Yes, full TypeScript support with excellent type definitions out of the box.

Q. How to test file downloads?

A. Use page.waitForEvent('download') to intercept downloads and verify file contents.

Q. Can I run tests in parallel on different browsers?

A. Yes, configure multiple projects in playwright.config.ts. Each project runs in parallel by default.

Q. How to debug failing tests in CI?

A. Enable trace and artifacts upload in CI. Download trace.zip and view it with npx playwright show-trace.


For deployment: git addgit commitgit pushnpm run deploy.