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
vi.mock("@/components/tasks/TaskList", () => ({
TaskList: ({ tasks, isLoading }: { tasks: unknown[]; isLoading: boolean }): React.JSX.Element => (
{isLoading ? "Loading" : `${String(tasks.length)} tasks`}
),
}));
// 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 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("mosaic-spinner")).toBeInTheDocument();
});
it("should render the TaskList with tasks after loading", async (): Promise => {
render();
await waitFor((): void => {
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");
});
});
it("should have proper layout structure", (): void => {
const { container } = render();
const main = container.querySelector("main");
expect(main).toBeInTheDocument();
});
it("should render the subtitle text", (): 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();
});
});
});