diff --git a/tests/ai-analysis.spec.ts b/tests/ai-analysis.spec.ts new file mode 100644 index 00000000..e392607e --- /dev/null +++ b/tests/ai-analysis.spec.ts @@ -0,0 +1,105 @@ +import { expect, type Page, test } from "./fixtures/auth"; + +const CARD_TITLE = "통합 광고 성과 AI 요약"; +const MOCK_TOKEN = "mock-ai-token-e2e"; + +/** POST /api/ai/organizations/{orgId}/analysis 모킹 */ +async function mockAiAnalysisRequest(page: Page) { + await page.route( + (url) => + url.pathname.includes("/api/ai/organizations/") && + url.pathname.endsWith("/analysis"), + (route) => + route.fulfill({ + status: 202, + contentType: "application/json", + body: JSON.stringify({ status: "OK", data: MOCK_TOKEN }), + }), + ); +} + +/** GET /api/ai/reports/{token} 모킹 — SUCCESS 즉시 반환 */ +async function mockAiAnalysisResult(page: Page) { + await page.route( + (url) => url.pathname.includes("/api/ai/reports/"), + (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + status: "OK", + data: { + accessToken: MOCK_TOKEN, + status: "SUCCESS", + result: { + strategySuggestion: + "광고 예산을 클릭률이 높은 플랫폼에 집중 배분하세요.", + performanceSummary: + "이번 기간 전체 CTR이 전월 대비 12% 상승했습니다.", + analysisReason: "네이버 광고의 노출 증가가 주요 원인입니다.", + performancePoint: ["CTR 12% 상승", "전환율 8% 개선"], + cautionPoint: ["메타 광고 CPC 상승 추세"], + }, + }, + }), + }), + ); +} + +test.describe("AI 분석 요청 E2E", () => { + test("통합 AI 요약 카드가 접힌 상태로 렌더된다", async ({ + dashboardPage: page, + }) => { + const expandButton = page.getByRole("button", { + name: `${CARD_TITLE} 펼치기`, + }); + await expect(expandButton).toBeVisible(); + await expect(expandButton).toHaveAttribute("aria-expanded", "false"); + }); + + test("카드를 펼치면 분석이 요청되고 로딩 상태가 표시된다", async ({ + dashboardPage: page, + }) => { + await mockAiAnalysisRequest(page); + // 결과를 PENDING으로 고정해 로딩 상태를 유지 + await page.route( + (url) => url.pathname.includes("/api/ai/reports/"), + (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + status: "OK", + data: { accessToken: MOCK_TOKEN, status: "PENDING", result: null }, + }), + }), + ); + + await page.getByRole("button", { name: `${CARD_TITLE} 펼치기` }).click(); + + await expect( + page.getByRole("button", { name: `${CARD_TITLE} 접기` }), + ).toBeVisible(); + + await expect(page.getByLabel("AI 요약 로딩")).toBeVisible(); + }); + + test("분석 결과가 도착하면 핵심 섹션 세 곳이 렌더된다", async ({ + dashboardPage: page, + }) => { + await mockAiAnalysisRequest(page); + await mockAiAnalysisResult(page); + + await page.getByRole("button", { name: `${CARD_TITLE} 펼치기` }).click(); + + await expect( + page.getByRole("heading", { name: "전략 제안" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "주의가 필요한 부분" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "분석 인사이트" }), + ).toBeVisible(); + }); +}); diff --git a/tests/fixtures/auth.ts b/tests/fixtures/auth.ts new file mode 100644 index 00000000..b620dd23 --- /dev/null +++ b/tests/fixtures/auth.ts @@ -0,0 +1,48 @@ +import { expect, type Page, test as base } from "@playwright/test"; + +const email = process.env.E2E_USER_EMAIL; +const password = process.env.E2E_USER_PASSWORD; + +export async function loginAndReachDashboard(page: Page) { + await page.goto("/login"); + await page.getByRole("heading", { name: "로그인" }).waitFor(); + + await page.getByPlaceholder("이메일을 입력하세요").fill(email!); + await page.getByPlaceholder("비밀번호를 입력하세요").fill(password!); + await page.getByRole("button", { name: "로그인하기" }).click(); + + await page.waitForURL(/\/dashboard/); +} + +/** 대시보드 콘텐츠 + 워크스페이스 자동 선택 완료까지 대기 */ +export async function waitForDashboardReady(page: Page) { + await expect(page.getByText("실시간 트래픽 변화")).toBeVisible(); +} + +/** 사이드바가 접혀 있으면 펼친다. click()으로 버튼 존재 여부를 대기 포함해 확인 */ +export async function expandSidebarIfCollapsed(page: Page) { + const expandButton = page.getByRole("button", { name: "사이드바 펼치기" }); + try { + await expandButton.click({ timeout: 3_000 }); + } catch { + // 버튼이 없으면 사이드바가 이미 펼쳐진 상태 + } +} + +type TAuthFixtures = { + dashboardPage: Page; +}; + +export const test = base.extend({ + dashboardPage: async ({ page }, use) => { + base.skip( + !email || !password, + "E2E_USER_EMAIL / E2E_USER_PASSWORD 가 .env 에 필요합니다.", + ); + await loginAndReachDashboard(page); + await waitForDashboardReady(page); + await use(page); + }, +}); + +export { expect, type Page } from "@playwright/test"; diff --git a/tests/login.spec.ts b/tests/login.spec.ts index dac99eaa..93b7803f 100644 --- a/tests/login.spec.ts +++ b/tests/login.spec.ts @@ -1,42 +1,18 @@ -import { expect, test } from "@playwright/test"; - -const email = process.env.E2E_USER_EMAIL; -const password = process.env.E2E_USER_PASSWORD; +import { expandSidebarIfCollapsed, expect, test } from "./fixtures/auth"; test.describe("로그인 E2E", () => { - test.beforeEach(() => { - test.skip( - !email || !password, - "E2E_USER_EMAIL / E2E_USER_PASSWORD 가 .env 에 필요합니다.", - ); - }); - - test("이메일 로그인 후 대시보드에 머문다", async ({ page }) => { - await page.goto("/login"); - await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible(); - - await page.getByPlaceholder("이메일을 입력하세요").fill(email!); - await page.getByPlaceholder("비밀번호를 입력하세요").fill(password!); - await page.getByRole("button", { name: "로그인하기" }).click(); + test("이메일 로그인 후 대시보드에 머문다", async ({ + dashboardPage: page, + }) => { + await expandSidebarIfCollapsed(page); - await expect(page).toHaveURL(/\/dashboard/); - - const sidebar = page.getByRole("navigation", { - name: "사이드바 내비게이션", - }); - const expandSidebar = page.getByRole("button", { name: "사이드바 펼치기" }); - if (await expandSidebar.isVisible()) { - await expandSidebar.click(); - } - await expect(sidebar.getByText("대시보드", { exact: true })).toBeVisible(); + const nav = page.getByRole("navigation", { name: "사이드바 내비게이션" }); + await expect( + nav.getByRole("link", { name: "통합 대시보드", exact: true }), + ).toBeVisible(); await expect( page.getByText("로그인에 실패했습니다.", { exact: true }), ).not.toBeVisible(); - - const aiSummary = page.getByRole("button", { name: "AI 요약하기" }); - if (await aiSummary.isVisible()) { - await expect(aiSummary).toBeVisible(); - } }); }); diff --git a/tests/navigation.spec.ts b/tests/navigation.spec.ts index a12a3c3c..e4c91176 100644 --- a/tests/navigation.spec.ts +++ b/tests/navigation.spec.ts @@ -1,36 +1,7 @@ -import { expect, type Page, test } from "@playwright/test"; - -const email = process.env.E2E_USER_EMAIL; -const password = process.env.E2E_USER_PASSWORD; - -async function loginAndReachDashboard(page: Page) { - await page.goto("/login"); - await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible(); - - await page.getByPlaceholder("이메일을 입력하세요").fill(email!); - await page.getByPlaceholder("비밀번호를 입력하세요").fill(password!); - await page.getByRole("button", { name: "로그인하기" }).click(); - - await expect(page).toHaveURL(/\/dashboard/); -} - -async function expandSidebarIfCollapsed(page: Page) { - const expandSidebar = page.getByRole("button", { name: "사이드바 펼치기" }); - if (await expandSidebar.isVisible()) { - await expandSidebar.click(); - } -} +import { expandSidebarIfCollapsed, expect, test } from "./fixtures/auth"; test.describe("로그인 후 사이드바 네비게이션", () => { - test.beforeEach(() => { - test.skip( - !email || !password, - "E2E_USER_EMAIL / E2E_USER_PASSWORD 가 .env 에 필요합니다.", - ); - }); - - test("통합 대시보드 화면을 확인한다", async ({ page }) => { - await loginAndReachDashboard(page); + test("통합 대시보드 화면을 확인한다", async ({ dashboardPage: page }) => { await expandSidebarIfCollapsed(page); const nav = page.getByRole("navigation", { @@ -52,10 +23,5 @@ test.describe("로그인 후 사이드바 네비게이션", () => { await expect( header.getByRole("link", { name: "통합 대시보드", exact: true }), ).toBeVisible(); - - const aiSummary = page.getByRole("button", { name: "AI 요약하기" }); - if (await aiSummary.isVisible()) { - await expect(aiSummary).toBeVisible(); - } }); }); diff --git a/tsconfig.node.json b/tsconfig.node.json index 4a694cae..21ee27de 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -22,7 +22,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }