@reqord/web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +661 -0
  2. package/next-env.d.ts +6 -0
  3. package/next.config.ts +7 -0
  4. package/package.json +59 -0
  5. package/postcss.config.mjs +7 -0
  6. package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
  7. package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
  8. package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
  9. package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
  10. package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
  11. package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
  12. package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
  13. package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
  14. package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
  15. package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
  16. package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
  17. package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
  18. package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
  19. package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
  20. package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
  21. package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
  22. package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
  23. package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
  24. package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
  25. package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
  26. package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
  27. package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
  28. package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
  29. package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
  30. package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
  31. package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
  32. package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
  33. package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
  34. package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
  35. package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
  36. package/src/__tests__/lib/dashboard-data.test.ts +334 -0
  37. package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
  38. package/src/__tests__/lib/gantt-data.test.ts +299 -0
  39. package/src/__tests__/lib/graph-data.test.ts +309 -0
  40. package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
  41. package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
  42. package/src/__tests__/lib/reqord-root.test.ts +31 -0
  43. package/src/__tests__/lib/specification-file.test.ts +63 -0
  44. package/src/__tests__/lib/tasks-data.test.ts +104 -0
  45. package/src/app/dashboard/loading.tsx +21 -0
  46. package/src/app/dashboard/page.tsx +50 -0
  47. package/src/app/error.tsx +22 -0
  48. package/src/app/feedback/loading.tsx +13 -0
  49. package/src/app/feedback/page.tsx +48 -0
  50. package/src/app/globals.css +2 -0
  51. package/src/app/graph/page.tsx +32 -0
  52. package/src/app/layout.tsx +25 -0
  53. package/src/app/page.tsx +5 -0
  54. package/src/app/requirements/[id]/edit/page.tsx +40 -0
  55. package/src/app/requirements/[id]/loading.tsx +14 -0
  56. package/src/app/requirements/[id]/not-found.tsx +18 -0
  57. package/src/app/requirements/[id]/page.tsx +43 -0
  58. package/src/app/requirements/loading.tsx +13 -0
  59. package/src/app/requirements/new/page.tsx +14 -0
  60. package/src/app/requirements/page.tsx +35 -0
  61. package/src/app/specifications/[id]/loading.tsx +14 -0
  62. package/src/app/specifications/[id]/not-found.tsx +18 -0
  63. package/src/app/specifications/[id]/page.tsx +52 -0
  64. package/src/app/specifications/loading.tsx +13 -0
  65. package/src/app/specifications/page.tsx +42 -0
  66. package/src/components/dashboard/critical-path-display.tsx +76 -0
  67. package/src/components/dashboard/progress-bar.tsx +45 -0
  68. package/src/components/dashboard/progress-section.tsx +57 -0
  69. package/src/components/dashboard/project-health.tsx +35 -0
  70. package/src/components/dashboard/status-card.tsx +27 -0
  71. package/src/components/dashboard/status-cards.tsx +28 -0
  72. package/src/components/dashboard/warning-alert.tsx +33 -0
  73. package/src/components/dashboard/warning-alerts.tsx +24 -0
  74. package/src/components/feedback/feedback-badge.tsx +48 -0
  75. package/src/components/feedback/feedback-client-view.tsx +38 -0
  76. package/src/components/feedback/feedback-filters.tsx +86 -0
  77. package/src/components/feedback/feedback-linked-items.tsx +93 -0
  78. package/src/components/feedback/feedback-list.tsx +40 -0
  79. package/src/components/feedback/feedback-table.tsx +115 -0
  80. package/src/components/gantt/gantt-bar.tsx +65 -0
  81. package/src/components/gantt/gantt-chart.tsx +88 -0
  82. package/src/components/gantt/gantt-constants.ts +15 -0
  83. package/src/components/gantt/gantt-critical-path.tsx +38 -0
  84. package/src/components/gantt/gantt-group.tsx +25 -0
  85. package/src/components/gantt/gantt-header.tsx +47 -0
  86. package/src/components/gantt/gantt-legend.tsx +26 -0
  87. package/src/components/graph/dag-layout.ts +131 -0
  88. package/src/components/graph/dependency-graph.tsx +88 -0
  89. package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
  90. package/src/components/graph/drilldown-graph.tsx +45 -0
  91. package/src/components/graph/edge-styles.ts +16 -0
  92. package/src/components/graph/graph-loader.tsx +25 -0
  93. package/src/components/graph/graph-page-client.tsx +98 -0
  94. package/src/components/graph/issue-node.tsx +46 -0
  95. package/src/components/graph/multi-level-graph.tsx +91 -0
  96. package/src/components/graph/requirement-node.tsx +69 -0
  97. package/src/components/graph/specification-node.tsx +39 -0
  98. package/src/components/requirement/delete-button.tsx +46 -0
  99. package/src/components/requirement/dependency-editor.tsx +79 -0
  100. package/src/components/requirement/markdown-editor.tsx +47 -0
  101. package/src/components/requirement/markdown-renderer.tsx +12 -0
  102. package/src/components/requirement/requirement-detail.tsx +228 -0
  103. package/src/components/requirement/requirement-form.tsx +390 -0
  104. package/src/components/requirement/requirement-table.tsx +203 -0
  105. package/src/components/requirement/requirement-tabs.tsx +65 -0
  106. package/src/components/requirement/success-criteria-editor.tsx +53 -0
  107. package/src/components/specification/spec-detail.tsx +103 -0
  108. package/src/components/specification/spec-tabs.tsx +66 -0
  109. package/src/components/specification/specification-table.tsx +193 -0
  110. package/src/components/specification/tab-coverage.tsx +52 -0
  111. package/src/components/specification/tab-design.tsx +16 -0
  112. package/src/components/specification/tab-history.tsx +61 -0
  113. package/src/components/specification/tab-issues.tsx +111 -0
  114. package/src/components/specification/tab-research.tsx +16 -0
  115. package/src/components/ui/badge.tsx +64 -0
  116. package/src/components/ui/nav.tsx +49 -0
  117. package/src/components/ui/tabs.tsx +39 -0
  118. package/src/lib/actions.ts +222 -0
  119. package/src/lib/dashboard-data.ts +224 -0
  120. package/src/lib/data.ts +21 -0
  121. package/src/lib/drilldown-graph-data.ts +98 -0
  122. package/src/lib/feedback-data.ts +33 -0
  123. package/src/lib/feedback-repository.ts +6 -0
  124. package/src/lib/file-system.ts +167 -0
  125. package/src/lib/gantt-data.ts +168 -0
  126. package/src/lib/get-repository.ts +43 -0
  127. package/src/lib/graph-data.ts +161 -0
  128. package/src/lib/id-generator.ts +23 -0
  129. package/src/lib/local-feedback-repository.ts +36 -0
  130. package/src/lib/local-repository.ts +78 -0
  131. package/src/lib/local-specification-repository.ts +61 -0
  132. package/src/lib/repository.ts +11 -0
  133. package/src/lib/reqord-root.ts +33 -0
  134. package/src/lib/specification-data.ts +28 -0
  135. package/src/lib/specification-file.ts +12 -0
  136. package/src/lib/specification-repository.ts +8 -0
  137. package/src/lib/tasks-data.ts +32 -0
  138. package/tsconfig.json +27 -0
@@ -0,0 +1,49 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import type { FeedbackEntry } from "@reqord/shared";
7
+ import { FeedbackList } from "../../../components/feedback/feedback-list";
8
+
9
+ const baseFeedback: FeedbackEntry = {
10
+ githubIssue: 42,
11
+ type: "bug",
12
+ severity: "high",
13
+ status: "open",
14
+ linkedTo: {
15
+ requirements: [],
16
+ createdRequirements: [],
17
+ specifications: [],
18
+ createdSpecifications: [],
19
+ },
20
+ syncedAt: "2026-01-15T10:00:00Z",
21
+ };
22
+
23
+ describe("FeedbackList", () => {
24
+ afterEach(() => cleanup());
25
+
26
+ it("feedbacks空: 何も表示されない", () => {
27
+ const { container } = render(<FeedbackList feedbacks={[]} />);
28
+ expect(container.innerHTML).toBe("");
29
+ });
30
+
31
+ it("titleがある場合はtitleが表示される", () => {
32
+ const fb: FeedbackEntry = { ...baseFeedback, title: "Login fails on Safari" };
33
+ render(<FeedbackList feedbacks={[fb]} />);
34
+
35
+ expect(screen.getByTestId("feedback-title")).toHaveTextContent("Login fails on Safari");
36
+ });
37
+
38
+ it("titleがundefinedの場合はtitleが表示されない", () => {
39
+ render(<FeedbackList feedbacks={[baseFeedback]} />);
40
+
41
+ expect(screen.queryByTestId("feedback-title")).toBeNull();
42
+ });
43
+
44
+ it("Issue番号が表示される", () => {
45
+ render(<FeedbackList feedbacks={[baseFeedback]} />);
46
+
47
+ expect(screen.getByTestId("feedback-issue")).toHaveTextContent("#42");
48
+ });
49
+ });
@@ -0,0 +1,165 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import type { FeedbackEntry } from "@reqord/shared";
7
+ import { FeedbackTable } from "../../../components/feedback/feedback-table";
8
+
9
+ const baseFeedback: FeedbackEntry = {
10
+ githubIssue: 42,
11
+ type: "bug",
12
+ severity: "high",
13
+ status: "open",
14
+ linkedTo: {
15
+ requirements: ["req-000001"],
16
+ createdRequirements: [],
17
+ specifications: [],
18
+ createdSpecifications: [],
19
+ },
20
+ syncedAt: "2026-01-15T10:00:00Z",
21
+ };
22
+
23
+ const reqTitles: Record<string, string> = { "req-000001": "認証" };
24
+ const specTitles: Record<string, string> = {};
25
+
26
+ describe("FeedbackTable", () => {
27
+ afterEach(() => cleanup());
28
+
29
+ it("feedbacks空: empty メッセージが表示される", () => {
30
+ render(
31
+ <FeedbackTable
32
+ feedbacks={[]}
33
+ requirementTitles={reqTitles}
34
+ specificationTitles={specTitles}
35
+ />,
36
+ );
37
+ expect(screen.getByTestId("feedback-empty")).toBeInTheDocument();
38
+ });
39
+
40
+ it("feedbacks配列に基づく行数の正確性", () => {
41
+ const feedbacks: FeedbackEntry[] = [
42
+ baseFeedback,
43
+ { ...baseFeedback, githubIssue: 43, type: "improvement", severity: "medium" },
44
+ { ...baseFeedback, githubIssue: 44, type: "security", severity: "critical" },
45
+ ];
46
+ render(
47
+ <FeedbackTable
48
+ feedbacks={feedbacks}
49
+ requirementTitles={reqTitles}
50
+ specificationTitles={specTitles}
51
+ />,
52
+ );
53
+
54
+ expect(screen.getAllByTestId("feedback-row")).toHaveLength(3);
55
+ });
56
+
57
+ it("Issue番号が表示される", () => {
58
+ render(
59
+ <FeedbackTable
60
+ feedbacks={[baseFeedback]}
61
+ requirementTitles={reqTitles}
62
+ specificationTitles={specTitles}
63
+ />,
64
+ );
65
+
66
+ expect(screen.getByTestId("feedback-issue")).toHaveTextContent("#42");
67
+ });
68
+
69
+ it("タイプバッジにbug用の色クラスが適用される", () => {
70
+ render(
71
+ <FeedbackTable
72
+ feedbacks={[baseFeedback]}
73
+ requirementTitles={reqTitles}
74
+ specificationTitles={specTitles}
75
+ />,
76
+ );
77
+
78
+ const badge = screen.getByTestId("feedback-type-badge");
79
+ expect(badge).toHaveTextContent("bug");
80
+ expect(badge).toHaveClass("bg-red-100");
81
+ });
82
+
83
+ it("severityバッジが表示される", () => {
84
+ render(
85
+ <FeedbackTable
86
+ feedbacks={[baseFeedback]}
87
+ requirementTitles={reqTitles}
88
+ specificationTitles={specTitles}
89
+ />,
90
+ );
91
+
92
+ const badge = screen.getByTestId("feedback-severity-badge");
93
+ expect(badge).toHaveTextContent("high");
94
+ expect(badge).toHaveClass("bg-orange-500");
95
+ });
96
+
97
+ it("statusバッジが表示される", () => {
98
+ render(
99
+ <FeedbackTable
100
+ feedbacks={[baseFeedback]}
101
+ requirementTitles={reqTitles}
102
+ specificationTitles={specTitles}
103
+ />,
104
+ );
105
+
106
+ const badge = screen.getByTestId("feedback-status-badge");
107
+ expect(badge).toHaveTextContent("open");
108
+ expect(badge).toHaveClass("bg-green-100");
109
+ });
110
+
111
+ it("type/severityがundefinedの場合はバッジが表示されない", () => {
112
+ const fb: FeedbackEntry = {
113
+ ...baseFeedback,
114
+ type: undefined,
115
+ severity: undefined,
116
+ };
117
+ render(
118
+ <FeedbackTable
119
+ feedbacks={[fb]}
120
+ requirementTitles={reqTitles}
121
+ specificationTitles={specTitles}
122
+ />,
123
+ );
124
+
125
+ expect(screen.queryByTestId("feedback-type-badge")).toBeNull();
126
+ expect(screen.queryByTestId("feedback-severity-badge")).toBeNull();
127
+ });
128
+
129
+ it("titleがある場合はtitleが表示される", () => {
130
+ const fb: FeedbackEntry = { ...baseFeedback, title: "Login button broken" };
131
+ render(
132
+ <FeedbackTable
133
+ feedbacks={[fb]}
134
+ requirementTitles={reqTitles}
135
+ specificationTitles={specTitles}
136
+ />,
137
+ );
138
+
139
+ expect(screen.getByTestId("feedback-title")).toHaveTextContent("Login button broken");
140
+ });
141
+
142
+ it("titleがundefinedの場合は'-'が表示される", () => {
143
+ render(
144
+ <FeedbackTable
145
+ feedbacks={[baseFeedback]}
146
+ requirementTitles={reqTitles}
147
+ specificationTitles={specTitles}
148
+ />,
149
+ );
150
+
151
+ expect(screen.getByTestId("feedback-title")).toHaveTextContent("-");
152
+ });
153
+
154
+ it("関連Req/Specリンクが表示される", () => {
155
+ render(
156
+ <FeedbackTable
157
+ feedbacks={[baseFeedback]}
158
+ requirementTitles={reqTitles}
159
+ specificationTitles={specTitles}
160
+ />,
161
+ );
162
+
163
+ expect(screen.getByTestId("linked-requirement")).toHaveTextContent("認証");
164
+ });
165
+ });
@@ -0,0 +1,41 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { FeedbackBadge } from "../../../components/feedback/feedback-badge";
7
+
8
+ describe("FeedbackBadge", () => {
9
+ afterEach(() => cleanup());
10
+
11
+ it("bugタイプにred色クラスが適用される", () => {
12
+ render(<FeedbackBadge type="bug" />);
13
+ const badge = screen.getByTestId("feedback-badge-bug");
14
+ expect(badge).toHaveClass("bg-red-100");
15
+ expect(badge).toHaveTextContent("Bug");
16
+ });
17
+
18
+ it("improvementタイプにblue色クラスが適用される", () => {
19
+ render(<FeedbackBadge type="improvement" />);
20
+ const badge = screen.getByTestId("feedback-badge-improvement");
21
+ expect(badge).toHaveClass("bg-blue-100");
22
+ expect(badge).toHaveTextContent("Improvement");
23
+ });
24
+
25
+ it("spec-mismatchタイプにpurple色クラスが適用される", () => {
26
+ render(<FeedbackBadge type="spec-mismatch" />);
27
+ const badge = screen.getByTestId("feedback-badge-spec-mismatch");
28
+ expect(badge).toHaveClass("bg-purple-100");
29
+ expect(badge).toHaveTextContent("Spec Mismatch");
30
+ });
31
+
32
+ it("severity propsがある場合のみseverityバッジが表示される", () => {
33
+ render(<FeedbackBadge type="bug" severity="high" />);
34
+ expect(screen.getByTestId("feedback-severity")).toHaveTextContent("high");
35
+ });
36
+
37
+ it("severity propsがない場合はseverityバッジが表示されない", () => {
38
+ render(<FeedbackBadge type="bug" />);
39
+ expect(screen.queryByTestId("feedback-severity")).toBeNull();
40
+ });
41
+ });
@@ -0,0 +1,51 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import type { FeedbackEntry } from "@reqord/shared";
7
+ import { FeedbackList } from "../../../components/feedback/feedback-list";
8
+
9
+ const makeFeedback = (overrides: Partial<FeedbackEntry> = {}): FeedbackEntry => ({
10
+ githubIssue: 1,
11
+ type: "bug",
12
+ severity: "high",
13
+ linkedTo: {
14
+ requirements: [],
15
+ createdRequirements: [],
16
+ specifications: ["spec-000001"],
17
+ createdSpecifications: [],
18
+ },
19
+ syncedAt: "2026-01-01T00:00:00Z",
20
+ status: "open",
21
+ ...overrides,
22
+ });
23
+
24
+ describe("FeedbackList", () => {
25
+ afterEach(() => cleanup());
26
+
27
+ it("feedbacks空配列: 何も表示されない", () => {
28
+ const { container } = render(<FeedbackList feedbacks={[]} />);
29
+ expect(container.firstChild).toBeNull();
30
+ });
31
+
32
+ it("feedbackエントリ: githubIssueとseverityバッジが表示される", () => {
33
+ render(<FeedbackList feedbacks={[makeFeedback({ githubIssue: 42, severity: "high" })]} />);
34
+
35
+ expect(screen.getByTestId("feedback-list")).toBeInTheDocument();
36
+ expect(screen.getByTestId("feedback-severity")).toHaveTextContent("high");
37
+ expect(screen.getByTestId("feedback-issue")).toHaveTextContent("#42");
38
+ });
39
+
40
+ it("複数feedback: 全件表示される", () => {
41
+ const feedbacks: FeedbackEntry[] = [
42
+ makeFeedback({ githubIssue: 1, type: "bug" }),
43
+ makeFeedback({ githubIssue: 2, type: "improvement" }),
44
+ makeFeedback({ githubIssue: 3, type: "security" }),
45
+ ];
46
+ render(<FeedbackList feedbacks={feedbacks} />);
47
+
48
+ const items = screen.getAllByTestId("feedback-item");
49
+ expect(items).toHaveLength(3);
50
+ });
51
+ });
@@ -0,0 +1,190 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, vi } from "vitest";
4
+ import { render, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { GanttBar } from "@/components/gantt/gantt-bar";
7
+ import type { GanttTask } from "@/lib/gantt-data";
8
+
9
+ describe("GanttBar", () => {
10
+ const mockTask: GanttTask = {
11
+ id: "1",
12
+ title: "Test Task",
13
+ issueNumber: 123,
14
+ issueUrl: "https://github.com/test/repo/issues/123",
15
+ priority: "P0",
16
+ state: "open",
17
+ estimatedHours: 4,
18
+ startOffset: 0,
19
+ dependencies: [],
20
+ isCriticalPath: false,
21
+ };
22
+
23
+ it("renders closed state with green color", () => {
24
+ const task = { ...mockTask, state: "closed" };
25
+ const { container } = render(
26
+ <svg>
27
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
28
+ </svg>
29
+ );
30
+
31
+ const rect = container.querySelector("rect");
32
+ expect(rect).toHaveAttribute("fill", "#22c55e");
33
+ });
34
+
35
+ it("renders in_progress state with blue color", () => {
36
+ const task = { ...mockTask, state: "in_progress" };
37
+ const { container } = render(
38
+ <svg>
39
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
40
+ </svg>
41
+ );
42
+
43
+ const rect = container.querySelector("rect");
44
+ expect(rect).toHaveAttribute("fill", "#3b82f6");
45
+ });
46
+
47
+ it("renders blocked state with red color", () => {
48
+ const task = { ...mockTask, state: "blocked" };
49
+ const { container } = render(
50
+ <svg>
51
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
52
+ </svg>
53
+ );
54
+
55
+ const rect = container.querySelector("rect");
56
+ expect(rect).toHaveAttribute("fill", "#ef4444");
57
+ });
58
+
59
+ it("renders open state with gray color", () => {
60
+ const task = { ...mockTask, state: "open" };
61
+ const { container } = render(
62
+ <svg>
63
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
64
+ </svg>
65
+ );
66
+
67
+ const rect = container.querySelector("rect");
68
+ expect(rect).toHaveAttribute("fill", "#9ca3af");
69
+ });
70
+
71
+ it("renders bar with correct width based on estimatedHours", () => {
72
+ const task = { ...mockTask, estimatedHours: 8 };
73
+ const { container } = render(
74
+ <svg>
75
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
76
+ </svg>
77
+ );
78
+
79
+ const rect = container.querySelector("rect");
80
+ expect(rect).toHaveAttribute("width", "480"); // 8 * 60
81
+ });
82
+
83
+ it("renders bar with correct x position based on startOffset", () => {
84
+ const task = { ...mockTask, startOffset: 4, estimatedHours: 4 };
85
+ const { container } = render(
86
+ <svg>
87
+ <GanttBar task={task} y={0} hourWidth={60} leftLabelWidth={200} />
88
+ </svg>
89
+ );
90
+
91
+ const rect = container.querySelector("rect");
92
+ expect(rect).toHaveAttribute("x", "440"); // 4 * 60 + 200
93
+ });
94
+
95
+ it("renders bar with correct y position", () => {
96
+ const { container } = render(
97
+ <svg>
98
+ <GanttBar task={mockTask} y={100} hourWidth={60} leftLabelWidth={200} />
99
+ </svg>
100
+ );
101
+
102
+ const rect = container.querySelector("rect");
103
+ expect(rect).toHaveAttribute("y", "100");
104
+ });
105
+
106
+ it("renders bar with correct height", () => {
107
+ const { container } = render(
108
+ <svg>
109
+ <GanttBar task={mockTask} y={0} hourWidth={60} leftLabelWidth={200} />
110
+ </svg>
111
+ );
112
+
113
+ const rect = container.querySelector("rect");
114
+ expect(rect).toHaveAttribute("height", "24"); // BAR_HEIGHT constant
115
+ });
116
+
117
+ it("calls onHover with task on mouse enter", () => {
118
+ const onHover = vi.fn();
119
+ const { container } = render(
120
+ <svg>
121
+ <GanttBar
122
+ task={mockTask}
123
+ y={0}
124
+ hourWidth={60}
125
+ leftLabelWidth={200}
126
+ onHover={onHover}
127
+ />
128
+ </svg>
129
+ );
130
+
131
+ const group = container.querySelector("g");
132
+ if (group) {
133
+ fireEvent.mouseEnter(group);
134
+ }
135
+ expect(onHover).toHaveBeenCalledWith(mockTask);
136
+ });
137
+
138
+ it("calls onHover with null on mouse leave", () => {
139
+ const onHover = vi.fn();
140
+ const { container } = render(
141
+ <svg>
142
+ <GanttBar
143
+ task={mockTask}
144
+ y={0}
145
+ hourWidth={60}
146
+ leftLabelWidth={200}
147
+ onHover={onHover}
148
+ />
149
+ </svg>
150
+ );
151
+
152
+ const group = container.querySelector("g");
153
+ if (group) {
154
+ fireEvent.mouseLeave(group);
155
+ }
156
+ expect(onHover).toHaveBeenCalledWith(null);
157
+ });
158
+
159
+ it("calls onClick with task when clicked", () => {
160
+ const onClick = vi.fn();
161
+ const { container } = render(
162
+ <svg>
163
+ <GanttBar
164
+ task={mockTask}
165
+ y={0}
166
+ hourWidth={60}
167
+ leftLabelWidth={200}
168
+ onClick={onClick}
169
+ />
170
+ </svg>
171
+ );
172
+
173
+ const group = container.querySelector("g");
174
+ if (group) {
175
+ fireEvent.click(group);
176
+ }
177
+ expect(onClick).toHaveBeenCalledWith(mockTask);
178
+ });
179
+
180
+ it("renders task title as text", () => {
181
+ const { container } = render(
182
+ <svg>
183
+ <GanttBar task={mockTask} y={0} hourWidth={60} leftLabelWidth={200} />
184
+ </svg>
185
+ );
186
+
187
+ const text = container.querySelector("text");
188
+ expect(text).toHaveTextContent("Test Task");
189
+ });
190
+ });
@@ -0,0 +1,141 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect } from "vitest";
4
+ import { render, screen } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { GanttChart } from "@/components/gantt/gantt-chart";
7
+ import type { GanttData } from "@/lib/gantt-data";
8
+
9
+ describe("GanttChart", () => {
10
+ const mockData: GanttData = {
11
+ specId: "spec-000001",
12
+ groups: [
13
+ {
14
+ priority: "P0",
15
+ label: "P0: Sequential",
16
+ tasks: [
17
+ {
18
+ id: "1",
19
+ title: "Task 1",
20
+ issueNumber: 100,
21
+ issueUrl: "https://github.com/test/repo/issues/100",
22
+ priority: "P0",
23
+ state: "closed",
24
+ estimatedHours: 4,
25
+ startOffset: 0,
26
+ dependencies: [],
27
+ isCriticalPath: true,
28
+ },
29
+ {
30
+ id: "2",
31
+ title: "Task 2",
32
+ issueNumber: 101,
33
+ issueUrl: "https://github.com/test/repo/issues/101",
34
+ priority: "P0",
35
+ state: "in_progress",
36
+ estimatedHours: 4,
37
+ startOffset: 4,
38
+ dependencies: [100],
39
+ isCriticalPath: true,
40
+ },
41
+ ],
42
+ },
43
+ {
44
+ priority: "P1",
45
+ label: "P1: Parallel",
46
+ tasks: [
47
+ {
48
+ id: "3",
49
+ title: "Task 3",
50
+ issueNumber: 102,
51
+ issueUrl: "https://github.com/test/repo/issues/102",
52
+ priority: "P1",
53
+ state: "open",
54
+ estimatedHours: 4,
55
+ startOffset: 8,
56
+ dependencies: [],
57
+ isCriticalPath: false,
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ totalEstimatedHours: 12,
63
+ timelineStart: 0,
64
+ timelineEnd: 12,
65
+ };
66
+
67
+ it("renders SVG element", () => {
68
+ const { container } = render(<GanttChart data={mockData} />);
69
+ const svg = container.querySelector("svg");
70
+ expect(svg).toBeInTheDocument();
71
+ });
72
+
73
+ it("renders correct number of task bars", () => {
74
+ const { container } = render(<GanttChart data={mockData} />);
75
+ const rects = container.querySelectorAll("rect");
76
+ // Each task has a rect, plus group headers
77
+ expect(rects.length).toBeGreaterThanOrEqual(3); // 3 tasks minimum
78
+ });
79
+
80
+ it("renders group labels", () => {
81
+ render(<GanttChart data={mockData} />);
82
+ const labels = screen.getAllByText("P0: Sequential");
83
+ expect(labels.length).toBeGreaterThanOrEqual(1);
84
+ const p1Labels = screen.getAllByText("P1: Parallel");
85
+ expect(p1Labels.length).toBeGreaterThanOrEqual(1);
86
+ });
87
+
88
+ it("renders legend component", () => {
89
+ render(<GanttChart data={mockData} />);
90
+ const completed = screen.getAllByText("Completed");
91
+ expect(completed.length).toBeGreaterThanOrEqual(1);
92
+ const inProgress = screen.getAllByText("In Progress");
93
+ expect(inProgress.length).toBeGreaterThanOrEqual(1);
94
+ });
95
+
96
+ it("renders empty chart when no groups", () => {
97
+ const emptyData: GanttData = {
98
+ specId: "spec-000001",
99
+ groups: [],
100
+ totalEstimatedHours: 0,
101
+ timelineStart: 0,
102
+ timelineEnd: 0,
103
+ };
104
+
105
+ const { container } = render(<GanttChart data={emptyData} />);
106
+ const svg = container.querySelector("svg");
107
+ expect(svg).toBeInTheDocument();
108
+ });
109
+
110
+ it("handles single task", () => {
111
+ const singleTaskData: GanttData = {
112
+ specId: "spec-000001",
113
+ groups: [
114
+ {
115
+ priority: "P0",
116
+ label: "P0: Sequential",
117
+ tasks: [
118
+ {
119
+ id: "1",
120
+ title: "Only Task",
121
+ issueNumber: 100,
122
+ issueUrl: "https://github.com/test/repo/issues/100",
123
+ priority: "P0",
124
+ state: "open",
125
+ estimatedHours: 4,
126
+ startOffset: 0,
127
+ dependencies: [],
128
+ isCriticalPath: true,
129
+ },
130
+ ],
131
+ },
132
+ ],
133
+ totalEstimatedHours: 4,
134
+ timelineStart: 0,
135
+ timelineEnd: 4,
136
+ };
137
+
138
+ render(<GanttChart data={singleTaskData} />);
139
+ expect(screen.getByText("Only Task")).toBeInTheDocument();
140
+ });
141
+ });