import { describe, it, expect, beforeAll, afterEach, vi } from 'vitest'
import Modal from '../../src/bootstrap/modal'
import { clearFixture, getFixture } from '../helpers/fixture'
describe('Modal', () => {
let fixtureEl: HTMLElement
beforeAll(() => {
fixtureEl = getFixture()
})
afterEach(() => {
clearFixture()
vi.restoreAllMocks()
document.body.classList.remove('modal-open')
for (const el of document.querySelectorAll('.modal-backdrop')) {
el.remove()
}
})
const createModalHTML = () =>
[
'
',
].join('')
describe('VERSION', () => {
it('should return plugin version', () => {
expect(typeof Modal.VERSION).toBe('string')
})
})
describe('Default', () => {
it('should return plugin default config', () => {
expect(Modal.Default).toBeDefined()
expect(Modal.Default.backdrop).toBe(true)
expect(Modal.Default.focus).toBe(true)
expect(Modal.Default.keyboard).toBe(true)
})
})
describe('DefaultType', () => {
it('should return plugin default type config', () => {
expect(Modal.DefaultType).toBeDefined()
expect(Modal.DefaultType.backdrop).toBe('(boolean|string)')
})
})
describe('NAME', () => {
it('should return plugin name', () => {
expect(Modal.NAME).toBe('modal')
})
})
describe('constructor', () => {
it('should create modal instance', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(modal).toBeInstanceOf(Modal)
expect(Modal.getInstance(modalEl)).toBe(modal)
})
it('should find dialog element', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(modal._dialog).not.toBeNull()
})
})
describe('toggle', () => {
it('should show when hidden', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
expect(modal._isShown).toBe(true)
resolve()
})
modal.toggle()
})
})
it('should hide when shown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidden.bs.modal', () => {
expect(modal._isShown).toBe(false)
resolve()
})
modal.toggle()
})
modal.show()
})
})
})
describe('show', () => {
it('should show modal', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
expect(modal._isShown).toBe(true)
expect(modalEl.classList.contains('show')).toBe(true)
expect(modalEl.getAttribute('aria-modal')).toBe('true')
expect(modalEl.getAttribute('role')).toBe('dialog')
expect(document.body.classList.contains('modal-open')).toBe(true)
resolve()
})
modal.show()
})
})
it('should not show if already shown', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
modal.show()
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modal.show()
})
})
it('should not show if show event is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('show.bs.modal', (event) => {
event.preventDefault()
setTimeout(() => {
expect(modal._isShown).toBe(false)
resolve()
}, 30)
})
modal.show()
})
})
it('should pass relatedTarget in show event', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML() + ''
const modalEl = fixtureEl.querySelector('.modal')!
const trigger = fixtureEl.querySelector('#trigger') as HTMLElement
const modal = new Modal(modalEl)
modalEl.addEventListener('show.bs.modal', (event: any) => {
expect(event.relatedTarget).toBe(trigger)
resolve()
})
modal.show(trigger)
})
})
})
describe('hide', () => {
it('should hide modal', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
modal.hide()
})
modalEl.addEventListener('hidden.bs.modal', () => {
expect(modal._isShown).toBe(false)
expect(modalEl.style.display).toBe('none')
expect(modalEl.getAttribute('aria-hidden')).toBe('true')
resolve()
})
modal.show()
})
})
it('should not hide if not shown', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modal.hide()
expect(modal._isShown).toBe(false)
})
it('should not hide if hide event is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hide.bs.modal', (event) => {
event.preventDefault()
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modal.hide()
})
modal.show()
})
})
})
describe('dispose', () => {
it('should dispose modal', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modal.dispose()
expect(Modal.getInstance(modalEl)).toBeNull()
})
})
describe('handleUpdate', () => {
it('should call _adjustDialog', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
const spy = vi.spyOn(modal, '_adjustDialog')
modal.handleUpdate()
expect(spy).toHaveBeenCalled()
})
})
describe('_isAnimated', () => {
it('should return true when fade class is present', () => {
fixtureEl.innerHTML = ''
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(modal._isAnimated()).toBe(true)
})
it('should return false without fade class', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(modal._isAnimated()).toBe(false)
})
})
describe('keyboard', () => {
it('should close on Escape when keyboard is true', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: true })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidden.bs.modal', () => {
expect(modal._isShown).toBe(false)
resolve()
})
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
})
modal.show()
})
})
it('should not close on Escape when keyboard is false', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: false })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modal.show()
})
})
it('should ignore non-Escape keys', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modal.show()
})
})
})
describe('getInstance', () => {
it('should return null if no instance', () => {
expect(Modal.getInstance(fixtureEl)).toBeNull()
})
it('should return modal instance', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(Modal.getInstance(modalEl)).toBe(modal)
})
})
describe('getOrCreateInstance', () => {
it('should return existing instance', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
expect(Modal.getOrCreateInstance(modalEl)).toBe(modal)
})
it('should create new instance', () => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
expect(Modal.getInstance(modalEl)).toBeNull()
expect(Modal.getOrCreateInstance(modalEl)).toBeInstanceOf(Modal)
})
})
describe('_triggerBackdropTransition', () => {
it('should add and remove modal-static class', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: false })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidePrevented.bs.modal', () => {
setTimeout(() => {
resolve()
}, 30)
})
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
})
modal.show()
})
})
it('should not transition if hidePrevented is prevented', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: false })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidePrevented.bs.modal', (event) => {
event.preventDefault()
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
})
modal.show()
})
})
it('should early return if overflowY is hidden', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: false })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.style.overflowY = 'hidden'
modalEl.addEventListener('hidePrevented.bs.modal', () => {
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
})
modal.show()
})
})
it('should early return if already has modal-static class', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { keyboard: false })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.classList.add('modal-static')
modalEl.addEventListener('hidePrevented.bs.modal', () => {
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 30)
})
modalEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
})
modal.show()
})
})
})
describe('backdrop click', () => {
it('should hide when clicking outside dialog with backdrop true', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { backdrop: true })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidden.bs.modal', () => {
expect(modal._isShown).toBe(false)
resolve()
})
modalEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
modalEl.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
modal.show()
})
})
it('should not hide when click starts inside dialog', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const dialog = modalEl.querySelector('.modal-dialog')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
const mousedownEvent = new MouseEvent('mousedown', { bubbles: true })
Object.defineProperty(mousedownEvent, 'target', { value: dialog })
modalEl.dispatchEvent(mousedownEvent)
modalEl.dispatchEvent(new MouseEvent('click', { bubbles: true }))
setTimeout(() => {
expect(modal._isShown).toBe(true)
resolve()
}, 50)
})
modal.show()
})
})
it('should trigger backdrop transition with static backdrop', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl, { backdrop: 'static' })
modalEl.addEventListener('shown.bs.modal', () => {
modalEl.addEventListener('hidePrevented.bs.modal', () => {
expect(modal._isShown).toBe(true)
resolve()
})
modalEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
modalEl.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
modal.show()
})
})
})
describe('_showElement', () => {
it('should append to body if not already in DOM', () => {
const modalEl = document.createElement('div')
modalEl.classList.add('modal')
modalEl.setAttribute('tabindex', '-1')
modalEl.innerHTML = ''
const modal = new Modal(modalEl)
return new Promise((resolve) => {
modalEl.addEventListener('shown.bs.modal', () => {
expect(document.body.contains(modalEl)).toBe(true)
modal.dispose()
modalEl.remove()
resolve()
})
modal.show()
})
})
it('should scroll modal body to top', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = createModalHTML()
const modalEl = fixtureEl.querySelector('.modal')!
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
expect(modalEl.scrollTop).toBe(0)
resolve()
})
modal.show()
})
})
})
describe('data-tblr-toggle', () => {
it('should open modal via data-tblr-toggle="modal"', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ''].join('')
const modalEl = fixtureEl.querySelector('#testModal')!
const btn = fixtureEl.querySelector('[data-tblr-toggle="modal"]') as HTMLElement
modalEl.addEventListener('shown.bs.modal', () => {
const modal = Modal.getInstance(modalEl) as Modal
expect(modal._isShown).toBe(true)
resolve()
})
btn.click()
})
})
it('should open modal via data-tblr-toggle with data-tblr-target', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = ['', ''].join('')
const modalEl = fixtureEl.querySelector('#testModal')!
const btn = fixtureEl.querySelector('[data-tblr-toggle="modal"]') as HTMLElement
modalEl.addEventListener('shown.bs.modal', () => {
const modal = Modal.getInstance(modalEl) as Modal
expect(modal._isShown).toBe(true)
resolve()
})
btn.click()
})
})
})
describe('data-tblr-dismiss', () => {
it('should close modal via data-tblr-dismiss="modal"', () => {
return new Promise((resolve) => {
fixtureEl.innerHTML = [''].join('')
const modalEl = fixtureEl.querySelector('.modal')!
const dismissBtn = fixtureEl.querySelector('[data-tblr-dismiss="modal"]') as HTMLElement
const modal = new Modal(modalEl)
modalEl.addEventListener('shown.bs.modal', () => {
dismissBtn.click()
})
modalEl.addEventListener('hidden.bs.modal', () => {
expect(modal._isShown).toBe(false)
resolve()
})
modal.show()
})
})
})
})