import { test, expect } from "@playwright/test"; test("서로 다른 유저는 /projects 에서 자신의 프로젝트만 볼 수 있어야 한다", async ({ page }) => { type Project = { id: string; title: string; slug: string; status: string; createdAt: string; updatedAt: string; }; let currentUser: "A" | "B" = "A"; const projectsA: Project[] = []; const projectsB: Project[] = []; let nextId = 1; const nowIso = () => new Date().toISOString(); await page.route("**/api/auth/me", async (route) => { const email = currentUser === "A" ? "user-a@example.com" : "user-b@example.com"; const id = currentUser === "A" ? "user-a" : "user-b"; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id, email, tokenVersion: 1 }), }); }); await page.route("**/api/projects*", async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); const ownerProjects = currentUser === "A" ? projectsA : projectsB; const otherProjects = currentUser === "A" ? projectsB : projectsA; if (url.pathname === "/api/projects" && method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(ownerProjects), }); return; } if (url.pathname === "/api/projects" && method === "POST") { const bodyText = request.postData() ?? "{}"; const body = JSON.parse(bodyText) as Partial & { title?: string; slug?: string }; const slug = body.slug ?? ""; const title = body.title ?? ""; const conflict = otherProjects.find((p) => p.slug === slug); if (conflict) { await route.fulfill({ status: 409, contentType: "application/json", body: JSON.stringify({ message: "이미 다른 사용자가 사용 중인 프로젝트 주소입니다." }), }); return; } const existingIndex = ownerProjects.findIndex((p) => p.slug === slug); const now = nowIso(); let project: Project; if (existingIndex >= 0) { const existing = ownerProjects[existingIndex]; project = { ...existing, title: title || existing.title, updatedAt: now }; ownerProjects[existingIndex] = project; } else { project = { id: String(nextId++), title: title || "제목 없음", slug, status: "DRAFT", createdAt: now, updatedAt: now, }; ownerProjects.push(project); } await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(project), }); return; } await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ message: "Not implemented in E2E mock" }), }); }); // 1) 유저 A: 에디터에서 프로젝트를 저장하고, /projects 에서 자신의 프로젝트만 보여야 한다. await page.goto("/editor"); const propertiesSidebarA = page.getByTestId("properties-sidebar"); await propertiesSidebarA.getByLabel("프로젝트 제목").fill("유저 A 프로젝트"); await propertiesSidebarA.getByLabel("프로젝트 주소 (slug)").fill("user-a-project"); await page.getByRole("button", { name: "메뉴 ▼" }).click(); await page.getByRole("button", { name: "프로젝트 저장/불러오기" }).click(); await page.getByRole("button", { name: "저장 (로컬 + 서버)" }).click(); await expect(page).toHaveURL(/\/_?projects/); await expect(page.getByText("user-a-project")).toBeVisible(); await expect(page.getByText("user-b-project")).toHaveCount(0); // 2) 유저 B: currentUser 전환 후 별도 프로젝트를 저장하고, /projects 에서 자신의 것만 보여야 한다. currentUser = "B"; await page.goto("/editor"); const propertiesSidebarB = page.getByTestId("properties-sidebar"); await propertiesSidebarB.getByLabel("프로젝트 제목").fill("유저 B 프로젝트"); await propertiesSidebarB.getByLabel("프로젝트 주소 (slug)").fill("user-b-project"); await page.getByRole("button", { name: "메뉴 ▼" }).click(); await page.getByRole("button", { name: "프로젝트 저장/불러오기" }).click(); await page.getByRole("button", { name: "저장 (로컬 + 서버)" }).click(); await expect(page).toHaveURL(/\/_?projects/); await expect(page.getByText("user-b-project")).toBeVisible(); await expect(page.getByText("user-a-project")).toHaveCount(0); // 3) 다시 유저 A 로 전환했을 때, /projects 에서는 유저 A 의 프로젝트만 보여야 한다. currentUser = "A"; await page.goto("/projects"); await expect(page.getByText("user-a-project")).toBeVisible(); await expect(page.getByText("user-b-project")).toHaveCount(0); }); test("프로젝트 목록에서 '폼 제출 내역' 링크를 클릭하면 해당 프로젝트의 제출 내역 페이지로 이동해야 한다", async ({ page }) => { // 로그인된 사용자 시나리오를 가정한다. await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: "user-e2e", email: "e2e@example.com", tokenVersion: 1 }), }); }); // /api/projects 응답을 목킹해 목록 페이지에 하나의 프로젝트가 보이도록 한다. await page.route("**/api/projects", async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); if (url.pathname === "/api/projects" && method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: "1", title: "테스트 프로젝트 A", slug: "test-project-a", status: "DRAFT", createdAt: new Date("2025-01-01T00:00:00.000Z").toISOString(), updatedAt: new Date("2025-01-01T00:00:00.000Z").toISOString(), }, ]), }); return; } await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ message: "Not implemented in E2E mock" }), }); }); // 선택한 프로젝트의 폼 제출 내역 API 응답도 목킹해, 제출 데이터가 테이블에 렌더링되는지 검증한다. await page.route("**/api/projects/test-project-a/submissions", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: "sub-1", projectId: "1", userId: "user-e2e", createdAt: new Date("2025-01-02T12:34:56.000Z").toISOString(), payload: { name: "홍길동", email: "user@example.com", phone: "010-1234-5678", birthdate: "1990-01-01", message: "안녕하세요", }, }, ]), }); }); // 프로젝트 목록 페이지로 이동한다. await page.goto("/projects"); // 목록에 테스트 프로젝트가 보여야 한다. await expect(page.getByText("테스트 프로젝트 A")).toBeVisible(); await expect(page.getByText("test-project-a")).toBeVisible(); // 액션 영역의 "폼 제출 내역" 링크를 클릭하면 제출 내역 페이지로 이동해야 한다. await page.getByRole("link", { name: "폼 제출 내역" }).click(); await expect(page).toHaveURL(/\/projects\/test-project-a\/submissions/); // 제출 내역 페이지의 헤더와 slug, 제출 데이터가 렌더링되어야 한다. await expect(page.getByRole("heading", { name: "폼 제출 내역" })).toBeVisible(); await expect(page.getByText("test-project-a")).toBeVisible(); await expect(page.getByText("홍길동")).toBeVisible(); await expect(page.getByText("user@example.com")).toBeVisible(); await expect(page.getByText("message: 안녕하세요")).toBeVisible(); await expect(page.getByText("1990-01-01")).toBeVisible(); await expect(page.getByText("message: 안녕하세요")).toBeVisible(); }); test("프로젝트 목록 헤더에서 대시보드로 이동할 수 있어야 한다", async ({ page }) => { await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: "user-e2e", email: "e2e@example.com", tokenVersion: 1 }), }); }); await page.route("**/api/projects", async (route) => { const request = route.request(); const url = new URL(request.url()); const method = request.method(); if (url.pathname === "/api/projects" && method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: "1", title: "테스트 프로젝트 A", slug: "test-project-a", status: "DRAFT", createdAt: new Date("2025-01-01T00:00:00.000Z").toISOString(), updatedAt: new Date("2025-01-01T00:00:00.000Z").toISOString(), }, ]), }); return; } await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ message: "Not implemented in E2E mock" }), }); }); await page.route("**/api/dashboard/overview", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ summaryStats: { totalProjects: 1, totalSubmissions: 0, todaySubmissions: 0, last7DaysSubmissions: 0, }, projectStats: [], dailySubmissions: [], }), }); }); await page.goto("/projects"); await expect(page.getByText("테스트 프로젝트 A")).toBeVisible(); await page.getByRole("link", { name: "대시보드" }).click(); await expect(page).toHaveURL(/\/dashboard/); await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible(); }); test("전체 제출 내역 페이지에서 공통 GNB와 메뉴가 보여야 한다", async ({ page }) => { await page.route("**/api/projects/submissions", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]), }); }); await page.route("**/api/auth/logout", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "ok" }), }); }); await page.goto("/projects/submissions"); await expect(page.getByRole("heading", { name: "전체 폼 제출 내역" })).toBeVisible(); const dashboardLink = page.getByRole("link", { name: "대시보드" }); await expect(dashboardLink).toBeVisible(); const projectsLink = page.getByRole("link", { name: "프로젝트 목록" }); await expect(projectsLink).toBeVisible(); const submissionsLink = page.getByRole("link", { name: "전체 제출 내역" }); await expect(submissionsLink).toBeVisible(); const menuButton = page.getByRole("button", { name: "메뉴" }); await expect(menuButton).toBeVisible(); await menuButton.click(); await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible(); });