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