@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.
- package/LICENSE +661 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +7 -0
- package/package.json +59 -0
- package/postcss.config.mjs +7 -0
- package/src/__tests__/components/dashboard/critical-path-display.test.tsx +129 -0
- package/src/__tests__/components/dashboard/progress-bar.test.tsx +87 -0
- package/src/__tests__/components/dashboard/project-health.test.tsx +57 -0
- package/src/__tests__/components/dashboard/warning-alert.test.tsx +75 -0
- package/src/__tests__/components/feedback/feedback-client-view.test.tsx +84 -0
- package/src/__tests__/components/feedback/feedback-filters.test.tsx +51 -0
- package/src/__tests__/components/feedback/feedback-linked-items.test.tsx +131 -0
- package/src/__tests__/components/feedback/feedback-list.test.tsx +49 -0
- package/src/__tests__/components/feedback/feedback-table.test.tsx +165 -0
- package/src/__tests__/components/flags/flag-badge.test.tsx +41 -0
- package/src/__tests__/components/flags/flag-list.test.tsx +51 -0
- package/src/__tests__/components/gantt/gantt-bar.test.tsx +190 -0
- package/src/__tests__/components/gantt/gantt-chart.test.tsx +141 -0
- package/src/__tests__/components/gantt/gantt-header.test.tsx +84 -0
- package/src/__tests__/components/gantt/gantt-legend.test.tsx +52 -0
- package/src/__tests__/components/graph/dag-layout.test.ts +129 -0
- package/src/__tests__/components/graph/dependency-graph.test.tsx +94 -0
- package/src/__tests__/components/graph/drilldown-breadcrumb.test.tsx +70 -0
- package/src/__tests__/components/graph/drilldown-graph.test.tsx +108 -0
- package/src/__tests__/components/graph/edge-styles.test.ts +27 -0
- package/src/__tests__/components/graph/graph-page-client.test.tsx +124 -0
- package/src/__tests__/components/graph/issue-node.test.tsx +173 -0
- package/src/__tests__/components/graph/requirement-node.test.tsx +151 -0
- package/src/__tests__/components/graph/specification-node.test.tsx +140 -0
- package/src/__tests__/components/specification/spec-tabs.test.tsx +153 -0
- package/src/__tests__/components/specification/tab-coverage.test.tsx +70 -0
- package/src/__tests__/components/specification/tab-design.test.tsx +42 -0
- package/src/__tests__/components/specification/tab-history.test.tsx +118 -0
- package/src/__tests__/components/specification/tab-issues.test.tsx +126 -0
- package/src/__tests__/components/specification/tab-research.test.tsx +42 -0
- package/src/__tests__/lib/dashboard-data.test.ts +334 -0
- package/src/__tests__/lib/drilldown-graph-data.test.ts +267 -0
- package/src/__tests__/lib/gantt-data.test.ts +299 -0
- package/src/__tests__/lib/graph-data.test.ts +309 -0
- package/src/__tests__/lib/local-feedback-repository.test.ts +74 -0
- package/src/__tests__/lib/local-specification-repository.test.ts +194 -0
- package/src/__tests__/lib/reqord-root.test.ts +31 -0
- package/src/__tests__/lib/specification-file.test.ts +63 -0
- package/src/__tests__/lib/tasks-data.test.ts +104 -0
- package/src/app/dashboard/loading.tsx +21 -0
- package/src/app/dashboard/page.tsx +50 -0
- package/src/app/error.tsx +22 -0
- package/src/app/feedback/loading.tsx +13 -0
- package/src/app/feedback/page.tsx +48 -0
- package/src/app/globals.css +2 -0
- package/src/app/graph/page.tsx +32 -0
- package/src/app/layout.tsx +25 -0
- package/src/app/page.tsx +5 -0
- package/src/app/requirements/[id]/edit/page.tsx +40 -0
- package/src/app/requirements/[id]/loading.tsx +14 -0
- package/src/app/requirements/[id]/not-found.tsx +18 -0
- package/src/app/requirements/[id]/page.tsx +43 -0
- package/src/app/requirements/loading.tsx +13 -0
- package/src/app/requirements/new/page.tsx +14 -0
- package/src/app/requirements/page.tsx +35 -0
- package/src/app/specifications/[id]/loading.tsx +14 -0
- package/src/app/specifications/[id]/not-found.tsx +18 -0
- package/src/app/specifications/[id]/page.tsx +52 -0
- package/src/app/specifications/loading.tsx +13 -0
- package/src/app/specifications/page.tsx +42 -0
- package/src/components/dashboard/critical-path-display.tsx +76 -0
- package/src/components/dashboard/progress-bar.tsx +45 -0
- package/src/components/dashboard/progress-section.tsx +57 -0
- package/src/components/dashboard/project-health.tsx +35 -0
- package/src/components/dashboard/status-card.tsx +27 -0
- package/src/components/dashboard/status-cards.tsx +28 -0
- package/src/components/dashboard/warning-alert.tsx +33 -0
- package/src/components/dashboard/warning-alerts.tsx +24 -0
- package/src/components/feedback/feedback-badge.tsx +48 -0
- package/src/components/feedback/feedback-client-view.tsx +38 -0
- package/src/components/feedback/feedback-filters.tsx +86 -0
- package/src/components/feedback/feedback-linked-items.tsx +93 -0
- package/src/components/feedback/feedback-list.tsx +40 -0
- package/src/components/feedback/feedback-table.tsx +115 -0
- package/src/components/gantt/gantt-bar.tsx +65 -0
- package/src/components/gantt/gantt-chart.tsx +88 -0
- package/src/components/gantt/gantt-constants.ts +15 -0
- package/src/components/gantt/gantt-critical-path.tsx +38 -0
- package/src/components/gantt/gantt-group.tsx +25 -0
- package/src/components/gantt/gantt-header.tsx +47 -0
- package/src/components/gantt/gantt-legend.tsx +26 -0
- package/src/components/graph/dag-layout.ts +131 -0
- package/src/components/graph/dependency-graph.tsx +88 -0
- package/src/components/graph/drilldown-breadcrumb.tsx +35 -0
- package/src/components/graph/drilldown-graph.tsx +45 -0
- package/src/components/graph/edge-styles.ts +16 -0
- package/src/components/graph/graph-loader.tsx +25 -0
- package/src/components/graph/graph-page-client.tsx +98 -0
- package/src/components/graph/issue-node.tsx +46 -0
- package/src/components/graph/multi-level-graph.tsx +91 -0
- package/src/components/graph/requirement-node.tsx +69 -0
- package/src/components/graph/specification-node.tsx +39 -0
- package/src/components/requirement/delete-button.tsx +46 -0
- package/src/components/requirement/dependency-editor.tsx +79 -0
- package/src/components/requirement/markdown-editor.tsx +47 -0
- package/src/components/requirement/markdown-renderer.tsx +12 -0
- package/src/components/requirement/requirement-detail.tsx +228 -0
- package/src/components/requirement/requirement-form.tsx +390 -0
- package/src/components/requirement/requirement-table.tsx +203 -0
- package/src/components/requirement/requirement-tabs.tsx +65 -0
- package/src/components/requirement/success-criteria-editor.tsx +53 -0
- package/src/components/specification/spec-detail.tsx +103 -0
- package/src/components/specification/spec-tabs.tsx +66 -0
- package/src/components/specification/specification-table.tsx +193 -0
- package/src/components/specification/tab-coverage.tsx +52 -0
- package/src/components/specification/tab-design.tsx +16 -0
- package/src/components/specification/tab-history.tsx +61 -0
- package/src/components/specification/tab-issues.tsx +111 -0
- package/src/components/specification/tab-research.tsx +16 -0
- package/src/components/ui/badge.tsx +64 -0
- package/src/components/ui/nav.tsx +49 -0
- package/src/components/ui/tabs.tsx +39 -0
- package/src/lib/actions.ts +222 -0
- package/src/lib/dashboard-data.ts +224 -0
- package/src/lib/data.ts +21 -0
- package/src/lib/drilldown-graph-data.ts +98 -0
- package/src/lib/feedback-data.ts +33 -0
- package/src/lib/feedback-repository.ts +6 -0
- package/src/lib/file-system.ts +167 -0
- package/src/lib/gantt-data.ts +168 -0
- package/src/lib/get-repository.ts +43 -0
- package/src/lib/graph-data.ts +161 -0
- package/src/lib/id-generator.ts +23 -0
- package/src/lib/local-feedback-repository.ts +36 -0
- package/src/lib/local-repository.ts +78 -0
- package/src/lib/local-specification-repository.ts +61 -0
- package/src/lib/repository.ts +11 -0
- package/src/lib/reqord-root.ts +33 -0
- package/src/lib/specification-data.ts +28 -0
- package/src/lib/specification-file.ts +12 -0
- package/src/lib/specification-repository.ts +8 -0
- package/src/lib/tasks-data.ts +32 -0
- 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
|
+
});
|