@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,128 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import type { Task } from "@fusion/core";
4
+ import { DependencyGraph } from "../DependencyGraph";
5
+
6
+ const fitToGraph = vi.fn();
7
+ const zoomIn = vi.fn();
8
+ const zoomOut = vi.fn();
9
+ const resetView = vi.fn();
10
+ const handleKeyDown = vi.fn();
11
+
12
+ vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
13
+ TaskCard: ({ task, onOpenDetail, disableDrag }: { task: Task; onOpenDetail: (task: Task) => void; disableDrag?: boolean }) => (
14
+ <button data-testid={`task-${task.id}`} draggable={!disableDrag} onClick={() => onOpenDetail(task)}>{task.id}</button>
15
+ ),
16
+ }));
17
+
18
+ vi.mock("../useGraphInteraction", () => ({
19
+ useGraphInteraction: () => ({
20
+ transform: "translate(0px, 0px) scale(1)",
21
+ zoom: 1,
22
+ transitioning: false,
23
+ zoomIn,
24
+ zoomOut,
25
+ resetView,
26
+ fitToGraph,
27
+ onPointerDown: vi.fn(),
28
+ onPointerMove: vi.fn(),
29
+ onPointerUp: vi.fn(),
30
+ onWheelZoom: vi.fn(),
31
+ handleKeyDown,
32
+ }),
33
+ }));
34
+
35
+ function createTask(id: string, column: Task["column"], dependencies: string[] = []): Task {
36
+ return { id, description: id, column, dependencies, steps: [], currentStep: 0, log: [] } as Task;
37
+ }
38
+
39
+ describe("DependencyGraph", () => {
40
+ beforeEach(() => {
41
+ fitToGraph.mockReset();
42
+ zoomIn.mockReset();
43
+ zoomOut.mockReset();
44
+ resetView.mockReset();
45
+ handleKeyDown.mockReset();
46
+ });
47
+
48
+ afterEach(() => {
49
+ cleanup();
50
+ });
51
+
52
+ it("renders empty state for empty list", () => {
53
+ render(<DependencyGraph tasks={[]} onOpenTaskDetail={vi.fn()} />);
54
+ expect(screen.getByText(/No active tasks/i)).toBeTruthy();
55
+ });
56
+
57
+ it("renders only triage/todo/in-progress/in-review nodes from mixed columns", () => {
58
+ render(
59
+ <DependencyGraph
60
+ tasks={[
61
+ createTask("A", "triage"),
62
+ createTask("B", "todo"),
63
+ createTask("C", "in-progress"),
64
+ createTask("D", "in-review"),
65
+ createTask("E", "done"),
66
+ createTask("F", "archived"),
67
+ ]}
68
+ onOpenTaskDetail={vi.fn()}
69
+ />,
70
+ );
71
+
72
+ expect(screen.getByTestId("graph-task-node-A")).toBeTruthy();
73
+ expect(screen.getByTestId("graph-task-node-B")).toBeTruthy();
74
+ expect(screen.getByTestId("graph-task-node-C")).toBeTruthy();
75
+ expect(screen.getByTestId("graph-task-node-D")).toBeTruthy();
76
+ expect(screen.queryByTestId("graph-task-node-E")).toBeNull();
77
+ expect(screen.queryByTestId("graph-task-node-F")).toBeNull();
78
+ });
79
+
80
+ it("auto-fits on initial load with active tasks", () => {
81
+ render(<DependencyGraph tasks={[createTask("A", "todo")]} onOpenTaskDetail={vi.fn()} />);
82
+ expect(fitToGraph).toHaveBeenCalled();
83
+ });
84
+
85
+ it("forwards keyboard events to interaction hook", () => {
86
+ render(<DependencyGraph tasks={[createTask("A", "todo")]} onOpenTaskDetail={vi.fn()} />);
87
+ const viewport = document.querySelector(".dependency-graph__viewport");
88
+ if (!viewport) throw new Error("missing viewport");
89
+ fireEvent.keyDown(viewport, { key: "=", ctrlKey: true });
90
+ expect(handleKeyDown).toHaveBeenCalled();
91
+ });
92
+
93
+ it("sets viewport tabIndex for keyboard focus", () => {
94
+ render(<DependencyGraph tasks={[createTask("A", "todo")]} onOpenTaskDetail={vi.fn()} />);
95
+ const viewport = document.querySelector(".dependency-graph__viewport");
96
+ expect(viewport?.getAttribute("tabindex")).toBe("0");
97
+ });
98
+
99
+ it("renders toolbar controls", () => {
100
+ render(<DependencyGraph tasks={[createTask("A", "todo")]} onOpenTaskDetail={vi.fn()} />);
101
+ expect(screen.getByRole("button", { name: "Zoom in" })).toBeTruthy();
102
+ expect(screen.getByRole("button", { name: "Zoom out" })).toBeTruthy();
103
+ expect(screen.getByRole("button", { name: "Fit to graph" })).toBeTruthy();
104
+ expect(screen.getByRole("button", { name: "Reset view" })).toBeTruthy();
105
+ expect(screen.getByText("100%")).toBeTruthy();
106
+ });
107
+
108
+ it("fit-to-graph button triggers fitToGraph", () => {
109
+ render(<DependencyGraph tasks={[createTask("A", "todo")]} onOpenTaskDetail={vi.fn()} />);
110
+ fireEvent.click(screen.getByRole("button", { name: "Fit to graph" }));
111
+ expect(fitToGraph).toHaveBeenCalled();
112
+ });
113
+
114
+ it("clicking a card triggers onOpenDetail exactly once", () => {
115
+ const onOpenDetail = vi.fn();
116
+ render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenDetail={onOpenDetail} />);
117
+ fireEvent.click(screen.getByTestId("task-A"));
118
+ expect(onOpenDetail).toHaveBeenCalledTimes(1);
119
+ expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "A" }));
120
+ });
121
+
122
+ it("falls back to onOpenTaskDetail when onOpenDetail is not provided", () => {
123
+ const onOpenTaskDetail = vi.fn();
124
+ render(<DependencyGraph tasks={[createTask("A", "in-progress")]} onOpenTaskDetail={onOpenTaskDetail} />);
125
+ fireEvent.click(screen.getByTestId("task-A"));
126
+ expect(onOpenTaskDetail).toHaveBeenCalledWith("A");
127
+ });
128
+ });
@@ -0,0 +1,82 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import type React from "react";
4
+ import type { Task } from "@fusion/core";
5
+ import { GraphTaskNode } from "../GraphTaskNode";
6
+
7
+ function task(id = "FN-1"): Task {
8
+ return { id, description: id, column: "todo", dependencies: [], steps: [], currentStep: 0, log: [] } as Task;
9
+ }
10
+
11
+ function props(overrides: Partial<React.ComponentProps<typeof GraphTaskNode>> = {}): React.ComponentProps<typeof GraphTaskNode> {
12
+ return {
13
+ task: task(),
14
+ projectId: "p1",
15
+ position: { x: 0, y: 0 },
16
+ scale: 1,
17
+ isHighlighted: false,
18
+ isDimmed: false,
19
+ onNodePositionChange: vi.fn(),
20
+ onNodeDragStateChange: vi.fn(),
21
+ onOpenDetail: vi.fn(),
22
+ addToast: vi.fn(),
23
+ onUpdateTask: vi.fn(),
24
+ onArchiveTask: vi.fn(),
25
+ onUnarchiveTask: vi.fn(),
26
+ onDeleteTask: vi.fn(),
27
+ onRetryTask: vi.fn(),
28
+ onOpenDetailWithTab: vi.fn(),
29
+ onMoveTask: vi.fn(),
30
+ onOpenMission: vi.fn(),
31
+ taskStuckTimeoutMs: 1000,
32
+ lastFetchTimeMs: Date.now(),
33
+ workflowStepNameLookup: new Map<string, string>(),
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ afterEach(() => {
39
+ cleanup();
40
+ });
41
+
42
+ describe("GraphTaskNode drag", () => {
43
+ it("does not open detail after drag threshold is exceeded", () => {
44
+ const onOpenDetail = vi.fn();
45
+ render(<GraphTaskNode {...props({ onOpenDetail })} />);
46
+ const node = screen.getByTestId("graph-task-node-FN-1");
47
+
48
+ fireEvent.pointerDown(node, { pointerId: 1, clientX: 10, clientY: 10, isPrimary: true });
49
+ fireEvent.pointerMove(node, { pointerId: 1, clientX: 25, clientY: 25, isPrimary: true });
50
+ fireEvent.pointerUp(node, { pointerId: 1, clientX: 25, clientY: 25, isPrimary: true });
51
+ fireEvent.click(node);
52
+
53
+ expect(onOpenDetail).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("applies dragging class only after threshold move", () => {
57
+ const onNodePositionChange = vi.fn();
58
+ render(<GraphTaskNode {...props({ onNodePositionChange })} />);
59
+ const node = screen.getByTestId("graph-task-node-FN-1");
60
+
61
+ fireEvent.pointerDown(node, { pointerId: 1, clientX: 10, clientY: 10, isPrimary: true });
62
+ fireEvent.pointerMove(node, { pointerId: 1, clientX: 12, clientY: 12, isPrimary: true });
63
+ expect(node.className).not.toContain("graph-node--dragging");
64
+
65
+ fireEvent.pointerMove(node, { pointerId: 1, clientX: 20, clientY: 20, isPrimary: true });
66
+ expect(node.className).toContain("graph-node--dragging");
67
+ expect(onNodePositionChange).toHaveBeenCalled();
68
+
69
+ fireEvent.pointerUp(node, { pointerId: 1, clientX: 20, clientY: 20, isPrimary: true });
70
+ expect(node.className).not.toContain("graph-node--dragging");
71
+ });
72
+
73
+ it("composes highlight and dragging classes", () => {
74
+ render(<GraphTaskNode {...props({ isHighlighted: true })} />);
75
+ const node = screen.getByTestId("graph-task-node-FN-1");
76
+ fireEvent.pointerDown(node, { pointerId: 1, clientX: 0, clientY: 0, isPrimary: true });
77
+ fireEvent.pointerMove(node, { pointerId: 1, clientX: 10, clientY: 10, isPrimary: true });
78
+
79
+ expect(node.className).toContain("graph-task-node--highlighted");
80
+ expect(node.className).toContain("graph-node--dragging");
81
+ });
82
+ });
@@ -0,0 +1,307 @@
1
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2
+ import type { Task } from "@fusion/core";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
5
+ import { GraphTaskNode } from "../GraphTaskNode";
6
+
7
+ function createTask(overrides: Partial<Task> = {}): Task {
8
+ return {
9
+ id: "FN-TEST",
10
+ description: "Task description",
11
+ column: "todo",
12
+ dependencies: [],
13
+ steps: [],
14
+ currentStep: 0,
15
+ log: [],
16
+ ...overrides,
17
+ } as Task;
18
+ }
19
+
20
+ function createProps(task: Task) {
21
+ return {
22
+ task,
23
+ position: { x: 0, y: 0 },
24
+ scale: 1,
25
+ onNodePositionChange: vi.fn(),
26
+ onNodeDragStateChange: vi.fn(),
27
+ projectId: "proj-1",
28
+ onOpenDetail: vi.fn(),
29
+ addToast: vi.fn(),
30
+ onUpdateTask: vi.fn(),
31
+ onArchiveTask: vi.fn(),
32
+ onUnarchiveTask: vi.fn(),
33
+ onDeleteTask: vi.fn(),
34
+ onRetryTask: vi.fn(),
35
+ onOpenDetailWithTab: vi.fn(),
36
+ onMoveTask: vi.fn(),
37
+ onOpenMission: vi.fn(),
38
+ taskStuckTimeoutMs: 60_000,
39
+ lastFetchTimeMs: Date.now(),
40
+ workflowStepNameLookup: new Map<string, string>(),
41
+ };
42
+ }
43
+
44
+ afterEach(() => {
45
+ cleanup();
46
+ });
47
+
48
+ describe("GraphTaskNode", () => {
49
+ it("renders a TaskCard and passes core props through", () => {
50
+ const props = createProps(createTask());
51
+ const { container } = render(<GraphTaskNode {...props} style={{ left: 10, top: 20 }} />);
52
+
53
+ const node = screen.getByTestId("graph-task-node-FN-TEST");
54
+ expect(node).toBeTruthy();
55
+ expect(container.querySelector(".card-title")?.textContent).toContain("Task description");
56
+ expect(node.getAttribute("draggable")).toBe("false");
57
+ expect(container.querySelector(".card")?.getAttribute("draggable")).toBe("false");
58
+ });
59
+
60
+ it("shows active indicator with capitalized status for in-progress executing tasks", () => {
61
+ const props = createProps(
62
+ createTask({
63
+ column: "in-progress",
64
+ status: "executing",
65
+ steps: [{ name: "step one", status: "in-progress" }],
66
+ }),
67
+ );
68
+
69
+ const { container } = render(<GraphTaskNode {...props} />);
70
+ const node = screen.getByTestId("graph-task-node-FN-TEST");
71
+ expect(node.className).toContain("graph-task-node--active");
72
+ expect(container.querySelector(".card")?.className).toContain("agent-active");
73
+ expect(screen.getByText("Executing")).toBeTruthy();
74
+ expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
75
+ });
76
+
77
+ it("defaults indicator text to Executing when status is missing on in-progress tasks", () => {
78
+ const props = createProps(createTask({ column: "in-progress", status: undefined }));
79
+ const { container } = render(<GraphTaskNode {...props} />);
80
+
81
+ expect(container.querySelector(".graph-task-active-indicator")).toBeTruthy();
82
+ expect(screen.getByText("Executing")).toBeTruthy();
83
+ });
84
+
85
+ it("applies in-review visual class and does not apply active class for in-review tasks", () => {
86
+ const props = createProps(createTask({ column: "in-review", status: "idle" }));
87
+ const { container } = render(<GraphTaskNode {...props} />);
88
+
89
+ const node = screen.getByTestId("graph-task-node-FN-TEST");
90
+ expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
91
+ expect(node.className).toContain("graph-task-node--in-review");
92
+ expect(node.className).not.toContain("graph-task-node--active");
93
+ });
94
+
95
+ it.each(["todo", "triage", "in-progress"] as const)("does not apply in-review class for %s tasks", (column) => {
96
+ const props = createProps(createTask({ column, status: column === "in-progress" ? "executing" : "idle" }));
97
+
98
+ render(<GraphTaskNode {...props} />);
99
+ expect(screen.getByTestId("graph-task-node-FN-TEST").className).not.toContain("graph-task-node--in-review");
100
+ });
101
+
102
+ it("does not render active indicator for paused in-progress tasks", () => {
103
+ const props = createProps(createTask({ column: "in-progress", status: "executing", paused: true }));
104
+ const { container } = render(<GraphTaskNode {...props} />);
105
+
106
+ expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
107
+ });
108
+
109
+ it("does not render active indicator for failed in-progress tasks", () => {
110
+ const props = createProps(createTask({ column: "in-progress", status: "failed" }));
111
+ const { container } = render(<GraphTaskNode {...props} />);
112
+
113
+ expect(container.querySelector(".graph-task-active-indicator")).toBeFalsy();
114
+ });
115
+
116
+ it("sets current-step attribute for active task when current step is valid", () => {
117
+ const props = createProps(
118
+ createTask({
119
+ column: "in-progress",
120
+ status: "executing",
121
+ steps: [
122
+ { name: "step one", status: "done" },
123
+ { name: "step two", status: "done" },
124
+ { name: "step three", status: "in-progress" },
125
+ ],
126
+ currentStep: 2,
127
+ }),
128
+ );
129
+
130
+ render(<GraphTaskNode {...props} />);
131
+ expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("2");
132
+ });
133
+
134
+ it("sets current-step attribute to zero when first step is active", () => {
135
+ const props = createProps(
136
+ createTask({
137
+ column: "in-progress",
138
+ status: "executing",
139
+ steps: [{ name: "step one", status: "in-progress" }],
140
+ currentStep: 0,
141
+ }),
142
+ );
143
+
144
+ render(<GraphTaskNode {...props} />);
145
+ expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("0");
146
+ });
147
+
148
+ it("omits current-step attribute when current step is out of bounds", () => {
149
+ const props = createProps(
150
+ createTask({
151
+ column: "in-progress",
152
+ status: "executing",
153
+ steps: [{ name: "step one", status: "in-progress" }],
154
+ currentStep: 10,
155
+ }),
156
+ );
157
+
158
+ render(<GraphTaskNode {...props} />);
159
+ expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
160
+ });
161
+
162
+ it("omits current-step attribute when current step is negative", () => {
163
+ const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: -1 }));
164
+
165
+ render(<GraphTaskNode {...props} />);
166
+ expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
167
+ });
168
+
169
+ it("omits current-step attribute when current step is undefined", () => {
170
+ const props = createProps(createTask({ column: "in-progress", status: "executing", steps: [], currentStep: undefined }));
171
+
172
+ render(<GraphTaskNode {...props} />);
173
+ expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
174
+ });
175
+
176
+ it("does not set current-step for non-active tasks", () => {
177
+ const props = createProps(
178
+ createTask({
179
+ column: "todo",
180
+ status: "queued",
181
+ steps: [{ name: "step one", status: "in-progress" }],
182
+ currentStep: 0,
183
+ }),
184
+ );
185
+
186
+ render(<GraphTaskNode {...props} />);
187
+ expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
188
+ });
189
+
190
+ it("sets current-step to native step index when workflow steps are present", () => {
191
+ const props = createProps(
192
+ createTask({
193
+ column: "in-progress",
194
+ status: "executing",
195
+ steps: [
196
+ { id: "native-1", name: "native one", status: "done" },
197
+ { id: "native-2", name: "native two", status: "in-progress" },
198
+ ],
199
+ enabledWorkflowSteps: ["wf-1"],
200
+ currentStep: 1,
201
+ }),
202
+ );
203
+
204
+ render(<GraphTaskNode {...props} />);
205
+ expect(screen.getByTestId("graph-task-node-FN-TEST").getAttribute("data-current-step")).toBe("1");
206
+ });
207
+
208
+ it("does not set current-step when native step list is empty even with workflow steps", () => {
209
+ const props = createProps(
210
+ createTask({
211
+ column: "in-progress",
212
+ status: "executing",
213
+ steps: [],
214
+ enabledWorkflowSteps: ["wf-1"],
215
+ currentStep: 0,
216
+ }),
217
+ );
218
+
219
+ render(<GraphTaskNode {...props} />);
220
+ expect(screen.getByTestId("graph-task-node-FN-TEST").hasAttribute("data-current-step")).toBe(false);
221
+ });
222
+
223
+ it("clicking card opens task detail exactly once", () => {
224
+ const props = createProps(createTask());
225
+ const { container } = render(<GraphTaskNode {...props} />);
226
+
227
+ const card = container.querySelector(".card");
228
+ expect(card).toBeTruthy();
229
+ fireEvent.click(card!);
230
+ expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
231
+ expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
232
+ });
233
+
234
+ it("clicking active indicator surface opens task detail", () => {
235
+ const props = createProps(createTask({ column: "in-progress", status: "executing" }));
236
+ const { container } = render(<GraphTaskNode {...props} />);
237
+
238
+ const indicator = container.querySelector(".graph-task-active-indicator");
239
+ expect(indicator).toBeTruthy();
240
+ fireEvent.click(indicator!);
241
+
242
+ expect(props.onOpenDetail).toHaveBeenCalledTimes(1);
243
+ expect(props.onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "FN-TEST" }));
244
+ });
245
+
246
+ it("applies highlighted class only when requested", () => {
247
+ const highlightedProps = createProps(createTask({ id: "FN-HL" }));
248
+ const neutralProps = createProps(createTask({ id: "FN-NEUTRAL" }));
249
+
250
+ const { unmount } = render(<GraphTaskNode {...highlightedProps} isHighlighted={true} />);
251
+ expect(screen.getByTestId("graph-task-node-FN-HL").className).toContain("graph-task-node--highlighted");
252
+ unmount();
253
+
254
+ render(<GraphTaskNode {...neutralProps} />);
255
+ const neutral = screen.getByTestId("graph-task-node-FN-NEUTRAL");
256
+ expect(neutral.className).not.toContain("graph-task-node--highlighted");
257
+ expect(neutral.className).not.toContain("graph-task-node--dimmed");
258
+ });
259
+
260
+ it("renders the same TaskCard structure as board usage", () => {
261
+ const task = createTask({
262
+ id: "FN-SAME",
263
+ column: "in-progress",
264
+ status: "executing",
265
+ error: "Execution failed",
266
+ missionId: "M-1",
267
+ sourceType: "automation",
268
+ sourceAgentId: "agent-1",
269
+ steps: [{ name: "sync", status: "in-progress" }],
270
+ currentStep: 0,
271
+ });
272
+ const props = createProps(task);
273
+
274
+ const { container } = render(
275
+ <div>
276
+ <TaskCard {...props} disableDrag={true} />
277
+ <GraphTaskNode {...props} />
278
+ </div>,
279
+ );
280
+
281
+ const cards = container.querySelectorAll(".card");
282
+ expect(cards.length).toBe(2);
283
+
284
+ const [boardCard, graphCard] = cards;
285
+ const selectors = [
286
+ ".card-id",
287
+ ".card-title",
288
+ ".card-status-badge",
289
+ ".card-step-dot",
290
+ ".card-step-name",
291
+ ".card-progress",
292
+ ".card-progress-fill",
293
+ ".card-error",
294
+ ".card-mission-badge",
295
+ ".card-provider-icons",
296
+ ".card-agent-badge",
297
+ ];
298
+
299
+ for (const selector of selectors) {
300
+ expect(Boolean(boardCard.querySelector(selector))).toBe(Boolean(graphCard.querySelector(selector)));
301
+ }
302
+
303
+ expect(Boolean(boardCard.querySelector(".card-error"))).toBe(Boolean(graphCard.querySelector(".card-error")));
304
+ expect(boardCard.querySelector(".card-id")?.textContent).toBe(graphCard.querySelector(".card-id")?.textContent);
305
+ expect(boardCard.querySelector(".card-title")?.textContent).toBe(graphCard.querySelector(".card-title")?.textContent);
306
+ });
307
+ });
@@ -0,0 +1,60 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
3
+ import { GraphToolbar } from "../GraphToolbar";
4
+
5
+ describe("GraphToolbar", () => {
6
+ afterEach(() => {
7
+ cleanup();
8
+ });
9
+ it("renders controls and zoom percent", () => {
10
+ render(
11
+ <GraphToolbar
12
+ zoom={1.25}
13
+ onZoomIn={vi.fn()}
14
+ onZoomOut={vi.fn()}
15
+ onFitToGraph={vi.fn()}
16
+ onResetView={vi.fn()}
17
+ />,
18
+ );
19
+
20
+ expect(screen.getByRole("button", { name: "Zoom in" })).toBeTruthy();
21
+ expect(screen.getByRole("button", { name: "Zoom out" })).toBeTruthy();
22
+ expect(screen.getByRole("button", { name: "Fit to graph" })).toBeTruthy();
23
+ expect(screen.getByRole("button", { name: "Reset view" })).toBeTruthy();
24
+ expect(screen.getByText("125%")).toBeTruthy();
25
+ });
26
+
27
+ it("fires callbacks", () => {
28
+ const onZoomIn = vi.fn();
29
+ const onZoomOut = vi.fn();
30
+ const onFitToGraph = vi.fn();
31
+ const onResetView = vi.fn();
32
+
33
+ render(
34
+ <GraphToolbar
35
+ zoom={1}
36
+ onZoomIn={onZoomIn}
37
+ onZoomOut={onZoomOut}
38
+ onFitToGraph={onFitToGraph}
39
+ onResetView={onResetView}
40
+ />,
41
+ );
42
+
43
+ fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
44
+ fireEvent.click(screen.getByRole("button", { name: "Zoom out" }));
45
+ fireEvent.click(screen.getByRole("button", { name: "Fit to graph" }));
46
+ fireEvent.click(screen.getByRole("button", { name: "Reset view" }));
47
+
48
+ expect(onZoomIn).toHaveBeenCalledOnce();
49
+ expect(onZoomOut).toHaveBeenCalledOnce();
50
+ expect(onFitToGraph).toHaveBeenCalledOnce();
51
+ expect(onResetView).toHaveBeenCalledOnce();
52
+ });
53
+
54
+ it("applies toolbar class", () => {
55
+ render(
56
+ <GraphToolbar zoom={1} onZoomIn={vi.fn()} onZoomOut={vi.fn()} onFitToGraph={vi.fn()} onResetView={vi.fn()} />,
57
+ );
58
+ expect(screen.getByTestId("graph-toolbar").className).toContain("graph-toolbar");
59
+ });
60
+ });
@@ -0,0 +1,75 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import { GraphEdges } from "../edges";
4
+ import type { GraphEdge } from "../types";
5
+
6
+ function renderEdges(edges: GraphEdge[], highlightedEdgeIds?: Set<string>) {
7
+ const positions = new Map([
8
+ ["A", { x: 0, y: 0 }],
9
+ ["B", { x: 320, y: 180 }],
10
+ ["C", { x: 640, y: 180 }],
11
+ ]);
12
+
13
+ return render(
14
+ <GraphEdges
15
+ edges={edges}
16
+ positions={positions}
17
+ highlightedEdgeIds={highlightedEdgeIds}
18
+ />,
19
+ );
20
+ }
21
+
22
+ describe("GraphEdges", () => {
23
+ afterEach(() => {
24
+ cleanup();
25
+ });
26
+ it("renders single edge", () => {
27
+ renderEdges([{ source: "A", target: "B" }]);
28
+ const edge = screen.getAllByTestId("dependency-edge")[0];
29
+ expect(edge.getAttribute("opacity")).toBe("1");
30
+ expect(edge.getAttribute("stroke")).toBe("var(--border)");
31
+ });
32
+
33
+ it("renders multiple edges", () => {
34
+ renderEdges([
35
+ { source: "A", target: "B" },
36
+ { source: "A", target: "C" },
37
+ ]);
38
+ expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
39
+ });
40
+
41
+ it("supports edges with same source", () => {
42
+ renderEdges([
43
+ { source: "A", target: "B" },
44
+ { source: "A", target: "C" },
45
+ ]);
46
+ expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
47
+ });
48
+
49
+ it("supports edges with same target", () => {
50
+ renderEdges([
51
+ { source: "B", target: "A" },
52
+ { source: "C", target: "A" },
53
+ ]);
54
+ expect(screen.getAllByTestId("dependency-edge")).toHaveLength(2);
55
+ });
56
+
57
+ it("dims non-highlighted edges when highlight set provided", () => {
58
+ renderEdges(
59
+ [
60
+ { source: "A", target: "B" },
61
+ { source: "A", target: "C" },
62
+ ],
63
+ new Set(["A->B"]),
64
+ );
65
+
66
+ const all = screen.getAllByTestId("dependency-edge");
67
+ const highlighted = all.find((edge) => edge.getAttribute("data-edge-id") === "A->B");
68
+ const dimmed = all.find((edge) => edge.getAttribute("data-edge-id") === "A->C");
69
+
70
+ expect(highlighted?.getAttribute("opacity")).toBe("1");
71
+ expect(highlighted?.getAttribute("class") ?? "").toContain("graph-edge--highlighted");
72
+ expect(dimmed?.getAttribute("opacity")).toBe("0.15");
73
+ expect(dimmed?.getAttribute("class") ?? "").toContain("graph-edge--dimmed");
74
+ });
75
+ });