toast.spec.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import Toast from '../../src/toast'
  2. /** Test helpers */
  3. import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
  4. describe('Toast', () => {
  5. let fixtureEl
  6. beforeAll(() => {
  7. fixtureEl = getFixture()
  8. })
  9. afterEach(() => {
  10. clearFixture()
  11. })
  12. describe('VERSION', () => {
  13. it('should return plugin version', () => {
  14. expect(Toast.VERSION).toEqual(jasmine.any(String))
  15. })
  16. })
  17. describe('DATA_KEY', () => {
  18. it('should return plugin data key', () => {
  19. expect(Toast.DATA_KEY).toEqual('bs.toast')
  20. })
  21. })
  22. describe('constructor', () => {
  23. it('should take care of element either passed as a CSS selector or DOM element', () => {
  24. fixtureEl.innerHTML = '<div class="toast"></div>'
  25. const toastEl = fixtureEl.querySelector('.toast')
  26. const toastBySelector = new Toast('.toast')
  27. const toastByElement = new Toast(toastEl)
  28. expect(toastBySelector._element).toEqual(toastEl)
  29. expect(toastByElement._element).toEqual(toastEl)
  30. })
  31. it('should allow to config in js', done => {
  32. fixtureEl.innerHTML = [
  33. '<div class="toast">',
  34. ' <div class="toast-body">',
  35. ' a simple toast',
  36. ' </div>',
  37. '</div>'
  38. ].join('')
  39. const toastEl = fixtureEl.querySelector('div')
  40. const toast = new Toast(toastEl, {
  41. delay: 1
  42. })
  43. toastEl.addEventListener('shown.bs.toast', () => {
  44. expect(toastEl.classList.contains('show')).toEqual(true)
  45. done()
  46. })
  47. toast.show()
  48. })
  49. it('should close toast when close element with data-bs-dismiss attribute is set', done => {
  50. fixtureEl.innerHTML = [
  51. '<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">',
  52. ' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
  53. '</div>'
  54. ].join('')
  55. const toastEl = fixtureEl.querySelector('div')
  56. const toast = new Toast(toastEl)
  57. toastEl.addEventListener('shown.bs.toast', () => {
  58. expect(toastEl.classList.contains('show')).toEqual(true)
  59. const button = toastEl.querySelector('.btn-close')
  60. button.click()
  61. })
  62. toastEl.addEventListener('hidden.bs.toast', () => {
  63. expect(toastEl.classList.contains('show')).toEqual(false)
  64. done()
  65. })
  66. toast.show()
  67. })
  68. })
  69. describe('Default', () => {
  70. it('should expose default setting to allow to override them', () => {
  71. const defaultDelay = 1000
  72. Toast.Default.delay = defaultDelay
  73. fixtureEl.innerHTML = [
  74. '<div class="toast" data-bs-autohide="false" data-bs-animation="false">',
  75. ' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>',
  76. '</div>'
  77. ].join('')
  78. const toastEl = fixtureEl.querySelector('div')
  79. const toast = new Toast(toastEl)
  80. expect(toast._config.delay).toEqual(defaultDelay)
  81. })
  82. })
  83. describe('DefaultType', () => {
  84. it('should expose default setting types for read', () => {
  85. expect(Toast.DefaultType).toEqual(jasmine.any(Object))
  86. })
  87. })
  88. describe('show', () => {
  89. it('should auto hide', done => {
  90. fixtureEl.innerHTML = [
  91. '<div class="toast" data-bs-delay="1">',
  92. ' <div class="toast-body">',
  93. ' a simple toast',
  94. ' </div>',
  95. '</div>'
  96. ].join('')
  97. const toastEl = fixtureEl.querySelector('.toast')
  98. const toast = new Toast(toastEl)
  99. toastEl.addEventListener('hidden.bs.toast', () => {
  100. expect(toastEl.classList.contains('show')).toEqual(false)
  101. done()
  102. })
  103. toast.show()
  104. })
  105. it('should not add fade class', done => {
  106. fixtureEl.innerHTML = [
  107. '<div class="toast" data-bs-delay="1" data-bs-animation="false">',
  108. ' <div class="toast-body">',
  109. ' a simple toast',
  110. ' </div>',
  111. '</div>'
  112. ].join('')
  113. const toastEl = fixtureEl.querySelector('.toast')
  114. const toast = new Toast(toastEl)
  115. toastEl.addEventListener('shown.bs.toast', () => {
  116. expect(toastEl.classList.contains('fade')).toEqual(false)
  117. done()
  118. })
  119. toast.show()
  120. })
  121. it('should not trigger shown if show is prevented', done => {
  122. fixtureEl.innerHTML = [
  123. '<div class="toast" data-bs-delay="1" data-bs-animation="false">',
  124. ' <div class="toast-body">',
  125. ' a simple toast',
  126. ' </div>',
  127. '</div>'
  128. ].join('')
  129. const toastEl = fixtureEl.querySelector('.toast')
  130. const toast = new Toast(toastEl)
  131. const assertDone = () => {
  132. setTimeout(() => {
  133. expect(toastEl.classList.contains('show')).toEqual(false)
  134. done()
  135. }, 20)
  136. }
  137. toastEl.addEventListener('show.bs.toast', event => {
  138. event.preventDefault()
  139. assertDone()
  140. })
  141. toastEl.addEventListener('shown.bs.toast', () => {
  142. throw new Error('shown event should not be triggered if show is prevented')
  143. })
  144. toast.show()
  145. })
  146. it('should clear timeout if toast is shown again before it is hidden', done => {
  147. fixtureEl.innerHTML = [
  148. '<div class="toast">',
  149. ' <div class="toast-body">',
  150. ' a simple toast',
  151. ' </div>',
  152. '</div>'
  153. ].join('')
  154. const toastEl = fixtureEl.querySelector('.toast')
  155. const toast = new Toast(toastEl)
  156. setTimeout(() => {
  157. toast._config.autohide = false
  158. toastEl.addEventListener('shown.bs.toast', () => {
  159. expect(toast._clearTimeout).toHaveBeenCalled()
  160. expect(toast._timeout).toBeNull()
  161. done()
  162. })
  163. toast.show()
  164. }, toast._config.delay / 2)
  165. spyOn(toast, '_clearTimeout').and.callThrough()
  166. toast.show()
  167. })
  168. it('should clear timeout if toast is interacted with mouse', done => {
  169. fixtureEl.innerHTML = [
  170. '<div class="toast">',
  171. ' <div class="toast-body">',
  172. ' a simple toast',
  173. ' </div>',
  174. '</div>'
  175. ].join('')
  176. const toastEl = fixtureEl.querySelector('.toast')
  177. const toast = new Toast(toastEl)
  178. const spy = spyOn(toast, '_clearTimeout').and.callThrough()
  179. setTimeout(() => {
  180. spy.calls.reset()
  181. toastEl.addEventListener('mouseover', () => {
  182. expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
  183. expect(toast._timeout).toBeNull()
  184. done()
  185. })
  186. const mouseOverEvent = createEvent('mouseover')
  187. toastEl.dispatchEvent(mouseOverEvent)
  188. }, toast._config.delay / 2)
  189. toast.show()
  190. })
  191. it('should clear timeout if toast is interacted with keyboard', done => {
  192. fixtureEl.innerHTML = [
  193. '<button id="outside-focusable">outside focusable</button>',
  194. '<div class="toast">',
  195. ' <div class="toast-body">',
  196. ' a simple toast',
  197. ' <button>with a button</button>',
  198. ' </div>',
  199. '</div>'
  200. ].join('')
  201. const toastEl = fixtureEl.querySelector('.toast')
  202. const toast = new Toast(toastEl)
  203. const spy = spyOn(toast, '_clearTimeout').and.callThrough()
  204. setTimeout(() => {
  205. spy.calls.reset()
  206. toastEl.addEventListener('focusin', () => {
  207. expect(toast._clearTimeout).toHaveBeenCalledTimes(1)
  208. expect(toast._timeout).toBeNull()
  209. done()
  210. })
  211. const insideFocusable = toastEl.querySelector('button')
  212. insideFocusable.focus()
  213. }, toast._config.delay / 2)
  214. toast.show()
  215. })
  216. it('should still auto hide after being interacted with mouse and keyboard', done => {
  217. fixtureEl.innerHTML = [
  218. '<button id="outside-focusable">outside focusable</button>',
  219. '<div class="toast">',
  220. ' <div class="toast-body">',
  221. ' a simple toast',
  222. ' <button>with a button</button>',
  223. ' </div>',
  224. '</div>'
  225. ].join('')
  226. const toastEl = fixtureEl.querySelector('.toast')
  227. const toast = new Toast(toastEl)
  228. setTimeout(() => {
  229. toastEl.addEventListener('mouseover', () => {
  230. const insideFocusable = toastEl.querySelector('button')
  231. insideFocusable.focus()
  232. })
  233. toastEl.addEventListener('focusin', () => {
  234. const mouseOutEvent = createEvent('mouseout')
  235. toastEl.dispatchEvent(mouseOutEvent)
  236. })
  237. toastEl.addEventListener('mouseout', () => {
  238. const outsideFocusable = document.getElementById('outside-focusable')
  239. outsideFocusable.focus()
  240. })
  241. toastEl.addEventListener('focusout', () => {
  242. expect(toast._timeout).not.toBeNull()
  243. done()
  244. })
  245. const mouseOverEvent = createEvent('mouseover')
  246. toastEl.dispatchEvent(mouseOverEvent)
  247. }, toast._config.delay / 2)
  248. toast.show()
  249. })
  250. it('should not auto hide if focus leaves but mouse pointer remains inside', done => {
  251. fixtureEl.innerHTML = [
  252. '<button id="outside-focusable">outside focusable</button>',
  253. '<div class="toast">',
  254. ' <div class="toast-body">',
  255. ' a simple toast',
  256. ' <button>with a button</button>',
  257. ' </div>',
  258. '</div>'
  259. ].join('')
  260. const toastEl = fixtureEl.querySelector('.toast')
  261. const toast = new Toast(toastEl)
  262. setTimeout(() => {
  263. toastEl.addEventListener('mouseover', () => {
  264. const insideFocusable = toastEl.querySelector('button')
  265. insideFocusable.focus()
  266. })
  267. toastEl.addEventListener('focusin', () => {
  268. const outsideFocusable = document.getElementById('outside-focusable')
  269. outsideFocusable.focus()
  270. })
  271. toastEl.addEventListener('focusout', () => {
  272. expect(toast._timeout).toBeNull()
  273. done()
  274. })
  275. const mouseOverEvent = createEvent('mouseover')
  276. toastEl.dispatchEvent(mouseOverEvent)
  277. }, toast._config.delay / 2)
  278. toast.show()
  279. })
  280. it('should not auto hide if mouse pointer leaves but focus remains inside', done => {
  281. fixtureEl.innerHTML = [
  282. '<button id="outside-focusable">outside focusable</button>',
  283. '<div class="toast">',
  284. ' <div class="toast-body">',
  285. ' a simple toast',
  286. ' <button>with a button</button>',
  287. ' </div>',
  288. '</div>'
  289. ].join('')
  290. const toastEl = fixtureEl.querySelector('.toast')
  291. const toast = new Toast(toastEl)
  292. setTimeout(() => {
  293. toastEl.addEventListener('mouseover', () => {
  294. const insideFocusable = toastEl.querySelector('button')
  295. insideFocusable.focus()
  296. })
  297. toastEl.addEventListener('focusin', () => {
  298. const mouseOutEvent = createEvent('mouseout')
  299. toastEl.dispatchEvent(mouseOutEvent)
  300. })
  301. toastEl.addEventListener('mouseout', () => {
  302. expect(toast._timeout).toBeNull()
  303. done()
  304. })
  305. const mouseOverEvent = createEvent('mouseover')
  306. toastEl.dispatchEvent(mouseOverEvent)
  307. }, toast._config.delay / 2)
  308. toast.show()
  309. })
  310. })
  311. describe('hide', () => {
  312. it('should allow to hide toast manually', done => {
  313. fixtureEl.innerHTML = [
  314. '<div class="toast" data-bs-delay="1" data-bs-autohide="false">',
  315. ' <div class="toast-body">',
  316. ' a simple toast',
  317. ' </div>',
  318. ' </div>'
  319. ].join('')
  320. const toastEl = fixtureEl.querySelector('.toast')
  321. const toast = new Toast(toastEl)
  322. toastEl.addEventListener('shown.bs.toast', () => {
  323. toast.hide()
  324. })
  325. toastEl.addEventListener('hidden.bs.toast', () => {
  326. expect(toastEl.classList.contains('show')).toEqual(false)
  327. done()
  328. })
  329. toast.show()
  330. })
  331. it('should do nothing when we call hide on a non shown toast', () => {
  332. fixtureEl.innerHTML = '<div></div>'
  333. const toastEl = fixtureEl.querySelector('div')
  334. const toast = new Toast(toastEl)
  335. spyOn(toastEl.classList, 'contains')
  336. toast.hide()
  337. expect(toastEl.classList.contains).toHaveBeenCalled()
  338. })
  339. it('should not trigger hidden if hide is prevented', done => {
  340. fixtureEl.innerHTML = [
  341. '<div class="toast" data-bs-delay="1" data-bs-animation="false">',
  342. ' <div class="toast-body">',
  343. ' a simple toast',
  344. ' </div>',
  345. '</div>'
  346. ].join('')
  347. const toastEl = fixtureEl.querySelector('.toast')
  348. const toast = new Toast(toastEl)
  349. const assertDone = () => {
  350. setTimeout(() => {
  351. expect(toastEl.classList.contains('show')).toEqual(true)
  352. done()
  353. }, 20)
  354. }
  355. toastEl.addEventListener('shown.bs.toast', () => {
  356. toast.hide()
  357. })
  358. toastEl.addEventListener('hide.bs.toast', event => {
  359. event.preventDefault()
  360. assertDone()
  361. })
  362. toastEl.addEventListener('hidden.bs.toast', () => {
  363. throw new Error('hidden event should not be triggered if hide is prevented')
  364. })
  365. toast.show()
  366. })
  367. })
  368. describe('dispose', () => {
  369. it('should allow to destroy toast', () => {
  370. fixtureEl.innerHTML = '<div></div>'
  371. const toastEl = fixtureEl.querySelector('div')
  372. spyOn(toastEl, 'addEventListener').and.callThrough()
  373. spyOn(toastEl, 'removeEventListener').and.callThrough()
  374. const toast = new Toast(toastEl)
  375. expect(Toast.getInstance(toastEl)).not.toBeNull()
  376. expect(toastEl.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
  377. toast.dispose()
  378. expect(Toast.getInstance(toastEl)).toBeNull()
  379. expect(toastEl.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
  380. })
  381. it('should allow to destroy toast and hide it before that', done => {
  382. fixtureEl.innerHTML = [
  383. '<div class="toast" data-bs-delay="0" data-bs-autohide="false">',
  384. ' <div class="toast-body">',
  385. ' a simple toast',
  386. ' </div>',
  387. '</div>'
  388. ].join('')
  389. const toastEl = fixtureEl.querySelector('div')
  390. const toast = new Toast(toastEl)
  391. const expected = () => {
  392. expect(toastEl.classList.contains('show')).toEqual(true)
  393. expect(Toast.getInstance(toastEl)).not.toBeNull()
  394. toast.dispose()
  395. expect(Toast.getInstance(toastEl)).toBeNull()
  396. expect(toastEl.classList.contains('show')).toEqual(false)
  397. done()
  398. }
  399. toastEl.addEventListener('shown.bs.toast', () => {
  400. setTimeout(expected, 1)
  401. })
  402. toast.show()
  403. })
  404. })
  405. describe('jQueryInterface', () => {
  406. it('should create a toast', () => {
  407. fixtureEl.innerHTML = '<div></div>'
  408. const div = fixtureEl.querySelector('div')
  409. jQueryMock.fn.toast = Toast.jQueryInterface
  410. jQueryMock.elements = [div]
  411. jQueryMock.fn.toast.call(jQueryMock)
  412. expect(Toast.getInstance(div)).not.toBeNull()
  413. })
  414. it('should not re create a toast', () => {
  415. fixtureEl.innerHTML = '<div></div>'
  416. const div = fixtureEl.querySelector('div')
  417. const toast = new Toast(div)
  418. jQueryMock.fn.toast = Toast.jQueryInterface
  419. jQueryMock.elements = [div]
  420. jQueryMock.fn.toast.call(jQueryMock)
  421. expect(Toast.getInstance(div)).toEqual(toast)
  422. })
  423. it('should call a toast method', () => {
  424. fixtureEl.innerHTML = '<div></div>'
  425. const div = fixtureEl.querySelector('div')
  426. const toast = new Toast(div)
  427. spyOn(toast, 'show')
  428. jQueryMock.fn.toast = Toast.jQueryInterface
  429. jQueryMock.elements = [div]
  430. jQueryMock.fn.toast.call(jQueryMock, 'show')
  431. expect(Toast.getInstance(div)).toEqual(toast)
  432. expect(toast.show).toHaveBeenCalled()
  433. })
  434. it('should throw error on undefined method', () => {
  435. fixtureEl.innerHTML = '<div></div>'
  436. const div = fixtureEl.querySelector('div')
  437. const action = 'undefinedMethod'
  438. jQueryMock.fn.toast = Toast.jQueryInterface
  439. jQueryMock.elements = [div]
  440. expect(() => {
  441. jQueryMock.fn.toast.call(jQueryMock, action)
  442. }).toThrowError(TypeError, `No method named "${action}"`)
  443. })
  444. })
  445. describe('getInstance', () => {
  446. it('should return a toast instance', () => {
  447. fixtureEl.innerHTML = '<div></div>'
  448. const div = fixtureEl.querySelector('div')
  449. const toast = new Toast(div)
  450. expect(Toast.getInstance(div)).toEqual(toast)
  451. expect(Toast.getInstance(div)).toBeInstanceOf(Toast)
  452. })
  453. it('should return null when there is no toast instance', () => {
  454. fixtureEl.innerHTML = '<div></div>'
  455. const div = fixtureEl.querySelector('div')
  456. expect(Toast.getInstance(div)).toEqual(null)
  457. })
  458. })
  459. describe('getOrCreateInstance', () => {
  460. it('should return toast instance', () => {
  461. fixtureEl.innerHTML = '<div></div>'
  462. const div = fixtureEl.querySelector('div')
  463. const toast = new Toast(div)
  464. expect(Toast.getOrCreateInstance(div)).toEqual(toast)
  465. expect(Toast.getInstance(div)).toEqual(Toast.getOrCreateInstance(div, {}))
  466. expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast)
  467. })
  468. it('should return new instance when there is no toast instance', () => {
  469. fixtureEl.innerHTML = '<div></div>'
  470. const div = fixtureEl.querySelector('div')
  471. expect(Toast.getInstance(div)).toEqual(null)
  472. expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast)
  473. })
  474. it('should return new instance when there is no toast instance with given configuration', () => {
  475. fixtureEl.innerHTML = '<div></div>'
  476. const div = fixtureEl.querySelector('div')
  477. expect(Toast.getInstance(div)).toEqual(null)
  478. const toast = Toast.getOrCreateInstance(div, {
  479. delay: 1
  480. })
  481. expect(toast).toBeInstanceOf(Toast)
  482. expect(toast._config.delay).toEqual(1)
  483. })
  484. it('should return the instance when exists without given configuration', () => {
  485. fixtureEl.innerHTML = '<div></div>'
  486. const div = fixtureEl.querySelector('div')
  487. const toast = new Toast(div, {
  488. delay: 1
  489. })
  490. expect(Toast.getInstance(div)).toEqual(toast)
  491. const toast2 = Toast.getOrCreateInstance(div, {
  492. delay: 2
  493. })
  494. expect(toast).toBeInstanceOf(Toast)
  495. expect(toast2).toEqual(toast)
  496. expect(toast2._config.delay).toEqual(1)
  497. })
  498. })
  499. })