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-testidor 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('[email protected]');
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'inusefor debugging (disable in high-volume CI if needed). - Trace: keep
trace: 'on-first-retry'— openplaywright show-trace trace.zipafter 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)
| Topic | Playwright | Cypress |
|---|---|---|
| Browsers | Chromium, Firefox, WebKit in one runner | Chrome, Edge, Firefox (WebKit limited) |
| Multi-tab / multi-origin | Strong | More constraints |
| Architecture | Out-of-process driver | In-browser (with trade-offs) |
| Parallel CI | Built-in sharding | Often 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
beforeEachor 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 rawplaywrightfor assertions and fixtures. page.routecontrols 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.tswithbaseURL, traces, andwebServer - Convert critical user journeys into spec files
- Introduce API mocks for flaky dependencies
- Turn on HTML + trace artifacts in CI
- Add
npm run test:e2eto 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: