From ffda74ec126edbc44b2936f22cc30fd74dab81c1 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 23 Feb 2026 03:59:56 +0000 Subject: [PATCH] test(web): update tasks page tests for real API integration (#475) Co-authored-by: Jason Woltje Co-committed-by: Jason Woltje --- .../app/(authenticated)/tasks/page.test.tsx | 121 +++++++++++++++++- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/apps/web/src/app/(authenticated)/tasks/page.test.tsx b/apps/web/src/app/(authenticated)/tasks/page.test.tsx index a0c9966..bcc7b75 100644 --- a/apps/web/src/app/(authenticated)/tasks/page.test.tsx +++ b/apps/web/src/app/(authenticated)/tasks/page.test.tsx @@ -1,5 +1,8 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { Task } from "@mosaic/shared"; +import { TaskStatus, TaskPriority } from "@mosaic/shared"; import TasksPage from "./page"; // Mock the TaskList component @@ -9,21 +12,121 @@ vi.mock("@/components/tasks/TaskList", () => ({ ), })); +// Mock MosaicSpinner +vi.mock("@/components/ui/MosaicSpinner", () => ({ + MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => ( +
{label ?? "Loading..."}
+ ), +})); + +// Mock useWorkspaceId +const mockUseWorkspaceId = vi.fn<() => string | null>(); +vi.mock("@/lib/hooks", () => ({ + useWorkspaceId: (): string | null => mockUseWorkspaceId(), +})); + +// Mock fetchTasks +const mockFetchTasks = vi.fn<() => Promise>(); +vi.mock("@/lib/api/tasks", () => ({ + fetchTasks: (...args: unknown[]): Promise => mockFetchTasks(...(args as [])), +})); + +const fakeTasks: Task[] = [ + { + id: "task-1", + title: "Test task 1", + description: "Description 1", + status: TaskStatus.IN_PROGRESS, + priority: TaskPriority.HIGH, + dueDate: new Date("2026-02-01"), + creatorId: "user-1", + assigneeId: "user-1", + workspaceId: "ws-1", + projectId: null, + parentId: null, + sortOrder: 0, + metadata: {}, + completedAt: null, + createdAt: new Date("2026-01-28"), + updatedAt: new Date("2026-01-28"), + }, + { + id: "task-2", + title: "Test task 2", + description: "Description 2", + status: TaskStatus.NOT_STARTED, + priority: TaskPriority.MEDIUM, + dueDate: new Date("2026-02-02"), + creatorId: "user-1", + assigneeId: "user-1", + workspaceId: "ws-1", + projectId: null, + parentId: null, + sortOrder: 1, + metadata: {}, + completedAt: null, + createdAt: new Date("2026-01-28"), + updatedAt: new Date("2026-01-28"), + }, +]; + describe("TasksPage", (): void => { + beforeEach((): void => { + vi.clearAllMocks(); + mockUseWorkspaceId.mockReturnValue("ws-1"); + mockFetchTasks.mockResolvedValue(fakeTasks); + }); + it("should render the page title", (): void => { render(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Tasks"); }); - it("should show loading state initially", (): void => { + it("should show loading spinner initially", (): void => { + // Never resolve so we stay in loading state + // eslint-disable-next-line @typescript-eslint/no-empty-function + mockFetchTasks.mockReturnValue(new Promise(() => {})); render(); - expect(screen.getByTestId("task-list")).toHaveTextContent("Loading"); + expect(screen.getByTestId("mosaic-spinner")).toBeInTheDocument(); }); it("should render the TaskList with tasks after loading", async (): Promise => { render(); await waitFor((): void => { - expect(screen.getByTestId("task-list")).toHaveTextContent("4 tasks"); + expect(screen.getByTestId("task-list")).toHaveTextContent("2 tasks"); + }); + }); + + it("should show empty state when no tasks exist", async (): Promise => { + mockFetchTasks.mockResolvedValue([]); + render(); + await waitFor((): void => { + expect(screen.getByText("No tasks found")).toBeInTheDocument(); + }); + }); + + it("should show error state on API failure", async (): Promise => { + mockFetchTasks.mockRejectedValue(new Error("Network error")); + render(); + await waitFor((): void => { + expect(screen.getByText("Network error")).toBeInTheDocument(); + }); + expect(screen.getByRole("button", { name: /try again/i })).toBeInTheDocument(); + }); + + it("should retry fetching on retry button click", async (): Promise => { + mockFetchTasks.mockRejectedValueOnce(new Error("Network error")); + render(); + await waitFor((): void => { + expect(screen.getByText("Network error")).toBeInTheDocument(); + }); + + mockFetchTasks.mockResolvedValueOnce(fakeTasks); + const user = userEvent.setup(); + await user.click(screen.getByRole("button", { name: /try again/i })); + + await waitFor((): void => { + expect(screen.getByTestId("task-list")).toHaveTextContent("2 tasks"); }); }); @@ -37,4 +140,14 @@ describe("TasksPage", (): void => { render(); expect(screen.getByText("Organize your work at your own pace")).toBeInTheDocument(); }); + + it("should not fetch when workspace ID is not available", async (): Promise => { + mockUseWorkspaceId.mockReturnValue(null); + render(); + + // Wait a tick to ensure useEffect ran + await waitFor((): void => { + expect(mockFetchTasks).not.toHaveBeenCalled(); + }); + }); });