diff --git a/tests/components/list/ListItem.test.tsx b/tests/components/list/ListItem.test.tsx
new file mode 100644
index 0000000..3f70138
--- /dev/null
+++ b/tests/components/list/ListItem.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from "@testing-library/react";
+import ListItem from "@/components/common/list/list-item/ListItem";
+
+jest.mock("@/components/common/list/list-item/ListItemRow", () =>
+ jest.fn(() =>
),
+);
+
+describe("ListItem 컴포넌트", () => {
+ it("items 개수만큼 ListItemRow를 렌더링한다", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getAllByTestId("list-item-row")).toHaveLength(2);
+ });
+
+ it("items가 비어 있어도 에러 없이 렌더링된다", () => {
+ render(
+ ,
+ );
+
+ expect(screen.queryAllByTestId("list-item-row")).toHaveLength(0);
+ });
+});
diff --git a/tests/components/list/ListItemButton.test.tsx b/tests/components/list/ListItemButton.test.tsx
new file mode 100644
index 0000000..3e91f1a
--- /dev/null
+++ b/tests/components/list/ListItemButton.test.tsx
@@ -0,0 +1,22 @@
+import ListItemButton from "@/components/common/list/list-button/ListItemButton";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+describe("ListItemButton", () => {
+ it("버튼을 클릭하면 onClick이 호출된다", async () => {
+ const user = userEvent.setup();
+ const handleClick = jest.fn();
+
+ render(
+ icon}
+ ariaLabel="테스트 버튼"
+ onClick={handleClick}
+ />,
+ );
+
+ await user.click(screen.getByRole("button", { name: "테스트 버튼" }));
+
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/tests/components/list/ListItemRow.test.tsx b/tests/components/list/ListItemRow.test.tsx
new file mode 100644
index 0000000..abe39f8
--- /dev/null
+++ b/tests/components/list/ListItemRow.test.tsx
@@ -0,0 +1,80 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import ListItemRow from "@/components/common/list/list-item/ListItemRow";
+import { ListActionType } from "@/components/common/list/list-item-actions/types";
+
+jest.mock("@/components/common/checkbox/Checkbox", () => {
+ const MockListToggleChecked = ({
+ checked,
+ onToggleChecked,
+ }: {
+ checked: boolean;
+ onToggleChecked: (checked: boolean) => void;
+ }) => (
+ onToggleChecked(e.target.checked)}
+ />
+ );
+
+ MockListToggleChecked.displayName = "MockListToggleChecked";
+
+ return MockListToggleChecked;
+});
+
+jest.mock("@/components/common/list/list-item-actions/ListItemActions", () => {
+ const MockListItemActions = ({
+ actions = [],
+ }: {
+ actions?: ListActionType[];
+ }) => (
+
+ {actions.map((action) => action.type).join(",")}
+
+ );
+
+ MockListItemActions.displayName = "MockListItemActions";
+
+ return MockListItemActions;
+});
+describe("ListItemRow 컴포넌트", () => {
+ it("체크박스를 클릭하면 onToggleChecked가 호출된다", async () => {
+ const user = userEvent.setup();
+ const onToggleChecked = jest.fn();
+
+ render(
+ ,
+ );
+
+ await user.click(screen.getByTestId("checkbox"));
+
+ expect(onToggleChecked).toHaveBeenCalledWith(1, true);
+ });
+
+ it("item에 link, file, note가 있으면 actions가 생성된다", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("actions")).toHaveTextContent(
+ "link,file,note,more",
+ );
+ });
+});
diff --git a/tests/unit/dashboard/dashboard.test.tsx b/tests/unit/dashboard/dashboard.test.tsx
new file mode 100644
index 0000000..2e4da51
--- /dev/null
+++ b/tests/unit/dashboard/dashboard.test.tsx
@@ -0,0 +1,119 @@
+import { screen } from "@testing-library/react";
+import ProgressContent from "@/app/(protected)/dashboard/_components/todos/progress/ProgressContent";
+import RecentTodosContent from "@/app/(protected)/dashboard/_components/todos/recent/RecentTodosContent";
+import GoalList from "@/app/(protected)/dashboard/_components/goal/GoalList";
+import { useProgressTodosSuspense } from "@/hooks/queries/todos/useProgressTodosSuspense";
+import { useTodosSuspense } from "@/hooks/queries/todos/useTodosSuspense";
+import { useGoalsInfiniteQuery } from "@/hooks/queries/goals/useGoalsInfiniteQuery";
+import { renderWithQueryClient } from "tests/test-utils";
+
+jest.mock("@/app/(protected)/_components/AsyncBoundary", () => {
+ const MockAsyncBoundary = ({ children }: { children: React.ReactNode }) => (
+ <>{children}>
+ );
+ MockAsyncBoundary.displayName = "MockAsyncBoundary";
+ return MockAsyncBoundary;
+});
+
+jest.mock("@/hooks/queries/todos/useTodosSuspense", () => ({
+ useTodosSuspense: jest.fn(),
+}));
+
+jest.mock("@/hooks/queries/todos/useProgressTodosSuspense", () => ({
+ useProgressTodosSuspense: jest.fn(),
+}));
+
+jest.mock("@/hooks/queries/goals/useGoalsInfiniteQuery", () => ({
+ useGoalsInfiniteQuery: jest.fn(),
+}));
+
+jest.mock("@/hooks/queries/todos/useToggleTodo", () => ({
+ useToggleTodo: () => ({
+ handleToggle: jest.fn(),
+ }),
+}));
+
+jest.mock("@/components/common/list/list-item-actions/ListItemActions", () => {
+ const MockListItemActions = () => null;
+ MockListItemActions.displayName = "MockListItemActions";
+ return MockListItemActions;
+});
+
+afterEach(() => {
+ jest.clearAllMocks();
+});
+
+describe("대시보드 - 최근 등록한 할 일", () => {
+ it("최근 4개만 보여준다", () => {
+ (useTodosSuspense as jest.Mock).mockReturnValue([
+ { id: 1, label: "todo1", checked: false },
+ { id: 2, label: "todo2", checked: false },
+ { id: 3, label: "todo3", checked: false },
+ { id: 4, label: "todo4", checked: false },
+ { id: 5, label: "todo5", checked: false },
+ ]);
+
+ renderWithQueryClient();
+
+ expect(screen.getByText("todo5")).toBeInTheDocument();
+ expect(screen.getByText("todo4")).toBeInTheDocument();
+ expect(screen.getByText("todo3")).toBeInTheDocument();
+ expect(screen.getByText("todo2")).toBeInTheDocument();
+ expect(screen.queryByText("todo1")).not.toBeInTheDocument();
+ });
+
+ it("할 일이 없으면 안내 문구를 보여준다", () => {
+ (useTodosSuspense as jest.Mock).mockReturnValue([]);
+
+ renderWithQueryClient();
+
+ expect(
+ screen.getByText("최근에 등록한 할 일이 없어요"),
+ ).toBeInTheDocument();
+ });
+});
+
+describe("대시보드 - 진행도", () => {
+ it("할 일들의 진행도를 보여준다", () => {
+ (useProgressTodosSuspense as jest.Mock).mockReturnValue({
+ progress: 80,
+ });
+
+ renderWithQueryClient();
+
+ expect(screen.getByText(/80/)).toBeInTheDocument();
+ });
+
+ it("progress가 없으면 0%를 보여준다", () => {
+ (useProgressTodosSuspense as jest.Mock).mockReturnValue({});
+
+ renderWithQueryClient();
+
+ expect(screen.getByText(/0/)).toBeInTheDocument();
+ });
+});
+
+describe("대시보드 - 목표별 할 일", () => {
+ it("목표가 잘 나온다", () => {
+ (useGoalsInfiniteQuery as jest.Mock).mockReturnValue({
+ data: {
+ pages: [
+ {
+ goals: [
+ { id: 1, title: "운동 목표", todos: [] },
+ { id: 2, title: "공부 목표", todos: [] },
+ ],
+ },
+ ],
+ },
+ hasNextPage: false,
+ fetchNextPage: jest.fn(),
+ isFetchingNextPage: false,
+ });
+
+ renderWithQueryClient();
+
+ expect(screen.getByText("운동 목표")).toBeInTheDocument();
+ expect(screen.getByText("공부 목표")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/goals/goals.test.tsx b/tests/unit/goals/goals.test.tsx
index 784c3db..1001e99 100644
--- a/tests/unit/goals/goals.test.tsx
+++ b/tests/unit/goals/goals.test.tsx
@@ -3,13 +3,15 @@ import GoalHeader from "@/app/(protected)/goals/[goalId]/_components/GoalHeader"
import GoalSection from "@/app/(protected)/goals/[goalId]/_components/GoalSection";
import { calcProgress } from "@/app/(protected)/goals/[goalId]/_utils/calcProgress";
import { EMPTY_MESSAGES } from "@/constants/messages";
-import { render, screen } from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithQueryClient } from "tests/test-utils";
const push = jest.fn();
+const replace = jest.fn();
+
jest.mock("next/navigation", () => ({
- useRouter: () => ({ push }),
+ useRouter: () => ({ push, replace }),
useParams: () => ({ goalId: "1" }),
}));
@@ -97,6 +99,10 @@ describe("목표 영역", () => {
expect(screen.getByText("정말 삭제하시겠어요?")).toBeInTheDocument();
await user.click(screen.getByText("확인"));
+ expect(replace).toHaveBeenCalledWith(
+ expect.stringContaining("dashboard"),
+ );
+
expect(deleteGoal).toHaveBeenCalled();
});
});