import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
import Dropdown from '../../src/bootstrap/dropdown'
import { clearFixture, getFixture } from '../helpers/fixture'
vi.mock('@popperjs/core', () => ({
createPopper: vi.fn(() => ({
destroy: vi.fn(),
update: vi.fn(),
setOptions: vi.fn(),
})),
}))
describe('Dropdown', () => {
let fixtureEl: HTMLElement
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
vi.restoreAllMocks()
})
describe('VERSION', () => {
it('should return plugin version', () => {
expect(typeof Dropdown.VERSION).toBe('string')
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(typeof Dropdown.Default).toBe('object')
})
})
describe('DefaultType', () => {
it('should return plugin default type config', () => {
expect(typeof Dropdown.DefaultType).toBe('object')
})
})
describe('DATA_KEY', () => {
it('should return plugin data key', () => {
expect(Dropdown.DATA_KEY).toBe('bs.dropdown')
})
})
describe('constructor', () => {
it('should accept element as CSS selector or DOM element', () => {
fixtureEl.innerHTML = ['
', ' Dropdown ', ' ', '
'].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).toBe(btnDropdown)
expect(dropdownByElement._element).toBe(btnDropdown)
})
it('should create offset from string data attribute', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
expect(dropdown._getOffset()).toEqual([10, 20])
})
it('should allow popperConfig override', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown, {
popperConfig: { placement: 'left' },
})
const popperConfig = dropdown._getPopperConfig()
expect(popperConfig.placement).toBe('left')
})
})
describe('toggle', () => {
it('should toggle a dropdown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
expect(btnDropdown.getAttribute('aria-expanded')).toBe('true')
resolve()
})
dropdown.toggle()
})
})
it('should toggle dropup', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
})
dropdown.toggle()
})
})
it('should toggle dropend', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
})
dropdown.toggle()
})
})
it('should toggle dropstart', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
})
dropdown.toggle()
})
})
})
describe('show', () => {
it('should show a dropdown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
})
dropdown.show()
})
})
it('should not show if disabled', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
dropdown.show()
expect(btnDropdown.classList.contains('show')).toBe(false)
})
it('should not show if already shown', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
const spy = vi.fn()
btnDropdown.addEventListener('shown.bs.dropdown', spy)
dropdown.show()
expect(spy).not.toHaveBeenCalled()
})
it('should not show if show event is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('show.bs.dropdown', (ev) => {
ev.preventDefault()
setTimeout(() => {
expect(btnDropdown.classList.contains('show')).toBe(false)
resolve()
}, 30)
})
dropdown.show()
})
})
})
describe('hide', () => {
it('should hide a dropdown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
dropdown.hide()
})
btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(false)
expect(btnDropdown.getAttribute('aria-expanded')).toBe('false')
resolve()
})
dropdown.show()
})
})
it('should not hide if not shown', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
const spy = vi.fn()
btnDropdown.addEventListener('hidden.bs.dropdown', spy)
dropdown.hide()
expect(spy).not.toHaveBeenCalled()
})
it('should not hide if hide event is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
btnDropdown.addEventListener('hide.bs.dropdown', (ev) => {
ev.preventDefault()
setTimeout(() => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.hide()
})
dropdown.show()
})
})
})
describe('dispose', () => {
it('should dispose a dropdown', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
expect(Dropdown.getInstance(btnDropdown)).not.toBeNull()
dropdown.dispose()
expect(Dropdown.getInstance(btnDropdown)).toBeNull()
})
})
describe('update', () => {
it('should call update on popper', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
btnDropdown.addEventListener('shown.bs.dropdown', () => {
dropdown.update()
expect(dropdown._popper!.update).toHaveBeenCalled()
resolve()
})
dropdown.show()
})
})
})
describe('getInstance', () => {
it('should return null if no instance', () => {
expect(Dropdown.getInstance(fixtureEl)).toBeNull()
})
it('should return dropdown instance', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
expect(Dropdown.getInstance(btnDropdown)).toBe(dropdown)
})
})
describe('getOrCreateInstance', () => {
it('should return dropdown instance', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
expect(Dropdown.getOrCreateInstance(btnDropdown)).toBe(dropdown)
})
it('should return new instance when there is no instance', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
expect(Dropdown.getInstance(btnDropdown)).toBeNull()
expect(Dropdown.getOrCreateInstance(btnDropdown)).toBeInstanceOf(Dropdown)
})
})
describe('data-api', () => {
it('should toggle via click on data-bs-toggle', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
resolve()
})
btnDropdown.click()
})
})
})
describe('_getPlacement', () => {
it('should return right for dropend', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
const placement = dropdown._getPlacement()
expect(placement).toMatch(/right|left/)
})
it('should return left for dropstart', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
const placement = dropdown._getPlacement()
expect(placement).toMatch(/right|left/)
})
it('should return top for dropup-center', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
expect(dropdown._getPlacement()).toBe('top')
})
it('should return bottom for dropdown-center', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
expect(dropdown._getPlacement()).toBe('bottom')
})
it('should return top-start for dropup', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
const placement = dropdown._getPlacement()
expect(placement).toMatch(/top/)
})
it('should return bottom-end when --bs-position is end', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const menu = fixtureEl.querySelector('.dropdown-menu') as HTMLElement
const dropdown = new Dropdown(btn)
const originalGetComputedStyle = window.getComputedStyle
vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
const result = originalGetComputedStyle(el)
if (el === menu) {
return new Proxy(result, {
get(target, prop) {
if (prop === 'getPropertyValue') {
return (name: string) => (name === '--bs-position' ? 'end' : target.getPropertyValue(name))
}
return (target as any)[prop]
},
}) as CSSStyleDeclaration
}
return result
})
expect(dropdown._getPlacement()).toBe('bottom-end')
})
it('should return top-end for dropup with --bs-position end', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const menu = fixtureEl.querySelector('.dropdown-menu') as HTMLElement
const dropdown = new Dropdown(btn)
const originalGetComputedStyle = window.getComputedStyle
vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
const result = originalGetComputedStyle(el)
if (el === menu) {
return new Proxy(result, {
get(target, prop) {
if (prop === 'getPropertyValue') {
return (name: string) => (name === '--bs-position' ? 'end' : target.getPropertyValue(name))
}
return (target as any)[prop]
},
}) as CSSStyleDeclaration
}
return result
})
expect(dropdown._getPlacement()).toBe('top-end')
})
})
describe('_detectNavbar', () => {
it('should detect when inside navbar', () => {
fixtureEl.innerHTML = ['', ' ', ' Dropdown ', ' ', '
', ' '].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
expect(dropdown._inNavbar).toBe(true)
})
it('should detect when not inside navbar', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
expect(dropdown._inNavbar).toBe(false)
})
})
describe('_getConfig', () => {
it('should throw on invalid reference object', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
expect(() => {
new Dropdown(btn, { reference: {} as any })
}).toThrow(TypeError)
})
})
describe('_getOffset', () => {
it('should handle function offset', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const offsetFn = vi.fn().mockReturnValue([10, 20])
const dropdown = new Dropdown(btn, { offset: offsetFn })
const offset = dropdown._getOffset()
expect(typeof offset).toBe('function')
})
it('should handle array offset', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { offset: [5, 10] })
expect(dropdown._getOffset()).toEqual([5, 10])
})
it('should handle string offset', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { offset: '10,20' as any })
expect(dropdown._getOffset()).toEqual([10, 20])
})
})
describe('_getPopperConfig', () => {
it('should set popper static in navbar', () => {
fixtureEl.innerHTML = ['', ' ', ' Dropdown ', ' ', '
', ' '].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
const config = dropdown._getPopperConfig()
expect(config.modifiers).toBeDefined()
expect(config.modifiers!.some((m: any) => m.name === 'applyStyles' && m.enabled === false)).toBe(true)
})
it('should set popper static when display is static', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { display: 'static' })
const config = dropdown._getPopperConfig()
expect(config.modifiers).toBeDefined()
expect(config.modifiers!.some((m: any) => m.name === 'applyStyles' && m.enabled === false)).toBe(true)
})
it('should apply custom popperConfig function', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const customPopperConfig = (_defaultConfig: any) => ({ ..._defaultConfig, strategy: 'fixed' as const })
const dropdown = new Dropdown(btn, { popperConfig: customPopperConfig })
const config = dropdown._getPopperConfig()
expect(config.strategy).toBe('fixed')
})
})
describe('clearMenus', () => {
it('should ignore right mouse button', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const event = new MouseEvent('click', { button: 2, bubbles: true })
Dropdown.clearMenus(event)
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
expect(btn.classList.contains('show')).toBe(true)
})
})
describe('show with touch device', () => {
it('should add mouseover listeners on touch device', () => {
return new Promise((resolve) => {
Object.defineProperty(document.documentElement, 'ontouchstart', {
value: null,
writable: true,
configurable: true,
})
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(true)
btn.addEventListener('hidden.bs.dropdown', () => {
delete (document.documentElement as any).ontouchstart
resolve()
})
dropdown.hide()
})
dropdown.show()
})
})
})
describe('_completeHide', () => {
it('should not hide if hide event is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hide.bs.dropdown', (event) => {
event.preventDefault()
})
dropdown._completeHide({ relatedTarget: btn })
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should remove touch listeners on hide', () => {
return new Promise((resolve) => {
Object.defineProperty(document.documentElement, 'ontouchstart', {
value: null,
writable: true,
configurable: true,
})
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(false)
delete (document.documentElement as any).ontouchstart
resolve()
})
dropdown._completeHide({ relatedTarget: btn })
})
dropdown.show()
})
})
})
describe('_selectMenuItem', () => {
it('should do nothing if no visible items', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
expect(() => {
dropdown._selectMenuItem({ key: 'ArrowDown', target: btn as HTMLElement })
}).not.toThrow()
})
it('should focus an item on ArrowDown when items are visible', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
dropdown._selectMenuItem({ key: 'ArrowDown', target: btn as HTMLElement })
})
})
describe('_createPopper with reference', () => {
it('should use parent as reference', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { reference: 'parent' })
btn.addEventListener('shown.bs.dropdown', () => {
expect(dropdown._popper).not.toBeNull()
resolve()
})
dropdown.show()
})
})
it('should use element reference', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { reference: fixtureEl })
btn.addEventListener('shown.bs.dropdown', () => {
expect(dropdown._popper).not.toBeNull()
resolve()
})
dropdown.show()
})
})
it('should use virtual element reference', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const virtualRef = {
getBoundingClientRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => {} }),
}
const dropdown = new Dropdown(btn, { reference: virtualRef as any })
btn.addEventListener('shown.bs.dropdown', () => {
expect(dropdown._popper).not.toBeNull()
resolve()
})
dropdown.show()
})
})
})
describe('dispose with popper', () => {
it('should destroy popper on dispose', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
const destroySpy = dropdown._popper!.destroy
dropdown.dispose()
expect(destroySpy).toHaveBeenCalled()
resolve()
})
dropdown.show()
})
})
})
describe('clearMenus (full flow)', () => {
it('should close open dropdown on outside click', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
', 'outside
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(true)
btn.addEventListener('hidden.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(false)
resolve()
})
const outsideEl = fixtureEl.querySelector('#outside')!
outsideEl.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
dropdown.show()
})
})
it('should skip autoClose=false dropdown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { autoClose: false })
btn.addEventListener('shown.bs.dropdown', () => {
const event = new MouseEvent('click', { bubbles: true })
document.dispatchEvent(event)
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should ignore keyup that is not Tab', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const event = new KeyboardEvent('keyup', { key: 'Escape', bubbles: true })
Dropdown.clearMenus(event)
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
expect(btn.classList.contains('show')).toBe(true)
})
it('should add clickEvent on click type event', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
resolve()
})
document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
dropdown.show()
})
})
it('should not close autoClose=inside when clicked outside', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
', 'outside
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const dropdown = new Dropdown(btn, { autoClose: 'inside' })
btn.addEventListener('shown.bs.dropdown', () => {
const outside = fixtureEl.querySelector('#outside')!
outside.dispatchEvent(new MouseEvent('click', { bubbles: true }))
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should not close autoClose=outside when clicked inside menu', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const menu = fixtureEl.querySelector('.dropdown-menu')!
const dropdown = new Dropdown(btn, { autoClose: 'outside' })
btn.addEventListener('shown.bs.dropdown', () => {
menu.querySelector('.dropdown-item')!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should not close when click composedPath includes toggle element', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
const event = new MouseEvent('click', { bubbles: true })
Object.defineProperty(event, 'composedPath', {
value: () => [btn, fixtureEl, document.body, document.documentElement, document],
configurable: true,
})
Dropdown.clearMenus(event)
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should not close on Tab keyup inside input within menu', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const menu = fixtureEl.querySelector('.dropdown-menu')!
const input = fixtureEl.querySelector('input') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
const tabEvent = new KeyboardEvent('keyup', { key: 'Tab', bubbles: true })
Object.defineProperty(tabEvent, 'composedPath', {
value: () => [input, menu, fixtureEl, document.body, document.documentElement, document],
configurable: true,
})
Object.defineProperty(tabEvent, 'target', { value: input, configurable: true })
Dropdown.clearMenus(tabEvent)
setTimeout(() => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
}, 30)
})
dropdown.show()
})
})
it('should close on Tab keyup targeting element outside menu', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
', 'Outside '].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')!
const outsideBtn = fixtureEl.querySelector('#outside-btn') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
resolve()
})
const tabEvent = new KeyboardEvent('keyup', { key: 'Tab', bubbles: true })
Object.defineProperty(tabEvent, 'composedPath', {
value: () => [outsideBtn, fixtureEl, document.body, document.documentElement, document],
configurable: true,
})
Object.defineProperty(tabEvent, 'target', { value: outsideBtn, configurable: true })
Dropdown.clearMenus(tabEvent)
})
dropdown.show()
})
})
})
describe('dataApiKeydownHandler', () => {
it('should ignore non-arrow/escape keys', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
const preventSpy = vi.spyOn(Event.prototype, 'preventDefault')
const keydown = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
btn.dispatchEvent(keydown)
expect(preventSpy).not.toHaveBeenCalled()
})
it('should ignore arrow keys in input', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const input = fixtureEl.querySelector('input') as HTMLElement
const preventSpy = vi.spyOn(Event.prototype, 'preventDefault')
const keydown = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
Object.defineProperty(keydown, 'target', { value: input, configurable: true })
input.dispatchEvent(keydown)
expect(preventSpy).not.toHaveBeenCalled()
})
it('should open dropdown on ArrowDown and call _selectMenuItem', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
btn.addEventListener('shown.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
})
const keydown = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
btn.dispatchEvent(keydown)
})
})
it('should close dropdown on Escape', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(false)
resolve()
})
const keydown = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
btn.dispatchEvent(keydown)
})
dropdown.show()
})
})
it('should allow Escape in input/textarea', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
const textarea = fixtureEl.querySelector('textarea') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(false)
resolve()
})
const keydown = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
textarea.dispatchEvent(keydown)
})
dropdown.show()
})
})
it('should handle ArrowUp to show and select', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
btn.addEventListener('shown.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(true)
resolve()
})
const keydown = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
btn.dispatchEvent(keydown)
})
})
it('should find toggle from menu item via keydown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btn = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') as HTMLElement
const menu = fixtureEl.querySelector('.dropdown-menu') as HTMLElement
const dropdown = new Dropdown(btn)
btn.addEventListener('shown.bs.dropdown', () => {
btn.addEventListener('hidden.bs.dropdown', () => {
expect(btn.classList.contains('show')).toBe(false)
resolve()
})
const keydown = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
menu.dispatchEvent(keydown)
})
dropdown.show()
})
})
})
describe('data-tblr-toggle', () => {
it('should toggle via data-tblr-toggle="dropdown"', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-tblr-toggle="dropdown"]') as HTMLElement
btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toBe(true)
expect(btnDropdown.getAttribute('aria-expanded')).toBe('true')
resolve()
})
btnDropdown.click()
})
})
it('should create instance via data-tblr-toggle', () => {
fixtureEl.innerHTML = ['', ' Dropdown ', ' ', '
'].join('')
const btnDropdown = fixtureEl.querySelector('[data-tblr-toggle="dropdown"]')!
const dropdown = new Dropdown(btnDropdown)
expect(Dropdown.getInstance(btnDropdown)).toBe(dropdown)
})
})
})