feat(#15): implement gantt chart component

- Add milestone support with diamond markers
- Implement dependency line rendering with SVG arrows
- Add isMilestone property to GanttTask type
- Create dependency calculation and visualization
- Add comprehensive tests for milestones and dependencies
- Add index module tests for exports
- Coverage: GanttChart 98.37%, types 91.66%, index 100%
This commit is contained in:
Jason Woltje
2026-01-29 19:08:47 -06:00
parent 9ff7718f9c
commit aa6d466321
6 changed files with 356 additions and 6 deletions

View File

@@ -354,28 +354,36 @@ describe("GanttChart", () => {
});
});
describe("Dependencies (stretch goal)", () => {
describe("Dependencies", () => {
it("should render dependency lines when showDependencies is true", () => {
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 visualization exists
// Check if dependency SVG exists
const chart = screen.getByRole("region", { name: /gantt chart/i });
expect(chart).toBeInTheDocument();
// Specific dependency rendering will depend on implementation
// This is a basic check that the prop is accepted
// 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", () => {
@@ -396,6 +404,138 @@ describe("GanttChart", () => {
// 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", () => {
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", () => {
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", () => {
it("should render milestone as diamond shape", () => {
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 () => {
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 () => {
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", () => {
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();
});
});
});