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 { TaskPriority, TaskStatus } from "@mosaic/shared"; import KanbanPage from "./page"; const mockReplace = vi.fn(); let mockSearchParams = new URLSearchParams(); vi.mock("next/navigation", () => ({ useRouter: (): { replace: typeof mockReplace } => ({ replace: mockReplace }), useSearchParams: (): URLSearchParams => mockSearchParams, })); vi.mock("@hello-pangea/dnd", () => ({ DragDropContext: ({ children }: { children: React.ReactNode }): React.JSX.Element => (
{children}
), Droppable: ({ children, droppableId, }: { children: (provided: { innerRef: (el: HTMLElement | null) => void; droppableProps: Record; placeholder: React.ReactNode; }) => React.ReactNode; droppableId: string; }): React.JSX.Element => (
{children({ innerRef: () => { /* noop */ }, droppableProps: {}, placeholder: null, })}
), Draggable: ({ children, draggableId, }: { children: ( provided: { innerRef: (el: HTMLElement | null) => void; draggableProps: { style: Record }; dragHandleProps: Record; }, snapshot: { isDragging: boolean } ) => React.ReactNode; draggableId: string; index: number; }): React.JSX.Element => (
{children( { innerRef: () => { /* noop */ }, draggableProps: { style: {} }, dragHandleProps: {}, }, { isDragging: false } )}
), })); vi.mock("@/components/ui/MosaicSpinner", () => ({ MosaicSpinner: ({ label }: { label?: string }): React.JSX.Element => (
{label ?? "Loading..."}
), })); const mockUseWorkspaceId = vi.fn<() => string | null>(); vi.mock("@/lib/hooks", () => ({ useWorkspaceId: (): string | null => mockUseWorkspaceId(), })); const mockFetchTasks = vi.fn<() => Promise>(); const mockUpdateTask = vi.fn<() => Promise>(); const mockCreateTask = vi.fn<() => Promise>(); vi.mock("@/lib/api/tasks", () => ({ fetchTasks: (...args: unknown[]): Promise => mockFetchTasks(...(args as [])), updateTask: (...args: unknown[]): Promise => mockUpdateTask(...(args as [])), createTask: (...args: unknown[]): Promise => mockCreateTask(...(args as [])), })); const mockFetchProjects = vi.fn<() => Promise>(); vi.mock("@/lib/api/projects", () => ({ fetchProjects: (...args: unknown[]): Promise => mockFetchProjects(...(args as [])), })); const createdTask: Task = { id: "task-new-1", title: "Ship Kanban add task flow", description: null, status: TaskStatus.NOT_STARTED, priority: TaskPriority.MEDIUM, dueDate: null, creatorId: "user-1", assigneeId: null, workspaceId: "ws-1", projectId: "project-42", parentId: null, sortOrder: 0, metadata: {}, completedAt: null, createdAt: new Date("2026-03-01"), updatedAt: new Date("2026-03-01"), }; describe("KanbanPage add task flow", (): void => { beforeEach((): void => { vi.clearAllMocks(); mockSearchParams = new URLSearchParams("project=project-42"); mockUseWorkspaceId.mockReturnValue("ws-1"); mockFetchTasks.mockResolvedValue([]); mockFetchProjects.mockResolvedValue([]); mockCreateTask.mockResolvedValue(createdTask); }); it("opens add-task form in a column and creates a task via API", async (): Promise => { const user = userEvent.setup(); render(); await waitFor((): void => { expect(screen.getByText("Kanban Board")).toBeInTheDocument(); }); // Click the "+ Add task" button in the To Do column const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await user.click(addTaskButtons[0]!); // First column is "To Do" // Type in the title input const titleInput = screen.getByPlaceholderText("Task title..."); await user.type(titleInput, createdTask.title); // Click the Add button await user.click(screen.getByRole("button", { name: /✓ Add/i })); await waitFor((): void => { expect(mockCreateTask).toHaveBeenCalledWith( expect.objectContaining({ title: createdTask.title, status: TaskStatus.NOT_STARTED, projectId: "project-42", }), "ws-1" ); }); }); it("cancels add-task form when pressing Escape", async (): Promise => { const user = userEvent.setup(); render(); await waitFor((): void => { expect(screen.getByText("Kanban Board")).toBeInTheDocument(); }); // Click the "+ Add task" button const addTaskButtons = screen.getAllByRole("button", { name: /\+ Add task/i }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await user.click(addTaskButtons[0]!); // Type in the title input const titleInput = screen.getByPlaceholderText("Task title..."); await user.type(titleInput, "Test task"); // Press Escape to cancel await user.keyboard("{Escape}"); // Form should be closed, back to "+ Add task" button await waitFor((): void => { const buttons = screen.getAllByRole("button", { name: /\+ Add task/i }); expect(buttons.length).toBe(5); // One per column }); // Should not have called createTask expect(mockCreateTask).not.toHaveBeenCalled(); }); });