UNPKG

orgchart

Version:

Simple and direct organization chart(tree-like hierarchy) library based on pure DOM.

1,132 lines (967 loc) 41.4 kB
const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai').default; const should = chai.should(); chai.use(sinonChai); require('jsdom-global')(); process.env.ORGCHART_TEST = '1'; const OrgChart = require('../../src/js/orgchart'); const getState = OrgChart.__testing__.getState; const setState = OrgChart.__testing__.setState; function query(selector, root) { return (root || document).querySelector(selector); } function queryAll(selector, root) { return Array.from((root || document).querySelectorAll(selector)); } function siblingElements(element, selector) { if (!element || !element.parentElement) { return []; } return Array.from(element.parentElement.children).filter(function (childEl) { return childEl !== element && (!selector || childEl.matches(selector)); }); } function cloneValue(value) { return JSON.parse(JSON.stringify(value)); } function dispatchNativeEvent(element, type, options) { const eventOptions = Object.assign({ bubbles: true }, options); let event; if (/click|mouse/.test(type)) { event = new window.MouseEvent(type, eventOptions); } else { event = new window.Event(type, eventOptions); } element.dispatchEvent(event); } function dispatchEventWithProperties(element, type, properties, options) { const event = new window.Event(type, Object.assign({ bubbles: true }, options)); Object.keys(properties || {}).forEach(function (key) { Object.defineProperty(event, key, { configurable: true, value: properties[key] }); }); element.dispatchEvent(event); } describe('orgchart -- integration tests', function () { let $container; const ds = { 'id': 'n1', 'name': 'Lao Lao', 'children': [ { 'id': 'n2', 'name': 'Bo Miao' }, { 'id': 'n3', 'name': 'Su Miao', 'children': [ { 'id': 'n5', 'name': 'Tie Hua', 'children' : [ { 'id': 'n8', 'name': 'Dan Dan' } ] }, { 'id': 'n6', 'name': 'Hei Hei', 'children': [ { 'id': 'n9', 'name': 'Er Dan' } ] }, { 'id': 'n7', 'name': 'Pang Pang', 'children': [ { 'id': 'n10', 'name': 'San Dan' } ] } ] }, { 'id': 'n4', 'name': 'Hong Miao' }, ] }; let oc = {}; const hierarchy = { id: 'n1', children: [ { id: 'n2' }, { id: 'n3', children: [ { id: 'n5', children: [ { id: 'n8' } ] }, { id: 'n6', children: [ { id: 'n9'} ] }, { id: 'n7', children: [ { id: 'n10' } ] } ] }, { id: 'n4' } ] }; let $laolao; let $bomiao; let $sumiao; let $hongmiao; let $chunmiao; let $tiehua; let $heihei; let $pangpang; let $dandan; let $erdan; beforeEach(function () { document.body.innerHTML = '<div id="chart-container"></div>'; $container = document.getElementById('chart-container'); oc = new OrgChart({ chartContainer: '#chart-container', 'data': ds }), $laolao = document.getElementById('n1'), $bomiao = document.getElementById('n2'), $sumiao = document.getElementById('n3'), $hongmiao = document.getElementById('n4'), $tiehua = document.getElementById('n5'), $heihei = document.getElementById('n6'), $pangpang = document.getElementById('n7'), $dandan = document.getElementById('n8'), $erdan = document.getElementById('n9'), $sandan = document.getElementById('n10'); }); afterEach(function () { $laolao = $bomiao = $sumiao = $hongmiao = $chunmiao = $tiehua = $heihei = $pangpang = $dandan = $erdan = $sandan = null; $container.innerHTML = ''; }); it('addParent()', function () { const expectedHierarchy = { id: 'n0', children: [cloneValue(hierarchy)] }; oc.addParent($laolao, { 'name': 'Lao Ye', 'id': 'n0' }); siblingElements($laolao.closest('.nodes'), '.node').should.lengthOf(1); query('.node', oc.chart).should.equal(query('#n0')); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('inserts a parent between a non-root node and its existing parent', function () { const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[0] = { id: 'n0', children: [expectedHierarchy.children[0]] }; oc.addParent($bomiao, { 'name': 'Lao Ye', 'id': 'n0' }); query('#n0').closest('.hierarchy').parentElement.parentElement.querySelector(':scope > .node').should.equal(query('#n1')); query('#n2').closest('.hierarchy').parentElement.previousElementSibling.should.equal(query('#n0')); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('addAncestors()', function () { oc.addAncestors({ 'name': 'Lao Ye', 'id': 'n0', 'relationship': '001' }, 'n0'); should.exist(query('#n0')); query('#n0').closest('.hierarchy').should.not.equal(query('#n1').closest('.hierarchy')); oc.getHierarchy().should.deep.equal(hierarchy); }); describe('addChildren()', function () { it('Add child nodes to the leaf node', function () { let siblingNodesElement; const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[0].children = [{ id: 'n11' }]; oc.addChildren($bomiao, [{'name': 'Li Xin', 'id': 'n11' }]); siblingNodesElement = siblingElements($bomiao, '.nodes')[0]; siblingElements($bomiao, '.nodes').should.lengthOf(1); queryAll('.hierarchy', siblingNodesElement).should.lengthOf(1); query('.node', siblingNodesElement).id.should.equal('n11'); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('Add child nodes to the un-leaf node', function () { let siblingNodesElements; let hierarchyElements; const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[1].children.push({ id: 'n11' }); oc.addChildren($sumiao, [{'name': 'Li Xin', 'id': 'n11' }]); siblingNodesElements = siblingElements($sumiao, '.nodes'); hierarchyElements = siblingNodesElements.flatMap(function (nodesElement) { return Array.from(nodesElement.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }); }); hierarchyElements.should.lengthOf(4); query('.node', hierarchyElements[hierarchyElements.length - 1]).id.should.equal('n11'); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); }); describe('addDescendants()', function () { it('Add descendant nodes from a native parent node', function () { let siblingNodesElement; const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[0].children = [{ id: 'n11' }]; oc.addDescendants([{ 'name': 'Li Xin', 'id': 'n11' }], $bomiao); siblingNodesElement = siblingElements($bomiao, '.nodes')[0]; siblingElements($bomiao, '.nodes').should.lengthOf(1); queryAll('.hierarchy', siblingNodesElement).should.lengthOf(1); query('.node', siblingNodesElement).id.should.equal('n11'); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('Preserves descendant insertion order and nested descendant structure', function () { const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[0].children = [ { id: 'n11', children: [{ id: 'n12' }] }, { id: 'n13' } ]; oc.addDescendants([ { 'name': 'Li Xin', 'id': 'n11', 'children': [ { 'name': 'Xiao Xin', 'id': 'n12' } ] }, { 'name': 'Li Na', 'id': 'n13' } ], $bomiao); Array.from($bomiao.nextElementSibling.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).should.lengthOf(2); queryAll('#n12', $bomiao.nextElementSibling).should.lengthOf(1); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); }); describe('addSiblings()', function () { it('Just add sibling nodes', function () { const expectedHierarchy = cloneValue(hierarchy); const rootHierarchyElement = $laolao.closest('.hierarchy'); const rootChildrenContainer = Array.from(rootHierarchyElement.children).find(function (childEl) { return childEl.classList && childEl.classList.contains('nodes'); }); expectedHierarchy.children.push({ id: 'n11' }); oc.addSiblings($sumiao, [{'name': 'Li Xin', 'id': 'n11' }]); Array.from(rootChildrenContainer.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).should.lengthOf(4); should.exist(query('#n11', rootChildrenContainer)); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('Add sibling nodes as well as parent node', function () { let hierarchySiblings; const expectedHierarchy = { id: 'n0', children: [{ id: 'n11' }, cloneValue(hierarchy)] }; oc.addSiblings($laolao, { 'name': 'Lao Ye', 'id': 'n0', 'children': [{'name': 'Li Xin', 'id': 'n11' }] }); siblingElements($laolao.closest('.nodes'), '.node').should.lengthOf(1); query('.node', oc.chart).should.equal(query('#n0')); hierarchySiblings = siblingElements($laolao.closest('.hierarchy')); hierarchySiblings.should.lengthOf(1); query('.node', hierarchySiblings[0]).id.should.equal('n11'); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); }); describe('removeNodes()', function () { it('Remove leaf node', function () { const expectedHierarchy = cloneValue(hierarchy); delete expectedHierarchy.children[1].children[0].children; oc.removeNodes($dandan); siblingElements($tiehua, '.nodes').should.lengthOf(0); should.not.exist(query('.bottomEdge', $tiehua)); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('Remove parent node', function () { const siblingNodesElement = siblingElements($sumiao, '.nodes')[0]; const expectedHierarchy = cloneValue(hierarchy); expectedHierarchy.children[1].children.splice(0, 1); oc.removeNodes($tiehua); Array.from(siblingNodesElement.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).should.lengthOf(2); should.not.exist(query('#n5')); should.not.exist(query('#n8')); oc.getHierarchy().should.deep.equal(expectedHierarchy); }); it('Remove a root child node and preserve sibling order', function () { const expectedHierarchy = cloneValue(hierarchy); const rootHierarchyElement = $laolao.closest('.hierarchy'); const rootChildrenContainer = Array.from(rootHierarchyElement.children).find(function (childEl) { return childEl.classList && childEl.classList.contains('nodes'); }); expectedHierarchy.children.splice(0, 1); oc.removeNodes($bomiao); should.not.exist(query('#n2')); oc.getHierarchy().should.deep.equal(expectedHierarchy); Array.from(query(':scope > .nodes', oc.chart).children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).map(function (hierarchyEl) { return query('.node', hierarchyEl).id; }).should.deep.equal(['n1']); Array.from(rootChildrenContainer.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).map(function (hierarchyEl) { return query('.node', hierarchyEl).id; }).should.deep.equal(['n3', 'n4']); }); it('Remove root node', function () { oc.removeNodes($laolao); $container.children.length.should.equal(0); }); }); describe('interaction flows', function () { it('dispatches hide/show children events through bottom-edge clicks', function () { const bottomEdge = query('.bottomEdge', $sumiao); const firstChildNode = siblingElements($sumiao, '.nodes')[0].querySelector('.node'); const events = []; $sumiao.addEventListener('hide-children.orgchart', function (event) { events.push(event.type); }); $sumiao.addEventListener('show-children.orgchart', function (event) { events.push(event.type); }); dispatchNativeEvent(bottomEdge, 'click'); dispatchNativeEvent(firstChildNode, 'transitionend'); dispatchNativeEvent(bottomEdge, 'click'); dispatchNativeEvent(firstChildNode, 'transitionend'); events.should.deep.equal(['hide-children.orgchart', 'show-children.orgchart']); }); it('dispatches hide/show siblings events through horizontal-edge clicks', function () { const leftEdge = query('.leftEdge', $sumiao); const firstSiblingNode = query('#n2'); const events = []; $sumiao.addEventListener('hide-siblings.orgchart', function (event) { events.push(event.type); }); $sumiao.addEventListener('show-siblings.orgchart', function (event) { events.push(event.type); }); dispatchNativeEvent(leftEdge, 'click'); dispatchNativeEvent(firstSiblingNode, 'transitionend'); dispatchNativeEvent(leftEdge, 'click'); dispatchNativeEvent(firstSiblingNode, 'transitionend'); events.should.deep.equal(['hide-siblings.orgchart', 'show-siblings.orgchart']); }); it('dispatches hide-parent and enters ancestor-collapsed state through top-edge click', function () { const topEdge = query('.topEdge', $heihei); const hideSpy = sinon.spy(); const currentHierarchy = $heihei.closest('.hierarchy'); const leftSiblingHierarchy = currentHierarchy.previousElementSibling; const rightSiblingHierarchy = currentHierarchy.nextElementSibling; $heihei.addEventListener('hide-parent.orgchart', hideSpy); dispatchNativeEvent(topEdge, 'click'); hideSpy.should.have.been.calledOnce; hideSpy.firstCall.args[0].type.should.equal('hide-parent.orgchart'); currentHierarchy.classList.contains('isAncestorsCollapsed').should.be.true; currentHierarchy.classList.contains('isSiblingsCollapsed').should.be.true; leftSiblingHierarchy.classList.contains('isCollapsedSibling').should.be.true; rightSiblingHierarchy.classList.contains('isCollapsedSibling').should.be.true; $sumiao.classList.contains('sliding').should.be.true; $sumiao.classList.contains('slide-down').should.be.true; $laolao.classList.contains('sliding').should.be.true; $laolao.classList.contains('slide-down').should.be.true; dispatchNativeEvent($sumiao, 'transitionend'); dispatchNativeEvent($laolao, 'transitionend'); $sumiao.classList.contains('sliding').should.be.false; $laolao.classList.contains('sliding').should.be.false; }); it('focuses the clicked node and clears focus when clicking chart background', function () { dispatchNativeEvent($sumiao, 'click'); $sumiao.classList.contains('focused').should.be.true; dispatchNativeEvent($hongmiao, 'click'); $sumiao.classList.contains('focused').should.be.false; $hongmiao.classList.contains('focused').should.be.true; dispatchNativeEvent(oc.chart, 'click'); queryAll('.node.focused', oc.chart).should.have.lengthOf(0); }); it('toggles children visibility through bottom-edge clicks and transition cleanup', function () { const childNodesContainer = siblingElements($sumiao, '.nodes')[0]; const bottomEdge = query('.bottomEdge', $sumiao); const childHierarchies = Array.from(childNodesContainer.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }); const visibleChildNodes = childHierarchies.map(function (hierarchyEl) { return query('.node', hierarchyEl); }); oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: true }); dispatchNativeEvent(bottomEdge, 'click'); childNodesContainer.classList.contains('hidden').should.be.false; childHierarchies.forEach(function (hierarchyEl) { hierarchyEl.classList.contains('isCollapsedDescendant').should.be.true; }); visibleChildNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.true; childNodeEl.classList.contains('slide-up').should.be.true; }); dispatchNativeEvent(visibleChildNodes[0], 'transitionend'); childNodesContainer.classList.contains('hidden').should.be.true; oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: false }); dispatchNativeEvent(bottomEdge, 'click'); childNodesContainer.classList.contains('hidden').should.be.false; visibleChildNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.true; childNodeEl.classList.contains('slide-up').should.be.false; }); dispatchNativeEvent(visibleChildNodes[0], 'transitionend'); oc.getNodeState($sumiao, 'children').should.deep.equal({ exist: true, visible: true }); }); it('toggles sibling visibility through horizontal-edge clicks and transition cleanup', function () { const leftEdge = query('.leftEdge', $sumiao); const siblingHierarchies = Array.from($sumiao.closest('.hierarchy').parentElement.children).filter(function (childEl) { return childEl !== $sumiao.closest('.hierarchy') && childEl.classList.contains('hierarchy'); }); const siblingNodes = siblingHierarchies.map(function (hierarchyEl) { return query('.node', hierarchyEl); }); oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: true }); dispatchNativeEvent(leftEdge, 'click'); siblingNodes.forEach(function (siblingNodeEl) { siblingNodeEl.classList.contains('sliding').should.be.true; }); dispatchNativeEvent(siblingNodes[0], 'transitionend'); siblingHierarchies.forEach(function (siblingHierarchyEl) { siblingHierarchyEl.classList.contains('hidden').should.be.true; }); oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: false }); dispatchNativeEvent(leftEdge, 'click'); siblingHierarchies.forEach(function (siblingHierarchyEl) { siblingHierarchyEl.classList.contains('hidden').should.be.false; }); siblingNodes.forEach(function (siblingNodeEl) { siblingNodeEl.classList.contains('sliding').should.be.true; }); dispatchNativeEvent(siblingNodes[0], 'transitionend'); oc.getNodeState($sumiao, 'siblings').should.deep.equal({ exist: true, visible: true }); }); it('shows directional edge icons on hover and clears them on mouseleave', function () { const topEdge = query('.topEdge', $sumiao); const bottomEdge = query('.bottomEdge', $sumiao); const leftEdge = query('.leftEdge', $sumiao); const rightEdge = query('.rightEdge', $sumiao); dispatchNativeEvent($sumiao, 'mouseenter', { bubbles: false }); topEdge.classList.contains(oc.options.icons.collapseToDown).should.be.true; bottomEdge.classList.contains(oc.options.icons.expandToUp).should.be.true; leftEdge.classList.contains(oc.options.icons.expandToRight).should.be.true; rightEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.true; dispatchNativeEvent($sumiao, 'mouseleave', { bubbles: false }); topEdge.classList.contains(oc.options.icons.expandToUp).should.be.false; topEdge.classList.contains(oc.options.icons.collapseToDown).should.be.false; bottomEdge.classList.contains(oc.options.icons.expandToUp).should.be.false; bottomEdge.classList.contains(oc.options.icons.collapseToDown).should.be.false; leftEdge.classList.contains(oc.options.icons.expandToRight).should.be.false; leftEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.false; rightEdge.classList.contains(oc.options.icons.expandToRight).should.be.false; rightEdge.classList.contains(oc.options.icons.collapseToLeft).should.be.false; }); }); describe('query methods', function () { it('returns current parent, children, and siblings as native nodes', function () { oc.getParent($sumiao).should.equal($laolao); oc.getChildren($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n5', 'n6', 'n7']); oc.getSiblings($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n2', 'n4']); }); it('updates relationship queries after tree mutations', function () { oc.addChildren($bomiao, [{ name: 'Li Xin', id: 'n11' }]); oc.addSiblings($sumiao, [{ name: 'Li Na', id: 'n12' }]); should.exist(query('#n11')); should.exist(query('#n12')); oc.getChildren($bomiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n11']); oc.getSiblings($sumiao).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n2', 'n4', 'n12']); oc.getParent(query('#n11')).should.equal($bomiao); oc.getParent(query('#n12')).should.equal($laolao); }); }); describe('runtime option flows', function () { it('rebuilds the rendered hierarchy through setOptions({ data })', function () { const replacementData = { id: 'r1', name: 'Replacement Root', children: [{ id: 'r2', name: 'Replacement Child' }] }; const previousChart = oc.chart; oc.setOptions({ data: replacementData }); oc.chart.should.not.equal(previousChart); should.not.exist(query('#n1', $container)); query('#r1', oc.chart).querySelector('.title').textContent.should.equal('Replacement Root'); oc.getChildren(query('#r1', oc.chart)).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['r2']); oc.getHierarchy().should.deep.equal({ id: 'r1', children: [{ id: 'r2' }] }); }); it('moves the chart through the minimap viewport and cleans up when disabled', function () { const minimapChart = new OrgChart({ chartContainer: '#chart-container', data: ds, minimap: true }); const minimapElement = query('.orgchart-minimap', $container); const minimapStage = query('.orgchart-minimap-stage', minimapElement); const minimapViewport = query('.orgchart-minimap-viewport', minimapElement); const containerRectStub = sinon.stub(minimapChart.chartContainer, 'getBoundingClientRect').returns({ left: 0, top: 0, right: 400, bottom: 240, width: 400, height: 240 }); const chartRectStub = sinon.stub(minimapChart.chart, 'getBoundingClientRect').returns({ left: -100, top: -50, right: 900, bottom: 550, width: 1000, height: 600 }); const stageRectStub = sinon.stub(minimapStage, 'getBoundingClientRect').returns({ left: 0, top: 0, right: 180, bottom: 104, width: 180, height: 104 }); const viewportRectStub = sinon.stub(minimapViewport, 'getBoundingClientRect').returns({ left: 20, top: 10, right: 89, bottom: 52, width: 69, height: 42 }); Object.defineProperty(minimapStage, 'clientWidth', { configurable: true, value: 180 }); Object.defineProperty(minimapStage, 'clientHeight', { configurable: true, value: 104 }); minimapChart.updateMinimap(true); dispatchEventWithProperties(minimapViewport, 'mousedown', { clientX: 20, clientY: 10, preventDefault: function () {} }); dispatchEventWithProperties(document, 'mousemove', { clientX: 40, clientY: 30 }); dispatchNativeEvent(document, 'mouseup'); minimapChart.chart.style.transform.should.not.equal(''); should.equal(getState(minimapChart.chart, 'minimapDragState'), null); minimapChart.setOptions('minimap', false); should.not.exist(query('.orgchart-minimap', $container)); $container.style.position.should.equal(''); containerRectStub.restore(); chartRectStub.restore(); stageRectStub.restore(); viewportRectStub.restore(); }); }); describe('hybrid vertical flows', function () { it('expands and collapses vertical descendants through toggle button clicks', function () { let hybridChart; let hybridSumiao; let toggleBtn; let descendantsWrapper; let childNodes; $container.innerHTML = ''; hybridChart = new OrgChart({ chartContainer: '#chart-container', data: { id: 'n1', name: 'Lao Lao', children: [ { id: 'n2', name: 'Bo Miao' }, { id: 'n3', name: 'Su Miao', children: [ { id: 'n5', name: 'Tie Hua' }, { id: 'n6', name: 'Hei Hei' } ] } ] }, verticalLevel: 2, visibleLevel: 2 }); hybridSumiao = query('#n3', hybridChart.chart); toggleBtn = query('.toggleBtn', hybridSumiao); descendantsWrapper = siblingElements(hybridSumiao, '.nodes')[0]; childNodes = Array.from(descendantsWrapper.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).map(function (hierarchyEl) { return query('.node', hierarchyEl); }); should.exist(toggleBtn); descendantsWrapper.classList.contains('hidden').should.be.true; childNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('slide-up').should.be.true; }); dispatchNativeEvent(toggleBtn, 'click'); descendantsWrapper.classList.contains('hidden').should.be.false; childNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.true; childNodeEl.classList.contains('slide-up').should.be.false; }); dispatchNativeEvent(childNodes[0], 'transitionend'); childNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.false; }); dispatchNativeEvent(toggleBtn, 'click'); descendantsWrapper.classList.contains('hidden').should.be.false; childNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.true; childNodeEl.classList.contains('slide-up').should.be.true; }); dispatchNativeEvent(childNodes[0], 'transitionend'); descendantsWrapper.classList.contains('hidden').should.be.true; childNodes.forEach(function (childNodeEl) { childNodeEl.classList.contains('sliding').should.be.false; }); }); }); describe('drag and drop flows', function () { it('filters allowed drop zones through the public dropCriteria option', function () { const draggableChart = new OrgChart({ chartContainer: '#chart-container', data: ds, draggable: true, dropCriteria: function (draggedNode, draggedParentNode, dropNode) { return draggedNode.id === 'n2' && draggedParentNode.id === 'n1' && dropNode.id === 'n4'; } }); const draggableBomiao = query('#n2', draggableChart.chart); dispatchEventWithProperties(draggableBomiao, 'dragstart', { dataTransfer: { setData: function () {} } }); queryAll('.allowedDrop', draggableChart.chart).map(function (nodeEl) { return nodeEl.id; }).should.deep.equal(['n4']); dispatchNativeEvent(draggableBomiao, 'dragend'); queryAll('.allowedDrop', draggableChart.chart).should.have.lengthOf(0); }); it('dispatches otherdropped for an external dragged item', async function () { const draggableChart = new OrgChart({ chartContainer: '#chart-container', data: ds, draggable: true }); const draggableBomiao = query('#n2', draggableChart.chart); const foreignItem = document.createElement('div'); const dropSpy = sinon.spy(); foreignItem.className = 'foreign-item'; draggableChart.chart.addEventListener('otherdropped.orgchart', dropSpy); dispatchEventWithProperties(draggableBomiao, 'dragstart', { dataTransfer: { setData: function () {} } }); setState(draggableChart.chart, 'dragged', foreignItem); await draggableChart.dropHandler({ dropZone: draggableBomiao, originalEvent: { currentTarget: draggableBomiao } }); dropSpy.should.have.been.calledOnce; dropSpy.firstCall.args[0].type.should.equal('otherdropped.orgchart'); dropSpy.firstCall.args[0].detail.draggedItem.should.equal(foreignItem); dropSpy.firstCall.args[0].detail.dropZone.should.equal(draggableBomiao); }); it('marks allowed drop zones on native dragstart and clears them on dragend', function () { const draggableChart = new OrgChart({ chartContainer: '#chart-container', data: ds, draggable: true }); const draggableBomiao = query('#n2', draggableChart.chart); const draggableHongmiao = query('#n4', draggableChart.chart); dispatchEventWithProperties(draggableBomiao, 'dragstart', { dataTransfer: { setData: function () {} } }); draggableHongmiao.classList.contains('allowedDrop').should.be.true; queryAll('.allowedDrop', draggableChart.chart).length.should.be.above(0); dispatchNativeEvent(draggableBomiao, 'dragend'); queryAll('.allowedDrop', draggableChart.chart).should.have.lengthOf(0); }); it('dispatches nodedrop and moves the dragged node through native drag/drop events', async function () { const originalJSONDigger = global.JSONDigger; const draggableChart = new OrgChart({ chartContainer: '#chart-container', data: ds, draggable: true }); const draggableBomiao = query('#n2', draggableChart.chart); const draggableHongmiao = query('#n4', draggableChart.chart); const rootChildrenContainer = siblingElements(query('#n1', draggableChart.chart), '.nodes')[0]; const dropSpy = sinon.spy(); global.JSONDigger = function () { return { findOneNode: function () { return null; } }; }; draggableChart.chart.addEventListener('nodedrop.orgchart', dropSpy); dispatchEventWithProperties(draggableBomiao, 'dragstart', { dataTransfer: { setData: function () {} } }); draggableHongmiao.classList.add('allowedDrop'); await draggableChart.dropHandler({ dropZone: draggableHongmiao, originalEvent: { currentTarget: draggableHongmiao } }); dropSpy.should.have.been.calledOnce; dropSpy.firstCall.args[0].type.should.equal('nodedrop.orgchart'); dropSpy.firstCall.args[0].detail.draggedNode.should.equal(draggableBomiao); dropSpy.firstCall.args[0].detail.dropZone.should.equal(draggableHongmiao); queryAll('#n2', siblingElements(draggableHongmiao, '.nodes')[0]).should.have.lengthOf(1); Array.from(rootChildrenContainer.children).filter(function (childEl) { return childEl.classList && childEl.classList.contains('hierarchy'); }).map(function (hierarchyEl) { return query('.node', hierarchyEl).id; }).should.deep.equal(['n3', 'n4']); global.JSONDigger = originalJSONDigger; }); it('adds a vertical toggle when hybrid drag and drop gives a leaf node children', async function () { const originalJSONDigger = global.JSONDigger; const hybridData = { id: 'n1', name: 'Root', children: [ { id: 'n2', name: 'Source Parent', children: [{ id: 'n3', name: 'Dragged Node' }] }, { id: 'n4', name: 'Drop Node' } ] }; const hybridChart = new OrgChart({ chartContainer: '#chart-container', data: hybridData, draggable: true, verticalLevel: 2 }); const draggedNode = query('#n3', hybridChart.chart); const dropNode = query('#n4', hybridChart.chart); global.JSONDigger = require('json-digger'); try { setState(hybridChart.chart, 'dragged', draggedNode); dropNode.classList.add('allowedDrop'); await hybridChart.dropHandler({ dropZone: dropNode, originalEvent: { currentTarget: dropNode } }); query('#n4', hybridChart.chart).querySelector(':scope > .toggleBtn').should.exist; query('#n3', hybridChart.chart).closest('.hierarchy').parentElement.previousElementSibling.id.should.equal('n4'); } finally { global.JSONDigger = originalJSONDigger; } }); }); describe('compact mode flows', function () { it('toggles loose and compact modes through compact-node controls', function () { let compactChart; let compactNode; let backToLooseSymbol; let backToCompactSymbol; $container.innerHTML = ''; compactChart = new OrgChart({ chartContainer: '#chart-container', data: { id: 'n1', name: 'Lao Lao', children: [ { id: 'n2', name: 'Bo Miao', compact: true, children: [ { id: 'n3', name: 'Su Miao' }, { id: 'n4', name: 'Hong Miao' } ] } ] } }); compactNode = query('#n2', compactChart.chart); backToLooseSymbol = query('.backToLooseSymbol', compactNode); backToCompactSymbol = query('.backToCompactSymbol', compactNode); should.exist(backToLooseSymbol); should.exist(backToCompactSymbol); compactNode.classList.contains('looseMode').should.be.false; backToCompactSymbol.classList.contains('hidden').should.be.true; dispatchNativeEvent(backToLooseSymbol, 'click'); compactNode.classList.contains('looseMode').should.be.true; backToLooseSymbol.classList.contains('hidden').should.be.true; backToCompactSymbol.classList.contains('hidden').should.be.false; dispatchNativeEvent(backToCompactSymbol, 'click'); compactNode.classList.contains('looseMode').should.be.false; backToLooseSymbol.classList.contains('hidden').should.be.false; backToCompactSymbol.classList.contains('hidden').should.be.true; }); }); describe('export flows', function () { it('invokes export() through the rendered export button click', function () { let exportChart; let exportButton; let exportStub; $container.innerHTML = ''; Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (buttonEl) { buttonEl.remove(); }); exportChart = new OrgChart({ chartContainer: '#chart-container', data: ds, exportButton: true }); exportButton = document.querySelector('.oc-export-btn'); exportStub = sinon.stub(exportChart, 'export'); should.exist(exportButton); dispatchNativeEvent(exportButton, 'click'); exportStub.should.have.been.calledOnce; exportButton.remove(); exportStub.restore(); }); }); describe('pan and zoom binding flows', function () { it('pans through native mousedown, mousemove, and mouseup bindings', function () { let panChart; let moveEvent; $container.innerHTML = ''; panChart = new OrgChart({ chartContainer: '#chart-container', data: ds, pan: true }); dispatchEventWithProperties(panChart.chart, 'mousedown', { target: panChart.chart, pageX: 10, pageY: 20 }); getState(panChart.chart, 'panning').should.equal(true); moveEvent = new window.Event('mousemove', { bubbles: true }); Object.defineProperty(moveEvent, 'pageX', { configurable: true, value: 30 }); Object.defineProperty(moveEvent, 'pageY', { configurable: true, value: 45 }); panChart.chart.dispatchEvent(moveEvent); panChart.chart.style.transform.should.equal('matrix(1, 0, 0, 1, 20, 25)'); dispatchNativeEvent(document, 'mouseup'); getState(panChart.chart, 'panning').should.equal(false); panChart.chart.style.cursor.should.equal('default'); }); it('zooms through the bound wheel listener on the chart container', function () { let zoomChart; $container.innerHTML = ''; zoomChart = new OrgChart({ chartContainer: '#chart-container', data: ds, zoom: true }); zoomChart.chart.getBoundingClientRect = function () { return { left: 0, top: 0 }; }; zoomChart.chartContainer.getBoundingClientRect = function () { return { left: 0, top: 0, right: 200, bottom: 100, width: 200, height: 100 }; }; dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', { deltaY: -1, clientX: 60, clientY: 45, preventDefault: function () {} }); zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -9)'); }); it('pinch-zooms through bound touchstart, touchmove, and touchend listeners', function () { let zoomChart; $container.innerHTML = ''; zoomChart = new OrgChart({ chartContainer: '#chart-container', data: ds, zoom: true }); zoomChart.chart.getBoundingClientRect = function () { return { left: 0, top: 0 }; }; zoomChart.chartContainer.getBoundingClientRect = function () { return { left: 0, top: 0, right: 200, bottom: 100, width: 200, height: 100 }; }; dispatchEventWithProperties(zoomChart.chartContainer, 'touchstart', { touches: [ { clientX: 40, clientY: 40 }, { clientX: 80, clientY: 40 } ] }); dispatchEventWithProperties(document, 'touchmove', { touches: [ { clientX: 30, clientY: 40 }, { clientX: 90, clientY: 40 } ] }); dispatchNativeEvent(document, 'touchend'); zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -8)'); }); it('toggles pan bindings through setOptions()', function () { let panChart; $container.innerHTML = ''; panChart = new OrgChart({ chartContainer: '#chart-container', data: ds }); setState(panChart.chart, 'panning', false); dispatchEventWithProperties(panChart.chart, 'mousedown', { target: panChart.chart, pageX: 10, pageY: 20 }); getState(panChart.chart, 'panning').should.equal(false); panChart.setOptions({ pan: true }); dispatchEventWithProperties(panChart.chart, 'mousedown', { target: panChart.chart, pageX: 10, pageY: 20 }); getState(panChart.chart, 'panning').should.equal(true); dispatchNativeEvent(document, 'mouseup'); panChart.setOptions({ pan: false }); setState(panChart.chart, 'panning', false); dispatchEventWithProperties(panChart.chart, 'mousedown', { target: panChart.chart, pageX: 30, pageY: 40 }); getState(panChart.chart, 'panning').should.equal(false); }); it('toggles zoom bindings through setOptions()', function () { let zoomChart; $container.innerHTML = ''; zoomChart = new OrgChart({ chartContainer: '#chart-container', data: ds }); zoomChart.chart.getBoundingClientRect = function () { return { left: 0, top: 0 }; }; zoomChart.chartContainer.getBoundingClientRect = function () { return { left: 0, top: 0, right: 200, bottom: 100, width: 200, height: 100 }; }; dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', { deltaY: -1, clientX: 60, clientY: 45, preventDefault: function () {} }); zoomChart.chart.style.transform.should.equal(''); zoomChart.setOptions({ zoom: true }); dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', { deltaY: -1, clientX: 60, clientY: 45, preventDefault: function () {} }); zoomChart.chart.style.transform.should.equal('matrix(1.2, 0, 0, 1.2, -12, -9)'); zoomChart.setOptions({ zoom: false }); zoomChart.chart.style.transform = 'none'; dispatchEventWithProperties(zoomChart.chartContainer, 'wheel', { deltaY: -1, clientX: 60, clientY: 45, preventDefault: function () {} }); zoomChart.chart.style.transform.should.equal('none'); }); }); });