pict-section-flow 1.4.0 → 2.0.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 +7 -2
- package/source/Pict-Section-Flow.js +20 -14
- package/source/providers/PictProvider-Flow-Background.js +303 -0
- package/source/providers/PictProvider-Flow-CSS.js +73 -7
- package/source/providers/PictProvider-Flow-Geometry.js +11 -421
- package/source/providers/PictProvider-Flow-Icons.js +12 -0
- package/source/providers/PictProvider-Flow-Layouts.js +107 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +1 -1
- package/source/services/PictService-Flow-CursorManager.js +113 -0
- package/source/services/PictService-Flow-InteractionManager.js +439 -59
- package/source/services/PictService-Flow-Layout.js +21 -16
- package/source/services/PictService-Flow-PathGenerator.js +30 -417
- package/source/services/PictService-Flow-RenderManager.js +9 -1
- package/source/services/PictService-Flow-ViewportManager.js +102 -0
- package/source/views/PictView-Flow-FloatingToolbar.js +5 -1
- package/source/views/PictView-Flow-Node.js +29 -0
- package/source/views/PictView-Flow-Toolbar.js +50 -3
- package/source/views/PictView-Flow.js +591 -2
- package/.claude/launch.json +0 -11
- package/docs/.nojekyll +0 -0
- package/docs/Architecture.md +0 -163
- package/docs/Custom-Styling.md +0 -275
- package/docs/Data_Model.md +0 -149
- package/docs/Event_System.md +0 -156
- package/docs/Getting_Started.md +0 -237
- package/docs/Implementation_Reference.md +0 -528
- package/docs/Layout_Persistence.md +0 -117
- package/docs/README.md +0 -103
- package/docs/Theme_Integration.md +0 -150
- package/docs/_brand.json +0 -18
- package/docs/_cover.md +0 -17
- package/docs/_playground.json +0 -24
- package/docs/_sidebar.md +0 -57
- package/docs/_topbar.md +0 -8
- package/docs/_version.json +0 -7
- package/docs/api/PictFlowCard.md +0 -216
- package/docs/api/PictFlowCardPropertiesPanel.md +0 -235
- package/docs/api/addConnection.md +0 -101
- package/docs/api/addNode.md +0 -137
- package/docs/api/autoLayout.md +0 -77
- package/docs/api/getFlowData.md +0 -112
- package/docs/api/marshalToView.md +0 -95
- package/docs/api/openPanel.md +0 -128
- package/docs/api/registerHandler.md +0 -174
- package/docs/api/registerNodeType.md +0 -142
- package/docs/api/removeConnection.md +0 -57
- package/docs/api/removeNode.md +0 -80
- package/docs/api/saveLayout.md +0 -152
- package/docs/api/screenToSVGCoords.md +0 -68
- package/docs/api/selectNode.md +0 -116
- package/docs/api/setTheme.md +0 -168
- package/docs/api/setZoom.md +0 -97
- package/docs/api/toggleFullscreen.md +0 -68
- package/docs/card-help/EACH.md +0 -19
- package/docs/card-help/FREAD.md +0 -24
- package/docs/card-help/FWRITE.md +0 -24
- package/docs/card-help/GET.md +0 -22
- package/docs/card-help/ITE.md +0 -23
- package/docs/card-help/LOG.md +0 -23
- package/docs/card-help/NOTE.md +0 -17
- package/docs/card-help/PREV.md +0 -18
- package/docs/card-help/SET.md +0 -27
- package/docs/card-help/SPKL.md +0 -22
- package/docs/card-help/STAT.md +0 -23
- package/docs/card-help/SW.md +0 -25
- package/docs/diagrams/architecture-at-a-glance.excalidraw +0 -4270
- package/docs/diagrams/architecture-at-a-glance.mmd +0 -30
- package/docs/diagrams/architecture-at-a-glance.svg +0 -2
- package/docs/diagrams/data-flow.excalidraw +0 -1451
- package/docs/diagrams/data-flow.mmd +0 -17
- package/docs/diagrams/data-flow.svg +0 -2
- package/docs/diagrams/high-level-design.excalidraw +0 -5767
- package/docs/diagrams/high-level-design.mmd +0 -86
- package/docs/diagrams/high-level-design.svg +0 -2
- package/docs/diagrams/relationships.excalidraw +0 -3852
- package/docs/diagrams/relationships.mmd +0 -9
- package/docs/diagrams/relationships.svg +0 -2
- package/docs/diagrams/service-initialization-sequence.excalidraw +0 -1466
- package/docs/diagrams/service-initialization-sequence.mmd +0 -19
- package/docs/diagrams/service-initialization-sequence.svg +0 -2
- package/docs/diagrams/svg-layer-structure.excalidraw +0 -1060
- package/docs/diagrams/svg-layer-structure.mmd +0 -18
- package/docs/diagrams/svg-layer-structure.svg +0 -2
- package/docs/examples/README.md +0 -9
- package/docs/examples/simple_cards/README.md +0 -677
- package/docs/examples/simple_cards/css/flowexample.css +0 -65
- package/docs/examples/simple_cards/index.html +0 -32
- package/docs/examples/simple_cards/js/pict.min.js +0 -12
- package/docs/examples/simple_cards/pict-section-flow-example-simple-cards.compatible.min.js +0 -1
- package/docs/index.html +0 -38
- package/docs/playground/app.json +0 -6
- package/docs/playground/appdata.json +0 -85
- package/docs/playground/application.js +0 -23
- package/docs/playground/pict.json +0 -17
- package/docs/playground/runtime/pict-application.min.js +0 -2
- package/docs/playground/runtime/pict-section-flow.min.js +0 -2
- package/docs/playground/runtime/pict-section-modal.min.js +0 -2
- package/docs/playground/runtime/pict.min.js +0 -12
- package/docs/retold-catalog.json +0 -244
- package/docs/retold-keyword-index.json +0 -26028
- package/example_applications/simple_cards/css/flowexample.css +0 -65
- package/example_applications/simple_cards/html/index.html +0 -32
- package/example_applications/simple_cards/package.json +0 -52
- package/example_applications/simple_cards/source/Pict-Application-FlowExample-Configuration.json +0 -15
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +0 -539
- package/example_applications/simple_cards/source/card-help-content.js +0 -16
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +0 -38
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +0 -44
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +0 -38
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +0 -56
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +0 -50
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +0 -37
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +0 -49
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +0 -55
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +0 -97
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +0 -100
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +0 -46
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +0 -39
- package/example_applications/simple_cards/source/providers/PictRouter-FlowExample-Configuration.json +0 -22
- package/example_applications/simple_cards/source/sample-flows.js +0 -410
- package/example_applications/simple_cards/source/views/PictView-FlowExample-About.js +0 -184
- package/example_applications/simple_cards/source/views/PictView-FlowExample-BottomBar.js +0 -77
- package/example_applications/simple_cards/source/views/PictView-FlowExample-Documentation.js +0 -325
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +0 -59
- package/example_applications/simple_cards/source/views/PictView-FlowExample-Layout.js +0 -90
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +0 -453
- package/example_applications/simple_cards/source/views/PictView-FlowExample-TopBar.js +0 -95
- package/scripts/generate-card-help.js +0 -214
- package/source/providers/edges/Edge-Bezier.js +0 -41
- package/source/providers/edges/Edge-Orthogonal.js +0 -37
- package/source/providers/edges/Edge-OrthogonalSnap.js +0 -72
- package/source/providers/edges/Edge-Perimeter-Linear.js +0 -31
- package/source/providers/edges/Edge-Perimeter-Orthogonal.js +0 -39
- package/source/providers/edges/Edge-Perimeter.js +0 -48
- package/source/providers/edges/Edge-PerimeterMath.js +0 -92
- package/source/providers/edges/Edge-Straight.js +0 -24
- package/source/providers/layouts/Layout-Circular.js +0 -203
- package/source/providers/layouts/Layout-Coerce.js +0 -40
- package/source/providers/layouts/Layout-Columnar.js +0 -134
- package/source/providers/layouts/Layout-Custom.js +0 -27
- package/source/providers/layouts/Layout-ForcedFromCenter.js +0 -256
- package/source/providers/layouts/Layout-Grid.js +0 -134
- package/source/providers/layouts/Layout-Layered.js +0 -155
- package/source/providers/layouts/Layout-Rank.js +0 -141
- package/source/providers/layouts/Layout-Staggered.js +0 -131
- package/source/providers/layouts/Layout-Tabular.js +0 -94
- package/test/CardPalette_tests.js +0 -43
- package/test/ConnectionHandleManager_tests.js +0 -717
- package/test/ConnectionRenderer_tests.js +0 -591
- package/test/ConnectionStyle_tests.js +0 -90
- package/test/DataManager_tests.js +0 -859
- package/test/Geometry_tests.js +0 -767
- package/test/InteractionManager_tests.js +0 -279
- package/test/Layout_tests.js +0 -1604
- package/test/NodeView_tests.js +0 -66
- package/test/PanelManager_tests.js +0 -172
- package/test/PathGenerator_tests.js +0 -978
- package/test/PortRenderer_tests.js +0 -376
- package/test/RenderManager_tests.js +0 -756
- package/test/Renderer_tests.js +0 -133
- package/test/SelectionManager_tests.js +0 -185
- package/test/StylePresets_tests.js +0 -153
- package/test/ToolbarExtraButtons_tests.js +0 -138
- package/test/UndirectedConnections_tests.js +0 -70
package/test/Layout_tests.js
DELETED
|
@@ -1,1604 +0,0 @@
|
|
|
1
|
-
const libFable = require('fable');
|
|
2
|
-
const libChai = require('chai');
|
|
3
|
-
const libExpect = libChai.expect;
|
|
4
|
-
|
|
5
|
-
const libLayoutService = require('../source/services/PictService-Flow-Layout.js');
|
|
6
|
-
|
|
7
|
-
const libLayoutCustom = require('../source/providers/layouts/Layout-Custom.js');
|
|
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');
|
|
11
|
-
const libLayoutForcedFromCenter = require('../source/providers/layouts/Layout-ForcedFromCenter.js');
|
|
12
|
-
const libLayoutGrid = require('../source/providers/layouts/Layout-Grid.js');
|
|
13
|
-
const libLayoutCircular = require('../source/providers/layouts/Layout-Circular.js');
|
|
14
|
-
const libLayoutTabular = require('../source/providers/layouts/Layout-Tabular.js');
|
|
15
|
-
const libLayoutColumnar = require('../source/providers/layouts/Layout-Columnar.js');
|
|
16
|
-
|
|
17
|
-
const libEdgeBezier = require('../source/providers/edges/Edge-Bezier.js');
|
|
18
|
-
const libEdgeOrthogonal = require('../source/providers/edges/Edge-Orthogonal.js');
|
|
19
|
-
const libEdgeStraight = require('../source/providers/edges/Edge-Straight.js');
|
|
20
|
-
const libEdgeOrthogonalSnap = require('../source/providers/edges/Edge-OrthogonalSnap.js');
|
|
21
|
-
const libEdgePerimeter = require('../source/providers/edges/Edge-Perimeter.js');
|
|
22
|
-
|
|
23
|
-
function makeNodes(pCount)
|
|
24
|
-
{
|
|
25
|
-
let tmpNodes = [];
|
|
26
|
-
for (let i = 0; i < pCount; i++)
|
|
27
|
-
{
|
|
28
|
-
tmpNodes.push({
|
|
29
|
-
Hash: `n-${i}`,
|
|
30
|
-
Title: `Node ${i}`,
|
|
31
|
-
X: 0,
|
|
32
|
-
Y: 0,
|
|
33
|
-
Width: 180,
|
|
34
|
-
Height: 80
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return tmpNodes;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function makeChain(pCount)
|
|
41
|
-
{
|
|
42
|
-
let tmpConns = [];
|
|
43
|
-
for (let i = 0; i < pCount - 1; i++)
|
|
44
|
-
{
|
|
45
|
-
tmpConns.push({
|
|
46
|
-
Hash: `c-${i}`,
|
|
47
|
-
SourceNodeHash: `n-${i}`,
|
|
48
|
-
TargetNodeHash: `n-${i + 1}`
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return tmpConns;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
suite
|
|
55
|
-
(
|
|
56
|
-
'PictService-Flow-Layout',
|
|
57
|
-
function ()
|
|
58
|
-
{
|
|
59
|
-
let _Fable;
|
|
60
|
-
let _LayoutService;
|
|
61
|
-
|
|
62
|
-
setup
|
|
63
|
-
(
|
|
64
|
-
function ()
|
|
65
|
-
{
|
|
66
|
-
_Fable = new libFable({});
|
|
67
|
-
_LayoutService = new libLayoutService(_Fable, {}, 'Layout-Test');
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// ── Service constructor ───────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
suite
|
|
74
|
-
(
|
|
75
|
-
'Constructor',
|
|
76
|
-
function ()
|
|
77
|
-
{
|
|
78
|
-
test
|
|
79
|
-
(
|
|
80
|
-
'should instantiate with correct serviceType',
|
|
81
|
-
function (fDone)
|
|
82
|
-
{
|
|
83
|
-
libExpect(_LayoutService).to.be.an('object');
|
|
84
|
-
libExpect(_LayoutService.serviceType).to.equal('PictServiceFlowLayout');
|
|
85
|
-
fDone();
|
|
86
|
-
}
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
test
|
|
90
|
-
(
|
|
91
|
-
'should register all eight built-in algorithms by default',
|
|
92
|
-
function (fDone)
|
|
93
|
-
{
|
|
94
|
-
let tmpNames = _LayoutService.getAlgorithmNames();
|
|
95
|
-
libExpect(tmpNames).to.include.members([
|
|
96
|
-
'Custom', 'Layered', 'Staggered', 'ForcedFromCenter',
|
|
97
|
-
'Grid', 'Circular', 'Tabular', 'Columnar'
|
|
98
|
-
]);
|
|
99
|
-
libExpect(tmpNames.length).to.equal(8);
|
|
100
|
-
fDone();
|
|
101
|
-
}
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// ── Registry ──────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
suite
|
|
109
|
-
(
|
|
110
|
-
'Registry',
|
|
111
|
-
function ()
|
|
112
|
-
{
|
|
113
|
-
test
|
|
114
|
-
(
|
|
115
|
-
'getAlgorithm returns null for unknown name',
|
|
116
|
-
function (fDone)
|
|
117
|
-
{
|
|
118
|
-
libExpect(_LayoutService.getAlgorithm('NopeNope')).to.equal(null);
|
|
119
|
-
fDone();
|
|
120
|
-
}
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
test
|
|
124
|
-
(
|
|
125
|
-
'getAlgorithm returns a descriptor with required fields',
|
|
126
|
-
function (fDone)
|
|
127
|
-
{
|
|
128
|
-
let tmpAlgo = _LayoutService.getAlgorithm('Layered');
|
|
129
|
-
libExpect(tmpAlgo).to.be.an('object');
|
|
130
|
-
libExpect(tmpAlgo.Name).to.equal('Layered');
|
|
131
|
-
libExpect(tmpAlgo.Apply).to.be.a('function');
|
|
132
|
-
libExpect(tmpAlgo.DefaultParameters).to.be.an('object');
|
|
133
|
-
libExpect(tmpAlgo.ParameterSchema).to.be.an('object');
|
|
134
|
-
fDone();
|
|
135
|
-
}
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
test
|
|
139
|
-
(
|
|
140
|
-
'registerAlgorithm round-trips a custom descriptor',
|
|
141
|
-
function (fDone)
|
|
142
|
-
{
|
|
143
|
-
let tmpCustom =
|
|
144
|
-
{
|
|
145
|
-
Name: 'TestStub',
|
|
146
|
-
Label: 'Test Stub',
|
|
147
|
-
Apply: function (pNodes)
|
|
148
|
-
{
|
|
149
|
-
for (let i = 0; i < pNodes.length; i++)
|
|
150
|
-
{
|
|
151
|
-
pNodes[i].X = 999;
|
|
152
|
-
pNodes[i].Y = i;
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
DefaultParameters: {},
|
|
156
|
-
ParameterSchema: {}
|
|
157
|
-
};
|
|
158
|
-
let tmpResult = _LayoutService.registerAlgorithm(tmpCustom);
|
|
159
|
-
libExpect(tmpResult).to.equal(true);
|
|
160
|
-
libExpect(_LayoutService.getAlgorithm('TestStub')).to.equal(tmpCustom);
|
|
161
|
-
|
|
162
|
-
let tmpNodes = makeNodes(3);
|
|
163
|
-
_LayoutService.applyLayout(tmpNodes, [], 'TestStub', {});
|
|
164
|
-
libExpect(tmpNodes[0].X).to.equal(999);
|
|
165
|
-
libExpect(tmpNodes[2].Y).to.equal(2);
|
|
166
|
-
fDone();
|
|
167
|
-
}
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
test
|
|
171
|
-
(
|
|
172
|
-
'registerAlgorithm rejects invalid descriptors',
|
|
173
|
-
function (fDone)
|
|
174
|
-
{
|
|
175
|
-
libExpect(_LayoutService.registerAlgorithm(null)).to.equal(false);
|
|
176
|
-
libExpect(_LayoutService.registerAlgorithm({})).to.equal(false);
|
|
177
|
-
libExpect(_LayoutService.registerAlgorithm({ Name: 'X' })).to.equal(false);
|
|
178
|
-
libExpect(_LayoutService.registerAlgorithm({ Apply: function () {} })).to.equal(false);
|
|
179
|
-
fDone();
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
test
|
|
184
|
-
(
|
|
185
|
-
'listAlgorithms returns descriptors for all registered algorithms',
|
|
186
|
-
function (fDone)
|
|
187
|
-
{
|
|
188
|
-
let tmpAll = _LayoutService.listAlgorithms();
|
|
189
|
-
libExpect(tmpAll.length).to.equal(8);
|
|
190
|
-
let tmpNames = tmpAll.map((pA) => pA.Name);
|
|
191
|
-
libExpect(tmpNames).to.include('Custom');
|
|
192
|
-
libExpect(tmpNames).to.include('Staggered');
|
|
193
|
-
libExpect(tmpNames).to.include('ForcedFromCenter');
|
|
194
|
-
fDone();
|
|
195
|
-
}
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
// ── Backwards compatibility ───────────────────────────────────
|
|
201
|
-
|
|
202
|
-
suite
|
|
203
|
-
(
|
|
204
|
-
'Backwards compatibility',
|
|
205
|
-
function ()
|
|
206
|
-
{
|
|
207
|
-
test
|
|
208
|
-
(
|
|
209
|
-
'autoLayout(nodes, connections) — 2-arg legacy form dispatches to Layered',
|
|
210
|
-
function (fDone)
|
|
211
|
-
{
|
|
212
|
-
// Build a 3-node chain n-0 -> n-1 -> n-2 and assert Layered positions
|
|
213
|
-
let tmpNodes = makeNodes(3);
|
|
214
|
-
let tmpConns = makeChain(3);
|
|
215
|
-
|
|
216
|
-
_LayoutService.autoLayout(tmpNodes, tmpConns);
|
|
217
|
-
|
|
218
|
-
// Layered defaults: HorizontalSpacing 250, VerticalSpacing 120, Start (100, 100)
|
|
219
|
-
// Expected: each node in its own layer, X advances by node-width (180) + 250
|
|
220
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
221
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
222
|
-
libExpect(tmpNodes[1].X).to.equal(100 + 180 + 250);
|
|
223
|
-
libExpect(tmpNodes[1].Y).to.equal(100);
|
|
224
|
-
libExpect(tmpNodes[2].X).to.equal(100 + (180 + 250) * 2);
|
|
225
|
-
libExpect(tmpNodes[2].Y).to.equal(100);
|
|
226
|
-
fDone();
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
test
|
|
231
|
-
(
|
|
232
|
-
'autoLayout — empty nodes is a no-op',
|
|
233
|
-
function (fDone)
|
|
234
|
-
{
|
|
235
|
-
_LayoutService.autoLayout([], []);
|
|
236
|
-
_LayoutService.autoLayout(null, []);
|
|
237
|
-
fDone();
|
|
238
|
-
}
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
test
|
|
242
|
-
(
|
|
243
|
-
'autoLayoutSubset — places orphans to the right of fixed nodes (always Layered)',
|
|
244
|
-
function (fDone)
|
|
245
|
-
{
|
|
246
|
-
let tmpFixed = [
|
|
247
|
-
{ Hash: 'fix-0', X: 0, Y: 0, Width: 100, Height: 50 },
|
|
248
|
-
{ Hash: 'fix-1', X: 200, Y: 0, Width: 100, Height: 50 }
|
|
249
|
-
];
|
|
250
|
-
let tmpOrphans = makeNodes(2);
|
|
251
|
-
_LayoutService.autoLayoutSubset(tmpOrphans, tmpFixed, []);
|
|
252
|
-
// Right-edge of fix-1 is 300; +HorizontalSpacing(250) = 550
|
|
253
|
-
libExpect(tmpOrphans[0].X).to.equal(550);
|
|
254
|
-
fDone();
|
|
255
|
-
}
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
test
|
|
259
|
-
(
|
|
260
|
-
'snapToGrid rounds to grid size',
|
|
261
|
-
function (fDone)
|
|
262
|
-
{
|
|
263
|
-
libExpect(_LayoutService.snapToGrid(13, 10)).to.equal(10);
|
|
264
|
-
libExpect(_LayoutService.snapToGrid(16, 10)).to.equal(20);
|
|
265
|
-
libExpect(_LayoutService.snapToGrid(50, 0)).to.equal(50); // disabled
|
|
266
|
-
fDone();
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
// ── Layered ───────────────────────────────────────────────────
|
|
273
|
-
|
|
274
|
-
suite
|
|
275
|
-
(
|
|
276
|
-
'Layered algorithm',
|
|
277
|
-
function ()
|
|
278
|
-
{
|
|
279
|
-
test
|
|
280
|
-
(
|
|
281
|
-
'one node lands at StartX/StartY',
|
|
282
|
-
function (fDone)
|
|
283
|
-
{
|
|
284
|
-
let tmpNodes = makeNodes(1);
|
|
285
|
-
libLayoutLayered.Apply(tmpNodes, [], libLayoutLayered.DefaultParameters);
|
|
286
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
287
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
288
|
-
fDone();
|
|
289
|
-
}
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
test
|
|
293
|
-
(
|
|
294
|
-
'parallel siblings stack vertically',
|
|
295
|
-
function (fDone)
|
|
296
|
-
{
|
|
297
|
-
// Two roots, no connections — they end up in the same layer
|
|
298
|
-
let tmpNodes = makeNodes(2);
|
|
299
|
-
libLayoutLayered.Apply(tmpNodes, [], libLayoutLayered.DefaultParameters);
|
|
300
|
-
libExpect(tmpNodes[0].X).to.equal(tmpNodes[1].X);
|
|
301
|
-
libExpect(tmpNodes[1].Y).to.equal(tmpNodes[0].Y + 80 + 120);
|
|
302
|
-
fDone();
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
test
|
|
307
|
-
(
|
|
308
|
-
'caller-supplied params override defaults',
|
|
309
|
-
function (fDone)
|
|
310
|
-
{
|
|
311
|
-
let tmpNodes = makeNodes(2);
|
|
312
|
-
let tmpConns = makeChain(2);
|
|
313
|
-
libLayoutLayered.Apply(tmpNodes, tmpConns, { HorizontalSpacing: 50, VerticalSpacing: 30, StartX: 0, StartY: 0 });
|
|
314
|
-
libExpect(tmpNodes[0].X).to.equal(0);
|
|
315
|
-
libExpect(tmpNodes[0].Y).to.equal(0);
|
|
316
|
-
libExpect(tmpNodes[1].X).to.equal(180 + 50);
|
|
317
|
-
libExpect(tmpNodes[1].Y).to.equal(0);
|
|
318
|
-
fDone();
|
|
319
|
-
}
|
|
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
|
-
);
|
|
522
|
-
}
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
// ── ForcedFromCenter ──────────────────────────────────────────
|
|
526
|
-
|
|
527
|
-
suite
|
|
528
|
-
(
|
|
529
|
-
'ForcedFromCenter algorithm',
|
|
530
|
-
function ()
|
|
531
|
-
{
|
|
532
|
-
test
|
|
533
|
-
(
|
|
534
|
-
'is deterministic for a fixed seed',
|
|
535
|
-
function (fDone)
|
|
536
|
-
{
|
|
537
|
-
let tmpNodesA = makeNodes(5);
|
|
538
|
-
let tmpNodesB = makeNodes(5);
|
|
539
|
-
let tmpConns = makeChain(5);
|
|
540
|
-
|
|
541
|
-
let tmpParams = Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 12345, Iterations: 50 });
|
|
542
|
-
libLayoutForcedFromCenter.Apply(tmpNodesA, tmpConns, tmpParams);
|
|
543
|
-
libLayoutForcedFromCenter.Apply(tmpNodesB, tmpConns, tmpParams);
|
|
544
|
-
|
|
545
|
-
for (let i = 0; i < 5; i++)
|
|
546
|
-
{
|
|
547
|
-
libExpect(tmpNodesA[i].X).to.equal(tmpNodesB[i].X);
|
|
548
|
-
libExpect(tmpNodesA[i].Y).to.equal(tmpNodesB[i].Y);
|
|
549
|
-
}
|
|
550
|
-
fDone();
|
|
551
|
-
}
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
test
|
|
555
|
-
(
|
|
556
|
-
'different seeds produce different positions',
|
|
557
|
-
function (fDone)
|
|
558
|
-
{
|
|
559
|
-
let tmpNodesA = makeNodes(5);
|
|
560
|
-
let tmpNodesB = makeNodes(5);
|
|
561
|
-
let tmpConns = makeChain(5);
|
|
562
|
-
|
|
563
|
-
libLayoutForcedFromCenter.Apply(tmpNodesA, tmpConns, Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 1, Iterations: 50 }));
|
|
564
|
-
libLayoutForcedFromCenter.Apply(tmpNodesB, tmpConns, Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Seed: 2, Iterations: 50 }));
|
|
565
|
-
|
|
566
|
-
let tmpDifferent = false;
|
|
567
|
-
for (let i = 0; i < 5; i++)
|
|
568
|
-
{
|
|
569
|
-
if (tmpNodesA[i].X !== tmpNodesB[i].X || tmpNodesA[i].Y !== tmpNodesB[i].Y)
|
|
570
|
-
{
|
|
571
|
-
tmpDifferent = true;
|
|
572
|
-
break;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
libExpect(tmpDifferent).to.equal(true);
|
|
576
|
-
fDone();
|
|
577
|
-
}
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
test
|
|
581
|
-
(
|
|
582
|
-
'PreservePositions=true keeps placed nodes',
|
|
583
|
-
function (fDone)
|
|
584
|
-
{
|
|
585
|
-
let tmpNodes = makeNodes(2);
|
|
586
|
-
tmpNodes[0].X = 12345;
|
|
587
|
-
tmpNodes[0].Y = 67890;
|
|
588
|
-
// One iteration, no forces should move much
|
|
589
|
-
libLayoutForcedFromCenter.Apply(tmpNodes, [], Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, {
|
|
590
|
-
Iterations: 0, PreservePositions: true
|
|
591
|
-
}));
|
|
592
|
-
libExpect(tmpNodes[0].X).to.equal(12345);
|
|
593
|
-
libExpect(tmpNodes[0].Y).to.equal(67890);
|
|
594
|
-
fDone();
|
|
595
|
-
}
|
|
596
|
-
);
|
|
597
|
-
|
|
598
|
-
test
|
|
599
|
-
(
|
|
600
|
-
'rounds final positions to integers',
|
|
601
|
-
function (fDone)
|
|
602
|
-
{
|
|
603
|
-
let tmpNodes = makeNodes(3);
|
|
604
|
-
libLayoutForcedFromCenter.Apply(tmpNodes, makeChain(3), Object.assign({}, libLayoutForcedFromCenter.DefaultParameters, { Iterations: 20 }));
|
|
605
|
-
for (let i = 0; i < 3; i++)
|
|
606
|
-
{
|
|
607
|
-
libExpect(tmpNodes[i].X).to.equal(Math.round(tmpNodes[i].X));
|
|
608
|
-
libExpect(tmpNodes[i].Y).to.equal(Math.round(tmpNodes[i].Y));
|
|
609
|
-
}
|
|
610
|
-
fDone();
|
|
611
|
-
}
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
// ── Grid ──────────────────────────────────────────────────────
|
|
617
|
-
|
|
618
|
-
suite
|
|
619
|
-
(
|
|
620
|
-
'Grid algorithm',
|
|
621
|
-
function ()
|
|
622
|
-
{
|
|
623
|
-
test
|
|
624
|
-
(
|
|
625
|
-
'auto columns = ceil(sqrt(n))',
|
|
626
|
-
function (fDone)
|
|
627
|
-
{
|
|
628
|
-
let tmpNodes = makeNodes(9);
|
|
629
|
-
libLayoutGrid.Apply(tmpNodes, [], libLayoutGrid.DefaultParameters);
|
|
630
|
-
// 9 nodes -> 3 cols, 3 rows
|
|
631
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
632
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
633
|
-
libExpect(tmpNodes[2].X).to.equal(100 + 2 * (180 + 40));
|
|
634
|
-
libExpect(tmpNodes[3].X).to.equal(100); // wraps
|
|
635
|
-
libExpect(tmpNodes[3].Y).to.equal(100 + (80 + 40));
|
|
636
|
-
fDone();
|
|
637
|
-
}
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
test
|
|
641
|
-
(
|
|
642
|
-
'explicit Columns: 4',
|
|
643
|
-
function (fDone)
|
|
644
|
-
{
|
|
645
|
-
let tmpNodes = makeNodes(8);
|
|
646
|
-
libLayoutGrid.Apply(tmpNodes, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 4 }));
|
|
647
|
-
libExpect(tmpNodes[3].X).to.equal(100 + 3 * (180 + 40));
|
|
648
|
-
libExpect(tmpNodes[3].Y).to.equal(100);
|
|
649
|
-
libExpect(tmpNodes[4].X).to.equal(100); // wraps after col 4
|
|
650
|
-
libExpect(tmpNodes[4].Y).to.equal(100 + (80 + 40));
|
|
651
|
-
fDone();
|
|
652
|
-
}
|
|
653
|
-
);
|
|
654
|
-
|
|
655
|
-
test
|
|
656
|
-
(
|
|
657
|
-
'OrderBy: hash sorts before placing',
|
|
658
|
-
function (fDone)
|
|
659
|
-
{
|
|
660
|
-
let tmpNodes = [
|
|
661
|
-
{ Hash: 'b', X: 0, Y: 0, Width: 180, Height: 80 },
|
|
662
|
-
{ Hash: 'a', X: 0, Y: 0, Width: 180, Height: 80 }
|
|
663
|
-
];
|
|
664
|
-
libLayoutGrid.Apply(tmpNodes, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 2, OrderBy: 'hash' }));
|
|
665
|
-
// 'a' should land at column 0, 'b' at column 1
|
|
666
|
-
let tmpA = tmpNodes.find((pN) => pN.Hash === 'a');
|
|
667
|
-
let tmpB = tmpNodes.find((pN) => pN.Hash === 'b');
|
|
668
|
-
libExpect(tmpA.X).to.equal(100);
|
|
669
|
-
libExpect(tmpB.X).to.equal(100 + (180 + 40));
|
|
670
|
-
fDone();
|
|
671
|
-
}
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
// ── Circular ─────────────────────────────────────────────────
|
|
677
|
-
|
|
678
|
-
suite
|
|
679
|
-
(
|
|
680
|
-
'Circular algorithm',
|
|
681
|
-
function ()
|
|
682
|
-
{
|
|
683
|
-
test
|
|
684
|
-
(
|
|
685
|
-
'no connections — single ring with everyone',
|
|
686
|
-
function (fDone)
|
|
687
|
-
{
|
|
688
|
-
let tmpNodes = makeNodes(4);
|
|
689
|
-
libLayoutCircular.Apply(tmpNodes, [], libLayoutCircular.DefaultParameters);
|
|
690
|
-
// All on ring 0 (radius 0 + 0*220 = 0) — but ring index 0 has radius 0,
|
|
691
|
-
// so the single-root center fast-path doesn't apply when ring has more than 1.
|
|
692
|
-
// They all end up centered around (CenterX, CenterY).
|
|
693
|
-
let tmpCx = 1000, tmpCy = 750;
|
|
694
|
-
for (let i = 0; i < 4; i++)
|
|
695
|
-
{
|
|
696
|
-
let tmpDist = Math.hypot(
|
|
697
|
-
tmpNodes[i].X + 90 - tmpCx,
|
|
698
|
-
tmpNodes[i].Y + 40 - tmpCy
|
|
699
|
-
);
|
|
700
|
-
libExpect(tmpDist).to.be.lessThan(1); // all at radius 0
|
|
701
|
-
}
|
|
702
|
-
fDone();
|
|
703
|
-
}
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
test
|
|
707
|
-
(
|
|
708
|
-
'with connections — root node at center',
|
|
709
|
-
function (fDone)
|
|
710
|
-
{
|
|
711
|
-
let tmpNodes = makeNodes(3);
|
|
712
|
-
let tmpConns = makeChain(3);
|
|
713
|
-
libLayoutCircular.Apply(tmpNodes, tmpConns, libLayoutCircular.DefaultParameters);
|
|
714
|
-
let tmpRoot = tmpNodes[0]; // n-0 has in-degree 0
|
|
715
|
-
// Root at center: X = CenterX - W/2, Y = CenterY - H/2
|
|
716
|
-
libExpect(tmpRoot.X).to.equal(1000 - 90);
|
|
717
|
-
libExpect(tmpRoot.Y).to.equal(750 - 40);
|
|
718
|
-
fDone();
|
|
719
|
-
}
|
|
720
|
-
);
|
|
721
|
-
}
|
|
722
|
-
);
|
|
723
|
-
|
|
724
|
-
// ── Tabular ──────────────────────────────────────────────────
|
|
725
|
-
|
|
726
|
-
suite
|
|
727
|
-
(
|
|
728
|
-
'Tabular algorithm',
|
|
729
|
-
function ()
|
|
730
|
-
{
|
|
731
|
-
test
|
|
732
|
-
(
|
|
733
|
-
'stacks nodes vertically',
|
|
734
|
-
function (fDone)
|
|
735
|
-
{
|
|
736
|
-
let tmpNodes = makeNodes(3);
|
|
737
|
-
libLayoutTabular.Apply(tmpNodes, [], libLayoutTabular.DefaultParameters);
|
|
738
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
739
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
740
|
-
libExpect(tmpNodes[1].Y).to.equal(100 + 80 + 40);
|
|
741
|
-
libExpect(tmpNodes[2].Y).to.equal(100 + (80 + 40) * 2);
|
|
742
|
-
// All same X
|
|
743
|
-
libExpect(tmpNodes[1].X).to.equal(100);
|
|
744
|
-
libExpect(tmpNodes[2].X).to.equal(100);
|
|
745
|
-
fDone();
|
|
746
|
-
}
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
);
|
|
750
|
-
|
|
751
|
-
// ── Columnar ─────────────────────────────────────────────────
|
|
752
|
-
|
|
753
|
-
suite
|
|
754
|
-
(
|
|
755
|
-
'Columnar algorithm',
|
|
756
|
-
function ()
|
|
757
|
-
{
|
|
758
|
-
test
|
|
759
|
-
(
|
|
760
|
-
'fills row-first across N columns',
|
|
761
|
-
function (fDone)
|
|
762
|
-
{
|
|
763
|
-
let tmpNodes = makeNodes(7);
|
|
764
|
-
libLayoutColumnar.Apply(tmpNodes, [], Object.assign({}, libLayoutColumnar.DefaultParameters, { Columns: 3 }));
|
|
765
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
766
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
767
|
-
libExpect(tmpNodes[1].X).to.equal(100 + (180 + 40));
|
|
768
|
-
libExpect(tmpNodes[2].X).to.equal(100 + (180 + 40) * 2);
|
|
769
|
-
libExpect(tmpNodes[3].X).to.equal(100); // wraps to row 1
|
|
770
|
-
libExpect(tmpNodes[3].Y).to.equal(100 + (80 + 40));
|
|
771
|
-
fDone();
|
|
772
|
-
}
|
|
773
|
-
);
|
|
774
|
-
|
|
775
|
-
test
|
|
776
|
-
(
|
|
777
|
-
'FillOrder: column flows column-first',
|
|
778
|
-
function (fDone)
|
|
779
|
-
{
|
|
780
|
-
let tmpNodes = makeNodes(6);
|
|
781
|
-
libLayoutColumnar.Apply(tmpNodes, [], Object.assign({}, libLayoutColumnar.DefaultParameters, { Columns: 3, FillOrder: 'column' }));
|
|
782
|
-
// 6 nodes / 3 cols = 2 rows. Column-first: n-0 (0,0), n-1 (0,1), n-2 (1,0), ...
|
|
783
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
784
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
785
|
-
libExpect(tmpNodes[1].X).to.equal(100);
|
|
786
|
-
libExpect(tmpNodes[1].Y).to.equal(100 + (80 + 40));
|
|
787
|
-
libExpect(tmpNodes[2].X).to.equal(100 + (180 + 40));
|
|
788
|
-
libExpect(tmpNodes[2].Y).to.equal(100);
|
|
789
|
-
fDone();
|
|
790
|
-
}
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
// ── PreciseNumber string parameters ──────────────────────────
|
|
796
|
-
|
|
797
|
-
suite
|
|
798
|
-
(
|
|
799
|
-
'PreciseNumber string parameters (big.js compatibility)',
|
|
800
|
-
function ()
|
|
801
|
-
{
|
|
802
|
-
test
|
|
803
|
-
(
|
|
804
|
-
'Layered: string parameters produce identical positions to numbers',
|
|
805
|
-
function (fDone)
|
|
806
|
-
{
|
|
807
|
-
let tmpA = makeNodes(3);
|
|
808
|
-
let tmpB = makeNodes(3);
|
|
809
|
-
let tmpConns = makeChain(3);
|
|
810
|
-
libLayoutLayered.Apply(tmpA, tmpConns, { HorizontalSpacing: 250, VerticalSpacing: 120, StartX: 100, StartY: 100 });
|
|
811
|
-
libLayoutLayered.Apply(tmpB, tmpConns, { HorizontalSpacing: '250', VerticalSpacing: '120', StartX: '100', StartY: '100' });
|
|
812
|
-
for (let i = 0; i < 3; i++)
|
|
813
|
-
{
|
|
814
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
815
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
816
|
-
}
|
|
817
|
-
fDone();
|
|
818
|
-
}
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
test
|
|
822
|
-
(
|
|
823
|
-
'ForcedFromCenter: string PreciseNumber params match numeric params',
|
|
824
|
-
function (fDone)
|
|
825
|
-
{
|
|
826
|
-
let tmpA = makeNodes(4);
|
|
827
|
-
let tmpB = makeNodes(4);
|
|
828
|
-
let tmpConns = makeChain(4);
|
|
829
|
-
let tmpNumeric = { Iterations: 30, Seed: 7, CenterX: 500, CenterY: 500, SpringLength: 150, SpringStiffness: 0.05, Repulsion: 8000, CenterAttraction: 0.01, CoolingFactor: 0.95, InitialTemperature: 100, InitialSpread: 400 };
|
|
830
|
-
let tmpStringy = { Iterations: 30, Seed: 7, CenterX: '500', CenterY: '500', SpringLength: '150', SpringStiffness: '0.05', Repulsion: '8000', CenterAttraction: '0.01', CoolingFactor: '0.95', InitialTemperature: '100', InitialSpread: '400' };
|
|
831
|
-
libLayoutForcedFromCenter.Apply(tmpA, tmpConns, tmpNumeric);
|
|
832
|
-
libLayoutForcedFromCenter.Apply(tmpB, tmpConns, tmpStringy);
|
|
833
|
-
for (let i = 0; i < 4; i++)
|
|
834
|
-
{
|
|
835
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
836
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
837
|
-
}
|
|
838
|
-
fDone();
|
|
839
|
-
}
|
|
840
|
-
);
|
|
841
|
-
|
|
842
|
-
test
|
|
843
|
-
(
|
|
844
|
-
'Grid: Columns as string-int matches Columns as int',
|
|
845
|
-
function (fDone)
|
|
846
|
-
{
|
|
847
|
-
let tmpA = makeNodes(8);
|
|
848
|
-
let tmpB = makeNodes(8);
|
|
849
|
-
libLayoutGrid.Apply(tmpA, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: 4 }));
|
|
850
|
-
libLayoutGrid.Apply(tmpB, [], Object.assign({}, libLayoutGrid.DefaultParameters, { Columns: '4' }));
|
|
851
|
-
for (let i = 0; i < 8; i++)
|
|
852
|
-
{
|
|
853
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
854
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
855
|
-
}
|
|
856
|
-
fDone();
|
|
857
|
-
}
|
|
858
|
-
);
|
|
859
|
-
|
|
860
|
-
test
|
|
861
|
-
(
|
|
862
|
-
'Circular: string CenterX/Y/RingSpacing match numeric',
|
|
863
|
-
function (fDone)
|
|
864
|
-
{
|
|
865
|
-
let tmpA = makeNodes(5);
|
|
866
|
-
let tmpB = makeNodes(5);
|
|
867
|
-
libLayoutCircular.Apply(tmpA, [], { CenterX: 1000, CenterY: 750, RingSpacing: 220, InnerRadius: 100, StartAngle: -90 });
|
|
868
|
-
libLayoutCircular.Apply(tmpB, [], { CenterX: '1000', CenterY: '750', RingSpacing: '220', InnerRadius: '100', StartAngle: '-90' });
|
|
869
|
-
for (let i = 0; i < 5; i++)
|
|
870
|
-
{
|
|
871
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
872
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
873
|
-
}
|
|
874
|
-
fDone();
|
|
875
|
-
}
|
|
876
|
-
);
|
|
877
|
-
|
|
878
|
-
test
|
|
879
|
-
(
|
|
880
|
-
'Tabular: string spacing matches numeric',
|
|
881
|
-
function (fDone)
|
|
882
|
-
{
|
|
883
|
-
let tmpA = makeNodes(4);
|
|
884
|
-
let tmpB = makeNodes(4);
|
|
885
|
-
libLayoutTabular.Apply(tmpA, [], { StartX: 100, StartY: 100, VerticalSpacing: 40 });
|
|
886
|
-
libLayoutTabular.Apply(tmpB, [], { StartX: '100', StartY: '100', VerticalSpacing: '40' });
|
|
887
|
-
for (let i = 0; i < 4; i++)
|
|
888
|
-
{
|
|
889
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
890
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
891
|
-
}
|
|
892
|
-
fDone();
|
|
893
|
-
}
|
|
894
|
-
);
|
|
895
|
-
|
|
896
|
-
test
|
|
897
|
-
(
|
|
898
|
-
'Columnar: mixed string and number params match all-number params',
|
|
899
|
-
function (fDone)
|
|
900
|
-
{
|
|
901
|
-
let tmpA = makeNodes(7);
|
|
902
|
-
let tmpB = makeNodes(7);
|
|
903
|
-
libLayoutColumnar.Apply(tmpA, [], { Columns: 3, ColumnSpacing: 40, RowSpacing: 40, StartX: 100, StartY: 100 });
|
|
904
|
-
libLayoutColumnar.Apply(tmpB, [], { Columns: '3', ColumnSpacing: '40', RowSpacing: '40', StartX: '100', StartY: '100' });
|
|
905
|
-
for (let i = 0; i < 7; i++)
|
|
906
|
-
{
|
|
907
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
908
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
909
|
-
}
|
|
910
|
-
fDone();
|
|
911
|
-
}
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
test
|
|
915
|
-
(
|
|
916
|
-
'Empty string parameters fall back to defaults gracefully',
|
|
917
|
-
function (fDone)
|
|
918
|
-
{
|
|
919
|
-
let tmpA = makeNodes(3);
|
|
920
|
-
let tmpB = makeNodes(3);
|
|
921
|
-
let tmpConns = makeChain(3);
|
|
922
|
-
libLayoutLayered.Apply(tmpA, tmpConns, {});
|
|
923
|
-
libLayoutLayered.Apply(tmpB, tmpConns, { HorizontalSpacing: '', VerticalSpacing: null, StartX: undefined });
|
|
924
|
-
for (let i = 0; i < 3; i++)
|
|
925
|
-
{
|
|
926
|
-
libExpect(tmpA[i].X).to.equal(tmpB[i].X);
|
|
927
|
-
libExpect(tmpA[i].Y).to.equal(tmpB[i].Y);
|
|
928
|
-
}
|
|
929
|
-
fDone();
|
|
930
|
-
}
|
|
931
|
-
);
|
|
932
|
-
}
|
|
933
|
-
);
|
|
934
|
-
|
|
935
|
-
// ── Custom (no-op) ───────────────────────────────────────────
|
|
936
|
-
|
|
937
|
-
suite
|
|
938
|
-
(
|
|
939
|
-
'Custom algorithm',
|
|
940
|
-
function ()
|
|
941
|
-
{
|
|
942
|
-
test
|
|
943
|
-
(
|
|
944
|
-
'preserves existing X/Y',
|
|
945
|
-
function (fDone)
|
|
946
|
-
{
|
|
947
|
-
let tmpNodes = [
|
|
948
|
-
{ Hash: 'a', X: 42, Y: 99, Width: 180, Height: 80 },
|
|
949
|
-
{ Hash: 'b', X: 314, Y: 271, Width: 180, Height: 80 }
|
|
950
|
-
];
|
|
951
|
-
libLayoutCustom.Apply(tmpNodes, [], {});
|
|
952
|
-
libExpect(tmpNodes[0].X).to.equal(42);
|
|
953
|
-
libExpect(tmpNodes[0].Y).to.equal(99);
|
|
954
|
-
libExpect(tmpNodes[1].X).to.equal(314);
|
|
955
|
-
libExpect(tmpNodes[1].Y).to.equal(271);
|
|
956
|
-
fDone();
|
|
957
|
-
}
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
// ── applyLayout dispatch ─────────────────────────────────────
|
|
963
|
-
|
|
964
|
-
suite
|
|
965
|
-
(
|
|
966
|
-
'applyLayout dispatch',
|
|
967
|
-
function ()
|
|
968
|
-
{
|
|
969
|
-
test
|
|
970
|
-
(
|
|
971
|
-
'falls back to Layered for unknown algorithm name',
|
|
972
|
-
function (fDone)
|
|
973
|
-
{
|
|
974
|
-
let tmpNodes = makeNodes(2);
|
|
975
|
-
let tmpConns = makeChain(2);
|
|
976
|
-
_LayoutService.applyLayout(tmpNodes, tmpConns, 'TotallyMadeUp', {});
|
|
977
|
-
// Should match Layered output (chain → 2 layers)
|
|
978
|
-
libExpect(tmpNodes[0].X).to.equal(100);
|
|
979
|
-
libExpect(tmpNodes[1].X).to.equal(100 + 180 + 250);
|
|
980
|
-
fDone();
|
|
981
|
-
}
|
|
982
|
-
);
|
|
983
|
-
|
|
984
|
-
test
|
|
985
|
-
(
|
|
986
|
-
'merges DefaultParameters with caller overrides',
|
|
987
|
-
function (fDone)
|
|
988
|
-
{
|
|
989
|
-
let tmpNodes = makeNodes(1);
|
|
990
|
-
_LayoutService.applyLayout(tmpNodes, [], 'Layered', { StartX: 500 });
|
|
991
|
-
// StartY left as default (100), StartX overridden
|
|
992
|
-
libExpect(tmpNodes[0].X).to.equal(500);
|
|
993
|
-
libExpect(tmpNodes[0].Y).to.equal(100);
|
|
994
|
-
fDone();
|
|
995
|
-
}
|
|
996
|
-
);
|
|
997
|
-
|
|
998
|
-
test
|
|
999
|
-
(
|
|
1000
|
-
'getMergedParameters returns defaults merged with overrides',
|
|
1001
|
-
function (fDone)
|
|
1002
|
-
{
|
|
1003
|
-
let tmpMerged = _LayoutService.getMergedParameters('Layered', { StartX: 999 });
|
|
1004
|
-
libExpect(tmpMerged.StartX).to.equal(999);
|
|
1005
|
-
libExpect(tmpMerged.StartY).to.equal(100); // default preserved
|
|
1006
|
-
libExpect(tmpMerged.HorizontalSpacing).to.equal(250);
|
|
1007
|
-
fDone();
|
|
1008
|
-
}
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
);
|
|
1012
|
-
|
|
1013
|
-
// ── Spacing multiplier ───────────────────────────────────────
|
|
1014
|
-
|
|
1015
|
-
suite
|
|
1016
|
-
(
|
|
1017
|
-
'Spacing multiplier',
|
|
1018
|
-
function ()
|
|
1019
|
-
{
|
|
1020
|
-
test
|
|
1021
|
-
(
|
|
1022
|
-
'Layered: Spacing=2 doubles the gap between layers',
|
|
1023
|
-
function (fDone)
|
|
1024
|
-
{
|
|
1025
|
-
let tmpA = makeNodes(2);
|
|
1026
|
-
let tmpB = makeNodes(2);
|
|
1027
|
-
let tmpConns = makeChain(2);
|
|
1028
|
-
libLayoutLayered.Apply(tmpA, tmpConns, { Spacing: 1.0 });
|
|
1029
|
-
libLayoutLayered.Apply(tmpB, tmpConns, { Spacing: 2.0 });
|
|
1030
|
-
// First node lands at StartX both times.
|
|
1031
|
-
libExpect(tmpA[0].X).to.equal(tmpB[0].X);
|
|
1032
|
-
// Second node Δx with Spacing=2 should be twice the Δx with Spacing=1.
|
|
1033
|
-
let tmpDxA = tmpA[1].X - tmpA[0].X;
|
|
1034
|
-
let tmpDxB = tmpB[1].X - tmpB[0].X;
|
|
1035
|
-
// Δx = nodeWidth + (HorizontalSpacing * Spacing). With nodeWidth=180, HSpace=250:
|
|
1036
|
-
// Spacing=1 → 430; Spacing=2 → 680. Difference is HorizontalSpacing (250).
|
|
1037
|
-
libExpect(tmpDxB - tmpDxA).to.equal(250);
|
|
1038
|
-
fDone();
|
|
1039
|
-
}
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
test
|
|
1043
|
-
(
|
|
1044
|
-
'Tabular: Spacing scales VerticalSpacing',
|
|
1045
|
-
function (fDone)
|
|
1046
|
-
{
|
|
1047
|
-
let tmpA = makeNodes(3);
|
|
1048
|
-
let tmpB = makeNodes(3);
|
|
1049
|
-
libLayoutTabular.Apply(tmpA, [], { Spacing: 1.0 });
|
|
1050
|
-
libLayoutTabular.Apply(tmpB, [], { Spacing: 0.5 });
|
|
1051
|
-
let tmpDyA = tmpA[1].Y - tmpA[0].Y; // height(80) + 40*1 = 120
|
|
1052
|
-
let tmpDyB = tmpB[1].Y - tmpB[0].Y; // height(80) + 40*0.5 = 100
|
|
1053
|
-
libExpect(tmpDyA).to.equal(120);
|
|
1054
|
-
libExpect(tmpDyB).to.equal(100);
|
|
1055
|
-
fDone();
|
|
1056
|
-
}
|
|
1057
|
-
);
|
|
1058
|
-
|
|
1059
|
-
test
|
|
1060
|
-
(
|
|
1061
|
-
'Circular: Spacing scales RingSpacing (and InnerRadius)',
|
|
1062
|
-
function (fDone)
|
|
1063
|
-
{
|
|
1064
|
-
let tmpA = makeNodes(3);
|
|
1065
|
-
let tmpB = makeNodes(3);
|
|
1066
|
-
let tmpConns = makeChain(3);
|
|
1067
|
-
libLayoutCircular.Apply(tmpA, tmpConns, { Spacing: 1.0 });
|
|
1068
|
-
libLayoutCircular.Apply(tmpB, tmpConns, { Spacing: 2.0 });
|
|
1069
|
-
// Root sits at center for both. Second node is on ring 1 (radius = RingSpacing*Spacing).
|
|
1070
|
-
let tmpRadiusA = Math.hypot((tmpA[1].X + 90) - 1000, (tmpA[1].Y + 40) - 750);
|
|
1071
|
-
let tmpRadiusB = Math.hypot((tmpB[1].X + 90) - 1000, (tmpB[1].Y + 40) - 750);
|
|
1072
|
-
libExpect(Math.round(tmpRadiusB / tmpRadiusA)).to.equal(2);
|
|
1073
|
-
fDone();
|
|
1074
|
-
}
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
);
|
|
1078
|
-
|
|
1079
|
-
// ── Edge-theme registry ──────────────────────────────────────
|
|
1080
|
-
|
|
1081
|
-
suite
|
|
1082
|
-
(
|
|
1083
|
-
'Edge-theme registry',
|
|
1084
|
-
function ()
|
|
1085
|
-
{
|
|
1086
|
-
test
|
|
1087
|
-
(
|
|
1088
|
-
'all four built-in themes are registered',
|
|
1089
|
-
function (fDone)
|
|
1090
|
-
{
|
|
1091
|
-
let tmpNames = _LayoutService.getEdgeThemeNames();
|
|
1092
|
-
libExpect(tmpNames).to.include.members(['Bezier', 'Orthogonal', 'Straight', 'OrthogonalSnap']);
|
|
1093
|
-
fDone();
|
|
1094
|
-
}
|
|
1095
|
-
);
|
|
1096
|
-
|
|
1097
|
-
test
|
|
1098
|
-
(
|
|
1099
|
-
'getEdgeTheme returns null for unknown name',
|
|
1100
|
-
function (fDone)
|
|
1101
|
-
{
|
|
1102
|
-
libExpect(_LayoutService.getEdgeTheme('NotARealTheme')).to.equal(null);
|
|
1103
|
-
fDone();
|
|
1104
|
-
}
|
|
1105
|
-
);
|
|
1106
|
-
|
|
1107
|
-
test
|
|
1108
|
-
(
|
|
1109
|
-
'registerEdgeTheme rejects descriptors missing GeneratePath',
|
|
1110
|
-
function (fDone)
|
|
1111
|
-
{
|
|
1112
|
-
libExpect(_LayoutService.registerEdgeTheme({ Name: 'X' })).to.equal(false);
|
|
1113
|
-
libExpect(_LayoutService.registerEdgeTheme({ GeneratePath: function () {} })).to.equal(false);
|
|
1114
|
-
fDone();
|
|
1115
|
-
}
|
|
1116
|
-
);
|
|
1117
|
-
|
|
1118
|
-
test
|
|
1119
|
-
(
|
|
1120
|
-
'registerEdgeTheme + dispatch via resolveActiveEdgeTheme',
|
|
1121
|
-
function (fDone)
|
|
1122
|
-
{
|
|
1123
|
-
let tmpStub =
|
|
1124
|
-
{
|
|
1125
|
-
Name: 'StubEdge',
|
|
1126
|
-
GeneratePath: function () { return 'M 0 0 L 10 10'; }
|
|
1127
|
-
};
|
|
1128
|
-
libExpect(_LayoutService.registerEdgeTheme(tmpStub)).to.equal(true);
|
|
1129
|
-
let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { EdgeTheme: 'StubEdge' } });
|
|
1130
|
-
libExpect(tmpResolved).to.equal(tmpStub);
|
|
1131
|
-
fDone();
|
|
1132
|
-
}
|
|
1133
|
-
);
|
|
1134
|
-
|
|
1135
|
-
test
|
|
1136
|
-
(
|
|
1137
|
-
'resolveActiveEdgeTheme — flow-level EdgeTheme overrides layout default',
|
|
1138
|
-
function (fDone)
|
|
1139
|
-
{
|
|
1140
|
-
_LayoutService._FlowView = { _FlowData: { EdgeTheme: 'Straight', LayoutAlgorithm: 'Layered' } };
|
|
1141
|
-
let tmpResolved = _LayoutService.resolveActiveEdgeTheme({});
|
|
1142
|
-
libExpect(tmpResolved.Name).to.equal('Straight');
|
|
1143
|
-
_LayoutService._FlowView = null;
|
|
1144
|
-
fDone();
|
|
1145
|
-
}
|
|
1146
|
-
);
|
|
1147
|
-
|
|
1148
|
-
test
|
|
1149
|
-
(
|
|
1150
|
-
'resolveActiveEdgeTheme — falls back to layout DefaultEdgeTheme when no flow override',
|
|
1151
|
-
function (fDone)
|
|
1152
|
-
{
|
|
1153
|
-
_LayoutService._FlowView = { _FlowData: { EdgeTheme: null, LayoutAlgorithm: 'Layered' } };
|
|
1154
|
-
let tmpResolved = _LayoutService.resolveActiveEdgeTheme({});
|
|
1155
|
-
// Layered's DefaultEdgeTheme is 'Orthogonal'
|
|
1156
|
-
libExpect(tmpResolved.Name).to.equal('Orthogonal');
|
|
1157
|
-
_LayoutService._FlowView = null;
|
|
1158
|
-
fDone();
|
|
1159
|
-
}
|
|
1160
|
-
);
|
|
1161
|
-
|
|
1162
|
-
test
|
|
1163
|
-
(
|
|
1164
|
-
'resolveActiveEdgeTheme — connection-level EdgeTheme beats flow-level',
|
|
1165
|
-
function (fDone)
|
|
1166
|
-
{
|
|
1167
|
-
_LayoutService._FlowView = { _FlowData: { EdgeTheme: 'Straight', LayoutAlgorithm: 'Layered' } };
|
|
1168
|
-
let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { EdgeTheme: 'Bezier' } });
|
|
1169
|
-
libExpect(tmpResolved.Name).to.equal('Bezier');
|
|
1170
|
-
_LayoutService._FlowView = null;
|
|
1171
|
-
fDone();
|
|
1172
|
-
}
|
|
1173
|
-
);
|
|
1174
|
-
|
|
1175
|
-
test
|
|
1176
|
-
(
|
|
1177
|
-
'resolveActiveEdgeTheme — legacy LineMode=orthogonal maps to Orthogonal theme',
|
|
1178
|
-
function (fDone)
|
|
1179
|
-
{
|
|
1180
|
-
let tmpResolved = _LayoutService.resolveActiveEdgeTheme({ Data: { LineMode: 'orthogonal' } });
|
|
1181
|
-
libExpect(tmpResolved.Name).to.equal('Orthogonal');
|
|
1182
|
-
fDone();
|
|
1183
|
-
}
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
);
|
|
1187
|
-
|
|
1188
|
-
// ── Edge themes — path generation ────────────────────────────
|
|
1189
|
-
|
|
1190
|
-
suite
|
|
1191
|
-
(
|
|
1192
|
-
'Edge themes — GeneratePath',
|
|
1193
|
-
function ()
|
|
1194
|
-
{
|
|
1195
|
-
let _Helpers;
|
|
1196
|
-
|
|
1197
|
-
setup
|
|
1198
|
-
(
|
|
1199
|
-
function ()
|
|
1200
|
-
{
|
|
1201
|
-
_Helpers =
|
|
1202
|
-
{
|
|
1203
|
-
generateBezier: function (s, e) { return `BEZ ${s.x},${s.y}→${e.x},${e.y}`; },
|
|
1204
|
-
generateMultiBezier: function (s, e, h) { return `MBEZ ${s.x},${s.y} hs=${h.length} →${e.x},${e.y}`; },
|
|
1205
|
-
generateOrthogonal: function (s, e, c, m) { return `ORT ${s.x},${s.y}→${e.x},${e.y} m=${m}`; },
|
|
1206
|
-
getBezierHandles: function (d) { return (d && d.HandleCustomized && d.BezierHandles) || []; }
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
);
|
|
1210
|
-
|
|
1211
|
-
test
|
|
1212
|
-
(
|
|
1213
|
-
'Bezier: emits straight-bezier when no custom handles',
|
|
1214
|
-
function (fDone)
|
|
1215
|
-
{
|
|
1216
|
-
let tmpPath = libEdgeBezier.GeneratePath({
|
|
1217
|
-
Source: { x: 0, y: 0, side: 'right' },
|
|
1218
|
-
Target: { x: 100, y: 50, side: 'left' },
|
|
1219
|
-
Connection: { Data: {} },
|
|
1220
|
-
Helpers: _Helpers, Parameters: {}
|
|
1221
|
-
});
|
|
1222
|
-
libExpect(tmpPath).to.equal('BEZ 0,0→100,50');
|
|
1223
|
-
fDone();
|
|
1224
|
-
}
|
|
1225
|
-
);
|
|
1226
|
-
|
|
1227
|
-
test
|
|
1228
|
-
(
|
|
1229
|
-
'Bezier: honors per-connection custom handles',
|
|
1230
|
-
function (fDone)
|
|
1231
|
-
{
|
|
1232
|
-
let tmpPath = libEdgeBezier.GeneratePath({
|
|
1233
|
-
Source: { x: 0, y: 0 }, Target: { x: 100, y: 50 },
|
|
1234
|
-
Connection: { Data: { HandleCustomized: true, BezierHandles: [{ x: 50, y: 25 }, { x: 70, y: 30 }] } },
|
|
1235
|
-
Helpers: _Helpers, Parameters: {}
|
|
1236
|
-
});
|
|
1237
|
-
libExpect(tmpPath.indexOf('MBEZ')).to.equal(0);
|
|
1238
|
-
libExpect(tmpPath.indexOf('hs=2')).to.be.above(0);
|
|
1239
|
-
fDone();
|
|
1240
|
-
}
|
|
1241
|
-
);
|
|
1242
|
-
|
|
1243
|
-
test
|
|
1244
|
-
(
|
|
1245
|
-
'Orthogonal: emits orthogonal path; respects custom corners',
|
|
1246
|
-
function (fDone)
|
|
1247
|
-
{
|
|
1248
|
-
let tmpPath = libEdgeOrthogonal.GeneratePath({
|
|
1249
|
-
Source: { x: 0, y: 0 }, Target: { x: 100, y: 50 },
|
|
1250
|
-
Connection: { Data: { OrthoMidOffset: 7 } },
|
|
1251
|
-
Helpers: _Helpers, Parameters: {}
|
|
1252
|
-
});
|
|
1253
|
-
libExpect(tmpPath).to.equal('ORT 0,0→100,50 m=7');
|
|
1254
|
-
fDone();
|
|
1255
|
-
}
|
|
1256
|
-
);
|
|
1257
|
-
|
|
1258
|
-
test
|
|
1259
|
-
(
|
|
1260
|
-
'Straight: literal M..L path',
|
|
1261
|
-
function (fDone)
|
|
1262
|
-
{
|
|
1263
|
-
let tmpPath = libEdgeStraight.GeneratePath({
|
|
1264
|
-
Source: { x: 12, y: 34 }, Target: { x: 56, y: 78 },
|
|
1265
|
-
Connection: { Data: {} }, Helpers: _Helpers, Parameters: {}
|
|
1266
|
-
});
|
|
1267
|
-
libExpect(tmpPath).to.equal('M 12 34 L 56 78');
|
|
1268
|
-
fDone();
|
|
1269
|
-
}
|
|
1270
|
-
);
|
|
1271
|
-
|
|
1272
|
-
test
|
|
1273
|
-
(
|
|
1274
|
-
'OrthogonalSnap: AdjustLayout snaps node positions to grid',
|
|
1275
|
-
function (fDone)
|
|
1276
|
-
{
|
|
1277
|
-
let tmpNodes =
|
|
1278
|
-
[
|
|
1279
|
-
{ X: 13, Y: 27, Width: 100, Height: 50 },
|
|
1280
|
-
{ X: 86, Y: 199, Width: 100, Height: 50 }
|
|
1281
|
-
];
|
|
1282
|
-
libEdgeOrthogonalSnap.AdjustLayout(tmpNodes, [], { GridSize: 20 });
|
|
1283
|
-
libExpect(tmpNodes[0].X).to.equal(20);
|
|
1284
|
-
libExpect(tmpNodes[0].Y).to.equal(20);
|
|
1285
|
-
libExpect(tmpNodes[1].X).to.equal(80);
|
|
1286
|
-
libExpect(tmpNodes[1].Y).to.equal(200);
|
|
1287
|
-
fDone();
|
|
1288
|
-
}
|
|
1289
|
-
);
|
|
1290
|
-
|
|
1291
|
-
test
|
|
1292
|
-
(
|
|
1293
|
-
'OrthogonalSnap: AdjustLayout no-op when GridSize<=0',
|
|
1294
|
-
function (fDone)
|
|
1295
|
-
{
|
|
1296
|
-
let tmpNodes = [{ X: 13, Y: 27, Width: 100, Height: 50 }];
|
|
1297
|
-
libEdgeOrthogonalSnap.AdjustLayout(tmpNodes, [], { GridSize: 0 });
|
|
1298
|
-
libExpect(tmpNodes[0].X).to.equal(13);
|
|
1299
|
-
libExpect(tmpNodes[0].Y).to.equal(27);
|
|
1300
|
-
fDone();
|
|
1301
|
-
}
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
);
|
|
1305
|
-
|
|
1306
|
-
// ── Edge-Perimeter (ResolveAttachment) ───────────────────────
|
|
1307
|
-
|
|
1308
|
-
suite
|
|
1309
|
-
(
|
|
1310
|
-
'Edge-Perimeter — perimeter-routing attachment',
|
|
1311
|
-
function ()
|
|
1312
|
-
{
|
|
1313
|
-
let _Hub;
|
|
1314
|
-
|
|
1315
|
-
setup
|
|
1316
|
-
(
|
|
1317
|
-
function ()
|
|
1318
|
-
{
|
|
1319
|
-
// 200×100 node at (400, 300) → center (500, 350)
|
|
1320
|
-
_Hub = { Hash: 'hub', X: 400, Y: 300, Width: 200, Height: 100 };
|
|
1321
|
-
}
|
|
1322
|
-
);
|
|
1323
|
-
|
|
1324
|
-
test
|
|
1325
|
-
(
|
|
1326
|
-
'aim due east → exits right edge at center-Y',
|
|
1327
|
-
function (fDone)
|
|
1328
|
-
{
|
|
1329
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1330
|
-
Node: _Hub,
|
|
1331
|
-
OtherNode: { X: 1000, Y: 300, Width: 200, Height: 100 } // center (1100, 350)
|
|
1332
|
-
});
|
|
1333
|
-
libExpect(tmpAttach.x).to.equal(600); // hub right edge
|
|
1334
|
-
libExpect(tmpAttach.y).to.equal(350); // hub center Y
|
|
1335
|
-
libExpect(tmpAttach.side).to.equal('right');
|
|
1336
|
-
fDone();
|
|
1337
|
-
}
|
|
1338
|
-
);
|
|
1339
|
-
|
|
1340
|
-
test
|
|
1341
|
-
(
|
|
1342
|
-
'aim due north → exits top edge at center-X',
|
|
1343
|
-
function (fDone)
|
|
1344
|
-
{
|
|
1345
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1346
|
-
Node: _Hub,
|
|
1347
|
-
OtherNode: { X: 400, Y: -100, Width: 200, Height: 100 } // center (500, -50)
|
|
1348
|
-
});
|
|
1349
|
-
libExpect(tmpAttach.x).to.equal(500); // hub center X
|
|
1350
|
-
libExpect(tmpAttach.y).to.equal(300); // hub top edge
|
|
1351
|
-
libExpect(tmpAttach.side).to.equal('top');
|
|
1352
|
-
fDone();
|
|
1353
|
-
}
|
|
1354
|
-
);
|
|
1355
|
-
|
|
1356
|
-
test
|
|
1357
|
-
(
|
|
1358
|
-
'aim due south → exits bottom edge at center-X',
|
|
1359
|
-
function (fDone)
|
|
1360
|
-
{
|
|
1361
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1362
|
-
Node: _Hub,
|
|
1363
|
-
OtherNode: { X: 400, Y: 700, Width: 200, Height: 100 } // center (500, 750)
|
|
1364
|
-
});
|
|
1365
|
-
libExpect(tmpAttach.x).to.equal(500);
|
|
1366
|
-
libExpect(tmpAttach.y).to.equal(400); // hub bottom edge
|
|
1367
|
-
libExpect(tmpAttach.side).to.equal('bottom');
|
|
1368
|
-
fDone();
|
|
1369
|
-
}
|
|
1370
|
-
);
|
|
1371
|
-
|
|
1372
|
-
test
|
|
1373
|
-
(
|
|
1374
|
-
'aim diagonally up-right → exits whichever edge the line hits first',
|
|
1375
|
-
function (fDone)
|
|
1376
|
-
{
|
|
1377
|
-
// Aim at (700, 100). dx=200, dy=-250.
|
|
1378
|
-
// halfW=100 → tx = 100/200 = 0.5
|
|
1379
|
-
// halfH=50 → ty = 50/250 = 0.2 ← smaller; top edge wins
|
|
1380
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1381
|
-
Node: _Hub,
|
|
1382
|
-
OtherNode: { X: 600, Y: 50, Width: 200, Height: 100 } // center (700, 100)
|
|
1383
|
-
});
|
|
1384
|
-
libExpect(tmpAttach.side).to.equal('top');
|
|
1385
|
-
libExpect(tmpAttach.y).to.equal(300);
|
|
1386
|
-
// At t=0.2 from (500,350) in direction (200,-250) → (540, 300).
|
|
1387
|
-
libExpect(tmpAttach.x).to.equal(540);
|
|
1388
|
-
fDone();
|
|
1389
|
-
}
|
|
1390
|
-
);
|
|
1391
|
-
|
|
1392
|
-
test
|
|
1393
|
-
(
|
|
1394
|
-
'8 spokes around a hub each get a unique exit point',
|
|
1395
|
-
function (fDone)
|
|
1396
|
-
{
|
|
1397
|
-
let tmpExits = {};
|
|
1398
|
-
for (let i = 0; i < 8; i++)
|
|
1399
|
-
{
|
|
1400
|
-
let tmpAngle = (i / 8) * 2 * Math.PI;
|
|
1401
|
-
let tmpSpoke = {
|
|
1402
|
-
X: 500 + Math.cos(tmpAngle) * 400 - 70,
|
|
1403
|
-
Y: 350 + Math.sin(tmpAngle) * 400 - 35,
|
|
1404
|
-
Width: 140, Height: 70
|
|
1405
|
-
};
|
|
1406
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1407
|
-
Node: _Hub,
|
|
1408
|
-
OtherNode: tmpSpoke
|
|
1409
|
-
});
|
|
1410
|
-
let tmpKey = `${Math.round(tmpAttach.x)},${Math.round(tmpAttach.y)}`;
|
|
1411
|
-
tmpExits[tmpKey] = (tmpExits[tmpKey] || 0) + 1;
|
|
1412
|
-
}
|
|
1413
|
-
let tmpUniqueExits = Object.keys(tmpExits).length;
|
|
1414
|
-
libExpect(tmpUniqueExits).to.equal(8);
|
|
1415
|
-
fDone();
|
|
1416
|
-
}
|
|
1417
|
-
);
|
|
1418
|
-
|
|
1419
|
-
test
|
|
1420
|
-
(
|
|
1421
|
-
'identical centers fall back to DefaultPosition',
|
|
1422
|
-
function (fDone)
|
|
1423
|
-
{
|
|
1424
|
-
let tmpDefault = { x: 600, y: 350, side: 'right' };
|
|
1425
|
-
let tmpAttach = libEdgePerimeter.ResolveAttachment({
|
|
1426
|
-
Node: _Hub,
|
|
1427
|
-
OtherNode: { X: 400, Y: 300, Width: 200, Height: 100 }, // identical center
|
|
1428
|
-
DefaultPosition: tmpDefault
|
|
1429
|
-
});
|
|
1430
|
-
libExpect(tmpAttach).to.equal(tmpDefault);
|
|
1431
|
-
fDone();
|
|
1432
|
-
}
|
|
1433
|
-
);
|
|
1434
|
-
|
|
1435
|
-
test
|
|
1436
|
-
(
|
|
1437
|
-
'returns null when node geometry is missing',
|
|
1438
|
-
function (fDone)
|
|
1439
|
-
{
|
|
1440
|
-
libExpect(libEdgePerimeter.ResolveAttachment({ Node: null })).to.equal(null);
|
|
1441
|
-
libExpect(libEdgePerimeter.ResolveAttachment({ Node: { X: 0 } })).to.equal(null);
|
|
1442
|
-
fDone();
|
|
1443
|
-
}
|
|
1444
|
-
);
|
|
1445
|
-
}
|
|
1446
|
-
);
|
|
1447
|
-
|
|
1448
|
-
// ── centerNodes ──────────────────────────────────────────────
|
|
1449
|
-
|
|
1450
|
-
suite
|
|
1451
|
-
(
|
|
1452
|
-
'centerNodes',
|
|
1453
|
-
function ()
|
|
1454
|
-
{
|
|
1455
|
-
test
|
|
1456
|
-
(
|
|
1457
|
-
'translates the bounding box center to a target',
|
|
1458
|
-
function (fDone)
|
|
1459
|
-
{
|
|
1460
|
-
let tmpNodes = [
|
|
1461
|
-
{ Hash: 'a', X: 0, Y: 0, Width: 100, Height: 50 },
|
|
1462
|
-
{ Hash: 'b', X: 200, Y: 0, Width: 100, Height: 50 }
|
|
1463
|
-
];
|
|
1464
|
-
_LayoutService.centerNodes(tmpNodes, 1000, 500);
|
|
1465
|
-
// Original bounding box: (0,0) to (300,50), center (150,25). Offset (850, 475).
|
|
1466
|
-
libExpect(tmpNodes[0].X).to.equal(850);
|
|
1467
|
-
libExpect(tmpNodes[0].Y).to.equal(475);
|
|
1468
|
-
libExpect(tmpNodes[1].X).to.equal(1050);
|
|
1469
|
-
libExpect(tmpNodes[1].Y).to.equal(475);
|
|
1470
|
-
fDone();
|
|
1471
|
-
}
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
);
|
|
1475
|
-
|
|
1476
|
-
// ── Auto-apply hookup (simulated) ────────────────────────────
|
|
1477
|
-
|
|
1478
|
-
suite
|
|
1479
|
-
(
|
|
1480
|
-
'Auto-apply event hookup',
|
|
1481
|
-
function ()
|
|
1482
|
-
{
|
|
1483
|
-
// Build a minimal mock FlowView that exposes just enough
|
|
1484
|
-
// to exercise the auto-apply handler logic in PictView-Flow.
|
|
1485
|
-
// We don't pull the full PictView-Flow because it requires a
|
|
1486
|
-
// pict-view runtime; instead we replicate the handler shape.
|
|
1487
|
-
function makeMockFlowView(pAlgorithm, pAutoApply)
|
|
1488
|
-
{
|
|
1489
|
-
let tmpHandlers = {};
|
|
1490
|
-
return {
|
|
1491
|
-
_FlowData: {
|
|
1492
|
-
Nodes: makeNodes(3),
|
|
1493
|
-
Connections: makeChain(3),
|
|
1494
|
-
LayoutAlgorithm: pAlgorithm,
|
|
1495
|
-
LayoutParameters: {},
|
|
1496
|
-
LayoutAutoApply: pAutoApply
|
|
1497
|
-
},
|
|
1498
|
-
_AutoApplyInProgress: false,
|
|
1499
|
-
_AutoApplyHandlerHashes: [],
|
|
1500
|
-
applyCalls: 0,
|
|
1501
|
-
_LayoutService: _LayoutService,
|
|
1502
|
-
applyCurrentLayout: function ()
|
|
1503
|
-
{
|
|
1504
|
-
this.applyCalls++;
|
|
1505
|
-
let tmpAlgo = this._FlowData.LayoutAlgorithm;
|
|
1506
|
-
if (tmpAlgo === 'Custom') return;
|
|
1507
|
-
this._AutoApplyInProgress = true;
|
|
1508
|
-
try
|
|
1509
|
-
{
|
|
1510
|
-
this._LayoutService.applyLayout(
|
|
1511
|
-
this._FlowData.Nodes,
|
|
1512
|
-
this._FlowData.Connections,
|
|
1513
|
-
tmpAlgo,
|
|
1514
|
-
this._FlowData.LayoutParameters
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
finally
|
|
1518
|
-
{
|
|
1519
|
-
this._AutoApplyInProgress = false;
|
|
1520
|
-
}
|
|
1521
|
-
},
|
|
1522
|
-
fireMockEvent: function (pName)
|
|
1523
|
-
{
|
|
1524
|
-
let tmpHandler = tmpHandlers[pName];
|
|
1525
|
-
if (tmpHandler) tmpHandler();
|
|
1526
|
-
},
|
|
1527
|
-
subscribe: function ()
|
|
1528
|
-
{
|
|
1529
|
-
let tmpEvents = ['onNodeAdded', 'onNodeRemoved', 'onConnectionCreated', 'onConnectionRemoved'];
|
|
1530
|
-
for (let i = 0; i < tmpEvents.length; i++)
|
|
1531
|
-
{
|
|
1532
|
-
((pEvent) =>
|
|
1533
|
-
{
|
|
1534
|
-
tmpHandlers[pEvent] = () =>
|
|
1535
|
-
{
|
|
1536
|
-
if (this._AutoApplyInProgress) return;
|
|
1537
|
-
if (!this._FlowData.LayoutAutoApply) return;
|
|
1538
|
-
if (!this._FlowData.LayoutAlgorithm || this._FlowData.LayoutAlgorithm === 'Custom') return;
|
|
1539
|
-
this.applyCurrentLayout();
|
|
1540
|
-
};
|
|
1541
|
-
})(tmpEvents[i]);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
};
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
test
|
|
1548
|
-
(
|
|
1549
|
-
'fires applyCurrentLayout on onNodeAdded when AutoApply is true and algorithm is non-Custom',
|
|
1550
|
-
function (fDone)
|
|
1551
|
-
{
|
|
1552
|
-
let tmpMock = makeMockFlowView('Grid', true);
|
|
1553
|
-
tmpMock.subscribe();
|
|
1554
|
-
tmpMock.fireMockEvent('onNodeAdded');
|
|
1555
|
-
libExpect(tmpMock.applyCalls).to.equal(1);
|
|
1556
|
-
fDone();
|
|
1557
|
-
}
|
|
1558
|
-
);
|
|
1559
|
-
|
|
1560
|
-
test
|
|
1561
|
-
(
|
|
1562
|
-
'does NOT fire when AutoApply is false',
|
|
1563
|
-
function (fDone)
|
|
1564
|
-
{
|
|
1565
|
-
let tmpMock = makeMockFlowView('Grid', false);
|
|
1566
|
-
tmpMock.subscribe();
|
|
1567
|
-
tmpMock.fireMockEvent('onNodeAdded');
|
|
1568
|
-
libExpect(tmpMock.applyCalls).to.equal(0);
|
|
1569
|
-
fDone();
|
|
1570
|
-
}
|
|
1571
|
-
);
|
|
1572
|
-
|
|
1573
|
-
test
|
|
1574
|
-
(
|
|
1575
|
-
'does NOT fire when algorithm is Custom',
|
|
1576
|
-
function (fDone)
|
|
1577
|
-
{
|
|
1578
|
-
let tmpMock = makeMockFlowView('Custom', true);
|
|
1579
|
-
tmpMock.subscribe();
|
|
1580
|
-
tmpMock.fireMockEvent('onNodeAdded');
|
|
1581
|
-
libExpect(tmpMock.applyCalls).to.equal(0);
|
|
1582
|
-
fDone();
|
|
1583
|
-
}
|
|
1584
|
-
);
|
|
1585
|
-
|
|
1586
|
-
test
|
|
1587
|
-
(
|
|
1588
|
-
'fires on each of the four structural events',
|
|
1589
|
-
function (fDone)
|
|
1590
|
-
{
|
|
1591
|
-
let tmpMock = makeMockFlowView('Grid', true);
|
|
1592
|
-
tmpMock.subscribe();
|
|
1593
|
-
tmpMock.fireMockEvent('onNodeAdded');
|
|
1594
|
-
tmpMock.fireMockEvent('onNodeRemoved');
|
|
1595
|
-
tmpMock.fireMockEvent('onConnectionCreated');
|
|
1596
|
-
tmpMock.fireMockEvent('onConnectionRemoved');
|
|
1597
|
-
libExpect(tmpMock.applyCalls).to.equal(4);
|
|
1598
|
-
fDone();
|
|
1599
|
-
}
|
|
1600
|
-
);
|
|
1601
|
-
}
|
|
1602
|
-
);
|
|
1603
|
-
}
|
|
1604
|
-
);
|