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,628 @@
1
+ const chai = require('chai');
2
+ const sinon = require('sinon');
3
+
4
+ require('jsdom-global')();
5
+
6
+ const OrgChart = require('../../src/js/orgchart');
7
+ const getState = OrgChart.__testing__.getState;
8
+ const defaultOptions = new OrgChart().defaultOptions;
9
+ const customIcons = Object.fromEntries(Object.entries(defaultOptions.icons).map(function (entry) {
10
+ return [entry[0], 'compat-' + entry[1]];
11
+ }));
12
+ const optionValues = {
13
+ icons: [defaultOptions.icons, customIcons],
14
+ nodeTitle: ['name', 'label'],
15
+ nodeId: ['id', 'key'],
16
+ toggleSiblingsResp: [false, true],
17
+ visibleLevel: [1, 2, 999],
18
+ chartClass: ['', 'compat-chart', 'compat-chart secondary-chart'],
19
+ exportButton: [false, true],
20
+ exportButtonName: ['Export', 'Save Chart'],
21
+ exportFilename: ['OrgChart', 'compat-export'],
22
+ exportFileextension: ['png', 'pdf', 'PDF'],
23
+ exportScale: [1, 2, 3],
24
+ draggable: [false, true],
25
+ direction: ['t2b', 'b2t', 'l2r', 'r2l'],
26
+ pan: [false, true],
27
+ minimap: [false, true],
28
+ zoom: [false, true],
29
+ zoominLimit: [2.5, 7],
30
+ zoomoutLimit: [0.5, 0.6]
31
+ };
32
+
33
+ function createData() {
34
+ const createPerson = function (identifier, children) {
35
+ const person = {
36
+ id: identifier,
37
+ key: 'key-' + identifier,
38
+ name: 'Name ' + identifier,
39
+ label: 'Label ' + identifier
40
+ };
41
+
42
+ if (children) {
43
+ person.children = children;
44
+ }
45
+ return person;
46
+ };
47
+
48
+ return createPerson('root', [
49
+ createPerson('first'),
50
+ createPerson('middle', [createPerson('grandchild-one'), createPerson('grandchild-two')]),
51
+ createPerson('last')
52
+ ]);
53
+ }
54
+
55
+ function createSourceFixture(sourceType, large) {
56
+ const tree = createData();
57
+ if (large) {
58
+ tree.children.push({ id: 'extra', key: 'key-extra', name: 'Name extra', label: 'Label extra' });
59
+ }
60
+ let model = tree;
61
+ if (sourceType === 'family') {
62
+ const convertChildren = function (person) {
63
+ if (person.children) {
64
+ person.children.forEach(convertChildren);
65
+ person.children = person.children.map(function (child) { return [child]; });
66
+ }
67
+ };
68
+ convertChildren(tree);
69
+ tree.children[0].push({ id: 'first-partner', key: 'key-first-partner', name: 'First partner', label: 'Partner label' });
70
+ model = [[tree, { id: 'root-partner', key: 'key-root-partner', name: 'Root partner', label: 'Root partner label' }]];
71
+ }
72
+ const records = [];
73
+ const collect = function (data, parent) {
74
+ if (Array.isArray(data)) {
75
+ data.forEach(function (item) { collect(item, parent); });
76
+ return;
77
+ }
78
+ records.push({ data: data, parent: parent });
79
+ (data.children || []).forEach(function (child) { collect(child, data); });
80
+ };
81
+ collect(model, null);
82
+ let datasource = model;
83
+ if (sourceType === 'ul') {
84
+ const buildList = function (person) {
85
+ const list = document.createElement('ul');
86
+ const item = document.createElement('li');
87
+ Object.keys(person).filter(function (key) { return key !== 'children'; }).forEach(function (key) {
88
+ item.dataset[key] = person[key];
89
+ });
90
+ item.appendChild(document.createTextNode(person.name));
91
+ if (person.children) {
92
+ const childList = document.createElement('ul');
93
+ person.children.forEach(function (child) { childList.appendChild(buildList(child).firstElementChild); });
94
+ item.appendChild(childList);
95
+ }
96
+ list.appendChild(item);
97
+ return list;
98
+ };
99
+ datasource = buildList(tree);
100
+ }
101
+ return { data: datasource, records: records };
102
+ }
103
+
104
+ function assertSourceHierarchy(chart, fixture) {
105
+ const nodeId = chart.options.nodeId;
106
+ const expectedIds = fixture.records.map(function (record) { return record.data[nodeId]; }).sort();
107
+ const nodes = Array.from(chart.chart.querySelectorAll('.node'));
108
+ chai.expect(nodes.map(function (node) { return node.id; }).sort()).to.deep.equal(expectedIds);
109
+ fixture.records.forEach(function (record) {
110
+ const node = nodes.find(function (element) { return element.id === record.data[nodeId]; });
111
+ const parent = chart.getParent(node);
112
+ chai.expect(parent && parent.id, 'parent of ' + node.id).to.equal(record.parent ? record.parent[nodeId] : null);
113
+ const expectedChildren = fixture.records.filter(function (child) { return child.parent === record.data; })
114
+ .map(function (child) { return child.data[nodeId]; }).sort();
115
+ chai.expect(chart.getChildren(node).map(function (child) { return child.id; }).sort(), 'children of ' + node.id).to.deep.equal(expectedChildren);
116
+ chai.expect(node.draggable).to.equal(chart.options.draggable);
117
+ });
118
+ const exportedIds = [];
119
+ const visitHierarchy = function (hierarchy) {
120
+ if (Array.isArray(hierarchy)) {
121
+ hierarchy.forEach(visitHierarchy);
122
+ return;
123
+ }
124
+ exportedIds.push(hierarchy.id);
125
+ (hierarchy.children || []).forEach(visitHierarchy);
126
+ };
127
+ visitHierarchy(chart.getHierarchy());
128
+ chai.expect(exportedIds.sort()).to.deep.equal(expectedIds);
129
+ }
130
+
131
+ function dispatchMouse(element, type, clientX, clientY) {
132
+ element.dispatchEvent(new window.MouseEvent(type, {
133
+ bubbles: true,
134
+ cancelable: true,
135
+ clientX: clientX || 0,
136
+ clientY: clientY || 0
137
+ }));
138
+ }
139
+
140
+ function dispatchWheel(element, deltaY) {
141
+ element.dispatchEvent(new window.WheelEvent('wheel', {
142
+ bubbles: true,
143
+ cancelable: true,
144
+ deltaY: deltaY === undefined ? -100 : deltaY,
145
+ clientX: 0,
146
+ clientY: 0
147
+ }));
148
+ }
149
+
150
+ function finishTransition(element) {
151
+ element.dispatchEvent(new window.Event('transitionend', { bubbles: true }));
152
+ }
153
+
154
+ function describeValue(value) {
155
+ return typeof value === 'object' ? value.theme : JSON.stringify(value);
156
+ }
157
+
158
+ describe('orgchart option compatibility', function () {
159
+ let container;
160
+ let chart;
161
+ let createdCharts;
162
+ let captureSpy;
163
+ let originalHtml2canvas;
164
+ let originalJSONDigger;
165
+
166
+ function mount(options, targetContainer) {
167
+ chart = new OrgChart(targetContainer || container, Object.assign({ data: createData() }, options));
168
+ createdCharts.push({ instance: chart, element: chart.chart });
169
+ return chart;
170
+ }
171
+
172
+ beforeEach(function () {
173
+ document.body.innerHTML = '<div id="chart-host"><div id="chart-container"></div></div>';
174
+ container = document.getElementById('chart-container');
175
+ createdCharts = [];
176
+ originalHtml2canvas = global.html2canvas;
177
+ originalJSONDigger = global.JSONDigger;
178
+ global.JSONDigger = require('json-digger');
179
+ captureSpy = sinon.spy(function (element, options) {
180
+ return Promise.resolve({
181
+ width: 640 * options.scale,
182
+ height: 320 * options.scale,
183
+ toDataURL: function () { return 'data:image/png;base64,fake'; }
184
+ });
185
+ });
186
+ global.html2canvas = captureSpy;
187
+ });
188
+
189
+ afterEach(function () {
190
+ createdCharts.forEach(function (createdChart) {
191
+ createdChart.instance.unbindPan();
192
+ createdChart.instance.unbindZoom();
193
+ createdChart.instance.detachMinimap();
194
+ if (createdChart.element !== createdChart.instance.chart) {
195
+ container.removeEventListener('wheel', getState(createdChart.element, 'zoomWheelBoundHandler'));
196
+ container.removeEventListener('touchstart', getState(createdChart.element, 'zoomStartBoundHandler'));
197
+ document.removeEventListener('touchmove', getState(createdChart.element, 'zoomingBoundHandler'));
198
+ document.removeEventListener('touchend', getState(createdChart.element, 'zoomEndBoundHandler'));
199
+ document.removeEventListener('mouseup', getState(createdChart.element, 'panEndBoundHandler'));
200
+ document.removeEventListener('touchend', getState(createdChart.element, 'panEndBoundHandler'));
201
+ }
202
+ });
203
+ global.html2canvas = originalHtml2canvas;
204
+ global.JSONDigger = originalJSONDigger;
205
+ sinon.restore();
206
+ document.body.innerHTML = '';
207
+ chart = null;
208
+ });
209
+
210
+ describe('pairwise configuration and supported workflows', function () {
211
+ const optionNames = Object.keys(defaultOptions);
212
+
213
+ it('covers every defaultOptions key and all 153 unordered pairs', function () {
214
+ chai.expect(Object.keys(optionValues)).to.have.members(optionNames);
215
+ chai.expect(optionNames.length * (optionNames.length - 1) / 2).to.equal(153);
216
+ });
217
+
218
+ optionNames.forEach(function (firstOption, firstIndex) {
219
+ optionNames.slice(firstIndex + 1).forEach(function (secondOption) {
220
+ describe(firstOption + ' + ' + secondOption, function () {
221
+ optionValues[firstOption].forEach(function (firstValue) {
222
+ optionValues[secondOption].forEach(function (secondValue) {
223
+ it(describeValue(firstValue) + ' / ' + describeValue(secondValue), async function () {
224
+ const selectedOptions = { [firstOption]: firstValue, [secondOption]: secondValue };
225
+ const expectedOptions = Object.assign({}, defaultOptions, selectedOptions);
226
+ const currentChart = mount(selectedOptions);
227
+ const nodeFor = function (identifier) {
228
+ return currentChart.chart.querySelector('[id="' + (expectedOptions.nodeId === 'key' ? 'key-' : '') + identifier + '"]');
229
+ };
230
+ const rootNode = nodeFor('root');
231
+ const firstNode = nodeFor('first');
232
+ const middleNode = nodeFor('middle');
233
+ const lastNode = nodeFor('last');
234
+ const nodeElements = Array.from(currentChart.chart.querySelectorAll('.node'));
235
+ const scaleSpy = sinon.spy(currentChart, 'setChartScale');
236
+
237
+ Object.keys(defaultOptions).forEach(function (optionName) {
238
+ chai.expect(currentChart.options[optionName], optionName).to.deep.equal(expectedOptions[optionName]);
239
+ });
240
+ chai.expect(nodeElements.length).to.equal(6);
241
+ chai.expect(rootNode.querySelector('.title').textContent).to.equal(expectedOptions.nodeTitle === 'label' ? 'Label root' : 'Name root');
242
+ expectedOptions.chartClass.split(' ').filter(Boolean).forEach(function (className) {
243
+ chai.expect(currentChart.chart.classList.contains(className)).to.equal(true);
244
+ });
245
+ chai.expect(currentChart.chart.classList.contains(expectedOptions.direction)).to.equal(expectedOptions.direction !== 't2b');
246
+ nodeElements.forEach(function (nodeElement) {
247
+ chai.expect(nodeElement.getAttribute('draggable')).to.equal(expectedOptions.draggable ? 'true' : null);
248
+ Array.from(nodeElement.querySelectorAll('.edge, .parentNodeSymbol')).forEach(function (iconElement) {
249
+ chai.expect(iconElement.classList.contains(expectedOptions.icons.theme)).to.equal(true);
250
+ });
251
+ });
252
+ chai.expect(currentChart.getNodeState(rootNode, 'children').visible).to.equal(expectedOptions.visibleLevel > 1);
253
+ chai.expect(!!container.querySelector('.orgchart-minimap')).to.equal(expectedOptions.minimap);
254
+ const exportButton = document.querySelector('.oc-export-btn');
255
+ chai.expect(!!exportButton).to.equal(expectedOptions.exportButton);
256
+ if (exportButton) {
257
+ chai.expect(exportButton.textContent).to.equal(expectedOptions.exportButtonName);
258
+ }
259
+
260
+ dispatchMouse(currentChart.chart, 'mousedown', 20, 20);
261
+ dispatchMouse(currentChart.chart, 'mousemove', 50, 30);
262
+ chai.expect(!!getState(currentChart.chart, 'panning')).to.equal(expectedOptions.pan);
263
+ dispatchMouse(document, 'mouseup', 50, 30);
264
+ chai.expect(!!getState(currentChart.chart, 'panning')).to.equal(false);
265
+ dispatchWheel(container);
266
+ chai.expect(scaleSpy.callCount).to.equal(expectedOptions.zoom ? 1 : 0);
267
+
268
+ if (!currentChart.getNodeState(rootNode, 'children').visible) {
269
+ dispatchMouse(rootNode, 'mouseenter');
270
+ dispatchMouse(rootNode.querySelector('.bottomEdge'), 'click');
271
+ finishTransition(firstNode);
272
+ }
273
+ dispatchMouse(middleNode, 'mouseenter');
274
+ chai.expect(middleNode.querySelector('.topEdge').classList.contains(expectedOptions.icons.collapseToDown)).to.equal(true);
275
+ dispatchMouse(middleNode.querySelector('.leftEdge'), 'click');
276
+ finishTransition(firstNode);
277
+ chai.expect(firstNode.closest('.hierarchy').classList.contains('hidden')).to.equal(true);
278
+ chai.expect(lastNode.closest('.hierarchy').classList.contains('hidden')).to.equal(!expectedOptions.toggleSiblingsResp);
279
+ dispatchMouse(middleNode.querySelector('.leftEdge'), 'click');
280
+ finishTransition(firstNode);
281
+ chai.expect(firstNode.closest('.hierarchy').classList.contains('hidden')).to.equal(false);
282
+ chai.expect(lastNode.closest('.hierarchy').classList.contains('hidden')).to.equal(false);
283
+
284
+ if (expectedOptions.draggable) {
285
+ currentChart.filterAllowedDropNodes(middleNode);
286
+ chai.expect(firstNode.classList.contains('allowedDrop')).to.equal(true);
287
+ chai.expect(middleNode.classList.contains('allowedDrop')).to.equal(false);
288
+ await currentChart.dropHandler({ dropZone: firstNode });
289
+ chai.expect(currentChart.getParent(middleNode)).to.equal(firstNode);
290
+ currentChart.hideDropZones();
291
+ }
292
+
293
+ const pngSpy = sinon.stub(currentChart, 'exportPNG');
294
+ const pdfSpy = sinon.stub(currentChart, 'exportPDF');
295
+ if (exportButton) {
296
+ dispatchMouse(exportButton, 'click');
297
+ } else {
298
+ currentChart.export();
299
+ }
300
+ await Promise.resolve();
301
+ chai.expect(captureSpy.callCount).to.equal(1);
302
+ chai.expect(captureSpy.firstCall.args[0]).to.equal(currentChart.chart);
303
+ chai.expect(captureSpy.firstCall.args[1].scale).to.equal(expectedOptions.exportScale);
304
+ const exportSpy = expectedOptions.exportFileextension.toLowerCase() === 'pdf' ? pdfSpy : pngSpy;
305
+ chai.expect(exportSpy.callCount).to.equal(1);
306
+ chai.expect(exportSpy.firstCall.args[1]).to.equal(expectedOptions.exportFilename);
307
+ chai.expect(container.classList.contains('canvasContainer')).to.equal(false);
308
+ chai.expect(container.querySelector('.mask').classList.contains('hidden')).to.equal(true);
309
+ });
310
+ });
311
+ });
312
+ });
313
+ });
314
+ });
315
+ });
316
+
317
+ describe('all switch combinations across datasource types', function () {
318
+ const switches = ['pan', 'zoom', 'minimap', 'draggable', 'compact', 'toggleSiblingsResp', 'exportButton'];
319
+ ['json', 'ul', 'family'].forEach(function (sourceType) {
320
+ ['t2b', 'b2t', 'l2r', 'r2l'].forEach(function (direction) {
321
+ [1, 2, 3].forEach(function (visibleLevel) {
322
+ for (let mask = 0; mask < 128; mask += 1) {
323
+ it(sourceType + ' / ' + direction + ' / depth ' + visibleLevel + ' / mask ' + mask, function () {
324
+ const fixture = createSourceFixture(sourceType);
325
+ const options = { data: fixture.data, direction: direction, visibleLevel: visibleLevel, nodeId: mask % 2 ? 'key' : 'id' };
326
+ switches.forEach(function (key, index) { options[key] = !!(mask & (1 << index)); });
327
+ mount(options);
328
+ assertSourceHierarchy(chart, fixture);
329
+ switches.forEach(function (key) { chai.expect(chart.options[key]).to.equal(options[key]); });
330
+ chai.expect(!!container.querySelector('.orgchart-minimap')).to.equal(options.minimap);
331
+ chai.expect(!!chart.exportButtonElement).to.equal(options.exportButton);
332
+ });
333
+ }
334
+ });
335
+ });
336
+ });
337
+ });
338
+
339
+ describe('three-option covering profiles for all public options', function () {
340
+ const publicOptions = Object.keys(defaultOptions).concat(['data', 'nodeContent', 'nodeTemplate', 'createNode', 'dropCriteria', 'initCompleted', 'compact', 'verticalLevel']);
341
+ const enabled = function (profile, index) {
342
+ let bits = profile & (32 + index);
343
+ let count = 0;
344
+ while (bits) {
345
+ count += bits & 1;
346
+ bits >>>= 1;
347
+ }
348
+ return count % 2 === 1;
349
+ };
350
+ it('covers all eight assignments for every triple of 26 public options', function () {
351
+ chai.expect(publicOptions).to.have.length(26);
352
+ for (let first = 0; first < publicOptions.length; first += 1) {
353
+ for (let second = first + 1; second < publicOptions.length; second += 1) {
354
+ for (let third = second + 1; third < publicOptions.length; third += 1) {
355
+ const assignments = new Set();
356
+ for (let profile = 0; profile < 64; profile += 1) {
357
+ assignments.add(Number(enabled(profile, first)) * 4 + Number(enabled(profile, second)) * 2 + Number(enabled(profile, third)));
358
+ }
359
+ chai.expect(assignments.size, [publicOptions[first], publicOptions[second], publicOptions[third]].join(' + ')).to.equal(8);
360
+ }
361
+ }
362
+ }
363
+ });
364
+
365
+ ['json', 'ul', 'family'].forEach(function (sourceType) {
366
+ for (let profile = 0; profile < 64; profile += 1) {
367
+ it(sourceType + ' profile ' + profile, async function () {
368
+ const active = Object.fromEntries(publicOptions.map(function (key, index) { return [key, enabled(profile, index)]; }));
369
+ const fixture = createSourceFixture(sourceType, active.data);
370
+ const nodeTitle = active.nodeTitle ? 'label' : 'name';
371
+ mount({
372
+ data: fixture.data,
373
+ icons: active.icons ? { parentNode: 'oci-plus-square' } : defaultOptions.icons,
374
+ nodeTitle: nodeTitle,
375
+ nodeId: active.nodeId ? 'key' : 'id',
376
+ toggleSiblingsResp: active.toggleSiblingsResp,
377
+ visibleLevel: active.visibleLevel ? 3 : 1,
378
+ chartClass: active.chartClass ? 'profile-chart secondary' : '',
379
+ exportButton: active.exportButton,
380
+ exportButtonName: active.exportButtonName ? 'Save Chart' : 'Export',
381
+ exportFilename: active.exportFilename ? 'Profile' : 'OrgChart',
382
+ exportFileextension: active.exportFileextension ? 'pdf' : 'png',
383
+ exportScale: active.exportScale ? 3 : 1,
384
+ draggable: active.draggable,
385
+ direction: active.direction ? 'l2r' : 't2b',
386
+ pan: active.pan, minimap: active.minimap, zoom: active.zoom,
387
+ zoominLimit: active.zoominLimit ? 2.5 : 7,
388
+ zoomoutLimit: active.zoomoutLimit ? 0.6 : 0.5,
389
+ nodeContent: active.nodeContent ? 'name' : undefined,
390
+ nodeTemplate: active.nodeTemplate ? function (data) { return '<div class="title">' + data[nodeTitle] + '</div><div class="content">' + data.name + '</div>'; } : undefined,
391
+ createNode: active.createNode ? function (node) { node.dataset.custom = 'true'; } : undefined,
392
+ dropCriteria: active.dropCriteria ? function (source, parent, target) { return target.id.endsWith('first'); } : undefined,
393
+ initCompleted: active.initCompleted ? function (element) { element.dataset.initialized = 'true'; } : undefined,
394
+ compact: active.compact ? function (data) { return (data.children || []).length > 1; } : false,
395
+ verticalLevel: active.verticalLevel ? 3 : undefined
396
+ });
397
+ assertSourceHierarchy(chart, fixture);
398
+ await Promise.resolve();
399
+ chai.expect(chart.chart.dataset.initialized === 'true').to.equal(active.initCompleted);
400
+ const node = chart.chart.querySelector('.node');
401
+ chai.expect(node.dataset.custom === 'true').to.equal(active.createNode);
402
+ const nodeData = getState(node, 'nodeData');
403
+ chai.expect(node.querySelector('.title').textContent).to.equal(nodeData[nodeTitle]);
404
+ });
405
+ }
406
+ });
407
+ });
408
+
409
+ describe('datasource edits survive option reconfiguration', function () {
410
+ ['json', 'ul', 'family'].forEach(function (sourceType) {
411
+ ['addChildren', 'addDescendants', 'addParent', 'addSiblings', 'removeNodes'].forEach(function (operation) {
412
+ it(sourceType + ' / ' + operation, function () {
413
+ const fixture = createSourceFixture(sourceType);
414
+ mount({ data: fixture.data, nodeId: 'key' });
415
+ const nodeFor = function (id) { return chart.chart.querySelector('[id="key-' + id + '"]'); };
416
+ const added = { id: 'added', key: 'key-added', name: 'Added', label: 'Label added' };
417
+ if (operation === 'addChildren') {
418
+ chart.addChildren(nodeFor('middle'), [added]);
419
+ } else if (operation === 'addDescendants') {
420
+ chart.addDescendants([added], nodeFor('middle'));
421
+ } else if (operation === 'addParent') {
422
+ chart.addParent(nodeFor('first'), added);
423
+ } else if (operation === 'addSiblings') {
424
+ chart.addSiblings(nodeFor('first'), [added]);
425
+ } else {
426
+ chart.removeNodes(nodeFor('first'));
427
+ }
428
+ const expectedCount = fixture.records.length + (operation === 'removeNodes' ? -1 : 1);
429
+ chai.expect(chart.chart.querySelectorAll('.node').length).to.equal(expectedCount);
430
+ const hierarchy = chart.getHierarchy();
431
+ const ids = [];
432
+ const collect = function (value) {
433
+ if (Array.isArray(value)) {
434
+ value.forEach(collect);
435
+ } else {
436
+ ids.push(value.id);
437
+ (value.children || []).forEach(collect);
438
+ }
439
+ };
440
+ collect(hierarchy);
441
+ chai.expect(ids).to.have.length(expectedCount);
442
+ if (operation !== 'removeNodes') {
443
+ chai.expect(ids).to.include('key-added');
444
+ }
445
+ chart.setOptions({ chartClass: 'edited-chart', compact: true });
446
+ chai.expect(chart.getHierarchy()).to.deep.equal(hierarchy);
447
+ chai.expect(chart.chart.querySelectorAll('.node').length).to.equal(expectedCount);
448
+ });
449
+ });
450
+ });
451
+ });
452
+
453
+ ['zoom', 'minimap'].forEach(function (navigationOption) {
454
+ [
455
+ { zoomoutLimit: 0.8, zoominLimit: 1.2, label: 'exact wheel-step boundaries' },
456
+ { zoomoutLimit: 0.9, zoominLimit: 1.1, label: 'narrow interval' }
457
+ ].forEach(function (limits) {
458
+ it(navigationOption + ' + zoom limits reaches both bounds for ' + limits.label, function () {
459
+ mount({ [navigationOption]: true, zoomoutLimit: limits.zoomoutLimit, zoominLimit: limits.zoominLimit });
460
+ const target = navigationOption === 'minimap' ? container.querySelector('.orgchart-minimap') : container;
461
+
462
+ dispatchWheel(target, -100);
463
+ chai.expect(Number(chart.chart.style.transform.slice(7, -1).split(',')[0])).to.equal(limits.zoominLimit);
464
+ chart.chart.style.transform = '';
465
+ dispatchWheel(target, 100);
466
+ chai.expect(Number(chart.chart.style.transform.slice(7, -1).split(',')[0])).to.equal(limits.zoomoutLimit);
467
+ });
468
+ });
469
+ it(navigationOption + ' + zoom limits rejects reversed bounds before initialization', function () {
470
+ chai.expect(function () {
471
+ mount({ [navigationOption]: true, zoomoutLimit: 2, zoominLimit: 1 });
472
+ }).to.throw(RangeError);
473
+ });
474
+ });
475
+
476
+ Object.keys(defaultOptions.icons).forEach(function (iconName) {
477
+ it('partial icons.' + iconName + ' + toggleSiblingsResp preserves other icon defaults', function () {
478
+ mount({ icons: { [iconName]: 'custom-icon' }, toggleSiblingsResp: true });
479
+
480
+ Object.keys(defaultOptions.icons).forEach(function (otherIconName) {
481
+ chai.expect(chart.options.icons[otherIconName], otherIconName).to.equal(otherIconName === iconName ? 'custom-icon' : defaultOptions.icons[otherIconName]);
482
+ });
483
+ });
484
+ });
485
+
486
+ [1, 2].forEach(function (visibleLevel) {
487
+ it('visibleLevel=' + visibleLevel + ' + minimap excludes hidden descendants', function () {
488
+ mount({ visibleLevel: visibleLevel, minimap: true });
489
+
490
+ const visibleNodes = Array.from(chart.chart.querySelectorAll('.node')).filter(function (nodeElement) {
491
+ return !nodeElement.closest('.hidden');
492
+ });
493
+ chai.expect(container.querySelectorAll('.orgchart-minimap-node').length).to.equal(visibleNodes.length);
494
+ });
495
+ });
496
+
497
+ it('pan + minimap refreshes the viewport on the first mouse move', function () {
498
+ mount({ pan: true, minimap: true });
499
+ const updateSpy = sinon.spy(chart, 'updateMinimap');
500
+
501
+ dispatchMouse(chart.chart, 'mousedown', 100, 100);
502
+ dispatchMouse(chart.chart, 'mousemove', 70, 80);
503
+
504
+ chai.expect(updateSpy.callCount).to.equal(1);
505
+ });
506
+
507
+ it('zoom + visibleLevel handles one wheel event once after init', function () {
508
+ mount({ zoom: true, visibleLevel: 1 });
509
+ chart.init({ data: createData(), visibleLevel: 2 });
510
+ const scaleSpy = sinon.spy(chart, 'setChartScale');
511
+
512
+ dispatchWheel(container);
513
+
514
+ chai.expect(scaleSpy.callCount).to.equal(1);
515
+ });
516
+
517
+ it('zoom + zoominLimit stops wheel zoom when disabled during init', function () {
518
+ mount({ zoom: true, zoominLimit: 2.5 });
519
+ chart.init({ data: createData(), zoom: false });
520
+ const scaleSpy = sinon.spy(chart, 'setChartScale');
521
+
522
+ dispatchWheel(container);
523
+
524
+ chai.expect(scaleSpy.callCount).to.equal(0);
525
+ });
526
+
527
+ it('chartClass + exportButton provides an export button for each chart instance', function () {
528
+ mount({ chartClass: 'first-chart', exportButton: true });
529
+ const secondContainer = document.createElement('div');
530
+ document.getElementById('chart-host').appendChild(secondContainer);
531
+ mount({ chartClass: 'second-chart', exportButton: true }, secondContainer);
532
+
533
+ chai.expect(document.querySelectorAll('.oc-export-btn').length).to.equal(2);
534
+ });
535
+
536
+ it('exportButton + exportButtonName updates the button label during init', function () {
537
+ mount({ exportButton: true, exportButtonName: 'First Export' });
538
+ chart.init({ data: createData(), exportButtonName: 'Second Export' });
539
+
540
+ chai.expect(document.querySelector('.oc-export-btn').textContent).to.equal('Second Export');
541
+ });
542
+
543
+ it('exportButton + visibleLevel removes a disabled export button during init', function () {
544
+ mount({ exportButton: true, visibleLevel: 1 });
545
+ chart.init({ data: createData(), exportButton: false, visibleLevel: 2 });
546
+
547
+ chai.expect(document.querySelector('.oc-export-btn')).to.equal(null);
548
+ });
549
+
550
+ it('chartClass with multiple tokens + exportFilename reuses the download anchor', function () {
551
+ mount({ chartClass: 'compat-chart secondary-chart', exportFilename: 'First Export' });
552
+ sinon.stub(window.HTMLAnchorElement.prototype, 'click');
553
+ document.documentElement.style.WebkitAppearance = 'none';
554
+ const canvas = { toDataURL: function () { return 'data:image/png;base64,fake'; } };
555
+
556
+ chart.exportPNG(canvas, chart.options.exportFilename);
557
+ chart.exportPNG(canvas, 'Second Export');
558
+
559
+ chai.expect(container.querySelectorAll('.download-btn').length).to.equal(1);
560
+ });
561
+
562
+ it('zoom + minimap applies one scale increment per wheel event', function () {
563
+ mount({
564
+ data: {
565
+ id: 'root',
566
+ name: 'Root',
567
+ children: [{ id: 'child', name: 'Child' }]
568
+ },
569
+ zoom: true,
570
+ minimap: true
571
+ });
572
+ const scaleSpy = sinon.spy(chart, 'setChartScale');
573
+
574
+ container.querySelector('.orgchart-minimap').dispatchEvent(new window.WheelEvent('wheel', {
575
+ bubbles: true,
576
+ cancelable: true,
577
+ deltaY: -100,
578
+ clientX: 0,
579
+ clientY: 0
580
+ }));
581
+
582
+ chai.expect(scaleSpy.callCount).to.equal(1);
583
+ chai.expect(Number(chart.chart.style.transform.slice(7, -1).split(',')[0])).to.be.closeTo(1.2, 0.000001);
584
+ });
585
+
586
+ it('exportButton + loading isolates buttons between chart instances', function () {
587
+ const firstChart = mount({ exportButton: true, chartClass: 'first' });
588
+ const secondContainer = document.createElement('div');
589
+ document.getElementById('chart-host').appendChild(secondContainer);
590
+ const secondChart = mount({ exportButton: true, chartClass: 'second' }, secondContainer);
591
+ firstChart.chart.querySelector('#root').dispatchEvent(new window.MouseEvent('mouseenter'));
592
+ const edge = firstChart.chart.querySelector('#root > .bottomEdge');
593
+
594
+ firstChart.startLoading(edge);
595
+ chai.expect(firstChart.exportButtonElement.disabled).to.equal(true);
596
+ chai.expect(secondChart.exportButtonElement.disabled).to.equal(false);
597
+ firstChart.endLoading(edge);
598
+ chai.expect(firstChart.exportButtonElement.disabled).to.equal(false);
599
+ });
600
+
601
+ it('createNode styles survive startLoading and endLoading', function () {
602
+ mount({
603
+ createNode: function (nodeElement) {
604
+ nodeElement.querySelector('.title').style.color = 'rgb(10, 20, 30)';
605
+ nodeElement.querySelector('.title').style.opacity = '0.8';
606
+ }
607
+ });
608
+ const title = chart.chart.querySelector('#root > .title');
609
+ chart.chart.querySelector('#root').dispatchEvent(new window.MouseEvent('mouseenter'));
610
+ const edge = chart.chart.querySelector('#root > .bottomEdge');
611
+
612
+ chart.startLoading(edge);
613
+ chart.endLoading(edge);
614
+ chai.expect(title.style.color).to.equal('rgb(10, 20, 30)');
615
+ chai.expect(title.style.opacity).to.equal('0.8');
616
+ });
617
+
618
+ it('initCompleted + reconfiguration completes only the current chart', async function () {
619
+ const completed = sinon.spy();
620
+ mount({ initCompleted: completed });
621
+ chart.setOptions('chartClass', 'reconfigured');
622
+ await Promise.resolve();
623
+
624
+ chai.expect(completed.callCount).to.equal(1);
625
+ chai.expect(completed.firstCall.args[0]).to.equal(chart.chart);
626
+ chai.expect(chart.chart.isConnected).to.equal(true);
627
+ });
628
+ });
@@ -21,7 +21,7 @@ describe('Demo Interactions', () => {
21
21
  expect($node[0].nextElementSibling).to.have.class('hidden');
22
22
  });
23
23
 
24
- cy.contains('#chart-container .node .title', 'Hei Hei').closest('.node').find('.bottomEdge').click();
24
+ cy.contains('#chart-container .node .title', 'Hei Hei').closest('.node').trigger('mouseenter').find('.bottomEdge').click();
25
25
  cy.contains('#chart-container .node .title', 'Dan Dan').should('be.visible');
26
26
  });
27
27
 
@@ -36,7 +36,7 @@ describe('Demo Interactions', () => {
36
36
 
37
37
  it('expands a selected branch to the configured depth', () => {
38
38
  cy.visit('demo/expand-to-given-depth.html');
39
- cy.contains('#chart-container .node .title', 'Su Miao').closest('.node').find('.bottomEdge').click();
39
+ cy.contains('#chart-container .node .title', 'Su Miao').closest('.node').trigger('mouseenter').find('.bottomEdge').click();
40
40
  cy.contains('#chart-container .node .title', 'Dan Zai').should('be.visible');
41
41
  cy.contains('#chart-container .node .title', 'Er Dan Zai').should('not.be.visible');
42
42
  cy.contains('#chart-container .node .title', 'AAA').should('not.be.visible');
@@ -79,7 +79,7 @@ describe('Demo Interactions', () => {
79
79
  cy.viewport(500, 720);
80
80
  cy.visit('demo/responsive-design.html');
81
81
  cy.window().trigger('resize');
82
- cy.get('#chart-container .verticalNodes, #chart-container .toggleBtn').should('exist');
82
+ cy.get('#chart-container .nodes.vertical').should('exist');
83
83
  });
84
84
 
85
85
  it('renders all 10,000 generated nodes', () => {