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 filetests/- 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();
Role-Based Selectors (Recommended)
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
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| Cross-Browser | Yes (all) | Limited | Yes (all) |
| Parallel Execution | Built-in | Paid | Manual |
| Auto-Wait | Yes | Yes | No |
| Multi-Tab | Yes | No | Yes |
| Network Control | Full | Limited | No |
| Mobile Testing | Yes | No | Yes |
| Language Support | JS/TS/Python/Java | JS/TS only | All |
| Learning Curve | Low | Low | High |
| CI Integration | Excellent | Excellent | Good |
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
| Issue | Cause | Solution |
|---|---|---|
| Flaky tests | Race conditions | Use auto-wait, avoid setTimeout |
| Slow execution | Serial tests | Enable parallel execution |
| Element not found | Wrong selector | Use Playwright Inspector |
| Timeout errors | Slow page load | Increase timeout, optimize app |
| CI failures | Missing dependencies | Use —with-deps flag |
| Video too large | Long tests | Use 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
Related Articles
- [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 add → git commit → git push → npm run deploy.