pict-section-flow 0.0.10 → 0.0.13

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 (88) hide show
  1. package/.claude/launch.json +1 -1
  2. package/README.md +176 -0
  3. package/docs/.nojekyll +0 -0
  4. package/docs/Architecture.md +303 -0
  5. package/docs/Custom-Styling.md +275 -0
  6. package/docs/Data_Model.md +158 -0
  7. package/docs/Event_System.md +156 -0
  8. package/docs/Getting_Started.md +237 -0
  9. package/docs/Implementation_Reference.md +528 -0
  10. package/docs/Layout_Persistence.md +117 -0
  11. package/docs/README.md +115 -52
  12. package/docs/_cover.md +11 -0
  13. package/docs/_sidebar.md +52 -0
  14. package/docs/_topbar.md +8 -0
  15. package/docs/api/PictFlowCard.md +216 -0
  16. package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
  17. package/docs/api/addConnection.md +101 -0
  18. package/docs/api/addNode.md +137 -0
  19. package/docs/api/autoLayout.md +77 -0
  20. package/docs/api/getFlowData.md +112 -0
  21. package/docs/api/marshalToView.md +95 -0
  22. package/docs/api/openPanel.md +128 -0
  23. package/docs/api/registerHandler.md +174 -0
  24. package/docs/api/registerNodeType.md +142 -0
  25. package/docs/api/removeConnection.md +57 -0
  26. package/docs/api/removeNode.md +80 -0
  27. package/docs/api/saveLayout.md +152 -0
  28. package/docs/api/screenToSVGCoords.md +68 -0
  29. package/docs/api/selectNode.md +116 -0
  30. package/docs/api/setTheme.md +168 -0
  31. package/docs/api/setZoom.md +97 -0
  32. package/docs/api/toggleFullscreen.md +68 -0
  33. package/docs/card-help/EACH.md +19 -0
  34. package/docs/card-help/FREAD.md +24 -0
  35. package/docs/card-help/FWRITE.md +24 -0
  36. package/docs/card-help/GET.md +22 -0
  37. package/docs/card-help/ITE.md +23 -0
  38. package/docs/card-help/LOG.md +23 -0
  39. package/docs/card-help/NOTE.md +17 -0
  40. package/docs/card-help/PREV.md +18 -0
  41. package/docs/card-help/SET.md +27 -0
  42. package/docs/card-help/SPKL.md +22 -0
  43. package/docs/card-help/STAT.md +23 -0
  44. package/docs/card-help/SW.md +25 -0
  45. package/docs/css/docuserve.css +73 -0
  46. package/docs/index.html +39 -0
  47. package/docs/retold-catalog.json +169 -0
  48. package/docs/retold-keyword-index.json +13942 -0
  49. package/example_applications/simple_cards/package.json +1 -0
  50. package/example_applications/simple_cards/source/card-help-content.js +16 -0
  51. package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
  52. package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
  53. package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
  54. package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
  55. package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
  56. package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
  57. package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
  58. package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
  59. package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
  60. package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
  61. package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
  62. package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
  63. package/package.json +11 -7
  64. package/scripts/generate-card-help.js +214 -0
  65. package/source/Pict-Section-Flow.js +4 -0
  66. package/source/PictFlowCard.js +3 -1
  67. package/source/providers/PictProvider-Flow-CSS.js +245 -152
  68. package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
  69. package/source/providers/PictProvider-Flow-Geometry.js +195 -38
  70. package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
  71. package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
  72. package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
  73. package/source/services/PictService-Flow-DataManager.js +338 -0
  74. package/source/services/PictService-Flow-InteractionManager.js +165 -7
  75. package/source/services/PictService-Flow-PathGenerator.js +282 -0
  76. package/source/services/PictService-Flow-PortRenderer.js +269 -0
  77. package/source/services/PictService-Flow-RenderManager.js +281 -0
  78. package/source/services/PictService-Flow-Tether.js +6 -42
  79. package/source/views/PictView-Flow-Node.js +2 -220
  80. package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
  81. package/source/views/PictView-Flow.js +130 -882
  82. package/test/ConnectionHandleManager_tests.js +717 -0
  83. package/test/ConnectionRenderer_tests.js +591 -0
  84. package/test/DataManager_tests.js +859 -0
  85. package/test/Geometry_tests.js +767 -0
  86. package/test/PathGenerator_tests.js +978 -0
  87. package/test/PortRenderer_tests.js +367 -0
  88. package/test/RenderManager_tests.js +756 -0
@@ -0,0 +1,859 @@
1
+ const libFable = require('fable');
2
+ const libChai = require('chai');
3
+ const libExpect = libChai.expect;
4
+
5
+ const libDataManager = require('../source/services/PictService-Flow-DataManager.js');
6
+
7
+ suite
8
+ (
9
+ 'PictService-Flow-DataManager',
10
+ function ()
11
+ {
12
+ let _Fable;
13
+ let _DataManager;
14
+ let _MockFlowView;
15
+
16
+ setup
17
+ (
18
+ function ()
19
+ {
20
+ _Fable = new libFable({});
21
+
22
+ _MockFlowView =
23
+ {
24
+ fable: _Fable,
25
+ pict: null,
26
+ log: _Fable.log,
27
+ options:
28
+ {
29
+ DefaultNodeType: 'generic',
30
+ DefaultNodeWidth: 160,
31
+ DefaultNodeHeight: 80,
32
+ FlowDataAddress: null
33
+ },
34
+ Bundle: {},
35
+ initialRenderComplete: false,
36
+ _FlowData:
37
+ {
38
+ Nodes: [],
39
+ Connections: [],
40
+ OpenPanels: [],
41
+ SavedLayouts: [],
42
+ ViewState:
43
+ {
44
+ PanX: 0,
45
+ PanY: 0,
46
+ Zoom: 1,
47
+ SelectedNodeHash: null,
48
+ SelectedConnectionHash: null,
49
+ SelectedTetherHash: null
50
+ }
51
+ },
52
+ _NodeTypeProvider:
53
+ {
54
+ getNodeType: function (pType)
55
+ {
56
+ if (pType === 'action')
57
+ {
58
+ return {
59
+ Label: 'Action',
60
+ DefaultWidth: 200,
61
+ DefaultHeight: 100,
62
+ DefaultPorts:
63
+ [
64
+ { Hash: 'p-in', Direction: 'input', Side: 'left', Label: 'In' },
65
+ { Hash: 'p-out', Direction: 'output', Side: 'right', Label: 'Out' },
66
+ { Hash: 'p-err', Direction: 'output', Side: 'bottom', Label: 'Error', PortType: 'error' }
67
+ ]
68
+ };
69
+ }
70
+ return null;
71
+ }
72
+ },
73
+ _LayoutProvider:
74
+ {
75
+ loadPersistedLayouts: function () {}
76
+ },
77
+ _EventHandlerProvider:
78
+ {
79
+ fireEvent: function () {}
80
+ },
81
+ renderFlow: function () {},
82
+ closePanelForNode: function (pNodeHash)
83
+ {
84
+ _MockFlowView._FlowData.OpenPanels = _MockFlowView._FlowData.OpenPanels.filter(
85
+ (pPanel) => pPanel.NodeHash !== pNodeHash
86
+ );
87
+ },
88
+ marshalFromView: function () {}
89
+ };
90
+
91
+ _DataManager = new libDataManager(_Fable, { FlowView: _MockFlowView }, 'DM-Test');
92
+ }
93
+ );
94
+
95
+ // ---- Constructor ----
96
+
97
+ suite
98
+ (
99
+ 'Constructor',
100
+ function ()
101
+ {
102
+ test
103
+ (
104
+ 'should instantiate with correct serviceType',
105
+ function (fDone)
106
+ {
107
+ libExpect(_DataManager).to.be.an('object');
108
+ libExpect(_DataManager.serviceType).to.equal('PictServiceFlowDataManager');
109
+ fDone();
110
+ }
111
+ );
112
+
113
+ test
114
+ (
115
+ 'should store FlowView reference from options',
116
+ function (fDone)
117
+ {
118
+ libExpect(_DataManager._FlowView).to.equal(_MockFlowView);
119
+ fDone();
120
+ }
121
+ );
122
+
123
+ test
124
+ (
125
+ 'should handle missing FlowView in options',
126
+ function (fDone)
127
+ {
128
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
129
+ libExpect(tmpManager._FlowView).to.be.null;
130
+ fDone();
131
+ }
132
+ );
133
+ }
134
+ );
135
+
136
+ // ---- getFlowData ----
137
+
138
+ suite
139
+ (
140
+ 'getFlowData',
141
+ function ()
142
+ {
143
+ test
144
+ (
145
+ 'should return a deep clone of the flow data',
146
+ function (fDone)
147
+ {
148
+ _MockFlowView._FlowData.Nodes.push({ Hash: 'node-1', X: 10, Y: 20 });
149
+ let tmpResult = _DataManager.getFlowData();
150
+
151
+ libExpect(tmpResult.Nodes).to.have.length(1);
152
+ libExpect(tmpResult.Nodes[0].Hash).to.equal('node-1');
153
+
154
+ // Verify it's a clone, not a reference
155
+ tmpResult.Nodes[0].X = 999;
156
+ libExpect(_MockFlowView._FlowData.Nodes[0].X).to.equal(10);
157
+ fDone();
158
+ }
159
+ );
160
+
161
+ test
162
+ (
163
+ 'should return empty object when no FlowView',
164
+ function (fDone)
165
+ {
166
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
167
+ let tmpResult = tmpManager.getFlowData();
168
+ libExpect(tmpResult).to.deep.equal({});
169
+ fDone();
170
+ }
171
+ );
172
+ }
173
+ );
174
+
175
+ // ---- setFlowData ----
176
+
177
+ suite
178
+ (
179
+ 'setFlowData',
180
+ function ()
181
+ {
182
+ test
183
+ (
184
+ 'should set flow data with validated structure',
185
+ function (fDone)
186
+ {
187
+ let tmpData =
188
+ {
189
+ Nodes: [{ Hash: 'n1', X: 50, Y: 50 }],
190
+ Connections: [{ Hash: 'c1', SourceNodeHash: 'n1', TargetNodeHash: 'n2' }],
191
+ ViewState: { PanX: 100, PanY: 200, Zoom: 2 }
192
+ };
193
+
194
+ _DataManager.setFlowData(tmpData);
195
+
196
+ libExpect(_MockFlowView._FlowData.Nodes).to.have.length(1);
197
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(1);
198
+ libExpect(_MockFlowView._FlowData.ViewState.PanX).to.equal(100);
199
+ libExpect(_MockFlowView._FlowData.ViewState.Zoom).to.equal(2);
200
+ fDone();
201
+ }
202
+ );
203
+
204
+ test
205
+ (
206
+ 'should provide defaults for missing arrays',
207
+ function (fDone)
208
+ {
209
+ _DataManager.setFlowData({ ViewState: { PanX: 10 } });
210
+
211
+ libExpect(_MockFlowView._FlowData.Nodes).to.be.an('array').that.is.empty;
212
+ libExpect(_MockFlowView._FlowData.Connections).to.be.an('array').that.is.empty;
213
+ libExpect(_MockFlowView._FlowData.OpenPanels).to.be.an('array').that.is.empty;
214
+ libExpect(_MockFlowView._FlowData.SavedLayouts).to.be.an('array').that.is.empty;
215
+ fDone();
216
+ }
217
+ );
218
+
219
+ test
220
+ (
221
+ 'should provide default ViewState when missing',
222
+ function (fDone)
223
+ {
224
+ _DataManager.setFlowData({ Nodes: [] });
225
+
226
+ libExpect(_MockFlowView._FlowData.ViewState.PanX).to.equal(0);
227
+ libExpect(_MockFlowView._FlowData.ViewState.PanY).to.equal(0);
228
+ libExpect(_MockFlowView._FlowData.ViewState.Zoom).to.equal(1);
229
+ libExpect(_MockFlowView._FlowData.ViewState.SelectedNodeHash).to.be.null;
230
+ fDone();
231
+ }
232
+ );
233
+
234
+ test
235
+ (
236
+ 'should reject null data',
237
+ function (fDone)
238
+ {
239
+ let tmpOriginalNodes = _MockFlowView._FlowData.Nodes;
240
+ _DataManager.setFlowData(null);
241
+ // Should not have changed
242
+ libExpect(_MockFlowView._FlowData.Nodes).to.equal(tmpOriginalNodes);
243
+ fDone();
244
+ }
245
+ );
246
+
247
+ test
248
+ (
249
+ 'should reject non-object data',
250
+ function (fDone)
251
+ {
252
+ let tmpOriginalNodes = _MockFlowView._FlowData.Nodes;
253
+ _DataManager.setFlowData('invalid');
254
+ libExpect(_MockFlowView._FlowData.Nodes).to.equal(tmpOriginalNodes);
255
+ fDone();
256
+ }
257
+ );
258
+
259
+ test
260
+ (
261
+ 'should call renderFlow when initialRenderComplete is true',
262
+ function (fDone)
263
+ {
264
+ let tmpRenderCalled = false;
265
+ _MockFlowView.renderFlow = function () { tmpRenderCalled = true; };
266
+ _MockFlowView.initialRenderComplete = true;
267
+
268
+ _DataManager.setFlowData({ Nodes: [] });
269
+
270
+ libExpect(tmpRenderCalled).to.be.true;
271
+ fDone();
272
+ }
273
+ );
274
+
275
+ test
276
+ (
277
+ 'should not call renderFlow when initialRenderComplete is false',
278
+ function (fDone)
279
+ {
280
+ let tmpRenderCalled = false;
281
+ _MockFlowView.renderFlow = function () { tmpRenderCalled = true; };
282
+ _MockFlowView.initialRenderComplete = false;
283
+
284
+ _DataManager.setFlowData({ Nodes: [] });
285
+
286
+ libExpect(tmpRenderCalled).to.be.false;
287
+ fDone();
288
+ }
289
+ );
290
+
291
+ test
292
+ (
293
+ 'should call loadPersistedLayouts when LayoutProvider exists',
294
+ function (fDone)
295
+ {
296
+ let tmpLoadCalled = false;
297
+ _MockFlowView._LayoutProvider.loadPersistedLayouts = function () { tmpLoadCalled = true; };
298
+
299
+ _DataManager.setFlowData({ Nodes: [] });
300
+
301
+ libExpect(tmpLoadCalled).to.be.true;
302
+ fDone();
303
+ }
304
+ );
305
+ }
306
+ );
307
+
308
+ // ---- addNode ----
309
+
310
+ suite
311
+ (
312
+ 'addNode',
313
+ function ()
314
+ {
315
+ test
316
+ (
317
+ 'should add a node with generated hash',
318
+ function (fDone)
319
+ {
320
+ let tmpNode = _DataManager.addNode('generic', 200, 300, 'Test Node');
321
+
322
+ libExpect(tmpNode).to.be.an('object');
323
+ libExpect(tmpNode.Hash).to.be.a('string').that.includes('node-');
324
+ libExpect(tmpNode.X).to.equal(200);
325
+ libExpect(tmpNode.Y).to.equal(300);
326
+ libExpect(tmpNode.Title).to.equal('Test Node');
327
+ libExpect(_MockFlowView._FlowData.Nodes).to.have.length(1);
328
+ fDone();
329
+ }
330
+ );
331
+
332
+ test
333
+ (
334
+ 'should use node type config for defaults',
335
+ function (fDone)
336
+ {
337
+ let tmpNode = _DataManager.addNode('action', 100, 100);
338
+
339
+ libExpect(tmpNode.Width).to.equal(200);
340
+ libExpect(tmpNode.Height).to.equal(100);
341
+ libExpect(tmpNode.Title).to.equal('Action');
342
+ libExpect(tmpNode.Ports).to.have.length(3);
343
+ fDone();
344
+ }
345
+ );
346
+
347
+ test
348
+ (
349
+ 'should use option defaults when type config is null',
350
+ function (fDone)
351
+ {
352
+ let tmpNode = _DataManager.addNode('unknown', 100, 100, 'My Node');
353
+
354
+ libExpect(tmpNode.Width).to.equal(160);
355
+ libExpect(tmpNode.Height).to.equal(80);
356
+ libExpect(tmpNode.Title).to.equal('My Node');
357
+ // Default ports (2 generic)
358
+ libExpect(tmpNode.Ports).to.have.length(2);
359
+ fDone();
360
+ }
361
+ );
362
+
363
+ test
364
+ (
365
+ 'should use DefaultNodeType when type is null',
366
+ function (fDone)
367
+ {
368
+ let tmpNode = _DataManager.addNode(null, 100, 100, 'Fallback');
369
+
370
+ libExpect(tmpNode.Type).to.equal('generic');
371
+ fDone();
372
+ }
373
+ );
374
+
375
+ test
376
+ (
377
+ 'should default position to 100,100 when not provided',
378
+ function (fDone)
379
+ {
380
+ let tmpNode = _DataManager.addNode('generic', 0, 0);
381
+
382
+ // 0 is falsy, so it should fall back to 100
383
+ // (This tests the || operator behavior)
384
+ libExpect(tmpNode.X).to.equal(100);
385
+ libExpect(tmpNode.Y).to.equal(100);
386
+ fDone();
387
+ }
388
+ );
389
+
390
+ test
391
+ (
392
+ 'should ensure all ports have unique hashes',
393
+ function (fDone)
394
+ {
395
+ let tmpNode = _DataManager.addNode('action', 100, 100);
396
+ let tmpHashes = tmpNode.Ports.map((p) => p.Hash);
397
+ let tmpUnique = new Set(tmpHashes);
398
+ libExpect(tmpUnique.size).to.equal(tmpHashes.length);
399
+ fDone();
400
+ }
401
+ );
402
+
403
+ test
404
+ (
405
+ 'should call renderFlow and marshalFromView',
406
+ function (fDone)
407
+ {
408
+ let tmpRenderCalled = false;
409
+ let tmpMarshalCalled = false;
410
+ _MockFlowView.renderFlow = function () { tmpRenderCalled = true; };
411
+ _DataManager.marshalFromView = function () { tmpMarshalCalled = true; };
412
+
413
+ _DataManager.addNode('generic', 100, 100);
414
+
415
+ libExpect(tmpRenderCalled).to.be.true;
416
+ libExpect(tmpMarshalCalled).to.be.true;
417
+ fDone();
418
+ }
419
+ );
420
+
421
+ test
422
+ (
423
+ 'should fire onNodeAdded and onFlowChanged events',
424
+ function (fDone)
425
+ {
426
+ let tmpEvents = [];
427
+ _MockFlowView._EventHandlerProvider.fireEvent = function (pEvent) { tmpEvents.push(pEvent); };
428
+
429
+ _DataManager.addNode('generic', 100, 100);
430
+
431
+ libExpect(tmpEvents).to.include('onNodeAdded');
432
+ libExpect(tmpEvents).to.include('onFlowChanged');
433
+ fDone();
434
+ }
435
+ );
436
+
437
+ test
438
+ (
439
+ 'should return null when no FlowView',
440
+ function (fDone)
441
+ {
442
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
443
+ let tmpResult = tmpManager.addNode('generic', 100, 100);
444
+ libExpect(tmpResult).to.be.null;
445
+ fDone();
446
+ }
447
+ );
448
+
449
+ test
450
+ (
451
+ 'should include custom data when provided',
452
+ function (fDone)
453
+ {
454
+ let tmpNode = _DataManager.addNode('generic', 100, 100, 'Test', { foo: 'bar' });
455
+ libExpect(tmpNode.Data).to.deep.equal({ foo: 'bar' });
456
+ fDone();
457
+ }
458
+ );
459
+ }
460
+ );
461
+
462
+ // ---- removeNode ----
463
+
464
+ suite
465
+ (
466
+ 'removeNode',
467
+ function ()
468
+ {
469
+ test
470
+ (
471
+ 'should remove a node by hash',
472
+ function (fDone)
473
+ {
474
+ let tmpNode = _DataManager.addNode('generic', 100, 100, 'ToRemove');
475
+ libExpect(_MockFlowView._FlowData.Nodes).to.have.length(1);
476
+
477
+ let tmpResult = _DataManager.removeNode(tmpNode.Hash);
478
+ libExpect(tmpResult).to.be.true;
479
+ libExpect(_MockFlowView._FlowData.Nodes).to.have.length(0);
480
+ fDone();
481
+ }
482
+ );
483
+
484
+ test
485
+ (
486
+ 'should return false for non-existent node',
487
+ function (fDone)
488
+ {
489
+ let tmpResult = _DataManager.removeNode('non-existent');
490
+ libExpect(tmpResult).to.be.false;
491
+ fDone();
492
+ }
493
+ );
494
+
495
+ test
496
+ (
497
+ 'should cascade-delete connections involving the node',
498
+ function (fDone)
499
+ {
500
+ // Set up two nodes with a connection
501
+ _MockFlowView._FlowData.Nodes = [
502
+ { Hash: 'n1', Ports: [{ Hash: 'p1', Direction: 'output' }] },
503
+ { Hash: 'n2', Ports: [{ Hash: 'p2', Direction: 'input' }] }
504
+ ];
505
+ _MockFlowView._FlowData.Connections = [
506
+ { Hash: 'c1', SourceNodeHash: 'n1', SourcePortHash: 'p1', TargetNodeHash: 'n2', TargetPortHash: 'p2' }
507
+ ];
508
+
509
+ _DataManager.removeNode('n1');
510
+
511
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(0);
512
+ fDone();
513
+ }
514
+ );
515
+
516
+ test
517
+ (
518
+ 'should close open panels for the node',
519
+ function (fDone)
520
+ {
521
+ _MockFlowView._FlowData.Nodes = [{ Hash: 'n1' }];
522
+ _MockFlowView._FlowData.OpenPanels = [{ Hash: 'panel-1', NodeHash: 'n1' }];
523
+
524
+ _DataManager.removeNode('n1');
525
+
526
+ libExpect(_MockFlowView._FlowData.OpenPanels).to.have.length(0);
527
+ fDone();
528
+ }
529
+ );
530
+
531
+ test
532
+ (
533
+ 'should clear selection if removed node was selected',
534
+ function (fDone)
535
+ {
536
+ _MockFlowView._FlowData.Nodes = [{ Hash: 'n1' }];
537
+ _MockFlowView._FlowData.ViewState.SelectedNodeHash = 'n1';
538
+
539
+ _DataManager.removeNode('n1');
540
+
541
+ libExpect(_MockFlowView._FlowData.ViewState.SelectedNodeHash).to.be.null;
542
+ fDone();
543
+ }
544
+ );
545
+
546
+ test
547
+ (
548
+ 'should fire onNodeRemoved and onFlowChanged events',
549
+ function (fDone)
550
+ {
551
+ _MockFlowView._FlowData.Nodes = [{ Hash: 'n1' }];
552
+ let tmpEvents = [];
553
+ _MockFlowView._EventHandlerProvider.fireEvent = function (pEvent) { tmpEvents.push(pEvent); };
554
+
555
+ _DataManager.removeNode('n1');
556
+
557
+ libExpect(tmpEvents).to.include('onNodeRemoved');
558
+ libExpect(tmpEvents).to.include('onFlowChanged');
559
+ fDone();
560
+ }
561
+ );
562
+
563
+ test
564
+ (
565
+ 'should return false when no FlowView',
566
+ function (fDone)
567
+ {
568
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
569
+ let tmpResult = tmpManager.removeNode('n1');
570
+ libExpect(tmpResult).to.be.false;
571
+ fDone();
572
+ }
573
+ );
574
+ }
575
+ );
576
+
577
+ // ---- addConnection ----
578
+
579
+ suite
580
+ (
581
+ 'addConnection',
582
+ function ()
583
+ {
584
+ setup
585
+ (
586
+ function ()
587
+ {
588
+ // Set up two nodes with ports
589
+ _MockFlowView._FlowData.Nodes = [
590
+ {
591
+ Hash: 'n1',
592
+ Ports: [
593
+ { Hash: 'p-out-1', Direction: 'output', Side: 'right', Label: 'Out' }
594
+ ]
595
+ },
596
+ {
597
+ Hash: 'n2',
598
+ Ports: [
599
+ { Hash: 'p-in-2', Direction: 'input', Side: 'left', Label: 'In' }
600
+ ]
601
+ }
602
+ ];
603
+ _MockFlowView._FlowData.Connections = [];
604
+ }
605
+ );
606
+
607
+ test
608
+ (
609
+ 'should create a connection between valid nodes/ports',
610
+ function (fDone)
611
+ {
612
+ let tmpConn = _DataManager.addConnection('n1', 'p-out-1', 'n2', 'p-in-2');
613
+
614
+ libExpect(tmpConn).to.be.an('object');
615
+ libExpect(tmpConn.Hash).to.be.a('string').that.includes('conn-');
616
+ libExpect(tmpConn.SourceNodeHash).to.equal('n1');
617
+ libExpect(tmpConn.TargetNodeHash).to.equal('n2');
618
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(1);
619
+ fDone();
620
+ }
621
+ );
622
+
623
+ test
624
+ (
625
+ 'should return false for missing source node',
626
+ function (fDone)
627
+ {
628
+ let tmpResult = _DataManager.addConnection('non-existent', 'p-out-1', 'n2', 'p-in-2');
629
+ libExpect(tmpResult).to.be.false;
630
+ fDone();
631
+ }
632
+ );
633
+
634
+ test
635
+ (
636
+ 'should return false for missing target node',
637
+ function (fDone)
638
+ {
639
+ let tmpResult = _DataManager.addConnection('n1', 'p-out-1', 'non-existent', 'p-in-2');
640
+ libExpect(tmpResult).to.be.false;
641
+ fDone();
642
+ }
643
+ );
644
+
645
+ test
646
+ (
647
+ 'should return false for missing source port',
648
+ function (fDone)
649
+ {
650
+ let tmpResult = _DataManager.addConnection('n1', 'bad-port', 'n2', 'p-in-2');
651
+ libExpect(tmpResult).to.be.false;
652
+ fDone();
653
+ }
654
+ );
655
+
656
+ test
657
+ (
658
+ 'should return false for missing target port',
659
+ function (fDone)
660
+ {
661
+ let tmpResult = _DataManager.addConnection('n1', 'p-out-1', 'n2', 'bad-port');
662
+ libExpect(tmpResult).to.be.false;
663
+ fDone();
664
+ }
665
+ );
666
+
667
+ test
668
+ (
669
+ 'should prevent self-connections',
670
+ function (fDone)
671
+ {
672
+ _MockFlowView._FlowData.Nodes[0].Ports.push(
673
+ { Hash: 'p-in-1', Direction: 'input', Side: 'left', Label: 'In' }
674
+ );
675
+
676
+ let tmpResult = _DataManager.addConnection('n1', 'p-out-1', 'n1', 'p-in-1');
677
+ libExpect(tmpResult).to.be.false;
678
+ fDone();
679
+ }
680
+ );
681
+
682
+ test
683
+ (
684
+ 'should prevent duplicate connections',
685
+ function (fDone)
686
+ {
687
+ _DataManager.addConnection('n1', 'p-out-1', 'n2', 'p-in-2');
688
+ let tmpResult = _DataManager.addConnection('n1', 'p-out-1', 'n2', 'p-in-2');
689
+
690
+ libExpect(tmpResult).to.be.false;
691
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(1);
692
+ fDone();
693
+ }
694
+ );
695
+
696
+ test
697
+ (
698
+ 'should include custom data when provided',
699
+ function (fDone)
700
+ {
701
+ let tmpConn = _DataManager.addConnection('n1', 'p-out-1', 'n2', 'p-in-2', { LineMode: 'bezier' });
702
+ libExpect(tmpConn.Data).to.deep.equal({ LineMode: 'bezier' });
703
+ fDone();
704
+ }
705
+ );
706
+
707
+ test
708
+ (
709
+ 'should fire onConnectionCreated and onFlowChanged events',
710
+ function (fDone)
711
+ {
712
+ let tmpEvents = [];
713
+ _MockFlowView._EventHandlerProvider.fireEvent = function (pEvent) { tmpEvents.push(pEvent); };
714
+
715
+ _DataManager.addConnection('n1', 'p-out-1', 'n2', 'p-in-2');
716
+
717
+ libExpect(tmpEvents).to.include('onConnectionCreated');
718
+ libExpect(tmpEvents).to.include('onFlowChanged');
719
+ fDone();
720
+ }
721
+ );
722
+
723
+ test
724
+ (
725
+ 'should return false when no FlowView',
726
+ function (fDone)
727
+ {
728
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
729
+ let tmpResult = tmpManager.addConnection('n1', 'p1', 'n2', 'p2');
730
+ libExpect(tmpResult).to.be.false;
731
+ fDone();
732
+ }
733
+ );
734
+ }
735
+ );
736
+
737
+ // ---- removeConnection ----
738
+
739
+ suite
740
+ (
741
+ 'removeConnection',
742
+ function ()
743
+ {
744
+ test
745
+ (
746
+ 'should remove a connection by hash',
747
+ function (fDone)
748
+ {
749
+ _MockFlowView._FlowData.Nodes = [
750
+ { Hash: 'n1', Ports: [{ Hash: 'p1', Direction: 'output' }] },
751
+ { Hash: 'n2', Ports: [{ Hash: 'p2', Direction: 'input' }] }
752
+ ];
753
+
754
+ let tmpConn = _DataManager.addConnection('n1', 'p1', 'n2', 'p2');
755
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(1);
756
+
757
+ let tmpResult = _DataManager.removeConnection(tmpConn.Hash);
758
+ libExpect(tmpResult).to.be.true;
759
+ libExpect(_MockFlowView._FlowData.Connections).to.have.length(0);
760
+ fDone();
761
+ }
762
+ );
763
+
764
+ test
765
+ (
766
+ 'should return false for non-existent connection',
767
+ function (fDone)
768
+ {
769
+ let tmpResult = _DataManager.removeConnection('non-existent');
770
+ libExpect(tmpResult).to.be.false;
771
+ fDone();
772
+ }
773
+ );
774
+
775
+ test
776
+ (
777
+ 'should clear selection if removed connection was selected',
778
+ function (fDone)
779
+ {
780
+ _MockFlowView._FlowData.Connections = [
781
+ { Hash: 'c1', SourceNodeHash: 'n1', TargetNodeHash: 'n2' }
782
+ ];
783
+ _MockFlowView._FlowData.ViewState.SelectedConnectionHash = 'c1';
784
+
785
+ _DataManager.removeConnection('c1');
786
+
787
+ libExpect(_MockFlowView._FlowData.ViewState.SelectedConnectionHash).to.be.null;
788
+ fDone();
789
+ }
790
+ );
791
+
792
+ test
793
+ (
794
+ 'should fire onConnectionRemoved and onFlowChanged events',
795
+ function (fDone)
796
+ {
797
+ _MockFlowView._FlowData.Connections = [
798
+ { Hash: 'c1', SourceNodeHash: 'n1', TargetNodeHash: 'n2' }
799
+ ];
800
+ let tmpEvents = [];
801
+ _MockFlowView._EventHandlerProvider.fireEvent = function (pEvent) { tmpEvents.push(pEvent); };
802
+
803
+ _DataManager.removeConnection('c1');
804
+
805
+ libExpect(tmpEvents).to.include('onConnectionRemoved');
806
+ libExpect(tmpEvents).to.include('onFlowChanged');
807
+ fDone();
808
+ }
809
+ );
810
+
811
+ test
812
+ (
813
+ 'should return false when no FlowView',
814
+ function (fDone)
815
+ {
816
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
817
+ let tmpResult = tmpManager.removeConnection('c1');
818
+ libExpect(tmpResult).to.be.false;
819
+ fDone();
820
+ }
821
+ );
822
+ }
823
+ );
824
+
825
+ // ---- marshalToView / marshalFromView ----
826
+
827
+ suite
828
+ (
829
+ 'marshalToView and marshalFromView',
830
+ function ()
831
+ {
832
+ test
833
+ (
834
+ 'should be no-ops when FlowDataAddress is not set',
835
+ function (fDone)
836
+ {
837
+ // marshalToView with no FlowDataAddress should not crash
838
+ _DataManager.marshalToView();
839
+ // marshalFromView with no FlowDataAddress should not crash
840
+ _DataManager.marshalFromView();
841
+ fDone();
842
+ }
843
+ );
844
+
845
+ test
846
+ (
847
+ 'should be no-ops when no FlowView',
848
+ function (fDone)
849
+ {
850
+ let tmpManager = new libDataManager(_Fable, {}, 'NoView');
851
+ tmpManager.marshalToView();
852
+ tmpManager.marshalFromView();
853
+ fDone();
854
+ }
855
+ );
856
+ }
857
+ );
858
+ }
859
+ );