diff options
Diffstat (limited to 'js/tests/unit/tab.spec.js')
| -rw-r--r-- | js/tests/unit/tab.spec.js | 1372 |
1 files changed, 933 insertions, 439 deletions
diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index 05f9db2ec..4fcf8ee0f 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -1,5 +1,7 @@ -import Tab from '../../src/tab' -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import Tab from '../../src/tab.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Tab', () => { let fixtureEl @@ -21,8 +23,12 @@ describe('Tab', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ - '<ul class="nav"><li><a href="#home" role="tab">Home</a></li></ul>', - '<ul><li id="home"></li></ul>' + '<ul class="nav">', + ' <li><a href="#home" role="tab">Home</a></li>', + '</ul>', + '<ul>', + ' <li id="home"></li>', + '</ul>' ].join('') const tabEl = fixtureEl.querySelector('[href="#home"]') @@ -32,334 +38,800 @@ describe('Tab', () => { expect(tabBySelector._element).toEqual(tabEl) expect(tabByElement._element).toEqual(tabEl) }) - }) - describe('show', () => { - it('should activate element by tab id (using buttons, the preferred semantic way)', done => { + it('Do not Throw exception if not parent', () => { fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>', - '</ul>', - '<ul>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ul>' + fixtureEl.innerHTML = '<div class=""><div class="nav-link"></div></div>' ].join('') + const navEl = fixtureEl.querySelector('.nav-link') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + expect(() => { + new Tab(navEl) // eslint-disable-line no-new + }).not.toThrowError(TypeError) + }) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + describe('show', () => { + it('should activate element by tab id (using buttons, the preferred semantic way)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>', + '<ul>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ul>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) + + tab.show() }) + }) - tab.show() + it('should activate element by tab id (using links for tabs - not ideal, but still supported)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><a href="#home" role="tab">Home</a></li>', + ' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>', + '</ul>', + '<ul>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ul>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) + + tab.show() + }) }) - it('should activate element by tab id (using links for tabs - not ideal, but still supported)', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><a href="#home" role="tab">Home</a></li>', - ' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>', - '</ul>', - '<ul>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ul>' - ].join('') + it('should activate element by tab id in ordered list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ol class="nav nav-pills">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>', + '</ol>', + '<ol>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ol>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + tab.show() + }) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + it('should activate element by tab id in nav list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="nav">', + ' <button type="button" data-bs-target="#home" role="tab">Home</button>', + ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', + '</nav>', + '<div>', + ' <div id="home" role="tabpanel"></div>', + ' <div id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + tab.show() }) + }) - tab.show() + it('should activate element by tab id in list group', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="list-group" role="tablist">', + ' <button type="button" data-bs-target="#home" role="tab">Home</button>', + ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', + '</div>', + '<div>', + ' <div id="home" role="tabpanel"></div>', + ' <div id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) + + tab.show() + }) }) - it('should activate element by tab id in ordered list', done => { + it('should work with tab id being an int', done => { fixtureEl.innerHTML = [ - '<ol class="nav nav-pills">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>', - '</ol>', - '<ol>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ol>' + '<div class="card-header d-block d-inline-block">', + ' <ul class="nav nav-tabs card-header-tabs" id="page_tabs">', + ' <li class="nav-item">', + ' <a class="nav-link" draggable="false" data-toggle="tab" href="#tab1">', + ' Working Tab 1 (#tab1)', + ' </a>', + ' </li>', + ' <li class="nav-item">', + ' <a id="trigger2" class="nav-link" draggable="false" data-toggle="tab" href="#2">', + ' Tab with numeric ID should work (#2)', + ' </a>', + ' </li>', + ' </ul>', + '</div>', + '<div class="card-body">', + ' <div class="tab-content" id="page_content">', + ' <div class="tab-pane fade" id="tab1">', + ' Working Tab 1 (#tab1) Content Here', + ' </div>', + ' <div class="tab-pane fade" id="2">', + ' Working Tab 2 (#2) with numeric ID', + ' </div>', + '</div>' ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const profileTriggerEl = fixtureEl.querySelector('#trigger2') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + expect(fixtureEl.querySelector(`#${CSS.escape('2')}`)).toHaveClass('active') done() }) tab.show() }) - it('should activate element by tab id in nav list', done => { - fixtureEl.innerHTML = [ - '<nav class="nav">', - ' <button type="button" data-bs-target="#home" role="tab">Home</button>', - ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</a>', - '</nav>', - '<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>' - ].join('') + it('should not fire shown when show is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' + + const navEl = fixtureEl.querySelector('.nav > div') + const tab = new Tab(navEl) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } + + navEl.addEventListener('show.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + navEl.addEventListener('shown.bs.tab', () => { + reject(new Error('should not trigger shown event')) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + tab.show() }) + }) - tab.show() + it('should not fire shown when tab is already active', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></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('button.active') + const tab = new Tab(triggerActive) + + triggerActive.addEventListener('shown.bs.tab', () => { + reject(new Error('should not trigger shown event')) + }) + + tab.show() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + }) }) - it('should activate element by tab id in list group', done => { - fixtureEl.innerHTML = [ - '<div class="list-group" role="tablist">', - ' <button type="button" data-bs-target="#home" role="tab">Home</button>', - ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', - '</div>', - '<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>' - ].join('') + it('show and shown events should reference correct relatedTarget', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></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.getAttribute('data-bs-target')).toEqual('#home') + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + secondTabTrigger.addEventListener('shown.bs.tab', ev => { + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') + expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') + expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + secondTab.show() }) + }) - tab.show() + it('should fire hide and hidden events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>' + ].join('') + + const triggerList = fixtureEl.querySelectorAll('button') + 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.getAttribute('data-bs-target')).toEqual('#profile') + }) + + triggerList[0].addEventListener('hidden.bs.tab', ev => { + expect(hideCalled).toBeTrue() + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') + resolve() + }) + + firstTab.show() + }) }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '<div class="nav"></div>' + it('should not fire hidden when hide is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>' + ].join('') + + const triggerList = fixtureEl.querySelectorAll('button') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } + + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) - const navEl = fixtureEl.querySelector('div') - const tab = new Tab(navEl) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + triggerList[0].addEventListener('hide.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + triggerList[0].addEventListener('hidden.bs.tab', () => { + reject(new Error('should not trigger hidden')) + }) - navEl.addEventListener('show.bs.tab', ev => { - ev.preventDefault() - expectDone() + firstTab.show() }) + }) + + it('should handle removed tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <a class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">', + ' <button class="btn-close" aria-label="Close"></button>', + ' </a>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <a id="secondNav" class="nav-link nav-tab" href="#buzz" role="tab" data-bs-toggle="tab">', + ' <button class="btn-close" aria-label="Close"></button>', + ' </a>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <a class="nav-link nav-tab" href="#references" role="tab" data-bs-toggle="tab">', + ' <button id="btnClose" class="btn-close" aria-label="Close"></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')).toHaveSize(2) + resolve() + }) - navEl.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + btnCloseEl.addEventListener('click', () => { + const linkEl = btnCloseEl.parentNode + const liEl = linkEl.parentNode + const tabId = linkEl.getAttribute('href') + const tabIdEl = fixtureEl.querySelector(tabId) + + liEl.remove() + tabIdEl.remove() + secondNavTab.show() + }) + + btnCloseEl.click() }) + }) - tab.show() + it('should not focus on opened tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" id="home" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>', + '<ul>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ul>' + ].join('') + + const firstTab = fixtureEl.querySelector('#home') + firstTab.focus() + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(document.activeElement).toBe(firstTab) + expect(document.activeElement).not.toBe(profileTriggerEl) + resolve() + }) + + tab.show() + }) }) + }) + + describe('dispose', () => { + it('should dispose a tab', () => { + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' + + const el = fixtureEl.querySelector('.nav > div') + const tab = new Tab(fixtureEl.querySelector('.nav > div')) + + expect(Tab.getInstance(el)).not.toBeNull() - it('should not fire shown when tab is already active', done => { + tab.dispose() + + expect(Tab.getInstance(el)).toBeNull() + }) + }) + + describe('_activate', () => { + it('should not be called if element argument is null', () => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li class="nav-link"></li>', + '</ul>' + ].join('') + + const tabEl = fixtureEl.querySelector('.nav-link') + const tab = new Tab(tabEl) + const spy = jasmine.createSpy('spy') + + const spyQueue = spyOn(tab, '_queueCallback') + tab._activate(null, spy) + expect(spyQueue).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + }) + }) + + describe('_setInitialAttributes', () => { + it('should put aria attributes', () => { fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', + '<ul class="nav">', + ' <li class="nav-link" id="foo" data-bs-target="#panel"></li>', + ' <li class="nav-link" data-bs-target="#panel2"></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>' + '<div id="panel"></div>', + '<div id="panel2"></div>' ].join('') - const triggerActive = fixtureEl.querySelector('button.active') - const tab = new Tab(triggerActive) + const tabEl = fixtureEl.querySelector('.nav-link') + const parent = fixtureEl.querySelector('.nav') + const children = fixtureEl.querySelectorAll('.nav-link') + const tabPanel = fixtureEl.querySelector('#panel') + const tabPanel2 = fixtureEl.querySelector('#panel2') - triggerActive.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) + expect(parent.getAttribute('role')).toEqual(null) + expect(tabEl.getAttribute('role')).toEqual(null) + expect(tabPanel.getAttribute('role')).toEqual(null) + const tab = new Tab(tabEl) + tab._setInitialAttributes(parent, children) - tab.show() - setTimeout(() => { - expect().nothing() - done() - }, 30) + expect(parent.getAttribute('role')).toEqual('tablist') + expect(tabEl.getAttribute('role')).toEqual('tab') + + expect(tabPanel.getAttribute('role')).toEqual('tabpanel') + expect(tabPanel2.getAttribute('role')).toEqual('tabpanel') + expect(tabPanel.hasAttribute('tabindex')).toBeFalse() + expect(tabPanel.hasAttribute('tabindex2')).toBeFalse() + + expect(tabPanel.getAttribute('aria-labelledby')).toEqual('foo') + expect(tabPanel2.hasAttribute('aria-labelledby')).toBeFalse() }) + }) - it('show and shown events should reference correct relatedTarget', done => { + describe('_keydown', () => { + it('if event is not one of left/right/up/down arrow, ignore it', () => { fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></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>', + '<ul class="nav">', + ' <li class="nav-link" data-bs-toggle="tab"></li>', + '</ul>' + ].join('') + + const tabEl = fixtureEl.querySelector('.nav-link') + const tab = new Tab(tabEl) + + const keydown = createEvent('keydown') + keydown.key = 'Enter' + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + const spyKeydown = spyOn(tab, '_keydown') + const spyGet = spyOn(tab, '_getChildren') + + tabEl.dispatchEvent(keydown) + expect(spyKeydown).toHaveBeenCalled() + expect(spyGet).not.toHaveBeenCalled() + + expect(spyStop).not.toHaveBeenCalled() + expect(spyPrevent).not.toHaveBeenCalled() + }) + + it('if keydown event is right/down arrow, handle it', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', '</div>' ].join('') - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') - const secondTab = new Tab(secondTabTrigger) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyShow3 = spyOn(tab3, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + tabEl1.dispatchEvent(keydown) + expect(spyShow2).toHaveBeenCalled() + expect(spyFocus2).toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + tabEl2.dispatchEvent(keydown) + expect(spyShow3).toHaveBeenCalled() + expect(spyFocus3).toHaveBeenCalled() + + tabEl3.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + + expect(spyStop).toHaveBeenCalledTimes(3) + expect(spyPrevent).toHaveBeenCalledTimes(3) + }) - secondTabTrigger.addEventListener('show.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - }) + it('if keydown event is left arrow, handle it', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') - secondTabTrigger.addEventListener('shown.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') - expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') - done() - }) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' + + tabEl2.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + + tabEl1.dispatchEvent(keydown) + expect(spyShow2).toHaveBeenCalled() + expect(spyFocus2).toHaveBeenCalled() - secondTab.show() + expect(spyStop).toHaveBeenCalledTimes(2) + expect(spyPrevent).toHaveBeenCalledTimes(2) }) - it('should fire hide and hidden events', done => { + it('if keydown event is Home, handle it', () => { fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" data-bs-target="#profile">Profile</button></li>', - '</ul>' + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' ].join('') - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl3 = fixtureEl.querySelector('#tab3') - let hideCalled = false - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + const tab3 = new Tab(tabEl3) + tab3.show() - triggerList[0].addEventListener('hide.bs.tab', ev => { - hideCalled = true - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - }) + const spyShown = jasmine.createSpy() + tabEl1.addEventListener('shown.bs.tab', spyShown) - triggerList[0].addEventListener('hidden.bs.tab', ev => { - expect(hideCalled).toEqual(true) - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - done() - }) + const keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl3.dispatchEvent(keydown) - firstTab.show() + expect(spyShown).toHaveBeenCalled() }) - it('should not fire hidden when hide is prevented', done => { + it('if keydown event is End, handle it', () => { fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>', - '</ul>' + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' ].join('') - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl3 = fixtureEl.querySelector('#tab3') - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + const tab1 = new Tab(tabEl1) + tab1.show() - triggerList[0].addEventListener('hide.bs.tab', ev => { - ev.preventDefault() - expectDone() - }) + const spyShown = jasmine.createSpy() + tabEl3.addEventListener('shown.bs.tab', spyShown) - triggerList[0].addEventListener('hidden.bs.tab', () => { - throw new Error('should not trigger hidden') - }) + const keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl1.dispatchEvent(keydown) - firstTab.show() + expect(spyShown).toHaveBeenCalled() }) - it('should handle removed tabs', done => { + it('if keydown event is right arrow and next element is disabled', () => { fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation">', - ' <a class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">', - ' <button class="btn-close" aria-label="Close"></button>', - ' </a>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <a id="secondNav" class="nav-link nav-tab" href="#buzz" role="tab" data-bs-toggle="tab">', - ' <button class="btn-close" aria-label="Close"></button>', - ' </a>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <a class="nav-link nav-tab" href="#references" role="tab" data-bs-toggle="tab">', - ' <button id="btnClose" class="btn-close" aria-label="Close"></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 class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab" disabled></span>', + ' <span id="tab3" class="nav-link disabled" data-bs-toggle="tab"></span>', + ' <span id="tab4" class="nav-link" data-bs-toggle="tab"></span>', '</div>' ].join('') - const secondNavEl = fixtureEl.querySelector('#secondNav') - const btnCloseEl = fixtureEl.querySelector('#btnClose') - const secondNavTab = new Tab(secondNavEl) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tabEl4 = fixtureEl.querySelector('#tab4') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const tab4 = new Tab(tabEl4) + const spy1 = spyOn(tab1, 'show').and.callThrough() + const spy2 = spyOn(tab2, 'show').and.callThrough() + const spy3 = spyOn(tab3, 'show').and.callThrough() + const spy4 = spyOn(tab4, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() + + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + tabEl1.dispatchEvent(keydown) + expect(spy1).not.toHaveBeenCalled() + expect(spy2).not.toHaveBeenCalled() + expect(spy3).not.toHaveBeenCalled() + expect(spy4).toHaveBeenCalledTimes(1) + expect(spyFocus1).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + expect(spyFocus4).toHaveBeenCalledTimes(1) + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.nav-tab').length).toEqual(2) - done() - }) + it('if keydown event is left arrow and next element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab" disabled></span>', + ' <span id="tab3" class="nav-link disabled" data-bs-toggle="tab"></span>', + ' <span id="tab4" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') - btnCloseEl.addEventListener('click', () => { - const linkEl = btnCloseEl.parentNode - const liEl = linkEl.parentNode - const tabId = linkEl.getAttribute('href') - const tabIdEl = fixtureEl.querySelector(tabId) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tabEl4 = fixtureEl.querySelector('#tab4') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const tab4 = new Tab(tabEl4) + const spy1 = spyOn(tab1, 'show').and.callThrough() + const spy2 = spyOn(tab2, 'show').and.callThrough() + const spy3 = spyOn(tab3, 'show').and.callThrough() + const spy4 = spyOn(tab4, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() + + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' + + tabEl4.dispatchEvent(keydown) + expect(spy4).not.toHaveBeenCalled() + expect(spy3).not.toHaveBeenCalled() + expect(spy2).not.toHaveBeenCalled() + expect(spy1).toHaveBeenCalledTimes(1) + expect(spyFocus4).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalledTimes(1) + }) - liEl.remove() - tabIdEl.remove() - secondNavTab.show() - }) + it('if keydown event is Home and first element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link disabled" data-bs-toggle="tab" disabled></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') + + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab3 = new Tab(tabEl3) - btnCloseEl.click() + tab3.show() + + const spyShown1 = jasmine.createSpy() + const spyShown2 = jasmine.createSpy() + tabEl1.addEventListener('shown.bs.tab', spyShown1) + tabEl2.addEventListener('shown.bs.tab', spyShown2) + + const keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl3.dispatchEvent(keydown) + + expect(spyShown1).not.toHaveBeenCalled() + expect(spyShown2).toHaveBeenCalled() }) - }) - describe('dispose', () => { - it('should dispose a tab', () => { - fixtureEl.innerHTML = '<div></div>' + it('if keydown event is End and last element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab" disabled></span>', + '</div>' + ].join('') - const el = fixtureEl.querySelector('div') - const tab = new Tab(fixtureEl.querySelector('div')) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) - expect(Tab.getInstance(el)).not.toBeNull() + tab1.show() - tab.dispose() + const spyShown2 = jasmine.createSpy() + const spyShown3 = jasmine.createSpy() + tabEl2.addEventListener('shown.bs.tab', spyShown2) + tabEl3.addEventListener('shown.bs.tab', spyShown3) - expect(Tab.getInstance(el)).toBeNull() + const keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl1.dispatchEvent(keydown) + + expect(spyShown3).not.toHaveBeenCalled() + expect(spyShown2).toHaveBeenCalled() }) }) describe('jQueryInterface', () => { it('should create a tab', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] @@ -370,9 +842,9 @@ describe('Tab', () => { }) it('should not re create a tab', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) jQueryMock.fn.tab = Tab.jQueryInterface @@ -384,12 +856,12 @@ describe('Tab', () => { }) it('should call a tab method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) - spyOn(tab, 'show') + const spy = spyOn(tab, 'show') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] @@ -397,13 +869,13 @@ describe('Tab', () => { jQueryMock.fn.tab.call(jQueryMock, 'show') expect(Tab.getInstance(div)).toEqual(tab) - expect(tab.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const action = 'undefinedMethod' jQueryMock.fn.tab = Tab.jQueryInterface @@ -417,13 +889,13 @@ describe('Tab', () => { describe('getInstance', () => { it('should return null if there is no instance', () => { - expect(Tab.getInstance(fixtureEl)).toEqual(null) + expect(Tab.getInstance(fixtureEl)).toBeNull() }) it('should return this instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const divEl = fixtureEl.querySelector('div') + const divEl = fixtureEl.querySelector('.nav > div') const tab = new Tab(divEl) expect(Tab.getInstance(divEl)).toEqual(tab) @@ -433,7 +905,7 @@ describe('Tab', () => { describe('getOrCreateInstance', () => { it('should return tab instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' const div = fixtureEl.querySelector('div') const tab = new Tab(div) @@ -444,37 +916,39 @@ describe('Tab', () => { }) it('should return new instance when there is no tab instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' const div = fixtureEl.querySelector('div') - expect(Tab.getInstance(div)).toEqual(null) + expect(Tab.getInstance(div)).toBeNull() expect(Tab.getOrCreateInstance(div)).toBeInstanceOf(Tab) }) }) describe('data-api', () => { - it('should create dynamically a tab', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></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') + it('should create dynamically a tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></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).toHaveClass('active') + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - secondTabTrigger.addEventListener('shown.bs.tab', () => { - expect(secondTabTrigger.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + secondTabTrigger.click() }) - - secondTabTrigger.click() }) it('selected tab should deactivate previous selected link in dropdown', () => { @@ -495,9 +969,9 @@ describe('Tab', () => { 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) + expect(firstLiLinkEl).toHaveClass('active') + expect(fixtureEl.querySelector('li:last-child a')).not.toHaveClass('active') + expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child')).not.toHaveClass('active') }) it('selecting a dropdown tab does not activate another', () => { @@ -529,10 +1003,10 @@ describe('Tab', () => { const firstDropItem = fixtureEl.querySelector('#nav1 .dropdown-item') firstDropItem.click() - expect(firstDropItem.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#nav1 .dropdown-toggle').classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#nav2 .dropdown-toggle').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('#nav2 .dropdown-item').classList.contains('active')).toEqual(false) + expect(firstDropItem).toHaveClass('active') + expect(fixtureEl.querySelector('#nav1 .dropdown-toggle')).toHaveClass('active') + expect(fixtureEl.querySelector('#nav2 .dropdown-toggle')).not.toHaveClass('active') + expect(fixtureEl.querySelector('#nav2 .dropdown-item')).not.toHaveClass('active') }) it('should support li > .dropdown-item', () => { @@ -550,209 +1024,229 @@ describe('Tab', () => { '</ul>' ].join('') - const firstDropItem = fixtureEl.querySelector('.dropdown-item') + const dropItems = fixtureEl.querySelectorAll('.dropdown-item') - firstDropItem.click() - expect(firstDropItem.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('.nav-link').classList.contains('active')).toEqual(false) + dropItems[1].click() + expect(dropItems[0]).not.toHaveClass('active') + expect(dropItems[1]).toHaveClass('active') + expect(fixtureEl.querySelector('.nav-link')).not.toHaveClass('active') }) - it('should handle nested tabs', done => { - fixtureEl.innerHTML = [ - '<nav class="nav nav-tabs" role="tablist">', - ' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>', - ' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>', - ' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>', - '</nav>', - '<div class="tab-content">', - ' <div class="tab-pane" id="x-tab1" role="tabpanel">', - ' <nav class="nav nav-tabs" role="tablist">', - ' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>', - ' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>', - ' </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') + it('should handle nested tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="nav nav-tabs" role="tablist">', + ' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>', + ' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>', + ' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>', + '</nav>', + '<div class="tab-content">', + ' <div class="tab-pane" id="x-tab1" role="tabpanel">', + ' <nav class="nav nav-tabs" role="tablist">', + ' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>', + ' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>', + ' </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).toHaveClass('active') + resolve() + }) - tabNested2El.addEventListener('shown.bs.tab', () => { - expect(xTab1El.classList.contains('active')).toEqual(true) - done() - }) + tab1El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + tabNested2El.click() + }) - tab1El.addEventListener('shown.bs.tab', () => { - expect(xTab1El.classList.contains('active')).toEqual(true) - tabNested2El.click() + tab1El.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" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></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) + it('should not remove fade class if no active pane is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></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') triggerTabHomeEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl.classList.contains('fade')).toEqual(true) - expect(tabProfileEl.classList.contains('show')).toEqual(false) + setTimeout(() => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).not.toHaveClass('show') - expect(tabHomeEl.classList.contains('fade')).toEqual(true) - expect(tabHomeEl.classList.contains('show')).toEqual(true) + expect(tabHomeEl).toHaveClass('fade') + expect(tabHomeEl).toHaveClass('show') - done() + resolve() + }, 10) }) - triggerTabHomeEl.click() - }) + triggerTabProfileEl.addEventListener('shown.bs.tab', () => { + setTimeout(() => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).toHaveClass('show') + triggerTabHomeEl.click() + }, 10) + }) - triggerTabProfileEl.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" role="presentation">', - ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', - ' </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') + it('should add `show` class to tab panes if there is no `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', + ' </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('.tab-content .show')).toHaveSize(1) + resolve() + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show').length).toEqual(0) - done() + secondNavEl.click() }) - - 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" role="presentation">', - ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', - ' </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') + it('should add show class to tab panes if there is a `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', + ' </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', () => { + setTimeout(() => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) + resolve() + }, 10) + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show').length).toEqual(1) - done() + secondNavEl.click() }) - - secondNavEl.click() }) - it('should prevent default when the trigger is <a> or <area>', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><a type="button" href="#test" class="active" role="tab" data-bs-toggle="tab">Home</a></li>', - ' <li><a type="button" href="#test2" role="tab" data-bs-toggle="tab">Home</a></li>', - '</ul>' - ].join('') - - const tabEl = fixtureEl.querySelector('[href="#test2"]') - spyOn(Event.prototype, 'preventDefault').and.callThrough() + it('should prevent default when the trigger is <a> or <area>', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><a type="button" href="#test" class="active" role="tab" data-bs-toggle="tab">Home</a></li>', + ' <li><a type="button" href="#test2" role="tab" data-bs-toggle="tab">Home</a></li>', + '</ul>' + ].join('') + + const tabEl = fixtureEl.querySelector('[href="#test2"]') + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + tabEl.addEventListener('shown.bs.tab', () => { + expect(tabEl).toHaveClass('active') + expect(spy).toHaveBeenCalled() + resolve() + }) - tabEl.addEventListener('shown.bs.tab', () => { - expect(tabEl.classList.contains('active')).toEqual(true) - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + tabEl.click() }) - - tabEl.click() }) - it('should not fire shown when tab has disabled attribute', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" disabled role="tab">Profile</button></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('') + it('should not fire shown when tab has disabled attribute', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" disabled role="tab">Profile</button></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('button[disabled]') + triggerDisabled.addEventListener('shown.bs.tab', () => { + reject(new Error('should not trigger shown event')) + }) - const triggerDisabled = fixtureEl.querySelector('button[disabled]') - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) }) - - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) - it('should not fire shown when tab has disabled class', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab" aria-selected="true">Home</a></li>', - ' <li class="nav-item" role="presentation"><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') + it('should not fire shown when tab has disabled class', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab" aria-selected="true">Home</a></li>', + ' <li class="nav-item" role="presentation"><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') + + triggerDisabled.addEventListener('shown.bs.tab', () => { + reject(new Error('should not trigger shown event')) + }) - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) }) - - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) }) }) |
