diff options
| author | Johann-S <[email protected]> | 2019-07-22 15:24:17 +0200 |
|---|---|---|
| committer | Johann-S <[email protected]> | 2019-07-23 14:23:50 +0200 |
| commit | 233f3fb1ce766d73273276e59a75ccec08673573 (patch) | |
| tree | 43b5c32ab827e50de2073a7ab0df3c5e51489e2b /js/src/tab | |
| parent | e1b5d8471a6d8004e32cd6e9189f49f7f39e2766 (diff) | |
| download | bootstrap-233f3fb1ce766d73273276e59a75ccec08673573.tar.xz bootstrap-233f3fb1ce766d73273276e59a75ccec08673573.zip | |
rewrite tab unit tests
Diffstat (limited to 'js/src/tab')
| -rw-r--r-- | js/src/tab/tab.js | 267 | ||||
| -rw-r--r-- | js/src/tab/tab.spec.js | 593 |
2 files changed, 860 insertions, 0 deletions
diff --git a/js/src/tab/tab.js b/js/src/tab/tab.js new file mode 100644 index 000000000..e882ff1d2 --- /dev/null +++ b/js/src/tab/tab.js @@ -0,0 +1,267 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): tab.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + makeArray, + reflow +} from '../util/index' +import Data from '../dom/data' +import EventHandler from '../dom/event-handler' +import SelectorEngine from '../dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'tab' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.tab' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const Event = { + HIDE: `hide${EVENT_KEY}`, + HIDDEN: `hidden${EVENT_KEY}`, + SHOW: `show${EVENT_KEY}`, + SHOWN: `shown${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + DROPDOWN_MENU: 'dropdown-menu', + ACTIVE: 'active', + DISABLED: 'disabled', + FADE: 'fade', + SHOW: 'show' +} + +const Selector = { + DROPDOWN: '.dropdown', + NAV_LIST_GROUP: '.nav, .list-group', + ACTIVE: '.active', + ACTIVE_UL: ':scope > li > .active', + DATA_TOGGLE: '[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]', + DROPDOWN_TOGGLE: '.dropdown-toggle', + DROPDOWN_ACTIVE_CHILD: ':scope > .dropdown-menu .active' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Tab { + constructor(element) { + this._element = element + + Data.setData(this._element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + show() { + if (this._element.parentNode && + this._element.parentNode.nodeType === Node.ELEMENT_NODE && + this._element.classList.contains(ClassName.ACTIVE) || + this._element.classList.contains(ClassName.DISABLED)) { + return + } + + let target + let previous + const listElement = SelectorEngine.closest(this._element, Selector.NAV_LIST_GROUP) + const selector = getSelectorFromElement(this._element) + + if (listElement) { + const itemSelector = listElement.nodeName === 'UL' || listElement.nodeName === 'OL' ? Selector.ACTIVE_UL : Selector.ACTIVE + previous = makeArray(SelectorEngine.find(itemSelector, listElement)) + previous = previous[previous.length - 1] + } + + let hideEvent = null + + if (previous) { + hideEvent = EventHandler.trigger(previous, Event.HIDE, { + relatedTarget: this._element + }) + } + + const showEvent = EventHandler.trigger(this._element, Event.SHOW, { + relatedTarget: previous + }) + + if (showEvent.defaultPrevented || + hideEvent !== null && hideEvent.defaultPrevented) { + return + } + + if (selector) { + target = SelectorEngine.findOne(selector) + } + + this._activate( + this._element, + listElement + ) + + const complete = () => { + EventHandler.trigger(previous, Event.HIDDEN, { + relatedTarget: this._element + }) + EventHandler.trigger(this._element, Event.SHOWN, { + relatedTarget: previous + }) + } + + if (target) { + this._activate(target, target.parentNode, complete) + } else { + complete() + } + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + this._element = null + } + + // Private + + _activate(element, container, callback) { + const activeElements = container && (container.nodeName === 'UL' || container.nodeName === 'OL') ? + SelectorEngine.find(Selector.ACTIVE_UL, container) : + SelectorEngine.children(container, Selector.ACTIVE) + + const active = activeElements[0] + const isTransitioning = callback && + (active && active.classList.contains(ClassName.FADE)) + + const complete = () => this._transitionComplete( + element, + active, + callback + ) + + if (active && isTransitioning) { + const transitionDuration = getTransitionDurationFromElement(active) + active.classList.remove(ClassName.SHOW) + + EventHandler.one(active, TRANSITION_END, complete) + emulateTransitionEnd(active, transitionDuration) + } else { + complete() + } + } + + _transitionComplete(element, active, callback) { + if (active) { + active.classList.remove(ClassName.ACTIVE) + + const dropdownChild = SelectorEngine.findOne(Selector.DROPDOWN_ACTIVE_CHILD, active.parentNode) + + if (dropdownChild) { + dropdownChild.classList.remove(ClassName.ACTIVE) + } + + if (active.getAttribute('role') === 'tab') { + active.setAttribute('aria-selected', false) + } + } + + element.classList.add(ClassName.ACTIVE) + if (element.getAttribute('role') === 'tab') { + element.setAttribute('aria-selected', true) + } + + reflow(element) + + if (element.classList.contains(ClassName.FADE)) { + element.classList.add(ClassName.SHOW) + } + + if (element.parentNode && element.parentNode.classList.contains(ClassName.DROPDOWN_MENU)) { + const dropdownElement = SelectorEngine.closest(element, Selector.DROPDOWN) + + if (dropdownElement) { + makeArray(SelectorEngine.find(Selector.DROPDOWN_TOGGLE)) + .forEach(dropdown => dropdown.classList.add(ClassName.ACTIVE)) + } + + element.setAttribute('aria-expanded', true) + } + + if (callback) { + callback() + } + } + + // Static + + static _jQueryInterface(config) { + return this.each(function () { + const data = Data.getData(this, DATA_KEY) || new Tab(this) + + if (typeof config === 'string') { + if (typeof data[config] === 'undefined') { + throw new TypeError(`No method named "${config}"`) + } + + data[config]() + } + }) + } + + static _getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler.on(document, Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) { + event.preventDefault() + + const data = Data.getData(this, DATA_KEY) || new Tab(this) + data.show() +}) + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .tab to jQuery only if jQuery is present + */ +/* istanbul ignore if */ +if (typeof $ !== 'undefined') { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Tab._jQueryInterface + $.fn[NAME].Constructor = Tab + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Tab._jQueryInterface + } +} + +export default Tab diff --git a/js/src/tab/tab.spec.js b/js/src/tab/tab.spec.js new file mode 100644 index 000000000..3fae366d1 --- /dev/null +++ b/js/src/tab/tab.spec.js @@ -0,0 +1,593 @@ +import Tab from './tab' + +/** Test helpers */ +import { getFixture, clearFixture, jQueryMock } from '../../tests/helpers/fixture' + +describe('Tab', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Tab.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('show', () => { + it('should activate element by tab id', done => { + fixtureEl.innerHTML = [ + '<ul class="nav">', + ' <li><a href="#home" role="tab">Home</a></li>', + ' <li><a id="triggerProfile" role="tab" href="#profile">Profile</a></li>', + '</ul>', + '<ul><li id="home"/><li id="profile"/></ul>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + done() + }) + + tab.show() + }) + + it('should activate element by tab id in ordered list', done => { + fixtureEl.innerHTML = [ + '<ol class="nav nav-pills">', + ' <li><a href="#home">Home</a></li>', + ' <li><a id="triggerProfile" href="#profile">Profile</a></li>', + '</ol>', + '<ol><li id="home"/><li id="profile"/></ol>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + done() + }) + + tab.show() + }) + + it('should activate element by tab id in nav list', done => { + fixtureEl.innerHTML = [ + '<nav class="nav">', + ' <a href="#home">Home</a>', + ' <a id="triggerProfile" href="#profile">Profile</a>', + '</nav>', + '<nav><div id="home"></div><div id="profile"></div></nav>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + done() + }) + + tab.show() + }) + + it('should activate element by tab id in list group', done => { + fixtureEl.innerHTML = [ + '<div class="list-group">', + ' <a href="#home">Home</a>', + ' <a id="triggerProfile" href="#profile">Profile</a>', + '</div>', + '<nav><div id="home"></div><div id="profile"></div></nav>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + done() + }) + + tab.show() + }) + + it('should not fire shown when show is prevented', done => { + fixtureEl.innerHTML = '<div class="nav"></div>' + + const navEl = fixtureEl.querySelector('div') + const tab = new Tab(navEl) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + done() + }, 30) + } + + navEl.addEventListener('show.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + navEl.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + tab.show() + }) + + it('should not fire shown when tab is already active', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item"><a href="#home" class="nav-link active" role="tab">Home</a></li>', + ' <li class="nav-item"><a href="#profile" class="nav-link" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerActive = fixtureEl.querySelector('a.active') + const tab = new Tab(triggerActive) + + triggerActive.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + tab.show() + setTimeout(() => { + expect().nothing() + done() + }, 30) + }) + + it('should not fire shown when tab is disabled', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item"><a href="#home" class="nav-link active" role="tab">Home</a></li>', + ' <li class="nav-item"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerDisabled = fixtureEl.querySelector('a.disabled') + const tab = new Tab(triggerDisabled) + + triggerDisabled.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) + + tab.show() + setTimeout(() => { + expect().nothing() + done() + }, 30) + }) + + it('show and shown events should reference correct relatedTarget', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item"><a href="#home" class="nav-link active" role="tab">Home</a></li>', + ' <li class="nav-item"><a id="triggerProfile" href="#profile" class="nav-link" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + const secondTab = new Tab(secondTabTrigger) + + secondTabTrigger.addEventListener('show.bs.tab', ev => { + expect(ev.relatedTarget.hash).toEqual('#home') + }) + + secondTabTrigger.addEventListener('shown.bs.tab', ev => { + expect(ev.relatedTarget.hash).toEqual('#home') + expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') + expect(fixtureEl.querySelector('a:not(.active)').getAttribute('aria-selected')).toEqual('false') + done() + }) + + secondTab.show() + }) + + it('should fire hide and hidden events', done => { + fixtureEl.innerHTML = [ + '<ul class="nav">', + ' <li><a href="#home">Home</a></li>', + ' <li><a href="#profile">Profile</a></li>', + '</ul>' + ].join('') + + const triggerList = fixtureEl.querySelectorAll('a') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) + + let hideCalled = false + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) + + triggerList[0].addEventListener('hide.bs.tab', ev => { + hideCalled = true + expect(ev.relatedTarget.hash).toEqual('#profile') + }) + + triggerList[0].addEventListener('hidden.bs.tab', ev => { + expect(hideCalled).toEqual(true) + expect(ev.relatedTarget.hash).toEqual('#profile') + done() + }) + + firstTab.show() + }) + + it('should not fire hidden when hide is prevented', done => { + fixtureEl.innerHTML = [ + '<ul class="nav">', + ' <li><a href="#home">Home</a></li>', + ' <li><a href="#profile">Profile</a></li>', + '</ul>' + ].join('') + + const triggerList = fixtureEl.querySelectorAll('a') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + done() + }, 30) + } + + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) + + triggerList[0].addEventListener('hide.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + triggerList[0].addEventListener('hidden.bs.tab', () => { + throw new Error('should not trigger hidden') + }) + + firstTab.show() + }) + + it('should handle removed tabs', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item">', + ' <a class="nav-link nav-tab" href="#profile" role="tab" data-toggle="tab">', + ' <button class="close"><span aria-hidden="true">×</span></button>', + ' </a>', + ' </li>', + ' <li class="nav-item">', + ' <a id="secondNav" class="nav-link nav-tab" href="#buzz" role="tab" data-toggle="tab">', + ' <button class="close"><span aria-hidden="true">×</span></button>', + ' </a>', + ' </li>', + ' <li class="nav-item">', + ' <a class="nav-link nav-tab" href="#references" role="tab" data-toggle="tab">', + ' <button id="btnClose" class="close"><span aria-hidden="true">×</span></button>', + ' </a>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane fade show active" id="profile">test 1</div>', + ' <div role="tabpanel" class="tab-pane fade" id="buzz">test 2</div>', + ' <div role="tabpanel" class="tab-pane fade" id="references">test 3</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + const btnCloseEl = fixtureEl.querySelector('#btnClose') + const secondNavTab = new Tab(secondNavEl) + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.nav-tab').length).toEqual(2) + done() + }) + + btnCloseEl.addEventListener('click', () => { + const linkEl = btnCloseEl.parentNode + const liEl = linkEl.parentNode + const tabId = linkEl.getAttribute('href') + const tabIdEl = fixtureEl.querySelector(tabId) + + liEl.parentNode.removeChild(liEl) + tabIdEl.parentNode.removeChild(tabIdEl) + secondNavTab.show() + }) + + btnCloseEl.click() + }) + }) + + describe('dispose', () => { + it('should dispose a tab', () => { + fixtureEl.innerHTML = '<div></div>' + + const el = fixtureEl.querySelector('div') + const tab = new Tab(fixtureEl.querySelector('div')) + + expect(Tab._getInstance(el)).not.toBeNull() + + tab.dispose() + + expect(Tab._getInstance(el)).toBeNull() + }) + }) + + describe('_jQueryInterface', () => { + it('should create a tab', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.tab = Tab._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.tab.call(jQueryMock) + + expect(Tab._getInstance(div)).toBeDefined() + }) + + it('should not re create a tab', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const tab = new Tab(div) + + jQueryMock.fn.tab = Tab._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.tab.call(jQueryMock) + + expect(Tab._getInstance(div)).toEqual(tab) + }) + + it('should call a tab method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const tab = new Tab(div) + + spyOn(tab, 'show') + + jQueryMock.fn.tab = Tab._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.tab.call(jQueryMock, 'show') + + expect(Tab._getInstance(div)).toEqual(tab) + expect(tab.show).toHaveBeenCalled() + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.tab = Tab._jQueryInterface + jQueryMock.elements = [div] + + try { + jQueryMock.fn.tab.call(jQueryMock, action) + } catch (error) { + expect(error.message).toEqual(`No method named "${action}"`) + } + }) + }) + + describe('_getInstance', () => { + it('should return null if there is no instance', () => { + expect(Tab._getInstance(fixtureEl)).toEqual(null) + }) + + it('should return this instance', () => { + fixtureEl.innerHTML = '<div></div>' + + const divEl = fixtureEl.querySelector('div') + const tab = new Tab(divEl) + + expect(Tab._getInstance(divEl)).toEqual(tab) + }) + }) + + describe('data-api', () => { + it('should create dynamicaly a tab', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item"><a href="#home" class="nav-link active" role="tab">Home</a></li>', + ' <li class="nav-item"><a id="triggerProfile" data-toggle="tab" href="#profile" class="nav-link" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + + secondTabTrigger.addEventListener('shown.bs.tab', () => { + expect(secondTabTrigger.classList.contains('active')).toEqual(true) + expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + done() + }) + + secondTabTrigger.click() + }) + + it('selected tab should deactivate previous selected link in dropdown', () => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs">', + ' <li class="nav-item"><a class="nav-link" href="#home" data-toggle="tab">Home</a></li>', + ' <li class="nav-item"><a class="nav-link" href="#profile" data-toggle="tab">Profile</a></li>', + ' <li class="nav-item dropdown">', + ' <a class="nav-link dropdown-toggle active" data-toggle="dropdown" href="#">Dropdown</>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item active" href="#dropdown1" id="dropdown1-tab" data-toggle="tab">@fat</a>', + ' <a class="dropdown-item" href="#dropdown2" id="dropdown2-tab" data-toggle="tab">@mdo</a>', + ' </div>', + ' </li>', + '</ul>' + ].join('') + + const firstLiLinkEl = fixtureEl.querySelector('li:first-child a') + + firstLiLinkEl.click() + expect(firstLiLinkEl.classList.contains('active')).toEqual(true) + expect(fixtureEl.querySelector('li:last-child a').classList.contains('active')).toEqual(false) + expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child').classList.contains('active')).toEqual(false) + }) + + it('should handle nested tabs', done => { + fixtureEl.innerHTML = [ + '<nav class="nav nav-tabs" role="tablist">', + ' <a id="tab1" href="#x-tab1" class="nav-item nav-link" data-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</a>', + ' <a href="#x-tab2" class="nav-item nav-link active" data-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</a>', + ' <a href="#x-tab3" class="nav-item nav-link" data-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</a>', + '</nav>', + '<div class="tab-content">', + ' <div class="tab-pane" id="x-tab1" role="tabpanel">', + ' <nav class="nav nav-tabs" role="tablist">', + ' <a href="#nested-tab1" class="nav-item nav-link active" data-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</a>', + ' <a id="tabNested2" href="#nested-tab2" class="nav-item nav-link" data-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</a>', + ' </nav>', + ' <div class="tab-content">', + ' <div class="tab-pane active" id="nested-tab1" role="tabpanel">Nested Tab1 Content</div>', + ' <div class="tab-pane" id="nested-tab2" role="tabpanel">Nested Tab2 Content</div>', + ' </div>', + ' </div>', + ' <div class="tab-pane active" id="x-tab2" role="tabpanel">Tab2 Content</div>', + ' <div class="tab-pane" id="x-tab3" role="tabpanel">Tab3 Content</div>', + '</div>' + ].join('') + + const tab1El = fixtureEl.querySelector('#tab1') + const tabNested2El = fixtureEl.querySelector('#tabNested2') + const xTab1El = fixtureEl.querySelector('#x-tab1') + + tabNested2El.addEventListener('shown.bs.tab', () => { + expect(xTab1El.classList.contains('active')).toEqual(true) + done() + }) + + tab1El.addEventListener('shown.bs.tab', () => { + expect(xTab1El.classList.contains('active')).toEqual(true) + tabNested2El.click() + }) + + tab1El.click() + }) + + it('should not remove fade class if no active pane is present', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item"><a id="tab-home" href="#home" class="nav-link" data-toggle="tab" role="tab">Home</a></li>', + ' <li class="nav-item"><a id="tab-profile" href="#profile" class="nav-link" data-toggle="tab" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane fade" id="home" role="tabpanel"></div>', + ' <div class="tab-pane fade" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') + const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') + const tabProfileEl = fixtureEl.querySelector('#profile') + const tabHomeEl = fixtureEl.querySelector('#home') + + triggerTabProfileEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl.classList.contains('fade')).toEqual(true) + expect(tabProfileEl.classList.contains('show')).toEqual(true) + + triggerTabHomeEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl.classList.contains('fade')).toEqual(true) + expect(tabProfileEl.classList.contains('show')).toEqual(false) + + expect(tabHomeEl.classList.contains('fade')).toEqual(true) + expect(tabHomeEl.classList.contains('show')).toEqual(true) + + done() + }) + + triggerTabHomeEl.click() + }) + + triggerTabProfileEl.click() + }) + + it('should not add show class to tab panes if there is no `.fade` class', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item">', + ' <a class="nav-link nav-tab" href="#home" role="tab" data-toggle="tab">Home</a>', + ' </li>', + ' <li class="nav-item">', + ' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-toggle="tab">Profile</a>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane" id="home">test 1</div>', + ' <div role="tabpanel" class="tab-pane" id="profile">test 2</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show').length).toEqual(0) + done() + }) + + secondNavEl.click() + }) + + it('should add show class to tab panes if there is a `.fade` class', done => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item">', + ' <a class="nav-link nav-tab" href="#home" role="tab" data-toggle="tab">Home</a>', + ' </li>', + ' <li class="nav-item">', + ' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-toggle="tab">Profile</a>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane fade" id="home">test 1</div>', + ' <div role="tabpanel" class="tab-pane fade" id="profile">test 2</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show').length).toEqual(1) + done() + }) + + secondNavEl.click() + }) + }) +}) |
