292 lines
9.8 KiB
TypeScript
292 lines
9.8 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
|
|
|
import AllProjectSubmissionsPage from "@/app/projects/submissions/page";
|
|
|
|
// /projects/submissions 페이지 유닛 테스트
|
|
// - /api/projects/submissions 응답을 테이블로 렌더링하는지
|
|
// - 401/404/기타 에러에 대해 올바른 메시지를 표시하는지
|
|
// - 상단에 "프로젝트 목록" 링크가 있고 클릭 시 /projects 로 이동하는지
|
|
|
|
export const pushMock = vi.fn();
|
|
|
|
vi.mock("next/navigation", () => {
|
|
return {
|
|
__esModule: true,
|
|
useRouter: () => ({ push: pushMock }),
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.unstubAllGlobals();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
|
it("/api/projects/submissions 응답을 프로젝트/필드 정보가 포함된 테이블로 렌더링해야 한다", async () => {
|
|
const submissions = [
|
|
{
|
|
id: "1",
|
|
createdAt: "2025-01-01T12:00:00.000Z",
|
|
projectSlug: "proj-1",
|
|
projectTitle: "프로젝트 1",
|
|
payload: {
|
|
name: "홍길동",
|
|
email: "user@example.com",
|
|
phone: "010-1234-5678",
|
|
birthdate: "1990-01-01",
|
|
message: "안녕하세요",
|
|
},
|
|
},
|
|
{
|
|
id: "2",
|
|
createdAt: "2025-01-02T08:30:00.000Z",
|
|
projectSlug: "proj-2",
|
|
projectTitle: "프로젝트 2",
|
|
payload: {
|
|
name: "김철수",
|
|
email: "another@example.com",
|
|
phone: "010-0000-0000",
|
|
birthdate: "1995-05-05",
|
|
message: "두 번째 문의입니다.",
|
|
},
|
|
},
|
|
];
|
|
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify(submissions), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
const [url, options] = fetchMock.mock.calls[0] as any;
|
|
expect(url).toBe("/api/projects/submissions");
|
|
expect(options).toBeUndefined();
|
|
|
|
expect(await screen.findByText("All form submissions")).toBeTruthy();
|
|
|
|
expect(screen.getByText("프로젝트 1")).toBeTruthy();
|
|
expect(screen.getByText("proj-1")).toBeTruthy();
|
|
expect(screen.getByText("홍길동")).toBeTruthy();
|
|
expect(screen.getByText("user@example.com")).toBeTruthy();
|
|
expect(screen.getByText("010-1234-5678")).toBeTruthy();
|
|
expect(screen.getByText("1990-01-01")).toBeTruthy();
|
|
expect(screen.getByText("message: 안녕하세요")).toBeTruthy();
|
|
|
|
expect(screen.getByText("프로젝트 2")).toBeTruthy();
|
|
expect(screen.getByText("proj-2")).toBeTruthy();
|
|
expect(screen.getByText("김철수")).toBeTruthy();
|
|
expect(screen.getByText("another@example.com")).toBeTruthy();
|
|
expect(screen.getByText("010-0000-0000")).toBeTruthy();
|
|
expect(screen.getByText("1995-05-05")).toBeTruthy();
|
|
expect(screen.getByText("message: 두 번째 문의입니다.")).toBeTruthy();
|
|
});
|
|
|
|
it("API 가 401 을 반환하면 로그인 페이지로 리다이렉트해야 한다", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({ message: "폼 제출 내역을 조회하려면 로그인이 필요합니다." }),
|
|
{
|
|
status: 401,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(pushMock).toHaveBeenCalledWith("/login");
|
|
});
|
|
});
|
|
|
|
it("API 가 500 등을 반환하면 에러 메시지를 화면에 표시해야 한다", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify({ message: "서버 에러" }), {
|
|
status: 500,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
const errorText = await screen.findByText(
|
|
"An error occurred while loading form submissions. Please try again later.",
|
|
);
|
|
|
|
expect(errorText).toBeTruthy();
|
|
});
|
|
|
|
it("상단에 '프로젝트 목록' 링크가 있고 클릭 시 /projects 로 이동해야 한다", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify([]), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
const backLink = await screen.findByRole("link", { name: "Projects" });
|
|
expect(backLink).toBeTruthy();
|
|
expect(backLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
|
});
|
|
|
|
it("상단 GNB 에 대시보드/프로젝트 목록/전체 제출 내역 링크가 있어야 한다", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify([]), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
|
expect(dashboardLink.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
|
|
|
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
|
expect(projectsLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
|
|
|
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
|
expect(submissionsLink.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
|
});
|
|
|
|
it("전체 제출 내역 테이블은 라이트 테마에서 읽기 쉬운 텍스트 색상을 사용해야 한다", async () => {
|
|
const submissions = [
|
|
{
|
|
id: "1",
|
|
createdAt: "2025-01-01T12:00:00.000Z",
|
|
projectSlug: "proj-1",
|
|
projectTitle: "프로젝트 1",
|
|
payload: {
|
|
name: "홍길동",
|
|
email: "user@example.com",
|
|
phone: "010-1234-5678",
|
|
birthdate: "1990-01-01",
|
|
message: "안녕하세요",
|
|
},
|
|
},
|
|
];
|
|
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify(submissions), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
const { container } = render(<AllProjectSubmissionsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
const headerRow = container.querySelector("thead tr") as HTMLElement | null;
|
|
expect(headerRow).not.toBeNull();
|
|
|
|
const headerClass = headerRow!.className;
|
|
expect(headerClass).toContain("border-slate-200");
|
|
expect(headerClass).toContain("text-slate-600");
|
|
expect(headerClass).toContain("dark:border-slate-800");
|
|
expect(headerClass).toContain("dark:text-slate-400");
|
|
|
|
const nameCellNode = await screen.findByText("홍길동");
|
|
const nameCell = nameCellNode.closest("td") as HTMLElement | null;
|
|
expect(nameCell).not.toBeNull();
|
|
|
|
const nameClass = nameCell!.className;
|
|
expect(nameClass).toContain("text-slate-900");
|
|
expect(nameClass).toContain("dark:text-slate-100");
|
|
});
|
|
|
|
it("GNB에서 현재 페이지인 '전체 제출 내역' 탭에 aria-current=\"page\"가 설정되어야 한다", async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue(
|
|
new Response(JSON.stringify([]), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
|
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
|
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
|
|
|
expect(submissionsLink.getAttribute("href")).toBe("/projects/submissions");
|
|
expect(submissionsLink.getAttribute("aria-current")).toBe("page");
|
|
|
|
expect(dashboardLink.getAttribute("aria-current")).toBeNull();
|
|
expect(projectsLink.getAttribute("aria-current")).toBeNull();
|
|
});
|
|
|
|
it("상단 메뉴 안 '로그아웃' 버튼을 클릭하면 /api/auth/logout 으로 POST 요청을 보내고 /login 으로 이동해야 한다", async () => {
|
|
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
|
const url = typeof input === "string" ? input : input.url;
|
|
const method = init?.method ?? "GET";
|
|
|
|
if (url === "/api/projects/submissions" && method === "GET") {
|
|
return Promise.resolve(
|
|
new Response(JSON.stringify([]), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (url === "/api/auth/logout" && method === "POST") {
|
|
return Promise.resolve(new Response(JSON.stringify({ message: "ok" }), { status: 200 }));
|
|
}
|
|
|
|
return Promise.resolve(new Response(null, { status: 500 }));
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock as any);
|
|
|
|
render(<AllProjectSubmissionsPage />);
|
|
|
|
const menuButton = await screen.findByRole("button", { name: "Menu" });
|
|
menuButton.click();
|
|
|
|
const logoutButton = await screen.findByRole("button", { name: "Log out" });
|
|
logoutButton.click();
|
|
|
|
await waitFor(() => {
|
|
const logoutCall = fetchMock.mock.calls.find(([url, options]) => {
|
|
return url === "/api/auth/logout" && options?.method === "POST";
|
|
});
|
|
|
|
expect(logoutCall).toBeDefined();
|
|
expect(pushMock).toHaveBeenCalledWith("/login");
|
|
});
|
|
});
|
|
});
|