5.9 KiB
5.9 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| testing-playwright-advanced | Playwright 심화 — Trace / Auth / Parallel | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Playwright 심화
Modern E2E. Auto-wait + trace viewer + parallel + auth state reuse. Cypress 보다 빠르고 multi-browser. CI 핵심: trace + screenshot + storage state.
📖 핵심 개념
- Auto-wait: 자동으로 element 대기.
- Trace: 단계별 screenshot + network + console.
- Storage state: 한 번 로그인 → JSON 저장 → 재사용.
- Fixtures: test 별 setup/teardown.
💻 코드 패턴
기본
import { test, expect } from '@playwright/test';
test('login', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('a@b.com');
await page.getByLabel('Password').fill('pw');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
});
Locator (modern, role-based)
page.getByRole('button', { name: 'Save' });
page.getByLabel('Email');
page.getByPlaceholder('Search…');
page.getByText('Welcome');
page.getByTestId('user-card'); // data-testid
→ 위에서 아래 우선. testId 는 마지막.
Storage state (auth reuse)
// global-setup.ts
import { chromium } from '@playwright/test';
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/login');
await page.getByLabel('Email').fill('a@b.com');
await page.getByLabel('Password').fill('pw');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: 'state.json' });
await browser.close();
}
export default globalSetup;
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup',
use: { storageState: 'state.json' },
});
→ 모든 test 가 이미 로그인 상태로 시작.
다양한 user (worker storage)
// fixtures.ts
type Auths = { adminPage: Page; userPage: Page };
export const test = base.extend<Auths>({
adminPage: async ({ browser }, use) => {
const ctx = await browser.newContext({ storageState: 'admin-state.json' });
await use(await ctx.newPage());
await ctx.close();
},
userPage: async ({ browser }, use) => {
const ctx = await browser.newContext({ storageState: 'user-state.json' });
await use(await ctx.newPage());
await ctx.close();
},
});
test('admin sees panel', async ({ adminPage }) => {
await adminPage.goto('/admin');
await expect(adminPage.getByText('Admin Panel')).toBeVisible();
});
Trace
// playwright.config.ts
use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure' }
# 실패 후 trace 보기
npx playwright show-trace trace.zip
Network mocking
test('handles api error', async ({ page }) => {
await page.route('**/api/users', (route) => route.fulfill({
status: 500,
body: JSON.stringify({ error: 'server error' }),
}));
await page.goto('/users');
await expect(page.getByText('Failed to load')).toBeVisible();
});
API testing
test('create user via API', async ({ request }) => {
const r = await request.post('/api/users', {
data: { email: 'a@b.com' },
});
expect(r.status()).toBe(201);
const body = await r.json();
expect(body.id).toBeTruthy();
});
Component testing (CT)
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './Button';
test('Button click', async ({ mount }) => {
let clicked = false;
const btn = await mount(<Button onClick={() => { clicked = true; }}>Click</Button>);
await btn.click();
expect(clicked).toBe(true);
});
Visual regression
await expect(page).toHaveScreenshot('home.png', {
maxDiffPixelRatio: 0.01,
});
→ 첫 run 시 저장, 이후 비교.
Parallel + sharding
npx playwright test --workers=4
npx playwright test --shard=1/4 # CI 분산
Retry on flake
// config
retries: process.env.CI ? 2 : 0,
Page object (재사용)
class LoginPage {
constructor(private page: Page) {}
async goto() { await this.page.goto('/login'); }
async login(email: string, pw: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(pw);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
test('login', async ({ page }) => {
const p = new LoginPage(page);
await p.goto();
await p.login('a@b.com', 'pw');
});
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Modern E2E web | Playwright |
| Multi-browser (Chrome / FF / Safari) | Playwright |
| Cypress 마이그 | Playwright (속도 + 안정성) |
| Component test | Playwright CT 또는 Vitest + RTL |
| Mobile E2E | Maestro / Detox (Playwright X) |
| API + UI | Playwright (둘 다) |
❌ 안티패턴
waitForTimeout(2000): flake. auto-wait + expect.- CSS selector 의존 (
.btn-primary): 변경 시 깨짐. role / label. - Real DB / external API: flake. mock / seed.
- Trace off prod: 실패 분석 못 함.
- Storage state 안 씀: 매 test 로그인 — 느림.
- Test 간 상태 의존: serial. 독립 setup.
- Sleep 으로 animation 기다림: visible() 또는 Promise.race.
🤖 LLM 활용 힌트
- getByRole > getByLabel > getByTestId.
- Storage state + fixtures = 빠름.
- Trace on-first-retry + screenshot + video retain.