pict-section-flow 1.1.0 → 1.3.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.
- package/package.json +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +23 -0
- package/source/providers/layouts/Layout-Layered.js +25 -79
- package/source/providers/layouts/Layout-Rank.js +141 -0
- package/source/providers/layouts/Layout-Staggered.js +131 -0
- package/source/services/PictService-Flow-DataManager.js +1 -1
- package/source/services/PictService-Flow-InteractionManager.js +354 -22
- package/source/services/PictService-Flow-Layout.js +2 -0
- package/source/services/PictService-Flow-RenderManager.js +3 -1
- package/source/services/PictService-Flow-SelectionManager.js +86 -5
- package/source/views/PictView-Flow-Node.js +90 -6
- package/source/views/PictView-Flow.js +48 -8
- package/test/InteractionManager_tests.js +279 -0
- package/test/Layout_tests.js +208 -4
- package/test/NodeView_tests.js +66 -0
- package/test/SelectionManager_tests.js +185 -0
package/test/Layout_tests.js
CHANGED
|
@@ -6,6 +6,8 @@ const libLayoutService = require('../source/services/PictService-Flow-Layout.js'
|
|
|
6
6
|
|
|
7
7
|
const libLayoutCustom = require('../source/providers/layouts/Layout-Custom.js');
|
|
8
8
|
const libLayoutLayered = require('../source/providers/layouts/Layout-Layered.js');
|
|
9
|
+
const libLayoutStaggered = require('../source/providers/layouts/Layout-Staggered.js');
|
|
10
|
+
const libLayoutRank = require('../source/providers/layouts/Layout-Rank.js');
|
|
9
11
|
const libLayoutForcedFromCenter = require('../source/providers/layouts/Layout-ForcedFromCenter.js');
|
|
10
12
|
const libLayoutGrid = require('../source/providers/layouts/Layout-Grid.js');
|
|
11
13
|
const libLayoutCircular = require('../source/providers/layouts/Layout-Circular.js');
|
|
@@ -86,15 +88,15 @@ suite
|
|
|
86
88
|
|
|
87
89
|
test
|
|
88
90
|
(
|
|
89
|
-
'should register all
|
|
91
|
+
'should register all eight built-in algorithms by default',
|
|
90
92
|
function (fDone)
|
|
91
93
|
{
|
|
92
94
|
let tmpNames = _LayoutService.getAlgorithmNames();
|
|
93
95
|
libExpect(tmpNames).to.include.members([
|
|
94
|
-
'Custom', 'Layered', 'ForcedFromCenter',
|
|
96
|
+
'Custom', 'Layered', 'Staggered', 'ForcedFromCenter',
|
|
95
97
|
'Grid', 'Circular', 'Tabular', 'Columnar'
|
|
96
98
|
]);
|
|
97
|
-
libExpect(tmpNames.length).to.equal(
|
|
99
|
+
libExpect(tmpNames.length).to.equal(8);
|
|
98
100
|
fDone();
|
|
99
101
|
}
|
|
100
102
|
);
|
|
@@ -184,9 +186,10 @@ suite
|
|
|
184
186
|
function (fDone)
|
|
185
187
|
{
|
|
186
188
|
let tmpAll = _LayoutService.listAlgorithms();
|
|
187
|
-
libExpect(tmpAll.length).to.equal(
|
|
189
|
+
libExpect(tmpAll.length).to.equal(8);
|
|
188
190
|
let tmpNames = tmpAll.map((pA) => pA.Name);
|
|
189
191
|
libExpect(tmpNames).to.include('Custom');
|
|
192
|
+
libExpect(tmpNames).to.include('Staggered');
|
|
190
193
|
libExpect(tmpNames).to.include('ForcedFromCenter');
|
|
191
194
|
fDone();
|
|
192
195
|
}
|
|
@@ -315,6 +318,207 @@ suite
|
|
|
315
318
|
fDone();
|
|
316
319
|
}
|
|
317
320
|
);
|
|
321
|
+
|
|
322
|
+
test
|
|
323
|
+
(
|
|
324
|
+
'a back-edge cycle does NOT collapse into one column (regression)',
|
|
325
|
+
function (fDone)
|
|
326
|
+
{
|
|
327
|
+
// n0 -> n1 -> n2 -> n3 -> n4 with a back-edge n4 -> n1.
|
|
328
|
+
// Plain Kahn's would place n0, then dump n1..n4 into a single
|
|
329
|
+
// trailing layer (one tall column). The cycle-tolerant ranker
|
|
330
|
+
// must spread them across columns instead.
|
|
331
|
+
let tmpNodes = makeNodes(5);
|
|
332
|
+
let tmpConns = makeChain(5);
|
|
333
|
+
tmpConns.push({ Hash: 'c-back', SourceNodeHash: 'n-4', TargetNodeHash: 'n-1' });
|
|
334
|
+
|
|
335
|
+
libLayoutLayered.Apply(tmpNodes, tmpConns, libLayoutLayered.DefaultParameters);
|
|
336
|
+
|
|
337
|
+
let tmpColumns = {};
|
|
338
|
+
let tmpMaxPerColumn = 0;
|
|
339
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
340
|
+
{
|
|
341
|
+
let tmpX = tmpNodes[i].X;
|
|
342
|
+
tmpColumns[tmpX] = (tmpColumns[tmpX] || 0) + 1;
|
|
343
|
+
tmpMaxPerColumn = Math.max(tmpMaxPerColumn, tmpColumns[tmpX]);
|
|
344
|
+
}
|
|
345
|
+
// Five distinct columns, one node each — no tower.
|
|
346
|
+
libExpect(Object.keys(tmpColumns).length).to.equal(5);
|
|
347
|
+
libExpect(tmpMaxPerColumn).to.equal(1);
|
|
348
|
+
fDone();
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
test
|
|
353
|
+
(
|
|
354
|
+
'a self-loop does not strand a node in a trailing column',
|
|
355
|
+
function (fDone)
|
|
356
|
+
{
|
|
357
|
+
// n0 -> n1 -> n2 with a self-loop on n1.
|
|
358
|
+
let tmpNodes = makeNodes(3);
|
|
359
|
+
let tmpConns = makeChain(3);
|
|
360
|
+
tmpConns.push({ Hash: 'c-self', SourceNodeHash: 'n-1', TargetNodeHash: 'n-1' });
|
|
361
|
+
|
|
362
|
+
libLayoutLayered.Apply(tmpNodes, tmpConns, libLayoutLayered.DefaultParameters);
|
|
363
|
+
|
|
364
|
+
// Clean chain: three columns left to right, one node each.
|
|
365
|
+
libExpect(tmpNodes[0].X).to.be.below(tmpNodes[1].X);
|
|
366
|
+
libExpect(tmpNodes[1].X).to.be.below(tmpNodes[2].X);
|
|
367
|
+
fDone();
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// ── Rank (shared ranker) ──────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
suite
|
|
376
|
+
(
|
|
377
|
+
'Layout-Rank ranker',
|
|
378
|
+
function ()
|
|
379
|
+
{
|
|
380
|
+
test
|
|
381
|
+
(
|
|
382
|
+
'a chain ranks one node per rank, in order',
|
|
383
|
+
function (fDone)
|
|
384
|
+
{
|
|
385
|
+
let tmpNodes = makeNodes(4);
|
|
386
|
+
let tmpRanks = libLayoutRank.toRanks(tmpNodes, makeChain(4));
|
|
387
|
+
libExpect(tmpRanks.length).to.equal(4);
|
|
388
|
+
libExpect(tmpRanks[0]).to.deep.equal(['n-0']);
|
|
389
|
+
libExpect(tmpRanks[3]).to.deep.equal(['n-3']);
|
|
390
|
+
fDone();
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
test
|
|
395
|
+
(
|
|
396
|
+
'unconnected nodes share the first rank',
|
|
397
|
+
function (fDone)
|
|
398
|
+
{
|
|
399
|
+
let tmpNodes = makeNodes(3);
|
|
400
|
+
let tmpRanks = libLayoutRank.toRanks(tmpNodes, []);
|
|
401
|
+
libExpect(tmpRanks.length).to.equal(1);
|
|
402
|
+
libExpect(tmpRanks[0].length).to.equal(3);
|
|
403
|
+
fDone();
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
test
|
|
408
|
+
(
|
|
409
|
+
'toOrder visits every node exactly once even with a cycle',
|
|
410
|
+
function (fDone)
|
|
411
|
+
{
|
|
412
|
+
let tmpNodes = makeNodes(5);
|
|
413
|
+
let tmpConns = makeChain(5);
|
|
414
|
+
tmpConns.push({ Hash: 'c-back', SourceNodeHash: 'n-4', TargetNodeHash: 'n-1' });
|
|
415
|
+
let tmpOrder = libLayoutRank.toOrder(tmpNodes, tmpConns);
|
|
416
|
+
libExpect(tmpOrder.length).to.equal(5);
|
|
417
|
+
let tmpSeen = {};
|
|
418
|
+
for (let i = 0; i < tmpOrder.length; i++) tmpSeen[tmpOrder[i]] = true;
|
|
419
|
+
libExpect(Object.keys(tmpSeen).length).to.equal(5);
|
|
420
|
+
fDone();
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
test
|
|
425
|
+
(
|
|
426
|
+
'empty input returns an empty rank list',
|
|
427
|
+
function (fDone)
|
|
428
|
+
{
|
|
429
|
+
libExpect(libLayoutRank.toRanks([], [])).to.deep.equal([]);
|
|
430
|
+
libExpect(libLayoutRank.toOrder(null, null)).to.deep.equal([]);
|
|
431
|
+
fDone();
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// ── Staggered ─────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
suite
|
|
440
|
+
(
|
|
441
|
+
'Staggered algorithm',
|
|
442
|
+
function ()
|
|
443
|
+
{
|
|
444
|
+
test
|
|
445
|
+
(
|
|
446
|
+
'two rows zigzag: X strictly increases, Y alternates',
|
|
447
|
+
function (fDone)
|
|
448
|
+
{
|
|
449
|
+
let tmpNodes = makeNodes(4);
|
|
450
|
+
let tmpConns = makeChain(4);
|
|
451
|
+
libLayoutStaggered.Apply(tmpNodes, tmpConns, { Rows: 2, ColumnSpacing: 80, RowOffset: 150, StartX: 0, StartY: 0 });
|
|
452
|
+
|
|
453
|
+
// Topological order is n0..n3; column pitch = 180 + 80 = 260.
|
|
454
|
+
libExpect(tmpNodes[0].X).to.equal(0);
|
|
455
|
+
libExpect(tmpNodes[1].X).to.equal(260);
|
|
456
|
+
libExpect(tmpNodes[2].X).to.equal(520);
|
|
457
|
+
libExpect(tmpNodes[3].X).to.equal(780);
|
|
458
|
+
// Rows=2 → row pattern 0,1,0,1 → Y 0,150,0,150.
|
|
459
|
+
libExpect(tmpNodes[0].Y).to.equal(0);
|
|
460
|
+
libExpect(tmpNodes[1].Y).to.equal(150);
|
|
461
|
+
libExpect(tmpNodes[2].Y).to.equal(0);
|
|
462
|
+
libExpect(tmpNodes[3].Y).to.equal(150);
|
|
463
|
+
fDone();
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
test
|
|
468
|
+
(
|
|
469
|
+
'three rows make a triangle-wave stairstep (down then up)',
|
|
470
|
+
function (fDone)
|
|
471
|
+
{
|
|
472
|
+
let tmpNodes = makeNodes(6);
|
|
473
|
+
let tmpConns = makeChain(6);
|
|
474
|
+
libLayoutStaggered.Apply(tmpNodes, tmpConns, { Rows: 3, RowOffset: 100, StartX: 0, StartY: 0 });
|
|
475
|
+
|
|
476
|
+
// period = 4 → row phases 0,1,2,1,0,1 → Y 0,100,200,100,0,100.
|
|
477
|
+
let tmpRows = tmpNodes.map((pN) => pN.Y / 100);
|
|
478
|
+
libExpect(tmpRows).to.deep.equal([0, 1, 2, 1, 0, 1]);
|
|
479
|
+
fDone();
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
test
|
|
484
|
+
(
|
|
485
|
+
'column pitch follows the widest node',
|
|
486
|
+
function (fDone)
|
|
487
|
+
{
|
|
488
|
+
let tmpNodes = makeNodes(3);
|
|
489
|
+
tmpNodes[1].Width = 400; // widest
|
|
490
|
+
libLayoutStaggered.Apply(tmpNodes, makeChain(3), { ColumnSpacing: 50, StartX: 0 });
|
|
491
|
+
// pitch = 400 + 50 = 450
|
|
492
|
+
libExpect(tmpNodes[1].X).to.equal(450);
|
|
493
|
+
libExpect(tmpNodes[2].X).to.equal(900);
|
|
494
|
+
fDone();
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
test
|
|
499
|
+
(
|
|
500
|
+
'Rows=1 places every node on a single row',
|
|
501
|
+
function (fDone)
|
|
502
|
+
{
|
|
503
|
+
let tmpNodes = makeNodes(4);
|
|
504
|
+
libLayoutStaggered.Apply(tmpNodes, makeChain(4), { Rows: 1, StartY: 42 });
|
|
505
|
+
for (let i = 0; i < tmpNodes.length; i++)
|
|
506
|
+
{
|
|
507
|
+
libExpect(tmpNodes[i].Y).to.equal(42);
|
|
508
|
+
}
|
|
509
|
+
fDone();
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
test
|
|
514
|
+
(
|
|
515
|
+
'empty node list does not throw',
|
|
516
|
+
function (fDone)
|
|
517
|
+
{
|
|
518
|
+
libExpect(function () { libLayoutStaggered.Apply([], [], {}); }).to.not.throw();
|
|
519
|
+
fDone();
|
|
520
|
+
}
|
|
521
|
+
);
|
|
318
522
|
}
|
|
319
523
|
);
|
|
320
524
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const libChai = require('chai');
|
|
2
|
+
const libExpect = libChai.expect;
|
|
3
|
+
|
|
4
|
+
const libPictViewFlowNode = require('../source/views/PictView-Flow-Node.js');
|
|
5
|
+
|
|
6
|
+
suite('PictView-Flow-Node',
|
|
7
|
+
function ()
|
|
8
|
+
{
|
|
9
|
+
// The title-bar bottom strip squares off the title bar's lower corners. The regression it guards
|
|
10
|
+
// against: a corner radius larger than the title bar made the strip taller than the whole title
|
|
11
|
+
// bar, so it painted over the rounded TOP corners and the card read as square on top (only the
|
|
12
|
+
// bottom rounded). See titleBarBottomStripHeight.
|
|
13
|
+
suite('titleBarBottomStripHeight',
|
|
14
|
+
function ()
|
|
15
|
+
{
|
|
16
|
+
test('never exceeds half the title bar height, even for a capsule radius',
|
|
17
|
+
function ()
|
|
18
|
+
{
|
|
19
|
+
// radius 24 on a 22px title bar must not produce a 24px strip (which would cover the top)
|
|
20
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(24, 22)).to.equal(11);
|
|
21
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(100, 30)).to.equal(15);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('covers small radii with the 8px floor',
|
|
25
|
+
function ()
|
|
26
|
+
{
|
|
27
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(5, 22)).to.equal(8);
|
|
28
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(0, 22)).to.equal(8);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('treats a null/absent radius as no override (8px floor)',
|
|
32
|
+
function ()
|
|
33
|
+
{
|
|
34
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(null, 22)).to.equal(8);
|
|
35
|
+
libExpect(libPictViewFlowNode.titleBarBottomStripHeight(undefined, 22)).to.equal(8);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('the strip is always at most the title bar height for any radius',
|
|
39
|
+
function ()
|
|
40
|
+
{
|
|
41
|
+
let tmpTitleBarHeight = 22;
|
|
42
|
+
for (let tmpRadius = 0; tmpRadius <= 60; tmpRadius++)
|
|
43
|
+
{
|
|
44
|
+
let tmpStrip = libPictViewFlowNode.titleBarBottomStripHeight(tmpRadius, tmpTitleBarHeight);
|
|
45
|
+
libExpect(tmpStrip).to.be.at.most(Math.floor(tmpTitleBarHeight / 2));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
suite('nodeTransform',
|
|
51
|
+
function ()
|
|
52
|
+
{
|
|
53
|
+
test('a zero / absent rotation is a plain position translate',
|
|
54
|
+
function ()
|
|
55
|
+
{
|
|
56
|
+
libExpect(libPictViewFlowNode.nodeTransform(40, 60, 0, 100, 80)).to.equal('translate(40, 60)');
|
|
57
|
+
libExpect(libPictViewFlowNode.nodeTransform(40, 60, null, 100, 80)).to.equal('translate(40, 60)');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a non-zero rotation rotates about the node center',
|
|
61
|
+
function ()
|
|
62
|
+
{
|
|
63
|
+
libExpect(libPictViewFlowNode.nodeTransform(40, 60, 15, 100, 80)).to.equal('translate(40, 60) rotate(15 50 40)');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const libFable = require('fable');
|
|
2
|
+
const libChai = require('chai');
|
|
3
|
+
const libExpect = libChai.expect;
|
|
4
|
+
|
|
5
|
+
const libSelectionManager = require('../source/services/PictService-Flow-SelectionManager.js');
|
|
6
|
+
|
|
7
|
+
// A minimal FlowView stand-in: just the surface the selection manager touches.
|
|
8
|
+
function makeMockFlowView(pNodes)
|
|
9
|
+
{
|
|
10
|
+
let tmpFired = [];
|
|
11
|
+
let tmpRemoved = [];
|
|
12
|
+
return {
|
|
13
|
+
_FlowData:
|
|
14
|
+
{
|
|
15
|
+
Nodes: pNodes.slice(),
|
|
16
|
+
Connections: [],
|
|
17
|
+
OpenPanels: [],
|
|
18
|
+
ViewState: { SelectedNodeHash: null, SelectedNodeHashes: [], SelectedConnectionHash: null, SelectedTetherHash: null }
|
|
19
|
+
},
|
|
20
|
+
renderFlow: function () { this._rendered = (this._rendered || 0) + 1; },
|
|
21
|
+
removeNode: function (pHash) { tmpRemoved.push(pHash); this._FlowData.Nodes = this._FlowData.Nodes.filter((n) => n.Hash !== pHash); return true; },
|
|
22
|
+
removeConnection: function (pHash) { tmpRemoved.push('conn:' + pHash); return true; },
|
|
23
|
+
_EventHandlerProvider: { fireEvent: function (pName, pPayload) { tmpFired.push({ Name: pName, Payload: pPayload }); } },
|
|
24
|
+
_firedEvents: tmpFired,
|
|
25
|
+
_removed: tmpRemoved
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeManager(pFable, pNodes)
|
|
30
|
+
{
|
|
31
|
+
let tmpFV = makeMockFlowView(pNodes);
|
|
32
|
+
let tmpSM = new libSelectionManager(pFable, { FlowView: tmpFV }, 'SM-Test');
|
|
33
|
+
return { sm: tmpSM, fv: tmpFV, vs: tmpFV._FlowData.ViewState };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
suite('PictService-Flow-SelectionManager',
|
|
37
|
+
function ()
|
|
38
|
+
{
|
|
39
|
+
let _Fable;
|
|
40
|
+
let _Nodes;
|
|
41
|
+
setup(function ()
|
|
42
|
+
{
|
|
43
|
+
_Fable = new libFable({});
|
|
44
|
+
_Nodes = [ { Hash: 'n1', X: 0, Y: 0, Width: 100, Height: 80 }, { Hash: 'n2', X: 200, Y: 0, Width: 100, Height: 80 }, { Hash: 'n3', X: 400, Y: 0, Width: 100, Height: 80 } ];
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
suite('single selection keeps the set in lockstep',
|
|
48
|
+
function ()
|
|
49
|
+
{
|
|
50
|
+
test('selectNode sets the primary and a one-element set',
|
|
51
|
+
function ()
|
|
52
|
+
{
|
|
53
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
54
|
+
tmp.sm.selectNode('n2');
|
|
55
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal('n2');
|
|
56
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n2']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('selectNode(null) clears both the primary and the set',
|
|
60
|
+
function ()
|
|
61
|
+
{
|
|
62
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
63
|
+
tmp.sm.selectNode('n2');
|
|
64
|
+
tmp.sm.selectNode(null);
|
|
65
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal(null);
|
|
66
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal([]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
suite('toggleNodeSelection',
|
|
71
|
+
function ()
|
|
72
|
+
{
|
|
73
|
+
test('adds a node, then a second, then removes the first',
|
|
74
|
+
function ()
|
|
75
|
+
{
|
|
76
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
77
|
+
tmp.sm.toggleNodeSelection('n1');
|
|
78
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n1']);
|
|
79
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal('n1');
|
|
80
|
+
|
|
81
|
+
tmp.sm.toggleNodeSelection('n3');
|
|
82
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n1', 'n3']);
|
|
83
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal('n3');
|
|
84
|
+
|
|
85
|
+
tmp.sm.toggleNodeSelection('n1');
|
|
86
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n3']);
|
|
87
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal('n3');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('toggling the last member empties the set and nulls the primary',
|
|
91
|
+
function ()
|
|
92
|
+
{
|
|
93
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
94
|
+
tmp.sm.toggleNodeSelection('n1');
|
|
95
|
+
tmp.sm.toggleNodeSelection('n1');
|
|
96
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal([]);
|
|
97
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal(null);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
suite('selectNodes',
|
|
102
|
+
function ()
|
|
103
|
+
{
|
|
104
|
+
test('replaces the set and sets the primary to the last hash',
|
|
105
|
+
function ()
|
|
106
|
+
{
|
|
107
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
108
|
+
tmp.sm.selectNode('n1');
|
|
109
|
+
tmp.sm.selectNodes(['n2', 'n3']);
|
|
110
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n2', 'n3']);
|
|
111
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal('n3');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('an empty array clears the selection',
|
|
115
|
+
function ()
|
|
116
|
+
{
|
|
117
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
118
|
+
tmp.sm.selectNodes(['n1', 'n2']);
|
|
119
|
+
tmp.sm.selectNodes([]);
|
|
120
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal([]);
|
|
121
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal(null);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('getSelectedNodeHashes returns a copy (mutating it does not change state)',
|
|
125
|
+
function ()
|
|
126
|
+
{
|
|
127
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
128
|
+
tmp.sm.selectNodes(['n1', 'n2']);
|
|
129
|
+
let tmpCopy = tmp.sm.getSelectedNodeHashes();
|
|
130
|
+
tmpCopy.push('n3');
|
|
131
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal(['n1', 'n2']);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
suite('deleteSelected (multi)',
|
|
136
|
+
function ()
|
|
137
|
+
{
|
|
138
|
+
test('removes every node in the selection set',
|
|
139
|
+
function ()
|
|
140
|
+
{
|
|
141
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
142
|
+
tmp.sm.selectNodes(['n1', 'n3']);
|
|
143
|
+
let tmpResult = tmp.sm.deleteSelected();
|
|
144
|
+
libExpect(tmpResult).to.equal(true);
|
|
145
|
+
libExpect(tmp.fv._removed).to.deep.equal(['n1', 'n3']);
|
|
146
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal([]);
|
|
147
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal(null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('falls back to the single primary when the set is empty',
|
|
151
|
+
function ()
|
|
152
|
+
{
|
|
153
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
154
|
+
tmp.fv._FlowData.ViewState.SelectedNodeHash = 'n2';
|
|
155
|
+
tmp.fv._FlowData.ViewState.SelectedNodeHashes = [];
|
|
156
|
+
tmp.sm.deleteSelected();
|
|
157
|
+
libExpect(tmp.fv._removed).to.deep.equal(['n2']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('deletes the selected connection when no nodes are selected',
|
|
161
|
+
function ()
|
|
162
|
+
{
|
|
163
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
164
|
+
tmp.fv._FlowData.ViewState.SelectedConnectionHash = 'c1';
|
|
165
|
+
tmp.sm.deleteSelected();
|
|
166
|
+
libExpect(tmp.fv._removed).to.deep.equal(['conn:c1']);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
suite('deselectAll',
|
|
171
|
+
function ()
|
|
172
|
+
{
|
|
173
|
+
test('clears the primary, the set, and the connection/tether selections',
|
|
174
|
+
function ()
|
|
175
|
+
{
|
|
176
|
+
let tmp = makeManager(_Fable, _Nodes);
|
|
177
|
+
tmp.sm.selectNodes(['n1', 'n2']);
|
|
178
|
+
tmp.fv._FlowData.ViewState.SelectedConnectionHash = 'c1';
|
|
179
|
+
tmp.sm.deselectAll();
|
|
180
|
+
libExpect(tmp.vs.SelectedNodeHash).to.equal(null);
|
|
181
|
+
libExpect(tmp.vs.SelectedNodeHashes).to.deep.equal([]);
|
|
182
|
+
libExpect(tmp.vs.SelectedConnectionHash).to.equal(null);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|