@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
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@reqord/web",
3
+ "version": "0.1.0",
4
+ "description": "Web dashboard for Reqord",
5
+ "license": "AGPL-3.0-or-later",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/kicchann/reqord.git",
9
+ "directory": "packages/web"
10
+ },
11
+ "engines": {
12
+ "node": ">=20.0.0"
13
+ },
14
+ "keywords": [
15
+ "reqord",
16
+ "requirements",
17
+ "dashboard",
18
+ "next.js"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "type": "module",
24
+ "files": [
25
+ "src",
26
+ "next.config.ts",
27
+ "postcss.config.mjs",
28
+ "tsconfig.json",
29
+ "next-env.d.ts"
30
+ ],
31
+ "dependencies": {
32
+ "@tailwindcss/typography": "^0.5.19",
33
+ "@xyflow/react": "^12.6.0",
34
+ "js-yaml": "^4.1.1",
35
+ "next": "^15.3.0",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "react-markdown": "^10.1.0",
39
+ "remark-gfm": "^4.0.1",
40
+ "@reqord/shared": "0.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@tailwindcss/postcss": "^4.1.0",
44
+ "@testing-library/jest-dom": "^6.9.1",
45
+ "@testing-library/react": "^16.3.2",
46
+ "@types/js-yaml": "^4.0.9",
47
+ "@types/react": "^19.0.0",
48
+ "@types/react-dom": "^19.0.0",
49
+ "tailwindcss": "^4.1.0",
50
+ "typescript": "^5.3.0"
51
+ },
52
+ "scripts": {
53
+ "dev": "next dev --port 3456",
54
+ "build": "next build",
55
+ "start": "next start --port 3456",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest"
58
+ }
59
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,129 @@
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 { CriticalPathDisplay } from "../../../components/dashboard/critical-path-display";
7
+ import type { CriticalPathItem } from "../../../lib/dashboard-data";
8
+
9
+ describe("CriticalPathDisplay", () => {
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ it("renders all critical path items", () => {
15
+ const items: CriticalPathItem[] = [
16
+ {
17
+ issueNumber: 100,
18
+ title: "Implement feature A",
19
+ url: "https://github.com/repo/issues/100",
20
+ priority: "high",
21
+ status: "open",
22
+ estimatedHours: 8,
23
+ specId: "spec-000001",
24
+ },
25
+ {
26
+ issueNumber: 101,
27
+ title: "Implement feature B",
28
+ url: "https://github.com/repo/issues/101",
29
+ priority: "medium",
30
+ status: "in_progress",
31
+ estimatedHours: 16,
32
+ specId: "spec-000002",
33
+ },
34
+ ];
35
+
36
+ render(<CriticalPathDisplay items={items} />);
37
+
38
+ expect(screen.getByText("#100")).toBeInTheDocument();
39
+ expect(screen.getByText("Implement feature A")).toBeInTheDocument();
40
+ expect(screen.getByText("#101")).toBeInTheDocument();
41
+ expect(screen.getByText("Implement feature B")).toBeInTheDocument();
42
+ });
43
+
44
+ it("applies line-through decoration to closed items", () => {
45
+ const items: CriticalPathItem[] = [
46
+ {
47
+ issueNumber: 100,
48
+ title: "Completed task",
49
+ url: "https://github.com/repo/issues/100",
50
+ priority: "high",
51
+ status: "closed",
52
+ estimatedHours: 8,
53
+ specId: "spec-000001",
54
+ },
55
+ ];
56
+
57
+ render(<CriticalPathDisplay items={items} />);
58
+
59
+ const item = screen.getByTestId("critical-path-item");
60
+ expect(item).toHaveClass("line-through");
61
+ });
62
+
63
+ it("applies bold styling to open items", () => {
64
+ const items: CriticalPathItem[] = [
65
+ {
66
+ issueNumber: 100,
67
+ title: "Open task",
68
+ url: "https://github.com/repo/issues/100",
69
+ priority: "high",
70
+ status: "open",
71
+ estimatedHours: 8,
72
+ specId: "spec-000001",
73
+ },
74
+ ];
75
+
76
+ render(<CriticalPathDisplay items={items} />);
77
+
78
+ const item = screen.getByTestId("critical-path-item");
79
+ expect(item).toHaveClass("font-bold");
80
+ });
81
+
82
+ it("applies bold styling to in_progress items", () => {
83
+ const items: CriticalPathItem[] = [
84
+ {
85
+ issueNumber: 100,
86
+ title: "In progress task",
87
+ url: "https://github.com/repo/issues/100",
88
+ priority: "high",
89
+ status: "in_progress",
90
+ estimatedHours: 8,
91
+ specId: "spec-000001",
92
+ },
93
+ ];
94
+
95
+ render(<CriticalPathDisplay items={items} />);
96
+
97
+ const item = screen.getByTestId("critical-path-item");
98
+ expect(item).toHaveClass("font-bold");
99
+ });
100
+
101
+ it("displays priority badges", () => {
102
+ const items: CriticalPathItem[] = [
103
+ {
104
+ issueNumber: 100,
105
+ title: "High priority task",
106
+ url: "https://github.com/repo/issues/100",
107
+ priority: "high",
108
+ status: "open",
109
+ estimatedHours: 8,
110
+ specId: "spec-000001",
111
+ },
112
+ ];
113
+
114
+ render(<CriticalPathDisplay items={items} />);
115
+
116
+ expect(screen.getByText("high")).toBeInTheDocument();
117
+ });
118
+
119
+ it("renders empty list when no items", () => {
120
+ const items: CriticalPathItem[] = [];
121
+
122
+ const { container } = render(<CriticalPathDisplay items={items} />);
123
+
124
+ const listItems = container.querySelectorAll(
125
+ '[data-testid="critical-path-item"]'
126
+ );
127
+ expect(listItems).toHaveLength(0);
128
+ });
129
+ });
@@ -0,0 +1,87 @@
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 { ProgressBar } from "../../../components/dashboard/progress-bar";
7
+
8
+ describe("ProgressBar", () => {
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ it("renders label and progress text", () => {
14
+ render(
15
+ <ProgressBar
16
+ label="Requirements"
17
+ current={8}
18
+ total={10}
19
+ percentage={80}
20
+ color="blue"
21
+ />
22
+ );
23
+
24
+ expect(screen.getByText("Requirements")).toBeInTheDocument();
25
+ expect(screen.getByText("8/10 (80%)")).toBeInTheDocument();
26
+ });
27
+
28
+ it("renders 0% width when percentage is 0", () => {
29
+ render(
30
+ <ProgressBar
31
+ label="Test"
32
+ current={0}
33
+ total={10}
34
+ percentage={0}
35
+ color="blue"
36
+ />
37
+ );
38
+
39
+ const progressBar = screen.getByTestId("progress-bar-fill");
40
+ expect(progressBar).toHaveStyle({ width: "0%" });
41
+ });
42
+
43
+ it("renders 50% width when percentage is 50", () => {
44
+ render(
45
+ <ProgressBar
46
+ label="Test"
47
+ current={5}
48
+ total={10}
49
+ percentage={50}
50
+ color="blue"
51
+ />
52
+ );
53
+
54
+ const progressBar = screen.getByTestId("progress-bar-fill");
55
+ expect(progressBar).toHaveStyle({ width: "50%" });
56
+ });
57
+
58
+ it("renders 100% width when percentage is 100", () => {
59
+ render(
60
+ <ProgressBar
61
+ label="Test"
62
+ current={10}
63
+ total={10}
64
+ percentage={100}
65
+ color="blue"
66
+ />
67
+ );
68
+
69
+ const progressBar = screen.getByTestId("progress-bar-fill");
70
+ expect(progressBar).toHaveStyle({ width: "100%" });
71
+ });
72
+
73
+ it("applies the correct color class", () => {
74
+ render(
75
+ <ProgressBar
76
+ label="Test"
77
+ current={5}
78
+ total={10}
79
+ percentage={50}
80
+ color="green"
81
+ />
82
+ );
83
+
84
+ const progressBar = screen.getByTestId("progress-bar-fill");
85
+ expect(progressBar).toHaveClass("bg-green-500");
86
+ });
87
+ });
@@ -0,0 +1,57 @@
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 { ProjectHealth } from "../../../components/dashboard/project-health";
7
+
8
+ describe("ProjectHealth", () => {
9
+ afterEach(() => {
10
+ cleanup();
11
+ });
12
+
13
+ it("renders score with green color when score >= 80", () => {
14
+ render(<ProjectHealth score={85} />);
15
+
16
+ const scoreElement = screen.getByTestId("health-score");
17
+ expect(scoreElement).toBeInTheDocument();
18
+ expect(scoreElement).toHaveTextContent("85");
19
+ expect(scoreElement).toHaveClass("text-green-500");
20
+ });
21
+
22
+ it("renders score with yellow color when score >= 50 and < 80", () => {
23
+ render(<ProjectHealth score={65} />);
24
+
25
+ const scoreElement = screen.getByTestId("health-score");
26
+ expect(scoreElement).toHaveTextContent("65");
27
+ expect(scoreElement).toHaveClass("text-yellow-500");
28
+ });
29
+
30
+ it("renders score with red color when score < 50", () => {
31
+ render(<ProjectHealth score={30} />);
32
+
33
+ const scoreElement = screen.getByTestId("health-score");
34
+ expect(scoreElement).toHaveTextContent("30");
35
+ expect(scoreElement).toHaveClass("text-red-500");
36
+ });
37
+
38
+ it("renders score at boundary (score = 80) as green", () => {
39
+ render(<ProjectHealth score={80} />);
40
+
41
+ const scoreElement = screen.getByTestId("health-score");
42
+ expect(scoreElement).toHaveClass("text-green-500");
43
+ });
44
+
45
+ it("renders score at boundary (score = 50) as yellow", () => {
46
+ render(<ProjectHealth score={50} />);
47
+
48
+ const scoreElement = screen.getByTestId("health-score");
49
+ expect(scoreElement).toHaveClass("text-yellow-500");
50
+ });
51
+
52
+ it("displays '/ 100' label", () => {
53
+ render(<ProjectHealth score={75} />);
54
+
55
+ expect(screen.getByText("/ 100")).toBeInTheDocument();
56
+ });
57
+ });
@@ -0,0 +1,75 @@
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 { WarningAlert } from "../../../components/dashboard/warning-alert";
7
+ import type { Warning } from "../../../lib/dashboard-data";
8
+
9
+ describe("WarningAlert", () => {
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+
14
+ it("renders error severity with red border and background", () => {
15
+ const warning: Warning = {
16
+ type: "design_verification_error",
17
+ message: "Critical issue detected",
18
+ severity: "error",
19
+ relatedId: "spec-000001",
20
+ };
21
+
22
+ render(<WarningAlert warning={warning} />);
23
+
24
+ const alert = screen.getByTestId("warning-alert");
25
+ expect(alert).toBeInTheDocument();
26
+ expect(alert).toHaveClass("border-red-500");
27
+ expect(alert).toHaveClass("bg-red-50");
28
+ expect(screen.getByText("Critical issue detected")).toBeInTheDocument();
29
+ });
30
+
31
+ it("renders warning severity with yellow border and background", () => {
32
+ const warning: Warning = {
33
+ type: "missing_specification",
34
+ message: "Specification is missing",
35
+ severity: "warning",
36
+ relatedId: "req-000001",
37
+ };
38
+
39
+ render(<WarningAlert warning={warning} />);
40
+
41
+ const alert = screen.getByTestId("warning-alert");
42
+ expect(alert).toHaveClass("border-yellow-500");
43
+ expect(alert).toHaveClass("bg-yellow-50");
44
+ expect(screen.getByText("Specification is missing")).toBeInTheDocument();
45
+ });
46
+
47
+ it("renders info severity with blue border and background", () => {
48
+ const warning: Warning = {
49
+ type: "unapproved_dependency",
50
+ message: "Dependency not approved",
51
+ severity: "info",
52
+ relatedId: "req-000002",
53
+ };
54
+
55
+ render(<WarningAlert warning={warning} />);
56
+
57
+ const alert = screen.getByTestId("warning-alert");
58
+ expect(alert).toHaveClass("border-blue-500");
59
+ expect(alert).toHaveClass("bg-blue-50");
60
+ expect(screen.getByText("Dependency not approved")).toBeInTheDocument();
61
+ });
62
+
63
+ it("displays the related ID", () => {
64
+ const warning: Warning = {
65
+ type: "missing_specification",
66
+ message: "Test message",
67
+ severity: "warning",
68
+ relatedId: "req-000123",
69
+ };
70
+
71
+ render(<WarningAlert warning={warning} />);
72
+
73
+ expect(screen.getByText("req-000123")).toBeInTheDocument();
74
+ });
75
+ });
@@ -0,0 +1,84 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, afterEach } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import type { FeedbackEntry } from "@reqord/shared";
7
+ import { FeedbackClientView } from "../../../components/feedback/feedback-client-view";
8
+
9
+ const feedbacks: FeedbackEntry[] = [
10
+ {
11
+ githubIssue: 1,
12
+ type: "bug",
13
+ severity: "high",
14
+ status: "open",
15
+ linkedTo: { requirements: [], createdRequirements: [], specifications: [], createdSpecifications: [] },
16
+ syncedAt: "2026-01-01T00:00:00Z",
17
+ },
18
+ {
19
+ githubIssue: 2,
20
+ type: "improvement",
21
+ severity: "medium",
22
+ status: "closed",
23
+ linkedTo: { requirements: [], createdRequirements: [], specifications: [], createdSpecifications: [] },
24
+ syncedAt: "2026-01-02T00:00:00Z",
25
+ },
26
+ {
27
+ githubIssue: 3,
28
+ type: "bug",
29
+ severity: "low",
30
+ status: "open",
31
+ linkedTo: { requirements: [], createdRequirements: [], specifications: [], createdSpecifications: [] },
32
+ syncedAt: "2026-01-03T00:00:00Z",
33
+ },
34
+ ];
35
+
36
+ describe("FeedbackClientView", () => {
37
+ afterEach(() => cleanup());
38
+
39
+ it("初期状態で全件表示される", () => {
40
+ render(
41
+ <FeedbackClientView feedbacks={feedbacks} requirementTitles={{}} specificationTitles={{}} />,
42
+ );
43
+
44
+ expect(screen.getAllByTestId("feedback-row")).toHaveLength(3);
45
+ });
46
+
47
+ it("typeフィルタで絞り込みができる", () => {
48
+ render(
49
+ <FeedbackClientView feedbacks={feedbacks} requirementTitles={{}} specificationTitles={{}} />,
50
+ );
51
+
52
+ fireEvent.click(screen.getByTestId("filter-type-bug"));
53
+ expect(screen.getAllByTestId("feedback-row")).toHaveLength(2);
54
+ });
55
+
56
+ it("statusフィルタで絞り込みができる", () => {
57
+ render(
58
+ <FeedbackClientView feedbacks={feedbacks} requirementTitles={{}} specificationTitles={{}} />,
59
+ );
60
+
61
+ fireEvent.click(screen.getByTestId("filter-status-closed"));
62
+ expect(screen.getAllByTestId("feedback-row")).toHaveLength(1);
63
+ });
64
+
65
+ it("複数フィルタのAND条件で絞り込みができる", () => {
66
+ render(
67
+ <FeedbackClientView feedbacks={feedbacks} requirementTitles={{}} specificationTitles={{}} />,
68
+ );
69
+
70
+ fireEvent.click(screen.getByTestId("filter-type-bug"));
71
+ fireEvent.click(screen.getByTestId("filter-severity-high"));
72
+ expect(screen.getAllByTestId("feedback-row")).toHaveLength(1);
73
+ expect(screen.getByTestId("feedback-issue")).toHaveTextContent("#1");
74
+ });
75
+
76
+ it("フィルタ結果が0件の場合emptyメッセージが表示される", () => {
77
+ render(
78
+ <FeedbackClientView feedbacks={feedbacks} requirementTitles={{}} specificationTitles={{}} />,
79
+ );
80
+
81
+ fireEvent.click(screen.getByTestId("filter-type-security"));
82
+ expect(screen.getByTestId("feedback-empty")).toBeInTheDocument();
83
+ });
84
+ });
@@ -0,0 +1,51 @@
1
+ // @vitest-environment jsdom
2
+ import React from "react";
3
+ import { describe, it, expect, vi, afterEach } from "vitest";
4
+ import { render, screen, cleanup, fireEvent } from "@testing-library/react";
5
+ import "@testing-library/jest-dom/vitest";
6
+ import { FeedbackFilters } from "../../../components/feedback/feedback-filters";
7
+
8
+ describe("FeedbackFilters", () => {
9
+ afterEach(() => cleanup());
10
+
11
+ it("全フィルタグループが表示される", () => {
12
+ const onChange = vi.fn();
13
+ render(<FeedbackFilters activeFilters={{}} onFilterChange={onChange} />);
14
+
15
+ expect(screen.getByTestId("feedback-filters")).toBeInTheDocument();
16
+ expect(screen.getByTestId("filter-type-all")).toBeInTheDocument();
17
+ expect(screen.getByTestId("filter-type-bug")).toBeInTheDocument();
18
+ expect(screen.getByTestId("filter-severity-all")).toBeInTheDocument();
19
+ expect(screen.getByTestId("filter-severity-critical")).toBeInTheDocument();
20
+ expect(screen.getByTestId("filter-status-all")).toBeInTheDocument();
21
+ expect(screen.getByTestId("filter-status-open")).toBeInTheDocument();
22
+ });
23
+
24
+ it("typeフィルタ選択でonFilterChangeが呼ばれる", () => {
25
+ const onChange = vi.fn();
26
+ render(<FeedbackFilters activeFilters={{}} onFilterChange={onChange} />);
27
+
28
+ fireEvent.click(screen.getByTestId("filter-type-bug"));
29
+ expect(onChange).toHaveBeenCalledWith({ type: "bug" });
30
+ });
31
+
32
+ it("allを選択するとフィルタがundefinedになる", () => {
33
+ const onChange = vi.fn();
34
+ render(
35
+ <FeedbackFilters activeFilters={{ type: "bug" }} onFilterChange={onChange} />,
36
+ );
37
+
38
+ fireEvent.click(screen.getByTestId("filter-type-all"));
39
+ expect(onChange).toHaveBeenCalledWith({ type: undefined });
40
+ });
41
+
42
+ it("アクティブなフィルタボタンにbg-blue-600クラスが適用される", () => {
43
+ const onChange = vi.fn();
44
+ render(
45
+ <FeedbackFilters activeFilters={{ severity: "high" }} onFilterChange={onChange} />,
46
+ );
47
+
48
+ expect(screen.getByTestId("filter-severity-high")).toHaveClass("bg-blue-600");
49
+ expect(screen.getByTestId("filter-severity-all")).not.toHaveClass("bg-blue-600");
50
+ });
51
+ });
@@ -0,0 +1,131 @@
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 { FeedbackLinkedTo } from "@reqord/shared";
7
+ import { FeedbackLinkedItems } from "../../../components/feedback/feedback-linked-items";
8
+
9
+ const emptyLinkedTo: FeedbackLinkedTo = {
10
+ requirements: [],
11
+ createdRequirements: [],
12
+ specifications: [],
13
+ createdSpecifications: [],
14
+ };
15
+
16
+ const reqTitles: Record<string, string> = {
17
+ "req-000001": "ユーザー認証",
18
+ "req-000002": "データ管理",
19
+ };
20
+
21
+ const specTitles: Record<string, string> = {
22
+ "spec-000001": "認証API",
23
+ "spec-000002": "管理画面",
24
+ };
25
+
26
+ describe("FeedbackLinkedItems", () => {
27
+ afterEach(() => cleanup());
28
+
29
+ it("空の場合: 何も表示されない", () => {
30
+ const { container } = render(
31
+ <FeedbackLinkedItems
32
+ linkedTo={emptyLinkedTo}
33
+ requirementTitles={reqTitles}
34
+ specificationTitles={specTitles}
35
+ />,
36
+ );
37
+ expect(container.firstChild).toBeNull();
38
+ });
39
+
40
+ it("requirementsリンクが表示される", () => {
41
+ render(
42
+ <FeedbackLinkedItems
43
+ linkedTo={{ ...emptyLinkedTo, requirements: ["req-000001"] }}
44
+ requirementTitles={reqTitles}
45
+ specificationTitles={specTitles}
46
+ />,
47
+ );
48
+
49
+ const link = screen.getByTestId("linked-requirement");
50
+ expect(link).toHaveTextContent("ユーザー認証");
51
+ expect(link).toHaveAttribute("href", "/requirements/req-000001");
52
+ });
53
+
54
+ it("createdRequirementsに「created」バッジが表示される", () => {
55
+ render(
56
+ <FeedbackLinkedItems
57
+ linkedTo={{ ...emptyLinkedTo, createdRequirements: ["req-000002"] }}
58
+ requirementTitles={reqTitles}
59
+ specificationTitles={specTitles}
60
+ />,
61
+ );
62
+
63
+ const link = screen.getByTestId("created-requirement");
64
+ expect(link).toHaveTextContent("データ管理");
65
+ expect(link).toHaveTextContent("created");
66
+ });
67
+
68
+ it("specificationsリンクが表示される", () => {
69
+ render(
70
+ <FeedbackLinkedItems
71
+ linkedTo={{ ...emptyLinkedTo, specifications: ["spec-000001"] }}
72
+ requirementTitles={reqTitles}
73
+ specificationTitles={specTitles}
74
+ />,
75
+ );
76
+
77
+ const link = screen.getByTestId("linked-specification");
78
+ expect(link).toHaveTextContent("認証API");
79
+ expect(link).toHaveAttribute("href", "/specifications/spec-000001");
80
+ });
81
+
82
+ it("createdSpecificationsに「created」バッジが表示される", () => {
83
+ render(
84
+ <FeedbackLinkedItems
85
+ linkedTo={{ ...emptyLinkedTo, createdSpecifications: ["spec-000002"] }}
86
+ requirementTitles={reqTitles}
87
+ specificationTitles={specTitles}
88
+ />,
89
+ );
90
+
91
+ const link = screen.getByTestId("created-specification");
92
+ expect(link).toHaveTextContent("管理画面");
93
+ expect(link).toHaveTextContent("created");
94
+ });
95
+
96
+ it("resolvedに「resolved」バッジが表示される", () => {
97
+ render(
98
+ <FeedbackLinkedItems
99
+ linkedTo={{
100
+ ...emptyLinkedTo,
101
+ resolved: {
102
+ requirements: ["req-000001"],
103
+ specifications: ["spec-000001"],
104
+ },
105
+ }}
106
+ requirementTitles={reqTitles}
107
+ specificationTitles={specTitles}
108
+ />,
109
+ );
110
+
111
+ const reqLink = screen.getByTestId("resolved-requirement");
112
+ expect(reqLink).toHaveTextContent("ユーザー認証");
113
+ expect(reqLink).toHaveTextContent("resolved");
114
+
115
+ const specLink = screen.getByTestId("resolved-specification");
116
+ expect(specLink).toHaveTextContent("認証API");
117
+ expect(specLink).toHaveTextContent("resolved");
118
+ });
119
+
120
+ it("タイトルマップにないIDはIDがそのまま表示される", () => {
121
+ render(
122
+ <FeedbackLinkedItems
123
+ linkedTo={{ ...emptyLinkedTo, requirements: ["req-999999"] }}
124
+ requirementTitles={reqTitles}
125
+ specificationTitles={specTitles}
126
+ />,
127
+ );
128
+
129
+ expect(screen.getByTestId("linked-requirement")).toHaveTextContent("req-999999");
130
+ });
131
+ });