Files
2nd/10_Wiki/Topics/Coding/Testing_Playwright_Advanced.md
T
2026-05-09 21:08:02 +09:00

225 lines
5.9 KiB
Markdown

---
id: testing-playwright-advanced
title: Playwright 심화 — Trace / Auth / Parallel
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [testing, playwright, e2e, vibe-coding]
tech_stack: { language: "TS / Playwright", applicable_to: ["Frontend"] }
applied_in: []
aliases: [Playwright, trace viewer, storage state, fixtures, page object, auto-wait]
---
# 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.
## 💻 코드 패턴
### 기본
```ts
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)
```ts
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)
```ts
// 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;
```
```ts
// playwright.config.ts
export default defineConfig({
globalSetup: './global-setup',
use: { storageState: 'state.json' },
});
```
→ 모든 test 가 이미 로그인 상태로 시작.
### 다양한 user (worker storage)
```ts
// 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
```ts
// playwright.config.ts
use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure' }
```
```bash
# 실패 후 trace 보기
npx playwright show-trace trace.zip
```
### Network mocking
```ts
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
```ts
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)
```ts
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
```ts
await expect(page).toHaveScreenshot('home.png', {
maxDiffPixelRatio: 0.01,
});
```
→ 첫 run 시 저장, 이후 비교.
### Parallel + sharding
```bash
npx playwright test --workers=4
npx playwright test --shard=1/4 # CI 분산
```
### Retry on flake
```ts
// config
retries: process.env.CI ? 2 : 0,
```
### Page object (재사용)
```ts
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.
## 🔗 관련 문서
- [[Mobile_E2E_Testing]]
- [[Frontend_A11y_Testing]]
- [[Testing_Mocking_Boundaries]]