test: Playwright e2e 셋업 + test-prefill-flow 스펙
- @playwright/test devDependency 추가 - playwright.config.ts - e2e/test-prefill-flow.spec.ts - .gitignore: test-results / playwright-report / .playwright 추가main
parent
a805afa39d
commit
7fb1831c91
|
|
@ -6,3 +6,6 @@ dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
.vite
|
.vite
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
|
.playwright
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* /test → 프리필 → 분석 시작 → loading phase 폴링 → /report/:id 도달 풀 플로우.
|
||||||
|
*
|
||||||
|
* 외부 API(YouTube/Apify/Naver/Firecrawl/Perplexity)에 실호출이라
|
||||||
|
* 1 run · serial로 운영. 워커는 `--workers=1` 로 강제.
|
||||||
|
*/
|
||||||
|
import { test, expect, type Page } from '@playwright/test'
|
||||||
|
import { CLINICS } from '../src/features/dev/fixtures/mockUrls'
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'serial' })
|
||||||
|
|
||||||
|
async function pickAndSubmit(page: Page) {
|
||||||
|
await page.goto('/test')
|
||||||
|
|
||||||
|
const heading = page.locator('text=선택된 병원:').first()
|
||||||
|
await expect(heading).toBeVisible({ timeout: 10_000 })
|
||||||
|
const text = await heading.innerText()
|
||||||
|
const match = text.match(/선택된 병원:\s*(.+?)\s*\(/)
|
||||||
|
const label = match?.[1]
|
||||||
|
expect(label, `라벨 파싱 실패: "${text}"`).toBeTruthy()
|
||||||
|
|
||||||
|
const clinic = CLINICS.find((c) => c.label === label)
|
||||||
|
expect(clinic, `${label} 가 CLINICS에 없음`).toBeTruthy()
|
||||||
|
console.log(`[e2e] 선택된 병원: ${label}`)
|
||||||
|
|
||||||
|
if (clinic!.urls.homepage) {
|
||||||
|
await expect(page.getByLabel('홈페이지')).toHaveValue(clinic!.urls.homepage)
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzeBtn = page.getByRole('button', { name: /Analyze/i })
|
||||||
|
await expect(analyzeBtn).toBeEnabled()
|
||||||
|
await analyzeBtn.click()
|
||||||
|
|
||||||
|
await page.waitForURL(/\/report\/loading/, { timeout: 10_000 })
|
||||||
|
console.log(`[e2e] /report/loading 진입 (${new Date().toISOString()})`)
|
||||||
|
return label!
|
||||||
|
}
|
||||||
|
|
||||||
|
test('/test → 프리필 → 분석 시작 → phase 폴링 → /report/:id 도달', async ({ page }) => {
|
||||||
|
// 외부 API 의존 풀 플로우 — discovery(~20s) + collect(~40s) + report(~60s) + plan(~30s)
|
||||||
|
test.setTimeout(8 * 60_000)
|
||||||
|
|
||||||
|
const consoleErrors: string[] = []
|
||||||
|
const pageErrors: string[] = []
|
||||||
|
page.on('pageerror', (e) => pageErrors.push(e.message))
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error') consoleErrors.push(msg.text())
|
||||||
|
})
|
||||||
|
|
||||||
|
await pickAndSubmit(page)
|
||||||
|
|
||||||
|
// ── Phase 1: discovering → discovered ────────────────────────────
|
||||||
|
// discoverChannels 완료 시점에 URL이 /report/loading/<reportId> 로 replace 됨
|
||||||
|
await expect(page.locator('text=Channels discovered')).toBeVisible({ timeout: 90_000 })
|
||||||
|
console.log(`[e2e] ✓ Phase 1: Channels discovered (${new Date().toISOString()})`)
|
||||||
|
await expect(page).toHaveURL(/\/report\/loading\/[A-Za-z0-9-]+/, { timeout: 5_000 })
|
||||||
|
|
||||||
|
// ── Phase 2: collecting → collected ──────────────────────────────
|
||||||
|
await expect(page.locator('text=Data collected')).toBeVisible({ timeout: 180_000 })
|
||||||
|
console.log(`[e2e] ✓ Phase 2: Data collected (${new Date().toISOString()})`)
|
||||||
|
|
||||||
|
// ── Phase 3: generating active 상태 확인 ─────────────────────────
|
||||||
|
// 'Report generated'(완료 라벨)는 planning → complete → navigate 흐름이 빨라
|
||||||
|
// viewport에서 잡히지 않는 경우가 있어, 활성 라벨까지만 확인하고 URL 폴링으로 넘어감
|
||||||
|
await expect(page.locator('text=Generating AI marketing report')).toBeVisible({ timeout: 30_000 })
|
||||||
|
console.log(`[e2e] ✓ Phase 3: Generating report... (${new Date().toISOString()})`)
|
||||||
|
|
||||||
|
// ── 최종 /report/:id 도달 (Perplexity 호출 + plan 생성 포함 — 최대 6분) ──
|
||||||
|
await page.waitForURL(/\/report\/[A-Za-z0-9-]+$/, { timeout: 6 * 60_000 })
|
||||||
|
const finalUrl = page.url()
|
||||||
|
console.log(`[e2e] ✓ 최종 navigate: ${finalUrl} (${new Date().toISOString()})`)
|
||||||
|
expect(finalUrl).not.toContain('/loading')
|
||||||
|
|
||||||
|
// ── Report 페이지 본문 렌더링 확인 ───────────────────────────────
|
||||||
|
// GuestReportPage 로딩 스피너 → ReportBody. 'Marketing Intelligence Report' 헤더가 viewport 상단.
|
||||||
|
await expect(page.locator('text=리포트를 불러오는 중...')).toBeHidden({ timeout: 60_000 })
|
||||||
|
await expect(page.locator('text=Marketing Intelligence Report')).toBeVisible({ timeout: 30_000 })
|
||||||
|
console.log(`[e2e] ✓ Report 페이지 렌더 완료`)
|
||||||
|
|
||||||
|
// ── 콘솔 / 페이지 에러 0건 ────────────────────────────────────────
|
||||||
|
expect(pageErrors, `pageerror: ${pageErrors.join('\n')}`).toHaveLength(0)
|
||||||
|
// 콘솔 에러는 외부 채널 partial failure 등으로 발생할 수 있어 경고만 출력
|
||||||
|
if (consoleErrors.length > 0) {
|
||||||
|
console.warn(`[e2e] 콘솔 에러 ${consoleErrors.length}건 (non-blocking):`)
|
||||||
|
consoleErrors.slice(0, 5).forEach((e) => console.warn(` • ${e}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query-devtools": "^5.59.0",
|
"@tanstack/react-query-devtools": "^5.59.0",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|
@ -1185,6 +1186,22 @@
|
||||||
"openapi3-ts": "4.5.0"
|
"openapi3-ts": "4.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/number": {
|
"node_modules/@radix-ui/number": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||||
|
|
@ -7229,6 +7246,53 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pony-cause": {
|
"node_modules/pony-cause": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"zustand": "^5.0.6"
|
"zustand": "^5.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query-devtools": "^5.59.0",
|
"@tanstack/react-query-devtools": "^5.59.0",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playwright config — /test 페이지를 통한 분석 플로우 E2E 검증용.
|
||||||
|
*
|
||||||
|
* dev 서버는 직접 띄워놓은 상태에서 BASE_URL 환경변수로 포트 지정 (기본 3000).
|
||||||
|
* `BASE_URL=http://localhost:3002 npx playwright test` 처럼 사용.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
workers: 3,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
|
||||||
|
trace: 'retain-on-failure',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue