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, }, }, }); 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; 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(); + }); +});