@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,42 @@
|
|
|
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 { TabDesign } from "../../../components/specification/tab-design";
|
|
7
|
+
|
|
8
|
+
// Mock MarkdownRenderer to avoid react-markdown SSR issues
|
|
9
|
+
vi.mock("../../../components/requirement/markdown-renderer", () => ({
|
|
10
|
+
MarkdownRenderer: ({ content }: { content: string }) => (
|
|
11
|
+
<div data-testid="markdown-renderer">{content}</div>
|
|
12
|
+
),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("TabDesign", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders markdown content when content is provided", () => {
|
|
21
|
+
render(<TabDesign content="# Design\n\nContent here" />);
|
|
22
|
+
|
|
23
|
+
const renderer = screen.getByTestId("markdown-renderer");
|
|
24
|
+
expect(renderer).toBeInTheDocument();
|
|
25
|
+
expect(renderer).toHaveTextContent("# Design");
|
|
26
|
+
expect(renderer).toHaveTextContent("Content here");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("shows empty state message when content is null", () => {
|
|
30
|
+
render(<TabDesign content={null} />);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText("Design document not available")).toBeInTheDocument();
|
|
33
|
+
expect(screen.queryByTestId("markdown-renderer")).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders empty content string as markdown", () => {
|
|
37
|
+
render(<TabDesign content="" />);
|
|
38
|
+
|
|
39
|
+
const renderer = screen.getByTestId("markdown-renderer");
|
|
40
|
+
expect(renderer).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
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 { VersionHistoryEntry } from "@reqord/shared";
|
|
7
|
+
import { TabHistory } from "../../../components/specification/tab-history";
|
|
8
|
+
|
|
9
|
+
describe("TabHistory", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const mockHistory: VersionHistoryEntry[] = [
|
|
15
|
+
{
|
|
16
|
+
version: "2.1.0",
|
|
17
|
+
status: "approved",
|
|
18
|
+
changedAt: "2026-02-10T10:00:00Z",
|
|
19
|
+
summary: "Added new feature X",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
version: "2.0.0",
|
|
23
|
+
status: "implemented",
|
|
24
|
+
changedAt: "2026-02-01T10:00:00Z",
|
|
25
|
+
summary: "Implemented core functionality",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
version: "1.0.0",
|
|
29
|
+
status: "draft",
|
|
30
|
+
changedAt: "2026-01-15T10:00:00Z",
|
|
31
|
+
summary: "Initial draft specification",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
it("renders timeline with all version history entries", () => {
|
|
36
|
+
render(<TabHistory versionHistory={mockHistory} />);
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText("2.1.0")).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText("2.0.0")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("1.0.0")).toBeInTheDocument();
|
|
41
|
+
|
|
42
|
+
expect(screen.getByText("Added new feature X")).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText("Implemented core functionality")).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText("Initial draft specification")).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("shows empty state when versionHistory is empty array", () => {
|
|
48
|
+
render(<TabHistory versionHistory={[]} />);
|
|
49
|
+
|
|
50
|
+
expect(screen.getByText("No version history yet")).toBeInTheDocument();
|
|
51
|
+
expect(screen.queryByText("2.1.0")).not.toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("latest version (first entry) is highlighted with ring", () => {
|
|
55
|
+
const { container } = render(<TabHistory versionHistory={mockHistory} />);
|
|
56
|
+
|
|
57
|
+
const dots = container.querySelectorAll(".absolute.-left-\\[33px\\]");
|
|
58
|
+
expect(dots.length).toBeGreaterThan(0);
|
|
59
|
+
|
|
60
|
+
// First dot should have ring-2 and ring-blue-500
|
|
61
|
+
const firstDot = dots[0];
|
|
62
|
+
expect(firstDot.className).toContain("ring-2");
|
|
63
|
+
expect(firstDot.className).toContain("ring-blue-500");
|
|
64
|
+
expect(firstDot.className).toContain("bg-blue-500");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("older versions have gray dots without ring", () => {
|
|
68
|
+
const { container } = render(<TabHistory versionHistory={mockHistory} />);
|
|
69
|
+
|
|
70
|
+
const dots = container.querySelectorAll(".absolute.-left-\\[33px\\]");
|
|
71
|
+
|
|
72
|
+
// Second and third dots should have bg-gray-400 and no ring
|
|
73
|
+
if (dots.length > 1) {
|
|
74
|
+
const secondDot = dots[1];
|
|
75
|
+
expect(secondDot.className).toContain("bg-gray-400");
|
|
76
|
+
expect(secondDot.className).not.toContain("ring-2");
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("renders timeline with left border", () => {
|
|
81
|
+
const { container } = render(<TabHistory versionHistory={mockHistory} />);
|
|
82
|
+
|
|
83
|
+
const timeline = container.querySelector(".border-l-2");
|
|
84
|
+
expect(timeline).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("displays dates in localized format", () => {
|
|
88
|
+
const singleEntry: VersionHistoryEntry[] = [
|
|
89
|
+
{
|
|
90
|
+
version: "1.0.0",
|
|
91
|
+
status: "draft",
|
|
92
|
+
changedAt: "2026-02-10T10:00:00Z",
|
|
93
|
+
summary: "Initial version",
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
render(<TabHistory versionHistory={singleEntry} />);
|
|
98
|
+
|
|
99
|
+
// Date should be rendered (format depends on locale, so just check it's there)
|
|
100
|
+
const dateElement = screen.getByText(/2026/);
|
|
101
|
+
expect(dateElement).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("displays status badges for each entry", () => {
|
|
105
|
+
render(<TabHistory versionHistory={mockHistory} />);
|
|
106
|
+
|
|
107
|
+
expect(screen.getByText("Approved")).toBeInTheDocument();
|
|
108
|
+
expect(screen.getByText("Implemented")).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText("Draft")).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("renders dots with correct size", () => {
|
|
113
|
+
const { container } = render(<TabHistory versionHistory={mockHistory} />);
|
|
114
|
+
|
|
115
|
+
const dots = container.querySelectorAll(".w-3.h-3");
|
|
116
|
+
expect(dots.length).toBe(mockHistory.length);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { IssueItem } from "../../../components/specification/tab-issues";
|
|
7
|
+
import { TabIssues } from "../../../components/specification/tab-issues";
|
|
8
|
+
|
|
9
|
+
describe("TabIssues", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
cleanup();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const mockIssues: IssueItem[] = [
|
|
15
|
+
{
|
|
16
|
+
number: 123,
|
|
17
|
+
title: "Implement user authentication",
|
|
18
|
+
url: "https://github.com/user/repo/issues/123",
|
|
19
|
+
priority: "P0",
|
|
20
|
+
status: "open",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
number: 124,
|
|
24
|
+
title: "Add validation for form inputs",
|
|
25
|
+
url: "https://github.com/user/repo/issues/124",
|
|
26
|
+
priority: "P1",
|
|
27
|
+
status: "in_progress",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
number: 125,
|
|
31
|
+
title: "Update documentation",
|
|
32
|
+
url: "https://github.com/user/repo/issues/125",
|
|
33
|
+
priority: "P2",
|
|
34
|
+
status: "closed",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
it("renders table with correct number of rows when issues are provided", () => {
|
|
39
|
+
render(<TabIssues issues={mockIssues} />);
|
|
40
|
+
|
|
41
|
+
// Check for table headers
|
|
42
|
+
expect(screen.getByText("#")).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByText("Priority")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("Status")).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText("Link")).toBeInTheDocument();
|
|
47
|
+
|
|
48
|
+
// Check for all issue titles
|
|
49
|
+
expect(screen.getByText("Implement user authentication")).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText("Add validation for form inputs")).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText("Update documentation")).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("shows empty state when issues is null", () => {
|
|
55
|
+
render(<TabIssues issues={null} />);
|
|
56
|
+
|
|
57
|
+
expect(screen.getByText("No issues generated yet")).toBeInTheDocument();
|
|
58
|
+
expect(screen.queryByText("#")).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("shows empty state when issues is empty array", () => {
|
|
62
|
+
render(<TabIssues issues={[]} />);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText("No issues generated yet")).toBeInTheDocument();
|
|
65
|
+
expect(screen.queryByText("#")).not.toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("each row has a GitHub URL link", () => {
|
|
69
|
+
render(<TabIssues issues={mockIssues} />);
|
|
70
|
+
|
|
71
|
+
const links = screen.getAllByText("View");
|
|
72
|
+
expect(links).toHaveLength(3);
|
|
73
|
+
|
|
74
|
+
expect(links[0]).toHaveAttribute("href", "https://github.com/user/repo/issues/123");
|
|
75
|
+
expect(links[0]).toHaveAttribute("target", "_blank");
|
|
76
|
+
expect(links[0]).toHaveAttribute("rel", "noopener noreferrer");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("priority badges have correct colors", () => {
|
|
80
|
+
render(<TabIssues issues={mockIssues} />);
|
|
81
|
+
|
|
82
|
+
const p0Badge = screen.getByText("P0");
|
|
83
|
+
const p1Badge = screen.getByText("P1");
|
|
84
|
+
const p2Badge = screen.getByText("P2");
|
|
85
|
+
|
|
86
|
+
expect(p0Badge.className).toContain("bg-red-100");
|
|
87
|
+
expect(p0Badge.className).toContain("text-red-800");
|
|
88
|
+
expect(p1Badge.className).toContain("bg-orange-100");
|
|
89
|
+
expect(p1Badge.className).toContain("text-orange-800");
|
|
90
|
+
expect(p2Badge.className).toContain("bg-yellow-100");
|
|
91
|
+
expect(p2Badge.className).toContain("text-yellow-800");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("status badges have correct colors", () => {
|
|
95
|
+
render(<TabIssues issues={mockIssues} />);
|
|
96
|
+
|
|
97
|
+
const openBadge = screen.getByText("open");
|
|
98
|
+
const inProgressBadge = screen.getByText("in_progress");
|
|
99
|
+
const closedBadge = screen.getByText("closed");
|
|
100
|
+
|
|
101
|
+
expect(openBadge.className).toContain("bg-gray-100");
|
|
102
|
+
expect(openBadge.className).toContain("text-gray-800");
|
|
103
|
+
expect(inProgressBadge.className).toContain("bg-blue-100");
|
|
104
|
+
expect(inProgressBadge.className).toContain("text-blue-800");
|
|
105
|
+
expect(closedBadge.className).toContain("bg-green-100");
|
|
106
|
+
expect(closedBadge.className).toContain("text-green-800");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders P3 priority with gray color", () => {
|
|
110
|
+
const p3Issue: IssueItem[] = [
|
|
111
|
+
{
|
|
112
|
+
number: 126,
|
|
113
|
+
title: "Low priority task",
|
|
114
|
+
url: "https://github.com/user/repo/issues/126",
|
|
115
|
+
priority: "P3",
|
|
116
|
+
status: "open",
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
render(<TabIssues issues={p3Issue} />);
|
|
121
|
+
|
|
122
|
+
const p3Badge = screen.getByText("P3");
|
|
123
|
+
expect(p3Badge.className).toContain("bg-gray-100");
|
|
124
|
+
expect(p3Badge.className).toContain("text-gray-800");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { TabResearch } from "../../../components/specification/tab-research";
|
|
7
|
+
|
|
8
|
+
// Mock MarkdownRenderer to avoid react-markdown SSR issues
|
|
9
|
+
vi.mock("../../../components/requirement/markdown-renderer", () => ({
|
|
10
|
+
MarkdownRenderer: ({ content }: { content: string }) => (
|
|
11
|
+
<div data-testid="markdown-renderer">{content}</div>
|
|
12
|
+
),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("TabResearch", () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
cleanup();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders markdown content when content is provided", () => {
|
|
21
|
+
render(<TabResearch content="# Research\n\nFindings" />);
|
|
22
|
+
|
|
23
|
+
const renderer = screen.getByTestId("markdown-renderer");
|
|
24
|
+
expect(renderer).toBeInTheDocument();
|
|
25
|
+
expect(renderer).toHaveTextContent("# Research");
|
|
26
|
+
expect(renderer).toHaveTextContent("Findings");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("shows empty state message when content is null", () => {
|
|
30
|
+
render(<TabResearch content={null} />);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText("Research document not available")).toBeInTheDocument();
|
|
33
|
+
expect(screen.queryByTestId("markdown-renderer")).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders empty content string as markdown", () => {
|
|
37
|
+
render(<TabResearch content="" />);
|
|
38
|
+
|
|
39
|
+
const renderer = screen.getByTestId("markdown-renderer");
|
|
40
|
+
expect(renderer).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { Requirement, Specification, TaskEntry, FeedbackEntry } from "@reqord/shared";
|
|
3
|
+
|
|
4
|
+
vi.mock("../../lib/data.js", () => ({
|
|
5
|
+
getAllRequirements: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("../../lib/specification-data.js", () => ({
|
|
9
|
+
getAllSpecifications: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../../lib/tasks-data.js", () => ({
|
|
13
|
+
loadTasksYaml: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("../../lib/feedback-data.js", () => ({
|
|
17
|
+
getAllFeedbacks: vi.fn().mockResolvedValue([]),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const makeRequirement = (
|
|
21
|
+
id: string,
|
|
22
|
+
status: string,
|
|
23
|
+
overrides: Partial<Requirement> = {},
|
|
24
|
+
): Requirement => ({
|
|
25
|
+
id,
|
|
26
|
+
title: `Req ${id}`,
|
|
27
|
+
status: status as Requirement["status"],
|
|
28
|
+
priority: "high",
|
|
29
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
30
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
versionHistory: [],
|
|
33
|
+
files: {
|
|
34
|
+
description: `requirements/${id}/description.md`,
|
|
35
|
+
supplementary: [],
|
|
36
|
+
},
|
|
37
|
+
successCriteria: [],
|
|
38
|
+
format: { type: "free-form" },
|
|
39
|
+
dependencies: { blockedBy: [], blocks: [], relatedTo: [] },
|
|
40
|
+
...overrides,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const makeSpecification = (
|
|
44
|
+
id: string,
|
|
45
|
+
requirementId: string,
|
|
46
|
+
status: string,
|
|
47
|
+
overrides: Partial<Specification> = {},
|
|
48
|
+
): Specification => ({
|
|
49
|
+
id,
|
|
50
|
+
requirementId,
|
|
51
|
+
status: status as Specification["status"],
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
54
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
55
|
+
versionHistory: [],
|
|
56
|
+
files: {
|
|
57
|
+
design: `specifications/${id}/design.md`,
|
|
58
|
+
supplementary: [],
|
|
59
|
+
},
|
|
60
|
+
...overrides,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const makeTaskEntry = (
|
|
64
|
+
number: number,
|
|
65
|
+
status: "open" | "closed",
|
|
66
|
+
specIds: string[] = [],
|
|
67
|
+
estimatedHours = 4,
|
|
68
|
+
): TaskEntry => ({
|
|
69
|
+
number,
|
|
70
|
+
title: `Issue ${number}`,
|
|
71
|
+
url: `https://github.com/owner/repo/issues/${number}`,
|
|
72
|
+
linkedTo: { specifications: specIds },
|
|
73
|
+
priority: "P1",
|
|
74
|
+
status,
|
|
75
|
+
estimatedHours,
|
|
76
|
+
syncedAt: "2026-01-01T00:00:00Z",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("dashboard-data", () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.resetModules();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getDashboardData", () => {
|
|
85
|
+
it("aggregates requirements, specifications, and issues from tasks.yaml correctly", async () => {
|
|
86
|
+
const { getAllRequirements } = await import("../../lib/data.js");
|
|
87
|
+
const { getAllSpecifications } = await import("../../lib/specification-data.js");
|
|
88
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
89
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
90
|
+
const { getDashboardData } = await import("../../lib/dashboard-data.js");
|
|
91
|
+
|
|
92
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
93
|
+
vi.mocked(getAllRequirements).mockResolvedValue([
|
|
94
|
+
makeRequirement("req-000001", "approved"),
|
|
95
|
+
makeRequirement("req-000002", "implemented"),
|
|
96
|
+
makeRequirement("req-000003", "draft"),
|
|
97
|
+
]);
|
|
98
|
+
vi.mocked(getAllSpecifications).mockResolvedValue([
|
|
99
|
+
makeSpecification("spec-000001", "req-000001", "approved"),
|
|
100
|
+
makeSpecification("spec-000002", "req-000002", "draft"),
|
|
101
|
+
]);
|
|
102
|
+
vi.mocked(loadTasksYaml).mockResolvedValue({
|
|
103
|
+
title: "Tasks",
|
|
104
|
+
tasks: [
|
|
105
|
+
makeTaskEntry(1, "closed", ["spec-000001"]),
|
|
106
|
+
makeTaskEntry(2, "open", ["spec-000001"]),
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const result = await getDashboardData();
|
|
111
|
+
|
|
112
|
+
expect(result.requirements.total).toBe(3);
|
|
113
|
+
expect(result.requirements.breakdown).toEqual({ approved: 1, implemented: 1, draft: 1 });
|
|
114
|
+
expect(result.requirements.approvalRate).toBeCloseTo(0.6667, 3);
|
|
115
|
+
expect(result.specifications.total).toBe(2);
|
|
116
|
+
expect(result.specifications.breakdown).toEqual({ approved: 1, draft: 1 });
|
|
117
|
+
expect(result.specifications.approvalRate).toBe(0.5);
|
|
118
|
+
expect(result.issues.total).toBe(2);
|
|
119
|
+
expect(result.issues.completed).toBe(1);
|
|
120
|
+
expect(result.issues.completionRate).toBe(0.5);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("handles zero requirements correctly", async () => {
|
|
124
|
+
const { getAllRequirements } = await import("../../lib/data.js");
|
|
125
|
+
const { getAllSpecifications } = await import("../../lib/specification-data.js");
|
|
126
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
127
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
128
|
+
const { getDashboardData } = await import("../../lib/dashboard-data.js");
|
|
129
|
+
|
|
130
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
131
|
+
vi.mocked(getAllRequirements).mockResolvedValue([]);
|
|
132
|
+
vi.mocked(getAllSpecifications).mockResolvedValue([]);
|
|
133
|
+
vi.mocked(loadTasksYaml).mockResolvedValue({ title: "Tasks", tasks: [] });
|
|
134
|
+
|
|
135
|
+
const result = await getDashboardData();
|
|
136
|
+
|
|
137
|
+
expect(result.requirements.total).toBe(0);
|
|
138
|
+
expect(result.requirements.approvalRate).toBe(0);
|
|
139
|
+
expect(result.specifications.total).toBe(0);
|
|
140
|
+
expect(result.specifications.approvalRate).toBe(0);
|
|
141
|
+
expect(result.issues.total).toBe(0);
|
|
142
|
+
expect(result.issues.completionRate).toBe(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("handles tasks.yaml with no tasks (empty tasks array)", async () => {
|
|
146
|
+
const { getAllRequirements } = await import("../../lib/data.js");
|
|
147
|
+
const { getAllSpecifications } = await import("../../lib/specification-data.js");
|
|
148
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
149
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
150
|
+
const { getDashboardData } = await import("../../lib/dashboard-data.js");
|
|
151
|
+
|
|
152
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
153
|
+
vi.mocked(getAllRequirements).mockResolvedValue([makeRequirement("req-000001", "approved")]);
|
|
154
|
+
vi.mocked(getAllSpecifications).mockResolvedValue([makeSpecification("spec-000001", "req-000001", "approved")]);
|
|
155
|
+
vi.mocked(loadTasksYaml).mockResolvedValue({ title: "Tasks", tasks: [] });
|
|
156
|
+
|
|
157
|
+
const result = await getDashboardData();
|
|
158
|
+
|
|
159
|
+
expect(result.issues.total).toBe(0);
|
|
160
|
+
expect(result.issues.completed).toBe(0);
|
|
161
|
+
expect(result.issues.completionRate).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("calculates health score correctly", async () => {
|
|
165
|
+
const { getAllRequirements } = await import("../../lib/data.js");
|
|
166
|
+
const { getAllSpecifications } = await import("../../lib/specification-data.js");
|
|
167
|
+
const { loadTasksYaml } = await import("../../lib/tasks-data.js");
|
|
168
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
169
|
+
const { getDashboardData } = await import("../../lib/dashboard-data.js");
|
|
170
|
+
|
|
171
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
172
|
+
vi.mocked(getAllRequirements).mockResolvedValue([makeRequirement("req-000001", "approved")]);
|
|
173
|
+
vi.mocked(getAllSpecifications).mockResolvedValue([
|
|
174
|
+
makeSpecification("spec-000001", "req-000001", "approved"),
|
|
175
|
+
makeSpecification("spec-000002", "req-000001", "draft"),
|
|
176
|
+
]);
|
|
177
|
+
vi.mocked(loadTasksYaml).mockResolvedValue({
|
|
178
|
+
title: "Tasks",
|
|
179
|
+
tasks: [
|
|
180
|
+
makeTaskEntry(1, "open", ["spec-000001"]),
|
|
181
|
+
makeTaskEntry(2, "open", ["spec-000001"]),
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const result = await getDashboardData();
|
|
186
|
+
|
|
187
|
+
// Health = 1.0 * 40 + 0.5 * 30 + 0.0 * 30 = 55
|
|
188
|
+
expect(result.healthScore).toBe(55);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("detectWarnings", () => {
|
|
193
|
+
it("detects missing specification warnings", async () => {
|
|
194
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
195
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
196
|
+
const { detectWarnings } = await import("../../lib/dashboard-data.js");
|
|
197
|
+
const warnings = await detectWarnings([makeRequirement("req-000001", "approved")], []);
|
|
198
|
+
expect(warnings).toHaveLength(1);
|
|
199
|
+
expect(warnings[0]).toEqual({
|
|
200
|
+
type: "missing_specification",
|
|
201
|
+
message: "Requirement req-000001 has no specification",
|
|
202
|
+
severity: "warning",
|
|
203
|
+
relatedId: "req-000001",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("does not warn for draft requirements without specifications", async () => {
|
|
208
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
209
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
210
|
+
const { detectWarnings } = await import("../../lib/dashboard-data.js");
|
|
211
|
+
const warnings = await detectWarnings([makeRequirement("req-000001", "draft")], []);
|
|
212
|
+
expect(warnings).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("detects unapproved dependency warnings", async () => {
|
|
216
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
217
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
218
|
+
const { detectWarnings } = await import("../../lib/dashboard-data.js");
|
|
219
|
+
const warnings = await detectWarnings(
|
|
220
|
+
[
|
|
221
|
+
makeRequirement("req-000001", "draft"),
|
|
222
|
+
makeRequirement("req-000002", "approved", {
|
|
223
|
+
dependencies: { blockedBy: ["req-000001"], blocks: [], relatedTo: [] },
|
|
224
|
+
}),
|
|
225
|
+
],
|
|
226
|
+
[makeSpecification("spec-000002", "req-000002", "approved")]
|
|
227
|
+
);
|
|
228
|
+
expect(warnings).toHaveLength(1);
|
|
229
|
+
expect(warnings[0]).toEqual({
|
|
230
|
+
type: "unapproved_dependency",
|
|
231
|
+
message: "Requirement req-000002 is blocked by unapproved requirement req-000001",
|
|
232
|
+
severity: "warning",
|
|
233
|
+
relatedId: "req-000002",
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("detects design verification error warnings from critical unresolved feedback", async () => {
|
|
238
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
239
|
+
const criticalFeedback: FeedbackEntry = {
|
|
240
|
+
githubIssue: 123,
|
|
241
|
+
type: "bug",
|
|
242
|
+
severity: "critical",
|
|
243
|
+
linkedTo: {
|
|
244
|
+
requirements: [],
|
|
245
|
+
createdRequirements: [],
|
|
246
|
+
specifications: ["spec-000001"],
|
|
247
|
+
createdSpecifications: [],
|
|
248
|
+
},
|
|
249
|
+
syncedAt: "2026-01-02T00:00:00Z",
|
|
250
|
+
status: "open",
|
|
251
|
+
};
|
|
252
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([criticalFeedback]);
|
|
253
|
+
const { detectWarnings } = await import("../../lib/dashboard-data.js");
|
|
254
|
+
const warnings = await detectWarnings(
|
|
255
|
+
[],
|
|
256
|
+
[makeSpecification("spec-000001", "req-000001", "draft")]
|
|
257
|
+
);
|
|
258
|
+
expect(warnings).toHaveLength(1);
|
|
259
|
+
expect(warnings[0]).toEqual({
|
|
260
|
+
type: "design_verification_error",
|
|
261
|
+
message: "Specification spec-000001 has critical/high unresolved feedback requiring attention",
|
|
262
|
+
severity: "error",
|
|
263
|
+
relatedId: "spec-000001",
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("returns empty array when no warnings detected", async () => {
|
|
268
|
+
const { getAllFeedbacks } = await import("../../lib/feedback-data.js");
|
|
269
|
+
vi.mocked(getAllFeedbacks).mockResolvedValue([]);
|
|
270
|
+
const { detectWarnings } = await import("../../lib/dashboard-data.js");
|
|
271
|
+
const warnings = await detectWarnings(
|
|
272
|
+
[makeRequirement("req-000001", "approved")],
|
|
273
|
+
[makeSpecification("spec-000001", "req-000001", "approved")]
|
|
274
|
+
);
|
|
275
|
+
expect(warnings).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("groupByStatus", () => {
|
|
280
|
+
it("counts items by status correctly", async () => {
|
|
281
|
+
const { groupByStatus } = await import("../../lib/dashboard-data.js");
|
|
282
|
+
const result = groupByStatus([
|
|
283
|
+
{ status: "draft" }, { status: "approved" }, { status: "draft" },
|
|
284
|
+
{ status: "implemented" }, { status: "approved" }, { status: "approved" },
|
|
285
|
+
]);
|
|
286
|
+
expect(result).toEqual({ draft: 2, approved: 3, implemented: 1 });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("returns empty object for empty array", async () => {
|
|
290
|
+
const { groupByStatus } = await import("../../lib/dashboard-data.js");
|
|
291
|
+
expect(groupByStatus([])).toEqual({});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("extractCriticalPath", () => {
|
|
296
|
+
it("extracts tasks from tasks.yaml entries", async () => {
|
|
297
|
+
const { extractCriticalPath } = await import("../../lib/dashboard-data.js");
|
|
298
|
+
const tasks: TaskEntry[] = [
|
|
299
|
+
makeTaskEntry(1, "open", ["spec-000001"], 10),
|
|
300
|
+
makeTaskEntry(2, "closed", ["spec-000001"], 10),
|
|
301
|
+
makeTaskEntry(3, "open", ["spec-000002"], 5),
|
|
302
|
+
];
|
|
303
|
+
const result = extractCriticalPath(tasks);
|
|
304
|
+
expect(result).toHaveLength(3);
|
|
305
|
+
expect(result![0]).toEqual({
|
|
306
|
+
issueNumber: 1, title: "Issue 1",
|
|
307
|
+
url: "https://github.com/owner/repo/issues/1",
|
|
308
|
+
priority: "P1", status: "open", estimatedHours: 10, specId: "spec-000001",
|
|
309
|
+
});
|
|
310
|
+
expect(result![1]).toEqual({
|
|
311
|
+
issueNumber: 2, title: "Issue 2",
|
|
312
|
+
url: "https://github.com/owner/repo/issues/2",
|
|
313
|
+
priority: "P1", status: "closed", estimatedHours: 10, specId: "spec-000001",
|
|
314
|
+
});
|
|
315
|
+
expect(result![2]).toEqual({
|
|
316
|
+
issueNumber: 3, title: "Issue 3",
|
|
317
|
+
url: "https://github.com/owner/repo/issues/3",
|
|
318
|
+
priority: "P1", status: "open", estimatedHours: 5, specId: "spec-000002",
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("uses empty string for specId when task has no linked specifications", async () => {
|
|
323
|
+
const { extractCriticalPath } = await import("../../lib/dashboard-data.js");
|
|
324
|
+
const result = extractCriticalPath([makeTaskEntry(1, "open", [], 4)]);
|
|
325
|
+
expect(result).toHaveLength(1);
|
|
326
|
+
expect(result![0].specId).toBe("");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("returns null when no tasks exist", async () => {
|
|
330
|
+
const { extractCriticalPath } = await import("../../lib/dashboard-data.js");
|
|
331
|
+
expect(extractCriticalPath([])).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|