Fixes all 542 ESLint problems in the web package to achieve 0 errors and 0 warnings. Changes: - Fixed 144 issues: nullish coalescing, return types, unused variables - Fixed 118 issues: unnecessary conditions, type safety, template literals - Fixed 79 issues: non-null assertions, unsafe assignments, empty functions - Fixed 67 issues: explicit return types, promise handling, enum comparisons - Fixed 45 final warnings: missing return types, optional chains - Fixed 25 typecheck-related issues: async/await, type assertions, formatting - Fixed JSX.Element namespace errors across 90+ files All Quality Rails violations resolved. Lint and typecheck both pass with 0 problems. Files modified: 118 components, tests, hooks, and utilities Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
548 lines
17 KiB
TypeScript
548 lines
17 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { GanttChart } from "./GanttChart";
|
|
import { TaskStatus, TaskPriority } from "@mosaic/shared";
|
|
import type { GanttTask } from "./types";
|
|
|
|
describe("GanttChart", (): void => {
|
|
const baseDate = new Date("2026-02-01T00:00:00Z");
|
|
|
|
const createGanttTask = (overrides: Partial<GanttTask> = {}): GanttTask => ({
|
|
id: `task-${String(Math.random())}`,
|
|
workspaceId: "workspace-1",
|
|
title: "Sample Task",
|
|
description: null,
|
|
status: TaskStatus.NOT_STARTED,
|
|
priority: TaskPriority.MEDIUM,
|
|
dueDate: new Date("2026-02-15T00:00:00Z"),
|
|
assigneeId: null,
|
|
creatorId: "user-1",
|
|
projectId: null,
|
|
parentId: null,
|
|
sortOrder: 0,
|
|
metadata: {},
|
|
completedAt: null,
|
|
createdAt: baseDate,
|
|
updatedAt: baseDate,
|
|
startDate: new Date("2026-02-01T00:00:00Z"),
|
|
endDate: new Date("2026-02-15T00:00:00Z"),
|
|
...overrides,
|
|
});
|
|
|
|
describe("Rendering", (): void => {
|
|
it("should render without crashing with empty task list", (): void => {
|
|
render(<GanttChart tasks={[]} />);
|
|
expect(screen.getByRole("region", { name: /gantt chart/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render task names in the task list", (): void => {
|
|
const tasks = [
|
|
createGanttTask({ id: "task-1", title: "Design mockups" }),
|
|
createGanttTask({ id: "task-2", title: "Implement frontend" }),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
// Tasks appear in both the list and bars, so use getAllByText
|
|
expect(screen.getAllByText("Design mockups").length).toBeGreaterThan(0);
|
|
expect(screen.getAllByText("Implement frontend").length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should render timeline bars for each task", (): void => {
|
|
const tasks = [
|
|
createGanttTask({ id: "task-1", title: "Task 1" }),
|
|
createGanttTask({ id: "task-2", title: "Task 2" }),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const bars = screen.getAllByRole("button", { name: /gantt bar/i });
|
|
expect(bars).toHaveLength(2);
|
|
});
|
|
|
|
it("should display date headers for the timeline", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-10"),
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
// Should show month or date indicators
|
|
const timeline = screen.getByRole("region", { name: /timeline/i });
|
|
expect(timeline).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Task Status Indicators", (): void => {
|
|
it("should visually distinguish completed tasks", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "completed-task",
|
|
title: "Completed Task",
|
|
status: TaskStatus.COMPLETED,
|
|
completedAt: new Date("2026-02-10"),
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const taskElements = screen.getAllByText("Completed Task");
|
|
const taskRow = taskElements[0]?.closest("[role='row']");
|
|
expect(taskRow?.className).toMatch(/Completed/i);
|
|
});
|
|
|
|
it("should visually distinguish in-progress tasks", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "active-task",
|
|
title: "Active Task",
|
|
status: TaskStatus.IN_PROGRESS,
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const taskElements = screen.getAllByText("Active Task");
|
|
const taskRow = taskElements[0]?.closest("[role='row']");
|
|
expect(taskRow?.className).toMatch(/InProgress/i);
|
|
});
|
|
});
|
|
|
|
describe("PDA-friendly language", (): void => {
|
|
it('should show "Target passed" for tasks past their end date', () => {
|
|
const pastTask = createGanttTask({
|
|
id: "past-task",
|
|
title: "Past Task",
|
|
startDate: new Date("2020-01-01"),
|
|
endDate: new Date("2020-01-15"),
|
|
status: TaskStatus.NOT_STARTED,
|
|
});
|
|
|
|
render(<GanttChart tasks={[pastTask]} />);
|
|
|
|
// Should show "Target passed" not "OVERDUE"
|
|
expect(screen.getByText(/target passed/i)).toBeInTheDocument();
|
|
expect(screen.queryByText(/overdue/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('should show "Approaching target" for tasks near their end date', () => {
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
const soonTask = createGanttTask({
|
|
id: "soon-task",
|
|
title: "Soon Task",
|
|
startDate: today,
|
|
endDate: tomorrow,
|
|
status: TaskStatus.IN_PROGRESS,
|
|
});
|
|
|
|
render(<GanttChart tasks={[soonTask]} />);
|
|
|
|
expect(screen.getByText(/approaching target/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Task Interactions", (): void => {
|
|
it("should call onTaskClick when a task bar is clicked", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
const onTaskClick = vi.fn();
|
|
|
|
const task = createGanttTask({ id: "clickable-task", title: "Click Me" });
|
|
|
|
render(<GanttChart tasks={[task]} onTaskClick={onTaskClick} />);
|
|
|
|
const taskBar = screen.getByRole("button", { name: /gantt bar.*click me/i });
|
|
await user.click(taskBar);
|
|
|
|
expect(onTaskClick).toHaveBeenCalledWith(task);
|
|
});
|
|
|
|
it("should not crash when clicking a task without onTaskClick handler", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
const task = createGanttTask({ id: "task-1", title: "No Handler" });
|
|
|
|
render(<GanttChart tasks={[task]} />);
|
|
|
|
const taskBar = screen.getByRole("button", { name: /gantt bar/i });
|
|
await user.click(taskBar);
|
|
|
|
// Should not throw
|
|
expect(taskBar).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Timeline Calculations", (): void => {
|
|
it("should calculate timeline range from task dates", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "early-task",
|
|
startDate: new Date("2026-01-01"),
|
|
endDate: new Date("2026-01-10"),
|
|
}),
|
|
createGanttTask({
|
|
id: "late-task",
|
|
startDate: new Date("2026-03-01"),
|
|
endDate: new Date("2026-03-31"),
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
// Timeline should span from earliest start to latest end
|
|
const timeline = screen.getByRole("region", { name: /timeline/i });
|
|
expect(timeline).toBeInTheDocument();
|
|
});
|
|
|
|
it("should position task bars proportionally to their dates", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Task 1",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-05"), // 4 days
|
|
}),
|
|
createGanttTask({
|
|
id: "task-2",
|
|
title: "Task 2",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-11"), // 10 days
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const bars = screen.getAllByRole("button", { name: /gantt bar/i });
|
|
expect(bars).toHaveLength(2);
|
|
|
|
// Second bar should be wider (more days)
|
|
const bar1Width = bars[0]?.style.width;
|
|
const bar2Width = bars[1]?.style.width;
|
|
|
|
// Basic check that widths are set (exact values depend on implementation)
|
|
expect(bar1Width).toBeTruthy();
|
|
expect(bar2Width).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("Accessibility", (): void => {
|
|
it("should have proper ARIA labels for the chart region", (): void => {
|
|
render(<GanttChart tasks={[]} />);
|
|
|
|
expect(screen.getByRole("region", { name: /gantt chart/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it("should have proper ARIA labels for task bars", (): void => {
|
|
const task = createGanttTask({
|
|
id: "task-1",
|
|
title: "Accessible Task",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-15"),
|
|
});
|
|
|
|
render(<GanttChart tasks={[task]} />);
|
|
|
|
const taskBar = screen.getByRole("button", {
|
|
name: /gantt bar.*accessible task/i,
|
|
});
|
|
expect(taskBar).toHaveAccessibleName();
|
|
});
|
|
|
|
it("should be keyboard navigable", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
const onTaskClick = vi.fn();
|
|
|
|
const task = createGanttTask({ id: "task-1", title: "Keyboard Task" });
|
|
|
|
render(<GanttChart tasks={[task]} onTaskClick={onTaskClick} />);
|
|
|
|
const taskBar = screen.getByRole("button", { name: /gantt bar/i });
|
|
|
|
// Tab to focus
|
|
await user.tab();
|
|
expect(taskBar).toHaveFocus();
|
|
|
|
// Enter to activate
|
|
await user.keyboard("{Enter}");
|
|
expect(onTaskClick).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Responsive Design", (): void => {
|
|
it("should accept custom height prop", (): void => {
|
|
const tasks = [createGanttTask({ id: "task-1" })];
|
|
|
|
render(<GanttChart tasks={tasks} height={600} />);
|
|
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
expect(chart).toHaveStyle({ height: "600px" });
|
|
});
|
|
|
|
it("should use default height when not specified", (): void => {
|
|
const tasks = [createGanttTask({ id: "task-1" })];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
expect(chart).toBeInTheDocument();
|
|
// Default height should be set in implementation
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", (): void => {
|
|
it("should handle tasks with same start and end date", (): void => {
|
|
const sameDay = new Date("2026-02-01");
|
|
const task = createGanttTask({
|
|
id: "same-day",
|
|
title: "Same Day Task",
|
|
startDate: sameDay,
|
|
endDate: sameDay,
|
|
});
|
|
|
|
render(<GanttChart tasks={[task]} />);
|
|
|
|
expect(screen.getAllByText("Same Day Task").length).toBeGreaterThan(0);
|
|
const bar = screen.getByRole("button", { name: /gantt bar/i });
|
|
expect(bar).toBeInTheDocument();
|
|
// Bar should have minimum width
|
|
});
|
|
|
|
it("should handle tasks with very long duration", (): void => {
|
|
const task = createGanttTask({
|
|
id: "long-task",
|
|
title: "Long Task",
|
|
startDate: new Date("2026-01-01"),
|
|
endDate: new Date("2027-12-31"), // 2 years
|
|
});
|
|
|
|
render(<GanttChart tasks={[task]} />);
|
|
|
|
expect(screen.getAllByText("Long Task").length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should sort tasks by start date", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "late-task",
|
|
title: "Late Task",
|
|
startDate: new Date("2026-03-01"),
|
|
}),
|
|
createGanttTask({
|
|
id: "early-task",
|
|
title: "Early Task",
|
|
startDate: new Date("2026-01-01"),
|
|
}),
|
|
createGanttTask({
|
|
id: "mid-task",
|
|
title: "Mid Task",
|
|
startDate: new Date("2026-02-01"),
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
const taskNames = screen.getAllByRole("row").map((row): string | null => row.textContent);
|
|
|
|
// Early Task should appear first
|
|
const earlyIndex = taskNames.findIndex(
|
|
(name): boolean => name?.includes("Early Task") ?? false
|
|
);
|
|
const lateIndex = taskNames.findIndex(
|
|
(name): boolean => name?.includes("Late Task") ?? false
|
|
);
|
|
|
|
expect(earlyIndex).toBeLessThan(lateIndex);
|
|
});
|
|
});
|
|
|
|
describe("Dependencies", (): void => {
|
|
it("should render dependency lines when showDependencies is true", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Foundation",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-10"),
|
|
}),
|
|
createGanttTask({
|
|
id: "task-2",
|
|
title: "Build on top",
|
|
startDate: new Date("2026-02-11"),
|
|
endDate: new Date("2026-02-20"),
|
|
dependencies: ["task-1"],
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} showDependencies={true} />);
|
|
|
|
// Check if dependency SVG exists
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
expect(chart).toBeInTheDocument();
|
|
|
|
// Look for dependency path element
|
|
const svg = chart.querySelector(".gantt-dependencies");
|
|
expect(svg).toBeInTheDocument();
|
|
|
|
const paths = chart.querySelectorAll(".dependency-line");
|
|
expect(paths).toHaveLength(1);
|
|
});
|
|
|
|
it("should not render dependencies by default", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Task 1",
|
|
}),
|
|
createGanttTask({
|
|
id: "task-2",
|
|
title: "Task 2",
|
|
dependencies: ["task-1"],
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
// Dependencies should not be shown by default
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
expect(chart).toBeInTheDocument();
|
|
|
|
const svg = chart.querySelector(".gantt-dependencies");
|
|
expect(svg).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("should handle tasks with non-existent dependencies gracefully", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Task 1",
|
|
dependencies: ["non-existent-task"],
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} showDependencies={true} />);
|
|
|
|
// Should not crash
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
expect(chart).toBeInTheDocument();
|
|
});
|
|
|
|
it("should render multiple dependency lines", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Task 1",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-05"),
|
|
}),
|
|
createGanttTask({
|
|
id: "task-2",
|
|
title: "Task 2",
|
|
startDate: new Date("2026-02-01"),
|
|
endDate: new Date("2026-02-05"),
|
|
}),
|
|
createGanttTask({
|
|
id: "task-3",
|
|
title: "Task 3",
|
|
startDate: new Date("2026-02-06"),
|
|
endDate: new Date("2026-02-10"),
|
|
dependencies: ["task-1", "task-2"],
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} showDependencies={true} />);
|
|
|
|
const chart = screen.getByRole("region", { name: /gantt chart/i });
|
|
const paths = chart.querySelectorAll(".dependency-line");
|
|
expect(paths).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("Milestones", (): void => {
|
|
it("should render milestone as diamond shape", (): void => {
|
|
const milestone = createGanttTask({
|
|
id: "milestone-1",
|
|
title: "Phase 1 Complete",
|
|
startDate: new Date("2026-02-15"),
|
|
endDate: new Date("2026-02-15"),
|
|
isMilestone: true,
|
|
});
|
|
|
|
render(<GanttChart tasks={[milestone]} />);
|
|
|
|
const milestoneElement = screen.getByRole("button", {
|
|
name: /milestone.*phase 1 complete/i,
|
|
});
|
|
expect(milestoneElement).toBeInTheDocument();
|
|
expect(milestoneElement).toHaveClass("gantt-milestone");
|
|
});
|
|
|
|
it("should handle click on milestone", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
const onTaskClick = vi.fn();
|
|
|
|
const milestone = createGanttTask({
|
|
id: "milestone-1",
|
|
title: "Milestone Task",
|
|
isMilestone: true,
|
|
});
|
|
|
|
render(<GanttChart tasks={[milestone]} onTaskClick={onTaskClick} />);
|
|
|
|
const milestoneElement = screen.getByRole("button", {
|
|
name: /milestone.*milestone task/i,
|
|
});
|
|
await user.click(milestoneElement);
|
|
|
|
expect(onTaskClick).toHaveBeenCalledWith(milestone);
|
|
});
|
|
|
|
it("should support keyboard navigation for milestones", async (): Promise<void> => {
|
|
const user = userEvent.setup();
|
|
const onTaskClick = vi.fn();
|
|
|
|
const milestone = createGanttTask({
|
|
id: "milestone-1",
|
|
title: "Keyboard Milestone",
|
|
isMilestone: true,
|
|
});
|
|
|
|
render(<GanttChart tasks={[milestone]} onTaskClick={onTaskClick} />);
|
|
|
|
const milestoneElement = screen.getByRole("button", {
|
|
name: /milestone/i,
|
|
});
|
|
|
|
await user.tab();
|
|
expect(milestoneElement).toHaveFocus();
|
|
|
|
await user.keyboard("{Enter}");
|
|
expect(onTaskClick).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should render milestones and regular tasks together", (): void => {
|
|
const tasks = [
|
|
createGanttTask({
|
|
id: "task-1",
|
|
title: "Regular Task",
|
|
isMilestone: false,
|
|
}),
|
|
createGanttTask({
|
|
id: "milestone-1",
|
|
title: "Milestone",
|
|
isMilestone: true,
|
|
}),
|
|
];
|
|
|
|
render(<GanttChart tasks={tasks} />);
|
|
|
|
expect(screen.getByRole("button", { name: /gantt bar.*regular task/i })).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: /milestone.*milestone/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|