modal.spec.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329
  1. import EventHandler from '../../src/dom/event-handler.js'
  2. import Modal from '../../src/modal.js'
  3. import ScrollBarHelper from '../../src/util/scrollbar.js'
  4. import {
  5. clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock
  6. } from '../helpers/fixture.js'
  7. describe('Modal', () => {
  8. let fixtureEl
  9. beforeAll(() => {
  10. fixtureEl = getFixture()
  11. })
  12. afterEach(() => {
  13. clearFixture()
  14. clearBodyAndDocument()
  15. document.body.classList.remove('modal-open')
  16. for (const backdrop of document.querySelectorAll('.modal-backdrop')) {
  17. backdrop.remove()
  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', () => {
  50. return new Promise(resolve => {
  51. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  52. const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
  53. const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
  54. const modalEl = fixtureEl.querySelector('.modal')
  55. const modal = new Modal(modalEl)
  56. modalEl.addEventListener('shown.bs.modal', () => {
  57. expect(spyHide).toHaveBeenCalled()
  58. modal.toggle()
  59. })
  60. modalEl.addEventListener('hidden.bs.modal', () => {
  61. expect(spyReset).toHaveBeenCalled()
  62. resolve()
  63. })
  64. modal.toggle()
  65. })
  66. })
  67. })
  68. describe('show', () => {
  69. it('should show a modal', () => {
  70. return new Promise(resolve => {
  71. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  72. const modalEl = fixtureEl.querySelector('.modal')
  73. const modal = new Modal(modalEl)
  74. modalEl.addEventListener('show.bs.modal', event => {
  75. expect(event).toBeDefined()
  76. })
  77. modalEl.addEventListener('shown.bs.modal', () => {
  78. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  79. expect(modalEl.getAttribute('role')).toEqual('dialog')
  80. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  81. expect(modalEl.style.display).toEqual('block')
  82. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  83. resolve()
  84. })
  85. modal.show()
  86. })
  87. })
  88. it('should show a modal without backdrop', () => {
  89. return new Promise(resolve => {
  90. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  91. const modalEl = fixtureEl.querySelector('.modal')
  92. const modal = new Modal(modalEl, {
  93. backdrop: false
  94. })
  95. modalEl.addEventListener('show.bs.modal', event => {
  96. expect(event).toBeDefined()
  97. })
  98. modalEl.addEventListener('shown.bs.modal', () => {
  99. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  100. expect(modalEl.getAttribute('role')).toEqual('dialog')
  101. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  102. expect(modalEl.style.display).toEqual('block')
  103. expect(document.querySelector('.modal-backdrop')).toBeNull()
  104. resolve()
  105. })
  106. modal.show()
  107. })
  108. })
  109. it('should show a modal and append the element', () => {
  110. return new Promise(resolve => {
  111. const modalEl = document.createElement('div')
  112. const id = 'dynamicModal'
  113. modalEl.setAttribute('id', id)
  114. modalEl.classList.add('modal')
  115. modalEl.innerHTML = '<div class="modal-dialog"></div>'
  116. const modal = new Modal(modalEl)
  117. modalEl.addEventListener('shown.bs.modal', () => {
  118. const dynamicModal = document.getElementById(id)
  119. expect(dynamicModal).not.toBeNull()
  120. dynamicModal.remove()
  121. resolve()
  122. })
  123. modal.show()
  124. })
  125. })
  126. it('should do nothing if a modal is shown', () => {
  127. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  128. const modalEl = fixtureEl.querySelector('.modal')
  129. const modal = new Modal(modalEl)
  130. const spy = spyOn(EventHandler, 'trigger')
  131. modal._isShown = true
  132. modal.show()
  133. expect(spy).not.toHaveBeenCalled()
  134. })
  135. it('should do nothing if a modal is transitioning', () => {
  136. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  137. const modalEl = fixtureEl.querySelector('.modal')
  138. const modal = new Modal(modalEl)
  139. const spy = spyOn(EventHandler, 'trigger')
  140. modal._isTransitioning = true
  141. modal.show()
  142. expect(spy).not.toHaveBeenCalled()
  143. })
  144. it('should not fire shown event when show is prevented', () => {
  145. return new Promise((resolve, reject) => {
  146. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  147. const modalEl = fixtureEl.querySelector('.modal')
  148. const modal = new Modal(modalEl)
  149. modalEl.addEventListener('show.bs.modal', event => {
  150. event.preventDefault()
  151. const expectedDone = () => {
  152. expect().nothing()
  153. resolve()
  154. }
  155. setTimeout(expectedDone, 10)
  156. })
  157. modalEl.addEventListener('shown.bs.modal', () => {
  158. reject(new Error('shown event triggered'))
  159. })
  160. modal.show()
  161. })
  162. })
  163. it('should be shown after the first call to show() has been prevented while fading is enabled ', () => {
  164. return new Promise(resolve => {
  165. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  166. const modalEl = fixtureEl.querySelector('.modal')
  167. const modal = new Modal(modalEl)
  168. let prevented = false
  169. modalEl.addEventListener('show.bs.modal', event => {
  170. if (!prevented) {
  171. event.preventDefault()
  172. prevented = true
  173. setTimeout(() => {
  174. modal.show()
  175. })
  176. }
  177. })
  178. modalEl.addEventListener('shown.bs.modal', () => {
  179. expect(prevented).toBeTrue()
  180. expect(modal._isAnimated()).toBeTrue()
  181. resolve()
  182. })
  183. modal.show()
  184. })
  185. })
  186. it('should set is transitioning if fade class is present', () => {
  187. return new Promise(resolve => {
  188. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  189. const modalEl = fixtureEl.querySelector('.modal')
  190. const modal = new Modal(modalEl)
  191. modalEl.addEventListener('show.bs.modal', () => {
  192. setTimeout(() => {
  193. expect(modal._isTransitioning).toBeTrue()
  194. })
  195. })
  196. modalEl.addEventListener('shown.bs.modal', () => {
  197. expect(modal._isTransitioning).toBeFalse()
  198. resolve()
  199. })
  200. modal.show()
  201. })
  202. })
  203. it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => {
  204. return new Promise(resolve => {
  205. fixtureEl.innerHTML = [
  206. '<div class="modal fade">',
  207. ' <div class="modal-dialog">',
  208. ' <div class="modal-header">',
  209. ' <button type="button" data-bs-dismiss="modal"></button>',
  210. ' </div>',
  211. ' </div>',
  212. '</div>'
  213. ].join('')
  214. const modalEl = fixtureEl.querySelector('.modal')
  215. const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
  216. const modal = new Modal(modalEl)
  217. const spy = spyOn(modal, 'hide').and.callThrough()
  218. modalEl.addEventListener('shown.bs.modal', () => {
  219. btnClose.click()
  220. })
  221. modalEl.addEventListener('hidden.bs.modal', () => {
  222. expect(spy).toHaveBeenCalled()
  223. resolve()
  224. })
  225. modal.show()
  226. })
  227. })
  228. it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => {
  229. return new Promise(resolve => {
  230. fixtureEl.innerHTML = [
  231. '<button type="button" data-bs-dismiss="modal" data-bs-target="#modal1"></button>',
  232. '<div id="modal1" class="modal fade">',
  233. ' <div class="modal-dialog"></div>',
  234. '</div>'
  235. ].join('')
  236. const modalEl = fixtureEl.querySelector('.modal')
  237. const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
  238. const modal = new Modal(modalEl)
  239. const spy = spyOn(modal, 'hide').and.callThrough()
  240. modalEl.addEventListener('shown.bs.modal', () => {
  241. btnClose.click()
  242. })
  243. modalEl.addEventListener('hidden.bs.modal', () => {
  244. expect(spy).toHaveBeenCalled()
  245. resolve()
  246. })
  247. modal.show()
  248. })
  249. })
  250. it('should set .modal\'s scroll top to 0', () => {
  251. return new Promise(resolve => {
  252. fixtureEl.innerHTML = [
  253. '<div class="modal fade">',
  254. ' <div class="modal-dialog"></div>',
  255. '</div>'
  256. ].join('')
  257. const modalEl = fixtureEl.querySelector('.modal')
  258. const modal = new Modal(modalEl)
  259. modalEl.addEventListener('shown.bs.modal', () => {
  260. expect(modalEl.scrollTop).toEqual(0)
  261. resolve()
  262. })
  263. modal.show()
  264. })
  265. })
  266. it('should set modal body scroll top to 0 if modal body do not exists', () => {
  267. return new Promise(resolve => {
  268. fixtureEl.innerHTML = [
  269. '<div class="modal fade">',
  270. ' <div class="modal-dialog">',
  271. ' <div class="modal-body"></div>',
  272. ' </div>',
  273. '</div>'
  274. ].join('')
  275. const modalEl = fixtureEl.querySelector('.modal')
  276. const modalBody = modalEl.querySelector('.modal-body')
  277. const modal = new Modal(modalEl)
  278. modalEl.addEventListener('shown.bs.modal', () => {
  279. expect(modalBody.scrollTop).toEqual(0)
  280. resolve()
  281. })
  282. modal.show()
  283. })
  284. })
  285. it('should not trap focus if focus equal to false', () => {
  286. return new Promise(resolve => {
  287. fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
  288. const modalEl = fixtureEl.querySelector('.modal')
  289. const modal = new Modal(modalEl, {
  290. focus: false
  291. })
  292. const spy = spyOn(modal._focustrap, 'activate').and.callThrough()
  293. modalEl.addEventListener('shown.bs.modal', () => {
  294. expect(spy).not.toHaveBeenCalled()
  295. resolve()
  296. })
  297. modal.show()
  298. })
  299. })
  300. it('should add listener when escape touch is pressed', () => {
  301. return new Promise(resolve => {
  302. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  303. const modalEl = fixtureEl.querySelector('.modal')
  304. const modal = new Modal(modalEl)
  305. const spy = spyOn(modal, 'hide').and.callThrough()
  306. modalEl.addEventListener('shown.bs.modal', () => {
  307. const keydownEscape = createEvent('keydown')
  308. keydownEscape.key = 'Escape'
  309. modalEl.dispatchEvent(keydownEscape)
  310. })
  311. modalEl.addEventListener('hidden.bs.modal', () => {
  312. expect(spy).toHaveBeenCalled()
  313. resolve()
  314. })
  315. modal.show()
  316. })
  317. })
  318. it('should do nothing when the pressed key is not escape', () => {
  319. return new Promise(resolve => {
  320. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  321. const modalEl = fixtureEl.querySelector('.modal')
  322. const modal = new Modal(modalEl)
  323. const spy = spyOn(modal, 'hide')
  324. const expectDone = () => {
  325. expect(spy).not.toHaveBeenCalled()
  326. resolve()
  327. }
  328. modalEl.addEventListener('shown.bs.modal', () => {
  329. const keydownTab = createEvent('keydown')
  330. keydownTab.key = 'Tab'
  331. modalEl.dispatchEvent(keydownTab)
  332. setTimeout(expectDone, 30)
  333. })
  334. modal.show()
  335. })
  336. })
  337. it('should adjust dialog on resize', () => {
  338. return new Promise(resolve => {
  339. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  340. const modalEl = fixtureEl.querySelector('.modal')
  341. const modal = new Modal(modalEl)
  342. const spy = spyOn(modal, '_adjustDialog').and.callThrough()
  343. const expectDone = () => {
  344. expect(spy).toHaveBeenCalled()
  345. resolve()
  346. }
  347. modalEl.addEventListener('shown.bs.modal', () => {
  348. const resizeEvent = createEvent('resize')
  349. window.dispatchEvent(resizeEvent)
  350. setTimeout(expectDone, 10)
  351. })
  352. modal.show()
  353. })
  354. })
  355. it('should not close modal when clicking on modal-content', () => {
  356. return new Promise((resolve, reject) => {
  357. fixtureEl.innerHTML = [
  358. '<div class="modal">',
  359. ' <div class="modal-dialog">',
  360. ' <div class="modal-content"></div>',
  361. ' </div>',
  362. '</div>'
  363. ].join('')
  364. const modalEl = fixtureEl.querySelector('.modal')
  365. const modal = new Modal(modalEl)
  366. const shownCallback = () => {
  367. setTimeout(() => {
  368. expect(modal._isShown).toEqual(true)
  369. resolve()
  370. }, 10)
  371. }
  372. modalEl.addEventListener('shown.bs.modal', () => {
  373. fixtureEl.querySelector('.modal-dialog').click()
  374. fixtureEl.querySelector('.modal-content').click()
  375. shownCallback()
  376. })
  377. modalEl.addEventListener('hidden.bs.modal', () => {
  378. reject(new Error('Should not hide a modal'))
  379. })
  380. modal.show()
  381. })
  382. })
  383. it('should not close modal when clicking outside of modal-content if backdrop = false', () => {
  384. return new Promise((resolve, reject) => {
  385. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  386. const modalEl = fixtureEl.querySelector('.modal')
  387. const modal = new Modal(modalEl, {
  388. backdrop: false
  389. })
  390. const shownCallback = () => {
  391. setTimeout(() => {
  392. expect(modal._isShown).toBeTrue()
  393. resolve()
  394. }, 10)
  395. }
  396. modalEl.addEventListener('shown.bs.modal', () => {
  397. modalEl.click()
  398. shownCallback()
  399. })
  400. modalEl.addEventListener('hidden.bs.modal', () => {
  401. reject(new Error('Should not hide a modal'))
  402. })
  403. modal.show()
  404. })
  405. })
  406. it('should not close modal when clicking outside of modal-content if backdrop = static', () => {
  407. return new Promise((resolve, reject) => {
  408. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  409. const modalEl = fixtureEl.querySelector('.modal')
  410. const modal = new Modal(modalEl, {
  411. backdrop: 'static'
  412. })
  413. const shownCallback = () => {
  414. setTimeout(() => {
  415. expect(modal._isShown).toBeTrue()
  416. resolve()
  417. }, 10)
  418. }
  419. modalEl.addEventListener('shown.bs.modal', () => {
  420. modalEl.click()
  421. shownCallback()
  422. })
  423. modalEl.addEventListener('hidden.bs.modal', () => {
  424. reject(new Error('Should not hide a modal'))
  425. })
  426. modal.show()
  427. })
  428. })
  429. it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => {
  430. return new Promise(resolve => {
  431. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  432. const modalEl = fixtureEl.querySelector('.modal')
  433. const modal = new Modal(modalEl, {
  434. backdrop: 'static',
  435. keyboard: true
  436. })
  437. const shownCallback = () => {
  438. setTimeout(() => {
  439. expect(modal._isShown).toBeFalse()
  440. resolve()
  441. }, 10)
  442. }
  443. modalEl.addEventListener('shown.bs.modal', () => {
  444. const keydownEscape = createEvent('keydown')
  445. keydownEscape.key = 'Escape'
  446. modalEl.dispatchEvent(keydownEscape)
  447. shownCallback()
  448. })
  449. modal.show()
  450. })
  451. })
  452. it('should not close modal when escape key is pressed with keyboard = false', () => {
  453. return new Promise((resolve, reject) => {
  454. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  455. const modalEl = fixtureEl.querySelector('.modal')
  456. const modal = new Modal(modalEl, {
  457. keyboard: false
  458. })
  459. const shownCallback = () => {
  460. setTimeout(() => {
  461. expect(modal._isShown).toBeTrue()
  462. resolve()
  463. }, 10)
  464. }
  465. modalEl.addEventListener('shown.bs.modal', () => {
  466. const keydownEscape = createEvent('keydown')
  467. keydownEscape.key = 'Escape'
  468. modalEl.dispatchEvent(keydownEscape)
  469. shownCallback()
  470. })
  471. modalEl.addEventListener('hidden.bs.modal', () => {
  472. reject(new Error('Should not hide a modal'))
  473. })
  474. modal.show()
  475. })
  476. })
  477. it('should not overflow when clicking outside of modal-content if backdrop = static', () => {
  478. return new Promise(resolve => {
  479. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>'
  480. const modalEl = fixtureEl.querySelector('.modal')
  481. const modal = new Modal(modalEl, {
  482. backdrop: 'static'
  483. })
  484. modalEl.addEventListener('shown.bs.modal', () => {
  485. modalEl.click()
  486. setTimeout(() => {
  487. expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight)
  488. resolve()
  489. }, 20)
  490. })
  491. modal.show()
  492. })
  493. })
  494. it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => {
  495. return new Promise(resolve => {
  496. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>'
  497. const modalEl = fixtureEl.querySelector('.modal')
  498. const modal = new Modal(modalEl, {
  499. backdrop: 'static'
  500. })
  501. modalEl.addEventListener('shown.bs.modal', () => {
  502. const spy = spyOn(modal, '_queueCallback').and.callThrough()
  503. const mouseDown = createEvent('mousedown')
  504. modalEl.dispatchEvent(mouseDown)
  505. modalEl.click()
  506. modalEl.dispatchEvent(mouseDown)
  507. modalEl.click()
  508. setTimeout(() => {
  509. expect(spy).toHaveBeenCalledTimes(1)
  510. resolve()
  511. }, 20)
  512. })
  513. modal.show()
  514. })
  515. })
  516. it('should trap focus', () => {
  517. return new Promise(resolve => {
  518. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  519. const modalEl = fixtureEl.querySelector('.modal')
  520. const modal = new Modal(modalEl)
  521. const spy = spyOn(modal._focustrap, 'activate').and.callThrough()
  522. modalEl.addEventListener('shown.bs.modal', () => {
  523. expect(spy).toHaveBeenCalled()
  524. resolve()
  525. })
  526. modal.show()
  527. })
  528. })
  529. })
  530. describe('hide', () => {
  531. it('should hide a modal', () => {
  532. return new Promise(resolve => {
  533. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  534. const modalEl = fixtureEl.querySelector('.modal')
  535. const modal = new Modal(modalEl)
  536. const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
  537. modalEl.addEventListener('shown.bs.modal', () => {
  538. modal.hide()
  539. })
  540. modalEl.addEventListener('hide.bs.modal', event => {
  541. expect(event).toBeDefined()
  542. })
  543. modalEl.addEventListener('hidden.bs.modal', () => {
  544. expect(modalEl.getAttribute('aria-modal')).toBeNull()
  545. expect(modalEl.getAttribute('role')).toBeNull()
  546. expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
  547. expect(modalEl.style.display).toEqual('none')
  548. expect(backdropSpy).toHaveBeenCalled()
  549. resolve()
  550. })
  551. modal.show()
  552. })
  553. })
  554. it('should close modal when clicking outside of modal-content', () => {
  555. return new Promise(resolve => {
  556. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  557. const modalEl = fixtureEl.querySelector('.modal')
  558. const dialogEl = modalEl.querySelector('.modal-dialog')
  559. const modal = new Modal(modalEl)
  560. const spy = spyOn(modal, 'hide')
  561. modalEl.addEventListener('shown.bs.modal', () => {
  562. const mouseDown = createEvent('mousedown')
  563. dialogEl.dispatchEvent(mouseDown)
  564. modalEl.click()
  565. expect(spy).not.toHaveBeenCalled()
  566. modalEl.dispatchEvent(mouseDown)
  567. modalEl.click()
  568. expect(spy).toHaveBeenCalled()
  569. resolve()
  570. })
  571. modal.show()
  572. })
  573. })
  574. it('should not close modal when clicking on an element removed from modal content', () => {
  575. return new Promise(resolve => {
  576. fixtureEl.innerHTML = [
  577. '<div class="modal">',
  578. ' <div class="modal-dialog">',
  579. ' <button class="btn">BTN</button>',
  580. ' </div>',
  581. '</div>'
  582. ].join('')
  583. const modalEl = fixtureEl.querySelector('.modal')
  584. const buttonEl = modalEl.querySelector('.btn')
  585. const modal = new Modal(modalEl)
  586. const spy = spyOn(modal, 'hide')
  587. buttonEl.addEventListener('click', () => {
  588. buttonEl.remove()
  589. })
  590. modalEl.addEventListener('shown.bs.modal', () => {
  591. modalEl.dispatchEvent(createEvent('mousedown'))
  592. buttonEl.click()
  593. expect(spy).not.toHaveBeenCalled()
  594. resolve()
  595. })
  596. modal.show()
  597. })
  598. })
  599. it('should do nothing is the modal is not shown', () => {
  600. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  601. const modalEl = fixtureEl.querySelector('.modal')
  602. const modal = new Modal(modalEl)
  603. modal.hide()
  604. expect().nothing()
  605. })
  606. it('should do nothing is the modal is transitioning', () => {
  607. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  608. const modalEl = fixtureEl.querySelector('.modal')
  609. const modal = new Modal(modalEl)
  610. modal._isTransitioning = true
  611. modal.hide()
  612. expect().nothing()
  613. })
  614. it('should not hide a modal if hide is prevented', () => {
  615. return new Promise((resolve, reject) => {
  616. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  617. const modalEl = fixtureEl.querySelector('.modal')
  618. const modal = new Modal(modalEl)
  619. modalEl.addEventListener('shown.bs.modal', () => {
  620. modal.hide()
  621. })
  622. const hideCallback = () => {
  623. setTimeout(() => {
  624. expect(modal._isShown).toBeTrue()
  625. resolve()
  626. }, 10)
  627. }
  628. modalEl.addEventListener('hide.bs.modal', event => {
  629. event.preventDefault()
  630. hideCallback()
  631. })
  632. modalEl.addEventListener('hidden.bs.modal', () => {
  633. reject(new Error('should not trigger hidden'))
  634. })
  635. modal.show()
  636. })
  637. })
  638. it('should release focus trap', () => {
  639. return new Promise(resolve => {
  640. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  641. const modalEl = fixtureEl.querySelector('.modal')
  642. const modal = new Modal(modalEl)
  643. const spy = spyOn(modal._focustrap, 'deactivate').and.callThrough()
  644. modalEl.addEventListener('shown.bs.modal', () => {
  645. modal.hide()
  646. })
  647. modalEl.addEventListener('hidden.bs.modal', () => {
  648. expect(spy).toHaveBeenCalled()
  649. resolve()
  650. })
  651. modal.show()
  652. })
  653. })
  654. })
  655. describe('dispose', () => {
  656. it('should dispose a modal', () => {
  657. fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  658. const modalEl = fixtureEl.querySelector('.modal')
  659. const modal = new Modal(modalEl)
  660. const focustrap = modal._focustrap
  661. const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough()
  662. expect(Modal.getInstance(modalEl)).toEqual(modal)
  663. const spyOff = spyOn(EventHandler, 'off')
  664. modal.dispose()
  665. expect(Modal.getInstance(modalEl)).toBeNull()
  666. expect(spyOff).toHaveBeenCalledTimes(3)
  667. expect(spyDeactivate).toHaveBeenCalled()
  668. })
  669. })
  670. describe('handleUpdate', () => {
  671. it('should call adjust dialog', () => {
  672. fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  673. const modalEl = fixtureEl.querySelector('.modal')
  674. const modal = new Modal(modalEl)
  675. const spy = spyOn(modal, '_adjustDialog')
  676. modal.handleUpdate()
  677. expect(spy).toHaveBeenCalled()
  678. })
  679. })
  680. describe('data-api', () => {
  681. it('should toggle modal', () => {
  682. return new Promise(resolve => {
  683. fixtureEl.innerHTML = [
  684. '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
  685. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  686. ].join('')
  687. const modalEl = fixtureEl.querySelector('.modal')
  688. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  689. modalEl.addEventListener('shown.bs.modal', () => {
  690. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  691. expect(modalEl.getAttribute('role')).toEqual('dialog')
  692. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  693. expect(modalEl.style.display).toEqual('block')
  694. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  695. setTimeout(() => trigger.click(), 10)
  696. })
  697. modalEl.addEventListener('hidden.bs.modal', () => {
  698. expect(modalEl.getAttribute('aria-modal')).toBeNull()
  699. expect(modalEl.getAttribute('role')).toBeNull()
  700. expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
  701. expect(modalEl.style.display).toEqual('none')
  702. expect(document.querySelector('.modal-backdrop')).toBeNull()
  703. resolve()
  704. })
  705. trigger.click()
  706. })
  707. })
  708. it('should not recreate a new modal', () => {
  709. return new Promise(resolve => {
  710. fixtureEl.innerHTML = [
  711. '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
  712. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  713. ].join('')
  714. const modalEl = fixtureEl.querySelector('.modal')
  715. const modal = new Modal(modalEl)
  716. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  717. const spy = spyOn(modal, 'show').and.callThrough()
  718. modalEl.addEventListener('shown.bs.modal', () => {
  719. expect(spy).toHaveBeenCalled()
  720. resolve()
  721. })
  722. trigger.click()
  723. })
  724. })
  725. it('should prevent default when the trigger is <a> or <area>', () => {
  726. return new Promise(resolve => {
  727. fixtureEl.innerHTML = [
  728. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  729. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  730. ].join('')
  731. const modalEl = fixtureEl.querySelector('.modal')
  732. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  733. const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
  734. modalEl.addEventListener('shown.bs.modal', () => {
  735. expect(modalEl.getAttribute('aria-modal')).toEqual('true')
  736. expect(modalEl.getAttribute('role')).toEqual('dialog')
  737. expect(modalEl.getAttribute('aria-hidden')).toBeNull()
  738. expect(modalEl.style.display).toEqual('block')
  739. expect(document.querySelector('.modal-backdrop')).not.toBeNull()
  740. expect(spy).toHaveBeenCalled()
  741. resolve()
  742. })
  743. trigger.click()
  744. })
  745. })
  746. it('should focus the trigger on hide', () => {
  747. return new Promise(resolve => {
  748. fixtureEl.innerHTML = [
  749. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  750. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  751. ].join('')
  752. const modalEl = fixtureEl.querySelector('.modal')
  753. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  754. const spy = spyOn(trigger, 'focus')
  755. modalEl.addEventListener('shown.bs.modal', () => {
  756. const modal = Modal.getInstance(modalEl)
  757. modal.hide()
  758. })
  759. const hideListener = () => {
  760. setTimeout(() => {
  761. expect(spy).toHaveBeenCalled()
  762. resolve()
  763. }, 20)
  764. }
  765. modalEl.addEventListener('hidden.bs.modal', () => {
  766. hideListener()
  767. })
  768. trigger.click()
  769. })
  770. })
  771. it('should open modal, having special characters in its id', () => {
  772. return new Promise(resolve => {
  773. fixtureEl.innerHTML = [
  774. '<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#j_id22:exampleModal">',
  775. ' Launch demo modal',
  776. '</button>',
  777. '<div class="modal fade" id="j_id22:exampleModal" aria-labelledby="exampleModalLabel" aria-hidden="true">',
  778. ' <div class="modal-dialog">',
  779. ' <div class="modal-content">',
  780. ' <div class="modal-body">',
  781. ' <p>modal body</p>',
  782. ' </div>',
  783. ' </div>',
  784. ' </div>',
  785. '</div>'
  786. ].join('')
  787. const modalEl = fixtureEl.querySelector('.modal')
  788. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  789. modalEl.addEventListener('shown.bs.modal', () => {
  790. resolve()
  791. })
  792. trigger.click()
  793. })
  794. })
  795. it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than <a> or <area>', () => {
  796. return new Promise(resolve => {
  797. fixtureEl.innerHTML = [
  798. '<div class="modal">',
  799. ' <div class="modal-dialog">',
  800. ' <button type="button" data-bs-dismiss="modal"></button>',
  801. ' </div>',
  802. '</div>'
  803. ].join('')
  804. const modalEl = fixtureEl.querySelector('.modal')
  805. const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]')
  806. const modal = new Modal(modalEl)
  807. const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
  808. modalEl.addEventListener('shown.bs.modal', () => {
  809. btnClose.click()
  810. })
  811. modalEl.addEventListener('hidden.bs.modal', () => {
  812. expect(spy).not.toHaveBeenCalled()
  813. resolve()
  814. })
  815. modal.show()
  816. })
  817. })
  818. it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is <a> or <area>', () => {
  819. return new Promise(resolve => {
  820. fixtureEl.innerHTML = [
  821. '<div class="modal">',
  822. ' <div class="modal-dialog">',
  823. ' <a type="button" data-bs-dismiss="modal"></a>',
  824. ' </div>',
  825. '</div>'
  826. ].join('')
  827. const modalEl = fixtureEl.querySelector('.modal')
  828. const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]')
  829. const modal = new Modal(modalEl)
  830. const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
  831. modalEl.addEventListener('shown.bs.modal', () => {
  832. btnClose.click()
  833. })
  834. modalEl.addEventListener('hidden.bs.modal', () => {
  835. expect(spy).toHaveBeenCalled()
  836. resolve()
  837. })
  838. modal.show()
  839. })
  840. })
  841. it('should not focus the trigger if the modal is not visible', () => {
  842. return new Promise(resolve => {
  843. fixtureEl.innerHTML = [
  844. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal" style="display: none;"></a>',
  845. '<div id="exampleModal" class="modal" style="display: none;"><div class="modal-dialog"></div></div>'
  846. ].join('')
  847. const modalEl = fixtureEl.querySelector('.modal')
  848. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  849. const spy = spyOn(trigger, 'focus')
  850. modalEl.addEventListener('shown.bs.modal', () => {
  851. const modal = Modal.getInstance(modalEl)
  852. modal.hide()
  853. })
  854. const hideListener = () => {
  855. setTimeout(() => {
  856. expect(spy).not.toHaveBeenCalled()
  857. resolve()
  858. }, 20)
  859. }
  860. modalEl.addEventListener('hidden.bs.modal', () => {
  861. hideListener()
  862. })
  863. trigger.click()
  864. })
  865. })
  866. it('should not focus the trigger if the modal is not shown', () => {
  867. return new Promise(resolve => {
  868. fixtureEl.innerHTML = [
  869. '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
  870. '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
  871. ].join('')
  872. const modalEl = fixtureEl.querySelector('.modal')
  873. const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
  874. const spy = spyOn(trigger, 'focus')
  875. const showListener = () => {
  876. setTimeout(() => {
  877. expect(spy).not.toHaveBeenCalled()
  878. resolve()
  879. }, 10)
  880. }
  881. modalEl.addEventListener('show.bs.modal', event => {
  882. event.preventDefault()
  883. showListener()
  884. })
  885. trigger.click()
  886. })
  887. })
  888. it('should call hide first, if another modal is open', () => {
  889. return new Promise(resolve => {
  890. fixtureEl.innerHTML = [
  891. '<button data-bs-toggle="modal" data-bs-target="#modal2"></button>',
  892. '<div id="modal1" class="modal fade"><div class="modal-dialog"></div></div>',
  893. '<div id="modal2" class="modal"><div class="modal-dialog"></div></div>'
  894. ].join('')
  895. const trigger2 = fixtureEl.querySelector('button')
  896. const modalEl1 = document.querySelector('#modal1')
  897. const modalEl2 = document.querySelector('#modal2')
  898. const modal1 = new Modal(modalEl1)
  899. modalEl1.addEventListener('shown.bs.modal', () => {
  900. trigger2.click()
  901. })
  902. modalEl1.addEventListener('hidden.bs.modal', () => {
  903. expect(Modal.getInstance(modalEl2)).not.toBeNull()
  904. expect(modalEl2).toHaveClass('show')
  905. resolve()
  906. })
  907. modal1.show()
  908. })
  909. })
  910. })
  911. describe('jQueryInterface', () => {
  912. it('should create a modal', () => {
  913. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  914. const div = fixtureEl.querySelector('div')
  915. jQueryMock.fn.modal = Modal.jQueryInterface
  916. jQueryMock.elements = [div]
  917. jQueryMock.fn.modal.call(jQueryMock)
  918. expect(Modal.getInstance(div)).not.toBeNull()
  919. })
  920. it('should create a modal with given config', () => {
  921. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  922. const div = fixtureEl.querySelector('div')
  923. jQueryMock.fn.modal = Modal.jQueryInterface
  924. jQueryMock.elements = [div]
  925. jQueryMock.fn.modal.call(jQueryMock, { keyboard: false })
  926. const spy = spyOn(Modal.prototype, 'constructor')
  927. expect(spy).not.toHaveBeenCalledWith(div, { keyboard: false })
  928. const modal = Modal.getInstance(div)
  929. expect(modal).not.toBeNull()
  930. expect(modal._config.keyboard).toBeFalse()
  931. })
  932. it('should not re create a modal', () => {
  933. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  934. const div = fixtureEl.querySelector('div')
  935. const modal = new Modal(div)
  936. jQueryMock.fn.modal = Modal.jQueryInterface
  937. jQueryMock.elements = [div]
  938. jQueryMock.fn.modal.call(jQueryMock)
  939. expect(Modal.getInstance(div)).toEqual(modal)
  940. })
  941. it('should throw error on undefined method', () => {
  942. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  943. const div = fixtureEl.querySelector('div')
  944. const action = 'undefinedMethod'
  945. jQueryMock.fn.modal = Modal.jQueryInterface
  946. jQueryMock.elements = [div]
  947. expect(() => {
  948. jQueryMock.fn.modal.call(jQueryMock, action)
  949. }).toThrowError(TypeError, `No method named "${action}"`)
  950. })
  951. it('should call show method', () => {
  952. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  953. const div = fixtureEl.querySelector('div')
  954. const modal = new Modal(div)
  955. jQueryMock.fn.modal = Modal.jQueryInterface
  956. jQueryMock.elements = [div]
  957. const spy = spyOn(modal, 'show')
  958. jQueryMock.fn.modal.call(jQueryMock, 'show')
  959. expect(spy).toHaveBeenCalled()
  960. })
  961. it('should not call show method', () => {
  962. fixtureEl.innerHTML = '<div class="modal" data-bs-show="false"><div class="modal-dialog"></div></div>'
  963. const div = fixtureEl.querySelector('div')
  964. jQueryMock.fn.modal = Modal.jQueryInterface
  965. jQueryMock.elements = [div]
  966. const spy = spyOn(Modal.prototype, 'show')
  967. jQueryMock.fn.modal.call(jQueryMock)
  968. expect(spy).not.toHaveBeenCalled()
  969. })
  970. })
  971. describe('getInstance', () => {
  972. it('should return modal instance', () => {
  973. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  974. const div = fixtureEl.querySelector('div')
  975. const modal = new Modal(div)
  976. expect(Modal.getInstance(div)).toEqual(modal)
  977. expect(Modal.getInstance(div)).toBeInstanceOf(Modal)
  978. })
  979. it('should return null when there is no modal instance', () => {
  980. fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
  981. const div = fixtureEl.querySelector('div')
  982. expect(Modal.getInstance(div)).toBeNull()
  983. })
  984. })
  985. describe('getOrCreateInstance', () => {
  986. it('should return modal instance', () => {
  987. fixtureEl.innerHTML = '<div></div>'
  988. const div = fixtureEl.querySelector('div')
  989. const modal = new Modal(div)
  990. expect(Modal.getOrCreateInstance(div)).toEqual(modal)
  991. expect(Modal.getInstance(div)).toEqual(Modal.getOrCreateInstance(div, {}))
  992. expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
  993. })
  994. it('should return new instance when there is no modal instance', () => {
  995. fixtureEl.innerHTML = '<div></div>'
  996. const div = fixtureEl.querySelector('div')
  997. expect(Modal.getInstance(div)).toBeNull()
  998. expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
  999. })
  1000. it('should return new instance when there is no modal instance with given configuration', () => {
  1001. fixtureEl.innerHTML = '<div></div>'
  1002. const div = fixtureEl.querySelector('div')
  1003. expect(Modal.getInstance(div)).toBeNull()
  1004. const modal = Modal.getOrCreateInstance(div, {
  1005. backdrop: true
  1006. })
  1007. expect(modal).toBeInstanceOf(Modal)
  1008. expect(modal._config.backdrop).toBeTrue()
  1009. })
  1010. it('should return the instance when exists without given configuration', () => {
  1011. fixtureEl.innerHTML = '<div></div>'
  1012. const div = fixtureEl.querySelector('div')
  1013. const modal = new Modal(div, {
  1014. backdrop: true
  1015. })
  1016. expect(Modal.getInstance(div)).toEqual(modal)
  1017. const modal2 = Modal.getOrCreateInstance(div, {
  1018. backdrop: false
  1019. })
  1020. expect(modal).toBeInstanceOf(Modal)
  1021. expect(modal2).toEqual(modal)
  1022. expect(modal2._config.backdrop).toBeTrue()
  1023. })
  1024. })
  1025. })