orgchart 6.0.0 → 7.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.
Files changed (35) hide show
  1. package/.github/workflows/publish.yml +11 -8
  2. package/README.md +47 -35
  3. package/README.zh-cn.md +73 -30
  4. package/README.zh-tw.md +73 -30
  5. package/demo/edit-chart.html +0 -4
  6. package/demo/edit-hybrid-chart.html +3 -4
  7. package/demo/index.html +5 -1
  8. package/demo/ondemand-loading-data.html +137 -80
  9. package/demo/options-compact-drag.html +82 -0
  10. package/demo/options-export-navigation.html +76 -0
  11. package/demo/options-horizontal-template.html +80 -0
  12. package/demo/options-hybrid-navigation.html +71 -0
  13. package/dist/css/orgchart.css +12 -1
  14. package/dist/css/orgchart.min.css +1 -1
  15. package/dist/js/orgchart.js +896 -523
  16. package/dist/js/orgchart.min.js +1 -1
  17. package/dist/js/orgchart.min.js.map +1 -1
  18. package/gulpfile.js +5 -1
  19. package/package.json +2 -1
  20. package/src/css/orgchart.css +12 -1
  21. package/src/js/orgchart.js +896 -523
  22. package/test/compatibility/README.md +123 -0
  23. package/test/compatibility/browser.tests.js +1279 -0
  24. package/test/compatibility/options.tests.js +628 -0
  25. package/test/cypress/e2e/demo-interactions.cy.js +3 -3
  26. package/test/cypress/e2e/demo-rendering.cy.js +62 -1
  27. package/test/cypress/e2e/drag-drop-hybrid-chart.cy.js +2 -1
  28. package/test/cypress/e2e/local-datasource.cy.js +16 -16
  29. package/test/cypress/e2e/ondemand-loading-data.cy.js +34 -10
  30. package/test/cypress/e2e/toggle-sibs-resp.cy.js +4 -4
  31. package/test/cypress/e2e/vertical-vevel.cy.js +5 -5
  32. package/test/integration/init-tests.js +30 -43
  33. package/test/integration/tests.js +21 -13
  34. package/test/unit/lifecycle-builders-export.tests.js +179 -118
  35. package/test/unit/tests.js +33 -34
@@ -0,0 +1,1279 @@
1
+ const chai = require('chai');
2
+ const path = require('node:path');
3
+ const puppeteer = require('puppeteer');
4
+
5
+ const repositoryRoot = path.resolve(__dirname, '../..');
6
+ const directions = ['t2b', 'b2t', 'l2r', 'r2l'];
7
+ const markerColors = [[240, 30, 30], [25, 180, 75], [250, 180, 25], [45, 90, 240], [170, 55, 210], [25, 190, 200]];
8
+
9
+ describe('orgchart browser option compatibility', function () {
10
+ this.timeout(30000);
11
+
12
+ let browser;
13
+ let page;
14
+
15
+ before(async function () {
16
+ browser = await puppeteer.launch({
17
+ headless: true,
18
+ args: process.platform === 'linux' && process.env.GITHUB_ACTIONS === 'true' ? ['--no-sandbox'] : []
19
+ });
20
+ });
21
+
22
+ after(async function () {
23
+ if (browser) {
24
+ await browser.close();
25
+ }
26
+ });
27
+
28
+ beforeEach(async function () {
29
+ page = await browser.newPage();
30
+ await page.setViewport({ width: 1280, height: 800 });
31
+ await page.setContent('<!doctype html><html><head></head><body><div id="chart-host"><div id="chart-container"></div></div></body></html>');
32
+ await page.addStyleTag({ path: path.join(repositoryRoot, 'src/css/orgchart.css') });
33
+ await page.addStyleTag({ content: '#chart-container { position: relative; width: 640px; max-width: calc(100vw - 16px); height: 280px; overflow: auto; } .orgchart.r2l { left: 260px; }' });
34
+ await page.addScriptTag({ path: path.join(repositoryRoot, 'src/js/orgchart.js') });
35
+ await page.addScriptTag({ path: path.join(repositoryRoot, 'node_modules/json-digger/dist/json-digger.js') });
36
+ });
37
+
38
+ afterEach(async function () {
39
+ if (page) {
40
+ await page.close();
41
+ }
42
+ });
43
+
44
+ describe('family datasource compatibility', function () {
45
+ async function mountFamilyChart(configuration) {
46
+ await page.evaluate(function (settings) {
47
+ const nodeId = settings.nodeId || 'key';
48
+ const person = function (identifier, children) {
49
+ const data = { [nodeId]: identifier, name: identifier };
50
+ if (children) {
51
+ data.children = children;
52
+ }
53
+ return data;
54
+ };
55
+ window.compatFamilyChart = new OrgChart(document.getElementById('chart-container'), {
56
+ data: [[
57
+ person('parent-one', [
58
+ [person('child-one'), person('child-partner')],
59
+ [person('child-two', [[person('grandchild')]])]
60
+ ]),
61
+ person('parent-two', settings.bothParents ? [[person('second-child')]] : undefined)
62
+ ]],
63
+ nodeId: nodeId,
64
+ compact: !!settings.compact,
65
+ visibleLevel: settings.visibleLevel === undefined ? 3 : settings.visibleLevel,
66
+ direction: settings.direction || 't2b',
67
+ draggable: true,
68
+ pan: true,
69
+ zoom: true,
70
+ minimap: true,
71
+ verticalLevel: settings.verticalLevel
72
+ });
73
+ }, configuration || {});
74
+ }
75
+
76
+ ['id', 'key'].forEach(function (nodeId) {
77
+ [false, true].forEach(function (compact) {
78
+ it('family couple renders both partners with nodeId=' + nodeId + ' and compact=' + compact, async function () {
79
+ const result = await page.evaluate(function (settings) {
80
+ const chart = new OrgChart(document.getElementById('chart-container'), {
81
+ data: [[
82
+ { [settings.nodeId]: 'partner-one', name: 'Partner one' },
83
+ { [settings.nodeId]: 'partner-two', name: 'Partner two' }
84
+ ]],
85
+ nodeId: settings.nodeId,
86
+ compact: settings.compact,
87
+ draggable: true
88
+ });
89
+ return Array.from(chart.chart.querySelectorAll('.node')).map(function (node) {
90
+ return { id: node.id, draggable: node.draggable };
91
+ });
92
+ }, { nodeId: nodeId, compact: compact });
93
+ chai.expect(result).to.deep.equal([
94
+ { id: 'partner-one', draggable: true },
95
+ { id: 'partner-two', draggable: true }
96
+ ]);
97
+ });
98
+ });
99
+ });
100
+
101
+ it('family descendants have actual parents and complete grouped hierarchy', async function () {
102
+ await mountFamilyChart();
103
+ const result = await page.evaluate(function () {
104
+ const chart = window.compatFamilyChart;
105
+ const parent = chart.chart.querySelector('#parent-one');
106
+ const child = chart.chart.querySelector('#child-one');
107
+ const grandchild = chart.chart.querySelector('#grandchild');
108
+ return {
109
+ nodes: chart.chart.querySelectorAll('.node').length,
110
+ parent: chart.getParent(child).id,
111
+ grandparent: chart.getParent(grandchild).id,
112
+ children: chart.getChildren(parent).map(function (node) { return node.id; }),
113
+ hierarchy: chart.getHierarchy()
114
+ };
115
+ });
116
+ chai.expect(result.nodes).to.equal(6);
117
+ chai.expect(result.parent).to.equal('parent-one');
118
+ chai.expect(result.grandparent).to.equal('child-two');
119
+ chai.expect(result.children).to.deep.equal(['child-one', 'child-partner', 'child-two']);
120
+ chai.expect(result.hierarchy).to.deep.equal([[
121
+ { id: 'parent-one', children: [[{ id: 'child-one' }, { id: 'child-partner' }], [{ id: 'child-two', children: [[{ id: 'grandchild' }]] }]] },
122
+ { id: 'parent-two' }
123
+ ]]);
124
+ });
125
+
126
+ [false, true].forEach(function (compact) {
127
+ it('family depth limits and expand controls work with compact=' + compact, async function () {
128
+ await mountFamilyChart({ compact: compact, visibleLevel: 1 });
129
+ const result = await page.evaluate(async function () {
130
+ const chart = window.compatFamilyChart;
131
+ const parent = chart.chart.querySelector('#parent-one');
132
+ const child = chart.chart.querySelector('#child-one');
133
+ const grandchild = chart.chart.querySelector('#grandchild');
134
+ const initiallyHidden = child.getClientRects().length === 0 || getComputedStyle(child).opacity === '0';
135
+ parent.dispatchEvent(new MouseEvent('mouseenter'));
136
+ const button = parent.querySelector(':scope > .bottomEdge, :scope > .toggleBtn, :scope > .backToLooseSymbol');
137
+ if (button) {
138
+ button.click();
139
+ await Promise.all(child.getAnimations().map(function (animation) { return animation.finished; }));
140
+ }
141
+ return {
142
+ initiallyHidden: initiallyHidden,
143
+ hasButton: !!button,
144
+ childVisible: child.getClientRects().length > 0 && getComputedStyle(child).opacity !== '0',
145
+ allChildrenVisible: chart.getChildren(parent).every(function (node) { return node.getClientRects().length > 0 && getComputedStyle(node).opacity !== '0'; }),
146
+ grandchildHidden: grandchild.getClientRects().length === 0 || getComputedStyle(grandchild).opacity === '0'
147
+ };
148
+ });
149
+ chai.expect(result).to.deep.equal({ initiallyHidden: true, hasButton: true, childVisible: true, allChildrenVisible: true, grandchildHidden: true });
150
+ });
151
+ });
152
+
153
+ [false, true].forEach(function (compact) {
154
+ it('family parents expand only their own child container with compact=' + compact, async function () {
155
+ await mountFamilyChart({ bothParents: true, visibleLevel: 1, compact: compact });
156
+ const result = await page.evaluate(async function () {
157
+ const chart = window.compatFamilyChart;
158
+ const parent = chart.chart.querySelector('#parent-two');
159
+ const ownChild = chart.chart.querySelector('#second-child');
160
+ parent.dispatchEvent(new MouseEvent('mouseenter'));
161
+ parent.querySelector(':scope > .bottomEdge, :scope > .backToLooseSymbol').click();
162
+ await Promise.all(ownChild.getAnimations().map(function (animation) { return animation.finished; }));
163
+ return {
164
+ ownVisible: ownChild.getClientRects().length > 0 && getComputedStyle(ownChild).opacity !== '0',
165
+ otherHidden: chart.chart.querySelector('#child-one').getClientRects().length === 0
166
+ };
167
+ });
168
+ chai.expect(result).to.deep.equal({ ownVisible: true, otherHidden: true });
169
+ });
170
+ });
171
+
172
+ it('family sibling queries include all nodes with the same chart parent', async function () {
173
+ await mountFamilyChart();
174
+ const siblings = await page.evaluate(function () {
175
+ const chart = window.compatFamilyChart;
176
+ return chart.getSiblings(chart.chart.querySelector('#child-one')).map(function (node) { return node.id; }).sort();
177
+ });
178
+ chai.expect(siblings).to.deep.equal(['child-partner', 'child-two']);
179
+ });
180
+
181
+ [false, true].forEach(function (compact) {
182
+ it('family addChildren and removeNodes retain other group members with compact=' + compact, async function () {
183
+ await mountFamilyChart({ compact: compact });
184
+ const result = await page.evaluate(function () {
185
+ const chart = window.compatFamilyChart;
186
+ chart.addChildren(chart.chart.querySelector('#parent-one'), [{ key: 'added-one', name: 'Added one' }]);
187
+ chart.addChildren(chart.chart.querySelector('#parent-two'), [{ key: 'added-two', name: 'Added two' }]);
188
+ chart.removeNodes(chart.chart.querySelector('#child-one'));
189
+ return {
190
+ firstParent: chart.getParent(chart.chart.querySelector('#added-one')).id,
191
+ secondParent: chart.getParent(chart.chart.querySelector('#added-two')).id,
192
+ partnerParent: chart.getParent(chart.chart.querySelector('#child-partner')).id,
193
+ count: chart.chart.querySelectorAll('.node').length,
194
+ hierarchy: chart.getHierarchy()
195
+ };
196
+ });
197
+ chai.expect(result.firstParent).to.equal('parent-one');
198
+ chai.expect(result.secondParent).to.equal('parent-two');
199
+ chai.expect(result.partnerParent).to.equal('parent-one');
200
+ chai.expect(result.count).to.equal(7);
201
+ chai.expect(result.hierarchy).to.be.an('array');
202
+ });
203
+
204
+ it('family addSiblings and addParent reparent only the selected member with compact=' + compact, async function () {
205
+ await mountFamilyChart({ compact: compact });
206
+ const result = await page.evaluate(function () {
207
+ const chart = window.compatFamilyChart;
208
+ chart.addSiblings(chart.chart.querySelector('#child-one'), [{ key: 'added-sibling', name: 'Added sibling' }]);
209
+ chart.addParent(chart.chart.querySelector('#child-one'), { key: 'added-parent', name: 'Added parent' });
210
+ return {
211
+ childParent: chart.getParent(chart.chart.querySelector('#child-one')).id,
212
+ parentParent: chart.getParent(chart.chart.querySelector('#added-parent')).id,
213
+ partnerParent: chart.getParent(chart.chart.querySelector('#child-partner')).id,
214
+ siblingParent: chart.getParent(chart.chart.querySelector('#added-sibling')).id,
215
+ count: chart.chart.querySelectorAll('.node').length,
216
+ hierarchy: chart.getHierarchy()
217
+ };
218
+ });
219
+ chai.expect(result).to.include({ childParent: 'added-parent', parentParent: 'parent-one', partnerParent: 'parent-one', siblingParent: 'parent-one', count: 8 });
220
+ chai.expect(result.hierarchy).to.be.an('array');
221
+ });
222
+ });
223
+ });
224
+
225
+ describe('extended browser workflows across datasource types', function () {
226
+ this.timeout(120000);
227
+ ['json', 'ul', 'family', 'hybrid', 'compact-data'].forEach(function (sourceType) {
228
+ directions.forEach(function (direction) {
229
+ it(sourceType + ' / ' + direction + ' / all 128 switch masks', async function () {
230
+ await page.addStyleTag({ content: '.orgchart .node { transition: none !important; }' });
231
+ const completed = await page.evaluate(async function (settings) {
232
+ const switches = ['pan', 'zoom', 'minimap', 'draggable', 'compact', 'toggleSiblingsResp', 'exportButton'];
233
+ const assert = function (condition, message, mask) {
234
+ if (!condition) {
235
+ throw new Error(settings.sourceType + ' / ' + settings.direction + ' / mask ' + mask + ': ' + message);
236
+ }
237
+ };
238
+ const makePerson = function (id, children) {
239
+ const person = { id: id, key: 'key-' + id, name: id, label: 'Label ' + id };
240
+ if (children) {
241
+ person.children = children;
242
+ }
243
+ return person;
244
+ };
245
+ const makeList = function (data) {
246
+ const list = document.createElement('ul');
247
+ const item = document.createElement('li');
248
+ ['id', 'key', 'label'].forEach(function (key) { item.dataset[key] = data[key]; });
249
+ item.appendChild(document.createTextNode(data.name));
250
+ if (data.children) {
251
+ const children = document.createElement('ul');
252
+ data.children.forEach(function (child) { children.appendChild(makeList(child).firstElementChild); });
253
+ item.appendChild(children);
254
+ }
255
+ list.appendChild(item);
256
+ return list;
257
+ };
258
+ const basis = function (element) {
259
+ const matrix = new DOMMatrix(getComputedStyle(element).transform);
260
+ const scale = Math.hypot(matrix.m11, matrix.m12, matrix.m13);
261
+ return [matrix.m11, matrix.m12, matrix.m21, matrix.m22].map(function (value) { return Math.round(value / scale * 1000000); }).join(',');
262
+ };
263
+ const finish = function (chart) {
264
+ Array.from(chart.chart.querySelectorAll('.sliding')).forEach(function (node) {
265
+ node.dispatchEvent(new Event('transitionend', { bubbles: true }));
266
+ });
267
+ };
268
+ const expand = function (chart, node) {
269
+ if (node.classList.contains('compact')) {
270
+ chart.backToLooseHandler({ compactNode: node });
271
+ } else if (!chart.getNodeState(node, 'children').visible) {
272
+ chart.showChildren(node);
273
+ finish(chart);
274
+ }
275
+ };
276
+ let total = 0;
277
+ for (let mask = 0; mask < 128; mask += 1) {
278
+ document.body.innerHTML = '<div id="chart-host"><div id="chart-container"></div></div>';
279
+ const root = makePerson('root', [makePerson('first'), makePerson('middle', [makePerson('grandchild')]), makePerson('last')]);
280
+ let data = root;
281
+ let expectedCount = 5;
282
+ if (settings.sourceType === 'family') {
283
+ root.children[1].children = root.children[1].children.map(function (person) { return [person]; });
284
+ root.children = root.children.map(function (person) { return [person]; });
285
+ root.children[0].push(makePerson('first-partner'));
286
+ data = [[root, makePerson('root-partner')]];
287
+ expectedCount = 7;
288
+ } else if (settings.sourceType === 'ul') {
289
+ data = makeList(root);
290
+ } else if (settings.sourceType === 'hybrid') {
291
+ root.hybrid = true;
292
+ } else if (settings.sourceType === 'compact-data') {
293
+ root.compact = true;
294
+ }
295
+ const options = {
296
+ data: data, direction: settings.direction, visibleLevel: mask % 3 + 1,
297
+ nodeId: mask % 2 ? 'key' : 'id', nodeTitle: mask % 2 ? 'label' : 'name',
298
+ nodeContent: 'name', icons: { parentNode: 'oci-plus-square' },
299
+ exportFilename: 'Workflow', exportScale: 2, exportFileextension: mask % 2 ? 'pdf' : 'png',
300
+ createNode: function (node) { node.dataset.custom = 'true'; }
301
+ };
302
+ switches.forEach(function (key, index) { options[key] = !!(mask & (1 << index)); });
303
+ const container = document.getElementById('chart-container');
304
+ const chart = new OrgChart(container, options);
305
+ const nodeFor = function (id) { return chart.chart.querySelector('[id="' + (options.nodeId === 'key' ? 'key-' : '') + id + '"]'); };
306
+ const rootNode = nodeFor('root');
307
+ const originalBasis = basis(chart.chart);
308
+ assert(chart.chart.querySelectorAll('.node').length === expectedCount, 'initial nodes missing', mask);
309
+ expand(chart, rootNode);
310
+ const source = nodeFor('first');
311
+ const target = nodeFor('middle');
312
+ assert(source.getClientRects().length > 0 && getComputedStyle(source).opacity !== '0', 'source cannot expand', mask);
313
+ if (options.zoom) {
314
+ const rect = container.getBoundingClientRect();
315
+ container.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: -100, clientX: rect.left + 80, clientY: rect.top + 80 }));
316
+ assert(basis(chart.chart) === originalBasis, 'zoom changed direction', mask);
317
+ }
318
+ if (options.minimap) {
319
+ const minimap = container.querySelector('.orgchart-minimap');
320
+ const before = new DOMMatrix(getComputedStyle(chart.chart).transform);
321
+ const scale = Math.hypot(before.m11, before.m12, before.m13);
322
+ const rect = minimap.getBoundingClientRect();
323
+ minimap.dispatchEvent(new WheelEvent('wheel', { bubbles: true, cancelable: true, deltaY: -100, clientX: rect.left + 5, clientY: rect.top + 5 }));
324
+ const after = new DOMMatrix(getComputedStyle(chart.chart).transform);
325
+ assert(Math.abs(Math.hypot(after.m11, after.m12, after.m13) - scale * 1.2) < 0.000001, 'minimap wheel repeated', mask);
326
+ assert(basis(chart.chart) === originalBasis, 'minimap changed direction', mask);
327
+ }
328
+ if (options.pan) {
329
+ chart.chart.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
330
+ chart.chart.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 120, clientY: 110 }));
331
+ document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
332
+ assert(basis(chart.chart) === originalBasis, 'pan changed direction', mask);
333
+ }
334
+ if (options.draggable) {
335
+ chart.filterAllowedDropNodes(source);
336
+ assert(target.classList.contains('allowedDrop'), 'valid target excluded', mask);
337
+ await chart.dropHandler({ dropZone: target });
338
+ assert(chart.getParent(nodeFor('first')).id === nodeFor('middle').id, 'wrong drop parent', mask);
339
+ assert(nodeFor('first').getClientRects().length > 0 && getComputedStyle(nodeFor('first')).opacity !== '0', 'drop target stayed collapsed', mask);
340
+ if (settings.sourceType === 'family') {
341
+ assert(chart.getParent(nodeFor('first-partner')).id === nodeFor('root').id, 'drag moved spouse too', mask);
342
+ }
343
+ chart.setOptions('chartClass', 'workflow-chart secondary');
344
+ assert(chart.getParent(nodeFor('first')).id === nodeFor('middle').id, 'reconfiguration lost drop', mask);
345
+ }
346
+ assert(chart.chart.querySelectorAll('.node').length === expectedCount, 'nodes lost after workflow', mask);
347
+ assert(chart.chart.querySelectorAll('[data-custom=true]').length === expectedCount, 'createNode lost', mask);
348
+ if (options.exportButton) {
349
+ let exported = false;
350
+ window.html2canvas = function () { return Promise.resolve({ width: 640, height: 320 }); };
351
+ chart.exportPNG = chart.exportPDF = function (canvas, filename) { exported = filename === 'Workflow'; };
352
+ chart.exportButtonElement.click();
353
+ await Promise.resolve();
354
+ assert(exported, 'export routing failed', mask);
355
+ }
356
+ chart.unbindPan();
357
+ chart.unbindZoom();
358
+ chart.detachMinimap();
359
+ total += 1;
360
+ }
361
+ return total;
362
+ }, { sourceType: sourceType, direction: direction });
363
+ chai.expect(completed).to.equal(128);
364
+ });
365
+ });
366
+ });
367
+ });
368
+
369
+ async function mountCompactChart(configuration) {
370
+ await page.evaluate(function (settings) {
371
+ const identifierField = settings.nodeId || 'id';
372
+ const person = function (identifier, children) {
373
+ const data = { [identifierField]: identifier, name: identifier };
374
+ if (children) {
375
+ data.children = children;
376
+ }
377
+ return data;
378
+ };
379
+ const data = person('root', [
380
+ person('manager', Array.from({ length: 10 }, function (unused, index) {
381
+ return person('child-' + index, settings.nested && index === 0 ? [person('grandchild')] : undefined);
382
+ })),
383
+ person('target', [person('target-child')]),
384
+ person('other')
385
+ ]);
386
+ const options = {
387
+ data: data,
388
+ nodeId: identifierField,
389
+ draggable: settings.mode !== 'literal',
390
+ visibleLevel: settings.visibleLevel === undefined ? 3 : settings.visibleLevel,
391
+ direction: settings.direction || 't2b',
392
+ verticalLevel: settings.verticalLevel,
393
+ pan: !!settings.navigation,
394
+ zoom: !!settings.navigation,
395
+ minimap: !!settings.navigation
396
+ };
397
+ if (settings.hybrid) {
398
+ data.children[0].hybrid = true;
399
+ }
400
+ if (settings.mode === 'data') {
401
+ data.children[0].compact = true;
402
+ } else if (settings.mode === 'boolean' || settings.mode === 'literal') {
403
+ options.compact = true;
404
+ if (settings.mode === 'literal') {
405
+ delete options.draggable;
406
+ options.drag = true;
407
+ }
408
+ } else if (settings.mode === 'normal') {
409
+ options.compact = false;
410
+ } else {
411
+ options.compact = function (nodeData) { return (nodeData.children || []).length >= 5; };
412
+ }
413
+ window.compatCompactChart = new OrgChart(document.getElementById('chart-container'), options);
414
+ }, configuration || {});
415
+ }
416
+
417
+ async function expandCompactManager() {
418
+ await page.evaluate(function () {
419
+ const chart = window.compatCompactChart;
420
+ const root = chart.chart.querySelector('#root');
421
+ root.dispatchEvent(new MouseEvent('mouseenter'));
422
+ if (!chart.getNodeState(root, 'children').visible) {
423
+ root.querySelector(':scope > .bottomEdge').click();
424
+ }
425
+ });
426
+ await page.waitForFunction(function () { return !window.compatCompactChart.chart.querySelector('.sliding'); });
427
+ await page.evaluate(function () {
428
+ window.compatCompactChart.chart.querySelector('#manager').dispatchEvent(new MouseEvent('mouseenter'));
429
+ window.compatCompactChart.chart.querySelector('#manager > .backToLooseSymbol').click();
430
+ });
431
+ }
432
+
433
+ describe('compact layout compatibility', function () {
434
+ ['callback', 'data'].forEach(function (mode) {
435
+ it('compact layout relations include every real child with ' + mode, async function () {
436
+ await mountCompactChart({ mode: mode });
437
+ const result = await page.evaluate(function () {
438
+ const chart = window.compatCompactChart;
439
+ const source = chart.chart.querySelector('#child-0');
440
+ const manager = chart.chart.querySelector('#manager');
441
+ const hierarchy = chart.getHierarchy();
442
+ return {
443
+ parent: chart.getParent(source).id,
444
+ children: chart.getChildren(manager).map(function (node) { return node.id; }),
445
+ siblings: chart.getSiblings(source).length,
446
+ hierarchyChildren: (hierarchy.children[0].children || []).length
447
+ };
448
+ });
449
+ chai.expect(result.parent).to.equal('manager');
450
+ chai.expect(result.children).to.deep.equal(Array.from({ length: 10 }, function (unused, index) { return 'child-' + index; }));
451
+ chai.expect(result.siblings).to.equal(9);
452
+ chai.expect(result.hierarchyChildren).to.equal(10);
453
+ });
454
+
455
+ ['id', 'key'].forEach(function (nodeId) {
456
+ it('compact layout drag moves only the selected child with ' + mode + ' and ' + nodeId, async function () {
457
+ await mountCompactChart({ mode: mode, nodeId: nodeId });
458
+ await expandCompactManager();
459
+ const result = await page.evaluate(async function () {
460
+ const chart = window.compatCompactChart;
461
+ const source = chart.chart.querySelector('#child-0');
462
+ const manager = chart.chart.querySelector('#manager');
463
+ const target = chart.chart.querySelector('#target');
464
+ const peer = chart.chart.querySelector('#child-1');
465
+ let oldParent;
466
+ chart.chart.addEventListener('nodedrop.orgchart', function (event) { oldParent = event.detail.dragZone.id; });
467
+ source.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: new DataTransfer() }));
468
+ const peerAllowed = peer.classList.contains('allowedDrop');
469
+ await chart.dropHandler({ dropZone: target });
470
+ const movedSource = chart.chart.querySelector('#child-0');
471
+ const movedManager = chart.chart.querySelector('#manager');
472
+ const movedTarget = chart.chart.querySelector('#target');
473
+ return {
474
+ oldParent: oldParent, peerAllowed: peerAllowed,
475
+ sourceParent: chart.getParent(movedSource).id,
476
+ managerParent: chart.getParent(movedManager).id,
477
+ managerChildren: chart.getChildren(movedManager).length,
478
+ targetChildren: chart.getChildren(movedTarget).map(function (node) { return node.id; }),
479
+ sourceNestedInManager: movedManager.contains(movedSource)
480
+ };
481
+ });
482
+ chai.expect(result.oldParent).to.equal('manager');
483
+ chai.expect(result.peerAllowed).to.equal(true);
484
+ chai.expect(result.sourceParent).to.equal('target');
485
+ chai.expect(result.managerParent).to.equal('root');
486
+ chai.expect(result.managerChildren).to.equal(9);
487
+ chai.expect(result.targetChildren).to.deep.equal(['target-child', 'child-0']);
488
+ chai.expect(result.sourceNestedInManager).to.equal(false);
489
+ });
490
+ });
491
+ });
492
+
493
+ [1, 2, 3].forEach(function (visibleLevel) {
494
+ it('compact layout expands children after visibleLevel=' + visibleLevel, async function () {
495
+ await mountCompactChart({ visibleLevel: visibleLevel });
496
+ await expandCompactManager();
497
+ const visible = await page.evaluate(function () {
498
+ const source = window.compatCompactChart.chart.querySelector('#child-0');
499
+ return source.getClientRects().length > 0 && getComputedStyle(source).opacity !== '0' && !source.classList.contains('slide-up');
500
+ });
501
+ chai.expect(visible).to.equal(true);
502
+ });
503
+ });
504
+
505
+ it('compact layout supports literal visibleLevel=true, drag=true, compact=true', async function () {
506
+ await mountCompactChart({ mode: 'literal', visibleLevel: true });
507
+ const result = await page.evaluate(function () {
508
+ const chart = window.compatCompactChart;
509
+ chart.chart.querySelector('#root').dispatchEvent(new MouseEvent('mouseenter'));
510
+ chart.chart.querySelector('#root > .backToLooseSymbol').click();
511
+ chart.chart.querySelector('#manager').dispatchEvent(new MouseEvent('mouseenter'));
512
+ chart.chart.querySelector('#manager > .backToLooseSymbol').click();
513
+ const source = chart.chart.querySelector('#child-0');
514
+ return { draggable: source.draggable, visible: getComputedStyle(source).opacity !== '0' && source.getClientRects().length > 0 };
515
+ });
516
+ chai.expect(result).to.deep.equal({ draggable: true, visible: true });
517
+ });
518
+
519
+ [
520
+ { source: 'target-child', target: 'manager', expectedParent: 'manager', parent: 'target', remaining: 0 },
521
+ { source: 'child-1', target: 'child-2', expectedParent: 'child-2', parent: 'manager', remaining: 9 },
522
+ { source: 'child-0', target: 'target', expectedParent: 'target', parent: 'manager', remaining: 9, nested: true },
523
+ { source: 'manager', target: 'target', expectedParent: 'target', parent: 'root', remaining: 2 }
524
+ ].forEach(function (scenario) {
525
+ it('compact layout moves ' + scenario.source + ' into ' + scenario.target + ' without losing descendants', async function () {
526
+ await mountCompactChart({ nested: scenario.nested });
527
+ await expandCompactManager();
528
+ const result = await page.evaluate(async function (settings) {
529
+ const chart = window.compatCompactChart;
530
+ const source = chart.chart.querySelector('#' + settings.source);
531
+ const target = chart.chart.querySelector('#' + settings.target);
532
+ chart.filterAllowedDropNodes(source);
533
+ const sourceAllowed = source.classList.contains('allowedDrop');
534
+ const descendantsAllowed = Array.from(source.querySelectorAll('.node')).some(function (node) { return node.classList.contains('allowedDrop'); });
535
+ await chart.dropHandler({ dropZone: target });
536
+ const movedSource = chart.chart.querySelector('#' + settings.source);
537
+ const parent = chart.chart.querySelector('#' + settings.parent);
538
+ const grandchild = chart.chart.querySelector('#grandchild');
539
+ return {
540
+ parent: chart.getParent(movedSource).id,
541
+ remaining: chart.getChildren(parent).length,
542
+ grandchildParent: grandchild ? chart.getParent(grandchild).id : null,
543
+ total: chart.chart.querySelectorAll('.node').length,
544
+ sourceAllowed: sourceAllowed, descendantsAllowed: descendantsAllowed
545
+ };
546
+ }, scenario);
547
+ chai.expect(result.parent).to.equal(scenario.expectedParent);
548
+ chai.expect(result.remaining).to.equal(scenario.remaining);
549
+ chai.expect(result.total).to.equal(scenario.nested ? 16 : 15);
550
+ chai.expect(result.sourceAllowed).to.equal(false);
551
+ chai.expect(result.descendantsAllowed).to.equal(false);
552
+ if (scenario.nested) {
553
+ chai.expect(result.grandchildParent).to.equal('child-0');
554
+ }
555
+ });
556
+ });
557
+
558
+ it('compact layout keeps the last child visible when the source manager becomes a leaf', async function () {
559
+ await mountCompactChart();
560
+ await expandCompactManager();
561
+ const result = await page.evaluate(async function () {
562
+ const chart = window.compatCompactChart;
563
+ for (let index = 0; index < 10; index += 1) {
564
+ const source = chart.chart.querySelector('#child-' + index);
565
+ const target = chart.chart.querySelector('#target');
566
+ chart.filterAllowedDropNodes(source);
567
+ await chart.dropHandler({ dropZone: target });
568
+ }
569
+ const source = chart.chart.querySelector('#child-9');
570
+ const manager = chart.chart.querySelector('#manager');
571
+ return {
572
+ total: chart.chart.querySelectorAll('.node').length,
573
+ children: chart.getChildren(manager).length,
574
+ targetChildren: chart.getChildren(chart.chart.querySelector('#target')).length,
575
+ parent: chart.getParent(source).id,
576
+ visible: getComputedStyle(source).opacity !== '0' && source.getClientRects().length > 0
577
+ };
578
+ });
579
+ chai.expect(result).to.deep.equal({ total: 15, children: 0, targetChildren: 11, parent: 'target', visible: true });
580
+ });
581
+
582
+ it('compact layout preserves a prior normal-layout drop when enabled later', async function () {
583
+ await mountCompactChart({ mode: 'normal' });
584
+ const result = await page.evaluate(async function () {
585
+ const chart = window.compatCompactChart;
586
+ chart.filterAllowedDropNodes(chart.chart.querySelector('#child-0'));
587
+ await chart.dropHandler({ dropZone: chart.chart.querySelector('#target') });
588
+ chart.setOptions('compact', true);
589
+ return { parent: chart.getParent(chart.chart.querySelector('#child-0')).id, total: chart.chart.querySelectorAll('.node').length };
590
+ });
591
+ chai.expect(result).to.deep.equal({ parent: 'target', total: 15 });
592
+ });
593
+
594
+ it('compact layout permits addChildren and removeNodes without changing sibling branches', async function () {
595
+ await mountCompactChart();
596
+ const result = await page.evaluate(function () {
597
+ const chart = window.compatCompactChart;
598
+ chart.addChildren(chart.chart.querySelector('#manager'), [{ id: 'added', name: 'Added' }]);
599
+ const afterAdd = chart.getChildren(chart.chart.querySelector('#manager')).map(function (node) { return node.id; });
600
+ chart.removeNodes(chart.chart.querySelector('#child-0'));
601
+ const manager = chart.chart.querySelector('#manager');
602
+ return {
603
+ afterAdd: afterAdd,
604
+ managerExists: !!manager,
605
+ managerChildren: manager ? chart.getChildren(manager).length : 0,
606
+ targetExists: !!chart.chart.querySelector('#target'),
607
+ total: chart.chart.querySelectorAll('.node').length
608
+ };
609
+ });
610
+ chai.expect(result.afterAdd).to.have.length(11);
611
+ chai.expect(result.afterAdd).to.include('added');
612
+ chai.expect(result.managerExists).to.equal(true);
613
+ chai.expect(result.managerChildren).to.equal(10);
614
+ chai.expect(result.targetExists).to.equal(true);
615
+ chai.expect(result.total).to.equal(15);
616
+ });
617
+
618
+ it('compact layout can be disabled without leaving inherited compact flags', async function () {
619
+ await mountCompactChart({ mode: 'boolean', nested: true });
620
+ const result = await page.evaluate(function () {
621
+ const chart = window.compatCompactChart;
622
+ chart.setOptions('compact', false);
623
+ return {
624
+ compactNodes: chart.chart.querySelectorAll('.node.compact').length,
625
+ parent: chart.getParent(chart.chart.querySelector('#child-0')).id,
626
+ grandchildParent: chart.getParent(chart.chart.querySelector('#grandchild')).id,
627
+ total: chart.chart.querySelectorAll('.node').length
628
+ };
629
+ });
630
+ chai.expect(result).to.deep.equal({ compactNodes: 0, parent: 'manager', grandchildParent: 'child-0', total: 16 });
631
+ });
632
+
633
+ directions.forEach(function (direction) {
634
+ [2, 3].forEach(function (verticalLevel) {
635
+ it('compact layout + verticalLevel=' + verticalLevel + ' + direction=' + direction + ' supports drag and expansion', async function () {
636
+ await mountCompactChart({ mode: 'boolean', direction: direction, verticalLevel: verticalLevel, visibleLevel: 1, navigation: true });
637
+ const result = await page.evaluate(async function () {
638
+ const chart = window.compatCompactChart;
639
+ const root = chart.chart.querySelector('#root');
640
+ const manager = chart.chart.querySelector('#manager');
641
+ const child = chart.chart.querySelector('#child-0');
642
+ root.dispatchEvent(new MouseEvent('mouseenter'));
643
+ const rootButton = root.querySelector(':scope > .backToLooseSymbol, :scope > .bottomEdge, :scope > .toggleBtn');
644
+ rootButton.click();
645
+ manager.dispatchEvent(new Event('transitionend', { bubbles: true }));
646
+ manager.dispatchEvent(new MouseEvent('mouseenter'));
647
+ const managerButton = manager.querySelector(':scope > .backToLooseSymbol, :scope > .bottomEdge, :scope > .toggleBtn');
648
+ managerButton.click();
649
+ child.dispatchEvent(new Event('transitionend', { bubbles: true }));
650
+ await Promise.all(child.getAnimations().map(function (animation) { return animation.finished; }));
651
+ const expanded = child.getClientRects().length > 0 && getComputedStyle(child).opacity !== '0';
652
+ chart.filterAllowedDropNodes(child);
653
+ await chart.dropHandler({ dropZone: chart.chart.querySelector('#target') });
654
+ const movedChild = chart.chart.querySelector('#child-0');
655
+ const movedManager = chart.chart.querySelector('#manager');
656
+ return {
657
+ expanded: expanded,
658
+ parent: chart.getParent(movedChild).id,
659
+ managerParent: chart.getParent(movedManager).id,
660
+ total: chart.chart.querySelectorAll('.node').length,
661
+ children: chart.getChildren(movedManager).length,
662
+ direction: chart.options.direction,
663
+ minimap: !!document.querySelector('.orgchart-minimap')
664
+ };
665
+ });
666
+ chai.expect(result.expanded).to.equal(true);
667
+ chai.expect(result.parent).to.equal('target');
668
+ chai.expect(result.managerParent).to.equal('root');
669
+ chai.expect(result.total).to.equal(15);
670
+ chai.expect(result.children).to.equal(9);
671
+ chai.expect(result.direction).to.equal(direction);
672
+ chai.expect(result.minimap).to.equal(true);
673
+ });
674
+ });
675
+ });
676
+
677
+ it('compact layout + hybrid data uses matching child containers and toggle controls', async function () {
678
+ await mountCompactChart({ mode: 'boolean', hybrid: true, visibleLevel: 2 });
679
+ const result = await page.evaluate(async function () {
680
+ const chart = window.compatCompactChart;
681
+ chart.chart.querySelector('#root').dispatchEvent(new MouseEvent('mouseenter'));
682
+ chart.chart.querySelector('#root > .backToLooseSymbol').click();
683
+ const manager = chart.chart.querySelector('#manager');
684
+ const source = chart.chart.querySelector('#child-0');
685
+ manager.dispatchEvent(new MouseEvent('mouseenter'));
686
+ const bottomEdge = manager.querySelector(':scope > .bottomEdge');
687
+ const initiallyHidden = source.getClientRects().length === 0 || getComputedStyle(source).opacity === '0';
688
+ bottomEdge.click();
689
+ source.dispatchEvent(new Event('transitionend', { bubbles: true }));
690
+ await Promise.all(source.getAnimations().map(function (animation) { return animation.finished; }));
691
+ return {
692
+ initiallyHidden: initiallyHidden,
693
+ expanded: source.getClientRects().length > 0 && getComputedStyle(source).opacity !== '0',
694
+ verticalContainer: manager.nextElementSibling.classList.contains('vertical'),
695
+ children: chart.getChildren(manager).length
696
+ };
697
+ });
698
+ chai.expect(result).to.deep.equal({ initiallyHidden: true, expanded: true, verticalContainer: true, children: 10 });
699
+ });
700
+
701
+ it('compact layout supports adding siblings and an intermediate parent', async function () {
702
+ await mountCompactChart();
703
+ const result = await page.evaluate(function () {
704
+ const chart = window.compatCompactChart;
705
+ chart.addSiblings(chart.chart.querySelector('#child-0'), [{ id: 'added-sibling', name: 'Sibling' }]);
706
+ chart.addParent(chart.chart.querySelector('#child-0'), { id: 'added-parent', name: 'Parent' });
707
+ const child = chart.chart.querySelector('#child-0');
708
+ const addedParent = chart.chart.querySelector('#added-parent');
709
+ return {
710
+ childParent: chart.getParent(child).id,
711
+ parentParent: chart.getParent(addedParent).id,
712
+ addedSiblingParent: chart.getParent(chart.chart.querySelector('#added-sibling')).id,
713
+ managerChildren: chart.getChildren(chart.chart.querySelector('#manager')).length,
714
+ total: chart.chart.querySelectorAll('.node').length
715
+ };
716
+ });
717
+ chai.expect(result).to.deep.equal({ childParent: 'added-parent', parentParent: 'manager', addedSiblingParent: 'manager', managerChildren: 11, total: 17 });
718
+ });
719
+
720
+ it('compact layout honors dropCriteria and cancelable drop events', async function () {
721
+ await mountCompactChart();
722
+ const result = await page.evaluate(async function () {
723
+ const chart = window.compatCompactChart;
724
+ let callbackParent;
725
+ chart.options.dropCriteria = function (source, parent, target) {
726
+ callbackParent = parent.id;
727
+ return target.id === 'target';
728
+ };
729
+ const source = chart.chart.querySelector('#child-0');
730
+ const target = chart.chart.querySelector('#target');
731
+ chart.filterAllowedDropNodes(source);
732
+ const allowed = Array.from(chart.chart.querySelectorAll('.allowedDrop')).map(function (node) { return node.id; });
733
+ chart.chart.addEventListener('nodedrop.orgchart', function (event) { event.preventDefault(); }, { once: true });
734
+ await chart.dropHandler({ dropZone: target });
735
+ return { allowed: allowed, callbackParent: callbackParent, parent: chart.getParent(source).id, total: chart.chart.querySelectorAll('.node').length };
736
+ });
737
+ chai.expect(result).to.deep.equal({ allowed: ['target'], callbackParent: 'manager', parent: 'manager', total: 15 });
738
+ });
739
+
740
+ it('compact layout retains nodeTemplate, nodeContent, createNode and custom icons during rebuild', async function () {
741
+ await mountCompactChart();
742
+ const result = await page.evaluate(async function () {
743
+ const chart = window.compatCompactChart;
744
+ chart.setOptions({
745
+ nodeContent: 'name',
746
+ nodeTemplate: function (data) { return '<div class="title">' + data.name + '</div><div class="content">' + data.name + '</div>'; },
747
+ createNode: function (node) { node.setAttribute('data-custom', 'true'); },
748
+ icons: { parentNode: 'oci-plus-square' }
749
+ });
750
+ chart.filterAllowedDropNodes(chart.chart.querySelector('#child-0'));
751
+ await chart.dropHandler({ dropZone: chart.chart.querySelector('#target') });
752
+ const source = chart.chart.querySelector('#child-0');
753
+ return {
754
+ parent: chart.getParent(source).id,
755
+ customNodes: chart.chart.querySelectorAll('[data-custom=true]').length,
756
+ title: source.querySelector('.title').textContent,
757
+ content: source.querySelector('.content').textContent,
758
+ icon: chart.options.icons.parentNode,
759
+ theme: chart.options.icons.theme
760
+ };
761
+ });
762
+ chai.expect(result).to.deep.equal({ parent: 'target', customNodes: 15, title: 'child-0', content: 'child-0', icon: 'oci-plus-square', theme: 'oci' });
763
+ });
764
+
765
+ it('compact layout hides vertical descendants when the compact parent is closed', async function () {
766
+ await mountCompactChart({ mode: 'boolean', verticalLevel: 3, visibleLevel: true });
767
+ const result = await page.evaluate(function () {
768
+ const chart = window.compatCompactChart;
769
+ chart.chart.querySelector('#root').dispatchEvent(new MouseEvent('mouseenter'));
770
+ chart.chart.querySelector('#root > .backToLooseSymbol').click();
771
+ chart.chart.querySelector('#root > .backToCompactSymbol').click();
772
+ return chart.chart.querySelector('#child-0').getClientRects().length;
773
+ });
774
+ chai.expect(result).to.equal(0);
775
+ });
776
+
777
+ it('compact layout + minimap refreshes after depth-limited children finish expanding', async function () {
778
+ await mountCompactChart({ visibleLevel: 2, navigation: true });
779
+ await expandCompactManager();
780
+ await page.waitForFunction(function () {
781
+ return Array.from(window.compatCompactChart.chart.querySelectorAll('.node')).every(function (node) {
782
+ return node.getAnimations().length === 0;
783
+ });
784
+ });
785
+ const result = await page.evaluate(function () {
786
+ const chart = window.compatCompactChart;
787
+ return {
788
+ visible: Array.from(chart.chart.querySelectorAll('.node')).filter(function (node) {
789
+ return node.getClientRects().length > 0 && getComputedStyle(node).opacity !== '0';
790
+ }).length,
791
+ preview: document.querySelectorAll('.orgchart-minimap-node').length
792
+ };
793
+ });
794
+ chai.expect(result.preview).to.equal(result.visible);
795
+ });
796
+ });
797
+
798
+ it('pan + zoom preserves fractional translations during the next drag', async function () {
799
+ const result = await page.evaluate(function () {
800
+ const chart = new OrgChart(document.getElementById('chart-container'), {
801
+ data: { id: 'root', name: 'Root', children: [{ id: 'child', name: 'Child' }] },
802
+ pan: true, zoom: true
803
+ });
804
+ chart.setChartScale(chart.chart, 1.2, { x: 50.25, y: 40.5 });
805
+ const before = chart.chart.getBoundingClientRect();
806
+ chart.chart.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
807
+ chart.chart.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 130, clientY: 110 }));
808
+ document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
809
+ const after = chart.chart.getBoundingClientRect();
810
+ return { deltaX: after.left - before.left, deltaY: after.top - before.top };
811
+ });
812
+ chai.expect(result.deltaX).to.be.closeTo(30, 0.0001);
813
+ chai.expect(result.deltaY).to.be.closeTo(10, 0.0001);
814
+ });
815
+
816
+ it('zoom + draggable tolerates pinch gestures losing a finger and resets pinch state', async function () {
817
+ const result = await page.evaluate(function () {
818
+ const chart = new OrgChart(document.getElementById('chart-container'), {
819
+ data: { id: 'root', name: 'Root', children: [{ id: 'child', name: 'Child' }] },
820
+ zoom: true, pan: true, draggable: true, minimap: true
821
+ });
822
+ const touches = [{ clientX: 10, clientY: 10 }, { clientX: 110, clientY: 10 }];
823
+ chart.zoomStartHandler({ orgChart: chart, touches: touches });
824
+ chart.zoomingHandler({ orgChart: chart, touches: [touches[0]] });
825
+ chart.zoomEndHandler({ orgChart: chart });
826
+ chart.zoomStartHandler({ orgChart: chart, touches: touches });
827
+ chart.zoomingHandler({ orgChart: chart, touches: [{ clientX: 10, clientY: 10 }, { clientX: 130, clientY: 10 }] });
828
+ chart.zoomEndHandler({ orgChart: chart });
829
+ const afterPinch = chart.chart.style.transform;
830
+ chart.zoomStartHandler({ orgChart: chart, touches: touches });
831
+ chart.zoomEndHandler({ orgChart: chart });
832
+ return { afterPinch: afterPinch, afterNoMove: chart.chart.style.transform };
833
+ });
834
+ chai.expect(result.afterPinch).to.not.equal('');
835
+ chai.expect(result.afterNoMove).to.equal(result.afterPinch);
836
+ });
837
+
838
+ ['id', 'key'].forEach(function (nodeId) {
839
+ it('nodeId=' + nodeId + ' + draggable reparents hybrid data using the configured identifier', async function () {
840
+ const result = await page.evaluate(async function (identifierField) {
841
+ const chart = new OrgChart(document.getElementById('chart-container'), {
842
+ data: {
843
+ [identifierField]: 'root', name: 'Root', hybrid: true,
844
+ children: [
845
+ { [identifierField]: 'source', name: 'Source' },
846
+ { [identifierField]: 'target', name: 'Target' }
847
+ ]
848
+ },
849
+ nodeId: identifierField,
850
+ draggable: true
851
+ });
852
+ const sourceNode = chart.chart.querySelector('#source');
853
+ const targetNode = chart.chart.querySelector('#target');
854
+ chart.filterAllowedDropNodes(sourceNode);
855
+ try {
856
+ await chart.dropHandler({ dropZone: targetNode });
857
+ const movedNode = chart.chart.querySelector('#source');
858
+ return { error: null, parent: chart.getParent(movedNode).id };
859
+ } catch (error) {
860
+ return { error: error.message, parent: chart.getParent(sourceNode).id };
861
+ }
862
+ }, nodeId);
863
+
864
+ chai.expect(result.error, JSON.stringify(result)).to.equal(null);
865
+ chai.expect(result.parent).to.equal('target');
866
+ });
867
+ });
868
+
869
+ [1280, 390].forEach(function (viewportWidth) {
870
+ it('pan + zoom + minimap handles one wheel event once at viewport ' + viewportWidth, async function () {
871
+ await page.setViewport({ width: viewportWidth, height: 844 });
872
+ const result = await page.evaluate(function () {
873
+ const container = document.getElementById('chart-container');
874
+ const chart = new OrgChart(container, {
875
+ data: { id: 'root', name: 'Root', children: [{ id: 'child', name: 'Child' }] },
876
+ pan: true, zoom: true, minimap: true
877
+ });
878
+ const minimap = container.querySelector('.orgchart-minimap');
879
+ const minimapRect = minimap.getBoundingClientRect();
880
+ minimap.dispatchEvent(new WheelEvent('wheel', {
881
+ bubbles: true, cancelable: true, deltaY: -100,
882
+ clientX: minimapRect.left + 10, clientY: minimapRect.top + 10
883
+ }));
884
+ return new DOMMatrix(getComputedStyle(chart.chart).transform).m11;
885
+ });
886
+
887
+ chai.expect(result).to.be.closeTo(1.2, 0.000001);
888
+ });
889
+
890
+ [1, 2].forEach(function (visibleLevel) {
891
+ it('visibleLevel=' + visibleLevel + ' + minimap matches visible nodes at viewport ' + viewportWidth, async function () {
892
+ await page.setViewport({ width: viewportWidth, height: 844 });
893
+ const result = await page.evaluate(function (level) {
894
+ const container = document.getElementById('chart-container');
895
+ const chart = new OrgChart(container, {
896
+ data: {
897
+ id: 'root', name: 'Root', children: [
898
+ { id: 'first', name: 'First' },
899
+ { id: 'middle', name: 'Middle', children: [{ id: 'grandchild-one', name: 'One' }, { id: 'grandchild-two', name: 'Two' }] },
900
+ { id: 'last', name: 'Last' }
901
+ ]
902
+ },
903
+ visibleLevel: level, minimap: true
904
+ });
905
+ const visibleNodes = Array.from(chart.chart.querySelectorAll('.node')).filter(function (nodeElement) {
906
+ return nodeElement.getClientRects().length > 0 && getComputedStyle(nodeElement).opacity !== '0';
907
+ });
908
+ return { visible: visibleNodes.length, minimap: container.querySelectorAll('.orgchart-minimap-node').length };
909
+ }, visibleLevel);
910
+
911
+ chai.expect(result.minimap, JSON.stringify(result)).to.equal(result.visible);
912
+ });
913
+ });
914
+ });
915
+
916
+ it('toggleSiblingsResp + minimap removes collapsed siblings from the preview', async function () {
917
+ const result = await page.evaluate(async function () {
918
+ const container = document.getElementById('chart-container');
919
+ const chart = new OrgChart(container, {
920
+ data: {
921
+ id: 'root', name: 'Root', children: [
922
+ { id: 'first', name: 'First' }, { id: 'middle', name: 'Middle' }, { id: 'last', name: 'Last' }
923
+ ]
924
+ },
925
+ toggleSiblingsResp: true, minimap: true
926
+ });
927
+ chart.chart.querySelector('#middle').dispatchEvent(new MouseEvent('mouseenter'));
928
+ chart.chart.querySelector('#middle .leftEdge').dispatchEvent(new MouseEvent('click', { bubbles: true }));
929
+ chart.chart.querySelector('#first').dispatchEvent(new Event('transitionend', { bubbles: true }));
930
+ await new Promise(requestAnimationFrame);
931
+ return {
932
+ visible: Array.from(chart.chart.querySelectorAll('.node')).filter(function (nodeElement) { return nodeElement.getClientRects().length > 0; }).length,
933
+ minimap: container.querySelectorAll('.orgchart-minimap-node').length
934
+ };
935
+ });
936
+
937
+ chai.expect(result.minimap, JSON.stringify(result)).to.equal(result.visible);
938
+ });
939
+
940
+ it('pan + minimap keeps the viewport synchronized on the first movement', async function () {
941
+ const result = await page.evaluate(function () {
942
+ const container = document.getElementById('chart-container');
943
+ const chart = new OrgChart(container, {
944
+ data: {
945
+ id: 'root', name: 'Root',
946
+ children: Array.from({ length: 10 }, function (unused, index) { return { id: 'child-' + index, name: 'Child ' + index }; })
947
+ },
948
+ pan: true, minimap: true
949
+ });
950
+ const viewport = container.querySelector('.orgchart-minimap-viewport');
951
+ chart.chart.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
952
+ chart.chart.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 70, clientY: 80 }));
953
+ const after = viewport.style.transform;
954
+ chart.updateMinimap();
955
+ return { after: after, expected: viewport.style.transform, chartTransform: chart.chart.style.transform };
956
+ });
957
+
958
+ chai.expect(result.after, JSON.stringify(result)).to.equal(result.expected);
959
+ });
960
+
961
+ it('pan=false + minimap synchronizes the viewport after native scrolling', async function () {
962
+ const result = await page.evaluate(async function () {
963
+ const container = document.getElementById('chart-container');
964
+ const chart = new OrgChart(container, {
965
+ data: {
966
+ id: 'root', name: 'Root',
967
+ children: Array.from({ length: 10 }, function (unused, index) { return { id: 'child-' + index, name: 'Child ' + index }; })
968
+ },
969
+ pan: false, minimap: true
970
+ });
971
+ container.scrollLeft = 100;
972
+ container.dispatchEvent(new Event('scroll'));
973
+ await new Promise(requestAnimationFrame);
974
+ const viewport = container.querySelector('.orgchart-minimap-viewport');
975
+ const after = viewport.style.transform;
976
+ chart.updateMinimap();
977
+ return { scrollLeft: container.scrollLeft, after: after, expected: viewport.style.transform };
978
+ });
979
+
980
+ chai.expect(result.scrollLeft).to.equal(100);
981
+ chai.expect(result.after, JSON.stringify(result)).to.equal(result.expected);
982
+ });
983
+
984
+ describe('actual PNG and PDF export pixels', function () {
985
+ const configurations = ['json', 'ul', 'family'].flatMap(function (sourceType) {
986
+ return directions.map(function (direction) { return { sourceType: sourceType, direction: direction }; });
987
+ });
988
+ configurations.forEach(function (settings) {
989
+ const direction = settings.direction;
990
+ [1, 2, 3].forEach(function (exportScale) {
991
+ ['png', 'pdf'].forEach(function (extension) {
992
+ it(settings.sourceType + ' / direction=' + direction + ' + exportScale=' + exportScale + ' + exportFileextension=' + extension + (exportScale === 3 ? ' + all options' : '') + ' exports every node', async function () {
993
+ await page.addScriptTag({ path: path.join(repositoryRoot, 'node_modules/html2canvas/dist/html2canvas.min.js') });
994
+ if (extension === 'pdf') {
995
+ await page.addScriptTag({ path: path.join(repositoryRoot, 'node_modules/jspdf/dist/jspdf.umd.min.js') });
996
+ }
997
+ const result = await page.evaluate(async function (configuration) {
998
+ const container = document.getElementById('chart-container');
999
+ const root = {
1000
+ id: 'root', key: 'key-root', name: 'Root', label: 'Root label', color: configuration.colors[0],
1001
+ children: configuration.colors.slice(1).map(function (color, index) {
1002
+ return { id: 'child-' + index, key: 'key-child-' + index, name: 'Child ' + index, label: 'Label ' + index, color: color };
1003
+ })
1004
+ };
1005
+ let datasource = root;
1006
+ if (configuration.sourceType === 'family') {
1007
+ const partner = root.children.shift();
1008
+ const grandchild = root.children.pop();
1009
+ root.children[2].children = [[grandchild]];
1010
+ root.children = [[root.children[0], root.children[1]], [root.children[2]]];
1011
+ datasource = [[root, partner]];
1012
+ } else if (configuration.sourceType === 'ul') {
1013
+ const buildList = function (data) {
1014
+ const list = document.createElement('ul');
1015
+ const item = document.createElement('li');
1016
+ Object.keys(data).filter(function (key) { return key !== 'children'; }).forEach(function (key) {
1017
+ item.dataset[key] = Array.isArray(data[key]) ? JSON.stringify(data[key]) : data[key];
1018
+ });
1019
+ item.appendChild(document.createTextNode(data.name));
1020
+ if (data.children) {
1021
+ const children = document.createElement('ul');
1022
+ data.children.forEach(function (child) { children.appendChild(buildList(child).firstElementChild); });
1023
+ item.appendChild(children);
1024
+ }
1025
+ list.appendChild(item);
1026
+ return list;
1027
+ };
1028
+ datasource = buildList(root);
1029
+ }
1030
+ const chart = new OrgChart(container, {
1031
+ data: datasource,
1032
+ compact: configuration.allOptions && configuration.sourceType === 'family',
1033
+ icons: Object.assign({}, new OrgChart().defaultOptions.icons, configuration.allOptions ? { parentNode: 'oci-plus-square', expandToUp: 'oci-chevron-down', collapseToDown: 'oci-chevron-up' } : {}),
1034
+ nodeTitle: configuration.allOptions ? 'label' : 'name',
1035
+ nodeId: configuration.allOptions ? 'key' : 'id',
1036
+ toggleSiblingsResp: configuration.allOptions,
1037
+ visibleLevel: configuration.allOptions ? 2 : 999,
1038
+ chartClass: configuration.allOptions ? 'compat-chart secondary-chart' : '',
1039
+ exportButton: configuration.allOptions,
1040
+ exportButtonName: 'Save Chart',
1041
+ draggable: configuration.allOptions,
1042
+ pan: configuration.allOptions,
1043
+ zoom: configuration.allOptions,
1044
+ minimap: configuration.allOptions,
1045
+ zoominLimit: configuration.allOptions ? 2.5 : 7,
1046
+ zoomoutLimit: configuration.allOptions ? 0.6 : 0.5,
1047
+ direction: configuration.direction,
1048
+ exportScale: configuration.exportScale,
1049
+ exportFileextension: configuration.extension,
1050
+ exportFilename: 'compatibility-export',
1051
+ createNode: function (nodeElement, data) {
1052
+ const color = typeof data.color === 'string' ? JSON.parse(data.color) : data.color;
1053
+ nodeElement.querySelector('.title').style.backgroundColor = 'rgb(' + color.join(',') + ')';
1054
+ }
1055
+ });
1056
+ if (configuration.sourceType === 'family') {
1057
+ Array.from(chart.chart.querySelectorAll('.node')).forEach(function (node) {
1058
+ if (node.classList.contains('compact')) {
1059
+ chart.backToLooseHandler({ compactNode: node });
1060
+ }
1061
+ });
1062
+ await Promise.all(Array.from(chart.chart.querySelectorAll('.node')).flatMap(function (node) {
1063
+ return node.getAnimations().map(function (animation) { return animation.finished; });
1064
+ }));
1065
+ }
1066
+ const isHorizontal = configuration.direction === 'l2r' || configuration.direction === 'r2l';
1067
+ const expectedWidth = (isHorizontal ? chart.chart.clientHeight : chart.chart.clientWidth) * configuration.exportScale;
1068
+ const expectedHeight = (isHorizontal ? chart.chart.clientWidth : chart.chart.clientHeight) * configuration.exportScale;
1069
+ if (configuration.allOptions) {
1070
+ chart.chart.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
1071
+ chart.chart.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 70, clientY: 80 }));
1072
+ document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
1073
+ const containerRect = container.getBoundingClientRect();
1074
+ container.dispatchEvent(new WheelEvent('wheel', {
1075
+ bubbles: true, cancelable: true, deltaY: -100,
1076
+ clientX: containerRect.left + 100, clientY: containerRect.top + 100
1077
+ }));
1078
+ }
1079
+ let pdf = null;
1080
+ if (configuration.extension === 'pdf') {
1081
+ window.jsPDF = function (options) {
1082
+ const pdfDocument = new window.jspdf.jsPDF(options);
1083
+ pdfDocument.save = function (filename) {
1084
+ const bytes = new Uint8Array(pdfDocument.output('arraybuffer'));
1085
+ pdf = {
1086
+ filename: filename, length: bytes.length,
1087
+ header: new TextDecoder().decode(bytes.slice(0, 5)),
1088
+ width: pdfDocument.internal.pageSize.getWidth(), height: pdfDocument.internal.pageSize.getHeight()
1089
+ };
1090
+ };
1091
+ return pdfDocument;
1092
+ };
1093
+ }
1094
+ const capture = await new Promise(function (resolve, reject) {
1095
+ const originalHtml2canvas = window.html2canvas;
1096
+ window.html2canvas = function (element, options) {
1097
+ return originalHtml2canvas(element, options).catch(function (error) { reject(error); throw error; });
1098
+ };
1099
+ const originalExportPDF = chart.exportPDF;
1100
+ chart.exportPNG = function (canvas, filename) { resolve({ canvas: canvas, filename: filename + '.png' }); };
1101
+ chart.exportPDF = function (canvas, filename, scale) {
1102
+ try {
1103
+ originalExportPDF.call(chart, canvas, filename, scale);
1104
+ resolve({ canvas: canvas, filename: pdf.filename });
1105
+ } catch (error) {
1106
+ reject(error);
1107
+ }
1108
+ };
1109
+ if (configuration.allOptions) {
1110
+ container.nextElementSibling.click();
1111
+ } else {
1112
+ chart.export();
1113
+ }
1114
+ });
1115
+ const pixelCounts = configuration.colors.map(function () { return 0; });
1116
+ const colorIndexes = new Map(configuration.colors.map(function (color, index) {
1117
+ return [color[0] * 65536 + color[1] * 256 + color[2], index];
1118
+ }));
1119
+ const pixels = capture.canvas.getContext('2d').getImageData(0, 0, capture.canvas.width, capture.canvas.height).data;
1120
+ for (let offset = 0; offset < pixels.length; offset += 4) {
1121
+ const index = colorIndexes.get(pixels[offset] * 65536 + pixels[offset + 1] * 256 + pixels[offset + 2]);
1122
+ if (index !== undefined) {
1123
+ pixelCounts[index] += 1;
1124
+ }
1125
+ }
1126
+ return {
1127
+ width: capture.canvas.width, height: capture.canvas.height,
1128
+ expectedWidth: expectedWidth, expectedHeight: expectedHeight,
1129
+ filename: capture.filename, pixelCounts: pixelCounts, pdf: pdf,
1130
+ maskHidden: container.querySelector('.mask').classList.contains('hidden'),
1131
+ exporting: container.classList.contains('canvasContainer')
1132
+ };
1133
+ }, { sourceType: settings.sourceType, direction: direction, exportScale: exportScale, extension: extension, colors: markerColors, allOptions: exportScale === 3 });
1134
+
1135
+ chai.expect(result.width).to.be.closeTo(result.expectedWidth, 1);
1136
+ chai.expect(result.height).to.be.closeTo(result.expectedHeight, 1);
1137
+ chai.expect(result.filename).to.equal('compatibility-export.' + extension);
1138
+ chai.expect(result.maskHidden).to.equal(true);
1139
+ chai.expect(result.exporting).to.equal(false);
1140
+ result.pixelCounts.forEach(function (pixelCount, index) {
1141
+ chai.expect(pixelCount, 'node ' + index + ': ' + JSON.stringify(result)).to.be.greaterThan(50 * exportScale * exportScale);
1142
+ });
1143
+ if (extension === 'pdf') {
1144
+ chai.expect(result.pdf.header).to.equal('%PDF-');
1145
+ chai.expect(result.pdf.length).to.be.greaterThan(500);
1146
+ chai.expect(result.pdf.width).to.be.closeTo(result.width / exportScale, 1);
1147
+ chai.expect(result.pdf.height).to.be.closeTo(result.height / exportScale, 1);
1148
+ }
1149
+ });
1150
+ });
1151
+ });
1152
+ });
1153
+ });
1154
+
1155
+ directions.forEach(function (direction) {
1156
+ it('direction=' + direction + ' + pan preserves orientation and follows the pointer', async function () {
1157
+ const result = await page.evaluate(function (selectedDirection) {
1158
+ const chart = new OrgChart(document.getElementById('chart-container'), {
1159
+ data: { id: 'root', name: 'Root', children: [{ id: 'child', name: 'Child' }] },
1160
+ direction: selectedDirection, pan: true
1161
+ });
1162
+ const beforeMatrix = new DOMMatrix(getComputedStyle(chart.chart).transform);
1163
+ const beforeRect = chart.chart.getBoundingClientRect();
1164
+ chart.chart.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
1165
+ chart.chart.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 130, clientY: 110 }));
1166
+ document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
1167
+ const afterMatrix = new DOMMatrix(getComputedStyle(chart.chart).transform);
1168
+ const afterRect = chart.chart.getBoundingClientRect();
1169
+ return {
1170
+ before: [beforeMatrix.m11, beforeMatrix.m12, beforeMatrix.m21, beforeMatrix.m22],
1171
+ after: [afterMatrix.m11, afterMatrix.m12, afterMatrix.m21, afterMatrix.m22],
1172
+ deltaX: afterRect.left - beforeRect.left, deltaY: afterRect.top - beforeRect.top
1173
+ };
1174
+ }, direction);
1175
+
1176
+ chai.expect(result.after).to.deep.equal(result.before);
1177
+ chai.expect(result.deltaX).to.be.closeTo(30, 0.001);
1178
+ chai.expect(result.deltaY).to.be.closeTo(10, 0.001);
1179
+ });
1180
+
1181
+ [false, true].forEach(function (zoom) {
1182
+ it('direction=' + direction + ' + draggable + zoom=' + zoom + ' creates a visible ghost and reparents the node', async function () {
1183
+ const result = await page.evaluate(async function (configuration) {
1184
+ const container = document.getElementById('chart-container');
1185
+ const chart = new OrgChart(container, {
1186
+ data: { id: 'root', name: 'Root', children: [{ id: 'source', name: 'Source' }, { id: 'target', name: 'Target' }] },
1187
+ direction: configuration.direction, draggable: true, zoom: configuration.zoom
1188
+ });
1189
+ if (configuration.zoom) {
1190
+ const containerRect = container.getBoundingClientRect();
1191
+ container.dispatchEvent(new WheelEvent('wheel', {
1192
+ bubbles: true, cancelable: true, deltaY: -100,
1193
+ clientX: containerRect.left + 100, clientY: containerRect.top + 100
1194
+ }));
1195
+ }
1196
+ const sourceNode = chart.chart.querySelector('#source');
1197
+ sourceNode.dispatchEvent(new DragEvent('dragstart', { bubbles: true, cancelable: true, dataTransfer: new DataTransfer() }));
1198
+ const ghost = chart.chart.querySelector('.ghost-node rect');
1199
+ const ghostWidth = ghost ? Number(ghost.getAttribute('width')) : null;
1200
+ const ghostHeight = ghost ? Number(ghost.getAttribute('height')) : null;
1201
+ await chart.dropHandler({ dropZone: chart.chart.querySelector('#target') });
1202
+ chart.dragendHandler();
1203
+ return { ghostWidth: ghostWidth, ghostHeight: ghostHeight, parent: chart.getParent(sourceNode).id };
1204
+ }, { direction: direction, zoom: zoom });
1205
+
1206
+ chai.expect(result.parent).to.equal('target');
1207
+ if (direction !== 't2b' || zoom) {
1208
+ chai.expect(result.ghostWidth, JSON.stringify(result)).to.be.greaterThan(0);
1209
+ chai.expect(result.ghostHeight, JSON.stringify(result)).to.be.greaterThan(0);
1210
+ }
1211
+ });
1212
+ });
1213
+
1214
+ it('direction=' + direction + ' + zoom preserves rotation and reflection', async function () {
1215
+ const result = await page.evaluate(function (selectedDirection) {
1216
+ const container = document.getElementById('chart-container');
1217
+ const chart = new OrgChart(container, {
1218
+ data: {
1219
+ id: 'root', name: 'Root',
1220
+ children: [{ id: 'first', name: 'First' }, { id: 'last', name: 'Last' }]
1221
+ },
1222
+ direction: selectedDirection,
1223
+ zoom: true
1224
+ });
1225
+ const orientation = function () {
1226
+ const matrix = new DOMMatrix(getComputedStyle(chart.chart).transform);
1227
+ const scale = Math.hypot(matrix.m11, matrix.m12);
1228
+ return [matrix.m11, matrix.m12, matrix.m21, matrix.m22].map(function (value) {
1229
+ return Math.round(value / scale * 1000000) / 1000000;
1230
+ });
1231
+ };
1232
+ const before = orientation();
1233
+ const containerRect = container.getBoundingClientRect();
1234
+ container.dispatchEvent(new WheelEvent('wheel', {
1235
+ bubbles: true, cancelable: true, deltaY: -100,
1236
+ clientX: containerRect.left + 100, clientY: containerRect.top + 100
1237
+ }));
1238
+ return { before: before, after: orientation(), transform: chart.chart.style.transform };
1239
+ }, direction);
1240
+
1241
+ chai.expect(result.after, JSON.stringify(result)).to.deep.equal(result.before);
1242
+ });
1243
+
1244
+ it('direction=' + direction + ' + minimap preserves rotation during viewport drag', async function () {
1245
+ const result = await page.evaluate(function (selectedDirection) {
1246
+ const container = document.getElementById('chart-container');
1247
+ const chart = new OrgChart(container, {
1248
+ data: {
1249
+ id: 'root', name: 'Root',
1250
+ children: [{ id: 'first', name: 'First' }, { id: 'last', name: 'Last' }]
1251
+ },
1252
+ direction: selectedDirection,
1253
+ minimap: true
1254
+ });
1255
+ const orientation = function () {
1256
+ const matrix = new DOMMatrix(getComputedStyle(chart.chart).transform);
1257
+ const scale = Math.hypot(matrix.m11, matrix.m12);
1258
+ return [matrix.m11, matrix.m12, matrix.m21, matrix.m22].map(function (value) {
1259
+ return Math.round(value / scale * 1000000) / 1000000;
1260
+ });
1261
+ };
1262
+ const before = orientation();
1263
+ const viewport = container.querySelector('.orgchart-minimap-viewport');
1264
+ const viewportRect = viewport.getBoundingClientRect();
1265
+ viewport.dispatchEvent(new MouseEvent('mousedown', {
1266
+ bubbles: true, cancelable: true,
1267
+ clientX: viewportRect.left + 5, clientY: viewportRect.top + 5
1268
+ }));
1269
+ document.dispatchEvent(new MouseEvent('mousemove', {
1270
+ bubbles: true, clientX: viewportRect.left + 25, clientY: viewportRect.top + 15
1271
+ }));
1272
+ document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
1273
+ return { before: before, after: orientation(), transform: chart.chart.style.transform };
1274
+ }, direction);
1275
+
1276
+ chai.expect(result.after, JSON.stringify(result)).to.deep.equal(result.before);
1277
+ });
1278
+ });
1279
+ });