@marktoflow/gui 2.0.0-alpha.5 → 2.0.2

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 (162) hide show
  1. package/README.md +48 -180
  2. package/dist/client/assets/index-DQeR1ew6.css +1 -0
  3. package/dist/client/assets/index-LbIVPHbD.js +833 -0
  4. package/dist/client/assets/index-LbIVPHbD.js.map +1 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/client/marktoflow-logo.png +0 -0
  7. package/dist/server/index.js +31 -5
  8. package/dist/server/index.js.map +1 -1
  9. package/dist/server/routes/admin.js +95 -0
  10. package/dist/server/routes/admin.js.map +1 -0
  11. package/dist/server/routes/ai.js +2 -2
  12. package/dist/server/routes/ai.js.map +1 -1
  13. package/dist/server/routes/collaboration.js +104 -0
  14. package/dist/server/routes/collaboration.js.map +1 -0
  15. package/dist/server/routes/execute.js +181 -14
  16. package/dist/server/routes/execute.js.map +1 -1
  17. package/dist/server/routes/form.js +160 -0
  18. package/dist/server/routes/form.js.map +1 -0
  19. package/dist/server/routes/settings.js +90 -0
  20. package/dist/server/routes/settings.js.map +1 -0
  21. package/dist/server/routes/templates.js +106 -0
  22. package/dist/server/routes/templates.js.map +1 -0
  23. package/dist/server/routes/versions.js +101 -0
  24. package/dist/server/routes/versions.js.map +1 -0
  25. package/dist/server/services/AIService.js +85 -2
  26. package/dist/server/services/AIService.js.map +1 -1
  27. package/dist/server/services/ExecutionManager.js +571 -0
  28. package/dist/server/services/ExecutionManager.js.map +1 -0
  29. package/dist/server/services/VersionService.js +65 -0
  30. package/dist/server/services/VersionService.js.map +1 -0
  31. package/dist/server/services/WorkflowService.js +8 -2
  32. package/dist/server/services/WorkflowService.js.map +1 -1
  33. package/dist/server/services/agents/copilot-provider.js +32 -0
  34. package/dist/server/services/agents/copilot-provider.js.map +1 -1
  35. package/dist/server/websocket/index.js +42 -0
  36. package/dist/server/websocket/index.js.map +1 -1
  37. package/dist/shared/constants.js +9 -0
  38. package/dist/shared/constants.js.map +1 -1
  39. package/dist/shared/settings.js +51 -0
  40. package/dist/shared/settings.js.map +1 -0
  41. package/package.json +14 -10
  42. package/public/marktoflow-logo.png +0 -0
  43. package/tests/integration/fixtures/test-workflow.md +6 -0
  44. package/.turbo/turbo-build.log +0 -42
  45. package/dist/client/assets/index-CM44OayM.js +0 -704
  46. package/dist/client/assets/index-CM44OayM.js.map +0 -1
  47. package/dist/client/assets/index-Dru63gi6.css +0 -1
  48. package/marktoflow-gui-2.0.0-alpha.5.tgz +0 -0
  49. package/playwright.config.ts +0 -27
  50. package/postcss.config.js +0 -6
  51. package/src/client/App.tsx +0 -520
  52. package/src/client/components/Canvas/Canvas.tsx +0 -425
  53. package/src/client/components/Canvas/ExecutionOverlay.tsx +0 -935
  54. package/src/client/components/Canvas/ForEachNode.tsx +0 -152
  55. package/src/client/components/Canvas/IfElseNode.tsx +0 -141
  56. package/src/client/components/Canvas/NodeContextMenu.tsx +0 -192
  57. package/src/client/components/Canvas/OutputNode.tsx +0 -111
  58. package/src/client/components/Canvas/ParallelNode.tsx +0 -157
  59. package/src/client/components/Canvas/StepNode.tsx +0 -106
  60. package/src/client/components/Canvas/SubWorkflowNode.tsx +0 -141
  61. package/src/client/components/Canvas/SwitchNode.tsx +0 -185
  62. package/src/client/components/Canvas/Toolbar.tsx +0 -227
  63. package/src/client/components/Canvas/TransformNode.tsx +0 -194
  64. package/src/client/components/Canvas/TriggerNode.tsx +0 -128
  65. package/src/client/components/Canvas/TryCatchNode.tsx +0 -164
  66. package/src/client/components/Canvas/WhileNode.tsx +0 -161
  67. package/src/client/components/Canvas/index.ts +0 -24
  68. package/src/client/components/Debug/VariableInspector.tsx +0 -148
  69. package/src/client/components/Editor/InputsEditor.tsx +0 -458
  70. package/src/client/components/Editor/NewStepWizard.tsx +0 -344
  71. package/src/client/components/Editor/StepEditor.tsx +0 -532
  72. package/src/client/components/Editor/YamlEditor.tsx +0 -160
  73. package/src/client/components/Panels/PropertiesPanel.tsx +0 -589
  74. package/src/client/components/Prompt/ChangePreview.tsx +0 -281
  75. package/src/client/components/Prompt/PromptHistoryPanel.tsx +0 -209
  76. package/src/client/components/Prompt/PromptInput.tsx +0 -110
  77. package/src/client/components/Settings/ProviderSwitcher.tsx +0 -228
  78. package/src/client/components/Sidebar/ImportDialog.tsx +0 -257
  79. package/src/client/components/Sidebar/Sidebar.tsx +0 -362
  80. package/src/client/components/common/Breadcrumb.tsx +0 -40
  81. package/src/client/components/common/Button.tsx +0 -68
  82. package/src/client/components/common/ContextMenu.tsx +0 -202
  83. package/src/client/components/common/KeyboardShortcuts.tsx +0 -149
  84. package/src/client/components/common/Modal.tsx +0 -93
  85. package/src/client/components/common/Tabs.tsx +0 -57
  86. package/src/client/components/common/ThemeToggle.tsx +0 -63
  87. package/src/client/components/index.ts +0 -32
  88. package/src/client/hooks/index.ts +0 -4
  89. package/src/client/hooks/useAIPrompt.ts +0 -108
  90. package/src/client/hooks/useCanvas.ts +0 -247
  91. package/src/client/hooks/useWebSocket.ts +0 -164
  92. package/src/client/hooks/useWorkflow.ts +0 -138
  93. package/src/client/main.tsx +0 -10
  94. package/src/client/stores/agentStore.ts +0 -109
  95. package/src/client/stores/canvasStore.ts +0 -348
  96. package/src/client/stores/editorStore.ts +0 -133
  97. package/src/client/stores/executionStore.ts +0 -502
  98. package/src/client/stores/index.ts +0 -4
  99. package/src/client/stores/layoutStore.ts +0 -103
  100. package/src/client/stores/navigationStore.ts +0 -49
  101. package/src/client/stores/promptStore.ts +0 -113
  102. package/src/client/stores/themeStore.ts +0 -75
  103. package/src/client/stores/workflowStore.ts +0 -185
  104. package/src/client/styles/globals.css +0 -452
  105. package/src/client/utils/cn.ts +0 -9
  106. package/src/client/utils/index.ts +0 -4
  107. package/src/client/utils/platform.ts +0 -46
  108. package/src/client/utils/serviceIcons.tsx +0 -97
  109. package/src/client/utils/stepValidation.ts +0 -155
  110. package/src/client/utils/workflowToGraph.ts +0 -523
  111. package/src/server/index.ts +0 -137
  112. package/src/server/routes/ai.ts +0 -91
  113. package/src/server/routes/execute.ts +0 -71
  114. package/src/server/routes/executions.ts +0 -136
  115. package/src/server/routes/tools.ts +0 -970
  116. package/src/server/routes/workflows.ts +0 -147
  117. package/src/server/services/AIService.ts +0 -105
  118. package/src/server/services/FileWatcher.ts +0 -69
  119. package/src/server/services/WorkflowService.ts +0 -601
  120. package/src/server/services/agents/claude-code-provider.ts +0 -320
  121. package/src/server/services/agents/claude-provider.ts +0 -248
  122. package/src/server/services/agents/codex-provider.ts +0 -398
  123. package/src/server/services/agents/copilot-provider.ts +0 -311
  124. package/src/server/services/agents/demo-provider.ts +0 -184
  125. package/src/server/services/agents/index.ts +0 -31
  126. package/src/server/services/agents/ollama-provider.ts +0 -267
  127. package/src/server/services/agents/prompts.ts +0 -509
  128. package/src/server/services/agents/registry.ts +0 -310
  129. package/src/server/services/agents/types.ts +0 -146
  130. package/src/server/websocket/index.ts +0 -117
  131. package/src/shared/constants.ts +0 -180
  132. package/src/shared/types.ts +0 -179
  133. package/tailwind.config.ts +0 -73
  134. package/tests/e2e/app.spec.ts +0 -90
  135. package/tests/e2e/canvas.spec.ts +0 -128
  136. package/tests/e2e/workflow.spec.ts +0 -185
  137. package/tests/integration/api.test.ts +0 -452
  138. package/tests/integration/testApp.ts +0 -31
  139. package/tests/setup.ts +0 -72
  140. package/tests/unit/ForEachNode.test.tsx +0 -308
  141. package/tests/unit/IfElseNode.test.tsx +0 -235
  142. package/tests/unit/ParallelNode.test.tsx +0 -344
  143. package/tests/unit/SwitchNode.test.tsx +0 -327
  144. package/tests/unit/TransformNode.test.tsx +0 -386
  145. package/tests/unit/TryCatchNode.test.tsx +0 -243
  146. package/tests/unit/WhileNode.test.tsx +0 -230
  147. package/tests/unit/agentStore.test.ts +0 -218
  148. package/tests/unit/canvasStore.test.ts +0 -502
  149. package/tests/unit/codexProvider.test.ts +0 -399
  150. package/tests/unit/components.test.tsx +0 -151
  151. package/tests/unit/executionStore.test.ts +0 -567
  152. package/tests/unit/layoutStore.test.ts +0 -194
  153. package/tests/unit/navigationStore.test.ts +0 -152
  154. package/tests/unit/platform.test.ts +0 -118
  155. package/tests/unit/serviceIcons.test.ts +0 -197
  156. package/tests/unit/stepValidation.test.ts +0 -226
  157. package/tests/unit/themeStore.test.ts +0 -141
  158. package/tests/unit/workflowToGraph.test.ts +0 -311
  159. package/tsconfig.json +0 -29
  160. package/tsconfig.server.json +0 -28
  161. package/vite.config.ts +0 -31
  162. package/vitest.config.ts +0 -26
@@ -1,344 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render, screen } from '@testing-library/react';
3
- import { ReactFlowProvider } from '@xyflow/react';
4
- import { ParallelNode, type ParallelNodeData } from '../../src/client/components/Canvas/ParallelNode';
5
-
6
- const createMockNode = (data: Partial<ParallelNodeData> = {}) => ({
7
- id: 'parallel-1',
8
- type: 'parallel' as const,
9
- position: { x: 0, y: 0 },
10
- data: {
11
- id: 'parallel-1',
12
- name: 'Fetch Data',
13
- branches: [
14
- { id: 'branch-1', name: 'Jira' },
15
- { id: 'branch-2', name: 'GitHub' },
16
- { id: 'branch-3', name: 'Slack' },
17
- ],
18
- maxConcurrent: 3,
19
- onError: 'stop' as const,
20
- status: 'pending' as const,
21
- ...data,
22
- },
23
- });
24
-
25
- const renderNode = (data: Partial<ParallelNodeData> = {}) => {
26
- const node = createMockNode(data);
27
- return render(
28
- <ReactFlowProvider>
29
- <ParallelNode
30
- id={node.id}
31
- type={node.type}
32
- data={node.data}
33
- selected={false}
34
- isConnectable={true}
35
- zIndex={0}
36
- positionAbsoluteX={0}
37
- positionAbsoluteY={0}
38
- dragging={false}
39
- />
40
- </ReactFlowProvider>
41
- );
42
- };
43
-
44
- describe('ParallelNode', () => {
45
- it('should render node with name and branches', () => {
46
- renderNode();
47
-
48
- expect(screen.getByText('Fetch Data')).toBeInTheDocument();
49
- expect(screen.getByText('Concurrent Execution')).toBeInTheDocument();
50
- expect(screen.getByText('Jira')).toBeInTheDocument();
51
- expect(screen.getByText('GitHub')).toBeInTheDocument();
52
- expect(screen.getByText('Slack')).toBeInTheDocument();
53
- });
54
-
55
- it('should render default name when name is not provided', () => {
56
- renderNode({ name: undefined });
57
-
58
- expect(screen.getByText('Parallel')).toBeInTheDocument();
59
- });
60
-
61
- it('should display branch count', () => {
62
- renderNode();
63
-
64
- expect(screen.getByText('Branches:')).toBeInTheDocument();
65
- const branchCounts = screen.getAllByText('3');
66
- expect(branchCounts.length).toBeGreaterThan(0);
67
- });
68
-
69
- it('should display max concurrent limit when provided', () => {
70
- renderNode({ maxConcurrent: 5 });
71
-
72
- expect(screen.getByText(/Max Concurrent:/)).toBeInTheDocument();
73
- expect(screen.getByText('5')).toBeInTheDocument();
74
- });
75
-
76
- it('should not display max concurrent when not provided', () => {
77
- renderNode({ maxConcurrent: undefined });
78
-
79
- expect(screen.queryByText(/Max Concurrent:/)).not.toBeInTheDocument();
80
- });
81
-
82
- describe('status states', () => {
83
- it('should render pending status', () => {
84
- renderNode({ status: 'pending' });
85
-
86
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
87
- expect(node).toBeInTheDocument();
88
- });
89
-
90
- it('should render running status with animation', () => {
91
- renderNode({ status: 'running' });
92
-
93
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
94
- expect(node).toHaveClass('running');
95
- });
96
-
97
- it('should render completed status', () => {
98
- renderNode({ status: 'completed' });
99
-
100
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
101
- expect(node).toBeInTheDocument();
102
- });
103
- });
104
-
105
- describe('branch status indicators', () => {
106
- it('should highlight active branches', () => {
107
- renderNode({
108
- activeBranches: ['branch-1', 'branch-2'],
109
- });
110
-
111
- const jiraBranch = screen.getByText('Jira');
112
- const githubBranch = screen.getByText('GitHub');
113
-
114
- expect(jiraBranch).toHaveClass('bg-blue-500/30', 'text-blue-200', 'animate-pulse');
115
- expect(githubBranch).toHaveClass('bg-blue-500/30', 'text-blue-200', 'animate-pulse');
116
- });
117
-
118
- it('should highlight completed branches', () => {
119
- renderNode({
120
- completedBranches: ['branch-1'],
121
- });
122
-
123
- const jiraBranch = screen.getByText('Jira');
124
- expect(jiraBranch).toHaveClass('bg-green-500/30', 'text-green-200');
125
- });
126
-
127
- it('should show inactive branches without highlighting', () => {
128
- renderNode({
129
- activeBranches: [],
130
- completedBranches: [],
131
- });
132
-
133
- const jiraBranch = screen.getByText('Jira');
134
- expect(jiraBranch).toHaveClass('bg-white/10', 'text-white/60');
135
- });
136
-
137
- it('should prioritize completed over active status', () => {
138
- renderNode({
139
- activeBranches: ['branch-1'],
140
- completedBranches: ['branch-1'],
141
- });
142
-
143
- const jiraBranch = screen.getByText('Jira');
144
- expect(jiraBranch).toHaveClass('bg-green-500/30', 'text-green-200');
145
- expect(jiraBranch).not.toHaveClass('animate-pulse');
146
- });
147
- });
148
-
149
- describe('branch display', () => {
150
- it('should display up to 6 branches', () => {
151
- renderNode({
152
- branches: [
153
- { id: 'b1', name: 'Branch 1' },
154
- { id: 'b2', name: 'Branch 2' },
155
- { id: 'b3', name: 'Branch 3' },
156
- { id: 'b4', name: 'Branch 4' },
157
- { id: 'b5', name: 'Branch 5' },
158
- { id: 'b6', name: 'Branch 6' },
159
- ],
160
- });
161
-
162
- expect(screen.getByText('Branch 1')).toBeInTheDocument();
163
- expect(screen.getByText('Branch 6')).toBeInTheDocument();
164
- expect(screen.queryByText('+1')).not.toBeInTheDocument();
165
- });
166
-
167
- it('should show "+N more" for branches beyond 6', () => {
168
- renderNode({
169
- branches: [
170
- { id: 'b1', name: 'Branch 1' },
171
- { id: 'b2', name: 'Branch 2' },
172
- { id: 'b3', name: 'Branch 3' },
173
- { id: 'b4', name: 'Branch 4' },
174
- { id: 'b5', name: 'Branch 5' },
175
- { id: 'b6', name: 'Branch 6' },
176
- { id: 'b7', name: 'Branch 7' },
177
- { id: 'b8', name: 'Branch 8' },
178
- ],
179
- });
180
-
181
- expect(screen.getByText('+2')).toBeInTheDocument();
182
- expect(screen.queryByText('Branch 7')).not.toBeInTheDocument();
183
- });
184
-
185
- it('should use branch id when name is not provided', () => {
186
- renderNode({
187
- branches: [{ id: 'branch-xyz', name: undefined }],
188
- });
189
-
190
- // Should display last 2 characters of id as "Byz"
191
- expect(screen.getByText('Byz')).toBeInTheDocument();
192
- });
193
- });
194
-
195
- describe('error handling display', () => {
196
- it('should display error handling policy', () => {
197
- renderNode({ onError: 'stop' });
198
-
199
- expect(screen.getByText('On Error:')).toBeInTheDocument();
200
- expect(screen.getByText('stop')).toBeInTheDocument();
201
- });
202
-
203
- it('should display continue policy', () => {
204
- renderNode({ onError: 'continue' });
205
-
206
- expect(screen.getByText('continue')).toBeInTheDocument();
207
- });
208
-
209
- it('should display default stop policy when not provided', () => {
210
- renderNode({ onError: undefined });
211
-
212
- expect(screen.getByText('stop')).toBeInTheDocument();
213
- });
214
- });
215
-
216
- describe('visual styling', () => {
217
- it('should have blue/cyan gradient background', () => {
218
- renderNode();
219
-
220
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
221
- expect(node).toHaveStyle({
222
- background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
223
- });
224
- });
225
- });
226
-
227
- describe('selection state', () => {
228
- it('should apply selected class when selected', () => {
229
- const node = createMockNode();
230
- const { container } = render(
231
- <ReactFlowProvider>
232
- <ParallelNode
233
- id={node.id}
234
- type={node.type}
235
- data={node.data}
236
- selected={true}
237
- isConnectable={true}
238
- zIndex={0}
239
- positionAbsoluteX={0}
240
- positionAbsoluteY={0}
241
- dragging={false}
242
- />
243
- </ReactFlowProvider>
244
- );
245
-
246
- const nodeElement = container.querySelector('.control-flow-node');
247
- expect(nodeElement).toHaveClass('selected');
248
- });
249
- });
250
-
251
- describe('empty branches', () => {
252
- it('should display 0 branches when array is empty', () => {
253
- renderNode({ branches: [] });
254
-
255
- expect(screen.getByText('0')).toBeInTheDocument();
256
- });
257
-
258
- it('should handle undefined branches', () => {
259
- renderNode({ branches: undefined });
260
-
261
- expect(screen.getByText('0')).toBeInTheDocument();
262
- });
263
- });
264
-
265
- describe('failed branches', () => {
266
- it('should highlight failed branches in red', () => {
267
- renderNode({
268
- failedBranches: ['branch-1'],
269
- });
270
-
271
- const jiraBranch = screen.getByText('Jira');
272
- expect(jiraBranch).toHaveClass('bg-red-500/30', 'text-red-200');
273
- });
274
-
275
- it('should prioritize failed over completed and active', () => {
276
- renderNode({
277
- activeBranches: ['branch-1'],
278
- completedBranches: ['branch-1'],
279
- failedBranches: ['branch-1'],
280
- });
281
-
282
- const jiraBranch = screen.getByText('Jira');
283
- expect(jiraBranch).toHaveClass('bg-red-500/30', 'text-red-200');
284
- expect(jiraBranch).not.toHaveClass('bg-green-500/30');
285
- expect(jiraBranch).not.toHaveClass('animate-pulse');
286
- });
287
- });
288
-
289
- describe('max concurrent exceeded', () => {
290
- it('should show rate limiting warning when maxConcurrentExceeded is true', () => {
291
- renderNode({
292
- maxConcurrent: 2,
293
- maxConcurrentExceeded: true,
294
- });
295
-
296
- expect(screen.getByText('Rate limiting active')).toBeInTheDocument();
297
- });
298
-
299
- it('should highlight max concurrent value in yellow when exceeded', () => {
300
- renderNode({
301
- maxConcurrent: 2,
302
- maxConcurrentExceeded: true,
303
- });
304
-
305
- const maxValue = screen.getByText('2');
306
- expect(maxValue).toHaveClass('text-yellow-300');
307
- });
308
-
309
- it('should not show warning when maxConcurrentExceeded is false', () => {
310
- renderNode({
311
- maxConcurrent: 2,
312
- maxConcurrentExceeded: false,
313
- });
314
-
315
- expect(screen.queryByText('Rate limiting active')).not.toBeInTheDocument();
316
- });
317
-
318
- it('should not highlight max concurrent value when not exceeded', () => {
319
- renderNode({
320
- maxConcurrent: 2,
321
- maxConcurrentExceeded: false,
322
- });
323
-
324
- const maxValue = screen.getByText('2');
325
- expect(maxValue).not.toHaveClass('text-yellow-300');
326
- });
327
- });
328
-
329
- describe('completed and failed states', () => {
330
- it('should show completed class on node', () => {
331
- renderNode({ status: 'completed' });
332
-
333
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
334
- expect(node).toHaveClass('completed');
335
- });
336
-
337
- it('should show failed class on node', () => {
338
- renderNode({ status: 'failed' });
339
-
340
- const node = screen.getByText('Fetch Data').closest('.control-flow-node');
341
- expect(node).toHaveClass('failed');
342
- });
343
- });
344
- });
@@ -1,327 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { render, screen } from '@testing-library/react';
3
- import { ReactFlowProvider } from '@xyflow/react';
4
- import { SwitchNode, type SwitchNodeData } from '../../src/client/components/Canvas/SwitchNode';
5
-
6
- const createMockNode = (data: Partial<SwitchNodeData> = {}) => ({
7
- id: 'switch-1',
8
- type: 'switch' as const,
9
- position: { x: 0, y: 0 },
10
- data: {
11
- id: 'switch-1',
12
- name: 'Route By Severity',
13
- expression: '{{ incident.severity }}',
14
- cases: {
15
- critical: {},
16
- high: {},
17
- medium: {},
18
- },
19
- hasDefault: true,
20
- status: 'pending' as const,
21
- ...data,
22
- },
23
- });
24
-
25
- const renderNode = (data: Partial<SwitchNodeData> = {}) => {
26
- const node = createMockNode(data);
27
- return render(
28
- <ReactFlowProvider>
29
- <SwitchNode
30
- id={node.id}
31
- type={node.type}
32
- data={node.data}
33
- selected={false}
34
- isConnectable={true}
35
- zIndex={0}
36
- positionAbsoluteX={0}
37
- positionAbsoluteY={0}
38
- dragging={false}
39
- />
40
- </ReactFlowProvider>
41
- );
42
- };
43
-
44
- describe('SwitchNode', () => {
45
- it('should render node with name, expression, and cases', () => {
46
- renderNode();
47
-
48
- expect(screen.getByText('Route By Severity')).toBeInTheDocument();
49
- expect(screen.getByText('Multi-Branch Router')).toBeInTheDocument();
50
- expect(screen.getByText('{{ incident.severity }}')).toBeInTheDocument();
51
- expect(screen.getByText('critical')).toBeInTheDocument();
52
- expect(screen.getByText('high')).toBeInTheDocument();
53
- expect(screen.getByText('medium')).toBeInTheDocument();
54
- });
55
-
56
- it('should render default name when name is not provided', () => {
57
- renderNode({ name: undefined });
58
-
59
- expect(screen.getByText('Switch')).toBeInTheDocument();
60
- });
61
-
62
- it('should display expression label', () => {
63
- renderNode();
64
-
65
- expect(screen.getByText('Expression:')).toBeInTheDocument();
66
- });
67
-
68
- it('should display "Not set" when expression is empty', () => {
69
- renderNode({ expression: '' });
70
-
71
- expect(screen.getByText('Not set')).toBeInTheDocument();
72
- });
73
-
74
- describe('status states', () => {
75
- it('should render pending status', () => {
76
- renderNode({ status: 'pending' });
77
-
78
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
79
- expect(node).toBeInTheDocument();
80
- });
81
-
82
- it('should render running status with animation', () => {
83
- renderNode({ status: 'running' });
84
-
85
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
86
- expect(node).toHaveClass('running');
87
- });
88
-
89
- it('should render completed status', () => {
90
- renderNode({ status: 'completed' });
91
-
92
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
93
- expect(node).toBeInTheDocument();
94
- });
95
- });
96
-
97
- describe('active case highlighting', () => {
98
- it('should highlight active case', () => {
99
- renderNode({ activeCase: 'critical' });
100
-
101
- const criticalCase = screen.getByText('critical');
102
- expect(criticalCase).toHaveClass('bg-purple-500/30', 'text-purple-200');
103
- });
104
-
105
- it('should not highlight inactive cases', () => {
106
- renderNode({ activeCase: 'critical' });
107
-
108
- const highCase = screen.getByText('high');
109
- expect(highCase).toHaveClass('bg-white/5', 'text-white/70');
110
- });
111
-
112
- it('should highlight default case when active', () => {
113
- renderNode({ activeCase: 'default' });
114
-
115
- const defaultCase = screen.getByText('default');
116
- expect(defaultCase).toHaveClass('bg-gray-500/30', 'text-gray-200');
117
- });
118
- });
119
-
120
- describe('case display', () => {
121
- it('should display up to 4 cases', () => {
122
- renderNode({
123
- cases: {
124
- case1: {},
125
- case2: {},
126
- case3: {},
127
- case4: {},
128
- },
129
- });
130
-
131
- expect(screen.getByText('case1')).toBeInTheDocument();
132
- expect(screen.getByText('case2')).toBeInTheDocument();
133
- expect(screen.getByText('case3')).toBeInTheDocument();
134
- expect(screen.getByText('case4')).toBeInTheDocument();
135
- expect(screen.queryByText('+1 more cases')).not.toBeInTheDocument();
136
- });
137
-
138
- it('should show "+N more cases" when more than 4 cases', () => {
139
- renderNode({
140
- cases: {
141
- case1: {},
142
- case2: {},
143
- case3: {},
144
- case4: {},
145
- case5: {},
146
- case6: {},
147
- },
148
- });
149
-
150
- expect(screen.getByText('+2 more cases')).toBeInTheDocument();
151
- expect(screen.queryByText('case5')).not.toBeInTheDocument();
152
- });
153
-
154
- it('should display case count', () => {
155
- renderNode({
156
- cases: {
157
- critical: {},
158
- high: {},
159
- medium: {},
160
- },
161
- });
162
-
163
- expect(screen.getByText('3 cases + default')).toBeInTheDocument();
164
- });
165
-
166
- it('should display singular case label when only one case', () => {
167
- renderNode({
168
- cases: {
169
- single: {},
170
- },
171
- });
172
-
173
- expect(screen.getByText('1 case + default')).toBeInTheDocument();
174
- });
175
-
176
- it('should not show "+ default" when hasDefault is false', () => {
177
- renderNode({
178
- cases: {
179
- critical: {},
180
- },
181
- hasDefault: false,
182
- });
183
-
184
- expect(screen.getByText('1 case')).toBeInTheDocument();
185
- expect(screen.queryByText('+ default')).not.toBeInTheDocument();
186
- });
187
- });
188
-
189
- describe('default case', () => {
190
- it('should display default case when hasDefault is true', () => {
191
- renderNode({ hasDefault: true });
192
-
193
- expect(screen.getByText('default')).toBeInTheDocument();
194
- });
195
-
196
- it('should not display default case when hasDefault is false', () => {
197
- renderNode({ hasDefault: false });
198
-
199
- expect(screen.queryByText('default')).not.toBeInTheDocument();
200
- });
201
- });
202
-
203
- describe('visual styling', () => {
204
- it('should have purple/magenta gradient background', () => {
205
- renderNode();
206
-
207
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
208
- expect(node).toHaveStyle({
209
- background: 'linear-gradient(135deg, #a855f7 0%, #ec4899 100%)',
210
- });
211
- });
212
-
213
- it('should display expression with monospace font', () => {
214
- renderNode();
215
-
216
- const expressionElement = screen.getByText('{{ incident.severity }}');
217
- expect(expressionElement).toHaveClass('font-mono');
218
- });
219
- });
220
-
221
- describe('selection state', () => {
222
- it('should apply selected class when selected', () => {
223
- const node = createMockNode();
224
- const { container } = render(
225
- <ReactFlowProvider>
226
- <SwitchNode
227
- id={node.id}
228
- type={node.type}
229
- data={node.data}
230
- selected={true}
231
- isConnectable={true}
232
- zIndex={0}
233
- positionAbsoluteX={0}
234
- positionAbsoluteY={0}
235
- dragging={false}
236
- />
237
- </ReactFlowProvider>
238
- );
239
-
240
- const nodeElement = container.querySelector('.control-flow-node');
241
- expect(nodeElement).toHaveClass('selected');
242
- });
243
- });
244
-
245
- describe('empty cases', () => {
246
- it('should display 0 cases when cases object is empty', () => {
247
- renderNode({ cases: {} });
248
-
249
- expect(screen.getByText('0 cases + default')).toBeInTheDocument();
250
- });
251
- });
252
-
253
- describe('skipped branches', () => {
254
- it('should show SKIPPED badge on skipped case', () => {
255
- renderNode({
256
- skippedBranches: ['high', 'medium'],
257
- activeCase: 'critical'
258
- });
259
-
260
- const skippedBadges = screen.getAllByText('SKIPPED');
261
- expect(skippedBadges).toHaveLength(2);
262
-
263
- const highCase = screen.getByText('high');
264
- expect(highCase).toHaveClass('line-through', 'text-gray-400');
265
- });
266
-
267
- it('should not show SKIPPED when no branches are skipped', () => {
268
- renderNode({ skippedBranches: [] });
269
-
270
- expect(screen.queryByText('SKIPPED')).not.toBeInTheDocument();
271
- });
272
-
273
- it('should show active case with ring highlight', () => {
274
- renderNode({ activeCase: 'critical' });
275
-
276
- const criticalCase = screen.getByText('critical');
277
- expect(criticalCase).toHaveClass('ring-1', 'ring-purple-400/50');
278
- });
279
- });
280
-
281
- describe('handle positioning', () => {
282
- it('should render output handles for all visible cases', () => {
283
- const { container } = renderNode({
284
- cases: { critical: {}, high: {}, medium: {} },
285
- hasDefault: true
286
- });
287
-
288
- // Should have 4 handles: 3 cases + 1 default
289
- const handles = container.querySelectorAll('.react-flow__handle-bottom');
290
- expect(handles.length).toBeGreaterThanOrEqual(3);
291
- });
292
-
293
- it('should position handles evenly to avoid overlap', () => {
294
- const { container } = renderNode({
295
- cases: { case1: {}, case2: {}, case3: {}, case4: {} },
296
- hasDefault: true
297
- });
298
-
299
- const handles = container.querySelectorAll('.react-flow__handle-bottom');
300
-
301
- // Each handle should have a unique position
302
- const positions = Array.from(handles).map(h =>
303
- (h as HTMLElement).style.left
304
- );
305
-
306
- // All positions should be different
307
- const uniquePositions = new Set(positions);
308
- expect(uniquePositions.size).toBe(positions.length);
309
- });
310
- });
311
-
312
- describe('completed and failed states', () => {
313
- it('should show completed class on node', () => {
314
- renderNode({ status: 'completed' });
315
-
316
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
317
- expect(node).toHaveClass('completed');
318
- });
319
-
320
- it('should show failed class on node', () => {
321
- renderNode({ status: 'failed' });
322
-
323
- const node = screen.getByText('Route By Severity').closest('.control-flow-node');
324
- expect(node).toHaveClass('failed');
325
- });
326
- });
327
- });