diff --git a/setupTests.ts b/setupTests.ts deleted file mode 100644 index c8cd8d2..0000000 --- a/setupTests.ts +++ /dev/null @@ -1,37 +0,0 @@ -import "@testing-library/jest-dom"; - -/* msw */ - -beforeAll(() => {}); - -afterEach(() => { - /** - * 모킹된 모의 객체 호출에 대한 히스토리를 초기화 - * 모킹된 모듈의 구현을 초기화하지는 않는다. -> 모킹된 상태로 유지됨. - * ⇨ 모킹 모듈 기반으로 작성한 테스트가 올바르게 실행 - * 반면, 모킹 히스토리가 계속 쌓임(호출 횟수나 인자가 계속 변경) ⇨ 다른 테스트에 영향을 줄 수 있음 - */ - vi.clearAllMocks(); -}); - -afterAll(() => { - // 모킹 모듈에 대한 모든 구현을 초기화 - vi.resetAllMocks(); -}); - -vi.mock("zustand"); - -// https://github.com/vitest-dev/vitest/issues/821 -Object.defineProperty(window, "matchMedia", { - writable: true, - value: vi.fn().mockImplementation((query) => ({ - matches: false, - media: query, - onchange: null, - addListener: vi.fn(), // deprecated - removeListener: vi.fn(), // deprecated - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - dispatchEvent: vi.fn(), - })), -}); diff --git a/src/api/todoAPI.ts b/src/api/todoAPI.ts index b3c41f3..72248a6 100644 --- a/src/api/todoAPI.ts +++ b/src/api/todoAPI.ts @@ -38,7 +38,7 @@ export const postFile = async (file: File) => { } }; -export const PostTodos = async ( +export const postTodos = async ( title: string, fileUrl: string | null, linkUrl: string | null, diff --git a/src/app/dashboard/components/ProgressTracker.test.tsx b/src/app/dashboard/components/ProgressTracker.test.tsx new file mode 100644 index 0000000..c54f2a5 --- /dev/null +++ b/src/app/dashboard/components/ProgressTracker.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import ProgressTracker from "./ProgressTracker"; + +describe("ProgressTracker 컴포넌트 테스트", () => { + it("렌더링 확인", () => { + const setprogressValue = vi.fn(); + render(); + + const progressText = screen.getByText("내 진행 상황"); + + expect(progressText).toBeInTheDocument(); + }); +}); diff --git a/src/app/dashboard/components/Sidebar.test.tsx b/src/app/dashboard/components/Sidebar.test.tsx new file mode 100644 index 0000000..32b5598 --- /dev/null +++ b/src/app/dashboard/components/Sidebar.test.tsx @@ -0,0 +1,127 @@ +import { useRouter } from "next/navigation"; +import { expect, vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +import { useAuthStore } from "@/store/authStore"; +import { GoalType, useGoalStore } from "@/store/goalStore"; +import CreateNewTodo from "@/components/CreateNewTodo"; +import { logout } from "@/utils/authUtils"; + +import SideBar from "./Sidebar"; + +export const mockGoals: GoalType[] = [ + { + id: 1, + teamId: "FESI3-5", + title: "스타벅스 가기", + userId: 1, + createdAt: "", + updatedAt: "", + }, + { + id: 2, + teamId: "FESI3-5", + title: "이삭 토스트 가기", + userId: 1, + createdAt: "", + updatedAt: "", + }, +]; + +// Next.js의 navigation과 Image 컴포넌트 모킹 +vi.mock("next/navigation", () => ({ + useRouter: vi.fn(() => ({ + push: vi.fn(), + })), + usePathname: () => "/mocked/path", +})); + +vi.mock("next/image", () => ({ + default: vi.fn(() => null), +})); + +// 스토어와 유틸리티 함수 모킹 +vi.mock("@/store/goalStore", () => ({ + useGoalStore: vi.fn(), +})); + +vi.mock("@/store/authStore", () => ({ + useAuthStore: vi.fn(), +})); + +vi.mock("@/utils/authUtils", () => ({ + logout: vi.fn(), +})); + +describe("SideBar 컴포넌트", () => { + const mockPush = vi.fn(); + const mockRefreshGoals = vi.fn(); + const mockAddGoal = vi.fn(); + const mockUser = { name: "테스트 사용자", email: "test@example.com" }; + + beforeEach(() => { + vi.clearAllMocks(); + useRouter.mockReturnValue({ + push: mockPush, + }); + useGoalStore.mockReturnValue({ + goals: [{ id: 1, title: "오늘의 할 일" }], + refreshGoals: mockRefreshGoals, + addGoal: mockAddGoal, + }); + useAuthStore.mockReturnValue({ user: mockUser }); + render( {}} goal={mockGoals[0]} />); + }); + + it("사이드바가 렌더링됩니다", async () => { + render(); + expect(screen.getByText("대시보드")).toBeInTheDocument(); + expect(screen.getByText("+ 새 할 일")).toBeInTheDocument(); + expect(screen.getByTestId("sidebar-goal-heading")).toBeInTheDocument(); + + // 목표가 비동기로 로드되므로 findByText 사용 + expect(await screen.findByText("스타벅스 가기")).toBeInTheDocument(); + }); + + it("로그아웃 클릭 시 로그아웃되어 / 경로로 갑니다.", async () => { + logout.mockResolvedValue(true); + render(); + + const logoutButton = screen.getByText("로그아웃"); + await fireEvent.click(logoutButton); + + expect(logout).toHaveBeenCalled(); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith("/"); + }); + }); + + it("새 목표 추가 기능이 작동합니다", async () => { + render(); + + const newGoalButton = screen.getByText("+ 새 목표"); + await userEvent.click(newGoalButton); + + const input = screen.getByRole("textbox", { name: "새목표" }); + await userEvent.type(input, "cypress까지 달리기"); + fireEvent.submit(input); + + expect(mockAddGoal).toHaveBeenCalledWith("cypress까지 달리기"); + }); + + it("사이드바 토글 기능이 작동합니다", async () => { + render(); + + const toggleButton = screen.getByTestId("main-sidebar-button"); + await userEvent.click(toggleButton); + + expect(screen.getByTestId("slim-sidebar-button")).toBeInTheDocument(); + }); + + it("컴포넌트 마운트 시 목표 새로고침합니다", () => { + render(); + expect(mockRefreshGoals).toHaveBeenCalled(); + }); +}); diff --git a/src/app/dashboard/components/Sidebar.tsx b/src/app/dashboard/components/Sidebar.tsx index 276d81c..fb1fc12 100644 --- a/src/app/dashboard/components/Sidebar.tsx +++ b/src/app/dashboard/components/Sidebar.tsx @@ -75,7 +75,7 @@ export default function SideBar() { > hamburger-button -

대시보드

+ {/*

대시보드

*/}
@@ -98,24 +102,24 @@ export default function SideBar() {
-
+

-

+
sidebar-home - 대시보드 +

대시보드

sidebar-flag - 목표 +

목표

    @@ -127,19 +131,22 @@ export default function SideBar() {
{inputVisible && (
- { - setGoalInput(e.target.value); - }} - className="block w-full border-b border-gray-300 focus:border-blue-500 focus:outline-none" - /> +
)}
-
+

-

+
@@ -157,8 +164,12 @@ export default function SideBar() { sidebar-brand-hide - )} diff --git a/src/app/sentry-example-page/page.tsx b/src/app/sentry-example-page/page.tsx deleted file mode 100644 index 741ca51..0000000 --- a/src/app/sentry-example-page/page.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import Head from "next/head"; -import * as Sentry from "@sentry/nextjs"; - -export default function Page() { - return ( -
- - Sentry Onboarding - - - -
-

- - - -

- -

Get started by sending us a sample error:

- - -

- Next, look for the error on the{" "} - Issues Page. -

-

- For more information, see{" "} - - https://docs.sentry.io/platforms/javascript/guides/nextjs/ - -

-
-
- ); -} diff --git a/src/components/CreateNewTodo.test.tsx b/src/components/CreateNewTodo.test.tsx new file mode 100644 index 0000000..864dd4f --- /dev/null +++ b/src/components/CreateNewTodo.test.tsx @@ -0,0 +1,131 @@ +import { vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { toast } from "react-toastify"; + +import { editTodo, postFile, postTodos } from "@/api/todoAPI"; +import { getGoal } from "@/api/goalAPI"; + +import CreateNewTodo from "./CreateNewTodo"; + +// next/navigation module 모킹 +vi.mock("next/navigation", () => ({ + usePathname: vi.fn(() => "/goals/1"), +})); + +// API 모킹 +vi.mock("@/api/goalAPI", () => ({ + getGoal: vi.fn(), +})); + +vi.mock("@/api/todoAPI", () => ({ + editTodo: vi.fn(), + postFile: vi.fn(), + postTodos: vi.fn(), +})); + +// react-toastify 모킹 +vi.mock("react-toastify", () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +describe("CreateNewTodo", () => { + const mockCloseCreateNewTodo = vi.fn(); + const mockOnUpdate = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("렌더링", () => { + render(); + expect(screen.getByLabelText("할 일의 제목")).toBeInTheDocument(); + expect(screen.getByText("파일 업로드")).toBeInTheDocument(); + expect(screen.getByText("링크 첨부")).toBeInTheDocument(); + expect(screen.getByText("목표를 선택해주세요")).toBeInTheDocument(); + }); + + it("제목 입력 하기", async () => { + render(); + const titleInput = screen.getByLabelText("할 일의 제목"); + await userEvent.type(titleInput, "빨래 하기"); + expect(titleInput).toHaveValue("빨래 하기"); + }); + + it("파일 업로드 하기", async () => { + const mockFile = new File(["테스트할 파일이에요."], "test.txt", { type: "text/plain" }); + postFile.mockResolvedValue({ url: "http://example.com/test.txt" }); + + render(); + const fileInput = screen.getByLabelText("파일을 업로드해주세요"); + await userEvent.upload(fileInput, mockFile); + + expect(postFile).toHaveBeenCalledWith(mockFile); + await waitFor(() => { + expect(screen.getByText("test.txt")).toBeInTheDocument(); + }); + }); + + it("링크 업로드 하기", async () => { + render(); + const linkAttachButton = screen.getByText("링크 첨부"); + fireEvent.click(linkAttachButton); + + expect(linkAttachButton).toHaveStyle("background-color: rgba(0, 0, 0, 0)"); + }); + + it("목표 기본 값으로 세팅하기", async () => { + const mockGoal = { id: 1, title: "오늘의 할 일", teamId: "FESI3-5", userId: 1, createdAt: "", updatedAt: "" }; + getGoal.mockResolvedValue(mockGoal); + + render(); + + await waitFor(() => { + expect(getGoal).toHaveBeenCalledWith(1); + expect(screen.getByText("오늘의 할 일")).toBeInTheDocument(); + }); + }); + + it("새 할 일 제출하기", async () => { + const mockGoal = { id: 1, title: "오늘의 할 일", teamId: "FESI3-5", userId: 1, createdAt: "", updatedAt: "" }; + getGoal.mockResolvedValue(mockGoal); + postTodos.mockResolvedValue({ id: 1, title: "노래 선정", goal: mockGoal }); + + render(); + + await userEvent.type(screen.getByLabelText("할 일의 제목"), "노래 선정"); + await userEvent.click(screen.getByText("확인")); + + expect(postTodos).toHaveBeenCalledWith("노래 선정", null, null, 1); + expect(toast.success).toHaveBeenCalledWith("할 일이 성공적으로 생성되었습니다"); + expect(mockCloseCreateNewTodo).toHaveBeenCalled(); + }); + + it("할 일 수정하기", async () => { + const mockGoal = { id: 1, title: "오늘의 할 일", teamId: "FESI3-5", userId: 1, createdAt: "", updatedAt: "" }; + const mockTodo = { id: 1, title: "노래 선정", goal: mockGoal, fileUrl: null, linkUrl: null }; + editTodo.mockResolvedValue(mockTodo); + + render( + , + ); + + await userEvent.clear(screen.getByLabelText("할 일의 제목")); + await userEvent.type(screen.getByLabelText("할 일의 제목"), "노래를 정해보자"); + await userEvent.click(screen.getByText("수정")); + + expect(editTodo).toHaveBeenCalledWith("노래를 정해보자", 1, null, null, 1); + expect(mockOnUpdate).toHaveBeenCalledWith(mockTodo); + expect(mockCloseCreateNewTodo).toHaveBeenCalled(); + }); +}); diff --git a/src/components/CreateNewTodo.tsx b/src/components/CreateNewTodo.tsx index e43e447..87cdba8 100644 --- a/src/components/CreateNewTodo.tsx +++ b/src/components/CreateNewTodo.tsx @@ -6,7 +6,7 @@ import { ChangeEvent, useEffect, useRef, useState } from "react"; import { usePathname } from "next/navigation"; import { getGoal } from "@/api/goalAPI"; -import { editTodo, postFile, PostTodos } from "@/api/todoAPI"; +import { editTodo, postFile, postTodos } from "@/api/todoAPI"; import useModal from "@/hook/useModal"; import LinkUpload from "./LinkUpload"; @@ -144,7 +144,7 @@ export default function CreateNewTodo({ console.error("수정 실패:", response); } } else { - const response = await PostTodos(todo.title, todo.fileUrl, todo.linkUrl, todo.goal.id); + const response = await postTodos(todo.title, todo.fileUrl, todo.linkUrl, todo.goal.id); if (response) { toast.success("할 일이 성공적으로 생성되었습니다"); @@ -198,7 +198,12 @@ export default function CreateNewTodo({

제목

+ 파일을 업로드해주세요

)} - + +
-

목표

+

+ 목표 +

setIsOpenGoals((prev) => !prev)} className="flex w-full cursor-pointer justify-between rounded-xl bg-slate-50 px-[20px] py-3"