harness/skills/zio-harness/references/playwright.md
ythong c25e55b64b feat: harness + zio-harness Claude Code 플러그인 초기 배포
- 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>
2026-06-17 13:48:56 +09:00

5.1 KiB

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

설정

// 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,
  },
});

페이지 오브젝트 패턴

// 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();
  }
}

테스트 픽스처 (로그인 상태 재사용)

// 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';

테스트 작성 패턴

// 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 모킹 (선택적)

// 외부 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)

# .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/

실행 명령

# 전체 테스트
npx playwright test

# 특정 파일
npx playwright test auth.spec.ts

# UI 모드 (인터랙티브)
npx playwright test --ui

# 리포트 열기
npx playwright show-report