diff options
Diffstat (limited to 'js/tests/unit/dropdown.spec.js')
| -rw-r--r-- | js/tests/unit/dropdown.spec.js | 3018 |
1 files changed, 1668 insertions, 1350 deletions
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index f099e9990..63ae4bd10 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1,7 +1,9 @@ -import Dropdown from '../../src/dropdown' -import EventHandler from '../../src/dom/event-handler' -import { noop } from '../../src/util/index' -import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Dropdown from '../../src/dropdown.js' +import { noop } from '../../src/util/index.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Dropdown', () => { let fixtureEl @@ -57,36 +59,61 @@ describe('Dropdown', () => { expect(dropdownByElement._element).toEqual(btnDropdown) }) - it('should create offset modifier correctly when offset option is a function', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should work on invalid markup', () => { + return new Promise(resolve => { + // TODO: REMOVE in v6 + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Link</a>', + ' </div>', + '</div>' + ].join('') - const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: getOffset, - popperConfig: { - onFirstUpdate: state => { - expect(getOffset).toHaveBeenCalledWith({ - popper: state.rects.popper, - reference: state.rects.reference, - placement: state.placement - }, btnDropdown) - done() + const dropdownElem = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(dropdownElem) + + dropdownElem.addEventListener('shown.bs.dropdown', () => { + resolve() + }) + + expect().nothing() + dropdown.show() + }) + }) + + it('should create offset modifier correctly when offset option is a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + offset: getOffset, + popperConfig: { + onFirstUpdate(state) { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, btnDropdown) + resolve() + } } - } - }) - const offset = dropdown._getOffset() + }) + const offset = dropdown._getOffset() - expect(typeof offset).toEqual('function') + expect(typeof offset).toEqual('function') - dropdown.show() + dropdown.show() + }) }) it('should create offset modifier correctly when offset option is a string into data attribute', () => { @@ -130,7 +157,7 @@ describe('Dropdown', () => { it('should allow to pass config to Popper with `popperConfig` as a function', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right" >Dropdown</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right">Dropdown</button>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', @@ -145,767 +172,875 @@ describe('Dropdown', () => { const popperConfig = dropdown._getPopperConfig() - expect(getPopperConfig).toHaveBeenCalled() + // Ensure that the function was called with the default config. + expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({ + placement: jasmine.any(String) + })) expect(popperConfig.placement).toEqual('left') }) }) describe('toggle', () => { - it('should toggle a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + it('should toggle a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() + dropdown.toggle() }) - - dropdown.toggle() }) - it('should destroy old popper references on toggle', done => { - fixtureEl.innerHTML = [ - '<div class="first dropdown">', - ' <button class="firstBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>', - '<div class="second dropdown">', - ' <button class="secondBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should destroy old popper references on toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="first dropdown">', + ' <button class="firstBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>', + '<div class="second dropdown">', + ' <button class="secondBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown1 = fixtureEl.querySelector('.firstBtn') + const btnDropdown2 = fixtureEl.querySelector('.secondBtn') + const firstDropdownEl = fixtureEl.querySelector('.first') + const secondDropdownEl = fixtureEl.querySelector('.second') + const dropdown1 = new Dropdown(btnDropdown1) + + firstDropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown1).toHaveClass('show') + spyOn(dropdown1._popper, 'destroy') + btnDropdown2.click() + }) - const btnDropdown1 = fixtureEl.querySelector('.firstBtn') - const btnDropdown2 = fixtureEl.querySelector('.secondBtn') - const firstDropdownEl = fixtureEl.querySelector('.first') - const secondDropdownEl = fixtureEl.querySelector('.second') - const dropdown1 = new Dropdown(btnDropdown1) + secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(dropdown1._popper.destroy).toHaveBeenCalled() + resolve() + })) - firstDropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown1.classList.contains('show')).toEqual(true) - spyOn(dropdown1._popper, 'destroy') - btnDropdown2.click() + dropdown1.toggle() }) + }) - secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(dropdown1._popper.destroy).toHaveBeenCalled() - done() - })) + it('should toggle a dropdown and add/remove event listener on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - dropdown1.toggle() - }) + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - it('should toggle a dropdown and add/remove event listener on mobile', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + document.documentElement.ontouchstart = noop + const spy = spyOn(EventHandler, 'on') + const spyOff = spyOn(EventHandler, 'off') - const defaultValueOnTouchStart = document.documentElement.ontouchstart - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'on') - spyOn(EventHandler, 'off') + dropdown.toggle() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(spyOff).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) dropdown.toggle() }) + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + it('should toggle a dropdown at the right', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu dropdown-menu-end">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - dropdown.toggle() + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) + + dropdown.toggle() + }) }) - it('should toggle a dropdown at the right', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu dropdown-menu-end">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a centered dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown-center">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropup', done => { - fixtureEl.innerHTML = [ - '<div class="dropup">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropup at the right', done => { - fixtureEl.innerHTML = [ - '<div class="dropup">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu dropdown-menu-end">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropup centered', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup-center">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup-center') + const dropdown = new Dropdown(btnDropdown) - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropend', done => { - fixtureEl.innerHTML = [ - '<div class="dropend">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropup at the right', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu dropdown-menu-end">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropendEl = fixtureEl.querySelector('.dropend') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) - dropendEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropstart', done => { - fixtureEl.innerHTML = [ - '<div class="dropstart">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropend', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropend">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropstartEl = fixtureEl.querySelector('.dropstart') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropendEl = fixtureEl.querySelector('.dropend') + const dropdown = new Dropdown(btnDropdown) - dropstartEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropendEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with parent reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropstart', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropstart">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: 'parent' - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropstartEl = fixtureEl.querySelector('.dropstart') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropstartEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a dom node reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with parent reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: fixtureEl - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: 'parent' + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a jquery object reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with a dom node reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: { 0: fixtureEl, jquery: 'jQuery' } - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: fixtureEl + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a valid virtual element reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with a jquery object reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const virtualElement = { - nodeType: 1, - getBoundingClientRect() { - return { - width: 0, - height: 0, - top: 0, - right: 0, - bottom: 0, - left: 0 - } - } - } + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: { 0: fixtureEl, jquery: 'jQuery' } + }) - expect(() => new Dropdown(btnDropdown, { - reference: {} - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - expect(() => new Dropdown(btnDropdown, { - reference: { - getBoundingClientRect: 'not-a-function' - } - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + dropdown.toggle() + }) + }) - // use onFirstUpdate as Poppers internal update is executed async - const dropdown = new Dropdown(btnDropdown, { - reference: virtualElement, - popperConfig: { - onFirstUpdate() { - expect(virtualElement.getBoundingClientRect).toHaveBeenCalled() - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() + it('should toggle a dropdown with a valid virtual element reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const virtualElement = { + nodeType: 1, + getBoundingClientRect() { + return { + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0 + } } } - }) - spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + expect(() => new Dropdown(btnDropdown, { + reference: {} + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') - dropdown.toggle() + expect(() => new Dropdown(btnDropdown, { + reference: { + getBoundingClientRect: 'not-a-function' + } + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + + // use onFirstUpdate as Poppers internal update is executed async + const dropdown = new Dropdown(btnDropdown, { + reference: virtualElement, + popperConfig: { + onFirstUpdate() { + expect(spy).toHaveBeenCalled() + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + } + }) + + const spy = spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + + dropdown.toggle() + }) }) - it('should not toggle a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the element is disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the element contains .disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the menu is shown', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if show event is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('show.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('show.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) }) describe('show', () => { - it('should show a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + it('should show a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + resolve() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - done() + dropdown.show() }) - - dropdown.show() }) - it('should not show a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the element is disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the element contains .disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the menu is shown', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if show event is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('show.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('show.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + reject(new Error('should not throw shown.bs.dropdown event')) + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) }) describe('hide', () => { - it('should hide a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + it('should hide a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + resolve() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - done() + dropdown.hide() }) - - dropdown.hide() }) - it('should hide a dropdown and destroy popper', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should hide a dropdown and destroy popper', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - spyOn(dropdown._popper, 'destroy') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + spyOn(dropdown._popper, 'destroy') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdown._popper.destroy).toHaveBeenCalled() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdown._popper.destroy).toHaveBeenCalled() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not hide a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the element is disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() - }, 10) + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }, 10) + }) }) - it('should not hide a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the element contains .disabled', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() - }, 10) + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }, 10) + }) }) - it('should not hide a dropdown if the menu is not shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the menu is not shown', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not hide a dropdown if hide event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if hide event is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hide.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('hide.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - done() + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }) }) }) - it('should remove event listener on touch-enabled device that was added in show method', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdwon item</a>', - ' </div>', - '</div>' - ].join('') + it('should remove event listener on touch-enabled device that was added in show method', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - const defaultValueOnTouchStart = document.documentElement.ontouchstart - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'off') + document.documentElement.ontouchstart = noop + const spy = spyOn(EventHandler, 'off') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalled() + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(spy).toHaveBeenCalled() - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) - dropdown.show() + dropdown.show() + }) }) }) @@ -927,13 +1062,13 @@ describe('Dropdown', () => { expect(dropdown._popper).toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() - spyOn(EventHandler, 'off') + const spy = spyOn(EventHandler, 'off') dropdown.dispose() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() - expect(EventHandler.off).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) + expect(spy).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) }) it('should dispose dropdown with Popper', () => { @@ -981,13 +1116,13 @@ describe('Dropdown', () => { expect(dropdown._popper).not.toBeNull() - spyOn(dropdown._popper, 'update') - spyOn(dropdown, '_detectNavbar') + const spyUpdate = spyOn(dropdown._popper, 'update') + const spyDetect = spyOn(dropdown, '_detectNavbar') dropdown.update() - expect(dropdown._popper.update).toHaveBeenCalled() - expect(dropdown._detectNavbar).toHaveBeenCalled() + expect(spyUpdate).toHaveBeenCalled() + expect(spyDetect).toHaveBeenCalled() }) it('should just detect navbar on update', () => { @@ -1003,904 +1138,1083 @@ describe('Dropdown', () => { const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) - spyOn(dropdown, '_detectNavbar') + const spy = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(dropdown._popper).toBeNull() - expect(dropdown._detectNavbar).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) describe('data-api', () => { - it('should show and hide a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - let showEventTriggered = false - let hideEventTriggered = false + it('should show and hide a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + let showEventTriggered = false + let hideEventTriggered = false + + btnDropdown.addEventListener('show.bs.dropdown', () => { + showEventTriggered = true + }) - btnDropdown.addEventListener('show.bs.dropdown', () => { - showEventTriggered = true - }) + btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(showEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + document.body.click() + })) - btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(showEventTriggered).toEqual(true) - expect(event.relatedTarget).toEqual(btnDropdown) - document.body.click() - })) + btnDropdown.addEventListener('hide.bs.dropdown', () => { + hideEventTriggered = true + }) - btnDropdown.addEventListener('hide.bs.dropdown', () => { - hideEventTriggered = true - }) + btnDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(hideEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + resolve() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', event => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(hideEventTriggered).toEqual(true) - expect(event.relatedTarget).toEqual(btnDropdown) - done() + btnDropdown.click() }) - - btnDropdown.click() }) - it('should not use Popper in navbar', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar navbar-expand-md navbar-light bg-light">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - ' </div>', - '</nav>' - ].join('') + it('should not use "static" Popper in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar navbar-expand-md bg-light">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + ' </div>', + '</nav>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown._popper).toBeNull() - expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown._popper).not.toBeNull() + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not collapse the dropdown when clicking a select option nested in the dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <select>', - ' <option selected>Open this select menu</option>', - ' <option value="1">One</option>', - ' </select>', - ' </div>', - '</div>' - ].join('') + it('should not collapse the dropdown when clicking a select option nested in the dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <select>', + ' <option selected>Open this select menu</option>', + ' <option value="1">One</option>', + ' </select>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - const hideSpy = spyOn(dropdown, '_completeHide') + const hideSpy = spyOn(dropdown, '_completeHide') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - const clickEvent = new MouseEvent('click', { - bubbles: true + btnDropdown.addEventListener('shown.bs.dropdown', () => { + const clickEvent = new MouseEvent('click', { + bubbles: true + }) + + dropdownMenu.querySelector('option').dispatchEvent(clickEvent) }) - dropdownMenu.querySelector('option').dispatchEvent(clickEvent) - }) + dropdownMenu.addEventListener('click', event => { + expect(event.target.tagName).toMatch(/select|option/i) - dropdownMenu.addEventListener('click', event => { - expect(event.target.tagName).toMatch(/select|option/i) + Dropdown.clearMenus(event) - Dropdown.clearMenus(event) + setTimeout(() => { + expect(hideSpy).not.toHaveBeenCalled() + resolve() + }, 10) + }) - setTimeout(() => { - expect(hideSpy).not.toHaveBeenCalled() - done() - }, 10) + dropdown.show() }) - - dropdown.show() }) - it('should manage bs attribute `data-bs-popper`="none" when dropdown is in navbar', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar navbar-expand-md navbar-light bg-light">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - ' </div>', - '</nav>' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar navbar-expand-md bg-light">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + ' </div>', + '</nav>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('none') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not use Popper if display set to static', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not use Popper if display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - // Popper adds this attribute when we use it - expect(dropdownMenu.getAttribute('data-popper-placement')).toEqual(null) - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + // Popper adds this attribute when we use it + expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() + resolve() + }) - btnDropdown.click() + btnDropdown.click() + }) }) - it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should remove "show" class if tabbing outside of menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if tabbing outside of menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') - const keyup = createEvent('keyup') + const keyup = createEvent('keyup') - keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + keyup.key = 'Tab' + document.dispatchEvent(keyup) + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + resolve() + }) - btnDropdown.click() + btnDropdown.click() + }) }) - it('should remove "show" class if body is clicked, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '<div class="nav">', - ' <div class="dropdown" id="testmenu">', - ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' </div>', - ' </div>', - '</div>', - '<div class="btn-group">', - ' <button class="btn">Actions</button>', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Action 1</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if body is clicked, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <div class="dropdown" id="testmenu">', + ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + ' </div>', + '</div>', + '<div class="btn-group">', + ' <button class="btn">Actions</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Action 1</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') - expect(triggerDropdownList.length).toEqual(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) - document.body.click() - }) + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) - triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) - triggerDropdownLast.click() - }) + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) - triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownLast.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) - document.body.click() - }) + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) - triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) - done() - }) + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) - triggerDropdownFirst.click() + triggerDropdownFirst.click() + }) }) - it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' </div>', - '</div>', - '<div class="btn-group">', - ' <button class="btn">Actions</button>', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Action 1</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + '</div>', + '<div class="btn-group">', + ' <button class="btn">Actions</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Action 1</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') - expect(triggerDropdownList.length).toEqual(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - const keyup = createEvent('keyup') - keyup.key = 'Tab' + const keyup = createEvent('keyup') + keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + document.dispatchEvent(keyup) + }) - triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') - triggerDropdownLast.click() - }) + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) - triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownLast.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - const keyup = createEvent('keyup') - keyup.key = 'Tab' + const keyup = createEvent('keyup') + keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + document.dispatchEvent(keyup) + }) - triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') - done() - }) + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) - triggerDropdownFirst.click() + triggerDropdownFirst.click() + }) }) - it('should fire hide and hidden event without a clickEvent if event type is not click', done => { + it('should be able to identify clicked dropdown, even with multiple dropdowns in the same tag', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' <button id="dropdown1" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu1" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + ' <button id="dropdown2" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu2" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', ' </div>', '</div>' ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - - triggerDropdown.addEventListener('hide.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - }) + const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') + const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') + const dropdownMenu1 = fixtureEl.querySelector('#menu1') + const dropdownMenu2 = fixtureEl.querySelector('#menu2') + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() - triggerDropdown.addEventListener('hidden.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - done() - }) + dropdownToggle1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') + dropdownToggle2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) - keydown.key = 'Escape' - triggerDropdown.dispatchEvent(keydown) - }) + dropdownMenu1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) - triggerDropdown.click() + dropdownMenu2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) }) - it('should bubble up the events to the parent elements', done => { + it('should be able to show the proper menu, even with multiple dropdowns in the same tag', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' <button id="dropdown1" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu1" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + ' <button id="dropdown2" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu2" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', ' </div>', '</div>' ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownParent = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(triggerDropdown) + const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') + const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') + const dropdownMenu1 = fixtureEl.querySelector('#menu1') + const dropdownMenu2 = fixtureEl.querySelector('#menu2') - const showFunction = jasmine.createSpy('showFunction') - dropdownParent.addEventListener('show.bs.dropdown', showFunction) + dropdownToggle1.click() + expect(dropdownMenu1).toHaveClass('show') + expect(dropdownMenu2).not.toHaveClass('show') - const shownFunction = jasmine.createSpy('shownFunction') - dropdownParent.addEventListener('shown.bs.dropdown', () => { - shownFunction() - dropdown.hide() - }) + dropdownToggle2.click() + expect(dropdownMenu1).not.toHaveClass('show') + expect(dropdownMenu2).toHaveClass('show') + }) - const hideFunction = jasmine.createSpy('hideFunction') - dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + it('should fire hide and hidden event without a clickEvent if event type is not click', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + '</div>' + ].join('') - dropdownParent.addEventListener('hidden.bs.dropdown', () => { - expect(showFunction).toHaveBeenCalled() - expect(shownFunction).toHaveBeenCalled() - expect(hideFunction).toHaveBeenCalled() - done() - }) + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('hide.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + }) + + triggerDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') - dropdown.show() + keydown.key = 'Escape' + triggerDropdown.dispatchEvent(keydown) + }) + + triggerDropdown.click() + }) }) - it('should ignore keyboard events within <input>s and <textarea>s', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' <input type="text">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + it('should bubble up the events to the parent elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) - expect(document.activeElement).toEqual(input, 'input still focused') + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) - textarea.focus() - textarea.dispatchEvent(keydown) + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() + dropdown.show() }) - - triggerDropdown.click() }) - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', - ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should ignore keyboard events within <input>s and <textarea>s', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' <input type="text">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') - expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') - done() - }) + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<style>', - ' .d-none {', - ' display: none;', - ' }', - '</style>', - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <button class="dropdown-item d-none" type="button">Hidden button by class</button>', - ' <a class="dropdown-item" href="#sub1" style="display: none">Hidden link</a>', - ' <a class="dropdown-item" href="#sub1" style="visibility: hidden">Hidden link</a>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', + ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('d-none')).toEqual(false, '.d-none not focused') - expect(document.activeElement.style.display).not.toBe('none', '"display: none" not focused') - expect(document.activeElement.style.visibility).not.toBe('hidden', '"visibility: hidden" not focused') + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<style>', + ' .d-none {', + ' display: none;', + ' }', + '</style>', + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <button class="dropdown-item d-none" type="button">Hidden button by class</button>', + ' <a class="dropdown-item" href="#sub1" style="display: none">Hidden link</a>', + ' <a class="dropdown-item" href="#sub1" style="visibility: hidden">Hidden link</a>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + triggerDropdown.dispatchEvent(keydown) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + resolve() + }) - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const lastItem = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') + + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' + + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + resolve() }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.click() + }) }) - it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const firstItem = fixtureEl.querySelector('#item1') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(firstItem, 'item1 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <input type="text">', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') - input.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - input.dispatchEvent(createEvent('click')) + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) }) - - triggerDropdown.click() }) - it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <input type="text">', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - textarea.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' </div>', - '</div>', - '<input type="text">' - ] + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true - })) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should ignore keyboard events for <input>s and <textarea>s within dropdown-menu, except for escape key', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' <input type="text">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' </div>', + '</div>', + '<input type="text">' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - const keydownSpace = createEvent('keydown') - keydownSpace.key = 'Space' + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for <input>s and <textarea>s within dropdown-menu, except for escape key', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' <input type="text">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) + } - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - input.focus() - input.dispatchEvent(keydownSpace) + test('Space', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key ArrowUp + test('ArrowUp', input) - textarea.focus() - textarea.dispatchEvent(keydownSpace) + test('ArrowUp', textarea) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + // Key ArrowDown + test('ArrowDown', input) - // Key ArrowUp - input.focus() - input.dispatchEvent(keydownArrowUp) + test('ArrowDown', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - textarea.focus() - textarea.dispatchEvent(keydownArrowUp) + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + triggerDropdown.click() + }) + }) - // Key ArrowDown - input.focus() - input.dispatchEvent(keydownArrowDown) + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="tabs">', + ' <div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' <a class="dropdown-item" href="#">Something else here</a>', + ' <div class="divider"></div>', + ' <a class="dropdown-item" href="#">Another link</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') - expect(document.activeElement).toEqual(input, 'input still focused') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') - textarea.focus() - textarea.dispatchEvent(keydownArrowDown) + const spy = spyOn(dropdown, 'toggle') - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) + + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="parent">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) - expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') - done() - }) + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - triggerDropdown.click() + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) }) - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '<div class="tabs">', - ' <div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' <a class="dropdown-item" href="#">Something else here</a>', - ' <div class="divider"></div>', - ' <a class="dropdown-item" href="#">Another link</a>', - ' </div>', - ' </div>', - '</div>' - ] + it('should not propagate escape key events if dropdown is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="parent">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - spyOn(dropdown, 'toggle') + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).not.toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown.classList.contains('show')).toEqual(false) - done() - }, 20) + toggle.click() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) }) - it('should propagate escape key events if dropdown is closed', done => { - fixtureEl.innerHTML = [ - '<div class="parent">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Some Item</a>', - ' </div>', - ' </div>', - '</div>' - ] + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + '</div>' + ].join('') + + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const parent = fixtureEl.querySelector('.parent') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() + toggle.click() }) + }) - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', - '</div>' - ] + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - dropdownMenu.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - })) + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - dropdownToggle.click() - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', - '</div>' - ] + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - document.documentElement.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - }) + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - dropdownToggle.click() + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.click() + }) }) - it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { + it('should be able to identify clicked dropdown, no matter the markup order', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', + ' </div>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', '</div>' - ] + ].join('') const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() - } - - expectDropdownToBeOpened(false) - }, 150) - - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + dropdownMenu.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) }) }) @@ -1963,7 +2277,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() }) }) @@ -1984,7 +2298,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) }) @@ -1993,7 +2307,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() const dropdown = Dropdown.getOrCreateInstance(div, { display: 'dynamic' }) @@ -2021,52 +2335,54 @@ describe('Dropdown', () => { }) }) - it('should open dropdown when pressing keydown or keyup', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', - ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', + ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - const keyup = createEvent('keyup') - keyup.key = 'ArrowUp' + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' - const handleArrowDown = () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true) - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true) - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - } - - dropdown.addEventListener('shown.bs.dropdown', event => { - if (event.target.key === 'ArrowDown') { - handleArrowDown() - } else { - handleArrowUp() + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) } - }) - triggerDropdown.dispatchEvent(keydown) + const handleArrowUp = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) }) it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { @@ -2092,27 +2408,29 @@ describe('Dropdown', () => { expect(delegatedClickListener).toHaveBeenCalled() }) - it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { - fixtureEl.innerHTML = [ - '<div class="container">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', - ' </div>', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const childElement = fixtureEl.querySelector('#childElement') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') - btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - })) + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) - childElement.click() + childElement.click() + }) }) }) |
