Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)"],
Expand All @@ -23,7 +24,7 @@ const config: StorybookConfig = {
return mergeConfig(baseConfig, {
resolve: {
alias: {
"@": path.resolve(__dirname, "../src"),
"@": ALIAS_PATH,
},
},
});
Expand Down
8 changes: 4 additions & 4 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
export default preview;
67 changes: 67 additions & 0 deletions tests/components/todos/todoFormFile.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<AttachmentSection
type="file"
value={null}
placeholder="ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•ด์ฃผ์„ธ์š”"
icon={<PaperClipIcon />}
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);
});
});
99 changes: 99 additions & 0 deletions tests/components/todos/todoFormModal.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<TodoFormContent
mode="create"
onClose={jest.fn()}
/>,
);

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(
<TodoFormContent
mode="edit"
todoId={1}
todo={{
id: 1,
title: "ํ• ์ผ1",
done: true,
linkUrl: "",
fileUrl: "",
noteId: 0,
updatedAt: "2026-01-01",
goal: { id: 1, title: "Java ๊ณต๋ถ€" },
}}
onClose={jest.fn()}
/>,
);

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(
<TodoFormContent
mode="create"
onClose={jest.fn()}
/>,
);

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();
});
});
106 changes: 106 additions & 0 deletions tests/components/todos/todos.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>TodoHeader</div>;
};
});

jest.mock("@/app/(protected)/todos/_components/TodosContent", () => {
return function MockTodosContent() {
const { data } = useTodosQuery();
const todos: ListTodoType[] = data?.todos ?? [];

if (todos.length === 0) {
return <div>๋“ฑ๋ก๋œ ํ•  ์ผ์ด ์—†์–ด์š”</div>;
}

return (
<div>
{todos.map((todo) => (
<div key={todo.id}>{todo.label}</div>
))}
</div>
);
};
});

jest.mock("@/app/(protected)/todos/_components/TodosLayout", () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
}));

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(<TodosPage />);

expect(screen.getByText("ํ• ์ผ1")).toBeInTheDocument();
expect(screen.getByText("ํ• ์ผ2")).toBeInTheDocument();
});

it("ํ•  ์ผ์ด ์—†์œผ๋ฉด ๋นˆ ์ƒํƒœ ๋ฌธ๊ตฌ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค", () => {
(useTodosQuery as jest.Mock).mockReturnValue({
data: {
todos: [],
totalCount: 0,
},
});

renderWithQueryClient(<TodosPage />);

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(<TodosPage />);

expect(screen.getByText("TodoHeader")).toBeInTheDocument();
});
});