@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,173 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach, vi } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { IssueNode } from "../../../components/graph/issue-node";
7
+
8
+ // Mock @xyflow/react Handle and Position
9
+ vi.mock("@xyflow/react", () => ({
10
+ Handle: ({ type }: any) => <div data-testid={`handle-${type}`} />,
11
+ Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
12
+ }));
13
+
14
+ const defaultNodeProps = {
15
+ selected: false,
16
+ dragging: false,
17
+ draggable: true,
18
+ selectable: true,
19
+ deletable: false,
20
+ zIndex: 0,
21
+ isConnectable: true,
22
+ positionAbsoluteX: 0,
23
+ positionAbsoluteY: 0,
24
+ };
25
+
26
+ describe("IssueNode", () => {
27
+ afterEach(() => {
28
+ cleanup();
29
+ });
30
+
31
+ it("renders issue number text", () => {
32
+ render(
33
+ <IssueNode
34
+ {...defaultNodeProps}
35
+ id="issue-spec-000001-123"
36
+ data={{
37
+ label: "Issue #123",
38
+ status: "open",
39
+ issueNumber: 123,
40
+ issueUrl: "https://github.com/test/repo/issues/123",
41
+ }}
42
+ type="issue"
43
+ />
44
+ );
45
+
46
+ expect(screen.getByText("Issue #123")).toBeInTheDocument();
47
+ });
48
+
49
+ it("renders open status with yellow background class", () => {
50
+ const { container } = render(
51
+ <IssueNode
52
+ {...defaultNodeProps}
53
+ id="issue-spec-000001-123"
54
+ data={{
55
+ label: "Issue #123",
56
+ status: "open",
57
+ issueNumber: 123,
58
+ issueUrl: "https://github.com/test/repo/issues/123",
59
+ }}
60
+ type="issue"
61
+ />
62
+ );
63
+
64
+ const nodeDiv = container.querySelector(".bg-yellow-200");
65
+ expect(nodeDiv).toBeInTheDocument();
66
+ });
67
+
68
+ it("renders closed status with green background class", () => {
69
+ const { container } = render(
70
+ <IssueNode
71
+ {...defaultNodeProps}
72
+ id="issue-spec-000001-124"
73
+ data={{
74
+ label: "Issue #124",
75
+ status: "closed",
76
+ issueNumber: 124,
77
+ issueUrl: "https://github.com/test/repo/issues/124",
78
+ }}
79
+ type="issue"
80
+ />
81
+ );
82
+
83
+ const nodeDiv = container.querySelector(".bg-green-200");
84
+ expect(nodeDiv).toBeInTheDocument();
85
+ });
86
+
87
+ it("renders in_progress status with blue background class", () => {
88
+ const { container } = render(
89
+ <IssueNode
90
+ {...defaultNodeProps}
91
+ id="issue-spec-000001-125"
92
+ data={{
93
+ label: "Issue #125",
94
+ status: "in_progress",
95
+ issueNumber: 125,
96
+ issueUrl: "https://github.com/test/repo/issues/125",
97
+ }}
98
+ type="issue"
99
+ />
100
+ );
101
+
102
+ const nodeDiv = container.querySelector(".bg-blue-200");
103
+ expect(nodeDiv).toBeInTheDocument();
104
+ });
105
+
106
+ it("renders status badge", () => {
107
+ render(
108
+ <IssueNode
109
+ {...defaultNodeProps}
110
+ id="issue-spec-000001-123"
111
+ data={{
112
+ label: "Issue #123",
113
+ status: "open",
114
+ issueNumber: 123,
115
+ issueUrl: "https://github.com/test/repo/issues/123",
116
+ }}
117
+ type="issue"
118
+ />
119
+ );
120
+
121
+ expect(screen.getByText("open")).toBeInTheDocument();
122
+ });
123
+
124
+ it("renders only target handle", () => {
125
+ render(
126
+ <IssueNode
127
+ {...defaultNodeProps}
128
+ id="issue-spec-000001-123"
129
+ data={{
130
+ label: "Issue #123",
131
+ status: "open",
132
+ issueNumber: 123,
133
+ issueUrl: "https://github.com/test/repo/issues/123",
134
+ }}
135
+ type="issue"
136
+ />
137
+ );
138
+
139
+ expect(screen.getByTestId("handle-target")).toBeInTheDocument();
140
+ expect(screen.queryByTestId("handle-source")).not.toBeInTheDocument();
141
+ });
142
+
143
+ it("opens issue URL in new tab when clicked", () => {
144
+ const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
145
+
146
+ render(
147
+ <IssueNode
148
+ {...defaultNodeProps}
149
+ id="issue-spec-000001-123"
150
+ data={{
151
+ label: "Issue #123",
152
+ status: "open",
153
+ issueNumber: 123,
154
+ issueUrl: "https://github.com/test/repo/issues/123",
155
+ }}
156
+ type="issue"
157
+ />
158
+ );
159
+
160
+ const node = screen.getByText("Issue #123").closest("div");
161
+ if (node) {
162
+ fireEvent.click(node);
163
+ }
164
+
165
+ expect(windowOpenSpy).toHaveBeenCalledWith(
166
+ "https://github.com/test/repo/issues/123",
167
+ "_blank",
168
+ "noopener,noreferrer"
169
+ );
170
+
171
+ windowOpenSpy.mockRestore();
172
+ });
173
+ });
@@ -0,0 +1,151 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach, vi } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { RequirementNode } from "../../../components/graph/requirement-node";
7
+
8
+ const mockPush = vi.fn();
9
+
10
+ // Mock next/navigation
11
+ vi.mock("next/navigation", () => ({
12
+ useRouter: () => ({ push: mockPush }),
13
+ }));
14
+
15
+ // Mock @xyflow/react Handle and Position
16
+ vi.mock("@xyflow/react", () => ({
17
+ Handle: ({ type }: any) => <div data-testid={`handle-${type}`} />,
18
+ Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
19
+ }));
20
+
21
+ describe("RequirementNode", () => {
22
+ afterEach(() => {
23
+ cleanup();
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ const baseProps = {
28
+ id: "req-000001",
29
+ type: "requirement" as const,
30
+ selected: false,
31
+ dragging: false,
32
+ draggable: true,
33
+ selectable: true,
34
+ deletable: false,
35
+ zIndex: 0,
36
+ isConnectable: true,
37
+ positionAbsoluteX: 0,
38
+ positionAbsoluteY: 0,
39
+ data: {
40
+ label: "User Login",
41
+ status: "draft",
42
+ priority: "medium",
43
+ specCount: 3,
44
+ },
45
+ };
46
+
47
+ describe("drill down button visibility", () => {
48
+ it("shows drill down button when onDrillDown is defined and specCount > 0", () => {
49
+ const onDrillDown = vi.fn();
50
+
51
+ render(
52
+ <RequirementNode
53
+ {...baseProps}
54
+ data={{ ...baseProps.data, specCount: 3, onDrillDown }}
55
+ />
56
+ );
57
+
58
+ expect(screen.getByRole("button", { name: /3 specs/i })).toBeInTheDocument();
59
+ });
60
+
61
+ it("does NOT show drill down button when onDrillDown is undefined", () => {
62
+ render(
63
+ <RequirementNode
64
+ {...baseProps}
65
+ data={{ ...baseProps.data, specCount: 3 }}
66
+ />
67
+ );
68
+
69
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
70
+ });
71
+
72
+ it("does NOT show drill down button when specCount is 0", () => {
73
+ const onDrillDown = vi.fn();
74
+
75
+ render(
76
+ <RequirementNode
77
+ {...baseProps}
78
+ data={{ ...baseProps.data, specCount: 0, onDrillDown }}
79
+ />
80
+ );
81
+
82
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
83
+ });
84
+ });
85
+
86
+ describe("drill down button interaction", () => {
87
+ it("calls onDrillDown with node id when button is clicked", () => {
88
+ const onDrillDown = vi.fn();
89
+
90
+ render(
91
+ <RequirementNode
92
+ {...baseProps}
93
+ data={{ ...baseProps.data, specCount: 2, onDrillDown }}
94
+ />
95
+ );
96
+
97
+ fireEvent.click(screen.getByRole("button", { name: /2 specs/i }));
98
+
99
+ expect(onDrillDown).toHaveBeenCalledWith("req-000001");
100
+ });
101
+
102
+ it("does NOT trigger parent onClick when button is clicked", () => {
103
+ const onDrillDown = vi.fn();
104
+
105
+ render(
106
+ <RequirementNode
107
+ {...baseProps}
108
+ data={{ ...baseProps.data, specCount: 2, onDrillDown }}
109
+ />
110
+ );
111
+
112
+ fireEvent.click(screen.getByRole("button", { name: /2 specs/i }));
113
+
114
+ // router.push should not be called - button stopPropagation prevents it
115
+ expect(mockPush).not.toHaveBeenCalled();
116
+ });
117
+ });
118
+
119
+ describe("body click navigation", () => {
120
+ it("calls router.push with requirement path when body is clicked", () => {
121
+ render(<RequirementNode {...baseProps} />);
122
+
123
+ // Click the node ID text area (part of the body, not button)
124
+ fireEvent.click(screen.getByText("req-000001"));
125
+
126
+ expect(mockPush).toHaveBeenCalledWith("/requirements/req-000001");
127
+ });
128
+ });
129
+
130
+ describe("rendering", () => {
131
+ it("renders node id and label", () => {
132
+ render(<RequirementNode {...baseProps} />);
133
+
134
+ expect(screen.getByText("req-000001")).toBeInTheDocument();
135
+ expect(screen.getByText("User Login")).toBeInTheDocument();
136
+ });
137
+
138
+ it("shows singular 'spec' for specCount of 1", () => {
139
+ const onDrillDown = vi.fn();
140
+
141
+ render(
142
+ <RequirementNode
143
+ {...baseProps}
144
+ data={{ ...baseProps.data, specCount: 1, onDrillDown }}
145
+ />
146
+ );
147
+
148
+ expect(screen.getByRole("button", { name: /1 spec$/i })).toBeInTheDocument();
149
+ });
150
+ });
151
+ });
@@ -0,0 +1,140 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach, vi } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { SpecificationNode } from "../../../components/graph/specification-node";
7
+
8
+ // Mock @xyflow/react Handle and Position
9
+ vi.mock("@xyflow/react", () => ({
10
+ Handle: ({ type }: any) => <div data-testid={`handle-${type}`} />,
11
+ Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
12
+ }));
13
+
14
+ const defaultNodeProps = {
15
+ selected: false,
16
+ dragging: false,
17
+ draggable: true,
18
+ selectable: true,
19
+ deletable: false,
20
+ zIndex: 0,
21
+ isConnectable: true,
22
+ positionAbsoluteX: 0,
23
+ positionAbsoluteY: 0,
24
+ };
25
+
26
+ describe("SpecificationNode", () => {
27
+ afterEach(() => {
28
+ cleanup();
29
+ });
30
+
31
+ it("renders spec ID text", () => {
32
+ render(
33
+ <SpecificationNode
34
+ {...defaultNodeProps}
35
+ id="spec-000001"
36
+ data={{ label: "spec-000001", status: "draft" }}
37
+ type="specification"
38
+ />
39
+ );
40
+
41
+ expect(screen.getByText("spec-000001")).toBeInTheDocument();
42
+ });
43
+
44
+ it("renders draft status with blue background class", () => {
45
+ const { container } = render(
46
+ <SpecificationNode
47
+ {...defaultNodeProps}
48
+ id="spec-000001"
49
+ data={{ label: "spec-000001", status: "draft" }}
50
+ type="specification"
51
+ />
52
+ );
53
+
54
+ const nodeDiv = container.querySelector(".bg-blue-200");
55
+ expect(nodeDiv).toBeInTheDocument();
56
+ });
57
+
58
+ it("renders approved status with green background class", () => {
59
+ const { container } = render(
60
+ <SpecificationNode
61
+ {...defaultNodeProps}
62
+ id="spec-000002"
63
+ data={{ label: "spec-000002", status: "approved" }}
64
+ type="specification"
65
+ />
66
+ );
67
+
68
+ const nodeDiv = container.querySelector(".bg-green-200");
69
+ expect(nodeDiv).toBeInTheDocument();
70
+ });
71
+
72
+ it("renders implemented status with emerald background class", () => {
73
+ const { container } = render(
74
+ <SpecificationNode
75
+ {...defaultNodeProps}
76
+ id="spec-000003"
77
+ data={{ label: "spec-000003", status: "implemented" }}
78
+ type="specification"
79
+ />
80
+ );
81
+
82
+ const nodeDiv = container.querySelector(".bg-emerald-300");
83
+ expect(nodeDiv).toBeInTheDocument();
84
+ });
85
+
86
+ it("renders implemented status with emerald background class", () => {
87
+ const { container } = render(
88
+ <SpecificationNode
89
+ {...defaultNodeProps}
90
+ id="spec-000004"
91
+ data={{ label: "spec-000004", status: "implemented" }}
92
+ type="specification"
93
+ />
94
+ );
95
+
96
+ const nodeDiv = container.querySelector(".bg-emerald-300");
97
+ expect(nodeDiv).toBeInTheDocument();
98
+ });
99
+
100
+ it("renders deprecated status with red background class", () => {
101
+ const { container } = render(
102
+ <SpecificationNode
103
+ {...defaultNodeProps}
104
+ id="spec-000005"
105
+ data={{ label: "spec-000005", status: "deprecated" }}
106
+ type="specification"
107
+ />
108
+ );
109
+
110
+ const nodeDiv = container.querySelector(".bg-red-200");
111
+ expect(nodeDiv).toBeInTheDocument();
112
+ });
113
+
114
+ it("renders status badge", () => {
115
+ render(
116
+ <SpecificationNode
117
+ {...defaultNodeProps}
118
+ id="spec-000001"
119
+ data={{ label: "spec-000001", status: "draft" }}
120
+ type="specification"
121
+ />
122
+ );
123
+
124
+ expect(screen.getByText("draft")).toBeInTheDocument();
125
+ });
126
+
127
+ it("renders both target and source handles", () => {
128
+ render(
129
+ <SpecificationNode
130
+ {...defaultNodeProps}
131
+ id="spec-000001"
132
+ data={{ label: "spec-000001", status: "draft" }}
133
+ type="specification"
134
+ />
135
+ );
136
+
137
+ expect(screen.getByTestId("handle-target")).toBeInTheDocument();
138
+ expect(screen.getByTestId("handle-source")).toBeInTheDocument();
139
+ });
140
+ });
@@ -0,0 +1,153 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, vi, afterEach } from "vitest";
4
+ import { render, screen, cleanup } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import userEvent from "@testing-library/user-event";
7
+ import { SpecTabs } from "../../../components/specification/spec-tabs";
8
+
9
+ // Mock child components
10
+ vi.mock("../../../components/specification/tab-design", () => ({
11
+ TabDesign: ({ content }: { content: string | null }) => (
12
+ <div data-testid="tab-design">Design: {content ?? "null"}</div>
13
+ ),
14
+ }));
15
+
16
+ vi.mock("../../../components/specification/tab-research", () => ({
17
+ TabResearch: ({ content }: { content: string | null }) => (
18
+ <div data-testid="tab-research">Research: {content ?? "null"}</div>
19
+ ),
20
+ }));
21
+
22
+ vi.mock("../../../components/specification/tab-coverage", () => ({
23
+ TabCoverage: ({ successCriteria }: { successCriteria: string[] | null }) => (
24
+ <div data-testid="tab-coverage">Coverage: {successCriteria?.length ?? 0} criteria</div>
25
+ ),
26
+ }));
27
+
28
+ vi.mock("../../../components/specification/tab-issues", () => ({
29
+ TabIssues: ({ issues }: { issues: any[] | null }) => (
30
+ <div data-testid="tab-issues">Issues: {issues?.length ?? 0} issues</div>
31
+ ),
32
+ }));
33
+
34
+ vi.mock("../../../components/specification/tab-history", () => ({
35
+ TabHistory: ({ versionHistory }: { versionHistory: any[] }) => (
36
+ <div data-testid="tab-history">History: {versionHistory.length} versions</div>
37
+ ),
38
+ }));
39
+
40
+ vi.mock("../../../components/ui/tabs", () => ({
41
+ Tabs: ({ tabs, activeTab, onTabChange }: any) => (
42
+ <div data-testid="tabs-container">
43
+ {tabs.map((tab: any) => (
44
+ <button
45
+ key={tab.id}
46
+ data-testid={`tab-button-${tab.id}`}
47
+ data-active={activeTab === tab.id}
48
+ onClick={() => onTabChange(tab.id)}
49
+ >
50
+ {tab.label}
51
+ </button>
52
+ ))}
53
+ </div>
54
+ ),
55
+ }));
56
+
57
+ describe("SpecTabs", () => {
58
+ afterEach(() => {
59
+ cleanup();
60
+ // Reset URL hash
61
+ if (typeof window !== "undefined") {
62
+ window.location.hash = "";
63
+ }
64
+ });
65
+
66
+ it("defaults to design tab", () => {
67
+ render(
68
+ <SpecTabs
69
+ design="# Design"
70
+ research="# Research"
71
+ successCriteria={[]}
72
+ issues={[]}
73
+ versionHistory={[]}
74
+ />
75
+ );
76
+
77
+ const designButton = screen.getByTestId("tab-button-design");
78
+ expect(designButton).toHaveAttribute("data-active", "true");
79
+ expect(screen.getByTestId("tab-design")).toBeInTheDocument();
80
+ });
81
+
82
+ it("changes active tab when tab button is clicked", async () => {
83
+ const user = userEvent.setup();
84
+ render(
85
+ <SpecTabs
86
+ design="# Design"
87
+ research="# Research"
88
+ successCriteria={[]}
89
+ issues={[]}
90
+ versionHistory={[]}
91
+ />
92
+ );
93
+
94
+ const researchButton = screen.getByTestId("tab-button-research");
95
+ await user.click(researchButton);
96
+
97
+ expect(researchButton).toHaveAttribute("data-active", "true");
98
+ expect(screen.getByTestId("tab-research")).toBeInTheDocument();
99
+ });
100
+
101
+ it("passes design content to TabDesign", () => {
102
+ render(
103
+ <SpecTabs
104
+ design="# Design content"
105
+ research={null}
106
+ successCriteria={[]}
107
+ issues={[]}
108
+ versionHistory={[]}
109
+ />
110
+ );
111
+
112
+ // Design tab should be active by default
113
+ const designTab = screen.getByTestId("tab-design");
114
+ expect(designTab).toBeInTheDocument();
115
+ expect(designTab).toHaveTextContent("Design: # Design content");
116
+ });
117
+
118
+ it("passes research content to TabResearch", async () => {
119
+ const user = userEvent.setup();
120
+ render(
121
+ <SpecTabs
122
+ design={null}
123
+ research="# Research content"
124
+ successCriteria={[]}
125
+ issues={[]}
126
+ versionHistory={[]}
127
+ />
128
+ );
129
+
130
+ await user.click(screen.getByTestId("tab-button-research"));
131
+
132
+ expect(screen.getByText("Research: # Research content")).toBeInTheDocument();
133
+ });
134
+
135
+ it("handles null content for both tabs", async () => {
136
+ const user = userEvent.setup();
137
+ render(<SpecTabs design={null} research={null} successCriteria={[]} issues={[]} versionHistory={[]} />);
138
+
139
+ // Design tab shown by default
140
+ expect(screen.getByTestId("tab-design")).toHaveTextContent("Design: null");
141
+
142
+ // Switch to research tab
143
+ await user.click(screen.getByTestId("tab-button-research"));
144
+ expect(screen.getByTestId("tab-research")).toHaveTextContent("Research: null");
145
+ });
146
+
147
+ it("renders both Design and Research tab labels", () => {
148
+ render(<SpecTabs design={null} research={null} successCriteria={[]} issues={[]} versionHistory={[]} />);
149
+
150
+ expect(screen.getByTestId("tab-button-design")).toHaveTextContent("Design");
151
+ expect(screen.getByTestId("tab-button-research")).toHaveTextContent("Research");
152
+ });
153
+ });
@@ -0,0 +1,70 @@
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 { TabCoverage } from "../../../components/specification/tab-coverage";
7
+
8
+ describe("TabCoverage", () => {
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ it("renders table with correct number of rows when successCriteria is provided", () => {
14
+ const criteria = [
15
+ "User can login with email and password",
16
+ "System validates input fields",
17
+ "Error messages are displayed on failure",
18
+ ];
19
+
20
+ render(<TabCoverage successCriteria={criteria} />);
21
+
22
+ // Check for table headers
23
+ expect(screen.getByText("#")).toBeInTheDocument();
24
+ expect(screen.getByText("Criteria")).toBeInTheDocument();
25
+ expect(screen.getByText("Status")).toBeInTheDocument();
26
+
27
+ // Check for all criteria rows
28
+ expect(screen.getByText("1")).toBeInTheDocument();
29
+ expect(screen.getByText("User can login with email and password")).toBeInTheDocument();
30
+ expect(screen.getByText("2")).toBeInTheDocument();
31
+ expect(screen.getByText("System validates input fields")).toBeInTheDocument();
32
+ expect(screen.getByText("3")).toBeInTheDocument();
33
+ expect(screen.getByText("Error messages are displayed on failure")).toBeInTheDocument();
34
+ });
35
+
36
+ it("shows empty state when successCriteria is null", () => {
37
+ render(<TabCoverage successCriteria={null} />);
38
+
39
+ expect(screen.getByText("No success criteria available")).toBeInTheDocument();
40
+ expect(screen.queryByText("#")).not.toBeInTheDocument();
41
+ });
42
+
43
+ it("shows empty state when successCriteria is empty array", () => {
44
+ render(<TabCoverage successCriteria={[]} />);
45
+
46
+ expect(screen.getByText("No success criteria available")).toBeInTheDocument();
47
+ expect(screen.queryByText("#")).not.toBeInTheDocument();
48
+ });
49
+
50
+ it("each criterion shows Unchecked status", () => {
51
+ const criteria = ["Criterion 1", "Criterion 2"];
52
+
53
+ render(<TabCoverage successCriteria={criteria} />);
54
+
55
+ const uncheckedStatuses = screen.getAllByText("Unchecked");
56
+ expect(uncheckedStatuses).toHaveLength(2);
57
+ });
58
+
59
+ it("renders striped table rows with even rows having gray background", () => {
60
+ const criteria = ["First", "Second", "Third"];
61
+
62
+ const { container } = render(<TabCoverage successCriteria={criteria} />);
63
+
64
+ const rows = container.querySelectorAll("tbody tr");
65
+ expect(rows).toHaveLength(3);
66
+
67
+ // Check that even rows (index 1) have bg-gray-50
68
+ expect(rows[1].className).toContain("bg-gray-50");
69
+ });
70
+ });