@runfusion/fusion 0.22.0 → 0.24.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 (206) hide show
  1. package/dist/bin.js +30071 -20735
  2. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
  3. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
  4. package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
  5. package/dist/client/assets/AgentsView-CV3vm7Qk.css +1 -0
  6. package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
  7. package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
  8. package/dist/client/assets/{DevServerView-l8RCyL2k.js → DevServerView-BkvtjZBa.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-CS1dwqcC.js → DirectoryPicker-BK-KbnhP.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-DmthQWDZ.js → DocumentsView-BEg1CQAk.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
  12. package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
  13. package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
  14. package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
  15. package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
  16. package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
  17. package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
  18. package/dist/client/assets/{MemoryView-CPwlKnUI.js → MemoryView-CKElJY_3.js} +2 -2
  19. package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
  20. package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
  21. package/dist/client/assets/{PiExtensionsManager-j8rPXqmB.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
  22. package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
  23. package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
  24. package/dist/client/assets/{ResearchView-D9DNJYDq.js → ResearchView-B256Lr8I.js} +1 -1
  25. package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
  26. package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
  27. package/dist/client/assets/{SettingsModal-fxvTFLtR.js → SettingsModal-yRqM4DV8.js} +1 -1
  28. package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
  29. package/dist/client/assets/{SkillsView-Ddf0YL8z.js → SkillsView-CP8JX0P_.js} +1 -1
  30. package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
  31. package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
  32. package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
  33. package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
  34. package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
  35. package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
  36. package/dist/client/assets/{folder-open-BiJpmnaT.js → folder-open-DHjELt8-.js} +1 -1
  37. package/dist/client/assets/index-CQyVRLOb.js +692 -0
  38. package/dist/client/assets/index-CxA2Nn0_.css +1 -0
  39. package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
  40. package/dist/client/assets/{star-BwRZmiuZ.js → star-DYesq1AV.js} +1 -1
  41. package/dist/client/assets/{upload-D4NwZhPp.js → upload-DTWF3Db5.js} +1 -1
  42. package/dist/client/assets/{users-DNISDtI1.js → users--syrel4l.js} +1 -1
  43. package/dist/client/index.html +12 -20
  44. package/dist/client/theme-data.css +106 -0
  45. package/dist/client/version.json +1 -1
  46. package/dist/droid-cli/package.json +1 -1
  47. package/dist/extension.js +17072 -9627
  48. package/dist/pi-claude-cli/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
  50. package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
  51. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
  52. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
  53. package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
  54. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
  55. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
  56. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
  57. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
  58. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
  59. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
  60. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
  61. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
  62. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
  63. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
  64. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
  65. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
  66. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
  67. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
  68. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
  69. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
  70. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
  71. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
  72. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
  73. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
  74. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
  75. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
  76. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
  95. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
  96. package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
  97. package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
  98. package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
  99. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +176 -7
  100. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  101. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
  102. package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
  103. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  104. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  105. package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
  106. package/dist/plugins/fusion-plugin-reports/package.json +26 -0
  107. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
  108. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
  109. package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
  110. package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
  111. package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
  112. package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
  113. package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
  114. package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
  115. package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
  116. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
  117. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
  118. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
  119. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
  120. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
  121. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
  122. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
  123. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
  124. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
  125. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
  126. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
  127. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
  128. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
  129. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
  130. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
  131. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
  132. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
  133. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
  134. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
  135. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
  136. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
  137. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
  138. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
  139. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
  140. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
  141. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
  142. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
  143. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
  144. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
  145. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
  146. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
  147. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
  148. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
  149. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
  150. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
  151. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
  152. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
  153. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
  154. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
  155. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
  156. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
  157. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
  158. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
  159. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
  160. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
  161. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
  162. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
  163. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
  164. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
  165. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
  166. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
  167. package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
  168. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
  169. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
  170. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
  171. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
  172. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
  173. package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
  174. package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
  175. package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
  176. package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
  177. package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
  178. package/package.json +2 -2
  179. package/skill/fusion/SKILL.md +2 -2
  180. package/skill/fusion/references/engine-tools.md +8 -2
  181. package/skill/fusion/references/extension-tools.md +39 -0
  182. package/skill/fusion/references/fusion-capabilities.md +3 -0
  183. package/dist/client/assets/AgentDetailView-BKKpbp1S.js +0 -18
  184. package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
  185. package/dist/client/assets/AgentsView-BRXFmrcJ.js +0 -527
  186. package/dist/client/assets/AgentsView-Bs03ptrd.css +0 -1
  187. package/dist/client/assets/ChatView-D7L2e_qu.js +0 -1
  188. package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
  189. package/dist/client/assets/InsightsView-DvXpMKmH.js +0 -11
  190. package/dist/client/assets/NodesView-BLlfUfsy.js +0 -14
  191. package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
  192. package/dist/client/assets/PluginManager-DA_T0GHn.css +0 -1
  193. package/dist/client/assets/PluginManager-pW6RMz5z.js +0 -1
  194. package/dist/client/assets/RoadmapsView-Djc_X35v.js +0 -6
  195. package/dist/client/assets/SettingsModal-BWe0KrGY.css +0 -1
  196. package/dist/client/assets/SettingsModal-WGCF_pk8.js +0 -31
  197. package/dist/client/assets/SetupWizardModal-tG_MF_nA.js +0 -1
  198. package/dist/client/assets/agentSkills-EwIwBlG8.js +0 -1
  199. package/dist/client/assets/index-D6ebxTPF.css +0 -1
  200. package/dist/client/assets/index-DYDLmOcK.js +0 -694
  201. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -132
  202. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
  203. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
  204. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -31
  205. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -23
  206. /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
@@ -0,0 +1,1144 @@
1
+ /* @vitest-environment jsdom */
2
+ import React from "react";
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
4
+ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
5
+ import userEvent from "@testing-library/user-event";
6
+ import { RoadmapsView } from "../RoadmapsView";
7
+ import * as api from "../api";
8
+ import type {
9
+ Roadmap,
10
+ RoadmapMilestone,
11
+ RoadmapFeature,
12
+ RoadmapWithHierarchy,
13
+ } from "../../roadmap-types";
14
+
15
+ // Mock the API module
16
+ vi.mock("../api", () => ({
17
+ fetchRoadmaps: vi.fn(),
18
+ fetchRoadmap: vi.fn(),
19
+ createRoadmap: vi.fn(),
20
+ updateRoadmap: vi.fn(),
21
+ deleteRoadmap: vi.fn(),
22
+ createRoadmapMilestone: vi.fn(),
23
+ updateRoadmapMilestone: vi.fn(),
24
+ deleteRoadmapMilestone: vi.fn(),
25
+ createRoadmapFeature: vi.fn(),
26
+ updateRoadmapFeature: vi.fn(),
27
+ deleteRoadmapFeature: vi.fn(),
28
+ reorderRoadmapMilestones: vi.fn(),
29
+ reorderRoadmapFeatures: vi.fn(),
30
+ moveRoadmapFeature: vi.fn(),
31
+ generateFeatureSuggestions: vi.fn(),
32
+ generateMilestoneSuggestions: vi.fn(),
33
+ }));
34
+
35
+ // Mock lucide-react icons
36
+ const mockConfirm = vi.fn();
37
+
38
+ vi.mock("../useConfirm", () => ({
39
+ useConfirm: () => ({ confirm: mockConfirm }),
40
+ }));
41
+
42
+ vi.mock("lucide-react", () => ({
43
+ Plus: ({ size, ...props }: { size?: number }) => (
44
+ <svg data-testid="plus-icon" {...props}>{`Plus ${size || 16}`}</svg>
45
+ ),
46
+ Pencil: (props: Record<string, unknown>) => <span data-testid="pencil-icon" {...props}>Edit</span>,
47
+ Trash2: (props: Record<string, unknown>) => <span data-testid="trash-icon" {...props}>Delete</span>,
48
+ Check: (props: Record<string, unknown>) => <span data-testid="check-icon" {...props}>Check</span>,
49
+ X: (props: Record<string, unknown>) => <span data-testid="x-icon" {...props}>X</span>,
50
+ GripVertical: (props: Record<string, unknown>) => <span data-testid="grip-icon" {...props}>Grip</span>,
51
+ Sparkles: (props: Record<string, unknown>) => <span data-testid="sparkles-icon" {...props}>Sparkles</span>,
52
+ Download: (props: Record<string, unknown>) => <span data-testid="download-icon" {...props}>Download</span>,
53
+ Copy: (props: Record<string, unknown>) => <span data-testid="copy-icon" {...props}>Copy</span>,
54
+ Loader: (props: Record<string, unknown>) => <span data-testid="loader-icon" {...props}>Loader</span>,
55
+ ArrowLeft: (props: Record<string, unknown>) => <span data-testid="arrow-left-icon" {...props}>ArrowLeft</span>,
56
+ ChevronLeft: (props: Record<string, unknown>) => <span data-testid="chevron-left-icon" {...props}>ChevronLeft</span>,
57
+ ChevronUp: (props: Record<string, unknown>) => <span data-testid="chevron-up-icon" {...props}>ChevronUp</span>,
58
+ }));
59
+
60
+ // Viewport mode mock helper
61
+ function mockViewport(mode: "mobile" | "desktop") {
62
+ Object.defineProperty(window, "matchMedia", {
63
+ writable: true,
64
+ value: vi.fn().mockImplementation((query: string) => {
65
+ const isMobileQuery = query === "(max-width: 768px)";
66
+ const isTabletQuery = query === "(min-width: 769px) and (max-width: 1024px)";
67
+ return {
68
+ matches: mode === "mobile" ? isMobileQuery : false,
69
+ media: query,
70
+ addEventListener: vi.fn(),
71
+ removeEventListener: vi.fn(),
72
+ dispatchEvent: vi.fn(),
73
+ };
74
+ }),
75
+ });
76
+ }
77
+
78
+ const mockRoadmaps: Roadmap[] = [
79
+ {
80
+ id: "RM-001",
81
+ title: "Q2 Roadmap",
82
+ description: "Q2 product roadmap",
83
+ createdAt: "2026-01-01T00:00:00.000Z",
84
+ updatedAt: "2026-01-01T00:00:00.000Z",
85
+ },
86
+ {
87
+ id: "RM-002",
88
+ title: "Q3 Roadmap",
89
+ description: "Q3 product roadmap",
90
+ createdAt: "2026-01-02T00:00:00.000Z",
91
+ updatedAt: "2026-01-02T00:00:00.000Z",
92
+ },
93
+ ];
94
+
95
+ const mockRoadmapHierarchy: RoadmapWithHierarchy = {
96
+ id: "RM-001",
97
+ title: "Q2 Roadmap",
98
+ description: "Q2 product roadmap",
99
+ createdAt: "2026-01-01T00:00:00.000Z",
100
+ updatedAt: "2026-01-01T00:00:00.000Z",
101
+ milestones: [
102
+ {
103
+ id: "RMS-001",
104
+ roadmapId: "RM-001",
105
+ title: "Milestone 1",
106
+ description: "First milestone",
107
+ orderIndex: 0,
108
+ createdAt: "2026-01-01T00:00:00.000Z",
109
+ updatedAt: "2026-01-01T00:00:00.000Z",
110
+ features: [
111
+ {
112
+ id: "RF-001",
113
+ milestoneId: "RMS-001",
114
+ title: "Feature 1",
115
+ description: "First feature",
116
+ orderIndex: 0,
117
+ createdAt: "2026-01-01T00:00:00.000Z",
118
+ updatedAt: "2026-01-01T00:00:00.000Z",
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ id: "RMS-002",
124
+ roadmapId: "RM-001",
125
+ title: "Milestone 2",
126
+ description: "Second milestone",
127
+ orderIndex: 1,
128
+ createdAt: "2026-01-01T00:00:00.000Z",
129
+ updatedAt: "2026-01-01T00:00:00.000Z",
130
+ features: [],
131
+ },
132
+ ],
133
+ };
134
+
135
+ const mockAddToast = vi.fn();
136
+
137
+ describe("RoadmapsView", () => {
138
+ beforeEach(() => {
139
+ vi.clearAllMocks();
140
+ mockConfirm.mockReset();
141
+ mockConfirm.mockResolvedValue(true);
142
+ mockViewport("desktop");
143
+ (api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmaps);
144
+ (api.fetchRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(mockRoadmapHierarchy);
145
+ });
146
+
147
+ afterEach(() => {
148
+ vi.restoreAllMocks();
149
+ });
150
+
151
+ it("renders roadmap list in sidebar", async () => {
152
+ render(<RoadmapsView addToast={mockAddToast} />);
153
+
154
+ await waitFor(() => {
155
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
156
+ });
157
+ expect(screen.getByText("Q3 Roadmap")).toBeInTheDocument();
158
+ expect(screen.getByText("Roadmaps")).toBeInTheDocument();
159
+ });
160
+
161
+ it("shows empty state when no roadmaps exist", async () => {
162
+ (api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue([]);
163
+
164
+ render(<RoadmapsView addToast={mockAddToast} />);
165
+
166
+ await waitFor(() => {
167
+ expect(screen.getByText("No roadmaps yet. Click + to create one.")).toBeInTheDocument();
168
+ });
169
+ });
170
+
171
+ it("shows loading state initially", async () => {
172
+ render(<RoadmapsView addToast={mockAddToast} />);
173
+
174
+ expect(screen.getByText("Loading roadmaps...")).toBeInTheDocument();
175
+ });
176
+
177
+ it("selects a roadmap and shows its content", async () => {
178
+ render(<RoadmapsView addToast={mockAddToast} />);
179
+
180
+ await waitFor(() => {
181
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
182
+ });
183
+
184
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
185
+ fireEvent.click(roadmapItem);
186
+
187
+ await waitFor(() => {
188
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
189
+ expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
190
+ });
191
+
192
+ // Should show milestones
193
+ expect(screen.getByText("Milestone 1")).toBeInTheDocument();
194
+ expect(screen.getByText("Milestone 2")).toBeInTheDocument();
195
+ });
196
+
197
+ it("creates a new roadmap", async () => {
198
+ const newRoadmap = {
199
+ id: "RM-003",
200
+ title: "New Roadmap",
201
+ createdAt: "2026-01-03T00:00:00.000Z",
202
+ updatedAt: "2026-01-03T00:00:00.000Z",
203
+ };
204
+ (api.createRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(newRoadmap);
205
+
206
+ render(<RoadmapsView addToast={mockAddToast} />);
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
210
+ });
211
+
212
+ // Click create button
213
+ const createBtn = screen.getByTestId("create-roadmap-btn");
214
+ fireEvent.click(createBtn);
215
+
216
+ // Fill in the form
217
+ const titleInput = screen.getByTestId("create-roadmap-title");
218
+ await userEvent.type(titleInput, "New Roadmap");
219
+
220
+ // Submit
221
+ const submitBtn = screen.getByTestId("create-roadmap-submit");
222
+ fireEvent.click(submitBtn);
223
+
224
+ await waitFor(() => {
225
+ expect(api.createRoadmap).toHaveBeenCalledWith(
226
+ { title: "New Roadmap" },
227
+ undefined
228
+ );
229
+ expect(mockAddToast).toHaveBeenCalledWith("Roadmap created", "success");
230
+ });
231
+ });
232
+
233
+ it("renders inline edit inputs with current values for milestone and feature", async () => {
234
+ render(<RoadmapsView addToast={mockAddToast} />);
235
+
236
+ await waitFor(() => {
237
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
238
+ });
239
+
240
+ fireEvent.click(screen.getByTestId("roadmap-item-RM-001"));
241
+
242
+ await waitFor(() => {
243
+ expect(screen.getByText("Milestone 1")).toBeInTheDocument();
244
+ });
245
+
246
+ fireEvent.click(screen.getByTestId("milestone-edit-RMS-001"));
247
+
248
+ const milestoneTitleInput = await screen.findByTestId("milestone-title-input-RMS-001");
249
+ expect(milestoneTitleInput).toHaveValue("Milestone 1");
250
+
251
+ fireEvent.click(screen.getByTestId("feature-edit-RF-001"));
252
+
253
+ const featureTitleInput = await screen.findByTestId("feature-title-input-RF-001");
254
+ expect(featureTitleInput).toHaveValue("Feature 1");
255
+ });
256
+
257
+ it("edits a roadmap title inline", async () => {
258
+ (api.updateRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue({
259
+ ...mockRoadmaps[0],
260
+ title: "Updated Title",
261
+ });
262
+
263
+ render(<RoadmapsView addToast={mockAddToast} />);
264
+
265
+ await waitFor(() => {
266
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
267
+ });
268
+
269
+ // Select the roadmap first
270
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
271
+ fireEvent.click(roadmapItem);
272
+
273
+ await waitFor(() => {
274
+ expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
275
+ });
276
+
277
+ // Click edit button
278
+ const editBtn = screen.getByTestId("edit-roadmap-btn");
279
+ fireEvent.click(editBtn);
280
+
281
+ // Should show input field
282
+ const titleInput = screen.getByTestId("roadmap-title-input");
283
+ expect(titleInput).toBeInTheDocument();
284
+
285
+ // Type new title
286
+ await userEvent.clear(titleInput);
287
+ await userEvent.type(titleInput, "Updated Title");
288
+
289
+ // Save
290
+ fireEvent.keyDown(titleInput, { key: "Enter" });
291
+
292
+ await waitFor(() => {
293
+ expect(api.updateRoadmap).toHaveBeenCalledWith("RM-001", { title: "Updated Title" }, undefined);
294
+ });
295
+ });
296
+
297
+ it("deletes a roadmap after confirmation", async () => {
298
+ (api.deleteRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
299
+
300
+ render(<RoadmapsView addToast={mockAddToast} />);
301
+
302
+ await waitFor(() => {
303
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
304
+ });
305
+
306
+ // Select the roadmap first
307
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
308
+ fireEvent.click(roadmapItem);
309
+
310
+ await waitFor(() => {
311
+ expect(screen.getByTestId("delete-roadmap-btn")).toBeInTheDocument();
312
+ });
313
+
314
+ // Click delete button
315
+ const deleteBtn = screen.getByTestId("delete-roadmap-btn");
316
+ fireEvent.click(deleteBtn);
317
+
318
+ await waitFor(() => {
319
+ expect(mockConfirm).toHaveBeenCalledWith({
320
+ title: "Delete Roadmap",
321
+ message: "Delete this roadmap? This cannot be undone.",
322
+ danger: true,
323
+ });
324
+ expect(api.deleteRoadmap).toHaveBeenCalledWith("RM-001", undefined);
325
+ expect(mockAddToast).toHaveBeenCalledWith("Roadmap deleted", "success");
326
+ });
327
+ });
328
+
329
+ it("cancels roadmap deletion when not confirmed", async () => {
330
+ mockConfirm.mockResolvedValueOnce(false);
331
+
332
+ render(<RoadmapsView addToast={mockAddToast} />);
333
+
334
+ await waitFor(() => {
335
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
336
+ });
337
+
338
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
339
+ fireEvent.click(roadmapItem);
340
+
341
+ await waitFor(() => {
342
+ expect(screen.getByTestId("delete-roadmap-btn")).toBeInTheDocument();
343
+ });
344
+
345
+ const deleteBtn = screen.getByTestId("delete-roadmap-btn");
346
+ fireEvent.click(deleteBtn);
347
+
348
+ expect(api.deleteRoadmap).not.toHaveBeenCalled();
349
+ });
350
+
351
+ it("shows empty milestones state", async () => {
352
+ const emptyHierarchy: RoadmapWithHierarchy = {
353
+ ...mockRoadmapHierarchy,
354
+ milestones: [],
355
+ };
356
+ (api.fetchRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(emptyHierarchy);
357
+
358
+ render(<RoadmapsView addToast={mockAddToast} />);
359
+
360
+ await waitFor(() => {
361
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
362
+ });
363
+
364
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
365
+ fireEvent.click(roadmapItem);
366
+
367
+ await waitFor(() => {
368
+ expect(screen.getByText("This roadmap has no milestones.")).toBeInTheDocument();
369
+ });
370
+ });
371
+
372
+ it("shows empty features state for a milestone", async () => {
373
+ render(<RoadmapsView addToast={mockAddToast} />);
374
+
375
+ await waitFor(() => {
376
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
377
+ });
378
+
379
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
380
+ fireEvent.click(roadmapItem);
381
+
382
+ await waitFor(() => {
383
+ expect(screen.getByText("Milestone 2")).toBeInTheDocument();
384
+ });
385
+
386
+ // Milestone 2 has no features
387
+ const milestone2 = screen.getByTestId("add-feature-RMS-002").closest(".roadmaps-view__milestone");
388
+ expect(milestone2?.textContent).toContain("No features yet.");
389
+ });
390
+
391
+ it("creates a milestone", async () => {
392
+ const newMilestone: RoadmapMilestone = {
393
+ id: "RMS-003",
394
+ roadmapId: "RM-001",
395
+ title: "New Milestone",
396
+ orderIndex: 2,
397
+ createdAt: "2026-01-03T00:00:00.000Z",
398
+ updatedAt: "2026-01-03T00:00:00.000Z",
399
+ };
400
+ (api.createRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(newMilestone);
401
+
402
+ render(<RoadmapsView addToast={mockAddToast} />);
403
+
404
+ await waitFor(() => {
405
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
406
+ });
407
+
408
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
409
+ fireEvent.click(roadmapItem);
410
+
411
+ await waitFor(() => {
412
+ expect(screen.getByTestId("add-milestone-btn")).toBeInTheDocument();
413
+ });
414
+
415
+ // Click add milestone button
416
+ const addBtn = screen.getByTestId("add-milestone-btn");
417
+ fireEvent.click(addBtn);
418
+
419
+ // Fill in the form
420
+ const titleInput = screen.getByTestId("create-milestone-title");
421
+ await userEvent.type(titleInput, "New Milestone");
422
+
423
+ // Submit
424
+ const submitBtn = screen.getByTestId("create-milestone-submit");
425
+ fireEvent.click(submitBtn);
426
+
427
+ await waitFor(() => {
428
+ expect(api.createRoadmapMilestone).toHaveBeenCalledWith("RM-001", { title: "New Milestone" }, undefined);
429
+ expect(mockAddToast).toHaveBeenCalledWith("Milestone created", "success");
430
+ });
431
+ });
432
+
433
+ it("deletes a milestone after confirmation", async () => {
434
+ (api.deleteRoadmapMilestone as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
435
+
436
+ render(<RoadmapsView addToast={mockAddToast} />);
437
+
438
+ await waitFor(() => {
439
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
440
+ });
441
+
442
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
443
+ fireEvent.click(roadmapItem);
444
+
445
+ await waitFor(() => {
446
+ expect(screen.getByTestId("milestone-delete-RMS-001")).toBeInTheDocument();
447
+ });
448
+
449
+ const deleteBtn = screen.getByTestId("milestone-delete-RMS-001");
450
+ fireEvent.click(deleteBtn);
451
+
452
+ await waitFor(() => {
453
+ expect(mockConfirm).toHaveBeenCalledWith({
454
+ title: "Delete Milestone",
455
+ message: "Delete this milestone and all its features?",
456
+ danger: true,
457
+ });
458
+ expect(api.deleteRoadmapMilestone).toHaveBeenCalledWith("RMS-001", undefined);
459
+ expect(mockAddToast).toHaveBeenCalledWith("Milestone deleted", "success");
460
+ });
461
+ });
462
+
463
+ it("creates a feature in a milestone", async () => {
464
+ const newFeature: RoadmapFeature = {
465
+ id: "RF-002",
466
+ milestoneId: "RMS-001",
467
+ title: "New Feature",
468
+ orderIndex: 1,
469
+ createdAt: "2026-01-03T00:00:00.000Z",
470
+ updatedAt: "2026-01-03T00:00:00.000Z",
471
+ };
472
+ (api.createRoadmapFeature as ReturnType<typeof vi.fn>).mockResolvedValue(newFeature);
473
+
474
+ render(<RoadmapsView addToast={mockAddToast} />);
475
+
476
+ await waitFor(() => {
477
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
478
+ });
479
+
480
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
481
+ fireEvent.click(roadmapItem);
482
+
483
+ await waitFor(() => {
484
+ expect(screen.getByTestId("add-feature-RMS-001")).toBeInTheDocument();
485
+ });
486
+
487
+ // Click add feature button
488
+ const addBtn = screen.getByTestId("add-feature-RMS-001");
489
+ fireEvent.click(addBtn);
490
+
491
+ // Fill in the form
492
+ const titleInput = screen.getByTestId("create-feature-title");
493
+ await userEvent.type(titleInput, "New Feature");
494
+
495
+ // Submit
496
+ const submitBtn = screen.getByTestId("create-feature-submit");
497
+ fireEvent.click(submitBtn);
498
+
499
+ await waitFor(() => {
500
+ expect(api.createRoadmapFeature).toHaveBeenCalledWith("RMS-001", { title: "New Feature" }, undefined);
501
+ expect(mockAddToast).toHaveBeenCalledWith("Feature created", "success");
502
+ });
503
+ });
504
+
505
+ it("deletes a feature after confirmation", async () => {
506
+ (api.deleteRoadmapFeature as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
507
+
508
+ render(<RoadmapsView addToast={mockAddToast} />);
509
+
510
+ await waitFor(() => {
511
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
512
+ });
513
+
514
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
515
+ fireEvent.click(roadmapItem);
516
+
517
+ await waitFor(() => {
518
+ expect(screen.getByTestId("feature-delete-RF-001")).toBeInTheDocument();
519
+ });
520
+
521
+ // Hover over feature to show actions
522
+ const featureItem = screen.getByTestId("feature-delete-RF-001").closest(".roadmaps-view__feature-item");
523
+ fireEvent.mouseEnter(featureItem!);
524
+
525
+ const deleteBtn = screen.getByTestId("feature-delete-RF-001");
526
+ fireEvent.click(deleteBtn);
527
+
528
+ await waitFor(() => {
529
+ expect(mockConfirm).toHaveBeenCalledWith({
530
+ title: "Delete Feature",
531
+ message: "Delete this feature?",
532
+ danger: true,
533
+ });
534
+ expect(api.deleteRoadmapFeature).toHaveBeenCalledWith("RF-001", undefined);
535
+ expect(mockAddToast).toHaveBeenCalledWith("Feature deleted", "success");
536
+ });
537
+ });
538
+
539
+ it("cancels inline edit on Escape", async () => {
540
+ render(<RoadmapsView addToast={mockAddToast} />);
541
+
542
+ await waitFor(() => {
543
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
544
+ });
545
+
546
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
547
+ fireEvent.click(roadmapItem);
548
+
549
+ await waitFor(() => {
550
+ expect(screen.getByTestId("edit-roadmap-btn")).toBeInTheDocument();
551
+ });
552
+
553
+ // Start editing
554
+ const editBtn = screen.getByTestId("edit-roadmap-btn");
555
+ fireEvent.click(editBtn);
556
+
557
+ const titleInput = screen.getByTestId("roadmap-title-input");
558
+ expect(titleInput).toBeInTheDocument();
559
+
560
+ // Type something
561
+ await userEvent.type(titleInput, "Changed");
562
+
563
+ // Cancel with Escape
564
+ fireEvent.keyDown(titleInput, { key: "Escape" });
565
+
566
+ // Edit should be cancelled
567
+ await waitFor(() => {
568
+ expect(screen.queryByTestId("roadmap-title-input")).not.toBeInTheDocument();
569
+ });
570
+ expect(api.updateRoadmap).not.toHaveBeenCalled();
571
+ });
572
+
573
+ it("shows empty main state when no roadmap selected", async () => {
574
+ render(<RoadmapsView addToast={mockAddToast} />);
575
+
576
+ await waitFor(() => {
577
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
578
+ });
579
+
580
+ expect(screen.getByText("Select a roadmap from the sidebar to view its milestones.")).toBeInTheDocument();
581
+ });
582
+
583
+ describe("Feature suggestions", () => {
584
+ it("shows AI Suggestions button when roadmap is selected", async () => {
585
+ render(<RoadmapsView addToast={mockAddToast} />);
586
+
587
+ // Wait for roadmap to load
588
+ await waitFor(() => {
589
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
590
+ });
591
+
592
+ // Select roadmap
593
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
594
+ fireEvent.click(roadmapItem);
595
+
596
+ // Wait for milestone to load and button to appear
597
+ await waitFor(() => {
598
+ expect(screen.getByTestId("generate-features-RMS-001")).toBeInTheDocument();
599
+ }, { timeout: 3000 });
600
+ });
601
+ });
602
+
603
+ describe("Suggestion editing", () => {
604
+ it("can edit milestone suggestion before accepting", async () => {
605
+ // Mock milestone suggestion generation
606
+ (api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
607
+ suggestions: [
608
+ { title: "Original Title", description: "Original description" },
609
+ ],
610
+ });
611
+
612
+ render(<RoadmapsView addToast={mockAddToast} />);
613
+
614
+ await waitFor(() => {
615
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
616
+ });
617
+
618
+ // Select roadmap
619
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
620
+ fireEvent.click(roadmapItem);
621
+
622
+ // Wait for roadmap to load
623
+ await waitFor(() => {
624
+ expect(screen.getByText("Generate Milestone Ideas")).toBeInTheDocument();
625
+ });
626
+
627
+ // Generate suggestions
628
+ const goalInput = screen.getByTestId("goal-prompt-input");
629
+ await userEvent.type(goalInput, "Build an app");
630
+
631
+ const generateBtn = screen.getByTestId("generate-suggestions-btn");
632
+ fireEvent.click(generateBtn);
633
+
634
+ // Wait for suggestion to appear
635
+ await waitFor(() => {
636
+ expect(screen.getByText("Original Title")).toBeInTheDocument();
637
+ });
638
+
639
+ // This test verifies the suggestion appears after generation
640
+ expect(screen.getByText("Original Title")).toBeInTheDocument();
641
+ });
642
+
643
+ it("can edit feature suggestion before accepting", async () => {
644
+ // Mock feature suggestion generation
645
+ (api.generateFeatureSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
646
+ suggestions: [
647
+ { title: "Feature Suggestion", description: "Feature description" },
648
+ ],
649
+ });
650
+
651
+ render(<RoadmapsView addToast={mockAddToast} />);
652
+
653
+ await waitFor(() => {
654
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
655
+ });
656
+
657
+ // Select roadmap
658
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
659
+ fireEvent.click(roadmapItem);
660
+
661
+ // Wait for milestone to load
662
+ await waitFor(() => {
663
+ expect(screen.getByTestId("generate-features-RMS-001")).toBeInTheDocument();
664
+ });
665
+
666
+ // Generate feature suggestions
667
+ const suggestBtn = screen.getByTestId("generate-features-RMS-001");
668
+ fireEvent.click(suggestBtn);
669
+
670
+ // Wait for suggestion to appear
671
+ await waitFor(() => {
672
+ expect(screen.getByText("Feature Suggestion")).toBeInTheDocument();
673
+ });
674
+
675
+ // Verify the suggestion card is rendered
676
+ expect(screen.getByText("Feature Suggestion")).toBeInTheDocument();
677
+ expect(screen.getByText("Feature description")).toBeInTheDocument();
678
+ });
679
+
680
+ it("shows edit button on suggestion cards", async () => {
681
+ // Mock milestone suggestion generation
682
+ (api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
683
+ suggestions: [
684
+ { title: "Test Milestone" },
685
+ ],
686
+ });
687
+
688
+ render(<RoadmapsView addToast={mockAddToast} />);
689
+
690
+ await waitFor(() => {
691
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
692
+ });
693
+
694
+ // Select roadmap
695
+ const roadmapItem = screen.getByTestId("roadmap-item-RM-001");
696
+ fireEvent.click(roadmapItem);
697
+
698
+ // Generate suggestions
699
+ const goalInput = screen.getByTestId("goal-prompt-input");
700
+ await userEvent.type(goalInput, "Build something");
701
+
702
+ const generateBtn = screen.getByTestId("generate-suggestions-btn");
703
+ fireEvent.click(generateBtn);
704
+
705
+ // Wait for suggestion to appear
706
+ await waitFor(() => {
707
+ expect(screen.getByText("Test Milestone")).toBeInTheDocument();
708
+ });
709
+
710
+ // Look for edit button - it should have data-testid
711
+ // The edit button is a pencil icon with testId like "suggestion-{id}-edit"
712
+ const editButtons = screen.queryAllByRole("button", { name: /edit/i });
713
+ expect(editButtons.length).toBeGreaterThan(0);
714
+ });
715
+
716
+ it("sidebar edit pencil selects the roadmap and shows inline edit", async () => {
717
+ render(<RoadmapsView addToast={mockAddToast} />);
718
+
719
+ await waitFor(() => {
720
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
721
+ });
722
+
723
+ // No roadmap selected yet — main content shows empty state
724
+ expect(screen.getByText("Select a roadmap from the sidebar to view its milestones.")).toBeInTheDocument();
725
+
726
+ // Click the edit pencil on RM-001 in the sidebar
727
+ const editBtn = screen.getByTestId("roadmap-edit-RM-001");
728
+ fireEvent.click(editBtn);
729
+
730
+ // Roadmap should be selected and inline edit should appear
731
+ await waitFor(() => {
732
+ expect(screen.getByTestId("roadmap-title-input")).toBeInTheDocument();
733
+ });
734
+ });
735
+ });
736
+
737
+ describe("Mobile roadmap controls", () => {
738
+ beforeEach(() => {
739
+ mockViewport("mobile");
740
+ });
741
+
742
+ afterEach(() => {
743
+ mockViewport("desktop");
744
+ });
745
+
746
+ it("shows mobile roadmap list when no roadmap selected on mobile", async () => {
747
+ render(<RoadmapsView addToast={mockAddToast} />);
748
+
749
+ await waitFor(() => {
750
+ expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
751
+ });
752
+
753
+ expect(screen.getByText("Q2 Roadmap")).toBeInTheDocument();
754
+ expect(screen.getByText("Q3 Roadmap")).toBeInTheDocument();
755
+ expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
756
+ });
757
+
758
+ it("shows mobile roadmap items when roadmaps exist on mobile", async () => {
759
+ render(<RoadmapsView addToast={mockAddToast} />);
760
+
761
+ await waitFor(() => {
762
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
763
+ });
764
+
765
+ expect(screen.getByTestId("mobile-roadmap-item-RM-002")).toBeInTheDocument();
766
+ });
767
+
768
+ it("can select a roadmap from mobile list", async () => {
769
+ render(<RoadmapsView addToast={mockAddToast} />);
770
+
771
+ await waitFor(() => {
772
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
773
+ });
774
+
775
+ // Click on a roadmap item
776
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
777
+
778
+ // Should show the mobile header with the roadmap title
779
+ await waitFor(() => {
780
+ expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
781
+ expect(screen.getByText("Q2 Roadmap", { selector: ".roadmaps-view__mobile-header-title" })).toBeInTheDocument();
782
+ });
783
+ });
784
+
785
+ it("mobile back button deselects roadmap", async () => {
786
+ render(<RoadmapsView addToast={mockAddToast} />);
787
+
788
+ await waitFor(() => {
789
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
790
+ });
791
+
792
+ // Select a roadmap
793
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
794
+
795
+ await waitFor(() => {
796
+ expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
797
+ });
798
+
799
+ // Click back button
800
+ fireEvent.click(screen.getByTestId("mobile-back-btn"));
801
+
802
+ // Should show mobile list again
803
+ await waitFor(() => {
804
+ expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
805
+ });
806
+ });
807
+
808
+ it("mobile create button shows create form", async () => {
809
+ render(<RoadmapsView addToast={mockAddToast} />);
810
+
811
+ await waitFor(() => {
812
+ expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
813
+ });
814
+
815
+ // Click create button
816
+ fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
817
+
818
+ // Should show create form
819
+ await waitFor(() => {
820
+ expect(screen.getByTestId("create-roadmap-form")).toBeInTheDocument();
821
+ expect(screen.getByTestId("create-roadmap-title")).toBeInTheDocument();
822
+ });
823
+ });
824
+
825
+ it("mobile can create roadmap via form", async () => {
826
+ const newRoadmap = {
827
+ id: "RM-003",
828
+ title: "New Roadmap",
829
+ createdAt: "2026-01-03T00:00:00.000Z",
830
+ updatedAt: "2026-01-03T00:00:00.000Z",
831
+ };
832
+ (api.createRoadmap as ReturnType<typeof vi.fn>).mockResolvedValue(newRoadmap);
833
+
834
+ render(<RoadmapsView addToast={mockAddToast} />);
835
+
836
+ await waitFor(() => {
837
+ expect(screen.getByTestId("mobile-create-roadmap-btn")).toBeInTheDocument();
838
+ });
839
+
840
+ // Click create button
841
+ fireEvent.click(screen.getByTestId("mobile-create-roadmap-btn"));
842
+
843
+ // Fill in the form using userEvent for better React integration
844
+ await waitFor(() => {
845
+ const titleInput = screen.getByTestId("create-roadmap-title");
846
+ expect(titleInput).toBeInTheDocument();
847
+ });
848
+
849
+ const titleInput = screen.getByTestId("create-roadmap-title");
850
+ await userEvent.type(titleInput, "New Roadmap");
851
+
852
+ // Submit the form using fireEvent.submit
853
+ const form = screen.getByTestId("create-roadmap-form").querySelector("form");
854
+ expect(form).toBeTruthy();
855
+ fireEvent.submit(form!);
856
+
857
+ await waitFor(() => {
858
+ expect(api.createRoadmap).toHaveBeenCalledWith(
859
+ { title: "New Roadmap" },
860
+ undefined
861
+ );
862
+ expect(mockAddToast).toHaveBeenCalledWith("Roadmap created", "success");
863
+ });
864
+ });
865
+
866
+ it("mobile edit and delete buttons are visible on roadmap items", async () => {
867
+ render(<RoadmapsView addToast={mockAddToast} />);
868
+
869
+ await waitFor(() => {
870
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
871
+ });
872
+
873
+ // Edit and delete buttons should be visible (not hidden behind hover on mobile)
874
+ expect(screen.getByTestId("mobile-roadmap-edit-RM-001")).toBeInTheDocument();
875
+ expect(screen.getByTestId("mobile-roadmap-delete-RM-001")).toBeInTheDocument();
876
+ expect(screen.getByTestId("mobile-roadmap-export-RM-001")).toBeInTheDocument();
877
+ });
878
+
879
+ it("shows empty state on mobile when no roadmaps", async () => {
880
+ (api.fetchRoadmaps as ReturnType<typeof vi.fn>).mockResolvedValue([]);
881
+
882
+ render(<RoadmapsView addToast={mockAddToast} />);
883
+
884
+ await waitFor(() => {
885
+ expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
886
+ });
887
+
888
+ expect(screen.getByText("No roadmaps yet.")).toBeInTheDocument();
889
+ expect(screen.getByTestId("roadmaps-view__mobile-list").textContent).toContain("Create Roadmap");
890
+ });
891
+
892
+ it("mobile header shows action buttons when roadmap selected", async () => {
893
+ render(<RoadmapsView addToast={mockAddToast} />);
894
+
895
+ await waitFor(() => {
896
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
897
+ });
898
+
899
+ // Select a roadmap
900
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
901
+
902
+ await waitFor(() => {
903
+ expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
904
+ });
905
+
906
+ // Action buttons should be visible in header
907
+ expect(screen.getByTestId("mobile-header-create-btn")).toBeInTheDocument();
908
+ expect(screen.getByTestId("mobile-header-edit-btn")).toBeInTheDocument();
909
+ expect(screen.getByTestId("mobile-header-delete-btn")).toBeInTheDocument();
910
+ expect(screen.getByTestId("mobile-back-btn")).toBeInTheDocument();
911
+ });
912
+
913
+ it("mobile edit pencil selects the roadmap and shows inline edit", async () => {
914
+ render(<RoadmapsView addToast={mockAddToast} />);
915
+
916
+ await waitFor(() => {
917
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
918
+ });
919
+
920
+ // Click the edit pencil on RM-001 in mobile list
921
+ const editBtn = screen.getByTestId("mobile-roadmap-edit-RM-001");
922
+ fireEvent.click(editBtn);
923
+
924
+ // Should show the mobile header (roadmap selected) and inline edit
925
+ await waitFor(() => {
926
+ expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
927
+ expect(screen.getByTestId("roadmap-title-input")).toBeInTheDocument();
928
+ });
929
+ });
930
+
931
+ it("mobile roadmap list has scrollable container structure", async () => {
932
+ render(<RoadmapsView addToast={mockAddToast} />);
933
+
934
+ await waitFor(() => {
935
+ expect(screen.getByTestId("roadmaps-view__mobile-list")).toBeInTheDocument();
936
+ });
937
+
938
+ // The mobile list should have the scrollable container
939
+ const mobileList = screen.getByTestId("roadmaps-view__mobile-list");
940
+ expect(mobileList).toBeInTheDocument();
941
+
942
+ // The list items container should exist and be scrollable
943
+ const listItems = mobileList.querySelector(".roadmaps-view__mobile-list-items");
944
+ expect(listItems).toBeInTheDocument();
945
+ });
946
+
947
+ it("selecting roadmap on mobile shows detail view with header and milestone content", async () => {
948
+ render(<RoadmapsView addToast={mockAddToast} />);
949
+
950
+ await waitFor(() => {
951
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
952
+ });
953
+
954
+ // Click on a roadmap item
955
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
956
+
957
+ // Should show the mobile header with back button
958
+ await waitFor(() => {
959
+ expect(screen.getByTestId("roadmaps-view__mobile-header")).toBeInTheDocument();
960
+ expect(screen.getByTestId("mobile-back-btn")).toBeInTheDocument();
961
+ });
962
+
963
+ // Should show milestone content
964
+ expect(screen.getByText("Milestone 1")).toBeInTheDocument();
965
+ expect(screen.getByText("Milestone 2")).toBeInTheDocument();
966
+ });
967
+ });
968
+
969
+ describe("Mobile suggestion panel collapse", () => {
970
+ beforeEach(() => {
971
+ mockViewport("mobile");
972
+ });
973
+
974
+ afterEach(() => {
975
+ vi.restoreAllMocks();
976
+ });
977
+
978
+ it("shows expand button instead of suggestion section on mobile", async () => {
979
+ render(<RoadmapsView addToast={mockAddToast} />);
980
+
981
+ await waitFor(() => {
982
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
983
+ });
984
+
985
+ // Select roadmap
986
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
987
+
988
+ // On mobile, should show the expand button instead of the goal prompt input
989
+ await waitFor(() => {
990
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
991
+ });
992
+ expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
993
+ });
994
+
995
+ it("expands suggestion panel on mobile when button is clicked", async () => {
996
+ render(<RoadmapsView addToast={mockAddToast} />);
997
+
998
+ await waitFor(() => {
999
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
1000
+ });
1001
+
1002
+ // Select roadmap
1003
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
1004
+
1005
+ // Expand the panel
1006
+ await waitFor(() => {
1007
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1008
+ });
1009
+ fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
1010
+
1011
+ // Panel should now be visible
1012
+ await waitFor(() => {
1013
+ expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
1014
+ });
1015
+ expect(screen.queryByTestId("expand-suggestion-panel-btn")).not.toBeInTheDocument();
1016
+ });
1017
+
1018
+ it("can collapse suggestion panel on mobile", async () => {
1019
+ render(<RoadmapsView addToast={mockAddToast} />);
1020
+
1021
+ await waitFor(() => {
1022
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
1023
+ });
1024
+
1025
+ // Select roadmap
1026
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
1027
+
1028
+ // Expand the panel
1029
+ await waitFor(() => {
1030
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1031
+ });
1032
+ fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
1033
+
1034
+ // Wait for panel to expand
1035
+ await waitFor(() => {
1036
+ expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
1037
+ });
1038
+
1039
+ // Collapse the panel
1040
+ fireEvent.click(screen.getByTestId("collapse-suggestion-panel-btn"));
1041
+
1042
+ // Panel should be hidden, expand button should be back
1043
+ await waitFor(() => {
1044
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1045
+ });
1046
+ expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
1047
+ });
1048
+
1049
+ it("persists goal prompt and suggestions across collapse/expand on mobile", async () => {
1050
+ // Mock milestone suggestion generation
1051
+ (api.generateMilestoneSuggestions as ReturnType<typeof vi.fn>).mockResolvedValue({
1052
+ suggestions: [
1053
+ { title: "Persisted Milestone", description: "Persisted description" },
1054
+ ],
1055
+ });
1056
+
1057
+ render(<RoadmapsView addToast={mockAddToast} />);
1058
+
1059
+ await waitFor(() => {
1060
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
1061
+ });
1062
+
1063
+ // Select roadmap
1064
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
1065
+
1066
+ // Expand the panel
1067
+ await waitFor(() => {
1068
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1069
+ });
1070
+ fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
1071
+
1072
+ // Type into goal prompt
1073
+ await waitFor(() => {
1074
+ expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
1075
+ });
1076
+ const goalInput = screen.getByTestId("goal-prompt-input");
1077
+ await userEvent.type(goalInput, "Build an app");
1078
+
1079
+ // Generate suggestions
1080
+ fireEvent.click(screen.getByTestId("generate-suggestions-btn"));
1081
+
1082
+ // Wait for suggestions to appear
1083
+ await waitFor(() => {
1084
+ expect(screen.getByText("Persisted Milestone")).toBeInTheDocument();
1085
+ });
1086
+
1087
+ // Collapse the panel
1088
+ fireEvent.click(screen.getByTestId("collapse-suggestion-panel-btn"));
1089
+
1090
+ // Wait for expand button to appear
1091
+ await waitFor(() => {
1092
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1093
+ });
1094
+
1095
+ // Re-expand the panel
1096
+ fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
1097
+
1098
+ // Goal prompt and suggestions should persist
1099
+ await waitFor(() => {
1100
+ expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
1101
+ });
1102
+ expect(screen.getByTestId("goal-prompt-input")).toHaveValue("Build an app");
1103
+ expect(screen.getByText("Persisted Milestone")).toBeInTheDocument();
1104
+ });
1105
+
1106
+ it("resets suggestion panel when switching roadmaps on mobile", async () => {
1107
+ render(<RoadmapsView addToast={mockAddToast} />);
1108
+
1109
+ await waitFor(() => {
1110
+ expect(screen.getByTestId("mobile-roadmap-item-RM-001")).toBeInTheDocument();
1111
+ });
1112
+
1113
+ // Select RM-001
1114
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-001"));
1115
+
1116
+ // Expand the panel
1117
+ await waitFor(() => {
1118
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1119
+ });
1120
+ fireEvent.click(screen.getByTestId("expand-suggestion-panel-btn"));
1121
+
1122
+ await waitFor(() => {
1123
+ expect(screen.getByTestId("goal-prompt-input")).toBeInTheDocument();
1124
+ });
1125
+
1126
+ // Go back to roadmap list
1127
+ fireEvent.click(screen.getByTestId("mobile-back-btn"));
1128
+
1129
+ // Wait for list to appear
1130
+ await waitFor(() => {
1131
+ expect(screen.getByTestId("mobile-roadmap-item-RM-002")).toBeInTheDocument();
1132
+ });
1133
+
1134
+ // Switch to RM-002
1135
+ fireEvent.click(screen.getByTestId("mobile-roadmap-item-RM-002"));
1136
+
1137
+ // Panel should be collapsed (expand button visible)
1138
+ await waitFor(() => {
1139
+ expect(screen.getByTestId("expand-suggestion-panel-btn")).toBeInTheDocument();
1140
+ });
1141
+ expect(screen.queryByTestId("goal-prompt-input")).not.toBeInTheDocument();
1142
+ });
1143
+ });
1144
+ });