Files
stack/apps/web/src/components/kanban/KanbanBoard.test.tsx
Jason Woltje f0704db560
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: Resolve web package lint and typecheck errors
Fixes ESLint and TypeScript errors in web package to pass CI checks:

- Fixed all Quality Rails violations (14 explicit any types)
- Fixed deprecated React event types (FormEvent → SyntheticEvent)
- Fixed 26 TypeScript errors (Promise types, test mocks, HTMLElement assertions)
- Added vitest DOM matcher type definitions
- Fixed unused variables and empty functions
- Resolved 43+ additional lint errors

Typecheck:  0 errors
Lint: 542 remaining (non-blocking in CI)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 21:34:12 -06:00

299 lines
10 KiB
TypeScript

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 }) => (
<div data-testid="dnd-context">{children}</div>
),
};
});
vi.mock("@dnd-kit/sortable", () => ({
SortableContext: ({ children }: { children: React.ReactNode }) => (
<div data-testid="sortable-context">{children}</div>
),
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<typeof vi.fn>).mockResolvedValue({
ok: true,
json: () => ({}),
} as Response);
});
describe("Rendering", (): void => {
it("should render all four status columns with spec names", (): void => {
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
// 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(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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(<KanbanBoard tasks={[]} onStatusChange={mockOnStatusChange} />);
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(<KanbanBoard tasks={undefined} onStatusChange={mockOnStatusChange} />);
expect(screen.getByText("To Do")).toBeInTheDocument();
});
});
describe("Task Cards", (): void => {
it("should display task title on each card", (): void => {
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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(<KanbanBoard tasks={tasksWithAssignee} onStatusChange={mockOnStatusChange} />);
// 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(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
expect(screen.getByTestId("dnd-context")).toBeInTheDocument();
});
it("should have droppable columns", (): void => {
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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<typeof vi.fn>;
fetchMock.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ status: TaskStatus.IN_PROGRESS }),
} as unknown as Response);
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
// 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<typeof vi.fn>;
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
fetchMock.mockResolvedValueOnce({
ok: false,
statusText: "Internal Server Error",
} as Response);
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
// 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(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
const h3Headings = screen.getAllByRole("heading", { level: 3 });
expect(h3Headings.length).toBe(4);
});
it("should have keyboard-navigable task cards", (): void => {
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
const taskCards = screen.getAllByRole("article");
expect(taskCards.length).toBe(mockTasks.length);
});
it("should announce column changes to screen readers", (): void => {
render(<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />);
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(
<KanbanBoard tasks={mockTasks} onStatusChange={mockOnStatusChange} />
);
const boardGrid = container.querySelector('[data-testid="kanban-grid"]');
expect(boardGrid).toBeInTheDocument();
const className = boardGrid?.className || "";
expect(className).toMatch(/grid/);
});
});
});