From 5b62353c0678d042e0bacc29b031e4bef4281944 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 22 Feb 2026 21:59:02 -0600 Subject: [PATCH] test(web): update tasks page tests for real API integration Update page tests to mock fetchTasks and useWorkspaceId instead of relying on removed mock data. Add coverage for loading, empty, error, and retry states. Refs #467 --- .../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(); + }); + }); });