用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/puk0806/gugbab-claude --skill e2e-testing命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
| name | e2e-testing |
| description | Playwright E2E 테스트 — 설치, 설정, 로케이터, POM, 네트워크 모킹, 인증, 비주얼 회귀, CI, 디버깅 전략 |
소스: https://playwright.dev/docs/intro | https://playwright.dev/docs/best-practices 검증일: 2026-08-26 (최초 2026-04-20 · 08-26 freshness 재검증: 로케이터 우선순위·
page.route·defineConfig·fixture 확장 전부 현행, 버전 문자열만 갱신) 대상 버전: Playwright v1.62.x (최신 안정, 2026-08 기준)
# 신규 프로젝트 초기화 (권장)
npm init playwright@latest
# 기존 프로젝트에 추가
npm install -D @playwright/test
npx playwright install --with-deps
초기화 시 생성되는 파일:
playwright.config.ts — 테스트 설정tests/example.spec.ts — 예시 테스트tests-examples/ — 추가 예제.github/workflows/playwright.yml — CI 워크플로 (선택)import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI, // CI에서 test.only 방지
retries: process.env.CI ? 2 : 0, // CI에서만 재시도
workers: process.env.CI ? 2 : undefined,
reporter: [
['html'],
['list'],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // 실패 시 트레이스 수집
screenshot: 'only-on-failure',
},
projects: [
// 인증 셋업 프로젝트
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
// 모바일 뷰포트
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
dependencies: ['setup'],
},
],
// 개발 서버 자동 실행
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
import { test, expect } from '@playwright/test';
test.describe('로그인 플로우', () => {
test('유효한 자격 증명으로 로그인 성공', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill('user@example.com');
await page.getByLabel('비밀번호').fill('password123');
await page.getByRole('button', { name: '로그인' }).click();
// Web-first assertion — 조건 충족까지 자동 재시도 (기본 5초)
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: '대시보드' })).toBeVisible();
});
test('잘못된 비밀번호로 에러 표시', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('이메일').fill();
page.().();
page.(, { : }).();
(page.()).();
});
});
여러 검증을 한 번에 수집하여 모든 실패를 한꺼번에 확인:
test('프로필 페이지 요소 확인', async ({ page }) => {
await page.goto('/profile');
// 실패해도 테스트를 중단하지 않음
await expect.soft(page.getByText('이름')).toBeVisible();
await expect.soft(page.getByText('이메일')).toBeVisible();
await expect.soft(page.getByText('가입일')).toBeVisible();
});
| 순위 | 로케이터 | 용도 |
|---|---|---|
| 1 | getByRole | ARIA 역할 기반 (접근성과 일치, 가장 견고) |
| 2 | getByLabel | 폼 필드 (label과 연결된 input) |
| 3 | getByPlaceholder | placeholder가 유일한 식별자일 때 |
| 4 | getByText | 텍스트 콘텐츠로 식별 |
| 5 | getByTestId | 시맨틱 식별이 불가능할 때 (data-testid) |
// 특정 리스트 아이템 내의 버튼 클릭
await page
.getByRole('listitem')
.filter({ hasText: '상품 A' })
.getByRole('button', { name: '삭제' })
.click();
// 나쁨 — CSS 선택자 (구현 종속, 리팩터링에 깨짐)
page.locator('.btn-primary.login-btn');
// 나쁨 — XPath (가독성 저하, 유지보수 어려움)
page.locator('//button[@class="submit"]');
// 좋음 — 사용자가 보는 것과 동일
page.getByRole('button', { name: '로그인' });
// e2e/pages/LoginPage.ts
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('이메일');
this.passwordInput = page.getByLabel('비밀번호');
this.submitButton = page.getByRole('button', { name: '로그인' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
..();
}
() {
..(email);
..(password);
..();
}
}
// e2e/tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('로그인 성공', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
// e2e/fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';
// e2e/tests/login.spec.ts
import { test, expect } from '../fixtures';
test('로그인 성공', async ({ loginPage, page }) => {
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
test('상품 목록 표시', async ({ page }) => {
// API 응답을 가로채서 목 데이터 반환
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
json: [
{ id: 1, name: '상품 A', price: 10000 },
{ id: 2, name: '상품 B', price: 20000 },
],
});
});
await page.goto('/products');
await expect(page.getByText('상품 A')).toBeVisible();
await expect(page.getByText('상품 B')).toBeVisible();
});
test('가격에 할인 적용', async ({ page }) => {
await page.route('**/api/products', async (route) => {
const response = await route.fetch();
const json = await response.json();
// 가격에 50% 할인 적용
const discounted = json.map((p: any) => ({
...p,
price: p.price * 0.5,
}));
await route.fulfill({ response, json: discounted });
});
await page.goto('/products');
});
// 1단계: HAR 녹화 (update: true)
test('API 응답 녹화', async ({ page }) => {
await page.routeFromHAR('e2e/fixtures/products.har', {
url: '**/api/products',
update: true, // 실제 API 호출하여 HAR 파일 생성
});
await page.goto('/products');
});
// 2단계: HAR 재생 (update 제거)
test('녹화된 API로 테스트', async ({ page }) => {
await page.routeFromHAR('e2e/fixtures/products.har', {
url: '**/api/products',
// update 없음 -> HAR 파일에서 응답 서빙
});
await page.goto('/products');
await expect(page.getByText('상품 A')).toBeVisible();
});
상세 레퍼런스 (예제·고급 패턴·흔한 실수) →
references/REFERENCE.md