@allternit/workflow-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +227 -0
  2. package/dist/engine/workflow-engine.d.ts +39 -0
  3. package/dist/engine/workflow-engine.d.ts.bak +39 -0
  4. package/dist/engine/workflow-engine.d.ts.map +1 -0
  5. package/dist/engine/workflow-engine.js +532 -0
  6. package/dist/engine/workflow-engine.js.bak +532 -0
  7. package/dist/engine/workflow-engine.js.map +1 -0
  8. package/dist/index.d.ts +38 -0
  9. package/dist/index.d.ts.bak +38 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +41 -0
  12. package/dist/index.js.bak +41 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/scheduler/index.d.ts +53 -0
  15. package/dist/scheduler/index.d.ts.bak +53 -0
  16. package/dist/scheduler/index.d.ts.map +1 -0
  17. package/dist/scheduler/index.js +135 -0
  18. package/dist/scheduler/index.js.bak +135 -0
  19. package/dist/scheduler/index.js.map +1 -0
  20. package/dist/types.d.ts +402 -0
  21. package/dist/types.d.ts.bak +402 -0
  22. package/dist/types.d.ts.map +1 -0
  23. package/dist/types.js +7 -0
  24. package/dist/types.js.bak +7 -0
  25. package/dist/types.js.map +1 -0
  26. package/dist/visualizer/index.d.ts +81 -0
  27. package/dist/visualizer/index.d.ts.bak +81 -0
  28. package/dist/visualizer/index.d.ts.map +1 -0
  29. package/dist/visualizer/index.js +277 -0
  30. package/dist/visualizer/index.js.bak +277 -0
  31. package/dist/visualizer/index.js.map +1 -0
  32. package/package.json +57 -0
  33. package/src/__tests__/visualizer.test.ts.bak +110 -0
  34. package/src/__tests__/workflow-engine.test.ts.bak +239 -0
  35. package/src/engine/workflow-engine.test.ts.bak +852 -0
  36. package/src/engine/workflow-engine.ts +651 -0
  37. package/src/engine/workflow-engine.ts.bak +651 -0
  38. package/src/index.ts +89 -0
  39. package/src/index.ts.bak +89 -0
  40. package/src/scheduler/index.ts +204 -0
  41. package/src/scheduler/index.ts.bak +204 -0
  42. package/src/scheduler/scheduler.test.ts.bak +521 -0
  43. package/src/types.ts +449 -0
  44. package/src/types.ts.bak +449 -0
  45. package/src/visualizer/index.ts +409 -0
  46. package/src/visualizer/index.ts.bak +409 -0
  47. package/src/visualizer/visualizer.test.ts.bak +844 -0
  48. package/tsconfig.json +22 -0
  49. package/vitest.config.ts.bak +9 -0
@@ -0,0 +1,844 @@
1
+ /**
2
+ * Workflow Visualizer Tests
3
+ *
4
+ * Comprehensive tests for the WorkflowVisualizer covering auto-layout algorithms,
5
+ * manual layout, SVG export, Mermaid export, and DOT export.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach } from 'vitest';
9
+ import { createVisualizer, globalVisualizer } from './index';
10
+ import type { Workflow, WorkflowNode, Connection, Position } from '../types';
11
+
12
+ describe('WorkflowVisualizer', () => {
13
+ let visualizer: ReturnType<typeof createVisualizer>;
14
+
15
+ beforeEach(() => {
16
+ visualizer = createVisualizer();
17
+ });
18
+
19
+ describe('createVisualizer', () => {
20
+ it('should create a visualizer with default config', () => {
21
+ const viz = createVisualizer();
22
+ expect(viz).toBeDefined();
23
+ expect(typeof viz.layout).toBe('function');
24
+ expect(typeof viz.autoLayout).toBe('function');
25
+ expect(typeof viz.toSVG).toBe('function');
26
+ expect(typeof viz.toMermaid).toBe('function');
27
+ expect(typeof viz.toDOT).toBe('function');
28
+ });
29
+
30
+ it('should create a visualizer with custom config', () => {
31
+ const viz = createVisualizer({
32
+ width: 2000,
33
+ height: 1200,
34
+ nodeWidth: 200,
35
+ nodeHeight: 80,
36
+ hSpacing: 120,
37
+ vSpacing: 100,
38
+ autoLayout: false,
39
+ });
40
+ expect(viz).toBeDefined();
41
+ });
42
+ });
43
+
44
+ describe('layout generation', () => {
45
+ it('should generate layout for a simple workflow', () => {
46
+ const workflow: Workflow = {
47
+ id: 'wf-simple',
48
+ name: 'Simple Workflow',
49
+ version: '1.0.0',
50
+ nodes: [
51
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
52
+ { id: 'node-2', type: 'output:result', name: 'End' },
53
+ ],
54
+ connections: [
55
+ { id: 'conn-1', source: 'node-1', target: 'node-2' },
56
+ ],
57
+ };
58
+
59
+ const layout = visualizer.layout(workflow);
60
+
61
+ expect(layout).toHaveProperty('nodes');
62
+ expect(layout).toHaveProperty('connections');
63
+ expect(layout).toHaveProperty('bounds');
64
+ expect(layout.nodes).toHaveLength(2);
65
+ expect(layout.connections).toHaveLength(1);
66
+ });
67
+
68
+ it('should include correct node properties', () => {
69
+ const workflow: Workflow = {
70
+ id: 'wf-props',
71
+ name: 'Properties Test',
72
+ version: '1.0.0',
73
+ nodes: [
74
+ { id: 'node-1', type: 'trigger:manual', name: 'Trigger Node' },
75
+ ],
76
+ connections: [],
77
+ };
78
+
79
+ const layout = visualizer.layout(workflow);
80
+ const node = layout.nodes[0];
81
+
82
+ expect(node).toHaveProperty('id', 'node-1');
83
+ expect(node).toHaveProperty('x');
84
+ expect(node).toHaveProperty('y');
85
+ expect(node).toHaveProperty('width');
86
+ expect(node).toHaveProperty('height');
87
+ expect(node).toHaveProperty('label');
88
+ expect(node).toHaveProperty('color');
89
+ expect(node).toHaveProperty('icon');
90
+ });
91
+
92
+ it('should include correct connection properties', () => {
93
+ const workflow: Workflow = {
94
+ id: 'wf-conn',
95
+ name: 'Connection Test',
96
+ version: '1.0.0',
97
+ nodes: [
98
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
99
+ { id: 'node-2', type: 'output:result', name: 'End' },
100
+ ],
101
+ connections: [
102
+ { id: 'conn-1', source: 'node-1', target: 'node-2' },
103
+ ],
104
+ };
105
+
106
+ const layout = visualizer.layout(workflow);
107
+ const conn = layout.connections[0];
108
+
109
+ expect(conn).toHaveProperty('id', 'conn-1');
110
+ expect(conn).toHaveProperty('source');
111
+ expect(conn).toHaveProperty('target');
112
+ expect(conn).toHaveProperty('path');
113
+ expect(conn.source).toHaveProperty('x');
114
+ expect(conn.source).toHaveProperty('y');
115
+ expect(conn.target).toHaveProperty('x');
116
+ expect(conn.target).toHaveProperty('y');
117
+ });
118
+ });
119
+
120
+ describe('auto-layout algorithms (dag-layered)', () => {
121
+ it('should auto-layout nodes in layers', () => {
122
+ const nodes: WorkflowNode[] = [
123
+ { id: 'A', type: 'trigger:manual', name: 'A' },
124
+ { id: 'B', type: 'transform:map', name: 'B' },
125
+ { id: 'C', type: 'transform:filter', name: 'C' },
126
+ { id: 'D', type: 'output:result', name: 'D' },
127
+ ];
128
+ const connections: Connection[] = [
129
+ { id: 'c1', source: 'A', target: 'B' },
130
+ { id: 'c2', source: 'A', target: 'C' },
131
+ { id: 'c3', source: 'B', target: 'D' },
132
+ { id: 'c4', source: 'C', target: 'D' },
133
+ ];
134
+
135
+ const positions = visualizer.autoLayout(nodes, connections);
136
+
137
+ expect(positions).toHaveProperty('A');
138
+ expect(positions).toHaveProperty('B');
139
+ expect(positions).toHaveProperty('C');
140
+ expect(positions).toHaveProperty('D');
141
+
142
+ // A should be in a different layer than D
143
+ expect(positions['A'].y).not.toBe(positions['D'].y);
144
+ });
145
+
146
+ it('should handle disconnected nodes', () => {
147
+ const nodes: WorkflowNode[] = [
148
+ { id: 'A', type: 'trigger:manual', name: 'A' },
149
+ { id: 'B', type: 'trigger:schedule', name: 'B' },
150
+ { id: 'C', type: 'output:result', name: 'C' },
151
+ ];
152
+ const connections: Connection[] = [];
153
+
154
+ const positions = visualizer.autoLayout(nodes, connections);
155
+
156
+ expect(Object.keys(positions)).toHaveLength(3);
157
+ nodes.forEach(node => {
158
+ expect(positions[node.id]).toHaveProperty('x');
159
+ expect(positions[node.id]).toHaveProperty('y');
160
+ });
161
+ });
162
+
163
+ it('should handle linear chains', () => {
164
+ const nodes: WorkflowNode[] = [
165
+ { id: 'n1', type: 'trigger:manual', name: 'N1' },
166
+ { id: 'n2', type: 'transform:map', name: 'N2' },
167
+ { id: 'n3', type: 'transform:filter', name: 'N3' },
168
+ { id: 'n4', type: 'output:result', name: 'N4' },
169
+ ];
170
+ const connections: Connection[] = [
171
+ { id: 'c1', source: 'n1', target: 'n2' },
172
+ { id: 'c2', source: 'n2', target: 'n3' },
173
+ { id: 'c3', source: 'n3', target: 'n4' },
174
+ ];
175
+
176
+ const positions = visualizer.autoLayout(nodes, connections);
177
+
178
+ // Each node should be in a different layer (y position)
179
+ expect(positions['n1'].y).toBeLessThan(positions['n2'].y);
180
+ expect(positions['n2'].y).toBeLessThan(positions['n3'].y);
181
+ expect(positions['n3'].y).toBeLessThan(positions['n4'].y);
182
+ });
183
+
184
+ it('should handle diamond patterns', () => {
185
+ const nodes: WorkflowNode[] = [
186
+ { id: 'start', type: 'trigger:manual', name: 'Start' },
187
+ { id: 'branch1', type: 'condition:if', name: 'Branch 1' },
188
+ { id: 'branch2', type: 'condition:if', name: 'Branch 2' },
189
+ { id: 'end', type: 'output:result', name: 'End' },
190
+ ];
191
+ const connections: Connection[] = [
192
+ { id: 'c1', source: 'start', target: 'branch1' },
193
+ { id: 'c2', source: 'start', target: 'branch2' },
194
+ { id: 'c3', source: 'branch1', target: 'end' },
195
+ { id: 'c4', source: 'branch2', target: 'end' },
196
+ ];
197
+
198
+ const positions = visualizer.autoLayout(nodes, connections);
199
+
200
+ // Branch nodes should be at same level
201
+ expect(positions['branch1'].y).toBe(positions['branch2'].y);
202
+ // But different x positions
203
+ expect(positions['branch1'].x).not.toBe(positions['branch2'].x);
204
+ });
205
+
206
+ it('should handle cycles gracefully', () => {
207
+ const nodes: WorkflowNode[] = [
208
+ { id: 'A', type: 'trigger:manual', name: 'A' },
209
+ { id: 'B', type: 'transform:map', name: 'B' },
210
+ { id: 'C', type: 'output:result', name: 'C' },
211
+ ];
212
+ // Create a cycle: A -> B -> C -> B
213
+ const connections: Connection[] = [
214
+ { id: 'c1', source: 'A', target: 'B' },
215
+ { id: 'c2', source: 'B', target: 'C' },
216
+ { id: 'c3', source: 'C', target: 'B' },
217
+ ];
218
+
219
+ // Should not throw
220
+ const positions = visualizer.autoLayout(nodes, connections);
221
+ expect(Object.keys(positions)).toHaveLength(3);
222
+ });
223
+
224
+ it('should calculate bounds correctly', () => {
225
+ const workflow: Workflow = {
226
+ id: 'wf-bounds',
227
+ name: 'Bounds Test',
228
+ version: '1.0.0',
229
+ nodes: [
230
+ { id: 'node-1', type: 'trigger:manual', name: 'Node 1' },
231
+ { id: 'node-2', type: 'output:result', name: 'Node 2' },
232
+ ],
233
+ connections: [
234
+ { id: 'c1', source: 'node-1', target: 'node-2' },
235
+ ],
236
+ };
237
+
238
+ const layout = visualizer.layout(workflow);
239
+
240
+ expect(layout.bounds).toHaveProperty('minX');
241
+ expect(layout.bounds).toHaveProperty('minY');
242
+ expect(layout.bounds).toHaveProperty('maxX');
243
+ expect(layout.bounds).toHaveProperty('maxY');
244
+ expect(layout.bounds).toHaveProperty('width');
245
+ expect(layout.bounds).toHaveProperty('height');
246
+
247
+ expect(layout.bounds.width).toBeGreaterThan(0);
248
+ expect(layout.bounds.height).toBeGreaterThan(0);
249
+ });
250
+ });
251
+
252
+ describe('manual layout with existing positions', () => {
253
+ it('should respect existing node positions when autoLayout is disabled', () => {
254
+ const customPositions = {
255
+ x: 100,
256
+ y: 200,
257
+ };
258
+
259
+ const workflow: Workflow = {
260
+ id: 'wf-manual',
261
+ name: 'Manual Layout Test',
262
+ version: '1.0.0',
263
+ nodes: [
264
+ { id: 'node-1', type: 'trigger:manual', name: 'Node 1', position: customPositions },
265
+ ],
266
+ connections: [],
267
+ };
268
+
269
+ const manualVisualizer = createVisualizer({ autoLayout: false });
270
+ const layout = manualVisualizer.layout(workflow);
271
+
272
+ expect(layout.nodes[0].x).toBe(customPositions.x);
273
+ expect(layout.nodes[0].y).toBe(customPositions.y);
274
+ });
275
+
276
+ it('should default to (0,0) for nodes without positions', () => {
277
+ const workflow: Workflow = {
278
+ id: 'wf-default-pos',
279
+ name: 'Default Position Test',
280
+ version: '1.0.0',
281
+ nodes: [
282
+ { id: 'node-1', type: 'trigger:manual', name: 'Node 1' },
283
+ ],
284
+ connections: [],
285
+ };
286
+
287
+ const manualVisualizer = createVisualizer({ autoLayout: false });
288
+ const layout = manualVisualizer.layout(workflow);
289
+
290
+ expect(layout.nodes[0].x).toBe(0);
291
+ expect(layout.nodes[0].y).toBe(0);
292
+ });
293
+
294
+ it('should mix auto-layout and manual positions', () => {
295
+ const workflow: Workflow = {
296
+ id: 'wf-mixed',
297
+ name: 'Mixed Layout Test',
298
+ version: '1.0.0',
299
+ nodes: [
300
+ { id: 'node-1', type: 'trigger:manual', name: 'Node 1', position: { x: 50, y: 50 } },
301
+ { id: 'node-2', type: 'output:result', name: 'Node 2' },
302
+ ],
303
+ connections: [
304
+ { id: 'c1', source: 'node-1', target: 'node-2' },
305
+ ],
306
+ };
307
+
308
+ // With autoLayout enabled, positions are recalculated
309
+ const autoLayout = createVisualizer({ autoLayout: true });
310
+ const layout = autoLayout.layout(workflow);
311
+
312
+ expect(layout.nodes).toHaveLength(2);
313
+ // Both nodes should have valid positions
314
+ layout.nodes.forEach(node => {
315
+ expect(typeof node.x).toBe('number');
316
+ expect(typeof node.y).toBe('number');
317
+ });
318
+ });
319
+ });
320
+
321
+ describe('SVG export', () => {
322
+ it('should generate valid SVG string', () => {
323
+ const workflow: Workflow = {
324
+ id: 'wf-svg',
325
+ name: 'SVG Test',
326
+ version: '1.0.0',
327
+ nodes: [
328
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
329
+ { id: 'node-2', type: 'output:result', name: 'End' },
330
+ ],
331
+ connections: [
332
+ { id: 'c1', source: 'node-1', target: 'node-2' },
333
+ ],
334
+ };
335
+
336
+ const svg = visualizer.toSVG(workflow);
337
+
338
+ expect(svg).toContain('<svg');
339
+ expect(svg).toContain('</svg>');
340
+ expect(svg).toContain('xmlns="http://www.w3.org/2000/svg"');
341
+ });
342
+
343
+ it('should include node rectangles in SVG', () => {
344
+ const workflow: Workflow = {
345
+ id: 'wf-svg-nodes',
346
+ name: 'SVG Nodes Test',
347
+ version: '1.0.0',
348
+ nodes: [
349
+ { id: 'node-1', type: 'trigger:manual', name: 'Test Node' },
350
+ ],
351
+ connections: [],
352
+ };
353
+
354
+ const svg = visualizer.toSVG(workflow);
355
+
356
+ expect(svg).toContain('<rect');
357
+ // SVG rect elements may be self-closing or have closing tags depending on generator
358
+ expect(svg).toMatch(/<rect[^>]*\/>|<rect[^>]*>.*<\/rect>/s);
359
+ });
360
+
361
+ it('should include connection paths in SVG', () => {
362
+ const workflow: Workflow = {
363
+ id: 'wf-svg-conn',
364
+ name: 'SVG Connection Test',
365
+ version: '1.0.0',
366
+ nodes: [
367
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
368
+ { id: 'node-2', type: 'output:result', name: 'End' },
369
+ ],
370
+ connections: [
371
+ { id: 'c1', source: 'node-1', target: 'node-2' },
372
+ ],
373
+ };
374
+
375
+ const svg = visualizer.toSVG(workflow);
376
+
377
+ expect(svg).toContain('<path');
378
+ expect(svg).toContain('d="M');
379
+ });
380
+
381
+ it('should include arrowhead marker in SVG', () => {
382
+ const workflow: Workflow = {
383
+ id: 'wf-svg-arrow',
384
+ name: 'SVG Arrow Test',
385
+ version: '1.0.0',
386
+ nodes: [
387
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
388
+ { id: 'node-2', type: 'output:result', name: 'End' },
389
+ ],
390
+ connections: [
391
+ { id: 'c1', source: 'node-1', target: 'node-2' },
392
+ ],
393
+ };
394
+
395
+ const svg = visualizer.toSVG(workflow);
396
+
397
+ expect(svg).toContain('<defs>');
398
+ expect(svg).toContain('<marker');
399
+ expect(svg).toContain('arrowhead');
400
+ });
401
+
402
+ it('should include node labels in SVG', () => {
403
+ const workflow: Workflow = {
404
+ id: 'wf-svg-labels',
405
+ name: 'SVG Labels Test',
406
+ version: '1.0.0',
407
+ nodes: [
408
+ { id: 'node-1', type: 'trigger:manual', name: 'My Node' },
409
+ ],
410
+ connections: [],
411
+ };
412
+
413
+ const svg = visualizer.toSVG(workflow);
414
+
415
+ expect(svg).toContain('<text');
416
+ expect(svg).toContain('My Node');
417
+ });
418
+
419
+ it('should generate SVG with correct viewBox', () => {
420
+ const workflow: Workflow = {
421
+ id: 'wf-svg-viewbox',
422
+ name: 'SVG ViewBox Test',
423
+ version: '1.0.0',
424
+ nodes: [
425
+ { id: 'node-1', type: 'trigger:manual', name: 'Node 1' },
426
+ { id: 'node-2', type: 'output:result', name: 'Node 2' },
427
+ ],
428
+ connections: [
429
+ { id: 'c1', source: 'node-1', target: 'node-2' },
430
+ ],
431
+ };
432
+
433
+ const svg = visualizer.toSVG(workflow);
434
+
435
+ expect(svg).toContain('viewBox');
436
+ expect(svg).toContain('width="');
437
+ expect(svg).toContain('height="');
438
+ });
439
+
440
+ it('should handle empty workflow SVG export', () => {
441
+ const workflow: Workflow = {
442
+ id: 'wf-empty',
443
+ name: 'Empty Workflow',
444
+ version: '1.0.0',
445
+ nodes: [],
446
+ connections: [],
447
+ };
448
+
449
+ const svg = visualizer.toSVG(workflow);
450
+
451
+ expect(svg).toContain('<svg');
452
+ expect(svg).toContain('</svg>');
453
+ });
454
+ });
455
+
456
+ describe('Mermaid export', () => {
457
+ it('should generate valid Mermaid diagram', () => {
458
+ const workflow: Workflow = {
459
+ id: 'wf-mermaid',
460
+ name: 'Mermaid Test',
461
+ version: '1.0.0',
462
+ nodes: [
463
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
464
+ { id: 'node-2', type: 'output:result', name: 'End' },
465
+ ],
466
+ connections: [
467
+ { id: 'c1', source: 'node-1', target: 'node-2' },
468
+ ],
469
+ };
470
+
471
+ const mermaid = visualizer.toMermaid(workflow);
472
+
473
+ expect(mermaid).toContain('graph TD');
474
+ expect(mermaid).toContain('node-1');
475
+ expect(mermaid).toContain('node-2');
476
+ });
477
+
478
+ it('should include node definitions in Mermaid', () => {
479
+ const workflow: Workflow = {
480
+ id: 'wf-mermaid-nodes',
481
+ name: 'Mermaid Nodes Test',
482
+ version: '1.0.0',
483
+ nodes: [
484
+ { id: 'A', type: 'trigger:manual', name: 'Trigger Node' },
485
+ { id: 'B', type: 'transform:map', name: 'Transform Node' },
486
+ ],
487
+ connections: [],
488
+ };
489
+
490
+ const mermaid = visualizer.toMermaid(workflow);
491
+
492
+ expect(mermaid).toContain('A["Trigger Node"]');
493
+ expect(mermaid).toContain('B["Transform Node"]');
494
+ });
495
+
496
+ it('should include connections in Mermaid', () => {
497
+ const workflow: Workflow = {
498
+ id: 'wf-mermaid-conn',
499
+ name: 'Mermaid Connection Test',
500
+ version: '1.0.0',
501
+ nodes: [
502
+ { id: 'A', type: 'trigger:manual', name: 'A' },
503
+ { id: 'B', type: 'transform:map', name: 'B' },
504
+ { id: 'C', type: 'output:result', name: 'C' },
505
+ ],
506
+ connections: [
507
+ { id: 'c1', source: 'A', target: 'B' },
508
+ { id: 'c2', source: 'B', target: 'C' },
509
+ ],
510
+ };
511
+
512
+ const mermaid = visualizer.toMermaid(workflow);
513
+
514
+ expect(mermaid).toContain('A --> B');
515
+ expect(mermaid).toContain('B --> C');
516
+ });
517
+
518
+ it('should handle empty workflow Mermaid export', () => {
519
+ const workflow: Workflow = {
520
+ id: 'wf-empty',
521
+ name: 'Empty Workflow',
522
+ version: '1.0.0',
523
+ nodes: [],
524
+ connections: [],
525
+ };
526
+
527
+ const mermaid = visualizer.toMermaid(workflow);
528
+
529
+ expect(mermaid).toContain('graph TD');
530
+ });
531
+
532
+ it('should handle complex workflows in Mermaid', () => {
533
+ const workflow: Workflow = {
534
+ id: 'wf-complex',
535
+ name: 'Complex Workflow',
536
+ version: '1.0.0',
537
+ nodes: [
538
+ { id: 'start', type: 'trigger:manual', name: 'Start' },
539
+ { id: 'condition', type: 'condition:if', name: 'Condition?' },
540
+ { id: 'path-a', type: 'transform:map', name: 'Path A' },
541
+ { id: 'path-b', type: 'transform:filter', name: 'Path B' },
542
+ { id: 'end', type: 'output:result', name: 'End' },
543
+ ],
544
+ connections: [
545
+ { id: 'c1', source: 'start', target: 'condition' },
546
+ { id: 'c2', source: 'condition', target: 'path-a' },
547
+ { id: 'c3', source: 'condition', target: 'path-b' },
548
+ { id: 'c4', source: 'path-a', target: 'end' },
549
+ { id: 'c5', source: 'path-b', target: 'end' },
550
+ ],
551
+ };
552
+
553
+ const mermaid = visualizer.toMermaid(workflow);
554
+
555
+ expect(mermaid).toContain('start');
556
+ expect(mermaid).toContain('condition');
557
+ expect(mermaid).toContain('path-a');
558
+ expect(mermaid).toContain('path-b');
559
+ expect(mermaid).toContain('end');
560
+ });
561
+ });
562
+
563
+ describe('DOT export', () => {
564
+ it('should generate valid DOT graph', () => {
565
+ const workflow: Workflow = {
566
+ id: 'wf-dot',
567
+ name: 'DOT Test',
568
+ version: '1.0.0',
569
+ nodes: [
570
+ { id: 'node-1', type: 'trigger:manual', name: 'Start' },
571
+ { id: 'node-2', type: 'output:result', name: 'End' },
572
+ ],
573
+ connections: [
574
+ { id: 'c1', source: 'node-1', target: 'node-2' },
575
+ ],
576
+ };
577
+
578
+ const dot = visualizer.toDOT(workflow);
579
+
580
+ expect(dot).toContain('digraph Workflow');
581
+ expect(dot).toContain('{');
582
+ expect(dot).toContain('}');
583
+ });
584
+
585
+ it('should include rankdir in DOT', () => {
586
+ const workflow: Workflow = {
587
+ id: 'wf-dot-rank',
588
+ name: 'DOT Rank Test',
589
+ version: '1.0.0',
590
+ nodes: [],
591
+ connections: [],
592
+ };
593
+
594
+ const dot = visualizer.toDOT(workflow);
595
+
596
+ expect(dot).toContain('rankdir=TB');
597
+ });
598
+
599
+ it('should include node definitions in DOT', () => {
600
+ const workflow: Workflow = {
601
+ id: 'wf-dot-nodes',
602
+ name: 'DOT Nodes Test',
603
+ version: '1.0.0',
604
+ nodes: [
605
+ { id: 'A', type: 'trigger:manual', name: 'Trigger' },
606
+ { id: 'B', type: 'transform:map', name: 'Transform' },
607
+ ],
608
+ connections: [],
609
+ };
610
+
611
+ const dot = visualizer.toDOT(workflow);
612
+
613
+ expect(dot).toContain('"A"');
614
+ expect(dot).toContain('"B"');
615
+ expect(dot).toContain('label="Trigger"');
616
+ expect(dot).toContain('label="Transform"');
617
+ });
618
+
619
+ it('should include node colors in DOT', () => {
620
+ const workflow: Workflow = {
621
+ id: 'wf-dot-colors',
622
+ name: 'DOT Colors Test',
623
+ version: '1.0.0',
624
+ nodes: [
625
+ { id: 'trigger', type: 'trigger:manual', name: 'Trigger' },
626
+ { id: 'transform', type: 'transform:map', name: 'Transform' },
627
+ { id: 'condition', type: 'condition:if', name: 'Condition' },
628
+ ],
629
+ connections: [],
630
+ };
631
+
632
+ const dot = visualizer.toDOT(workflow);
633
+
634
+ expect(dot).toContain('fillcolor=');
635
+ expect(dot).toContain('fontcolor=white');
636
+ });
637
+
638
+ it('should include edges in DOT', () => {
639
+ const workflow: Workflow = {
640
+ id: 'wf-dot-edges',
641
+ name: 'DOT Edges Test',
642
+ version: '1.0.0',
643
+ nodes: [
644
+ { id: 'A', type: 'trigger:manual', name: 'A' },
645
+ { id: 'B', type: 'transform:map', name: 'B' },
646
+ { id: 'C', type: 'output:result', name: 'C' },
647
+ ],
648
+ connections: [
649
+ { id: 'c1', source: 'A', target: 'B' },
650
+ { id: 'c2', source: 'B', target: 'C' },
651
+ ],
652
+ };
653
+
654
+ const dot = visualizer.toDOT(workflow);
655
+
656
+ expect(dot).toContain('"A" -> "B"');
657
+ expect(dot).toContain('"B" -> "C"');
658
+ });
659
+
660
+ it('should handle empty workflow DOT export', () => {
661
+ const workflow: Workflow = {
662
+ id: 'wf-empty',
663
+ name: 'Empty Workflow',
664
+ version: '1.0.0',
665
+ nodes: [],
666
+ connections: [],
667
+ };
668
+
669
+ const dot = visualizer.toDOT(workflow);
670
+
671
+ expect(dot).toContain('digraph Workflow');
672
+ expect(dot).toContain('{');
673
+ expect(dot).toContain('}');
674
+ });
675
+
676
+ it('should include node style attributes in DOT', () => {
677
+ const workflow: Workflow = {
678
+ id: 'wf-dot-style',
679
+ name: 'DOT Style Test',
680
+ version: '1.0.0',
681
+ nodes: [
682
+ { id: 'node-1', type: 'trigger:manual', name: 'Node' },
683
+ ],
684
+ connections: [],
685
+ };
686
+
687
+ const dot = visualizer.toDOT(workflow);
688
+
689
+ expect(dot).toContain('node [');
690
+ expect(dot).toContain('shape=box');
691
+ expect(dot).toContain('style=filled');
692
+ });
693
+ });
694
+
695
+ describe('node type styling', () => {
696
+ it('should assign correct colors to node types', () => {
697
+ const workflow: Workflow = {
698
+ id: 'wf-colors',
699
+ name: 'Colors Test',
700
+ version: '1.0.0',
701
+ nodes: [
702
+ { id: 'trigger', type: 'trigger:manual', name: 'Trigger' },
703
+ { id: 'schedule', type: 'trigger:schedule', name: 'Schedule' },
704
+ { id: 'webhook', type: 'trigger:webhook', name: 'Webhook' },
705
+ { id: 'map', type: 'transform:map', name: 'Map' },
706
+ { id: 'filter', type: 'transform:filter', name: 'Filter' },
707
+ { id: 'condition', type: 'condition:if', name: 'Condition' },
708
+ { id: 'loop', type: 'loop:for-each', name: 'Loop' },
709
+ { id: 'delay', type: 'delay:wait', name: 'Delay' },
710
+ { id: 'http', type: 'http:request', name: 'HTTP' },
711
+ { id: 'result', type: 'output:result', name: 'Result' },
712
+ { id: 'log', type: 'output:log', name: 'Log' },
713
+ { id: 'unknown', type: 'unknown:type', name: 'Unknown' },
714
+ ],
715
+ connections: [],
716
+ };
717
+
718
+ const layout = visualizer.layout(workflow);
719
+
720
+ // All nodes should have colors
721
+ layout.nodes.forEach(node => {
722
+ expect(node.color).toBeDefined();
723
+ expect(node.color).toMatch(/^#[0-9a-fA-F]{6}$/);
724
+ });
725
+ });
726
+
727
+ it('should assign correct icons to node types', () => {
728
+ const workflow: Workflow = {
729
+ id: 'wf-icons',
730
+ name: 'Icons Test',
731
+ version: '1.0.0',
732
+ nodes: [
733
+ { id: 'trigger', type: 'trigger:manual', name: 'Trigger' },
734
+ { id: 'schedule', type: 'trigger:schedule', name: 'Schedule' },
735
+ { id: 'webhook', type: 'trigger:webhook', name: 'Webhook' },
736
+ { id: 'map', type: 'transform:map', name: 'Map' },
737
+ { id: 'filter', type: 'transform:filter', name: 'Filter' },
738
+ { id: 'condition', type: 'condition:if', name: 'Condition' },
739
+ { id: 'loop', type: 'loop:for-each', name: 'Loop' },
740
+ { id: 'delay', type: 'delay:wait', name: 'Delay' },
741
+ { id: 'http', type: 'http:request', name: 'HTTP' },
742
+ { id: 'result', type: 'output:result', name: 'Result' },
743
+ { id: 'log', type: 'output:log', name: 'Log' },
744
+ { id: 'unknown', type: 'unknown:type', name: 'Unknown' },
745
+ ],
746
+ connections: [],
747
+ };
748
+
749
+ const layout = visualizer.layout(workflow);
750
+
751
+ // All nodes should have icons
752
+ layout.nodes.forEach(node => {
753
+ expect(node.icon).toBeDefined();
754
+ expect(typeof node.icon).toBe('string');
755
+ expect(node.icon!.length).toBeGreaterThan(0);
756
+ });
757
+ });
758
+ });
759
+
760
+ describe('path generation', () => {
761
+ it('should generate valid SVG path for connections', () => {
762
+ const workflow: Workflow = {
763
+ id: 'wf-path',
764
+ name: 'Path Test',
765
+ version: '1.0.0',
766
+ nodes: [
767
+ { id: 'A', type: 'trigger:manual', name: 'A' },
768
+ { id: 'B', type: 'output:result', name: 'B' },
769
+ ],
770
+ connections: [
771
+ { id: 'c1', source: 'A', target: 'B' },
772
+ ],
773
+ };
774
+
775
+ const layout = visualizer.layout(workflow);
776
+
777
+ expect(layout.connections[0].path).toContain('M');
778
+ expect(layout.connections[0].path).toContain('C');
779
+ });
780
+ });
781
+
782
+ describe('global visualizer instance', () => {
783
+ it('should have a global visualizer instance', () => {
784
+ expect(globalVisualizer).toBeDefined();
785
+ expect(typeof globalVisualizer.layout).toBe('function');
786
+ expect(typeof globalVisualizer.toSVG).toBe('function');
787
+ expect(typeof globalVisualizer.toMermaid).toBe('function');
788
+ expect(typeof globalVisualizer.toDOT).toBe('function');
789
+ });
790
+ });
791
+
792
+ describe('complex workflow visualization', () => {
793
+ it('should handle complex workflow with multiple branches', () => {
794
+ const workflow: Workflow = {
795
+ id: 'wf-complex-viz',
796
+ name: 'Complex Visualization',
797
+ version: '1.0.0',
798
+ nodes: [
799
+ { id: 'input', type: 'trigger:manual', name: 'Input' },
800
+ { id: 'validate', type: 'transform:filter', name: 'Validate' },
801
+ { id: 'check', type: 'condition:if', name: 'Check' },
802
+ { id: 'process-a', type: 'transform:map', name: 'Process A' },
803
+ { id: 'process-b', type: 'transform:map', name: 'Process B' },
804
+ { id: 'merge', type: 'transform:map', name: 'Merge' },
805
+ { id: 'delay', type: 'delay:wait', name: 'Wait' },
806
+ { id: 'output', type: 'output:result', name: 'Output' },
807
+ ],
808
+ connections: [
809
+ { id: 'c1', source: 'input', target: 'validate' },
810
+ { id: 'c2', source: 'validate', target: 'check' },
811
+ { id: 'c3', source: 'check', target: 'process-a' },
812
+ { id: 'c4', source: 'check', target: 'process-b' },
813
+ { id: 'c5', source: 'process-a', target: 'merge' },
814
+ { id: 'c6', source: 'process-b', target: 'merge' },
815
+ { id: 'c7', source: 'merge', target: 'delay' },
816
+ { id: 'c8', source: 'delay', target: 'output' },
817
+ ],
818
+ };
819
+
820
+ const layout = visualizer.layout(workflow);
821
+
822
+ expect(layout.nodes).toHaveLength(8);
823
+ expect(layout.connections).toHaveLength(8);
824
+
825
+ // All nodes should have valid positions
826
+ layout.nodes.forEach(node => {
827
+ expect(node.x).toBeGreaterThanOrEqual(0);
828
+ expect(node.y).toBeGreaterThanOrEqual(0);
829
+ });
830
+
831
+ // SVG should generate without errors
832
+ const svg = visualizer.toSVG(workflow);
833
+ expect(svg).toContain('<svg');
834
+
835
+ // Mermaid should generate without errors
836
+ const mermaid = visualizer.toMermaid(workflow);
837
+ expect(mermaid).toContain('graph TD');
838
+
839
+ // DOT should generate without errors
840
+ const dot = visualizer.toDOT(workflow);
841
+ expect(dot).toContain('digraph Workflow');
842
+ });
843
+ });
844
+ });