- harness: 도메인 한 줄 → 에이전트 팀 자동 생성 메타 스킬 (v1.2.0) - zio-harness: React + Spring Boot + Mobile 풀스택 개발 에이전트 팀 (v1.0.0) - 에이전트 4종: orchestrator / analyst / bot / agent - PROJECT_MAP.md 폴더 구조 메모리 시스템 - references/: react, spring-boot, mobile, playwright, database, folder-map Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
220 lines
5.1 KiB
Markdown
220 lines
5.1 KiB
Markdown
# Playwright E2E 테스트 가이드
|
|
|
|
## 스택
|
|
- Playwright + TypeScript
|
|
- 페이지 오브젝트 패턴 (POM)
|
|
- CI 통합 (GitHub Actions)
|
|
|
|
---
|
|
|
|
## 폴더 구조
|
|
|
|
```
|
|
e2e/
|
|
├── tests/
|
|
│ ├── auth.spec.ts # 인증 시나리오
|
|
│ ├── users.spec.ts # 사용자 관련 시나리오
|
|
│ └── ...
|
|
├── pages/ # 페이지 오브젝트 (POM)
|
|
│ ├── LoginPage.ts
|
|
│ ├── HomePage.ts
|
|
│ └── BasePage.ts
|
|
├── fixtures/ # 테스트 픽스처 및 헬퍼
|
|
│ ├── auth.fixture.ts # 로그인 상태 픽스처
|
|
│ └── data.ts # 테스트 데이터
|
|
└── playwright.config.ts
|
|
```
|
|
|
|
---
|
|
|
|
## 설정
|
|
|
|
```typescript
|
|
// playwright.config.ts
|
|
import { defineConfig, devices } from '@playwright/test';
|
|
|
|
export default defineConfig({
|
|
testDir: './tests',
|
|
fullyParallel: true,
|
|
retries: process.env.CI ? 2 : 0,
|
|
workers: process.env.CI ? 1 : undefined,
|
|
reporter: 'html',
|
|
use: {
|
|
baseURL: process.env.BASE_URL || 'http://localhost:5173',
|
|
trace: 'on-first-retry',
|
|
screenshot: 'only-on-failure',
|
|
},
|
|
projects: [
|
|
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
{ name: 'Mobile Chrome', use: { ...devices['Pixel 5'] } },
|
|
],
|
|
webServer: {
|
|
command: 'npm run dev',
|
|
url: 'http://localhost:5173',
|
|
reuseExistingServer: !process.env.CI,
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 페이지 오브젝트 패턴
|
|
|
|
```typescript
|
|
// pages/BasePage.ts
|
|
export class BasePage {
|
|
constructor(protected page: Page) {}
|
|
|
|
async goto(path: string) {
|
|
await this.page.goto(path);
|
|
}
|
|
|
|
async waitForLoaded() {
|
|
await this.page.waitForLoadState('networkidle');
|
|
}
|
|
}
|
|
|
|
// pages/LoginPage.ts
|
|
export class LoginPage extends BasePage {
|
|
readonly emailInput = this.page.getByLabel('Email');
|
|
readonly passwordInput = this.page.getByLabel('Password');
|
|
readonly submitButton = this.page.getByRole('button', { name: 'Login' });
|
|
readonly errorMessage = this.page.getByRole('alert');
|
|
|
|
async login(email: string, password: string) {
|
|
await this.goto('/login');
|
|
await this.emailInput.fill(email);
|
|
await this.passwordInput.fill(password);
|
|
await this.submitButton.click();
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 픽스처 (로그인 상태 재사용)
|
|
|
|
```typescript
|
|
// fixtures/auth.fixture.ts
|
|
type AuthFixtures = {
|
|
authenticatedPage: Page;
|
|
loginPage: LoginPage;
|
|
};
|
|
|
|
export const test = base.extend<AuthFixtures>({
|
|
authenticatedPage: async ({ page }, use) => {
|
|
// API로 직접 로그인 (UI 우회 — 빠른 셋업)
|
|
const response = await page.request.post('/api/v1/auth/login', {
|
|
data: { email: 'test@example.com', password: 'password123' }
|
|
});
|
|
const { token } = await response.json();
|
|
|
|
await page.context().addCookies([{ name: 'token', value: token, url: 'http://localhost:5173' }]);
|
|
await use(page);
|
|
},
|
|
|
|
loginPage: async ({ page }, use) => {
|
|
await use(new LoginPage(page));
|
|
},
|
|
});
|
|
|
|
export { expect } from '@playwright/test';
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 작성 패턴
|
|
|
|
```typescript
|
|
// tests/auth.spec.ts
|
|
import { test, expect } from '../fixtures/auth.fixture';
|
|
import { LoginPage } from '../pages/LoginPage';
|
|
|
|
test.describe('인증', () => {
|
|
test('유효한 자격증명으로 로그인 성공', async ({ page }) => {
|
|
const loginPage = new LoginPage(page);
|
|
await loginPage.login('user@example.com', 'password123');
|
|
|
|
await expect(page).toHaveURL('/home');
|
|
await expect(page.getByText('Welcome')).toBeVisible();
|
|
});
|
|
|
|
test('잘못된 비밀번호로 로그인 실패', async ({ page }) => {
|
|
const loginPage = new LoginPage(page);
|
|
await loginPage.login('user@example.com', 'wrong');
|
|
|
|
await expect(loginPage.errorMessage).toContainText('Invalid credentials');
|
|
await expect(page).toHaveURL('/login');
|
|
});
|
|
|
|
test('로그인한 사용자는 대시보드 접근 가능', async ({ authenticatedPage }) => {
|
|
await authenticatedPage.goto('/dashboard');
|
|
await expect(authenticatedPage).toHaveURL('/dashboard');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## API 모킹 (선택적)
|
|
|
|
```typescript
|
|
// 외부 API 모킹 (백엔드 불안정 시)
|
|
test('결제 성공 시나리오', async ({ page }) => {
|
|
await page.route('/api/v1/payments', (route) => {
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ success: true, orderId: 'TEST-001' }),
|
|
});
|
|
});
|
|
|
|
// 테스트 진행...
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## CI 통합 (GitHub Actions)
|
|
|
|
```yaml
|
|
# .github/workflows/e2e.yml
|
|
name: E2E Tests
|
|
on: [push, pull_request]
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with: { node-version: 20 }
|
|
- run: npm ci
|
|
- run: npx playwright install --with-deps chromium
|
|
- run: npx playwright test
|
|
env:
|
|
BASE_URL: http://localhost:5173
|
|
- uses: actions/upload-artifact@v4
|
|
if: failure()
|
|
with:
|
|
name: playwright-report
|
|
path: playwright-report/
|
|
```
|
|
|
|
---
|
|
|
|
## 실행 명령
|
|
|
|
```bash
|
|
# 전체 테스트
|
|
npx playwright test
|
|
|
|
# 특정 파일
|
|
npx playwright test auth.spec.ts
|
|
|
|
# UI 모드 (인터랙티브)
|
|
npx playwright test --ui
|
|
|
|
# 리포트 열기
|
|
npx playwright show-report
|
|
```
|