대시보드, GNB 추가
This commit is contained in:
@@ -146,11 +146,96 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
|
||||
const backLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
expect(backLink).toBeTruthy();
|
||||
expect(backLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
});
|
||||
|
||||
backLink.click();
|
||||
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: "대시보드" });
|
||||
expect(dashboardLink.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
||||
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
expect(projectsLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
expect(submissionsLink.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
||||
});
|
||||
|
||||
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(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
|
||||
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: "메뉴" });
|
||||
menuButton.click();
|
||||
|
||||
const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
||||
|
||||
import DashboardPage from "@/app/dashboard/page";
|
||||
|
||||
export const pushMock = vi.fn();
|
||||
|
||||
vi.mock("next/navigation", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
useRouter: () => ({ push: pushMock }),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("DashboardPage - GNB와 테마", () => {
|
||||
it("GNB에서 현재 페이지인 '대시보드' 탭에 aria-current=\"page\"가 설정되어야 한다", async () => {
|
||||
const overview = {
|
||||
summaryStats: {
|
||||
totalProjects: 0,
|
||||
totalSubmissions: 0,
|
||||
todaySubmissions: 0,
|
||||
last7DaysSubmissions: 0,
|
||||
},
|
||||
projectStats: [],
|
||||
dailySubmissions: [],
|
||||
};
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(overview), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<DashboardPage />);
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
|
||||
expect(dashboardLink.getAttribute("href")).toBe("/dashboard");
|
||||
expect(dashboardLink.getAttribute("aria-current")).toBe("page");
|
||||
|
||||
expect(projectsLink.getAttribute("aria-current")).toBeNull();
|
||||
expect(submissionsLink.getAttribute("aria-current")).toBeNull();
|
||||
});
|
||||
|
||||
it("헤더에 테마 전환 버튼이 있고 클릭 시 html 요소의 dark 클래스가 토글되어야 한다", async () => {
|
||||
const overview = {
|
||||
summaryStats: {
|
||||
totalProjects: 0,
|
||||
totalSubmissions: 0,
|
||||
todaySubmissions: 0,
|
||||
last7DaysSubmissions: 0,
|
||||
},
|
||||
projectStats: [],
|
||||
dailySubmissions: [],
|
||||
};
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(overview), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<DashboardPage />);
|
||||
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(true);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ describe("LoginPage", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("올바른 이메일/비밀번호로 로그인하면 /api/auth/login 으로 요청을 보내고 /projects 로 이동해야 한다", async () => {
|
||||
it("올바른 이메일/비밀번호로 로그인하면 /api/auth/login 으로 요청을 보내고 /dashboard 로 이동해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
const method = init?.method ?? "GET";
|
||||
@@ -82,7 +82,7 @@ describe("LoginPage", () => {
|
||||
expect(body.password).toBe("securePass1");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("LoginPage", () => {
|
||||
expect(errorText).toBeTruthy();
|
||||
});
|
||||
|
||||
it("이미 로그인된 상태에서 /login 에 접근하면 /projects 로 리다이렉트해야 한다", async () => {
|
||||
it("이미 로그인된 상태에서 /login 에 접근하면 /dashboard 로 리다이렉트해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: "1", email: "user@example.com", tokenVersion: 1 }), {
|
||||
status: 200,
|
||||
@@ -144,7 +144,7 @@ describe("LoginPage", () => {
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/me");
|
||||
expect(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,6 +112,119 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
||||
});
|
||||
|
||||
it("헤더에 대시보드 페이지(/dashboard)로 이동하는 링크가 있어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const link = screen.getByText("대시보드");
|
||||
expect(link).toBeTruthy();
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
||||
});
|
||||
|
||||
it("GNB에서 현재 페이지인 '프로젝트 목록' 탭에 aria-current=\"page\"가 설정되어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
|
||||
expect(projectsLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
expect(projectsLink.closest("a")?.getAttribute("aria-current")).toBe("page");
|
||||
|
||||
expect(dashboardLink.closest("a")?.getAttribute("aria-current")).toBeNull();
|
||||
expect(submissionsLink.closest("a")?.getAttribute("aria-current")).toBeNull();
|
||||
});
|
||||
|
||||
it("헤더에 테마 전환 버튼이 있고 클릭 시 html 요소의 dark 클래스가 토글되어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(true);
|
||||
|
||||
themeToggleButton.click();
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
|
||||
it("각 프로젝트 행에는 폼 제출 내역 페이지(/projects/[slug]/submissions)로 이동하는 링크가 있어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
@@ -227,6 +340,44 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("프로젝트 목록 상단 툴바에 '새 프로젝트 만들기' 버튼과 '선택 삭제' 버튼이 함께 보여야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "테스트 프로젝트 B",
|
||||
slug: "test-project-b",
|
||||
status: "PUBLISHED",
|
||||
createdAt: "2025-01-02T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
// 상단 툴바에서 선택 삭제와 새 프로젝트 만들기 버튼이 함께 보여야 한다.
|
||||
const toolbar = await screen.findByTestId("projects-toolbar");
|
||||
|
||||
expect(toolbar.querySelector("button[type='button']")?.textContent).toContain("선택 삭제");
|
||||
expect(toolbar.querySelector("a[href='/editor?new=1']")?.textContent).toContain("새 프로젝트 만들기");
|
||||
});
|
||||
|
||||
it("체크박스로 여러 프로젝트를 선택한 뒤 '선택 삭제' 버튼으로 일괄 삭제할 수 있어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
@@ -336,7 +487,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("헤더의 '로그아웃' 버튼을 클릭하면 /api/auth/logout 으로 POST 요청을 보내고 /login 으로 이동해야 한다", async () => {
|
||||
it("헤더의 메뉴 안 '로그아웃' 버튼을 클릭하면 /api/auth/logout 으로 POST 요청을 보내고 /login 으로 이동해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
@@ -376,6 +527,9 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const menuButton = await screen.findByRole("button", { name: "메뉴" });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("SignupPage", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("새 이메일/비밀번호로 회원가입하면 /api/auth/signup 으로 요청을 보내고 /projects 로 이동해야 한다", async () => {
|
||||
it("새 이메일/비밀번호로 회원가입하면 /api/auth/signup 으로 요청을 보내고 /dashboard 로 이동해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
const method = init?.method ?? "GET";
|
||||
@@ -82,7 +82,7 @@ describe("SignupPage", () => {
|
||||
expect(body.password).toBe("securePass1");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("SignupPage", () => {
|
||||
expect(errorText).toBeTruthy();
|
||||
});
|
||||
|
||||
it("이미 로그인된 상태에서 /signup 에 접근하면 /projects 로 리다이렉트해야 한다", async () => {
|
||||
it("이미 로그인된 상태에서 /signup 에 접근하면 /dashboard 로 리다이렉트해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: "1", email: "user@example.com", tokenVersion: 1 }), {
|
||||
status: 200,
|
||||
@@ -144,7 +144,7 @@ describe("SignupPage", () => {
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/me");
|
||||
expect(pushMock).toHaveBeenCalledWith("/projects");
|
||||
expect(pushMock).toHaveBeenCalledWith("/dashboard");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user