apostrophe 4.27.1 → 4.28.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/CHANGELOG.md +35 -0
- package/index.js +3 -0
- package/lib/stream-proxy.js +49 -0
- package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextTitle.vue +2 -11
- package/modules/@apostrophecms/area/ui/apos/apps/AposAreas.js +38 -6
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaEditor.vue +12 -1
- package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +111 -41
- package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +1 -0
- package/modules/@apostrophecms/area/ui/apos/components/AposWidgetControls.vue +22 -10
- package/modules/@apostrophecms/area/ui/apos/logic/AposAreaEditor.js +40 -0
- package/modules/@apostrophecms/asset/index.js +3 -2
- package/modules/@apostrophecms/attachment/index.js +270 -0
- package/modules/@apostrophecms/doc/index.js +8 -2
- package/modules/@apostrophecms/doc-type/index.js +81 -1
- package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +18 -2
- package/modules/@apostrophecms/express/index.js +30 -1
- package/modules/@apostrophecms/file/index.js +71 -6
- package/modules/@apostrophecms/i18n/index.js +20 -1
- package/modules/@apostrophecms/image/index.js +11 -0
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +31 -6
- package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +12 -10
- package/modules/@apostrophecms/login/index.js +43 -11
- package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +2 -1
- package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +5 -0
- package/modules/@apostrophecms/page/index.js +9 -11
- package/modules/@apostrophecms/page-type/index.js +6 -1
- package/modules/@apostrophecms/piece-page-type/index.js +100 -13
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposImageControlDialog.vue +1 -0
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposRichTextWidgetEditor.vue +28 -12
- package/modules/@apostrophecms/rich-text-widget/ui/apos/components/AposTiptapLink.vue +1 -0
- package/modules/@apostrophecms/schema/ui/apos/components/AposSearchList.vue +1 -1
- package/modules/@apostrophecms/styles/lib/apiRoutes.js +25 -5
- package/modules/@apostrophecms/styles/lib/handlers.js +19 -0
- package/modules/@apostrophecms/styles/lib/methods.js +35 -12
- package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +7 -2
- package/modules/@apostrophecms/task/index.js +9 -1
- package/modules/@apostrophecms/template/views/outerLayoutBase.html +3 -0
- package/modules/@apostrophecms/ui/index.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposButtonGroup.vue +1 -1
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenuDialog.vue +5 -0
- package/modules/@apostrophecms/ui/ui/apos/lib/vue.js +2 -0
- package/modules/@apostrophecms/ui/ui/apos/stores/widget.js +12 -7
- package/modules/@apostrophecms/ui/ui/apos/stores/widgetGraph.js +461 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/graph.js +452 -0
- package/modules/@apostrophecms/ui/ui/apos/universal/widgetGraph.js +10 -0
- package/modules/@apostrophecms/uploadfs/index.js +15 -1
- package/modules/@apostrophecms/url/index.js +419 -1
- package/package.json +6 -6
- package/test/add-missing-schema-fields-project/node_modules/.package-lock.json +131 -0
- package/test/external-front.js +1 -0
- package/test/files.js +135 -0
- package/test/login-requirements.js +145 -3
- package/test/static-build.js +2701 -0
- package/test/universal-graph.js +1135 -0
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
|
|
3
|
+
const getGraph = async () => import(
|
|
4
|
+
'../modules/@apostrophecms/ui/ui/apos/universal/graph.js'
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
describe('DirectedGraph (universal)', function () {
|
|
8
|
+
let DirectedGraph;
|
|
9
|
+
|
|
10
|
+
before(async function () {
|
|
11
|
+
const mod = await getGraph();
|
|
12
|
+
DirectedGraph = mod.default;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('#addNode', function () {
|
|
16
|
+
it('should add a node as a root with empty metadata', function () {
|
|
17
|
+
const g = new DirectedGraph();
|
|
18
|
+
g.addNode('a');
|
|
19
|
+
assert.equal(g.hasNode('a'), true);
|
|
20
|
+
assert.equal(g.size, 1);
|
|
21
|
+
assert.equal(g.getParent('a'), null);
|
|
22
|
+
assert.deepEqual(g.getMeta('a'), {});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should add a node with metadata', function () {
|
|
26
|
+
const g = new DirectedGraph();
|
|
27
|
+
g.addNode('w1', {
|
|
28
|
+
type: 'hero',
|
|
29
|
+
areaId: 'area-1'
|
|
30
|
+
});
|
|
31
|
+
assert.deepEqual(g.getMeta('w1'), {
|
|
32
|
+
type: 'hero',
|
|
33
|
+
areaId: 'area-1'
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should shallow-merge metadata when adding an existing node', function () {
|
|
38
|
+
const g = new DirectedGraph();
|
|
39
|
+
g.addNode('w1', {
|
|
40
|
+
type: 'hero',
|
|
41
|
+
areaId: 'area-1'
|
|
42
|
+
});
|
|
43
|
+
g.addNode('w1', { areaId: 'area-2' });
|
|
44
|
+
assert.deepEqual(g.getMeta('w1'), {
|
|
45
|
+
type: 'hero',
|
|
46
|
+
areaId: 'area-2'
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should not overwrite metadata when called with empty meta on existing node', function () {
|
|
51
|
+
const g = new DirectedGraph();
|
|
52
|
+
g.addNode('w1', { type: 'hero' });
|
|
53
|
+
g.addNode('w1');
|
|
54
|
+
assert.deepEqual(g.getMeta('w1'), { type: 'hero' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not duplicate the node on repeated calls', function () {
|
|
58
|
+
const g = new DirectedGraph();
|
|
59
|
+
g.addNode('a');
|
|
60
|
+
g.addNode('a');
|
|
61
|
+
assert.equal(g.size, 1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should support chaining', function () {
|
|
65
|
+
const g = new DirectedGraph();
|
|
66
|
+
const result = g.addNode('a');
|
|
67
|
+
assert.equal(result, g);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('#addEdge', function () {
|
|
72
|
+
it('should link parent to child', function () {
|
|
73
|
+
const g = new DirectedGraph();
|
|
74
|
+
g.addEdge('p', 'c');
|
|
75
|
+
assert.equal(g.hasEdge('p', 'c'), true);
|
|
76
|
+
assert.equal(g.getParent('c'), 'p');
|
|
77
|
+
assert.deepEqual(g.getChildren('p'), [ 'c' ]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should auto-create both nodes', function () {
|
|
81
|
+
const g = new DirectedGraph();
|
|
82
|
+
g.addEdge('p', 'c');
|
|
83
|
+
assert.equal(g.hasNode('p'), true);
|
|
84
|
+
assert.equal(g.hasNode('c'), true);
|
|
85
|
+
assert.equal(g.size, 2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should be idempotent for the same edge', function () {
|
|
89
|
+
const g = new DirectedGraph();
|
|
90
|
+
g.addEdge('p', 'c');
|
|
91
|
+
g.addEdge('p', 'c');
|
|
92
|
+
assert.deepEqual(g.getChildren('p'), [ 'c' ]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should throw on self-loop', function () {
|
|
96
|
+
const g = new DirectedGraph();
|
|
97
|
+
assert.throws(
|
|
98
|
+
() => g.addEdge('a', 'a'),
|
|
99
|
+
/Self-loop not allowed: "a"/
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw when child already has a different parent', function () {
|
|
104
|
+
const g = new DirectedGraph();
|
|
105
|
+
g.addEdge('p1', 'c');
|
|
106
|
+
assert.throws(
|
|
107
|
+
() => g.addEdge('p2', 'c'),
|
|
108
|
+
/already has parent "p1"/
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should throw when edge would create a cycle', function () {
|
|
113
|
+
const g = new DirectedGraph();
|
|
114
|
+
g.addEdge('a', 'b');
|
|
115
|
+
g.addEdge('b', 'c');
|
|
116
|
+
assert.throws(
|
|
117
|
+
() => g.addEdge('c', 'a'),
|
|
118
|
+
/would create a cycle/
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should throw on two-node cycle', function () {
|
|
123
|
+
const g = new DirectedGraph();
|
|
124
|
+
g.addEdge('a', 'b');
|
|
125
|
+
assert.throws(
|
|
126
|
+
() => g.addEdge('b', 'a'),
|
|
127
|
+
/would create a cycle/
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should support chaining', function () {
|
|
132
|
+
const g = new DirectedGraph();
|
|
133
|
+
const result = g.addEdge('a', 'b');
|
|
134
|
+
assert.equal(result, g);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should allow multiple children for one parent', function () {
|
|
138
|
+
const g = new DirectedGraph();
|
|
139
|
+
g.addEdge('p', 'c1');
|
|
140
|
+
g.addEdge('p', 'c2');
|
|
141
|
+
g.addEdge('p', 'c3');
|
|
142
|
+
const children = g.getChildren('p');
|
|
143
|
+
assert.equal(children.length, 3);
|
|
144
|
+
assert.ok(children.includes('c1'));
|
|
145
|
+
assert.ok(children.includes('c2'));
|
|
146
|
+
assert.ok(children.includes('c3'));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('#removeEdge', function () {
|
|
151
|
+
it('should detach the child (making it a root) but keep both nodes', function () {
|
|
152
|
+
const g = new DirectedGraph();
|
|
153
|
+
g.addEdge('p', 'c');
|
|
154
|
+
g.removeEdge('p', 'c');
|
|
155
|
+
assert.equal(g.hasEdge('p', 'c'), false);
|
|
156
|
+
assert.equal(g.getParent('c'), null);
|
|
157
|
+
assert.deepEqual(g.getChildren('p'), []);
|
|
158
|
+
assert.deepEqual(g.getChildren('c'), []);
|
|
159
|
+
assert.equal(g.hasNode('p'), true);
|
|
160
|
+
assert.equal(g.hasNode('c'), true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should preserve the subtree below the detached child', function () {
|
|
164
|
+
const g = new DirectedGraph();
|
|
165
|
+
g.addEdge('a', 'b');
|
|
166
|
+
g.addEdge('b', 'c');
|
|
167
|
+
g.addEdge('c', 'd');
|
|
168
|
+
g.removeEdge('a', 'b');
|
|
169
|
+
// b-c-d subtree is intact, b is now root
|
|
170
|
+
assert.equal(g.getParent('b'), null);
|
|
171
|
+
assert.equal(g.getParent('c'), 'b');
|
|
172
|
+
assert.equal(g.getParent('d'), 'c');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should be a no-op when edge does not exist', function () {
|
|
176
|
+
const g = new DirectedGraph();
|
|
177
|
+
g.addNode('a');
|
|
178
|
+
g.addNode('b');
|
|
179
|
+
g.removeEdge('a', 'b');
|
|
180
|
+
assert.equal(g.size, 2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should be a no-op when nodes do not exist', function () {
|
|
184
|
+
const g = new DirectedGraph();
|
|
185
|
+
g.removeEdge('x', 'y'); // no crash
|
|
186
|
+
assert.equal(g.size, 0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should not remove wrong edge when parentId does not match', function () {
|
|
190
|
+
const g = new DirectedGraph();
|
|
191
|
+
g.addEdge('p', 'c');
|
|
192
|
+
g.addNode('other');
|
|
193
|
+
g.removeEdge('other', 'c');
|
|
194
|
+
// Edge p→c should still exist
|
|
195
|
+
assert.equal(g.hasEdge('p', 'c'), true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should support chaining', function () {
|
|
199
|
+
const g = new DirectedGraph();
|
|
200
|
+
g.addEdge('p', 'c');
|
|
201
|
+
const result = g.removeEdge('p', 'c');
|
|
202
|
+
assert.equal(result, g);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('#removeNode', function () {
|
|
207
|
+
it('should remove a leaf node', function () {
|
|
208
|
+
const g = new DirectedGraph();
|
|
209
|
+
g.addEdge('p', 'c');
|
|
210
|
+
g.removeNode('c');
|
|
211
|
+
assert.equal(g.hasNode('c'), false);
|
|
212
|
+
assert.equal(g.hasNode('p'), true);
|
|
213
|
+
assert.deepEqual(g.getChildren('p'), []);
|
|
214
|
+
assert.equal(g.size, 1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should remove a node and its entire subtree', function () {
|
|
218
|
+
const g = new DirectedGraph();
|
|
219
|
+
g.addEdge('root', 'a');
|
|
220
|
+
g.addEdge('a', 'b');
|
|
221
|
+
g.addEdge('a', 'c');
|
|
222
|
+
g.addEdge('b', 'd');
|
|
223
|
+
g.removeNode('a');
|
|
224
|
+
assert.equal(g.hasNode('a'), false);
|
|
225
|
+
assert.equal(g.hasNode('b'), false);
|
|
226
|
+
assert.equal(g.hasNode('c'), false);
|
|
227
|
+
assert.equal(g.hasNode('d'), false);
|
|
228
|
+
assert.equal(g.hasNode('root'), true);
|
|
229
|
+
assert.deepEqual(g.getChildren('root'), []);
|
|
230
|
+
assert.equal(g.size, 1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should remove a root node and its entire subtree', function () {
|
|
234
|
+
const g = new DirectedGraph();
|
|
235
|
+
g.addEdge('root', 'a');
|
|
236
|
+
g.addEdge('root', 'b');
|
|
237
|
+
g.addEdge('a', 'c');
|
|
238
|
+
g.removeNode('root');
|
|
239
|
+
assert.equal(g.size, 0);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should remove metadata of all subtree nodes', function () {
|
|
243
|
+
const g = new DirectedGraph();
|
|
244
|
+
g.addNode('p', { type: 'parent' });
|
|
245
|
+
g.addNode('c', { type: 'child' });
|
|
246
|
+
g.addEdge('p', 'c');
|
|
247
|
+
g.removeNode('p');
|
|
248
|
+
assert.equal(g.getMeta('p'), null);
|
|
249
|
+
assert.equal(g.getMeta('c'), null);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should leave sibling subtrees intact', function () {
|
|
253
|
+
const g = new DirectedGraph();
|
|
254
|
+
g.addEdge('root', 'a');
|
|
255
|
+
g.addEdge('root', 'b');
|
|
256
|
+
g.addEdge('a', 'a1');
|
|
257
|
+
g.addEdge('b', 'b1');
|
|
258
|
+
g.removeNode('a');
|
|
259
|
+
assert.equal(g.hasNode('a'), false);
|
|
260
|
+
assert.equal(g.hasNode('a1'), false);
|
|
261
|
+
assert.equal(g.hasNode('b'), true);
|
|
262
|
+
assert.equal(g.hasNode('b1'), true);
|
|
263
|
+
assert.equal(g.getParent('b'), 'root');
|
|
264
|
+
assert.deepEqual(g.getChildren('root'), [ 'b' ]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should be a no-op when node does not exist', function () {
|
|
268
|
+
const g = new DirectedGraph();
|
|
269
|
+
g.addNode('a');
|
|
270
|
+
g.removeNode('xyz');
|
|
271
|
+
assert.equal(g.size, 1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should support chaining', function () {
|
|
275
|
+
const g = new DirectedGraph();
|
|
276
|
+
g.addNode('a');
|
|
277
|
+
const result = g.removeNode('a');
|
|
278
|
+
assert.equal(result, g);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('#hasNode / #hasEdge', function () {
|
|
283
|
+
it('should return false for non-existent node', function () {
|
|
284
|
+
const g = new DirectedGraph();
|
|
285
|
+
assert.equal(g.hasNode('nope'), false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should return false for non-existent edge', function () {
|
|
289
|
+
const g = new DirectedGraph();
|
|
290
|
+
g.addNode('a');
|
|
291
|
+
g.addNode('b');
|
|
292
|
+
assert.equal(g.hasEdge('a', 'b'), false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should return false for reversed edge direction', function () {
|
|
296
|
+
const g = new DirectedGraph();
|
|
297
|
+
g.addEdge('a', 'b');
|
|
298
|
+
assert.equal(g.hasEdge('b', 'a'), false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should return false when nodes do not exist at all', function () {
|
|
302
|
+
const g = new DirectedGraph();
|
|
303
|
+
assert.equal(g.hasEdge('x', 'y'), false);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('#getParent', function () {
|
|
308
|
+
it('should return null for a root node', function () {
|
|
309
|
+
const g = new DirectedGraph();
|
|
310
|
+
g.addNode('root');
|
|
311
|
+
assert.equal(g.getParent('root'), null);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should return the parent id', function () {
|
|
315
|
+
const g = new DirectedGraph();
|
|
316
|
+
g.addEdge('p', 'c');
|
|
317
|
+
assert.equal(g.getParent('c'), 'p');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should return only the direct parent, not a grandparent', function () {
|
|
321
|
+
const g = new DirectedGraph();
|
|
322
|
+
g.addEdge('gp', 'p');
|
|
323
|
+
g.addEdge('p', 'c');
|
|
324
|
+
assert.equal(g.getParent('c'), 'p');
|
|
325
|
+
assert.notEqual(g.getParent('c'), 'gp');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should return null for non-existent node', function () {
|
|
329
|
+
const g = new DirectedGraph();
|
|
330
|
+
assert.equal(g.getParent('nope'), null);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('#getChildren', function () {
|
|
335
|
+
it('should return an empty array for a leaf', function () {
|
|
336
|
+
const g = new DirectedGraph();
|
|
337
|
+
g.addNode('a');
|
|
338
|
+
assert.deepEqual(g.getChildren('a'), []);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should return children in insertion order', function () {
|
|
342
|
+
const g = new DirectedGraph();
|
|
343
|
+
g.addEdge('p', 'c1');
|
|
344
|
+
g.addEdge('p', 'c2');
|
|
345
|
+
g.addEdge('p', 'c3');
|
|
346
|
+
// doesn't report grandchildren
|
|
347
|
+
g.addEdge('c3', 'c3-1');
|
|
348
|
+
assert.deepEqual(g.getChildren('p'), [ 'c1', 'c2', 'c3' ]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should return an empty array for non-existent node', function () {
|
|
352
|
+
const g = new DirectedGraph();
|
|
353
|
+
assert.deepEqual(g.getChildren('nope'), []);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should return a copy, not a live reference', function () {
|
|
357
|
+
const g = new DirectedGraph();
|
|
358
|
+
g.addEdge('p', 'c');
|
|
359
|
+
const children = g.getChildren('p');
|
|
360
|
+
children.push('fake');
|
|
361
|
+
assert.deepEqual(g.getChildren('p'), [ 'c' ]);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('#hasCommonParent', function () {
|
|
366
|
+
it('should return true for siblings', function () {
|
|
367
|
+
const g = new DirectedGraph();
|
|
368
|
+
g.addEdge('p', 'a');
|
|
369
|
+
g.addEdge('p', 'b');
|
|
370
|
+
assert.equal(g.hasCommonParent('a', 'b'), true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should return false for nodes with different parents', function () {
|
|
374
|
+
const g = new DirectedGraph();
|
|
375
|
+
g.addEdge('p1', 'a');
|
|
376
|
+
g.addEdge('p2', 'b');
|
|
377
|
+
assert.equal(g.hasCommonParent('a', 'b'), false);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should return false for two root nodes', function () {
|
|
381
|
+
const g = new DirectedGraph();
|
|
382
|
+
g.addNode('r1');
|
|
383
|
+
g.addNode('r2');
|
|
384
|
+
assert.equal(g.hasCommonParent('r1', 'r2'), false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should return false when one node does not exist', function () {
|
|
388
|
+
const g = new DirectedGraph();
|
|
389
|
+
g.addNode('a');
|
|
390
|
+
assert.equal(g.hasCommonParent('a', 'nope'), false);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should return false for parent-child pair (not siblings)', function () {
|
|
394
|
+
const g = new DirectedGraph();
|
|
395
|
+
g.addEdge('p', 'c');
|
|
396
|
+
assert.equal(g.hasCommonParent('p', 'c'), false);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should return false when neither node exists', function () {
|
|
400
|
+
const g = new DirectedGraph();
|
|
401
|
+
assert.equal(g.hasCommonParent('x', 'y'), false);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('#hasCommonAncestor', function () {
|
|
406
|
+
it('should return true for siblings (common direct parent)', function () {
|
|
407
|
+
const g = new DirectedGraph();
|
|
408
|
+
g.addEdge('p', 'a');
|
|
409
|
+
g.addEdge('p', 'b');
|
|
410
|
+
assert.equal(g.hasCommonAncestor('a', 'b'), true);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should return true for cousins (common grandparent)', function () {
|
|
414
|
+
const g = new DirectedGraph();
|
|
415
|
+
g.addEdge('root', 'p1');
|
|
416
|
+
g.addEdge('root', 'p2');
|
|
417
|
+
g.addEdge('p1', 'a');
|
|
418
|
+
g.addEdge('p2', 'b');
|
|
419
|
+
assert.equal(g.hasCommonAncestor('a', 'b'), true);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should return true for nodes at different depths sharing an ancestor', function () {
|
|
423
|
+
const g = new DirectedGraph();
|
|
424
|
+
g.addEdge('root', 'p');
|
|
425
|
+
g.addEdge('p', 'c');
|
|
426
|
+
g.addEdge('root', 'sibling');
|
|
427
|
+
// c is at depth 2, sibling at depth 1, both share root
|
|
428
|
+
assert.equal(g.hasCommonAncestor('c', 'sibling'), true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should return true for parent-child (they share the grandparent)', function () {
|
|
432
|
+
const g = new DirectedGraph();
|
|
433
|
+
g.addEdge('root', 'p');
|
|
434
|
+
g.addEdge('p', 'c');
|
|
435
|
+
// p's ancestors: {root}, c's ancestors: {p, root} → root is common
|
|
436
|
+
assert.equal(g.hasCommonAncestor('p', 'c'), true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should return false for two root nodes', function () {
|
|
440
|
+
const g = new DirectedGraph();
|
|
441
|
+
g.addNode('r1');
|
|
442
|
+
g.addNode('r2');
|
|
443
|
+
assert.equal(g.hasCommonAncestor('r1', 'r2'), false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should return false for nodes in separate trees', function () {
|
|
447
|
+
const g = new DirectedGraph();
|
|
448
|
+
g.addEdge('r1', 'a');
|
|
449
|
+
g.addEdge('r2', 'b');
|
|
450
|
+
assert.equal(g.hasCommonAncestor('a', 'b'), false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should return false when one node is a root and the other is its child', function () {
|
|
454
|
+
const g = new DirectedGraph();
|
|
455
|
+
g.addEdge('root', 'c');
|
|
456
|
+
// root has no ancestors, so no common ancestor possible
|
|
457
|
+
assert.equal(g.hasCommonAncestor('root', 'c'), false);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should return false when one node does not exist', function () {
|
|
461
|
+
const g = new DirectedGraph();
|
|
462
|
+
g.addNode('a');
|
|
463
|
+
assert.equal(g.hasCommonAncestor('a', 'nope'), false);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should return false when neither node exists', function () {
|
|
467
|
+
const g = new DirectedGraph();
|
|
468
|
+
assert.equal(g.hasCommonAncestor('x', 'y'), false);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('#getMeta / #setMeta', function () {
|
|
473
|
+
it('should return null for non-existent node', function () {
|
|
474
|
+
const g = new DirectedGraph();
|
|
475
|
+
assert.equal(g.getMeta('nope'), null);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should return an empty object for a node added without meta', function () {
|
|
479
|
+
const g = new DirectedGraph();
|
|
480
|
+
g.addNode('a');
|
|
481
|
+
assert.deepEqual(g.getMeta('a'), {});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should shallow-merge metadata via setMeta', function () {
|
|
485
|
+
const g = new DirectedGraph();
|
|
486
|
+
g.addNode('w', { type: 'hero' });
|
|
487
|
+
g.setMeta('w', { areaId: 'area-1' });
|
|
488
|
+
assert.deepEqual(g.getMeta('w'), {
|
|
489
|
+
type: 'hero',
|
|
490
|
+
areaId: 'area-1'
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('should overwrite existing keys via setMeta', function () {
|
|
495
|
+
const g = new DirectedGraph();
|
|
496
|
+
g.addNode('w', {
|
|
497
|
+
type: 'hero',
|
|
498
|
+
areaId: 'area-1'
|
|
499
|
+
});
|
|
500
|
+
g.setMeta('w', { type: 'banner' });
|
|
501
|
+
assert.deepEqual(g.getMeta('w'), {
|
|
502
|
+
type: 'banner',
|
|
503
|
+
areaId: 'area-1'
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should throw when setting meta on non-existent node', function () {
|
|
508
|
+
const g = new DirectedGraph();
|
|
509
|
+
assert.throws(
|
|
510
|
+
() => g.setMeta('nope', { type: 'x' }),
|
|
511
|
+
/does not exist/
|
|
512
|
+
);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should support chaining on setMeta', function () {
|
|
516
|
+
const g = new DirectedGraph();
|
|
517
|
+
g.addNode('a');
|
|
518
|
+
const result = g.setMeta('a', { type: 'x' });
|
|
519
|
+
assert.equal(result, g);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should return a live reference to metadata (not a copy)', function () {
|
|
523
|
+
const g = new DirectedGraph();
|
|
524
|
+
g.addNode('a', { type: 'hero' });
|
|
525
|
+
const meta = g.getMeta('a');
|
|
526
|
+
meta.type = 'banner';
|
|
527
|
+
assert.equal(g.getMeta('a').type, 'banner');
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('#hasAncestor', function () {
|
|
532
|
+
it('should return true for a direct parent', function () {
|
|
533
|
+
const g = new DirectedGraph();
|
|
534
|
+
g.addEdge('p', 'c');
|
|
535
|
+
assert.equal(g.hasAncestor('c', 'p'), true);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should return true for a grandparent', function () {
|
|
539
|
+
const g = new DirectedGraph();
|
|
540
|
+
g.addEdge('gp', 'p');
|
|
541
|
+
g.addEdge('p', 'c');
|
|
542
|
+
assert.equal(g.hasAncestor('c', 'gp'), true);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should return false for self', function () {
|
|
546
|
+
const g = new DirectedGraph();
|
|
547
|
+
g.addNode('a');
|
|
548
|
+
assert.equal(g.hasAncestor('a', 'a'), false);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should return false for a descendant', function () {
|
|
552
|
+
const g = new DirectedGraph();
|
|
553
|
+
g.addEdge('p', 'c');
|
|
554
|
+
assert.equal(g.hasAncestor('p', 'c'), false);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should return false for unrelated nodes', function () {
|
|
558
|
+
const g = new DirectedGraph();
|
|
559
|
+
g.addNode('a');
|
|
560
|
+
g.addNode('b');
|
|
561
|
+
assert.equal(g.hasAncestor('a', 'b'), false);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should return false for non-existent nodes', function () {
|
|
565
|
+
const g = new DirectedGraph();
|
|
566
|
+
assert.equal(g.hasAncestor('x', 'y'), false);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('#hasDescendant', function () {
|
|
571
|
+
it('should return true for a direct child', function () {
|
|
572
|
+
const g = new DirectedGraph();
|
|
573
|
+
g.addEdge('p', 'c');
|
|
574
|
+
assert.equal(g.hasDescendant('p', 'c'), true);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should return true for a deeply nested descendant', function () {
|
|
578
|
+
const g = new DirectedGraph();
|
|
579
|
+
g.addEdge('a', 'b');
|
|
580
|
+
g.addEdge('b', 'c');
|
|
581
|
+
g.addEdge('c', 'd');
|
|
582
|
+
assert.equal(g.hasDescendant('a', 'd'), true);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should return false for self', function () {
|
|
586
|
+
const g = new DirectedGraph();
|
|
587
|
+
g.addNode('a');
|
|
588
|
+
assert.equal(g.hasDescendant('a', 'a'), false);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it('should return false for an ancestor', function () {
|
|
592
|
+
const g = new DirectedGraph();
|
|
593
|
+
g.addEdge('p', 'c');
|
|
594
|
+
assert.equal(g.hasDescendant('c', 'p'), false);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should return false for unrelated nodes', function () {
|
|
598
|
+
const g = new DirectedGraph();
|
|
599
|
+
g.addNode('a');
|
|
600
|
+
g.addNode('b');
|
|
601
|
+
assert.equal(g.hasDescendant('a', 'b'), false);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should return false for non-existent nodes', function () {
|
|
605
|
+
const g = new DirectedGraph();
|
|
606
|
+
assert.equal(g.hasDescendant('x', 'y'), false);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
describe('#getAncestors', function () {
|
|
611
|
+
it('should return empty array for a root', function () {
|
|
612
|
+
const g = new DirectedGraph();
|
|
613
|
+
g.addNode('root');
|
|
614
|
+
assert.deepEqual(g.getAncestors('root'), []);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('should return ancestors ordered nearest-first', function () {
|
|
618
|
+
const g = new DirectedGraph();
|
|
619
|
+
g.addEdge('gp', 'p');
|
|
620
|
+
g.addEdge('p', 'c');
|
|
621
|
+
g.addEdge('c', 'gc');
|
|
622
|
+
assert.deepEqual(g.getAncestors('gc'), [ 'c', 'p', 'gp' ]);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('should return empty array for non-existent node', function () {
|
|
626
|
+
const g = new DirectedGraph();
|
|
627
|
+
assert.deepEqual(g.getAncestors('nope'), []);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should return only the direct parent for depth-1 node', function () {
|
|
631
|
+
const g = new DirectedGraph();
|
|
632
|
+
g.addEdge('p', 'c');
|
|
633
|
+
assert.deepEqual(g.getAncestors('c'), [ 'p' ]);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe('#getDescendants', function () {
|
|
638
|
+
it('should return empty array for a leaf', function () {
|
|
639
|
+
const g = new DirectedGraph();
|
|
640
|
+
g.addNode('leaf');
|
|
641
|
+
assert.deepEqual(g.getDescendants('leaf'), []);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('should return all descendants breadth-first', function () {
|
|
645
|
+
const g = new DirectedGraph();
|
|
646
|
+
g.addEdge('root', 'a');
|
|
647
|
+
g.addEdge('root', 'b');
|
|
648
|
+
g.addEdge('a', 'a1');
|
|
649
|
+
g.addEdge('a', 'a2');
|
|
650
|
+
g.addEdge('b', 'b1');
|
|
651
|
+
const desc = g.getDescendants('root');
|
|
652
|
+
assert.equal(desc.length, 5);
|
|
653
|
+
// a and b before their own children
|
|
654
|
+
assert.ok(desc.indexOf('a') < desc.indexOf('a1'));
|
|
655
|
+
assert.ok(desc.indexOf('a') < desc.indexOf('a2'));
|
|
656
|
+
assert.ok(desc.indexOf('b') < desc.indexOf('b1'));
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should return empty for non-existent node', function () {
|
|
660
|
+
const g = new DirectedGraph();
|
|
661
|
+
assert.deepEqual(g.getDescendants('nope'), []);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe('#getRoot', function () {
|
|
666
|
+
it('should return the node itself if it is a root', function () {
|
|
667
|
+
const g = new DirectedGraph();
|
|
668
|
+
g.addNode('r');
|
|
669
|
+
assert.equal(g.getRoot('r'), 'r');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should return the topmost ancestor', function () {
|
|
673
|
+
const g = new DirectedGraph();
|
|
674
|
+
g.addEdge('r', 'a');
|
|
675
|
+
g.addEdge('a', 'b');
|
|
676
|
+
g.addEdge('b', 'c');
|
|
677
|
+
assert.equal(g.getRoot('c'), 'r');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should return null for non-existent node', function () {
|
|
681
|
+
const g = new DirectedGraph();
|
|
682
|
+
assert.equal(g.getRoot('nope'), null);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe('#getRoots / #getLeaves', function () {
|
|
687
|
+
it('should return all root nodes', function () {
|
|
688
|
+
const g = new DirectedGraph();
|
|
689
|
+
g.addEdge('r1', 'a');
|
|
690
|
+
g.addEdge('r2', 'b');
|
|
691
|
+
const roots = g.getRoots();
|
|
692
|
+
assert.equal(roots.length, 2);
|
|
693
|
+
assert.ok(roots.includes('r1'));
|
|
694
|
+
assert.ok(roots.includes('r2'));
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should return all leaf nodes', function () {
|
|
698
|
+
const g = new DirectedGraph();
|
|
699
|
+
g.addEdge('r', 'a');
|
|
700
|
+
g.addEdge('r', 'b');
|
|
701
|
+
g.addEdge('a', 'c');
|
|
702
|
+
const leaves = g.getLeaves();
|
|
703
|
+
assert.equal(leaves.length, 2);
|
|
704
|
+
assert.ok(leaves.includes('b'));
|
|
705
|
+
assert.ok(leaves.includes('c'));
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('should count an isolated node as both root and leaf', function () {
|
|
709
|
+
const g = new DirectedGraph();
|
|
710
|
+
g.addNode('solo');
|
|
711
|
+
assert.deepEqual(g.getRoots(), [ 'solo' ]);
|
|
712
|
+
assert.deepEqual(g.getLeaves(), [ 'solo' ]);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('should return empty arrays for empty graph', function () {
|
|
716
|
+
const g = new DirectedGraph();
|
|
717
|
+
assert.deepEqual(g.getRoots(), []);
|
|
718
|
+
assert.deepEqual(g.getLeaves(), []);
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
describe('#getDepth', function () {
|
|
723
|
+
it('should return 0 for a root node', function () {
|
|
724
|
+
const g = new DirectedGraph();
|
|
725
|
+
g.addNode('r');
|
|
726
|
+
assert.equal(g.getDepth('r'), 0);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('should return correct depth for nested nodes', function () {
|
|
730
|
+
const g = new DirectedGraph();
|
|
731
|
+
g.addEdge('r', 'a');
|
|
732
|
+
g.addEdge('a', 'b');
|
|
733
|
+
g.addEdge('b', 'c');
|
|
734
|
+
assert.equal(g.getDepth('r'), 0);
|
|
735
|
+
assert.equal(g.getDepth('a'), 1);
|
|
736
|
+
assert.equal(g.getDepth('b'), 2);
|
|
737
|
+
assert.equal(g.getDepth('c'), 3);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('should return -1 for non-existent node', function () {
|
|
741
|
+
const g = new DirectedGraph();
|
|
742
|
+
assert.equal(g.getDepth('nope'), -1);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
describe('#getNodes / #size', function () {
|
|
747
|
+
it('should return all node ids', function () {
|
|
748
|
+
const g = new DirectedGraph();
|
|
749
|
+
g.addEdge('a', 'b');
|
|
750
|
+
g.addNode('c');
|
|
751
|
+
const nodes = g.getNodes();
|
|
752
|
+
assert.equal(nodes.length, 3);
|
|
753
|
+
assert.ok(nodes.includes('a'));
|
|
754
|
+
assert.ok(nodes.includes('b'));
|
|
755
|
+
assert.ok(nodes.includes('c'));
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should report correct size', function () {
|
|
759
|
+
const g = new DirectedGraph();
|
|
760
|
+
assert.equal(g.size, 0);
|
|
761
|
+
g.addNode('a');
|
|
762
|
+
assert.equal(g.size, 1);
|
|
763
|
+
g.addEdge('a', 'b');
|
|
764
|
+
assert.equal(g.size, 2);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should return empty array for empty graph', function () {
|
|
768
|
+
const g = new DirectedGraph();
|
|
769
|
+
assert.deepEqual(g.getNodes(), []);
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
describe('#clear', function () {
|
|
774
|
+
it('should remove everything', function () {
|
|
775
|
+
const g = new DirectedGraph();
|
|
776
|
+
g.addEdge('a', 'b');
|
|
777
|
+
g.addEdge('b', 'c');
|
|
778
|
+
g.addNode('d', { type: 'x' });
|
|
779
|
+
g.clear();
|
|
780
|
+
assert.equal(g.size, 0);
|
|
781
|
+
assert.equal(g.hasNode('a'), false);
|
|
782
|
+
assert.equal(g.getMeta('d'), null);
|
|
783
|
+
assert.deepEqual(g.getNodes(), []);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('should support chaining', function () {
|
|
787
|
+
const g = new DirectedGraph();
|
|
788
|
+
const result = g.clear();
|
|
789
|
+
assert.equal(result, g);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should allow re-use after clear', function () {
|
|
793
|
+
const g = new DirectedGraph();
|
|
794
|
+
g.addEdge('a', 'b');
|
|
795
|
+
g.clear();
|
|
796
|
+
g.addEdge('x', 'y');
|
|
797
|
+
assert.equal(g.size, 2);
|
|
798
|
+
assert.equal(g.hasEdge('x', 'y'), true);
|
|
799
|
+
assert.equal(g.hasNode('a'), false);
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
describe('chaining', function () {
|
|
804
|
+
it('should support fluent construction', function () {
|
|
805
|
+
const g = new DirectedGraph();
|
|
806
|
+
g.addNode('root', { type: 'page' })
|
|
807
|
+
.addNode('hero', {
|
|
808
|
+
type: 'hero',
|
|
809
|
+
areaId: 'main'
|
|
810
|
+
})
|
|
811
|
+
.addEdge('root', 'hero')
|
|
812
|
+
.addEdge('hero', 'text')
|
|
813
|
+
.setMeta('text', {
|
|
814
|
+
type: 'text',
|
|
815
|
+
areaId: 'hero-content'
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
assert.equal(g.size, 3);
|
|
819
|
+
assert.equal(g.getParent('text'), 'hero');
|
|
820
|
+
assert.deepEqual(g.getMeta('text'), {
|
|
821
|
+
type: 'text',
|
|
822
|
+
areaId: 'hero-content'
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('forest (multiple independent trees)', function () {
|
|
828
|
+
it('should support independent trees with isolated queries', function () {
|
|
829
|
+
const g = new DirectedGraph();
|
|
830
|
+
g.addEdge('r1', 'a');
|
|
831
|
+
g.addEdge('r1', 'b');
|
|
832
|
+
g.addEdge('r2', 'c');
|
|
833
|
+
g.addEdge('r2', 'd');
|
|
834
|
+
|
|
835
|
+
assert.equal(g.getRoot('a'), 'r1');
|
|
836
|
+
assert.equal(g.getRoot('c'), 'r2');
|
|
837
|
+
assert.equal(g.hasDescendant('r1', 'c'), false);
|
|
838
|
+
assert.equal(g.hasAncestor('a', 'r2'), false);
|
|
839
|
+
assert.equal(g.hasCommonParent('a', 'b'), true);
|
|
840
|
+
assert.equal(g.hasCommonParent('a', 'c'), false);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('should remove one tree without affecting the other', function () {
|
|
844
|
+
const g = new DirectedGraph();
|
|
845
|
+
g.addEdge('r1', 'a');
|
|
846
|
+
g.addEdge('r2', 'b');
|
|
847
|
+
g.removeNode('r1');
|
|
848
|
+
assert.equal(g.hasNode('r1'), false);
|
|
849
|
+
assert.equal(g.hasNode('a'), false);
|
|
850
|
+
assert.equal(g.hasNode('r2'), true);
|
|
851
|
+
assert.equal(g.hasNode('b'), true);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe('complex widget-like scenario', function () {
|
|
856
|
+
let g;
|
|
857
|
+
|
|
858
|
+
beforeEach(function () {
|
|
859
|
+
// Tree 1 (area-main):
|
|
860
|
+
// hero (type: hero, area: main) depth 0
|
|
861
|
+
// ├── text (type: text, area: hero-body) depth 1
|
|
862
|
+
// └── slideshow (type: slideshow, area: hero-body) depth 1
|
|
863
|
+
// ├── img1 (type: image, area: slides) depth 2
|
|
864
|
+
// └── img2 (type: image, area: slides) depth 2
|
|
865
|
+
// Tree 2 (area-sidebar):
|
|
866
|
+
// layout (type: two-col, area: sidebar) depth 0
|
|
867
|
+
// ├── card1 (type: card, area: col-left) depth 1
|
|
868
|
+
// └── card2 (type: card, area: col-right) depth 1
|
|
869
|
+
// └── badge (type: badge, area: card-footer) depth 2
|
|
870
|
+
g = new DirectedGraph();
|
|
871
|
+
|
|
872
|
+
// Tree 1
|
|
873
|
+
g.addNode('hero', {
|
|
874
|
+
type: 'hero',
|
|
875
|
+
areaId: 'main'
|
|
876
|
+
});
|
|
877
|
+
g.addNode('text', {
|
|
878
|
+
type: 'text',
|
|
879
|
+
areaId: 'hero-body'
|
|
880
|
+
});
|
|
881
|
+
g.addNode('slideshow', {
|
|
882
|
+
type: 'slideshow',
|
|
883
|
+
areaId: 'hero-body'
|
|
884
|
+
});
|
|
885
|
+
g.addNode('img1', {
|
|
886
|
+
type: 'image',
|
|
887
|
+
areaId: 'slides'
|
|
888
|
+
});
|
|
889
|
+
g.addNode('img2', {
|
|
890
|
+
type: 'image',
|
|
891
|
+
areaId: 'slides'
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
g.addEdge('hero', 'text');
|
|
895
|
+
g.addEdge('hero', 'slideshow');
|
|
896
|
+
g.addEdge('slideshow', 'img1');
|
|
897
|
+
g.addEdge('slideshow', 'img2');
|
|
898
|
+
|
|
899
|
+
// Tree 2
|
|
900
|
+
g.addNode('layout', {
|
|
901
|
+
type: 'two-col',
|
|
902
|
+
areaId: 'sidebar'
|
|
903
|
+
});
|
|
904
|
+
g.addNode('card1', {
|
|
905
|
+
type: 'card',
|
|
906
|
+
areaId: 'col-left'
|
|
907
|
+
});
|
|
908
|
+
g.addNode('card2', {
|
|
909
|
+
type: 'card',
|
|
910
|
+
areaId: 'col-right'
|
|
911
|
+
});
|
|
912
|
+
g.addNode('badge', {
|
|
913
|
+
type: 'badge',
|
|
914
|
+
areaId: 'card-footer'
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
g.addEdge('layout', 'card1');
|
|
918
|
+
g.addEdge('layout', 'card2');
|
|
919
|
+
g.addEdge('card2', 'badge');
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('should have 9 total nodes across two trees', function () {
|
|
923
|
+
assert.equal(g.size, 9);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('should report correct parent chain for deeply nested node', function () {
|
|
927
|
+
assert.deepEqual(g.getAncestors('img1'), [ 'slideshow', 'hero' ]);
|
|
928
|
+
assert.deepEqual(g.getAncestors('badge'), [ 'card2', 'layout' ]);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should report correct depth', function () {
|
|
932
|
+
assert.equal(g.getDepth('hero'), 0);
|
|
933
|
+
assert.equal(g.getDepth('slideshow'), 1);
|
|
934
|
+
assert.equal(g.getDepth('img1'), 2);
|
|
935
|
+
assert.equal(g.getDepth('layout'), 0);
|
|
936
|
+
assert.equal(g.getDepth('card2'), 1);
|
|
937
|
+
assert.equal(g.getDepth('badge'), 2);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should report siblings via hasCommonParent', function () {
|
|
941
|
+
// Same parent
|
|
942
|
+
assert.equal(g.hasCommonParent('text', 'slideshow'), true);
|
|
943
|
+
assert.equal(g.hasCommonParent('img1', 'img2'), true);
|
|
944
|
+
assert.equal(g.hasCommonParent('card1', 'card2'), true);
|
|
945
|
+
// Different parents
|
|
946
|
+
assert.equal(g.hasCommonParent('text', 'img1'), false);
|
|
947
|
+
assert.equal(g.hasCommonParent('img1', 'card1'), false);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
it('should detect common ancestors across depth levels', function () {
|
|
951
|
+
// Within tree 1: img1 and text share ancestor hero
|
|
952
|
+
assert.equal(g.hasCommonAncestor('img1', 'text'), true);
|
|
953
|
+
// Within tree 1: img1 and img2 share ancestors (slideshow, hero)
|
|
954
|
+
assert.equal(g.hasCommonAncestor('img1', 'img2'), true);
|
|
955
|
+
// Within tree 2: badge and card1 share ancestor layout
|
|
956
|
+
assert.equal(g.hasCommonAncestor('badge', 'card1'), true);
|
|
957
|
+
// Across trees: no common ancestor
|
|
958
|
+
assert.equal(g.hasCommonAncestor('img1', 'badge'), false);
|
|
959
|
+
assert.equal(g.hasCommonAncestor('text', 'card1'), false);
|
|
960
|
+
// Root nodes: no ancestors at all
|
|
961
|
+
assert.equal(g.hasCommonAncestor('hero', 'layout'), false);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('should find two roots and correct leaves', function () {
|
|
965
|
+
const roots = g.getRoots();
|
|
966
|
+
assert.equal(roots.length, 2);
|
|
967
|
+
assert.ok(roots.includes('hero'));
|
|
968
|
+
assert.ok(roots.includes('layout'));
|
|
969
|
+
|
|
970
|
+
const leaves = g.getLeaves();
|
|
971
|
+
assert.equal(leaves.length, 5);
|
|
972
|
+
assert.ok(leaves.includes('text'));
|
|
973
|
+
assert.ok(leaves.includes('img1'));
|
|
974
|
+
assert.ok(leaves.includes('img2'));
|
|
975
|
+
assert.ok(leaves.includes('card1'));
|
|
976
|
+
assert.ok(leaves.includes('badge'));
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('should return all descendants of a subtree', function () {
|
|
980
|
+
const desc = g.getDescendants('hero');
|
|
981
|
+
assert.equal(desc.length, 4);
|
|
982
|
+
assert.ok(desc.includes('text'));
|
|
983
|
+
assert.ok(desc.includes('slideshow'));
|
|
984
|
+
assert.ok(desc.includes('img1'));
|
|
985
|
+
assert.ok(desc.includes('img2'));
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('should check ancestor/descendant relationships within and across trees', function () {
|
|
989
|
+
assert.equal(g.hasAncestor('img1', 'hero'), true);
|
|
990
|
+
assert.equal(g.hasAncestor('img1', 'slideshow'), true);
|
|
991
|
+
assert.equal(g.hasAncestor('img1', 'layout'), false);
|
|
992
|
+
assert.equal(g.hasDescendant('hero', 'img2'), true);
|
|
993
|
+
assert.equal(g.hasDescendant('hero', 'badge'), false);
|
|
994
|
+
assert.equal(g.hasDescendant('layout', 'badge'), true);
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('should get root from any node in either tree', function () {
|
|
998
|
+
assert.equal(g.getRoot('img2'), 'hero');
|
|
999
|
+
assert.equal(g.getRoot('hero'), 'hero');
|
|
1000
|
+
assert.equal(g.getRoot('badge'), 'layout');
|
|
1001
|
+
assert.equal(g.getRoot('layout'), 'layout');
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('should removeNode (slideshow) and take its subtree', function () {
|
|
1005
|
+
g.removeNode('slideshow');
|
|
1006
|
+
assert.equal(g.hasNode('slideshow'), false);
|
|
1007
|
+
assert.equal(g.hasNode('img1'), false);
|
|
1008
|
+
assert.equal(g.hasNode('img2'), false);
|
|
1009
|
+
// rest of tree 1 intact
|
|
1010
|
+
assert.equal(g.hasNode('hero'), true);
|
|
1011
|
+
assert.equal(g.hasNode('text'), true);
|
|
1012
|
+
assert.deepEqual(g.getChildren('hero'), [ 'text' ]);
|
|
1013
|
+
// tree 2 unaffected
|
|
1014
|
+
assert.equal(g.hasNode('layout'), true);
|
|
1015
|
+
assert.equal(g.hasNode('badge'), true);
|
|
1016
|
+
assert.equal(g.size, 6);
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
it('should removeEdge (hero→slideshow) and make slideshow subtree a new tree', function () {
|
|
1020
|
+
g.removeEdge('hero', 'slideshow');
|
|
1021
|
+
assert.equal(g.getParent('slideshow'), null);
|
|
1022
|
+
assert.equal(g.getParent('img1'), 'slideshow');
|
|
1023
|
+
assert.equal(g.getRoot('img1'), 'slideshow');
|
|
1024
|
+
const roots = g.getRoots();
|
|
1025
|
+
assert.equal(roots.length, 3);
|
|
1026
|
+
assert.ok(roots.includes('hero'));
|
|
1027
|
+
assert.ok(roots.includes('slideshow'));
|
|
1028
|
+
assert.ok(roots.includes('layout'));
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it('should read metadata via getMeta', function () {
|
|
1032
|
+
assert.deepEqual(g.getMeta('hero'), {
|
|
1033
|
+
type: 'hero',
|
|
1034
|
+
areaId: 'main'
|
|
1035
|
+
});
|
|
1036
|
+
assert.deepEqual(g.getMeta('img1'), {
|
|
1037
|
+
type: 'image',
|
|
1038
|
+
areaId: 'slides'
|
|
1039
|
+
});
|
|
1040
|
+
assert.deepEqual(g.getMeta('badge'), {
|
|
1041
|
+
type: 'badge',
|
|
1042
|
+
areaId: 'card-footer'
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
describe('edge cases', function () {
|
|
1048
|
+
it('should handle a single isolated node', function () {
|
|
1049
|
+
const g = new DirectedGraph();
|
|
1050
|
+
g.addNode('solo');
|
|
1051
|
+
assert.equal(g.getParent('solo'), null);
|
|
1052
|
+
assert.deepEqual(g.getChildren('solo'), []);
|
|
1053
|
+
assert.deepEqual(g.getAncestors('solo'), []);
|
|
1054
|
+
assert.deepEqual(g.getDescendants('solo'), []);
|
|
1055
|
+
assert.equal(g.getRoot('solo'), 'solo');
|
|
1056
|
+
assert.equal(g.getDepth('solo'), 0);
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should handle a long chain (depth stress)', function () {
|
|
1060
|
+
const g = new DirectedGraph();
|
|
1061
|
+
const depth = 1000;
|
|
1062
|
+
for (let i = 0; i < depth; i++) {
|
|
1063
|
+
g.addEdge(`n${i}`, `n${i + 1}`);
|
|
1064
|
+
}
|
|
1065
|
+
assert.equal(g.size, depth + 1);
|
|
1066
|
+
assert.equal(g.getDepth(`n${depth}`), depth);
|
|
1067
|
+
assert.equal(g.getRoot(`n${depth}`), 'n0');
|
|
1068
|
+
assert.equal(g.hasAncestor(`n${depth}`, 'n0'), true);
|
|
1069
|
+
assert.equal(g.hasDescendant('n0', `n${depth}`), true);
|
|
1070
|
+
assert.equal(g.getAncestors(`n${depth}`).length, depth);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it('should handle a wide tree (many children)', function () {
|
|
1074
|
+
const g = new DirectedGraph();
|
|
1075
|
+
const width = 500;
|
|
1076
|
+
for (let i = 0; i < width; i++) {
|
|
1077
|
+
g.addEdge('root', `c${i}`);
|
|
1078
|
+
}
|
|
1079
|
+
assert.equal(g.size, width + 1);
|
|
1080
|
+
assert.equal(g.getChildren('root').length, width);
|
|
1081
|
+
assert.deepEqual(g.getLeaves().length, width);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('should handle removeNode on the only node', function () {
|
|
1085
|
+
const g = new DirectedGraph();
|
|
1086
|
+
g.addNode('only');
|
|
1087
|
+
g.removeNode('only');
|
|
1088
|
+
assert.equal(g.size, 0);
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it('should handle re-adding a node after removal', function () {
|
|
1092
|
+
const g = new DirectedGraph();
|
|
1093
|
+
g.addNode('a', { type: 'x' });
|
|
1094
|
+
g.addEdge('root', 'a');
|
|
1095
|
+
g.removeNode('a');
|
|
1096
|
+
g.addNode('a', { type: 'y' });
|
|
1097
|
+
assert.equal(g.hasNode('a'), true);
|
|
1098
|
+
assert.deepEqual(g.getMeta('a'), { type: 'y' });
|
|
1099
|
+
assert.equal(g.getParent('a'), null);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it('should handle re-adding an edge after removeEdge', function () {
|
|
1103
|
+
const g = new DirectedGraph();
|
|
1104
|
+
g.addEdge('p', 'c');
|
|
1105
|
+
g.removeEdge('p', 'c');
|
|
1106
|
+
g.addEdge('p', 'c');
|
|
1107
|
+
assert.equal(g.hasEdge('p', 'c'), true);
|
|
1108
|
+
assert.equal(g.getParent('c'), 'p');
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('should allow moving a child to a new parent via removeEdge + addEdge', function () {
|
|
1112
|
+
const g = new DirectedGraph();
|
|
1113
|
+
g.addEdge('p1', 'c');
|
|
1114
|
+
g.addEdge('p2', 'other');
|
|
1115
|
+
g.removeEdge('p1', 'c');
|
|
1116
|
+
g.addEdge('p2', 'c');
|
|
1117
|
+
assert.equal(g.getParent('c'), 'p2');
|
|
1118
|
+
assert.deepEqual(g.getChildren('p1'), []);
|
|
1119
|
+
assert.ok(g.getChildren('p2').includes('c'));
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('should handle empty string as node id', function () {
|
|
1123
|
+
const g = new DirectedGraph();
|
|
1124
|
+
g.addNode('', { type: 'empty-id' });
|
|
1125
|
+
assert.equal(g.hasNode(''), true);
|
|
1126
|
+
assert.deepEqual(g.getMeta(''), { type: 'empty-id' });
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('should handle numeric-looking string ids', function () {
|
|
1130
|
+
const g = new DirectedGraph();
|
|
1131
|
+
g.addEdge('123', '456');
|
|
1132
|
+
assert.equal(g.getParent('456'), '123');
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
});
|