toast.spec.js 19 KB

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