225 lines
5.9 KiB
Markdown
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]]
|