UNPKG

orgchart

Version:

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

1,371 lines (1,112 loc) 90.9 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 createElementFromHtml(html) { const template = document.createElement('template'); template.innerHTML = html.trim(); return template.content.firstElementChild; } 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 childElements(element, selector) { return Array.from(element ? element.children : []).filter(function (childEl) { return !selector || childEl.matches(selector); }); } function closestElement(element, selector) { return element ? element.closest(selector) : null; } function firstSibling(element, selector) { return siblingElements(element, selector)[0] || null; } function previousSiblings(element, selector) { const siblings = []; let current = element ? element.previousElementSibling : null; while (current) { if (!selector || current.matches(selector)) { siblings.push(current); } current = current.previousElementSibling; } return siblings; } function nextSiblings(element, selector) { const siblings = []; let current = element ? element.nextElementSibling : null; while (current) { if (!selector || current.matches(selector)) { siblings.push(current); } current = current.nextElementSibling; } return siblings; } function addClasses(element, classNames) { if (!element) { return element; } classNames.split(/\s+/).filter(Boolean).forEach(function (className) { element.classList.add(className); }); return element; } function removeClasses(element, classNames) { if (!element) { return element; } classNames.split(/\s+/).filter(Boolean).forEach(function (className) { element.classList.remove(className); }); return element; } function matchesSelector(element, selector) { return !!(element && element.matches(selector)); } function setOneTimeListener(element, type, data, listener) { function once(event) { event.data = data; element.removeEventListener(type, once); listener.call(element, event); } element.addEventListener(type, once); } function triggerEvent(element, type) { element.dispatchEvent(new window.Event(type, { bubbles: true })); } function refreshNodeReferences(chartEl) { $laolao = query('#n1', chartEl); $bomiao = query('#n2', chartEl); $sumiao = query('#n3', chartEl); $hongmiao = query('#n4', chartEl); $tiehua = query('#n5', chartEl); $heihei = query('#n6', chartEl); $pangpang = query('#n7', chartEl); $dandan = query('#n8', chartEl); $erdan = query('#n9', chartEl); $sandan = query('#n10', chartEl); } describe('orgchart -- unit tests', function () { let $container; const ds = { 'id': 'n1', 'name': 'Lao Lao', 'title': 'general manager', 'children': [ { 'id': 'n2', 'name': 'Bo Miao', 'title': 'department manager' }, { 'id': 'n3', 'name': 'Su Miao', 'title': 'department manager', 'children': [ { 'id': 'n5', 'name': 'Tie Hua', 'title': 'senior engineer', 'children': [ { 'id': 'n8', 'name': 'Dan Dan', 'title': 'engineer' } ] }, { 'id': 'n6', 'name': 'Hei Hei', 'title': 'senior engineer', 'children': [ { 'id': 'n9', 'name': 'Er Dan', 'title': 'engineer' } ] }, { 'id': 'n7', 'name': 'Pang Pang', 'title': 'senior engineer', 'children': [ { 'id': 'n10', 'name': 'San Dan', 'title': 'engineer' } ] } ] }, { 'id': 'n4', 'name': 'Hong Miao', 'title': 'department manager' } ] }; const baseDs = JSON.stringify({ 'id': 'n1', 'name': 'Lao Lao', 'title': 'general manager', 'children': [ { 'id': 'n2', 'name': 'Bo Miao', 'title': 'department manager' }, { 'id': 'n3', 'name': 'Su Miao', 'title': 'department manager', 'children': [ { 'id': 'n5', 'name': 'Tie Hua', 'title': 'senior engineer', 'children': [ { 'id': 'n8', 'name': 'Dan Dan', 'title': 'engineer' } ] }, { 'id': 'n6', 'name': 'Hei Hei', 'title': 'senior engineer', 'children': [ { 'id': 'n9', 'name': 'Er Dan', 'title': 'engineer' } ] }, { 'id': 'n7', 'name': 'Pang Pang', 'title': 'senior engineer', 'children': [ { 'id': 'n10', 'name': 'San Dan', 'title': 'engineer' } ] } ] }, { 'id': 'n4', 'name': 'Hong Miao', 'title': 'department manager' } ] }); 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; let $sandan; beforeEach(function () { document.body.innerHTML = '<div id="chart-container"></div>'; $container = document.getElementById('chart-container'); oc = new OrgChart({ chartContainer: '#chart-container', 'data': ds, 'nodeContent': 'title' }), $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 = $tiehua = $heihei = $pangpang = $dandan = $erdan = $sandan = null; $container.innerHTML = ''; }); it('loopChart()', function () { oc.loopChart(query('.orgchart')).should.deep.equal(hierarchy); }); it('exposes native chartContainer and chart instance properties', function () { oc.chartContainer.should.equal($container); oc.chartContainer.firstElementChild.should.equal(oc.chart); oc.chart.should.have.property('nodeType', 1); }); it('buildJsonDS() accepts a native li element', function () { const ulContainer = document.createElement('ul'); ulContainer.innerHTML = '<li data-id="root" data-title="chief">Root<ul><li data-id="child">Child</li></ul></li>'; oc.buildJsonDS(ulContainer.firstElementChild).should.deep.equal({ id: 'root', title: 'chief', name: 'Root', relationship: '001', children: [ { id: 'child', name: 'Child', relationship: '100' } ] }); }); it('getHierarchy()', function () { oc.getHierarchy().should.deep.equal(hierarchy); const oc2 = new OrgChart({ chartContainer: '#chart-container', 'data': { name: 'Lao Lao', 'children': [ { name: 'Bo Miao' } ] } }); oc2.getHierarchy().should.include('Error'); oc2.chart.innerHTML = ""; oc2.getHierarchy().should.include('Error'); oc2.chart = null; oc2.getHierarchy().should.include('Error'); }); it('triggerShowEvent() accepts a native node', function () { const eventSpy = sinon.spy(); $sumiao.addEventListener('show-children.orgchart', eventSpy); oc.triggerShowEvent($sumiao, 'children'); eventSpy.should.have.been.calledOnce; eventSpy.firstCall.args[0].type.should.equal('show-children.orgchart'); }); it('triggerHideEvent() accepts a native node', function () { const eventSpy = sinon.spy(); $sumiao.addEventListener('hide-children.orgchart', eventSpy); oc.triggerHideEvent($sumiao, 'children'); eventSpy.should.have.been.calledOnce; eventSpy.firstCall.args[0].type.should.equal('hide-children.orgchart'); }); it('getNodeState()', function () { const stateDs = JSON.parse(baseDs); let chartEl; let laolaoEl; let bomiaoEl; let sumiaoEl; let tiehuaEl; let heiheiEl; let pangpangEl; let dandanEl; $container.innerHTML = ''; oc = new OrgChart({ chartContainer: '#chart-container', data: stateDs, nodeContent: 'title', visibleLevel: 2, verticalLevel: 3 }); chartEl = oc.chart; laolaoEl = query('#n1', chartEl); bomiaoEl = query('#n2', chartEl); sumiaoEl = query('#n3', chartEl); tiehuaEl = query('#n5', chartEl); heiheiEl = query('#n6', chartEl); pangpangEl = query('#n7', chartEl); dandanEl = query('#n8', chartEl); oc.getNodeState(laolaoEl).should.deep.equal({ 'exist': true, 'visible': true }, 'laolao->self'); oc.getNodeState(laolaoEl, 'parent').should.deep.equal({ 'exist': false, 'visible': false }, 'laolao->parent'); oc.getNodeState(laolaoEl, 'children').should.deep.equal({ 'exist': true, 'visible': true }, 'laolao->children'); oc.getNodeState(laolaoEl, 'siblings').should.deep.equal({ 'exist': false, 'visible': false }, 'laolao->siblings'); oc.getNodeState(bomiaoEl).should.deep.equal({ 'exist': true, 'visible': true }, 'bomiao->self'); oc.getNodeState(bomiaoEl, 'parent').should.deep.equal({ 'exist': true, 'visible': true }, 'bomiao->parent'); oc.getNodeState(bomiaoEl, 'children').should.deep.equal({ 'exist': false, 'visible': false }, 'bomiao->children'); oc.getNodeState(bomiaoEl, 'siblings').should.deep.equal({ 'exist': true, 'visible': true }, 'bomiao->siblings'); oc.getNodeState(sumiaoEl).should.deep.equal({ 'exist': true, 'visible': true }, 'sumiao->self'); oc.getNodeState(sumiaoEl, 'parent').should.deep.equal({ 'exist': true, 'visible': true }, 'sumiao->parent'); oc.getNodeState(sumiaoEl, 'children').should.deep.equal({ 'exist': true, 'visible': false }, 'sumiao->children'); oc.getNodeState(sumiaoEl, 'siblings').should.deep.equal({ 'exist': true, 'visible': true }, 'sumiao->siblings'); oc.getNodeState(tiehuaEl).should.deep.equal({ 'exist': true, 'visible': false }, 'tiehua->self'); oc.getNodeState(tiehuaEl, 'parent').should.deep.equal({ 'exist': true, 'visible': true }, 'tiehua->parent'); oc.getNodeState(tiehuaEl, 'children').should.deep.equal({ 'exist': true, 'visible': false }, 'tiehua->children'); oc.getNodeState(tiehuaEl, 'siblings').should.deep.equal({ 'exist': true, 'visible': false }, 'tiehua->siblings'); oc.getNodeState(heiheiEl).should.deep.equal({ 'exist': true, 'visible': false }, 'heihei->self'); oc.getNodeState(heiheiEl, 'parent').should.deep.equal({ 'exist': true, 'visible': true }, 'heihei->parent'); oc.getNodeState(heiheiEl, 'children').should.deep.equal({ 'exist': true, 'visible': false }, 'heihei->children'); oc.getNodeState(heiheiEl, 'siblings').should.deep.equal({ 'exist': true, 'visible': false }, 'heihei->siblings'); oc.getNodeState(pangpangEl).should.deep.equal({ 'exist': true, 'visible': false }, 'pangpang->self'); oc.getNodeState(pangpangEl, 'parent').should.deep.equal({ 'exist': true, 'visible': true }, 'pangpang->parent'); oc.getNodeState(pangpangEl, 'children').should.deep.equal({ 'exist': true, 'visible': false }, 'pangpang->children'); oc.getNodeState(pangpangEl, 'siblings').should.deep.equal({ 'exist': true, 'visible': false }, 'pangpang->siblings'); oc.getNodeState(dandanEl).should.deep.equal({ 'exist': true, 'visible': false }, 'dandan->self'); oc.getNodeState(dandanEl, 'parent').should.deep.equal({ 'exist': true, 'visible': false }, 'dandan->parent'); oc.getNodeState(dandanEl, 'children').should.deep.equal({ 'exist': false, 'visible': false }, 'dandan->children'); oc.getNodeState(dandanEl, 'siblings').should.deep.equal({ 'exist': false, 'visible': false }, 'dandan->siblings'); }); it('triggerInitEvent() calls initCompleted with the native chart element', function () { const originalMutationObserver = global.MutationObserver; const originalWindowMutationObserver = window.MutationObserver; const observeSpy = sinon.spy(); const disconnectSpy = sinon.spy(); const initCompletedSpy = sinon.spy(); const fakeMutationObserver = function (callback) { this.observe = function (target, options) { observeSpy(target, options); callback([ { addedNodes: [oc.chart] } ]); }; this.disconnect = disconnectSpy; }; global.MutationObserver = fakeMutationObserver; window.MutationObserver = fakeMutationObserver; oc.options.initCompleted = initCompletedSpy; oc.triggerInitEvent(); observeSpy.should.have.been.calledOnce; observeSpy.firstCall.args[0].should.equal($container); observeSpy.firstCall.args[1].should.deep.equal({ childList: true }); disconnectSpy.should.have.been.calledOnce; initCompletedSpy.should.have.been.calledOnce; initCompletedSpy.firstCall.args[0].should.equal(oc.chart); global.MutationObserver = originalMutationObserver; window.MutationObserver = originalWindowMutationObserver; }); it('triggerInitEvent() triggers init.orgchart on the chart collection', function () { const originalMutationObserver = global.MutationObserver; const originalWindowMutationObserver = window.MutationObserver; const eventSpy = sinon.spy(); const fakeMutationObserver = function (callback) { this.observe = function () { callback([ { addedNodes: [oc.chart] } ]); }; this.disconnect = function () {}; }; global.MutationObserver = fakeMutationObserver; window.MutationObserver = fakeMutationObserver; oc.chart.addEventListener('init.orgchart', eventSpy); oc.triggerInitEvent(); eventSpy.should.have.been.calledOnce; eventSpy.firstCall.args[0].type.should.equal('init.orgchart'); global.MutationObserver = originalMutationObserver; window.MutationObserver = originalWindowMutationObserver; }); it('handleCompactNodes() tags compact nodes by compact-ancestor parity', function () { const compactRootEl = document.createElement('div'); const compactChildEl = document.createElement('div'); const compactGrandchildEl = document.createElement('div'); compactRootEl.className = 'node compact'; compactChildEl.className = 'node compact'; compactGrandchildEl.className = 'node compact'; compactRootEl.appendChild(compactChildEl); compactChildEl.appendChild(compactGrandchildEl); oc.chart.appendChild(compactRootEl); oc.handleCompactNodes(); compactRootEl.classList.contains('even').should.equal(true); compactChildEl.classList.contains('odd').should.equal(true); compactGrandchildEl.classList.contains('even').should.equal(true); }); it('getRelatedNodes()', function () { oc.getRelatedNodes().should.deep.equal([]); oc.getRelatedNodes({}, 'children').should.deep.equal([]); oc.getRelatedNodes(query('.hierarchy', oc.chart), 'children').should.deep.equal([]); oc.getRelatedNodes(query('.node', oc.chart), 'child').should.deep.equal([]); should.equal(oc.getRelatedNodes($laolao, 'parent'), null); oc.getRelatedNodes($laolao, 'children').should.have.members([$bomiao, $sumiao, $hongmiao]); oc.getRelatedNodes($laolao, 'siblings').should.deep.equal([]); oc.getRelatedNodes($bomiao, 'parent').should.equal($laolao); oc.getRelatedNodes($bomiao, 'children').should.have.lengthOf(0); oc.getRelatedNodes($bomiao, 'siblings').should.have.members([$sumiao, $hongmiao]); }); it('getParent()', function () { should.equal(oc.getParent(), null); should.equal(oc.getParent({}), null); should.equal(oc.getParent(query('.hierarchy', oc.chart)), null); should.equal(oc.getParent($laolao), null); oc.getParent($bomiao).should.equal($laolao); oc.getParent($sandan).should.equal($pangpang); }); it('getSiblings()', function () { oc.getSiblings().should.deep.equal([]); oc.getSiblings({}).should.deep.equal([]); oc.getSiblings(query('.hierarchy', oc.chart)).should.deep.equal([]); oc.getSiblings($laolao).should.deep.equal([]); oc.getSiblings($bomiao).should.have.members([$sumiao, $hongmiao]); oc.getSiblings($sandan).should.deep.equal([]); }); it('getChildren()', function () { oc.getChildren().should.deep.equal([]); oc.getChildren({}).should.deep.equal([]); oc.getChildren(query('.hierarchy', oc.chart)).should.deep.equal([]); oc.getChildren($laolao).should.have.members([$bomiao, $sumiao, $hongmiao]); oc.getChildren($bomiao).should.deep.equal([]); oc.getChildren($sumiao).should.have.members([$tiehua, $heihei, $pangpang]); oc.getChildren($sandan).should.deep.equal([]); }); it('hideParent()', function () { const spy = sinon.spy(oc, 'hideParent'); const spy2 = sinon.spy(oc, 'hideSiblings'); oc.hideParent($heihei); setOneTimeListener($sumiao, 'transitionend', null, function (event) { event.parent = parentEl; spy.call(oc, event); }); spy.should.have.been.callCount(2); spy.getCall(0).args[0].should.equal($heihei); spy.getCall(1).args[0].should.equal($sumiao); spy2.should.have.been.callCount(2); spy2.getCall(0).args[0].should.equal($heihei); spy2.getCall(1).args[0].should.equal($sumiao); }); it('hideParent() hides the transitioned parent node', function () { oc.hideParent($sumiao); triggerEvent($laolao, 'transitionend'); $laolao.classList.contains('sliding').should.equal(false); $laolao.classList.contains('hidden').should.equal(true); }); it('hideParent() clears inAjax when the parent node contains a spinner', function () { const spinnerEl = document.createElement('i'); spinnerEl.className = 'spinner'; $sumiao.appendChild(spinnerEl); setState(oc.chart, 'inAjax', true); oc.hideParent($heihei); getState(oc.chart, 'inAjax').should.be.false; spinnerEl.remove(); }); it('hideParentEnd()', function () { const parentEl = firstSibling(closestElement($heihei, '.nodes'), '.node'); addClasses($sumiao, 'sliding slide-down'); oc.hideParentEnd({ animatedNode: $sumiao, parent: parentEl }); matchesSelector($sumiao, 'sliding').should.be.false; matchesSelector(parentEl, '.hidden').should.be.true; }); it('hideParentEnd() accepts native animated and parent payloads', function () { addClasses($sumiao, 'sliding'); removeClasses($sumiao, 'hidden'); oc.hideParentEnd({ animatedNode: $sumiao, parent: $sumiao }); $sumiao.classList.contains('sliding').should.equal(false); $sumiao.classList.contains('hidden').should.equal(true); }); it('hideParentEnd() accepts wrapped native animated and parent payloads', function () { addClasses($sumiao, 'sliding'); oc.hideParentEnd({ originalEvent: { target: $sumiao, parent: $heihei } }); $sumiao.classList.contains('sliding').should.equal(false); $heihei.classList.contains('hidden').should.equal(true); }); it('showParent()', function () { const spy = sinon.spy(oc, 'repaint'); addClasses(closestElement($heihei, '.hierarchy'), 'isAncestorsCollapsed'); addClasses($sumiao, 'slide-down hidden'); addClasses(closestElement($sumiao, '.hierarchy'), 'isAncestorsCollapsed'); addClasses($laolao, 'slide-down hidden'); oc.showParent($heihei); spy.should.have.been.called; matchesSelector(closestElement($heihei, '.hierarchy'), '.isAncestorsCollapsed').should.be.false; matchesSelector($sumiao, '.slide-down, .hidden').should.be.false; matchesSelector(closestElement($sumiao, '.hierarchy'), '.isAncestorsCollapsed').should.be.true; matchesSelector($laolao, '.slide-down.hidden').should.be.true; matchesSelector($sumiao, '.sliding').should.be.true; matchesSelector($sumiao, '.slide-down').should.be.false; }); it('showParentEnd()', function () { const spy = sinon.spy(oc, 'showParentEnd'); const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); addClasses($sumiao, 'sliding'); removeClasses($sumiao, 'slide-down'); setOneTimeListener($sumiao, 'transitionend', null, function (event) { event.node = $heihei; spy.call(oc, event); }); triggerEvent($sumiao, 'transitionend'); spy.should.have.been.called; matchesSelector($sumiao, '.sliding').should.be.false; spy2.should.have.been.calledWith($heihei); spy3.should.not.have.been.called; }); it('showParentEnd() accepts native animated and node payloads', function () { addClasses($sumiao, 'sliding'); oc.showParentEnd({ animatedNode: $sumiao, node: $heihei }); $sumiao.classList.contains('sliding').should.equal(false); }); it('showParentEnd() accepts wrapped native animated and node payloads', function () { addClasses($sumiao, 'sliding'); oc.showParentEnd({ originalEvent: { target: $sumiao, node: $heihei } }); $sumiao.classList.contains('sliding').should.equal(false); }); it('hideChildren()', function () { const spy = sinon.spy(oc, 'repaint'); oc.hideChildren($sumiao); spy.should.have.been.called; matchesSelector(closestElement($sumiao, '.hierarchy'), '.isChildrenCollapsed').should.be.true; childElements(firstSibling($sumiao, '.nodes'), '.isCollapsedDescendant').should.lengthOf(3); matchesSelector($tiehua, '.sliding.slide-up').should.be.true; matchesSelector($heihei, '.sliding.slide-up').should.be.true; matchesSelector($pangpang, '.sliding.slide-up').should.be.true; matchesSelector($erdan, '.sliding.slide-up').should.be.true; }); it('hideChildrenEnd()', function () { const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); addClasses($tiehua, 'sliding slide-up'); oc.hideChildrenEnd({ animatedNodes: $tiehua, node: $sumiao }); matchesSelector($tiehua, '.sliding').should.be.false; matchesSelector(closestElement($tiehua, '.nodes'), '.hidden').should.be.true; spy2.should.have.been.calledWith($sumiao); spy3.should.not.have.been.called; }); it('hideChildrenEnd() accepts direct animated and node payloads', function () { const bottomEdgeEl = query('.bottomEdge', $sumiao); const verticalSpy = sinon.spy(oc, 'switchVerticalArrow'); removeClasses($tiehua, 'hidden'); addClasses($tiehua, 'sliding'); removeClasses(closestElement($tiehua, '.nodes'), 'hidden'); bottomEdgeEl.classList.add(oc.options.icons.collapseToDown); oc.hideChildrenEnd({ animatedNodes: [$tiehua], node: $sumiao }); $tiehua.classList.contains('sliding').should.equal(false); closestElement($tiehua, '.nodes').classList.contains('hidden').should.equal(true); verticalSpy.should.have.been.calledWith(bottomEdgeEl); verticalSpy.restore(); }); it('hideChildrenEnd() accepts wrapped native animated and node payloads', function () { const bottomEdgeEl = query('.bottomEdge', $sumiao); const verticalSpy = sinon.spy(oc, 'switchVerticalArrow'); removeClasses($tiehua, 'hidden'); addClasses($tiehua, 'sliding'); removeClasses(closestElement($tiehua, '.nodes'), 'hidden'); bottomEdgeEl.classList.add(oc.options.icons.collapseToDown); oc.hideChildrenEnd({ originalEvent: { animatedNodes: [$tiehua], node: $sumiao } }); $tiehua.classList.contains('sliding').should.equal(false); closestElement($tiehua, '.nodes').classList.contains('hidden').should.equal(true); verticalSpy.should.have.been.calledWith(bottomEdgeEl); verticalSpy.restore(); }); it('showChildren()', function () { const spy = sinon.spy(oc, 'repaint'); const descendantsWrapperEl = firstSibling($sumiao, '.nodes'); queryAll('.node', descendantsWrapperEl).forEach(function (nodeEl) { addClasses(nodeEl, 'slide-up'); }); queryAll('.nodes', descendantsWrapperEl).forEach(function (nodeEl) { addClasses(nodeEl, 'hidden'); }); addClasses(descendantsWrapperEl, 'hidden'); oc.showChildren($sumiao); spy.should.have.been.calledWith($tiehua); matchesSelector($tiehua, '.sliding:not(.slide-up)').should.be.true; matchesSelector($heihei, '.sliding:not(.slide-up)').should.be.true; matchesSelector($pangpang, '.sliding:not(.slide-up)').should.be.true; matchesSelector($erdan, '.slide-up:not(.sliding)').should.be.true; }); it('showChildrenEnd()', function () { const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); addClasses($tiehua, 'sliding'); oc.showChildrenEnd({ node: $sumiao, animatedNodes: $tiehua }); matchesSelector($tiehua, '.sliding').should.be.false; spy2.should.have.been.calledWith($sumiao); spy3.should.not.have.been.called; }); it('showChildrenEnd() accepts direct animated and node payloads', function () { addClasses($tiehua, 'sliding'); oc.showChildrenEnd({ node: $sumiao, animatedNodes: [$tiehua] }); $tiehua.classList.contains('sliding').should.equal(false); }); it('showChildrenEnd() accepts wrapped native animated and node payloads', function () { addClasses($tiehua, 'sliding'); oc.showChildrenEnd({ originalEvent: { node: $sumiao, animatedNodes: [$tiehua] } }); $tiehua.classList.contains('sliding').should.equal(false); }); it('showSiblingsEnd() accepts direct visible and node payloads', function () { const topEdgeEl = query('.topEdge', $heihei); const switchSpy = sinon.spy(oc, 'switchHorizontalArrow'); addClasses($tiehua, 'sliding'); addClasses($pangpang, 'sliding'); topEdgeEl.classList.add(oc.options.icons.expandToUp); oc.showSiblingsEnd({ node: $heihei, visibleNodes: [$tiehua, $pangpang] }); $tiehua.classList.contains('sliding').should.equal(false); $pangpang.classList.contains('sliding').should.equal(false); switchSpy.should.have.been.calledWith($heihei); topEdgeEl.classList.contains(oc.options.icons.expandToUp).should.equal(false); topEdgeEl.classList.contains(oc.options.icons.collapseToDown).should.equal(true); switchSpy.restore(); }); it('showSiblingsEnd() accepts wrapped native visible and node payloads', function () { const topEdgeEl = query('.topEdge', $heihei); const switchSpy = sinon.spy(oc, 'switchHorizontalArrow'); addClasses($tiehua, 'sliding'); addClasses($pangpang, 'sliding'); topEdgeEl.classList.add(oc.options.icons.expandToUp); oc.showSiblingsEnd({ originalEvent: { node: $heihei, visibleNodes: [$tiehua, $pangpang] } }); $tiehua.classList.contains('sliding').should.equal(false); $pangpang.classList.contains('sliding').should.equal(false); switchSpy.should.have.been.calledWith($heihei); topEdgeEl.classList.contains(oc.options.icons.expandToUp).should.equal(false); topEdgeEl.classList.contains(oc.options.icons.collapseToDown).should.equal(true); switchSpy.restore(); }); it('showRelatedParentEnd() removes sliding from a native animated node payload', function () { addClasses($sumiao, 'sliding'); oc.showRelatedParentEnd({ animatedNode: $sumiao }); $sumiao.classList.contains('sliding').should.equal(false); }); it('showRelatedParentEnd() accepts a wrapped native animated node payload', function () { addClasses($sumiao, 'sliding'); oc.showRelatedParentEnd({ originalEvent: { target: $sumiao } }); $sumiao.classList.contains('sliding').should.equal(false); }); it('showSiblings() accepts a native node and reveals the requested side with animation', function () { const repaintSpy = sinon.spy(oc, 'repaint'); addClasses(closestElement($tiehua, '.hierarchy'), 'hidden isCollapsedSibling left-sibs'); addClasses($tiehua, 'slide-right'); addClasses(closestElement($heihei, '.hierarchy'), 'isSiblingsCollapsed left-sibs isAncestorsCollapsed'); addClasses($sumiao, 'hidden slide-down'); oc.showSiblings($heihei, 'left'); repaintSpy.should.have.been.calledWith($sumiao); closestElement($tiehua, '.hierarchy').classList.contains('hidden').should.equal(false); closestElement($tiehua, '.hierarchy').classList.contains('isCollapsedSibling').should.equal(false); closestElement($heihei, '.hierarchy').classList.contains('left-sibs').should.equal(false); closestElement($heihei, '.hierarchy').classList.contains('isSiblingsCollapsed').should.equal(false); $tiehua.classList.contains('sliding').should.equal(true); $tiehua.classList.contains('slide-right').should.equal(false); $sumiao.classList.contains('hidden').should.equal(false); $sumiao.classList.contains('sliding').should.equal(true); $sumiao.classList.contains('slide-down').should.equal(false); repaintSpy.restore(); }); describe('hideSiblings()', function () { context('when passing only one parameter -- node', function () { it('should hide all the sibling nodes and their descendants', function () { oc.hideSiblings($heihei); matchesSelector($tiehua, '.sliding.slide-right').should.be.true; matchesSelector($dandan, '.sliding.slide-right').should.be.true; matchesSelector($pangpang, '.sliding.slide-left').should.be.true; matchesSelector($sandan, '.sliding.slide-left').should.be.true; matchesSelector(closestElement($heihei, '.hierarchy'), '.isSiblingsCollapsed').should.be.true; siblingElements(closestElement($heihei, '.hierarchy'), '.isChildrenCollapsed.isCollapsedSibling').should.lengthOf(2); }); it('clears inAjax when a sibling hierarchy contains a spinner', function () { const spinnerEl = document.createElement('i'); spinnerEl.className = 'spinner'; closestElement($tiehua, '.hierarchy').appendChild(spinnerEl); setState(oc.chart, 'inAjax', true); oc.hideSiblings($heihei); getState(oc.chart, 'inAjax').should.be.false; spinnerEl.remove(); }); }); context('when passing two parameters -- node and direction', function () { it('hide the left side sibling nodes and their descendants', function () { oc.hideSiblings($heihei, 'left'); matchesSelector(closestElement($heihei, '.hierarchy'), '.isSiblingsCollapsed.left-sibs').should.be.true; matchesSelector(closestElement($heihei, '.hierarchy').previousElementSibling, '.isChildrenCollapsed.isCollapsedSibling').should.be.true; matchesSelector(closestElement($heihei, '.hierarchy').nextElementSibling, '.isChildrenCollapsed, .isCollapsedSibling').should.be.false; matchesSelector($tiehua, '.sliding.slide-right').should.be.true; matchesSelector($dandan, '.sliding.slide-right').should.be.true; matchesSelector($pangpang, '.sliding').should.be.false; }); it('hide the right side sibling nodes and their descendants', function () { oc.hideSiblings($heihei, 'right'); matchesSelector(closestElement($heihei, '.hierarchy'), '.isSiblingsCollapsed.right-sibs').should.be.true; matchesSelector(closestElement($heihei, '.hierarchy').nextElementSibling, '.isChildrenCollapsed.isCollapsedSibling').should.be.true; matchesSelector(closestElement($heihei, '.hierarchy').previousElementSibling, '.isChildrenCollapsed, .isCollapsedSibling').should.be.false; matchesSelector($pangpang, '.sliding.slide-left').should.be.true; matchesSelector($sandan, '.sliding.slide-left').should.be.true; matchesSelector($tiehua, '.sliding').should.be.false; }); }); }); describe('hideSiblingsEnd()', function () { context('when invoking transitionend event without specifying direction', function () { it('clean up final classList for hidden siblings', function () { const spy = sinon.spy(oc, 'hideSiblingsEnd'); const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); const nodeContainerEl = closestElement($heihei, '.hierarchy'); oc.hideSiblings($heihei); setOneTimeListener($tiehua, 'transitionend', { 'node': $heihei, 'nodeContainer': nodeContainerEl, 'direction': undefined, 'animatedNodes': $tiehua }, spy.bind(oc)); triggerEvent($tiehua, 'transitionend'); spy.should.have.been.called; matchesSelector($tiehua, '.slide-right:not(.sliding)').should.be.true; matchesSelector($dandan, '.slide-up:not(.sliding, .slide-right)').should.be.true; matchesSelector($pangpang, '.slide-left:not(.sibling)').should.be.true; matchesSelector($sandan, '.slide-up:not(.sliding, .slide-left)').should.be.true; siblingElements(nodeContainerEl, '.hidden').should.lengthOf(2); siblingElements(nodeContainerEl).reduce(function (count, siblingEl) { return count + queryAll('.nodes.hidden', siblingEl).length; }, 0).should.equal(2); spy2.should.have.been.calledWith($heihei); spy3.should.not.have.been.called; }); }); context('when invoking transitionend event with specifying direction', function () { it('clean up final classList for left side hidden siblings', function () { const spy = sinon.spy(oc, 'hideSiblingsEnd'); const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); const nodeContainerEl = closestElement($heihei, '.hierarchy'); oc.hideSiblings($heihei, 'left'); setOneTimeListener($tiehua, 'transitionend', { 'node': $heihei, 'nodeContainer': nodeContainerEl, 'direction': 'left', 'animatedNodes': $tiehua }, spy.bind(oc)); triggerEvent($tiehua, 'transitionend'); spy.should.have.been.called; matchesSelector($tiehua, '.slide-right:not(.sliding)').should.be.true; matchesSelector($dandan, '.slide-up:not(.slide-right)').should.be.true; matchesSelector($pangpang, '.slide-left').should.be.false; matchesSelector($sandan, '.slide-up').should.be.false; previousSiblings(nodeContainerEl, '.hidden').should.lengthOf(1); previousSiblings(nodeContainerEl).reduce(function (count, siblingEl) { return count + queryAll('.nodes.hidden', siblingEl).length; }, 0).should.equal(1); nextSiblings(nodeContainerEl).reduce(function (count, siblingEl) { return count + queryAll('.nodes.hidden', siblingEl).length; }, 0).should.equal(0); spy2.should.have.been.calledWith($heihei); spy3.should.not.have.been.called; }); it('clean up final classList for right side hidden siblings', function () { const spy2 = sinon.spy(oc, 'isInAction'); const spy3 = sinon.spy(oc, 'switchVerticalArrow'); const nodeContainerEl = closestElement($heihei, '.hierarchy'); oc.hideSiblings($heihei, 'right'); oc.hideSiblingsEnd({ node: $heihei, nodeContainer: nodeContainerEl, direction: 'right', animatedNodes: $tiehua }); matchesSelector($tiehua, '.slide-right').should.be.false; matchesSelector($dandan, '.slide-up').should.be.false; matchesSelector($pangpang, '.slide-left:not(.slidiing)').should.be.true; matchesSelector($sandan, '.slide-up:not(.slide-left)').should.be.true; nextSiblings(nodeContainerEl, '.hidden').should.lengthOf(1); nextSiblings(nodeContainerEl).reduce(function (count, siblingEl) { return count + queryAll('.nodes.hidden', siblingEl).length; }, 0).should.equal(1); previousSiblings(nodeContainerEl).reduce(function (count, siblingEl) { return count + queryAll('.nodes.hidden', siblingEl).length; }, 0).should.equal(0); spy2.should.have.been.calledWith($heihei); spy3.should.not.have.been.called; }); it('accepts direct node, container, and animated payloads', function () { const horizontalSpy = sinon.spy(oc, 'switchHorizontalArrow'); const nodeContainerEl = closestElement($heihei, '.hierarchy'); oc.hideSiblings($heihei, 'left'); addClasses($tiehua, 'sliding'); removeClasses(closestElement($tiehua, '.nodes'), 'hidden'); query('.leftEdge', $heihei).classList.add(oc.options.icons.collapseToLeft); oc.hideSiblingsEnd({ node: $heihei, nodeContainer: nodeContainerEl, direction: 'left', animatedNodes: [$tiehua] }); $tiehua.classList.contains('sliding').should.equal(false); nodeContainerEl.previousElementSibling.classList.contains('hidden').should.equal(true); horizontalSpy.should.have.been.calledWith($heihei); horizontalSpy.restore(); }); it('accepts wrapped native node, container, and animated payloads', function () { const horizontalSpy = sinon.spy(oc, 'switchHorizontalArrow'); const nodeContainerEl = closestElement($heihei, '.hierarchy'); oc.hideSiblings($heihei, 'left'); addClasses($tiehua, 'sliding'); removeClasses(closestElement($tiehua, '.nodes'), 'hidden'); query('.leftEdge', $heihei).classList.add(oc.options.icons.collapseToLeft); oc.hideSiblingsEnd({ originalEvent: { node: $heihei, nodeContainer: nodeContainerEl, direction: 'left', animatedNodes: [$tiehua] } }); $tiehua.classList.contains('sliding').should.equal(false); nodeContainerEl.previousElementSibling.classList.contains('hidden').should.equal(true); horizontalSpy.should.have.been.calledWith($heihei); horizontalSpy.restore(); }); }); }); it('nodeEnterLeaveHandler() accepts a native node payload for relationship checks', function () { const spy = sinon.spy(oc, 'switchHorizontalArrow'); oc.nodeEnterLeaveHandler({ type: 'mouseenter', node: $sumiao }); spy.should.have.been.calledWith($sumiao); }); it('nodeEnterLeaveHandler() removes arrow classes from a native node payload on mouseleave', function () { const nodeEl = $sumiao; Array.from(nodeEl.querySelectorAll('.edge')).forEach(function (edgeEl) { edgeEl.classList.add( oc.options.icons.expandToUp, oc.options.icons.collapseToDown, oc.options.icons.collapseToLeft, oc.options.icons.expandToRight ); }); oc.nodeEnterLeaveHandler({ type: 'mouseleave', node: nodeEl }); Array.from(nodeEl.querySelectorAll('.edge')).forEach(function (edgeEl) { edgeEl.classList.contains(oc.options.icons.expandToUp).should.equal(false); edgeEl.classList.contains(oc.options.icons.collapseToDown).should.equal(false); edgeEl.classList.contains(oc.options.icons.collapseToLeft).should.equal(false); edgeEl.classList.contains(oc.options.icons.expandToRight).should.equal(false); }); }); it('nodeEnterLeaveHandler() accepts wrapped native node payloads', function () { const spy = sinon.spy(oc, 'switchHorizontalArrow'); oc.nodeEnterLeaveHandler({ originalEvent: { type: 'mouseenter', currentTarget: $sumiao } }); spy.should.have.been.calledWith($sumiao); spy.restore(); }); it('HideFirstParentEnd() accepts a native top edge payload', function () { const topEdgeEl = query('.topEdge', $heihei); const verticalSpy = sinon.spy(oc, 'switchVerticalArrow'); const horizontalSpy = sinon.spy(oc, 'switchHorizontalArrow'); topEdgeEl.classList.add(oc.options.icons.expandToUp); oc.HideFirstParentEnd({ topEdge: topEdgeEl }); verticalSpy.should.have.been.calledWith(topEdgeEl); horizontalSpy.should.have.been.calledWith($heihei); verticalSpy.restore(); horizontalSpy.restore(); }); it('HideFirstParentEnd() accepts a wrapped native top edge payload', function () { const topEdgeEl = query('.topEdge', $heihei); const verticalSpy = sinon.spy(oc, 'switchVerticalArrow'); const horizontalSpy = sinon.spy(oc, 'switchHorizontalArrow'); topEdgeEl.classList.add(oc.options.icons.expandToUp); oc.HideFirstParentEnd({ originalEvent: { topEdge: topEdgeEl } }); verticalSpy.should.have.been.calledWith(topEdgeEl); horizontalSpy.should.have.been.calledWith($heihei); verticalSpy.restore(); horizontalSpy.restore(); }); it('nodeClickHandler() focuses the native node payload and clears previous focus', function () { addClasses($bomiao, 'focused'); oc.nodeClickHandler({ node: $sumiao }); $bomiao.classList.contains('focused').should.equal(false); $sumiao.classList.contains('focused').should.equal(true); }); it('nodeClickHandler() focuses a wrapped native node payload', function () { addClasses($bomiao, 'focused'); oc.nodeClickHandler({ originalEvent: { currentTarget: $sumiao } }); $bomiao.classList.contains('focused').should.equal(false); $sumiao.classList.contains('focused').should.equal(true); }); it('init() clears focused nodes when clicking the chart background', function () { addClasses($bomiao, 'focused'); addClasses($sumiao, 'focused'); oc.chart.dispatchEvent(new window.MouseEvent('click', { bubbles: true })); $bomiao.classList.contains('focused').should.equal(false); $sumiao.classList.contains('focused').should.equal(false); }); it('init() clears focused nodes from a wrapped native background click payload', function () { const wrappedClickEvent = new window.Event('click', { bubbles: true }); addClasses($bomiao, 'focused'); addClasses($sumiao, 'focused'); wrappedClickEvent.originalEvent = { target: oc.chart }; oc.chart.dispatchEvent(wrappedClickEvent); $bomiao.classList.contains('focused').should.equal(false); $sumiao.classList.contains('focused').should.equal(false); }); it('init() appends the export button only once', function () { let oc2; Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (exportButtonEl) { exportButtonEl.remove(); }); oc2 = new OrgChart({ chartContainer: '#chart-container', data: ds, exportButton: true }); document.querySelectorAll('.oc-export-btn').length.should.equal(1); oc2.init(); document.querySelectorAll('.oc-export-btn').length.should.equal(1); Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (exportButtonEl) { exportButtonEl.remove(); }); }); it('attachExportButton() creates a native export button that calls export()', function () { const exportStub = sinon.stub(oc, 'export'); Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (exportButtonEl) { exportButtonEl.remove(); }); oc.attachExportButton(); document.querySelectorAll('.oc-export-btn').length.should.equal(1); document.querySelector('.oc-export-btn').dispatchEvent(new window.MouseEvent('click', { bubbles: true })); exportStub.should.have.been.calledOnce; exportStub.restore(); Array.from(document.querySelectorAll('.oc-export-btn')).forEach(function (exportButtonEl) { exportButtonEl.remove(); }); }); it('topEdgeClickHandler() passes a native node to hideParent()', function () { const spy = sinon.spy(oc, 'hideParent'); const spy2 = sinon.spy(oc, 'triggerHideEvent'); oc.topEdgeClickHandler({ edge: query('.topEdge', $heihei), node: $heihei }); spy.should.have.been.calledWith($heihei); spy2.should.have.been.called; }); it('topEdgeClickHandler() accepts wrapped native edge payloads', function () { const spy = sinon.spy(oc, 'hideParent'); oc.topEdgeClickHandler({ originalEvent: { currentTarget: $heihei, target: query('.topEdge', $heihei) } }); spy.should.have.been.calledWith($heihei); spy.restore(); }); it('bottomEdgeClickHandler() passes a native node to hideChildren()', function () { const spy = sinon.spy(oc, 'hideChildren'); const spy2 = sinon.spy(oc, 'triggerHideEvent'); oc.bottomEdgeClickHandler({ edge: query('.bottomEdge', $sumiao), node: $sumiao }); spy.should.have.been.calledWith($sumiao); spy2.should.have.been.called; }); it('bottomEdgeClickHandler() accepts wrapped native node payloads', function () { const spy = sinon.spy(oc, 'hideChildren'); oc.bottomEdgeClickHandler({ originalEvent: { currentTarget: $sumiao } }); spy.should.have.been.calledWith($sumiao); spy.restore(); }); it('hEdgeClickHandler() passes a native node to hideSiblings()', function () { const spy = sinon.spy(oc, 'hideSiblings'); const spy2 = sinon.spy(oc, 'triggerHideEvent'); oc.hEdgeClickHandler({ edge: query('.leftEdge', $sumiao), node: $sumiao }); spy.should.have.been.calledWith($sumiao); spy2.should.have.been.called; }); it('hEdgeClickHandler() accepts wrapped native edge payloads', function () { const spy = sinon.spy(oc, 'hideSiblings'); oc.hEdgeClickHandler({ originalEvent: { currentTarget: $sumiao, target: query('.leftEdge', $sumiao) } }); spy.should.have.been.calledWith($sumiao); spy.restore(); }); it('hEdgeClickHandler() restores the collapsed parent after topEdgeClickHandler()', function () { oc.topEdgeClickHandler({ edge: query('.topEdge', $sumiao), node: $sumiao }); triggerEvent($bomiao, 'transitionend'); triggerEvent($laolao, 'transitionend'); oc.hEdgeClickHandler({ edge: query('.leftEdge', $sumiao), node: $sumiao }); closestElement($sumiao, '.hierarchy').classList.contains('isAncestorsCollapsed').should.equal(false); $laolao.classList.contains('hidden').should.equal(false); $laolao.classList.contains('slide-down').should.equal(false); $laolao.classList.contains('sliding').should.equal(true); }); it('startLoading() and endLoading() accept a native edge node', function () { const edgeEl = query('.bottomEdge', $sumiao); oc.startLoading(edgeEl).should.be.true; matchesSelector(edgeEl, '.hidden').should.be.true; queryAll('.spinner', edgeEl.parentElement).should.have.lengthOf(1); getState(oc.chart, 'inAjax').should.be.true; oc.endLoading(edgeEl); matchesSelector(edgeEl, '.hidden').should.be.false; queryAll('.spinner', edgeEl.parentElement).should.have.lengthOf(0); getState(oc.chart, 'inAjax').should.be.false; }); it('startLoading() and endLoading() toggle native export buttons', function () { const edgeEl = query('.bottomEdge', $sumiao); const exportButtonEl = document.createElement('button'); exportButtonEl.className = 'oc-export-btn'; document.body.appendChild(exportButtonEl); oc.startLoading(edgeEl); exportButtonEl.disabled.should.equal(true); oc.endLoading(edgeEl); exportButtonEl.disabled.should.equal(false); exportButtonEl.remove(); }); it('stopAjax() accepts a native node level element', function () { const nodeLevelEl = oc.getChildren($sumiao)[0].closest('.nodes'); const spinnerEl = document.createElement('i'); spinnerEl.className = 'spinner'; nodeLevelEl.appendChild(spinnerEl); setState(oc.chart, 'inAjax', true); oc.stopAjax(nodeLevelEl); getState(oc.chart, 'inAjax').should.be.false; spinnerEl.remove(); }); it('backToLooseHandler() and backToCompactHandler() toggle compact mode classes', function () { const compactNodeEl = createElementFromHtml('<div class="node compact"><i class="backToCompactSymbol hidden"></i><i class="backToLooseSymbol"></i></div>'); $container.appendChild(compactNodeEl); oc.backToLooseHandler({ compactNode: compactNodeEl }); matchesSelector(compactNodeEl, '.looseMode').should.be.true; matchesSelector(query('.backToLooseSymbol', compactNodeEl), '.hidden').should.be.true; matchesSelector(query('.backToCompactSymbol', compactNodeEl), '.hidden').should.be.false; oc.backToCompactHandler({ compactNode: compactNodeEl }); matchesSelector(compactNodeEl, '.looseMode').should.be.false; matchesSelector(query('.backToCompactSymbol', compactNodeEl), '.hidden').should.be.true; matchesSelector(query('.backToLooseSymbol', compactNodeEl), '.hidden').should.be.false; }); it('backToLooseHandler() and backToCompactHandler() accept wrapped native compact-node payloads', function () { const compactNodeEl = createElementFromHtml('<div class="node compact"><i class="backToCompactSymbol hidden"></i><i class="backToLooseSymbol"></i></div>'); $container.appendChild(compactNodeEl); oc.back