modal.spec.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. import Modal from '../../src/modal'
  2. import EventHandler from '../../src/dom/event-handler'
  3. import ScrollBarHelper from '../../src/util/scrollbar'
  4. /** Test helpers */
  5. import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
  6. describe('Modal', () => {
  7. let fixtureEl
  8. beforeAll(() => {
  9. fixtureEl = getFixture()
  10. })
  11. afterEach(() => {
  12. clearFixture()
  13. clearBodyAndDocument()
  14. document.body.classList.remove('modal-open')
  15. document.querySelectorAll('.modal-backdrop')
  16. .forEach(backdrop => {
  17. document.body.removeChild(backdrop)
  18. })
  19. })
  20. beforeEach(() => {
  21. clearBodyAndDocument()
  22. })
  23. describe('VERSION', () => {
  24. it('should return plugin version', () => {
  25. expect(Modal.VERSION).toEqual(jasmine.any(String))
  26. })
  27. })
  28. describe('Default', () => {
  29. it('should return plugin default config', () => {
  30. expect(Modal.Default).toEqual(jasmine.any(Object))
  31. })
  32. })
  33. describe('DATA_KEY', () => {
  34. it('should return plugin data key', () => {
  35. expect(Modal.DATA_KEY).toEqual('bs.modal')
  36. })
  37. })
  38. describe('constructor', () => {
  39. it('should take care of element either passed as a CSS selector or DOM element', () => {
  40. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  41. const modalEl = fixtureEl.querySelector('.modal')
  42. const modalBySelector = new Modal('.modal')
  43. const modalByElement = new Modal(modalEl)
  44. expect(modalBySelector._element).toEqual(modalEl)
  45. expect(modalByElement._element).toEqual(modalEl)
  46. })
  47. })
  48. describe('toggle', () => {
  49. it('should call ScrollBarHelper to handle scrollBar on body', done => {
  50. fixtureEl.innerHTML = [
  51. '<div class="modal"><div class="modal-dialog"></div></div>'
  52. ].join('')
  53. spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
  54. spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
  55. const modalEl = fixtureEl.querySelector('.modal')
  56. const modal = new Modal(modalEl)
  57. modalEl.addEventListener('shown.bs.modal', () => {
  58. expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled()
  59. modal.toggle()
  60. })
  61. modalEl.addEventListener('hidden.bs.modal', () => {
  62. expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled()
  63. done()
  64. })
  65. modal.toggle()
  66. })
  67. })
  68. describe('show', () => {
  69. it('should show a modal', done => {
  70. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  71. const modalEl = fixtureEl.querySelector('.modal')
  72. const modal = new Modal(modalEl)
  73. modalEl.addEventListener('show.bs.modal', e => {
  74. expect(e).toBeDefined()
  75. })
  76. modalEl.addEventListener('shown.bs.modal', () => {
  77. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  78. expect(modalEl.getAttribute('role')).toEqual('dialog')
  79. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  80. expect(modalEl.style.display).toEqual('block')
  81. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  82. done()
  83. })
  84. modal.show()
  85. })
  86. it('should show a modal without backdrop', done => {
  87. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  88. const modalEl = fixtureEl.querySelector('.modal')
  89. const modal = new Modal(modalEl, {
  90. backdrop: false
  91. })
  92. modalEl.addEventListener('show.bs.modal', e => {
  93. expect(e).toBeDefined()
  94. })
  95. modalEl.addEventListener('shown.bs.modal', () => {
  96. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  97. expect(modalEl.getAttribute('role')).toEqual('dialog')
  98. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  99. expect(modalEl.style.display).toEqual('block')
  100. expect(document.querySelector('.modal-backdrop')).toBeNull()
  101. done()
  102. })
  103. modal.show()
  104. })
  105. it('should show a modal and append the element', done => {
  106. const modalEl = document.createElement('div')
  107. const id = 'dynamicModal'
  108. modalEl.setAttribute('id', id)
  109. modalEl.classList.add('modal')
  110. modalEl.innerHTML = '<div class="modal-dialog"></div>'
  111. const modal = new Modal(modalEl)
  112. modalEl.addEventListener('shown.bs.modal', () => {
  113. const dynamicModal = document.getElementById(id)
  114. expect(dynamicModal).not.toBeNull()
  115. dynamicModal.parentNode.removeChild(dynamicModal)
  116. done()
  117. })
  118. modal.show()
  119. })
  120. it('should do nothing if a modal is shown', () => {
  121. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  122. const modalEl = fixtureEl.querySelector('.modal')
  123. const modal = new Modal(modalEl)
  124. spyOn(EventHandler, 'trigger')
  125. modal._isShown = true
  126. modal.show()
  127. expect(EventHandler.trigger).not.toHaveBeenCalled()
  128. })
  129. it('should do nothing if a modal is transitioning', () => {
  130. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  131. const modalEl = fixtureEl.querySelector('.modal')
  132. const modal = new Modal(modalEl)
  133. spyOn(EventHandler, 'trigger')
  134. modal._isTransitioning = true
  135. modal.show()
  136. expect(EventHandler.trigger).not.toHaveBeenCalled()
  137. })
  138. it('should not fire shown event when show is prevented', done => {
  139. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  140. const modalEl = fixtureEl.querySelector('.modal')
  141. const modal = new Modal(modalEl)
  142. modalEl.addEventListener('show.bs.modal', e => {
  143. e.preventDefault()
  144. const expectedDone = () => {
  145. expect().nothing()
  146. done()
  147. }
  148. setTimeout(expectedDone, 10)
  149. })
  150. modalEl.addEventListener('shown.bs.modal', () => {
  151. throw new Error('shown event triggered')
  152. })
  153. modal.show()
  154. })
  155. it('should be shown after the first call to show() has been prevented while fading is enabled ', done => {
  156. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  157. const modalEl = fixtureEl.querySelector('.modal')
  158. const modal = new Modal(modalEl)
  159. let prevented = false
  160. modalEl.addEventListener('show.bs.modal', e => {
  161. if (!prevented) {
  162. e.preventDefault()
  163. prevented = true
  164. setTimeout(() => {
  165. modal.show()
  166. })
  167. }
  168. })
  169. modalEl.addEventListener('shown.bs.modal', () => {
  170. expect(prevented).toBeTrue()
  171. expect(modal._isAnimated()).toBeTrue()
  172. done()
  173. })
  174. modal.show()
  175. })
  176. it('should set is transitioning if fade class is present', done => {
  177. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  178. const modalEl = fixtureEl.querySelector('.modal')
  179. const modal = new Modal(modalEl)
  180. modalEl.addEventListener('show.bs.modal', () => {
  181. setTimeout(() => {
  182. expect(modal._isTransitioning).toEqual(true)
  183. })
  184. })
  185. modalEl.addEventListener('shown.bs.modal', () => {
  186. expect(modal._isTransitioning).toEqual(false)
  187. done()
  188. })
  189. modal.show()
  190. })
  191. it('should close modal when a click occurred on data-bs-dismiss="modal"', done => {
  192. fixtureEl.innerHTML = [
  193. '<div class="modal fade">',
  194. ' <div class="modal-dialog">',
  195. ' <div class="modal-header">',
  196. ' <button type="button" data-bs-dismiss="modal"></button>',
  197. ' </div>',
  198. ' </div>',
  199. '</div>'
  200. ].join('')
  201. const modalEl = fixtureEl.querySelector('.modal')
  202. const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
  203. const modal = new Modal(modalEl)
  204. spyOn(modal, 'hide').and.callThrough()
  205. modalEl.addEventListener('shown.bs.modal', () => {
  206. btnClose.click()
  207. })
  208. modalEl.addEventListener('hidden.bs.modal', () => {
  209. expect(modal.hide).toHaveBeenCalled()
  210. done()
  211. })
  212. modal.show()
  213. })
  214. it('should set .modal\'s scroll top to 0', done => {
  215. fixtureEl.innerHTML = [
  216. '<div class="modal fade">',
  217. ' <div class="modal-dialog">',
  218. ' </div>',
  219. '</div>'
  220. ].join('')
  221. const modalEl = fixtureEl.querySelector('.modal')
  222. const modal = new Modal(modalEl)
  223. modalEl.addEventListener('shown.bs.modal', () => {
  224. expect(modalEl.scrollTop).toEqual(0)
  225. done()
  226. })
  227. modal.show()
  228. })
  229. it('should set modal body scroll top to 0 if modal body do not exists', done => {
  230. fixtureEl.innerHTML = [
  231. '<div class="modal fade">',
  232. ' <div class="modal-dialog">',
  233. ' <div class="modal-body"></div>',
  234. ' </div>',
  235. '</div>'
  236. ].join('')
  237. const modalEl = fixtureEl.querySelector('.modal')
  238. const modalBody = modalEl.querySelector('.modal-body')
  239. const modal = new Modal(modalEl)
  240. modalEl.addEventListener('shown.bs.modal', () => {
  241. expect(modalBody.scrollTop).toEqual(0)
  242. done()
  243. })
  244. modal.show()
  245. })
  246. it('should not enforce focus if focus equal to false', done => {
  247. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  248. const modalEl = fixtureEl.querySelector('.modal')
  249. const modal = new Modal(modalEl, {
  250. focus: false
  251. })
  252. spyOn(modal, '_enforceFocus')
  253. modalEl.addEventListener('shown.bs.modal', () => {
  254. expect(modal._enforceFocus).not.toHaveBeenCalled()
  255. done()
  256. })
  257. modal.show()
  258. })
  259. it('should add listener when escape touch is pressed', done => {
  260. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  261. const modalEl = fixtureEl.querySelector('.modal')
  262. const modal = new Modal(modalEl)
  263. spyOn(modal, 'hide').and.callThrough()
  264. modalEl.addEventListener('shown.bs.modal', () => {
  265. const keydownEscape = createEvent('keydown')
  266. keydownEscape.key = 'Escape'
  267. modalEl.dispatchEvent(keydownEscape)
  268. })
  269. modalEl.addEventListener('hidden.bs.modal', () => {
  270. expect(modal.hide).toHaveBeenCalled()
  271. done()
  272. })
  273. modal.show()
  274. })
  275. it('should do nothing when the pressed key is not escape', done => {
  276. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  277. const modalEl = fixtureEl.querySelector('.modal')
  278. const modal = new Modal(modalEl)
  279. spyOn(modal, 'hide')
  280. const expectDone = () => {
  281. expect(modal.hide).not.toHaveBeenCalled()
  282. done()
  283. }
  284. modalEl.addEventListener('shown.bs.modal', () => {
  285. const keydownTab = createEvent('keydown')
  286. keydownTab.key = 'Tab'
  287. modalEl.dispatchEvent(keydownTab)
  288. setTimeout(expectDone, 30)
  289. })
  290. modal.show()
  291. })
  292. it('should adjust dialog on resize', done => {
  293. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  294. const modalEl = fixtureEl.querySelector('.modal')
  295. const modal = new Modal(modalEl)
  296. spyOn(modal, '_adjustDialog').and.callThrough()
  297. const expectDone = () => {
  298. expect(modal._adjustDialog).toHaveBeenCalled()
  299. done()
  300. }
  301. modalEl.addEventListener('shown.bs.modal', () => {
  302. const resizeEvent = createEvent('resize')
  303. window.dispatchEvent(resizeEvent)
  304. setTimeout(expectDone, 10)
  305. })
  306. modal.show()
  307. })
  308. it('should not close modal when clicking outside of modal-content if backdrop = false', done => {
  309. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  310. const modalEl = fixtureEl.querySelector('.modal')
  311. const modal = new Modal(modalEl, {
  312. backdrop: false
  313. })
  314. const shownCallback = () => {
  315. setTimeout(() => {
  316. expect(modal._isShown).toEqual(true)
  317. done()
  318. }, 10)
  319. }
  320. modalEl.addEventListener('shown.bs.modal', () => {
  321. modalEl.click()
  322. shownCallback()
  323. })
  324. modalEl.addEventListener('hidden.bs.modal', () => {
  325. throw new Error('Should not hide a modal')
  326. })
  327. modal.show()
  328. })
  329. it('should not close modal when clicking outside of modal-content if backdrop = static', done => {
  330. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  331. const modalEl = fixtureEl.querySelector('.modal')
  332. const modal = new Modal(modalEl, {
  333. backdrop: 'static'
  334. })
  335. const shownCallback = () => {
  336. setTimeout(() => {
  337. expect(modal._isShown).toEqual(true)
  338. done()
  339. }, 10)
  340. }
  341. modalEl.addEventListener('shown.bs.modal', () => {
  342. modalEl.click()
  343. shownCallback()
  344. })
  345. modalEl.addEventListener('hidden.bs.modal', () => {
  346. throw new Error('Should not hide a modal')
  347. })
  348. modal.show()
  349. })
  350. it('should close modal when escape key is pressed with keyboard = true and backdrop is static', done => {
  351. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  352. const modalEl = fixtureEl.querySelector('.modal')
  353. const modal = new Modal(modalEl, {
  354. backdrop: 'static',
  355. keyboard: true
  356. })
  357. const shownCallback = () => {
  358. setTimeout(() => {
  359. expect(modal._isShown).toEqual(false)
  360. done()
  361. }, 10)
  362. }
  363. modalEl.addEventListener('shown.bs.modal', () => {
  364. const keydownEscape = createEvent('keydown')
  365. keydownEscape.key = 'Escape'
  366. modalEl.dispatchEvent(keydownEscape)
  367. shownCallback()
  368. })
  369. modal.show()
  370. })
  371. it('should not close modal when escape key is pressed with keyboard = false', done => {
  372. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  373. const modalEl = fixtureEl.querySelector('.modal')
  374. const modal = new Modal(modalEl, {
  375. keyboard: false
  376. })
  377. const shownCallback = () => {
  378. setTimeout(() => {
  379. expect(modal._isShown).toEqual(true)
  380. done()
  381. }, 10)
  382. }
  383. modalEl.addEventListener('shown.bs.modal', () => {
  384. const keydownEscape = createEvent('keydown')
  385. keydownEscape.key = 'Escape'
  386. modalEl.dispatchEvent(keydownEscape)
  387. shownCallback()
  388. })
  389. modalEl.addEventListener('hidden.bs.modal', () => {
  390. throw new Error('Should not hide a modal')
  391. })
  392. modal.show()
  393. })
  394. it('should not overflow when clicking outside of modal-content if backdrop = static', done => {
  395. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>'
  396. const modalEl = fixtureEl.querySelector('.modal')
  397. const modal = new Modal(modalEl, {
  398. backdrop: 'static'
  399. })
  400. modalEl.addEventListener('shown.bs.modal', () => {
  401. modalEl.click()
  402. setTimeout(() => {
  403. expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight)
  404. done()
  405. }, 20)
  406. })
  407. modal.show()
  408. })
  409. it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => {
  410. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>'
  411. const modalEl = fixtureEl.querySelector('.modal')
  412. const modal = new Modal(modalEl, {
  413. backdrop: 'static'
  414. })
  415. modalEl.addEventListener('shown.bs.modal', () => {
  416. const spy = spyOn(modal, '_queueCallback').and.callThrough()
  417. modalEl.click()
  418. modalEl.click()
  419. setTimeout(() => {
  420. expect(spy).toHaveBeenCalledTimes(1)
  421. done()
  422. }, 20)
  423. })
  424. modal.show()
  425. })
  426. it('should enforce focus', done => {
  427. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  428. const modalEl = fixtureEl.querySelector('.modal')
  429. const modal = new Modal(modalEl)
  430. spyOn(modal, '_enforceFocus').and.callThrough()
  431. const focusInListener = () => {
  432. expect(modal._element.focus).toHaveBeenCalled()
  433. document.removeEventListener('focusin', focusInListener)
  434. done()
  435. }
  436. modalEl.addEventListener('shown.bs.modal', () => {
  437. expect(modal._enforceFocus).toHaveBeenCalled()
  438. spyOn(modal._element, 'focus')
  439. document.addEventListener('focusin', focusInListener)
  440. const focusInEvent = createEvent('focusin', { bubbles: true })
  441. Object.defineProperty(focusInEvent, 'target', {
  442. value: fixtureEl
  443. })
  444. document.dispatchEvent(focusInEvent)
  445. })
  446. modal.show()
  447. })
  448. })
  449. describe('hide', () => {
  450. it('should hide a modal', done => {
  451. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  452. const modalEl = fixtureEl.querySelector('.modal')
  453. const modal = new Modal(modalEl)
  454. modalEl.addEventListener('shown.bs.modal', () => {
  455. modal.hide()
  456. })
  457. modalEl.addEventListener('hide.bs.modal', e => {
  458. expect(e).toBeDefined()
  459. })
  460. modalEl.addEventListener('hidden.bs.modal', () => {
  461. expect(modalEl.getAttribute('aria-modal')).toBeNull()
  462. expect(modalEl.getAttribute('role')).toBeNull()
  463. expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
  464. expect(modalEl.style.display).toEqual('none')
  465. expect(document.querySelector('.modal-backdrop')).toBeNull()
  466. done()
  467. })
  468. modal.show()
  469. })
  470. it('should close modal when clicking outside of modal-content', done => {
  471. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  472. const modalEl = fixtureEl.querySelector('.modal')
  473. const modal = new Modal(modalEl)
  474. modalEl.addEventListener('shown.bs.modal', () => {
  475. modalEl.click()
  476. })
  477. modalEl.addEventListener('hidden.bs.modal', () => {
  478. expect(modalEl.getAttribute('aria-modal')).toBeNull()
  479. expect(modalEl.getAttribute('role')).toBeNull()
  480. expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
  481. expect(modalEl.style.display).toEqual('none')
  482. expect(document.querySelector('.modal-backdrop')).toBeNull()
  483. done()
  484. })
  485. modal.show()
  486. })
  487. it('should do nothing is the modal is not shown', () => {
  488. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  489. const modalEl = fixtureEl.querySelector('.modal')
  490. const modal = new Modal(modalEl)
  491. modal.hide()
  492. expect().nothing()
  493. })
  494. it('should do nothing is the modal is transitioning', () => {
  495. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  496. const modalEl = fixtureEl.querySelector('.modal')
  497. const modal = new Modal(modalEl)
  498. modal._isTransitioning = true
  499. modal.hide()
  500. expect().nothing()
  501. })
  502. it('should not hide a modal if hide is prevented', done => {
  503. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  504. const modalEl = fixtureEl.querySelector('.modal')
  505. const modal = new Modal(modalEl)
  506. modalEl.addEventListener('shown.bs.modal', () => {
  507. modal.hide()
  508. })
  509. const hideCallback = () => {
  510. setTimeout(() => {
  511. expect(modal._isShown).toEqual(true)
  512. done()
  513. }, 10)
  514. }
  515. modalEl.addEventListener('hide.bs.modal', e => {
  516. e.preventDefault()
  517. hideCallback()
  518. })
  519. modalEl.addEventListener('hidden.bs.modal', () => {
  520. throw new Error('should not trigger hidden')
  521. })
  522. modal.show()
  523. })
  524. })
  525. describe('dispose', () => {
  526. it('should dispose a modal', () => {
  527. fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  528. const modalEl = fixtureEl.querySelector('.modal')
  529. const modal = new Modal(modalEl)
  530. expect(Modal.getInstance(modalEl)).toEqual(modal)
  531. spyOn(EventHandler, 'off')
  532. modal.dispose()
  533. expect(Modal.getInstance(modalEl)).toBeNull()
  534. expect(EventHandler.off).toHaveBeenCalledTimes(4)
  535. })
  536. })
  537. describe('handleUpdate', () => {
  538. it('should call adjust dialog', () => {
  539. fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  540. const modalEl = fixtureEl.querySelector('.modal')
  541. const modal = new Modal(modalEl)
  542. spyOn(modal, '_adjustDialog')
  543. modal.handleUpdate()
  544. expect(modal._adjustDialog).toHaveBeenCalled()
  545. })
  546. })
  547. describe('data-api', () => {
  548. it('should toggle modal', done => {
  549. fixtureEl.innerHTML = [
  550. '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
  551. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  552. ].join('')
  553. const modalEl = fixtureEl.querySelector('.modal')
  554. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  555. modalEl.addEventListener('shown.bs.modal', () => {
  556. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  557. expect(modalEl.getAttribute('role')).toEqual('dialog')
  558. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  559. expect(modalEl.style.display).toEqual('block')
  560. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  561. setTimeout(() => trigger.click(), 10)
  562. })
  563. modalEl.addEventListener('hidden.bs.modal', () => {
  564. expect(modalEl.getAttribute('aria-modal')).toBeNull()
  565. expect(modalEl.getAttribute('role')).toBeNull()
  566. expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
  567. expect(modalEl.style.display).toEqual('none')
  568. expect(document.querySelector('.modal-backdrop')).toBeNull()
  569. done()
  570. })
  571. trigger.click()
  572. })
  573. it('should not recreate a new modal', done => {
  574. fixtureEl.innerHTML = [
  575. '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
  576. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  577. ].join('')
  578. const modalEl = fixtureEl.querySelector('.modal')
  579. const modal = new Modal(modalEl)
  580. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  581. spyOn(modal, 'show').and.callThrough()
  582. modalEl.addEventListener('shown.bs.modal', () => {
  583. expect(modal.show).toHaveBeenCalled()
  584. done()
  585. })
  586. trigger.click()
  587. })
  588. it('should prevent default when the trigger is <a> or <area>', done => {
  589. fixtureEl.innerHTML = [
  590. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  591. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  592. ].join('')
  593. const modalEl = fixtureEl.querySelector('.modal')
  594. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  595. spyOn(Event.prototype, 'preventDefault').and.callThrough()
  596. modalEl.addEventListener('shown.bs.modal', () => {
  597. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  598. expect(modalEl.getAttribute('role')).toEqual('dialog')
  599. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  600. expect(modalEl.style.display).toEqual('block')
  601. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  602. expect(Event.prototype.preventDefault).toHaveBeenCalled()
  603. done()
  604. })
  605. trigger.click()
  606. })
  607. it('should focus the trigger on hide', done => {
  608. fixtureEl.innerHTML = [
  609. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  610. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  611. ].join('')
  612. const modalEl = fixtureEl.querySelector('.modal')
  613. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  614. spyOn(trigger, 'focus')
  615. modalEl.addEventListener('shown.bs.modal', () => {
  616. const modal = Modal.getInstance(modalEl)
  617. modal.hide()
  618. })
  619. const hideListener = () => {
  620. setTimeout(() => {
  621. expect(trigger.focus).toHaveBeenCalled()
  622. done()
  623. }, 20)
  624. }
  625. modalEl.addEventListener('hidden.bs.modal', () => {
  626. hideListener()
  627. })
  628. trigger.click()
  629. })
  630. it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than <a> or <area>', done => {
  631. fixtureEl.innerHTML = [
  632. '<div class="modal">',
  633. ' <div class="modal-dialog">',
  634. ' <button type="button" data-bs-dismiss="modal"></button>',
  635. ' </div>',
  636. '</div>'
  637. ].join('')
  638. const modalEl = fixtureEl.querySelector('.modal')
  639. const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]')
  640. const modal = new Modal(modalEl)
  641. spyOn(Event.prototype, 'preventDefault').and.callThrough()
  642. modalEl.addEventListener('shown.bs.modal', () => {
  643. btnClose.click()
  644. })
  645. modalEl.addEventListener('hidden.bs.modal', () => {
  646. expect(Event.prototype.preventDefault).not.toHaveBeenCalled()
  647. done()
  648. })
  649. modal.show()
  650. })
  651. it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is <a> or <area>', done => {
  652. fixtureEl.innerHTML = [
  653. '<div class="modal">',
  654. ' <div class="modal-dialog">',
  655. ' <a type="button" data-bs-dismiss="modal"></a>',
  656. ' </div>',
  657. '</div>'
  658. ].join('')
  659. const modalEl = fixtureEl.querySelector('.modal')
  660. const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]')
  661. const modal = new Modal(modalEl)
  662. spyOn(Event.prototype, 'preventDefault').and.callThrough()
  663. modalEl.addEventListener('shown.bs.modal', () => {
  664. btnClose.click()
  665. })
  666. modalEl.addEventListener('hidden.bs.modal', () => {
  667. expect(Event.prototype.preventDefault).toHaveBeenCalled()
  668. done()
  669. })
  670. modal.show()
  671. })
  672. it('should not focus the trigger if the modal is not visible', done => {
  673. fixtureEl.innerHTML = [
  674. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal" style="display: none;"></a>',
  675. '<div id="exampleModal" class="modal" style="display: none;"><div class="modal-dialog"></div></div>'
  676. ].join('')
  677. const modalEl = fixtureEl.querySelector('.modal')
  678. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  679. spyOn(trigger, 'focus')
  680. modalEl.addEventListener('shown.bs.modal', () => {
  681. const modal = Modal.getInstance(modalEl)
  682. modal.hide()
  683. })
  684. const hideListener = () => {
  685. setTimeout(() => {
  686. expect(trigger.focus).not.toHaveBeenCalled()
  687. done()
  688. }, 20)
  689. }
  690. modalEl.addEventListener('hidden.bs.modal', () => {
  691. hideListener()
  692. })
  693. trigger.click()
  694. })
  695. it('should not focus the trigger if the modal is not shown', done => {
  696. fixtureEl.innerHTML = [
  697. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  698. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  699. ].join('')
  700. const modalEl = fixtureEl.querySelector('.modal')
  701. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  702. spyOn(trigger, 'focus')
  703. const showListener = () => {
  704. setTimeout(() => {
  705. expect(trigger.focus).not.toHaveBeenCalled()
  706. done()
  707. }, 10)
  708. }
  709. modalEl.addEventListener('show.bs.modal', e => {
  710. e.preventDefault()
  711. showListener()
  712. })
  713. trigger.click()
  714. })
  715. })
  716. describe('jQueryInterface', () => {
  717. it('should create a modal', () => {
  718. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  719. const div = fixtureEl.querySelector('div')
  720. jQueryMock.fn.modal = Modal.jQueryInterface
  721. jQueryMock.elements = [div]
  722. jQueryMock.fn.modal.call(jQueryMock)
  723. expect(Modal.getInstance(div)).not.toBeNull()
  724. })
  725. it('should create a modal with given config', () => {
  726. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  727. const div = fixtureEl.querySelector('div')
  728. jQueryMock.fn.modal = Modal.jQueryInterface
  729. jQueryMock.elements = [div]
  730. jQueryMock.fn.modal.call(jQueryMock, { keyboard: false })
  731. spyOn(Modal.prototype, 'constructor')
  732. expect(Modal.prototype.constructor).not.toHaveBeenCalledWith(div, { keyboard: false })
  733. const modal = Modal.getInstance(div)
  734. expect(modal).not.toBeNull()
  735. expect(modal._config.keyboard).toBe(false)
  736. })
  737. it('should not re create a modal', () => {
  738. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  739. const div = fixtureEl.querySelector('div')
  740. const modal = new Modal(div)
  741. jQueryMock.fn.modal = Modal.jQueryInterface
  742. jQueryMock.elements = [div]
  743. jQueryMock.fn.modal.call(jQueryMock)
  744. expect(Modal.getInstance(div)).toEqual(modal)
  745. })
  746. it('should throw error on undefined method', () => {
  747. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  748. const div = fixtureEl.querySelector('div')
  749. const action = 'undefinedMethod'
  750. jQueryMock.fn.modal = Modal.jQueryInterface
  751. jQueryMock.elements = [div]
  752. expect(() => {
  753. jQueryMock.fn.modal.call(jQueryMock, action)
  754. }).toThrowError(TypeError, `No method named "${action}"`)
  755. })
  756. it('should call show method', () => {
  757. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  758. const div = fixtureEl.querySelector('div')
  759. const modal = new Modal(div)
  760. jQueryMock.fn.modal = Modal.jQueryInterface
  761. jQueryMock.elements = [div]
  762. spyOn(modal, 'show')
  763. jQueryMock.fn.modal.call(jQueryMock, 'show')
  764. expect(modal.show).toHaveBeenCalled()
  765. })
  766. it('should not call show method', () => {
  767. fixtureEl.innerHTML = '<div class="modal" data-bs-show="false"><div class="modal-dialog"></div></div>'
  768. const div = fixtureEl.querySelector('div')
  769. jQueryMock.fn.modal = Modal.jQueryInterface
  770. jQueryMock.elements = [div]
  771. spyOn(Modal.prototype, 'show')
  772. jQueryMock.fn.modal.call(jQueryMock)
  773. expect(Modal.prototype.show).not.toHaveBeenCalled()
  774. })
  775. })
  776. describe('getInstance', () => {
  777. it('should return modal instance', () => {
  778. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  779. const div = fixtureEl.querySelector('div')
  780. const modal = new Modal(div)
  781. expect(Modal.getInstance(div)).toEqual(modal)
  782. expect(Modal.getInstance(div)).toBeInstanceOf(Modal)
  783. })
  784. it('should return null when there is no modal instance', () => {
  785. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  786. const div = fixtureEl.querySelector('div')
  787. expect(Modal.getInstance(div)).toBeNull()
  788. })
  789. })
  790. describe('getOrCreateInstance', () => {
  791. it('should return modal instance', () => {
  792. fixtureEl.innerHTML = '<div></div>'
  793. const div = fixtureEl.querySelector('div')
  794. const modal = new Modal(div)
  795. expect(Modal.getOrCreateInstance(div)).toEqual(modal)
  796. expect(Modal.getInstance(div)).toEqual(Modal.getOrCreateInstance(div, {}))
  797. expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
  798. })
  799. it('should return new instance when there is no modal instance', () => {
  800. fixtureEl.innerHTML = '<div></div>'
  801. const div = fixtureEl.querySelector('div')
  802. expect(Modal.getInstance(div)).toEqual(null)
  803. expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
  804. })
  805. it('should return new instance when there is no modal instance with given configuration', () => {
  806. fixtureEl.innerHTML = '<div></div>'
  807. const div = fixtureEl.querySelector('div')
  808. expect(Modal.getInstance(div)).toEqual(null)
  809. const modal = Modal.getOrCreateInstance(div, {
  810. backdrop: true
  811. })
  812. expect(modal).toBeInstanceOf(Modal)
  813. expect(modal._config.backdrop).toEqual(true)
  814. })
  815. it('should return the instance when exists without given configuration', () => {
  816. fixtureEl.innerHTML = '<div></div>'
  817. const div = fixtureEl.querySelector('div')
  818. const modal = new Modal(div, {
  819. backdrop: true
  820. })
  821. expect(Modal.getInstance(div)).toEqual(modal)
  822. const modal2 = Modal.getOrCreateInstance(div, {
  823. backdrop: false
  824. })
  825. expect(modal).toBeInstanceOf(Modal)
  826. expect(modal2).toEqual(modal)
  827. expect(modal2._config.backdrop).toEqual(true)
  828. })
  829. })
  830. })