From 3ea39841c8049525e31e9f4d6300f0c60cdb42de Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 24 Jan 2023 13:33:51 +0100 Subject: Adding upstream version 5.2.3+dfsg. Signed-off-by: Daniel Baumann --- js/tests/unit/dropdown.spec.js | 2430 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2430 insertions(+) create mode 100644 js/tests/unit/dropdown.spec.js (limited to 'js/tests/unit/dropdown.spec.js') diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js new file mode 100644 index 0000000..2bbd7c0 --- /dev/null +++ b/js/tests/unit/dropdown.spec.js @@ -0,0 +1,2430 @@ +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' + +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 = [ + '' + ].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 = [ + '' + ].join('') + + const dropdownElem = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(dropdownElem) + + dropdownElem.addEventListener('shown.bs.dropdown', () => { + resolve() + }) + + dropdown.show() + }) + }) + + it('should create offset modifier correctly when offset option is a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '', + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '
', + ' ', + ' ', + '
' + ].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 = [ + '
', + ' ', + ' ', + '
' + ].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 = [ + '
', + ' ', + ' ', + '
' + ].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 = [ + '
', + ' ', + ' ', + '
' + ].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 = [ + '
', + ' ', + ' ', + '
' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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 = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].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 = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].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) + + const keyup = createEvent('keyup') + keyup.key = 'Tab' + + document.dispatchEvent(keyup) + }) + + 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) + + const keyup = createEvent('keyup') + keyup.key = 'Tab' + + document.dispatchEvent(keyup) + }) + + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) + + triggerDropdownFirst.click() + }) + }) + + it('should be able to identify clicked dropdown, even with multiple dropdowns in the same tag', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + 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() + + dropdownToggle1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) + + dropdownToggle2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) + + dropdownMenu1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) + + dropdownMenu2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) + }) + + it('should be able to show the proper menu, even with multiple dropdowns in the same tag', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') + const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') + const dropdownMenu1 = fixtureEl.querySelector('#menu1') + const dropdownMenu2 = fixtureEl.querySelector('#menu2') + + dropdownToggle1.click() + expect(dropdownMenu1).toHaveClass('show') + expect(dropdownMenu2).not.toHaveClass('show') + + dropdownToggle2.click() + expect(dropdownMenu1).not.toHaveClass('show') + expect(dropdownMenu2).toHaveClass('show') + }) + + it('should fire hide and hidden event without a clickEvent if event type is not click', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + 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') + + keydown.key = 'Escape' + triggerDropdown.dispatchEvent(keydown) + }) + + triggerDropdown.click() + }) + }) + + it('should bubble up the events to the parent elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) + + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) + + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) + + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) + + dropdown.show() + }) + }) + + it('should ignore keyboard events within s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') + + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydown) + + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') + + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') + + 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() + }) + + triggerDropdown.click() + }) + }) + + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) + }) + + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) + }) + }) + + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + }) + + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') + + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + }) + + 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 = [ + '', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) + + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for s and ', + ' ', + '' + ].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 keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) + + test('Space', textarea) + + // Key ArrowUp + test('ArrowUp', input) + + test('ArrowUp', textarea) + + // Key ArrowDown + test('ArrowDown', input) + + test('ArrowDown', textarea) + + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) + + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) + + triggerDropdown.click() + }) + }) + + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + + const spy = spyOn(dropdown, 'toggle') + + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.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 = [ + '
', + ' ', + '
' + ].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() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) + + it('should not propagate escape key events if dropdown is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].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).not.toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.click() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) + }) + + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + 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) + }) + + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) + + toggle.click() + }) + }) + + 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 = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) + + dropdownToggle.click() + }) + }) + + 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 = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) + + 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"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + 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 be able to identify clicked dropdown, no matter the markup order', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() + + dropdownToggle.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + dropdownMenu.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + }) + }) + + describe('jQueryInterface', () => { + it('should create a dropdown', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.dropdown.call(jQueryMock) + + expect(Dropdown.getInstance(div)).not.toBeNull() + }) + + it('should not re create a dropdown', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div) + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.dropdown.call(jQueryMock) + + expect(Dropdown.getInstance(div)).toEqual(dropdown) + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.dropdown.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + }) + + describe('getInstance', () => { + it('should return dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div) + + expect(Dropdown.getInstance(div)).toEqual(dropdown) + expect(Dropdown.getInstance(div)).toBeInstanceOf(Dropdown) + }) + + it('should return null when there is no dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Dropdown.getInstance(div)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should return dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div) + + expect(Dropdown.getOrCreateInstance(div)).toEqual(dropdown) + expect(Dropdown.getInstance(div)).toEqual(Dropdown.getOrCreateInstance(div, {})) + expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) + }) + + it('should return new instance when there is no dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Dropdown.getInstance(div)).toBeNull() + expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) + }) + + it('should return new instance when there is no dropdown instance with given configuration', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Dropdown.getInstance(div)).toBeNull() + const dropdown = Dropdown.getOrCreateInstance(div, { + display: 'dynamic' + }) + expect(dropdown).toBeInstanceOf(Dropdown) + + expect(dropdown._config.display).toEqual('dynamic') + }) + + it('should return the instance when exists without given configuration', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div, { + display: 'dynamic' + }) + expect(Dropdown.getInstance(div)).toEqual(dropdown) + + const dropdown2 = Dropdown.getOrCreateInstance(div, { + display: 'static' + }) + expect(dropdown).toBeInstanceOf(Dropdown) + expect(dropdown2).toEqual(dropdown) + + expect(dropdown2._config.display).toEqual('dynamic') + }) + }) + + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' + + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) + } + + 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', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const clickListener = jasmine.createSpy('clickListener') + const delegatedClickListener = jasmine.createSpy('delegatedClickListener') + + btnDropdown.addEventListener('click', clickListener) + document.addEventListener('click', delegatedClickListener) + + btnDropdown.click() + + expect(clickListener).toHaveBeenCalled() + expect(delegatedClickListener).toHaveBeenCalled() + }) + + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') + + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) + + childElement.click() + }) + }) +}) -- cgit v1.2.3