UNPKG

uba-bootstrap-theme

Version:
1,432 lines (1,181 loc) 85.9 kB
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 beforeAll(() => { fixtureEl = getFixture() }) afterEach(() => { clearFixture() }) describe('VERSION', () => { it('should return plugin version', () => { expect(Dropdown.VERSION).toEqual(jasmine.any(String)) }) }) describe('Default', () => { it('should return plugin default config', () => { expect(Dropdown.Default).toEqual(jasmine.any(Object)) }) }) describe('DefaultType', () => { it('should return plugin default type config', () => { expect(Dropdown.DefaultType).toEqual(jasmine.any(Object)) }) }) describe('DATA_KEY', () => { it('should return plugin data key', () => { expect(Dropdown.DATA_KEY).toEqual('bs.dropdown') }) }) describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { 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="#">Link</a>', ' </div>', '</div>' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]') const dropdownByElement = new Dropdown(btnDropdown) expect(dropdownBySelector._element).toEqual(btnDropdown) expect(dropdownByElement._element).toEqual(btnDropdown) }) 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 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() expect(typeof offset).toEqual('function') dropdown.show() }) }) it('should create offset modifier correctly when offset option is a string into data attribute', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-offset="10,20">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) expect(dropdown._getOffset()).toEqual([10, 20]) }) it('should allow to pass config to Popper with `popperConfig`', () => { 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, { popperConfig: { placement: 'left' } }) const popperConfig = dropdown._getPopperConfig() expect(popperConfig.placement).toEqual('left') }) 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>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', '</div>' ].join('') const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' }) const dropdown = new Dropdown(btnDropdown, { popperConfig: getPopperConfig }) const popperConfig = dropdown._getPopperConfig() expect(getPopperConfig).toHaveBeenCalled() expect(popperConfig.placement).toEqual('left') }) }) describe('toggle', () => { 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() }) dropdown.toggle() }) }) 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() }) secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { expect(dropdown1._popper.destroy).toHaveBeenCalled() resolve() })) dropdown1.toggle() }) }) 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('') const defaultValueOnTouchStart = document.documentElement.ontouchstart const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) document.documentElement.ontouchstart = noop const spy = spyOn(EventHandler, 'on') const spyOff = spyOn(EventHandler, 'off') btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) dropdown.toggle() }) 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() }) }) 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('') 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() }) dropdown.toggle() }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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-center') const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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 dropupEl = fixtureEl.querySelector('.dropup') const dropdown = new Dropdown(btnDropdown) dropupEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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 dropendEl = fixtureEl.querySelector('.dropend') const dropdown = new Dropdown(btnDropdown) dropendEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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 dropstartEl = fixtureEl.querySelector('.dropstart') const dropdown = new Dropdown(btnDropdown) dropstartEl.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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: 'parent' }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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: fixtureEl }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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 dropdown = new Dropdown(btnDropdown, { reference: { 0: fixtureEl, jquery: 'jQuery' } }) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') resolve() }) dropdown.toggle() }) }) 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 } } } expect(() => new Dropdown(btnDropdown, { reference: {} })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') 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', () => { 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) 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) btnDropdown.addEventListener('show.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.toggle() setTimeout(() => { expect().nothing() resolve() }) }) }) }) describe('show', () => { 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() }) dropdown.show() }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) 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) btnDropdown.addEventListener('show.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('shown.bs.dropdown', () => { reject(new Error('should not throw shown.bs.dropdown event')) }) dropdown.show() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) }) describe('hide', () => { 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() }) dropdown.hide() }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { spyOn(dropdown._popper, 'destroy') dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(dropdown._popper.destroy).toHaveBeenCalled() resolve() }) dropdown.show() }) }) 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) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }, 10) }) }) 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) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }, 10) }) }) 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) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect().nothing() resolve() }, 10) }) }) 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) btnDropdown.addEventListener('hide.bs.dropdown', event => { event.preventDefault() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { reject(new Error('should not throw hidden.bs.dropdown event')) }) dropdown.hide() setTimeout(() => { expect(dropdownMenu).toHaveClass('show') resolve() }) }) }) 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) document.documentElement.ontouchstart = noop const spy = spyOn(EventHandler, 'off') btnDropdown.addEventListener('shown.bs.dropdown', () => { dropdown.hide() }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown).not.toHaveClass('show') expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') expect(spy).toHaveBeenCalled() document.documentElement.ontouchstart = defaultValueOnTouchStart resolve() }) dropdown.show() }) }) }) describe('dispose', () => { it('should dispose dropdown', () => { 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) expect(dropdown._popper).toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() const spy = spyOn(EventHandler, 'off') dropdown.dispose() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() expect(spy).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) }) it('should dispose dropdown with Popper', () => { 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) dropdown.toggle() expect(dropdown._popper).not.toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() dropdown.dispose() expect(dropdown._popper).toBeNull() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() }) }) describe('update', () => { it('should call Popper and detect navbar on update', () => { 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) dropdown.toggle() expect(dropdown._popper).not.toBeNull() const spyUpdate = spyOn(dropdown._popper, 'update') const spyDetect = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(spyUpdate).toHaveBeenCalled() expect(spyDetect).toHaveBeenCalled() }) it('should just detect navbar on update', () => { 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 spy = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(dropdown._popper).toBeNull() expect(spy).toHaveBeenCalled() }) }) describe('data-api', () => { 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('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('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.click() }) }) 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) btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(dropdown._popper).not.toBeNull() expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') resolve() }) dropdown.show() }) }) 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 hideSpy = spyOn(dropdown, '_completeHide') btnDropdown.addEventListener('shown.bs.dropdown', () => { const clickEvent = new MouseEvent('click', { bubbles: true }) dropdownMenu.querySelector('option').dispatchEvent(clickEvent) }) dropdownMenu.addEventListener('click', event => { expect(event.target.tagName).toMatch(/select|option/i) Dropdown.clearMenus(event) setTimeout(() => { expect(hideSpy).not.toHaveBeenCalled() resolve() }, 10) }) dropdown.show() }) }) 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) 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() resolve() }) dropdown.show() }) }) 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') btnDropdown.addEventListener('shown.bs.dropdown', () => { // Popper adds this attribute when we use it expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() resolve() }) btnDropdown.click() }) }) 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) 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() resolve() }) dropdown.show() }) }) 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"]') btnDropdown.addEventListener('shown.bs.dropdown', () => { expect(btnDropdown).toHaveClass('show') const keyup = createEvent('keyup') keyup.key = 'Tab' document.dispatchEvent(keyup) }) btnDropdown.addEventListener('hidden.bs.dropdown', () => { expect(btnDropdown).not.toHaveClass('show') resolve() }) btnDropdown.click() }) }) 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"]') expect(triggerDropdownList).toHaveSize(2) const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList 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')).toHaveSize(0) triggerDropdownLast.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')).toHaveSize(0) resolve() }) triggerDropdownFirst.click() }) }) 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