From fd059dfc044a3fc0f8316f4a941adef92d7609dd Mon Sep 17 00:00:00 2001 From: iyxxnjin <131842947+iyxxnjin@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:22:51 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A7=AA=20Test=20:=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=ED=95=A0=20=EC=9D=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/todos/todoFormFile.test.tsx | 67 +++++++++++ tests/components/todos/todoFormModal.test.tsx | 99 ++++++++++++++++ tests/components/todos/todos.test.tsx | 106 ++++++++++++++++++ 3 files changed, 272 insertions(+) create mode 100644 tests/components/todos/todoFormFile.test.tsx create mode 100644 tests/components/todos/todoFormModal.test.tsx create mode 100644 tests/components/todos/todos.test.tsx diff --git a/tests/components/todos/todoFormFile.test.tsx b/tests/components/todos/todoFormFile.test.tsx new file mode 100644 index 0000000..517269d --- /dev/null +++ b/tests/components/todos/todoFormFile.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import AttachmentSection from "@/app/(protected)/_components/todo-modal/_components/sections/AttachmentSection"; +import { PaperClipIcon } from "@heroicons/react/24/outline"; + +describe("AttachmentSection - 파일 유효성 검사", () => { + const renderComponent = (onChange = jest.fn()) => { + render( + } + onChange={onChange} + />, + ); + }; + + beforeEach(() => { + jest.spyOn(window, "alert").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("3MB 초과 파일은 alert를 띄우고 onChange를 호출하지 않는다", () => { + const onChange = jest.fn(); + renderComponent(onChange); + + const input = screen.getByLabelText("파일을 업로드해주세요"); + + const bigFile = new File( + ["a".repeat(4 * 1024 * 1024)], // 4MB + "big.png", + { type: "image/png" }, + ); + + fireEvent.change(input, { + target: { files: [bigFile] }, + }); + + expect(window.alert).toHaveBeenCalledWith( + "3MB 이하의 파일만 업로드 할 수 있습니다.", + ); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("3MB 이하 파일은 정상적으로 onChange가 호출된다", () => { + const onChange = jest.fn(); + renderComponent(onChange); + + const input = screen.getByLabelText("파일을 업로드해주세요"); + + const smallFile = new File( + ["a".repeat(1024 * 1024)], // 1MB + "small.png", + { type: "image/png" }, + ); + + fireEvent.change(input, { + target: { files: [smallFile] }, + }); + + expect(window.alert).not.toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith(smallFile); + }); +}); diff --git a/tests/components/todos/todoFormModal.test.tsx b/tests/components/todos/todoFormModal.test.tsx new file mode 100644 index 0000000..96593f9 --- /dev/null +++ b/tests/components/todos/todoFormModal.test.tsx @@ -0,0 +1,99 @@ +import { screen, fireEvent } from "@testing-library/react"; +import { renderWithQueryClient } from "tests/test-utils"; +import TodoFormContent from "@/app/(protected)/_components/todo-modal/_components/TodoFormContent"; +import { useGoalList } from "@/hooks/queries/goals/useGoalList"; +import { useDeviceSize } from "@/hooks/useDeviceSize"; + +jest.mock("@/hooks/queries/goals/useGoalList", () => ({ + useGoalList: jest.fn(), +})); + +jest.mock("@/hooks/useDeviceSize", () => ({ + useDeviceSize: jest.fn(), +})); + +jest.mock("@/hooks/queries/todos/useCreateMutation", () => ({ + useCreateMutation: () => ({ mutate: jest.fn() }), +})); + +jest.mock("@/hooks/queries/todos/useEditMutation", () => ({ + useEditMutation: () => ({ mutate: jest.fn() }), +})); + +jest.mock("@/hooks/queries/files/useFileUploadMutation", () => ({ + useFileUploadMutation: () => ({ mutateAsync: jest.fn() }), +})); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe("TodoFormContent", () => { + it("create 모드이면 생성 화면을 보여준다", () => { + (useDeviceSize as jest.Mock).mockReturnValue({ isMobile: false }); + (useGoalList as jest.Mock).mockReturnValue({ + data: { goals: [{ id: 1, title: "React 공부" }] }, + }); + + renderWithQueryClient( + , + ); + + expect(screen.getByText("할 일 생성")).toBeInTheDocument(); + expect(screen.getByText("목표를 선택해주세요")).toBeInTheDocument(); + }); + + it("edit 모드이면 기존 값이 input에 채워진다", () => { + (useDeviceSize as jest.Mock).mockReturnValue({ isMobile: false }); + (useGoalList as jest.Mock).mockReturnValue({ + data: { goals: [{ id: 1, title: "Java 공부" }] }, + }); + + renderWithQueryClient( + , + ); + + expect(screen.getByDisplayValue("할일1")).toBeInTheDocument(); + }); + + it("파일을 선택하면 input change 이벤트가 정상 동작한다", () => { + (useDeviceSize as jest.Mock).mockReturnValue({ isMobile: false }); + (useGoalList as jest.Mock).mockReturnValue({ + data: { goals: [{ id: 1, title: "React 공부" }] }, + }); + + renderWithQueryClient( + , + ); + + const input = screen.getByLabelText("파일을 업로드해주세요"); + const file = new File(["test"], "test.png", { type: "image/png" }); + + fireEvent.change(input, { + target: { files: [file] }, + }); + + // ❗ file input은 value 확인 불가 → 에러 없이 동작하면 OK + expect(input).toBeInTheDocument(); + }); +}); diff --git a/tests/components/todos/todos.test.tsx b/tests/components/todos/todos.test.tsx new file mode 100644 index 0000000..6a9d47f --- /dev/null +++ b/tests/components/todos/todos.test.tsx @@ -0,0 +1,106 @@ +import { screen } from "@testing-library/react"; +import type { ComponentType } from "react"; +import TodosPage from "@/app/(protected)/todos/page"; +import { renderWithQueryClient } from "tests/test-utils"; +import type { ListTodoType } from "@/components/common/list/list-item/types"; + +import { useTodosQuery } from "@/hooks/queries/todos"; + +// react-error-boundary는 ES Module 형식이라 Jest가 변환 못하여, +// 패키지 전체를 모킹 후 children만 렌더링 +jest.mock("react-error-boundary", () => ({ + ErrorBoundary: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + useErrorHandler: () => () => {}, + withErrorBoundary: (component: ComponentType) => component, +})); + +// mocks +jest.mock("@/hooks/queries/todos"); + +jest.mock("@/app/(protected)/todos/_components/TodoHeader", () => { + return function MockTodoHeader() { + return
TodoHeader
; + }; +}); + +jest.mock("@/app/(protected)/todos/_components/TodosContent", () => { + return function MockTodosContent() { + const { data } = useTodosQuery(); + const todos: ListTodoType[] = data?.todos ?? []; + + if (todos.length === 0) { + return
등록된 할 일이 없어요
; + } + + return ( +
+ {todos.map((todo) => ( +
{todo.label}
+ ))} +
+ ); + }; +}); + +jest.mock("@/app/(protected)/todos/_components/TodosLayout", () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), +})); + +afterEach(() => { + jest.clearAllMocks(); +}); + +// tests +describe("모든 할 일 페이지", () => { + it("할 일이 있으면 할 일 리스트를 보여준다", () => { + (useTodosQuery as jest.Mock).mockReturnValue({ + data: { + todos: [ + { id: 1, label: "할일1", checked: false }, + { id: 2, label: "할일2", checked: true }, + ], + totalCount: 2, + }, + }); + + renderWithQueryClient(); + + expect(screen.getByText("할일1")).toBeInTheDocument(); + expect(screen.getByText("할일2")).toBeInTheDocument(); + }); + + it("할 일이 없으면 빈 상태 문구를 보여준다", () => { + (useTodosQuery as jest.Mock).mockReturnValue({ + data: { + todos: [], + totalCount: 0, + }, + }); + + renderWithQueryClient(); + + expect(screen.getByText("등록된 할 일이 없어요")).toBeInTheDocument(); + }); + + it("모든 할 일 개수가 Header에 보여진다", () => { + (useTodosQuery as jest.Mock).mockReturnValue({ + data: { + todos: [ + { id: 1, label: "할일1", checked: false }, + { id: 2, label: "할일2", checked: true }, + { id: 3, label: "할일3", checked: false }, + ], + totalCount: 3, + }, + }); + + renderWithQueryClient(); + + expect(screen.getByText("TodoHeader")).toBeInTheDocument(); + }); +}); From ae06754285b8672c65ddb28a4a7239196b35dd8f Mon Sep 17 00:00:00 2001 From: iyxxnjin <131842947+iyxxnjin@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:14:43 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=84=A4=EC=A0=95=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 65d2f92..2a5e597 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from "url"; import { mergeConfig } from "vite"; const __dirname = dirname(fileURLToPath(import.meta.url)); +const ALIAS_PATH = path.resolve(__dirname, "../src"); const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], @@ -23,7 +24,7 @@ const config: StorybookConfig = { return mergeConfig(baseConfig, { resolve: { alias: { - "@": path.resolve(__dirname, "../src"), + "@": ALIAS_PATH, }, }, }); From 1a9c6f7cd97003b39bfad5e870ef3302754438b2 Mon Sep 17 00:00:00 2001 From: iyxxnjin <131842947+iyxxnjin@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:15:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=84=A4=EC=A0=95=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/preview.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 5e80da2..2f08867 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -11,12 +11,12 @@ const preview: Preview = { }, a11y: { - // 'todo' - show a11y violations in the test UI only - // 'error' - fail CI on a11y violations - // 'off' - skip a11y checks entirely + // todo: show a11y violations in the test UI only + // error: fail CI on a11y violations + // off: skip a11y checks entirely test: "todo", }, }, }; -export default preview; \ No newline at end of file +export default preview;