import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, within } from "@testing-library/react"; import { KanbanBoard } from "./KanbanBoard"; import type { Task } from "@mosaic/shared"; import { TaskStatus, TaskPriority } from "@mosaic/shared"; // Mock fetch globally global.fetch = vi.fn(); // Mock @dnd-kit modules vi.mock("@dnd-kit/core", async () => { const actual = await vi.importActual("@dnd-kit/core"); return { ...actual, DndContext: ({ children }: { children: React.ReactNode }) => (
{children}
), }; }); vi.mock("@dnd-kit/sortable", () => ({ SortableContext: ({ children }: { children: React.ReactNode }) => (
{children}
), verticalListSortingStrategy: {}, useSortable: () => ({ attributes: {}, listeners: {}, setNodeRef: () => {}, transform: null, transition: null, }), })); const mockTasks: Task[] = [ { id: "task-1", title: "Design homepage", description: "Create wireframes for the new homepage", status: TaskStatus.NOT_STARTED, priority: TaskPriority.HIGH, dueDate: new Date("2026-02-01"), assigneeId: "user-1", creatorId: "user-1", workspaceId: "workspace-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: "Implement authentication", description: "Add OAuth support", status: TaskStatus.IN_PROGRESS, priority: TaskPriority.HIGH, dueDate: new Date("2026-01-30"), assigneeId: "user-2", creatorId: "user-1", workspaceId: "workspace-1", projectId: null, parentId: null, sortOrder: 1, metadata: {}, completedAt: null, createdAt: new Date("2026-01-28"), updatedAt: new Date("2026-01-28"), }, { id: "task-3", title: "Write unit tests", description: "Achieve 85% coverage", status: TaskStatus.PAUSED, priority: TaskPriority.MEDIUM, dueDate: new Date("2026-02-05"), assigneeId: "user-3", creatorId: "user-1", workspaceId: "workspace-1", projectId: null, parentId: null, sortOrder: 2, metadata: {}, completedAt: null, createdAt: new Date("2026-01-28"), updatedAt: new Date("2026-01-28"), }, { id: "task-4", title: "Deploy to production", description: "Set up CI/CD pipeline", status: TaskStatus.COMPLETED, priority: TaskPriority.LOW, dueDate: new Date("2026-01-25"), assigneeId: "user-1", creatorId: "user-1", workspaceId: "workspace-1", projectId: null, parentId: null, sortOrder: 3, metadata: {}, completedAt: new Date("2026-01-25"), createdAt: new Date("2026-01-20"), updatedAt: new Date("2026-01-25"), }, ]; describe("KanbanBoard", (): void => { const mockOnStatusChange = vi.fn(); beforeEach((): void => { vi.clearAllMocks(); (global.fetch as ReturnType).mockResolvedValue({ ok: true, json: () => ({}), } as Response); }); describe("Rendering", (): void => { it("should render all four status columns with spec names", (): void => { render(); // Spec requires: todo, in_progress, review, done expect(screen.getByText("To Do")).toBeInTheDocument(); expect(screen.getByText("In Progress")).toBeInTheDocument(); expect(screen.getByText("Review")).toBeInTheDocument(); expect(screen.getByText("Done")).toBeInTheDocument(); }); it("should organize tasks by status into correct columns", (): void => { render(); const todoColumn = screen.getByTestId("column-NOT_STARTED"); const inProgressColumn = screen.getByTestId("column-IN_PROGRESS"); const reviewColumn = screen.getByTestId("column-PAUSED"); const doneColumn = screen.getByTestId("column-COMPLETED"); expect(within(todoColumn).getByText("Design homepage")).toBeInTheDocument(); expect(within(inProgressColumn).getByText("Implement authentication")).toBeInTheDocument(); expect(within(reviewColumn).getByText("Write unit tests")).toBeInTheDocument(); expect(within(doneColumn).getByText("Deploy to production")).toBeInTheDocument(); }); it("should render empty state when no tasks provided", (): void => { render(); expect(screen.getByText("To Do")).toBeInTheDocument(); expect(screen.getByText("In Progress")).toBeInTheDocument(); expect(screen.getByText("Review")).toBeInTheDocument(); expect(screen.getByText("Done")).toBeInTheDocument(); }); it("should handle undefined tasks array gracefully", (): void => { // @ts-expect-error Testing error case render(); expect(screen.getByText("To Do")).toBeInTheDocument(); }); }); describe("Task Cards", (): void => { it("should display task title on each card", (): void => { render(); expect(screen.getByText("Design homepage")).toBeInTheDocument(); expect(screen.getByText("Implement authentication")).toBeInTheDocument(); expect(screen.getByText("Write unit tests")).toBeInTheDocument(); expect(screen.getByText("Deploy to production")).toBeInTheDocument(); }); it("should display task priority badge", (): void => { render(); const highPriorityElements = screen.getAllByText("High"); const mediumPriorityElements = screen.getAllByText("Medium"); const lowPriorityElements = screen.getAllByText("Low"); expect(highPriorityElements.length).toBeGreaterThan(0); expect(mediumPriorityElements.length).toBeGreaterThan(0); expect(lowPriorityElements.length).toBeGreaterThan(0); }); it("should display due date when available", (): void => { render(); expect(screen.getByText(/Feb 1/)).toBeInTheDocument(); expect(screen.getByText(/Jan 30/)).toBeInTheDocument(); }); it("should display assignee avatar when assignee data is provided", (): void => { const tasksWithAssignee: Task[] = [ { ...mockTasks[0]!, // Task type uses assigneeId, not assignee object assigneeId: "user-john", }, ]; render(); // Note: This test may need to be updated based on how the component fetches/displays assignee info // For now, just checking the component renders without errors expect(screen.getByRole("main")).toBeInTheDocument(); }); }); describe("Drag and Drop", (): void => { it("should initialize DndContext for drag-and-drop", (): void => { render(); expect(screen.getByTestId("dnd-context")).toBeInTheDocument(); }); it("should have droppable columns", (): void => { render(); const columns = screen.getAllByTestId(/^column-/); expect(columns.length).toBe(4); }); }); describe("Status Update API Call", (): void => { it("should call PATCH /api/tasks/:id when status changes", (): void => { const fetchMock = global.fetch as ReturnType; fetchMock.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ status: TaskStatus.IN_PROGRESS }), } as unknown as Response); render(); // Simulate drag end by calling the component's internal method // In a real test, we'd simulate actual drag-and-drop events // For now, we'll test the fetch call directly // This is a simplified test - full E2E would use Playwright expect(screen.getByTestId("dnd-context")).toBeInTheDocument(); }); it("should handle API errors gracefully", (): void => { const fetchMock = global.fetch as ReturnType; const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); fetchMock.mockResolvedValueOnce({ ok: false, statusText: "Internal Server Error", } as Response); render(); // Component should still render even if API fails expect(screen.getByText("To Do")).toBeInTheDocument(); consoleErrorSpy.mockRestore(); }); }); describe("Accessibility", (): void => { it("should have proper heading hierarchy", (): void => { render(); const h3Headings = screen.getAllByRole("heading", { level: 3 }); expect(h3Headings.length).toBe(4); }); it("should have keyboard-navigable task cards", (): void => { render(); const taskCards = screen.getAllByRole("article"); expect(taskCards.length).toBe(mockTasks.length); }); it("should announce column changes to screen readers", (): void => { render(); const columns = screen.getAllByRole("region"); expect(columns.length).toBeGreaterThan(0); columns.forEach((column) => { expect(column).toHaveAttribute("aria-label"); }); }); }); describe("Responsive Design", (): void => { it("should apply responsive grid classes", (): void => { const { container } = render( ); const boardGrid = container.querySelector('[data-testid="kanban-grid"]'); expect(boardGrid).toBeInTheDocument(); const className = boardGrid?.className || ""; expect(className).toMatch(/grid/); }); }); });