Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions tests/ai-analysis.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
48 changes: 48 additions & 0 deletions tests/fixtures/auth.ts
Original file line number Diff line number Diff line change
@@ -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<TAuthFixtures>({
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";
42 changes: 9 additions & 33 deletions tests/login.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
38 changes: 2 additions & 36 deletions tests/navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -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", {
Expand All @@ -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();
}
});
});
1 change: 0 additions & 1 deletion tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,

"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
Expand Down
Loading