collapse.spec.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. import Collapse from '../../src/collapse.js'
  2. import EventHandler from '../../src/dom/event-handler.js'
  3. import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js'
  4. describe('Collapse', () => {
  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(Collapse.VERSION).toEqual(jasmine.any(String))
  15. })
  16. })
  17. describe('Default', () => {
  18. it('should return plugin default config', () => {
  19. expect(Collapse.Default).toEqual(jasmine.any(Object))
  20. })
  21. })
  22. describe('DATA_KEY', () => {
  23. it('should return plugin data key', () => {
  24. expect(Collapse.DATA_KEY).toEqual('bs.collapse')
  25. })
  26. })
  27. describe('constructor', () => {
  28. it('should take care of element either passed as a CSS selector or DOM element', () => {
  29. fixtureEl.innerHTML = '<div class="my-collapse"></div>'
  30. const collapseEl = fixtureEl.querySelector('div.my-collapse')
  31. const collapseBySelector = new Collapse('div.my-collapse')
  32. const collapseByElement = new Collapse(collapseEl)
  33. expect(collapseBySelector._element).toEqual(collapseEl)
  34. expect(collapseByElement._element).toEqual(collapseEl)
  35. })
  36. it('should allow jquery object in parent config', () => {
  37. fixtureEl.innerHTML = [
  38. '<div class="my-collapse">',
  39. ' <div class="item">',
  40. ' <a data-bs-toggle="collapse" href="#">Toggle item</a>',
  41. ' <div class="collapse">Lorem ipsum</div>',
  42. ' </div>',
  43. '</div>'
  44. ].join('')
  45. const collapseEl = fixtureEl.querySelector('div.collapse')
  46. const myCollapseEl = fixtureEl.querySelector('.my-collapse')
  47. const fakejQueryObject = {
  48. 0: myCollapseEl,
  49. jquery: 'foo'
  50. }
  51. const collapse = new Collapse(collapseEl, {
  52. parent: fakejQueryObject
  53. })
  54. expect(collapse._config.parent).toEqual(myCollapseEl)
  55. })
  56. it('should allow non jquery object in parent config', () => {
  57. fixtureEl.innerHTML = [
  58. '<div class="my-collapse">',
  59. ' <div class="item">',
  60. ' <a data-bs-toggle="collapse" href="#">Toggle item</a>',
  61. ' <div class="collapse">Lorem ipsum</div>',
  62. ' </div>',
  63. '</div>'
  64. ].join('')
  65. const collapseEl = fixtureEl.querySelector('div.collapse')
  66. const myCollapseEl = fixtureEl.querySelector('.my-collapse')
  67. const collapse = new Collapse(collapseEl, {
  68. parent: myCollapseEl
  69. })
  70. expect(collapse._config.parent).toEqual(myCollapseEl)
  71. })
  72. it('should allow string selector in parent config', () => {
  73. fixtureEl.innerHTML = [
  74. '<div class="my-collapse">',
  75. ' <div class="item">',
  76. ' <a data-bs-toggle="collapse" href="#">Toggle item</a>',
  77. ' <div class="collapse">Lorem ipsum</div>',
  78. ' </div>',
  79. '</div>'
  80. ].join('')
  81. const collapseEl = fixtureEl.querySelector('div.collapse')
  82. const myCollapseEl = fixtureEl.querySelector('.my-collapse')
  83. const collapse = new Collapse(collapseEl, {
  84. parent: 'div.my-collapse'
  85. })
  86. expect(collapse._config.parent).toEqual(myCollapseEl)
  87. })
  88. })
  89. describe('toggle', () => {
  90. it('should call show method if show class is not present', () => {
  91. fixtureEl.innerHTML = '<div></div>'
  92. const collapseEl = fixtureEl.querySelector('div')
  93. const collapse = new Collapse(collapseEl)
  94. const spy = spyOn(collapse, 'show')
  95. collapse.toggle()
  96. expect(spy).toHaveBeenCalled()
  97. })
  98. it('should call hide method if show class is present', () => {
  99. fixtureEl.innerHTML = '<div class="show"></div>'
  100. const collapseEl = fixtureEl.querySelector('.show')
  101. const collapse = new Collapse(collapseEl, {
  102. toggle: false
  103. })
  104. const spy = spyOn(collapse, 'hide')
  105. collapse.toggle()
  106. expect(spy).toHaveBeenCalled()
  107. })
  108. it('should find collapse children if they have collapse class too not only data-bs-parent', () => {
  109. return new Promise(resolve => {
  110. fixtureEl.innerHTML = [
  111. '<div class="my-collapse">',
  112. ' <div class="item">',
  113. ' <a data-bs-toggle="collapse" href="#">Toggle item 1</a>',
  114. ' <div id="collapse1" class="collapse show">Lorem ipsum 1</div>',
  115. ' </div>',
  116. ' <div class="item">',
  117. ' <a id="triggerCollapse2" data-bs-toggle="collapse" href="#">Toggle item 2</a>',
  118. ' <div id="collapse2" class="collapse">Lorem ipsum 2</div>',
  119. ' </div>',
  120. '</div>'
  121. ].join('')
  122. const parent = fixtureEl.querySelector('.my-collapse')
  123. const collapseEl1 = fixtureEl.querySelector('#collapse1')
  124. const collapseEl2 = fixtureEl.querySelector('#collapse2')
  125. const collapseList = [].concat(...fixtureEl.querySelectorAll('.collapse'))
  126. .map(el => new Collapse(el, {
  127. parent,
  128. toggle: false
  129. }))
  130. collapseEl2.addEventListener('shown.bs.collapse', () => {
  131. expect(collapseEl2).toHaveClass('show')
  132. expect(collapseEl1).not.toHaveClass('show')
  133. resolve()
  134. })
  135. collapseList[1].toggle()
  136. })
  137. })
  138. })
  139. describe('show', () => {
  140. it('should do nothing if is transitioning', () => {
  141. fixtureEl.innerHTML = '<div></div>'
  142. const spy = spyOn(EventHandler, 'trigger')
  143. const collapseEl = fixtureEl.querySelector('div')
  144. const collapse = new Collapse(collapseEl, {
  145. toggle: false
  146. })
  147. collapse._isTransitioning = true
  148. collapse.show()
  149. expect(spy).not.toHaveBeenCalled()
  150. })
  151. it('should do nothing if already shown', () => {
  152. fixtureEl.innerHTML = '<div class="show"></div>'
  153. const spy = spyOn(EventHandler, 'trigger')
  154. const collapseEl = fixtureEl.querySelector('div')
  155. const collapse = new Collapse(collapseEl, {
  156. toggle: false
  157. })
  158. collapse.show()
  159. expect(spy).not.toHaveBeenCalled()
  160. })
  161. it('should show a collapsed element', () => {
  162. return new Promise(resolve => {
  163. fixtureEl.innerHTML = '<div class="collapse" style="height: 0px;"></div>'
  164. const collapseEl = fixtureEl.querySelector('div')
  165. const collapse = new Collapse(collapseEl, {
  166. toggle: false
  167. })
  168. collapseEl.addEventListener('show.bs.collapse', () => {
  169. expect(collapseEl.style.height).toEqual('0px')
  170. })
  171. collapseEl.addEventListener('shown.bs.collapse', () => {
  172. expect(collapseEl).toHaveClass('show')
  173. expect(collapseEl.style.height).toEqual('')
  174. resolve()
  175. })
  176. collapse.show()
  177. })
  178. })
  179. it('should show a collapsed element on width', () => {
  180. return new Promise(resolve => {
  181. fixtureEl.innerHTML = '<div class="collapse collapse-horizontal" style="width: 0px;"></div>'
  182. const collapseEl = fixtureEl.querySelector('div')
  183. const collapse = new Collapse(collapseEl, {
  184. toggle: false
  185. })
  186. collapseEl.addEventListener('show.bs.collapse', () => {
  187. expect(collapseEl.style.width).toEqual('0px')
  188. })
  189. collapseEl.addEventListener('shown.bs.collapse', () => {
  190. expect(collapseEl).toHaveClass('show')
  191. expect(collapseEl.style.width).toEqual('')
  192. resolve()
  193. })
  194. collapse.show()
  195. })
  196. })
  197. it('should collapse only the first collapse', () => {
  198. return new Promise(resolve => {
  199. fixtureEl.innerHTML = [
  200. '<div class="card" id="accordion1">',
  201. ' <div id="collapse1" class="collapse"></div>',
  202. '</div>',
  203. '<div class="card" id="accordion2">',
  204. ' <div id="collapse2" class="collapse show"></div>',
  205. '</div>'
  206. ].join('')
  207. const el1 = fixtureEl.querySelector('#collapse1')
  208. const el2 = fixtureEl.querySelector('#collapse2')
  209. const collapse = new Collapse(el1, {
  210. toggle: false
  211. })
  212. el1.addEventListener('shown.bs.collapse', () => {
  213. expect(el1).toHaveClass('show')
  214. expect(el2).toHaveClass('show')
  215. resolve()
  216. })
  217. collapse.show()
  218. })
  219. })
  220. it('should be able to handle toggling of other children siblings', () => {
  221. return new Promise(resolve => {
  222. fixtureEl.innerHTML = [
  223. '<div id="parentGroup" class="accordion">',
  224. ' <div class="accordion-header">',
  225. ' <button data-bs-target="#parentContent" data-bs-toggle="collapse" class="accordion-toggle">Parent</button>',
  226. ' </div>',
  227. ' <div id="parentContent" class="accordion-collapse collapse" data-bs-parent="#parentGroup">',
  228. ' <div class="accordion-body">',
  229. ' <div id="childGroup" class="accordion">',
  230. ' <div class="accordion-item">',
  231. ' <div class="accordion-header">',
  232. ' <button data-bs-target="#childContent1" data-bs-toggle="collapse" class="accordion-toggle">Child 1</button>',
  233. ' </div>',
  234. ' <div id="childContent1" class="accordion-collapse collapse" data-bs-parent="#childGroup">',
  235. ' <div>content</div>',
  236. ' </div>',
  237. ' </div>',
  238. ' <div class="accordion-item">',
  239. ' <div class="accordion-header">',
  240. ' <button data-bs-target="#childContent2" data-bs-toggle="collapse" class="accordion-toggle">Child 2</button>',
  241. ' </div>',
  242. ' <div id="childContent2" class="accordion-collapse collapse" data-bs-parent="#childGroup">',
  243. ' <div>content</div>',
  244. ' </div>',
  245. ' </div>',
  246. ' </div>',
  247. ' </div>',
  248. ' </div>',
  249. '</div>'
  250. ].join('')
  251. const el = selector => fixtureEl.querySelector(selector)
  252. const parentBtn = el('[data-bs-target="#parentContent"]')
  253. const childBtn1 = el('[data-bs-target="#childContent1"]')
  254. const childBtn2 = el('[data-bs-target="#childContent2"]')
  255. const parentCollapseEl = el('#parentContent')
  256. const childCollapseEl1 = el('#childContent1')
  257. const childCollapseEl2 = el('#childContent2')
  258. parentCollapseEl.addEventListener('shown.bs.collapse', () => {
  259. expect(parentCollapseEl).toHaveClass('show')
  260. childBtn1.click()
  261. })
  262. childCollapseEl1.addEventListener('shown.bs.collapse', () => {
  263. expect(childCollapseEl1).toHaveClass('show')
  264. childBtn2.click()
  265. })
  266. childCollapseEl2.addEventListener('shown.bs.collapse', () => {
  267. expect(childCollapseEl2).toHaveClass('show')
  268. expect(childCollapseEl1).not.toHaveClass('show')
  269. resolve()
  270. })
  271. parentBtn.click()
  272. })
  273. })
  274. it('should not change tab tabpanels descendants on accordion', () => {
  275. return new Promise(resolve => {
  276. fixtureEl.innerHTML = [
  277. '<div class="accordion" id="accordionExample">',
  278. ' <div class="accordion-item">',
  279. ' <h2 class="accordion-header">',
  280. ' <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">',
  281. ' Accordion Item #1',
  282. ' </button>',
  283. ' </h2>',
  284. ' <div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#accordionExample">',
  285. ' <div class="accordion-body">',
  286. ' <nav>',
  287. ' <div class="nav nav-tabs" id="nav-tab" role="tablist">',
  288. ' <button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Home</button>',
  289. ' <button class="nav-link" id="nav-profile-tab" data-bs-toggle="tab" data-bs-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</button>',
  290. ' </div>',
  291. ' </nav>',
  292. ' <div class="tab-content" id="nav-tabContent">',
  293. ' <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">Home</div>',
  294. ' <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">Profile</div>',
  295. ' </div>',
  296. ' </div>',
  297. ' </div>',
  298. ' </div>',
  299. '</div>'
  300. ].join('')
  301. const el = fixtureEl.querySelector('#collapseOne')
  302. const activeTabPane = fixtureEl.querySelector('#nav-home')
  303. const collapse = new Collapse(el)
  304. let times = 1
  305. el.addEventListener('hidden.bs.collapse', () => {
  306. collapse.show()
  307. })
  308. el.addEventListener('shown.bs.collapse', () => {
  309. expect(activeTabPane).toHaveClass('show')
  310. times++
  311. if (times === 2) {
  312. resolve()
  313. }
  314. collapse.hide()
  315. })
  316. collapse.show()
  317. })
  318. })
  319. it('should not fire shown when show is prevented', () => {
  320. return new Promise((resolve, reject) => {
  321. fixtureEl.innerHTML = '<div class="collapse"></div>'
  322. const collapseEl = fixtureEl.querySelector('div')
  323. const collapse = new Collapse(collapseEl, {
  324. toggle: false
  325. })
  326. const expectEnd = () => {
  327. setTimeout(() => {
  328. expect().nothing()
  329. resolve()
  330. }, 10)
  331. }
  332. collapseEl.addEventListener('show.bs.collapse', event => {
  333. event.preventDefault()
  334. expectEnd()
  335. })
  336. collapseEl.addEventListener('shown.bs.collapse', () => {
  337. reject(new Error('should not fire shown event'))
  338. })
  339. collapse.show()
  340. })
  341. })
  342. })
  343. describe('hide', () => {
  344. it('should do nothing if is transitioning', () => {
  345. fixtureEl.innerHTML = '<div></div>'
  346. const spy = spyOn(EventHandler, 'trigger')
  347. const collapseEl = fixtureEl.querySelector('div')
  348. const collapse = new Collapse(collapseEl, {
  349. toggle: false
  350. })
  351. collapse._isTransitioning = true
  352. collapse.hide()
  353. expect(spy).not.toHaveBeenCalled()
  354. })
  355. it('should do nothing if already shown', () => {
  356. fixtureEl.innerHTML = '<div></div>'
  357. const spy = spyOn(EventHandler, 'trigger')
  358. const collapseEl = fixtureEl.querySelector('div')
  359. const collapse = new Collapse(collapseEl, {
  360. toggle: false
  361. })
  362. collapse.hide()
  363. expect(spy).not.toHaveBeenCalled()
  364. })
  365. it('should hide a collapsed element', () => {
  366. return new Promise(resolve => {
  367. fixtureEl.innerHTML = '<div class="collapse show"></div>'
  368. const collapseEl = fixtureEl.querySelector('div')
  369. const collapse = new Collapse(collapseEl, {
  370. toggle: false
  371. })
  372. collapseEl.addEventListener('hidden.bs.collapse', () => {
  373. expect(collapseEl).not.toHaveClass('show')
  374. expect(collapseEl.style.height).toEqual('')
  375. resolve()
  376. })
  377. collapse.hide()
  378. })
  379. })
  380. it('should not fire hidden when hide is prevented', () => {
  381. return new Promise((resolve, reject) => {
  382. fixtureEl.innerHTML = '<div class="collapse show"></div>'
  383. const collapseEl = fixtureEl.querySelector('div')
  384. const collapse = new Collapse(collapseEl, {
  385. toggle: false
  386. })
  387. const expectEnd = () => {
  388. setTimeout(() => {
  389. expect().nothing()
  390. resolve()
  391. }, 10)
  392. }
  393. collapseEl.addEventListener('hide.bs.collapse', event => {
  394. event.preventDefault()
  395. expectEnd()
  396. })
  397. collapseEl.addEventListener('hidden.bs.collapse', () => {
  398. reject(new Error('should not fire hidden event'))
  399. })
  400. collapse.hide()
  401. })
  402. })
  403. })
  404. describe('dispose', () => {
  405. it('should destroy a collapse', () => {
  406. fixtureEl.innerHTML = '<div class="collapse show"></div>'
  407. const collapseEl = fixtureEl.querySelector('div')
  408. const collapse = new Collapse(collapseEl, {
  409. toggle: false
  410. })
  411. expect(Collapse.getInstance(collapseEl)).toEqual(collapse)
  412. collapse.dispose()
  413. expect(Collapse.getInstance(collapseEl)).toBeNull()
  414. })
  415. })
  416. describe('data-api', () => {
  417. it('should prevent url change if click on nested elements', () => {
  418. return new Promise(resolve => {
  419. fixtureEl.innerHTML = [
  420. '<a role="button" data-bs-toggle="collapse" class="collapsed" href="#collapse">',
  421. ' <span id="nested"></span>',
  422. '</a>',
  423. '<div id="collapse" class="collapse"></div>'
  424. ].join('')
  425. const triggerEl = fixtureEl.querySelector('a')
  426. const nestedTriggerEl = fixtureEl.querySelector('#nested')
  427. const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
  428. triggerEl.addEventListener('click', event => {
  429. expect(event.target.isEqualNode(nestedTriggerEl)).toBeTrue()
  430. expect(event.delegateTarget.isEqualNode(triggerEl)).toBeTrue()
  431. expect(spy).toHaveBeenCalled()
  432. resolve()
  433. })
  434. nestedTriggerEl.click()
  435. })
  436. })
  437. it('should show multiple collapsed elements', () => {
  438. return new Promise(resolve => {
  439. fixtureEl.innerHTML = [
  440. '<a role="button" data-bs-toggle="collapse" class="collapsed" href=".multi"></a>',
  441. '<div id="collapse1" class="collapse multi"></div>',
  442. '<div id="collapse2" class="collapse multi"></div>'
  443. ].join('')
  444. const trigger = fixtureEl.querySelector('a')
  445. const collapse1 = fixtureEl.querySelector('#collapse1')
  446. const collapse2 = fixtureEl.querySelector('#collapse2')
  447. collapse2.addEventListener('shown.bs.collapse', () => {
  448. expect(trigger.getAttribute('aria-expanded')).toEqual('true')
  449. expect(trigger).not.toHaveClass('collapsed')
  450. expect(collapse1).toHaveClass('show')
  451. expect(collapse1).toHaveClass('show')
  452. resolve()
  453. })
  454. trigger.click()
  455. })
  456. })
  457. it('should hide multiple collapsed elements', () => {
  458. return new Promise(resolve => {
  459. fixtureEl.innerHTML = [
  460. '<a role="button" data-bs-toggle="collapse" href=".multi"></a>',
  461. '<div id="collapse1" class="collapse multi show"></div>',
  462. '<div id="collapse2" class="collapse multi show"></div>'
  463. ].join('')
  464. const trigger = fixtureEl.querySelector('a')
  465. const collapse1 = fixtureEl.querySelector('#collapse1')
  466. const collapse2 = fixtureEl.querySelector('#collapse2')
  467. collapse2.addEventListener('hidden.bs.collapse', () => {
  468. expect(trigger.getAttribute('aria-expanded')).toEqual('false')
  469. expect(trigger).toHaveClass('collapsed')
  470. expect(collapse1).not.toHaveClass('show')
  471. expect(collapse1).not.toHaveClass('show')
  472. resolve()
  473. })
  474. trigger.click()
  475. })
  476. })
  477. it('should remove "collapsed" class from target when collapse is shown', () => {
  478. return new Promise(resolve => {
  479. fixtureEl.innerHTML = [
  480. '<a id="link1" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>',
  481. '<a id="link2" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>',
  482. '<div id="test1"></div>'
  483. ].join('')
  484. const link1 = fixtureEl.querySelector('#link1')
  485. const link2 = fixtureEl.querySelector('#link2')
  486. const collapseTest1 = fixtureEl.querySelector('#test1')
  487. collapseTest1.addEventListener('shown.bs.collapse', () => {
  488. expect(link1.getAttribute('aria-expanded')).toEqual('true')
  489. expect(link2.getAttribute('aria-expanded')).toEqual('true')
  490. expect(link1).not.toHaveClass('collapsed')
  491. expect(link2).not.toHaveClass('collapsed')
  492. resolve()
  493. })
  494. link1.click()
  495. })
  496. })
  497. it('should add "collapsed" class to target when collapse is hidden', () => {
  498. return new Promise(resolve => {
  499. fixtureEl.innerHTML = [
  500. '<a id="link1" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>',
  501. '<a id="link2" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>',
  502. '<div id="test1" class="show"></div>'
  503. ].join('')
  504. const link1 = fixtureEl.querySelector('#link1')
  505. const link2 = fixtureEl.querySelector('#link2')
  506. const collapseTest1 = fixtureEl.querySelector('#test1')
  507. collapseTest1.addEventListener('hidden.bs.collapse', () => {
  508. expect(link1.getAttribute('aria-expanded')).toEqual('false')
  509. expect(link2.getAttribute('aria-expanded')).toEqual('false')
  510. expect(link1).toHaveClass('collapsed')
  511. expect(link2).toHaveClass('collapsed')
  512. resolve()
  513. })
  514. link1.click()
  515. })
  516. })
  517. it('should allow accordion to use children other than card', () => {
  518. return new Promise(resolve => {
  519. fixtureEl.innerHTML = [
  520. '<div id="accordion">',
  521. ' <div class="item">',
  522. ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>',
  523. ' <div id="collapseOne" class="collapse" role="tabpanel" data-bs-parent="#accordion"></div>',
  524. ' </div>',
  525. ' <div class="item">',
  526. ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>',
  527. ' <div id="collapseTwo" class="collapse show" role="tabpanel" data-bs-parent="#accordion"></div>',
  528. ' </div>',
  529. '</div>'
  530. ].join('')
  531. const trigger = fixtureEl.querySelector('#linkTrigger')
  532. const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo')
  533. const collapseOne = fixtureEl.querySelector('#collapseOne')
  534. const collapseTwo = fixtureEl.querySelector('#collapseTwo')
  535. collapseOne.addEventListener('shown.bs.collapse', () => {
  536. expect(collapseOne).toHaveClass('show')
  537. expect(collapseTwo).not.toHaveClass('show')
  538. collapseTwo.addEventListener('shown.bs.collapse', () => {
  539. expect(collapseOne).not.toHaveClass('show')
  540. expect(collapseTwo).toHaveClass('show')
  541. resolve()
  542. })
  543. triggerTwo.click()
  544. })
  545. trigger.click()
  546. })
  547. })
  548. it('should not prevent event for input', () => {
  549. return new Promise(resolve => {
  550. fixtureEl.innerHTML = [
  551. '<input type="checkbox" data-bs-toggle="collapse" data-bs-target="#collapsediv1">',
  552. '<div id="collapsediv1"></div>'
  553. ].join('')
  554. const target = fixtureEl.querySelector('input')
  555. const collapseEl = fixtureEl.querySelector('#collapsediv1')
  556. collapseEl.addEventListener('shown.bs.collapse', () => {
  557. expect(collapseEl).toHaveClass('show')
  558. expect(target.checked).toBeTrue()
  559. resolve()
  560. })
  561. target.click()
  562. })
  563. })
  564. it('should allow accordion to contain nested elements', () => {
  565. return new Promise(resolve => {
  566. fixtureEl.innerHTML = [
  567. '<div id="accordion">',
  568. ' <div class="row">',
  569. ' <div class="col-lg-6">',
  570. ' <div class="item">',
  571. ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>',
  572. ' <div id="collapseOne" class="collapse" role="tabpanel" data-bs-parent="#accordion"></div>',
  573. ' </div>',
  574. ' </div>',
  575. ' <div class="col-lg-6">',
  576. ' <div class="item">',
  577. ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>',
  578. ' <div id="collapseTwo" class="collapse show" role="tabpanel" data-bs-parent="#accordion"></div>',
  579. ' </div>',
  580. ' </div>',
  581. ' </div>',
  582. '</div>'
  583. ].join('')
  584. const triggerEl = fixtureEl.querySelector('#linkTrigger')
  585. const triggerTwoEl = fixtureEl.querySelector('#linkTriggerTwo')
  586. const collapseOneEl = fixtureEl.querySelector('#collapseOne')
  587. const collapseTwoEl = fixtureEl.querySelector('#collapseTwo')
  588. collapseOneEl.addEventListener('shown.bs.collapse', () => {
  589. expect(collapseOneEl).toHaveClass('show')
  590. expect(triggerEl).not.toHaveClass('collapsed')
  591. expect(triggerEl.getAttribute('aria-expanded')).toEqual('true')
  592. expect(collapseTwoEl).not.toHaveClass('show')
  593. expect(triggerTwoEl).toHaveClass('collapsed')
  594. expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false')
  595. collapseTwoEl.addEventListener('shown.bs.collapse', () => {
  596. expect(collapseOneEl).not.toHaveClass('show')
  597. expect(triggerEl).toHaveClass('collapsed')
  598. expect(triggerEl.getAttribute('aria-expanded')).toEqual('false')
  599. expect(collapseTwoEl).toHaveClass('show')
  600. expect(triggerTwoEl).not.toHaveClass('collapsed')
  601. expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true')
  602. resolve()
  603. })
  604. triggerTwoEl.click()
  605. })
  606. triggerEl.click()
  607. })
  608. })
  609. it('should allow accordion to target multiple elements', () => {
  610. return new Promise(resolve => {
  611. fixtureEl.innerHTML = [
  612. '<div id="accordion">',
  613. ' <a id="linkTriggerOne" data-bs-toggle="collapse" data-bs-target=".collapseOne" href="#" aria-expanded="false" aria-controls="collapseOne"></a>',
  614. ' <a id="linkTriggerTwo" data-bs-toggle="collapse" data-bs-target=".collapseTwo" href="#" aria-expanded="false" aria-controls="collapseTwo"></a>',
  615. ' <div id="collapseOneOne" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>',
  616. ' <div id="collapseOneTwo" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>',
  617. ' <div id="collapseTwoOne" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>',
  618. ' <div id="collapseTwoTwo" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>',
  619. '</div>'
  620. ].join('')
  621. const trigger = fixtureEl.querySelector('#linkTriggerOne')
  622. const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo')
  623. const collapseOneOne = fixtureEl.querySelector('#collapseOneOne')
  624. const collapseOneTwo = fixtureEl.querySelector('#collapseOneTwo')
  625. const collapseTwoOne = fixtureEl.querySelector('#collapseTwoOne')
  626. const collapseTwoTwo = fixtureEl.querySelector('#collapseTwoTwo')
  627. const collapsedElements = {
  628. one: false,
  629. two: false
  630. }
  631. function firstTest() {
  632. expect(collapseOneOne).toHaveClass('show')
  633. expect(collapseOneTwo).toHaveClass('show')
  634. expect(collapseTwoOne).not.toHaveClass('show')
  635. expect(collapseTwoTwo).not.toHaveClass('show')
  636. triggerTwo.click()
  637. }
  638. function secondTest() {
  639. expect(collapseOneOne).not.toHaveClass('show')
  640. expect(collapseOneTwo).not.toHaveClass('show')
  641. expect(collapseTwoOne).toHaveClass('show')
  642. expect(collapseTwoTwo).toHaveClass('show')
  643. resolve()
  644. }
  645. collapseOneOne.addEventListener('shown.bs.collapse', () => {
  646. if (collapsedElements.one) {
  647. firstTest()
  648. } else {
  649. collapsedElements.one = true
  650. }
  651. })
  652. collapseOneTwo.addEventListener('shown.bs.collapse', () => {
  653. if (collapsedElements.one) {
  654. firstTest()
  655. } else {
  656. collapsedElements.one = true
  657. }
  658. })
  659. collapseTwoOne.addEventListener('shown.bs.collapse', () => {
  660. if (collapsedElements.two) {
  661. secondTest()
  662. } else {
  663. collapsedElements.two = true
  664. }
  665. })
  666. collapseTwoTwo.addEventListener('shown.bs.collapse', () => {
  667. if (collapsedElements.two) {
  668. secondTest()
  669. } else {
  670. collapsedElements.two = true
  671. }
  672. })
  673. trigger.click()
  674. })
  675. })
  676. it('should collapse accordion children but not nested accordion children', () => {
  677. return new Promise(resolve => {
  678. fixtureEl.innerHTML = [
  679. '<div id="accordion">',
  680. ' <div class="item">',
  681. ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>',
  682. ' <div id="collapseOne" data-bs-parent="#accordion" class="collapse" role="tabpanel">',
  683. ' <div id="nestedAccordion">',
  684. ' <div class="item">',
  685. ' <a id="nestedLinkTrigger" data-bs-toggle="collapse" href="#nestedCollapseOne" aria-expanded="false" aria-controls="nestedCollapseOne"></a>',
  686. ' <div id="nestedCollapseOne" data-bs-parent="#nestedAccordion" class="collapse" role="tabpanel"></div>',
  687. ' </div>',
  688. ' </div>',
  689. ' </div>',
  690. ' </div>',
  691. ' <div class="item">',
  692. ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>',
  693. ' <div id="collapseTwo" data-bs-parent="#accordion" class="collapse show" role="tabpanel"></div>',
  694. ' </div>',
  695. '</div>'
  696. ].join('')
  697. const trigger = fixtureEl.querySelector('#linkTrigger')
  698. const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo')
  699. const nestedTrigger = fixtureEl.querySelector('#nestedLinkTrigger')
  700. const collapseOne = fixtureEl.querySelector('#collapseOne')
  701. const collapseTwo = fixtureEl.querySelector('#collapseTwo')
  702. const nestedCollapseOne = fixtureEl.querySelector('#nestedCollapseOne')
  703. function handlerCollapseOne() {
  704. expect(collapseOne).toHaveClass('show')
  705. expect(collapseTwo).not.toHaveClass('show')
  706. expect(nestedCollapseOne).not.toHaveClass('show')
  707. nestedCollapseOne.addEventListener('shown.bs.collapse', handlerNestedCollapseOne)
  708. nestedTrigger.click()
  709. collapseOne.removeEventListener('shown.bs.collapse', handlerCollapseOne)
  710. }
  711. function handlerNestedCollapseOne() {
  712. expect(collapseOne).toHaveClass('show')
  713. expect(collapseTwo).not.toHaveClass('show')
  714. expect(nestedCollapseOne).toHaveClass('show')
  715. collapseTwo.addEventListener('shown.bs.collapse', () => {
  716. expect(collapseOne).not.toHaveClass('show')
  717. expect(collapseTwo).toHaveClass('show')
  718. expect(nestedCollapseOne).toHaveClass('show')
  719. resolve()
  720. })
  721. triggerTwo.click()
  722. nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne)
  723. }
  724. collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne)
  725. trigger.click()
  726. })
  727. })
  728. it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', () => {
  729. return new Promise(resolve => {
  730. fixtureEl.innerHTML = [
  731. '<a id="trigger1" role="button" data-bs-toggle="collapse" href="#test1"></a>',
  732. '<a id="trigger2" role="button" data-bs-toggle="collapse" href="#0/my/id"></a>',
  733. '<a id="trigger3" role="button" data-bs-toggle="collapse" href=".multi"></a>',
  734. '<div id="test1" class="multi"></div>',
  735. '<div id="0/my/id" class="multi"></div>'
  736. ].join('')
  737. const trigger1 = fixtureEl.querySelector('#trigger1')
  738. const trigger2 = fixtureEl.querySelector('#trigger2')
  739. const trigger3 = fixtureEl.querySelector('#trigger3')
  740. const target1 = fixtureEl.querySelector('#test1')
  741. const target2 = fixtureEl.querySelector(`#${CSS.escape('0/my/id')}`)
  742. const target2Shown = () => {
  743. expect(trigger1).not.toHaveClass('collapsed')
  744. expect(trigger1.getAttribute('aria-expanded')).toEqual('true')
  745. expect(trigger2).not.toHaveClass('collapsed')
  746. expect(trigger2.getAttribute('aria-expanded')).toEqual('true')
  747. expect(trigger3).not.toHaveClass('collapsed')
  748. expect(trigger3.getAttribute('aria-expanded')).toEqual('true')
  749. target2.addEventListener('hidden.bs.collapse', () => {
  750. expect(trigger1).not.toHaveClass('collapsed')
  751. expect(trigger1.getAttribute('aria-expanded')).toEqual('true')
  752. expect(trigger2).toHaveClass('collapsed')
  753. expect(trigger2.getAttribute('aria-expanded')).toEqual('false')
  754. expect(trigger3).not.toHaveClass('collapsed')
  755. expect(trigger3.getAttribute('aria-expanded')).toEqual('true')
  756. target1.addEventListener('hidden.bs.collapse', () => {
  757. expect(trigger1).toHaveClass('collapsed')
  758. expect(trigger1.getAttribute('aria-expanded')).toEqual('false')
  759. expect(trigger2).toHaveClass('collapsed')
  760. expect(trigger2.getAttribute('aria-expanded')).toEqual('false')
  761. expect(trigger3).toHaveClass('collapsed')
  762. expect(trigger3.getAttribute('aria-expanded')).toEqual('false')
  763. resolve()
  764. })
  765. trigger1.click()
  766. })
  767. trigger2.click()
  768. }
  769. target2.addEventListener('shown.bs.collapse', target2Shown)
  770. trigger3.click()
  771. })
  772. })
  773. })
  774. describe('jQueryInterface', () => {
  775. it('should create a collapse', () => {
  776. fixtureEl.innerHTML = '<div></div>'
  777. const div = fixtureEl.querySelector('div')
  778. jQueryMock.fn.collapse = Collapse.jQueryInterface
  779. jQueryMock.elements = [div]
  780. jQueryMock.fn.collapse.call(jQueryMock)
  781. expect(Collapse.getInstance(div)).not.toBeNull()
  782. })
  783. it('should not re create a collapse', () => {
  784. fixtureEl.innerHTML = '<div></div>'
  785. const div = fixtureEl.querySelector('div')
  786. const collapse = new Collapse(div)
  787. jQueryMock.fn.collapse = Collapse.jQueryInterface
  788. jQueryMock.elements = [div]
  789. jQueryMock.fn.collapse.call(jQueryMock)
  790. expect(Collapse.getInstance(div)).toEqual(collapse)
  791. })
  792. it('should throw error on undefined method', () => {
  793. fixtureEl.innerHTML = '<div></div>'
  794. const div = fixtureEl.querySelector('div')
  795. const action = 'undefinedMethod'
  796. jQueryMock.fn.collapse = Collapse.jQueryInterface
  797. jQueryMock.elements = [div]
  798. expect(() => {
  799. jQueryMock.fn.collapse.call(jQueryMock, action)
  800. }).toThrowError(TypeError, `No method named "${action}"`)
  801. })
  802. })
  803. describe('getInstance', () => {
  804. it('should return collapse instance', () => {
  805. fixtureEl.innerHTML = '<div></div>'
  806. const div = fixtureEl.querySelector('div')
  807. const collapse = new Collapse(div)
  808. expect(Collapse.getInstance(div)).toEqual(collapse)
  809. expect(Collapse.getInstance(div)).toBeInstanceOf(Collapse)
  810. })
  811. it('should return null when there is no collapse instance', () => {
  812. fixtureEl.innerHTML = '<div></div>'
  813. const div = fixtureEl.querySelector('div')
  814. expect(Collapse.getInstance(div)).toBeNull()
  815. })
  816. })
  817. describe('getOrCreateInstance', () => {
  818. it('should return collapse instance', () => {
  819. fixtureEl.innerHTML = '<div></div>'
  820. const div = fixtureEl.querySelector('div')
  821. const collapse = new Collapse(div)
  822. expect(Collapse.getOrCreateInstance(div)).toEqual(collapse)
  823. expect(Collapse.getInstance(div)).toEqual(Collapse.getOrCreateInstance(div, {}))
  824. expect(Collapse.getOrCreateInstance(div)).toBeInstanceOf(Collapse)
  825. })
  826. it('should return new instance when there is no collapse instance', () => {
  827. fixtureEl.innerHTML = '<div></div>'
  828. const div = fixtureEl.querySelector('div')
  829. expect(Collapse.getInstance(div)).toBeNull()
  830. expect(Collapse.getOrCreateInstance(div)).toBeInstanceOf(Collapse)
  831. })
  832. it('should return new instance when there is no collapse instance with given configuration', () => {
  833. fixtureEl.innerHTML = '<div></div>'
  834. const div = fixtureEl.querySelector('div')
  835. expect(Collapse.getInstance(div)).toBeNull()
  836. const collapse = Collapse.getOrCreateInstance(div, {
  837. toggle: false
  838. })
  839. expect(collapse).toBeInstanceOf(Collapse)
  840. expect(collapse._config.toggle).toBeFalse()
  841. })
  842. it('should return the instance when exists without given configuration', () => {
  843. fixtureEl.innerHTML = '<div></div>'
  844. const div = fixtureEl.querySelector('div')
  845. const collapse = new Collapse(div, {
  846. toggle: false
  847. })
  848. expect(Collapse.getInstance(div)).toEqual(collapse)
  849. const collapse2 = Collapse.getOrCreateInstance(div, {
  850. toggle: true
  851. })
  852. expect(collapse).toBeInstanceOf(Collapse)
  853. expect(collapse2).toEqual(collapse)
  854. expect(collapse2._config.toggle).toBeFalse()
  855. })
  856. })
  857. })