diff options
author | GeoSot <geo.sotis@gmail.com> | 2022-01-30 15:30:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-30 15:30:04 +0300 |
commit | aa650f0f1e30279f0868433a4afab9c3efa93b2c (patch) | |
tree | 88cf537f2e7b6613bc2e53d4e38ca93798786d07 | |
parent | d09281705988690b63a5364548447c603cb557fd (diff) |
tests: replace 'done' callback with 'Promise' to fix deprecation errors (#35659)
Reference:
https://jasmine.github.io/tutorials/async
'DEPRECATION: An asynchronous function called its 'done' callback more than once. This is a bug in the spec, beforeAll, beforeEach, afterAll, or afterEach function in question. This will be treated as an error in a future version. See<https://jasmine.github.io/tutorials/upgrading_to_Jasmine_4.0#deprecations-due-to-calling-done-multiple-times> for more information.
-rw-r--r-- | js/tests/README.md | 28 | ||||
-rw-r--r-- | js/tests/unit/alert.spec.js | 84 | ||||
-rw-r--r-- | js/tests/unit/carousel.spec.js | 1208 | ||||
-rw-r--r-- | js/tests/unit/collapse.spec.js | 1164 | ||||
-rw-r--r-- | js/tests/unit/dom/event-handler.spec.js | 541 | ||||
-rw-r--r-- | js/tests/unit/dom/manipulator.spec.js | 60 | ||||
-rw-r--r-- | js/tests/unit/dropdown.spec.js | 2856 | ||||
-rw-r--r-- | js/tests/unit/jquery.spec.js | 32 | ||||
-rw-r--r-- | js/tests/unit/modal.spec.js | 1362 | ||||
-rw-r--r-- | js/tests/unit/offcanvas.spec.js | 586 | ||||
-rw-r--r-- | js/tests/unit/popover.spec.js | 270 | ||||
-rw-r--r-- | js/tests/unit/scrollspy.spec.js | 796 | ||||
-rw-r--r-- | js/tests/unit/tab.spec.js | 914 | ||||
-rw-r--r-- | js/tests/unit/toast.spec.js | 678 | ||||
-rw-r--r-- | js/tests/unit/tooltip.spec.js | 1222 | ||||
-rw-r--r-- | js/tests/unit/util/backdrop.spec.js | 428 | ||||
-rw-r--r-- | js/tests/unit/util/focustrap.spec.js | 240 | ||||
-rw-r--r-- | js/tests/unit/util/index.spec.js | 165 | ||||
-rw-r--r-- | js/tests/unit/util/scrollbar.spec.js | 9 | ||||
-rw-r--r-- | js/tests/unit/util/swipe.spec.js | 204 |
20 files changed, 6715 insertions, 6132 deletions
diff --git a/js/tests/README.md b/js/tests/README.md index ca99c0ede0..79d05d444f 100644 --- a/js/tests/README.md +++ b/js/tests/README.md @@ -50,22 +50,24 @@ describe('getInstance', () => { }) // Asynchronous test -it('should show a tooltip without the animation', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' +it('should show a tooltip without the animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip.classList.contains('fade')).toEqual(false) - done() - }) + expect(tip).not.toBeNull() + expect(tip.classList.contains('fade')).toEqual(false) + resolve() + }) - tooltip.show() + tooltip.show() + }) }) ``` diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index 210ae9a25e..e2fe49246a 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -63,60 +63,66 @@ describe('Alert', () => { }) describe('close', () => { - it('should close an alert', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - fixtureEl.innerHTML = '<div class="alert"></div>' + it('should close an alert', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + fixtureEl.innerHTML = '<div class="alert"></div>' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - expect(spy).not.toHaveBeenCalled() - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + resolve() + }) - alert.close() + alert.close() + }) }) - it('should close alert with fade class', done => { - fixtureEl.innerHTML = '<div class="alert fade"></div>' + it('should close alert with fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="alert fade"></div>' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('transitionend', () => { - expect().nothing() - }) + alertEl.addEventListener('transitionend', () => { + expect().nothing() + }) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + resolve() + }) - alert.close() + alert.close() + }) }) - it('should not remove alert if close event is prevented', done => { - fixtureEl.innerHTML = '<div class="alert"></div>' + it('should not remove alert if close event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="alert"></div>' - const getAlert = () => document.querySelector('.alert') - const alertEl = getAlert() - const alert = new Alert(alertEl) + const getAlert = () => document.querySelector('.alert') + const alertEl = getAlert() + const alert = new Alert(alertEl) - alertEl.addEventListener('close.bs.alert', event => { - event.preventDefault() - setTimeout(() => { - expect(getAlert()).not.toBeNull() - done() - }, 10) - }) + alertEl.addEventListener('close.bs.alert', event => { + event.preventDefault() + setTimeout(() => { + expect(getAlert()).not.toBeNull() + resolve() + }, 10) + }) - alertEl.addEventListener('closed.bs.alert', () => { - throw new Error('should not fire closed event') - }) + alertEl.addEventListener('closed.bs.alert', () => { + throw new Error('should not fire closed event') + }) - alert.close() + alert.close() + }) }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 7b58b9de9e..1c91cebec4 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -63,94 +63,100 @@ describe('Carousel', () => { expect(carouselByElement._element).toEqual(carouselEl) }) - it('should go to next item if right arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div id="item2" class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should go to next item if right arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(carousel._keydown).toHaveBeenCalled() + resolve() + }) - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) - - spyOn(carousel, '_keydown').and.callThrough() + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() + carouselEl.dispatchEvent(keydown) }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowRight' - - carouselEl.dispatchEvent(keydown) }) - it('should go to previous item if left arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div id="item1" class="carousel-item">item 1</div>', - ' <div class="carousel-item active">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should go to previous item if left arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div id="item1" class="carousel-item">item 1</div>', + ' <div class="carousel-item active">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) - spyOn(carousel, '_keydown').and.callThrough() + spyOn(carousel, '_keydown').and.callThrough() - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(carousel._keydown).toHaveBeenCalled() + resolve() + }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowLeft' + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' - carouselEl.dispatchEvent(keydown) + carouselEl.dispatchEvent(keydown) + }) }) - it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) - spyOn(carousel, '_keydown').and.callThrough() + spyOn(carousel, '_keydown').and.callThrough() - carouselEl.addEventListener('keydown', event => { - expect(carousel._keydown).toHaveBeenCalled() - expect(event.defaultPrevented).toBeFalse() - done() - }) + carouselEl.addEventListener('keydown', event => { + expect(carousel._keydown).toHaveBeenCalled() + expect(event.defaultPrevented).toBeFalse() + resolve() + }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - carouselEl.dispatchEvent(keydown) + carouselEl.dispatchEvent(keydown) + }) }) it('should ignore keyboard events within <input>s and <textarea>s', () => { @@ -222,70 +228,74 @@ describe('Carousel', () => { expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() }) - it('should wrap around from end to start when wrap option is true', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div id="one" class="carousel-item active"></div>', - ' <div id="two" class="carousel-item"></div>', - ' <div id="three" class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { wrap: true }) - const getActiveId = () => carouselEl.querySelector('.carousel-item.active').getAttribute('id') - - carouselEl.addEventListener('slid.bs.carousel', event => { - const activeId = getActiveId() - - if (activeId === 'two') { - carousel.next() - return - } - - if (activeId === 'three') { - carousel.next() - return - } - - if (activeId === 'one') { - // carousel wrapped around and slid from 3rd to 1st slide - expect(activeId).toEqual('one') - expect(event.from + 1).toEqual(3) - done() - } + it('should wrap around from end to start when wrap option is true', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div id="one" class="carousel-item active"></div>', + ' <div id="two" class="carousel-item"></div>', + ' <div id="three" class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { wrap: true }) + const getActiveId = () => carouselEl.querySelector('.carousel-item.active').getAttribute('id') + + carouselEl.addEventListener('slid.bs.carousel', event => { + const activeId = getActiveId() + + if (activeId === 'two') { + carousel.next() + return + } + + if (activeId === 'three') { + carousel.next() + return + } + + if (activeId === 'one') { + // carousel wrapped around and slid from 3rd to 1st slide + expect(activeId).toEqual('one') + expect(event.from + 1).toEqual(3) + resolve() + } + }) + + carousel.next() }) - - carousel.next() }) - it('should stay at the start when the prev method is called and wrap is false', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div id="one" class="carousel-item active"></div>', - ' <div id="two" class="carousel-item"></div>', - ' <div id="three" class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should stay at the start when the prev method is called and wrap is false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div id="one" class="carousel-item active"></div>', + ' <div id="two" class="carousel-item"></div>', + ' <div id="three" class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const firstElement = fixtureEl.querySelector('#one') - const carousel = new Carousel(carouselEl, { wrap: false }) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const firstElement = fixtureEl.querySelector('#one') + const carousel = new Carousel(carouselEl, { wrap: false }) - carouselEl.addEventListener('slid.bs.carousel', () => { - throw new Error('carousel slid when it should not have slid') - }) + carouselEl.addEventListener('slid.bs.carousel', () => { + throw new Error('carousel slid when it should not have slid') + }) - carousel.prev() + carousel.prev() - setTimeout(() => { - expect(firstElement).toHaveClass('active') - done() - }, 10) + setTimeout(() => { + expect(firstElement).toHaveClass('active') + resolve() + }, 10) + }) }) it('should not add touch event listeners if touch = false', () => { @@ -335,274 +345,290 @@ describe('Carousel', () => { expect(carousel._addTouchEventListeners).toHaveBeenCalled() }) - it('should allow swiperight and call _slide (prev) with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - document.documentElement.ontouchstart = noop - document.head.append(stylesCarousel) - Simulator.setType('pointer') - - fixtureEl.innerHTML = [ - '<div class="carousel" data-bs-interval="false">', - ' <div class="carousel-inner">', - ' <div id="item" class="carousel-item">', - ' <img alt="">', - ' </div>', - ' <div class="carousel-item active">', - ' <img alt="">', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, '_slide').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', event => { - expect(item).toHaveClass('active') - expect(carousel._slide).toHaveBeenCalledWith('right') - expect(event.direction).toEqual('right') - stylesCarousel.remove() - delete document.documentElement.ontouchstart - done() - }) + it('should allow swiperight and call _slide (prev) with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { + expect().nothing() + resolve() + return + } - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 + document.documentElement.ontouchstart = noop + document.head.append(stylesCarousel) + Simulator.setType('pointer') + + fixtureEl.innerHTML = [ + '<div class="carousel" data-bs-interval="false">', + ' <div class="carousel-inner">', + ' <div id="item" class="carousel-item">', + ' <img alt="">', + ' </div>', + ' <div class="carousel-item active">', + ' <img alt="">', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).toHaveClass('active') + expect(carousel._slide).toHaveBeenCalledWith('right') + expect(event.direction).toEqual('right') + stylesCarousel.remove() + delete document.documentElement.ontouchstart + resolve() + }) + + Simulator.gestures.swipe(carouselEl, { + deltaX: 300, + deltaY: 0 + }) }) }) - it('should allow swipeleft and call next with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - document.documentElement.ontouchstart = noop - document.head.append(stylesCarousel) - Simulator.setType('pointer') - - fixtureEl.innerHTML = [ - '<div class="carousel" data-bs-interval="false">', - ' <div class="carousel-inner">', - ' <div id="item" class="carousel-item active">', - ' <img alt="">', - ' </div>', - ' <div class="carousel-item">', - ' <img alt="">', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, '_slide').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', event => { - expect(item).not.toHaveClass('active') - expect(carousel._slide).toHaveBeenCalledWith('left') - expect(event.direction).toEqual('left') - stylesCarousel.remove() - delete document.documentElement.ontouchstart - done() - }) + it('should allow swipeleft and call next with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { + expect().nothing() + resolve() + return + } - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 + document.documentElement.ontouchstart = noop + document.head.append(stylesCarousel) + Simulator.setType('pointer') + + fixtureEl.innerHTML = [ + '<div class="carousel" data-bs-interval="false">', + ' <div class="carousel-inner">', + ' <div id="item" class="carousel-item active">', + ' <img alt="">', + ' </div>', + ' <div class="carousel-item">', + ' <img alt="">', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).not.toHaveClass('active') + expect(carousel._slide).toHaveBeenCalledWith('left') + expect(event.direction).toEqual('left') + stylesCarousel.remove() + delete document.documentElement.ontouchstart + resolve() + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) }) }) - it('should allow swiperight and call _slide (prev) with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = noop - - fixtureEl.innerHTML = [ - '<div class="carousel" data-bs-interval="false">', - ' <div class="carousel-inner">', - ' <div id="item" class="carousel-item">', - ' <img alt="">', - ' </div>', - ' <div class="carousel-item active">', - ' <img alt="">', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, '_slide').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', event => { - expect(item).toHaveClass('active') - expect(carousel._slide).toHaveBeenCalledWith('right') - expect(event.direction).toEqual('right') - delete document.documentElement.ontouchstart - restorePointerEvents() - done() - }) - - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 + it('should allow swiperight and call _slide (prev) with touch events', () => { + return new Promise(resolve => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = noop + + fixtureEl.innerHTML = [ + '<div class="carousel" data-bs-interval="false">', + ' <div class="carousel-inner">', + ' <div id="item" class="carousel-item">', + ' <img alt="">', + ' </div>', + ' <div class="carousel-item active">', + ' <img alt="">', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).toHaveClass('active') + expect(carousel._slide).toHaveBeenCalledWith('right') + expect(event.direction).toEqual('right') + delete document.documentElement.ontouchstart + restorePointerEvents() + resolve() + }) + + Simulator.gestures.swipe(carouselEl, { + deltaX: 300, + deltaY: 0 + }) }) }) - it('should allow swipeleft and call _slide (next) with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = noop - - fixtureEl.innerHTML = [ - '<div class="carousel" data-bs-interval="false">', - ' <div class="carousel-inner">', - ' <div id="item" class="carousel-item active">', - ' <img alt="">', - ' </div>', - ' <div class="carousel-item">', - ' <img alt="">', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const item = fixtureEl.querySelector('#item') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, '_slide').and.callThrough() - - carouselEl.addEventListener('slid.bs.carousel', event => { - expect(item).not.toHaveClass('active') - expect(carousel._slide).toHaveBeenCalledWith('left') - expect(event.direction).toEqual('left') - delete document.documentElement.ontouchstart - restorePointerEvents() - done() - }) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 + it('should allow swipeleft and call _slide (next) with touch events', () => { + return new Promise(resolve => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = noop + + fixtureEl.innerHTML = [ + '<div class="carousel" data-bs-interval="false">', + ' <div class="carousel-inner">', + ' <div id="item" class="carousel-item active">', + ' <img alt="">', + ' </div>', + ' <div class="carousel-item">', + ' <img alt="">', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).not.toHaveClass('active') + expect(carousel._slide).toHaveBeenCalledWith('left') + expect(event.direction).toEqual('left') + delete document.documentElement.ontouchstart + restorePointerEvents() + resolve() + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) }) }) - it('should not slide when swiping and carousel is sliding', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = noop - - fixtureEl.innerHTML = [ - '<div class="carousel" data-bs-interval="false">', - ' <div class="carousel-inner">', - ' <div id="item" class="carousel-item active">', - ' <img alt="">', - ' </div>', - ' <div class="carousel-item">', - ' <img alt="">', - ' </div>', - ' </div>', - '</div>' - ].join('') + it('should not slide when swiping and carousel is sliding', () => { + return new Promise(resolve => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = noop + + fixtureEl.innerHTML = [ + '<div class="carousel" data-bs-interval="false">', + ' <div class="carousel-inner">', + ' <div id="item" class="carousel-item active">', + ' <img alt="">', + ' </div>', + ' <div class="carousel-item">', + ' <img alt="">', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) + carousel._isSliding = true + + spyOn(carousel, '_triggerSlideEvent') + + Simulator.gestures.swipe(carouselEl, { + deltaX: 300, + deltaY: 0 + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - carousel._isSliding = true - - spyOn(carousel, '_triggerSlideEvent') - - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 - }) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 + setTimeout(() => { + expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() + delete document.documentElement.ontouchstart + restorePointerEvents() + resolve() + }, 300) }) - - setTimeout(() => { - expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() - delete document.documentElement.ontouchstart - restorePointerEvents() - done() - }, 300) }) - it('should not allow pinch with touch events', done => { - Simulator.setType('touch') - clearPointerEvents() - document.documentElement.ontouchstart = noop - - fixtureEl.innerHTML = '<div class="carousel" data-bs-interval="false"></div>' - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0, - touches: 2 - }, () => { - restorePointerEvents() - delete document.documentElement.ontouchstart - expect(carousel._swipeHelper._deltaX).toEqual(0) - done() + it('should not allow pinch with touch events', () => { + return new Promise(resolve => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = noop + + fixtureEl.innerHTML = '<div class="carousel" data-bs-interval="false"></div>' + + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0, + touches: 2 + }, () => { + restorePointerEvents() + delete document.documentElement.ontouchstart + expect(carousel._swipeHelper._deltaX).toEqual(0) + resolve() + }) }) }) - it('should call pause method on mouse over with pause equal to hover', done => { - fixtureEl.innerHTML = '<div class="carousel"></div>' + it('should call pause method on mouse over with pause equal to hover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="carousel"></div>' - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) - spyOn(carousel, 'pause') + spyOn(carousel, 'pause') - const mouseOverEvent = createEvent('mouseover') - carouselEl.dispatchEvent(mouseOverEvent) + const mouseOverEvent = createEvent('mouseover') + carouselEl.dispatchEvent(mouseOverEvent) - setTimeout(() => { - expect(carousel.pause).toHaveBeenCalled() - done() - }, 10) + setTimeout(() => { + expect(carousel.pause).toHaveBeenCalled() + resolve() + }, 10) + }) }) - it('should call cycle on mouse out with pause equal to hover', done => { - fixtureEl.innerHTML = '<div class="carousel"></div>' + it('should call cycle on mouse out with pause equal to hover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="carousel"></div>' - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) - spyOn(carousel, 'cycle') + spyOn(carousel, 'cycle') - const mouseOutEvent = createEvent('mouseout') - carouselEl.dispatchEvent(mouseOutEvent) + const mouseOutEvent = createEvent('mouseout') + carouselEl.dispatchEvent(mouseOutEvent) - setTimeout(() => { - expect(carousel.cycle).toHaveBeenCalled() - done() - }, 10) + setTimeout(() => { + expect(carousel.cycle).toHaveBeenCalled() + resolve() + }, 10) + }) }) }) @@ -621,100 +647,106 @@ describe('Carousel', () => { expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() }) - it('should not fire slid when slide is prevented', done => { - fixtureEl.innerHTML = '<div></div>' + it('should not fire slid when slide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const carouselEl = fixtureEl.querySelector('div') - const carousel = new Carousel(carouselEl, {}) - let slidEvent = false + const carouselEl = fixtureEl.querySelector('div') + const carousel = new Carousel(carouselEl, {}) + let slidEvent = false - const doneTest = () => { - setTimeout(() => { - expect(slidEvent).toBeFalse() - done() - }, 20) - } + const doneTest = () => { + setTimeout(() => { + expect(slidEvent).toBeFalse() + resolve() + }, 20) + } - carouselEl.addEventListener('slide.bs.carousel', event => { - event.preventDefault() - doneTest() - }) + carouselEl.addEventListener('slide.bs.carousel', event => { + event.preventDefault() + doneTest() + }) - carouselEl.addEventListener('slid.bs.carousel', () => { - slidEvent = true - }) + carouselEl.addEventListener('slid.bs.carousel', () => { + slidEvent = true + }) - carousel.next() + carousel.next() + }) }) - it('should fire slide event with: direction, relatedTarget, from and to', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should fire slide event with: direction, relatedTarget, from and to', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) - const onSlide = event => { - expect(event.direction).toEqual('left') - expect(event.relatedTarget).toHaveClass('carousel-item') - expect(event.from).toEqual(0) - expect(event.to).toEqual(1) + const onSlide = event => { + expect(event.direction).toEqual('left') + expect(event.relatedTarget).toHaveClass('carousel-item') + expect(event.from).toEqual(0) + expect(event.to).toEqual(1) - carouselEl.removeEventListener('slide.bs.carousel', onSlide) - carouselEl.addEventListener('slide.bs.carousel', onSlide2) + carouselEl.removeEventListener('slide.bs.carousel', onSlide) + carouselEl.addEventListener('slide.bs.carousel', onSlide2) - carousel.prev() - } + carousel.prev() + } - const onSlide2 = event => { - expect(event.direction).toEqual('right') - done() - } + const onSlide2 = event => { + expect(event.direction).toEqual('right') + resolve() + } - carouselEl.addEventListener('slide.bs.carousel', onSlide) - carousel.next() + carouselEl.addEventListener('slide.bs.carousel', onSlide) + carousel.next() + }) }) - it('should fire slid event with: direction, relatedTarget, from and to', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should fire slid event with: direction, relatedTarget, from and to', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) - const onSlid = event => { - expect(event.direction).toEqual('left') - expect(event.relatedTarget).toHaveClass('carousel-item') - expect(event.from).toEqual(0) - expect(event.to).toEqual(1) + const onSlid = event => { + expect(event.direction).toEqual('left') + expect(event.relatedTarget).toHaveClass('carousel-item') + expect(event.from).toEqual(0) + expect(event.to).toEqual(1) - carouselEl.removeEventListener('slid.bs.carousel', onSlid) - carouselEl.addEventListener('slid.bs.carousel', onSlid2) + carouselEl.removeEventListener('slid.bs.carousel', onSlid) + carouselEl.addEventListener('slid.bs.carousel', onSlid2) - carousel.prev() - } + carousel.prev() + } - const onSlid2 = event => { - expect(event.direction).toEqual('right') - done() - } + const onSlid2 = event => { + expect(event.direction).toEqual('right') + resolve() + } - carouselEl.addEventListener('slid.bs.carousel', onSlid) - carousel.next() + carouselEl.addEventListener('slid.bs.carousel', onSlid) + carousel.next() + }) }) it('should update the active element to the next item before sliding', () => { @@ -737,36 +769,38 @@ describe('Carousel', () => { expect(carousel._activeElement).toEqual(secondItemEl) }) - it('should update indicators if present', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-indicators">', - ' <button type="button" id="firstIndicator" data-bs-target="myCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>', - ' <button type="button" id="secondIndicator" data-bs-target="myCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>', - ' <button type="button" data-bs-target="myCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>', - ' </div>', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item" data-bs-interval="7">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const firstIndicator = fixtureEl.querySelector('#firstIndicator') - const secondIndicator = fixtureEl.querySelector('#secondIndicator') - const carousel = new Carousel(carouselEl) - - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(firstIndicator).not.toHaveClass('active') - expect(firstIndicator.hasAttribute('aria-current')).toBeFalse() - expect(secondIndicator).toHaveClass('active') - expect(secondIndicator.getAttribute('aria-current')).toEqual('true') - done() + it('should update indicators if present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-indicators">', + ' <button type="button" id="firstIndicator" data-bs-target="myCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>', + ' <button type="button" id="secondIndicator" data-bs-target="myCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>', + ' <button type="button" data-bs-target="myCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>', + ' </div>', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-bs-interval="7">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const firstIndicator = fixtureEl.querySelector('#firstIndicator') + const secondIndicator = fixtureEl.querySelector('#secondIndicator') + const carousel = new Carousel(carouselEl) + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(firstIndicator).not.toHaveClass('active') + expect(firstIndicator.hasAttribute('aria-current')).toBeFalse() + expect(secondIndicator).toHaveClass('active') + expect(secondIndicator.getAttribute('aria-current')).toEqual('true') + resolve() + }) + + carousel.next() }) - - carousel.next() }) it('should call next()/prev() instance methods when clicking the respective direction buttons', () => { @@ -1018,51 +1052,55 @@ describe('Carousel', () => { }) describe('to', () => { - it('should go directly to the provided index', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div id="item1" class="carousel-item active">item 1</div>', - ' <div class="carousel-item">item 2</div>', - ' <div id="item3" class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should go directly to the provided index', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div id="item1" class="carousel-item active">item 1</div>', + ' <div class="carousel-item">item 2</div>', + ' <div id="item3" class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) - - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - carousel.to(2) + carousel.to(2) - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) - done() + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) + resolve() + }) }) }) - it('should return to a previous slide if the provided index is lower than the current', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item">item 1</div>', - ' <div id="item2" class="carousel-item">item 2</div>', - ' <div id="item3" class="carousel-item active">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should return to a previous slide if the provided index is lower than the current', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div id="item3" class="carousel-item active">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) - carousel.to(1) + carousel.to(1) - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - done() + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + resolve() + }) }) }) @@ -1118,36 +1156,38 @@ describe('Carousel', () => { expect(carousel.cycle).toHaveBeenCalled() }) - it('should wait before performing to if a slide is sliding', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item" data-bs-interval="7">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - '</div>' - ].join('') + it('should wait before performing to if a slide is sliding', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-bs-interval="7">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) - spyOn(EventHandler, 'one').and.callThrough() - spyOn(carousel, '_slide') + spyOn(EventHandler, 'one').and.callThrough() + spyOn(carousel, '_slide') - carousel._isSliding = true - carousel.to(1) + carousel._isSliding = true + carousel.to(1) - expect(carousel._slide).not.toHaveBeenCalled() - expect(EventHandler.one).toHaveBeenCalled() + expect(carousel._slide).not.toHaveBeenCalled() + expect(EventHandler.one).toHaveBeenCalled() - spyOn(carousel, 'to') + spyOn(carousel, 'to') - EventHandler.trigger(carouselEl, 'slid.bs.carousel') + EventHandler.trigger(carouselEl, 'slid.bs.carousel') - setTimeout(() => { - expect(carousel.to).toHaveBeenCalledWith(1) - done() + setTimeout(() => { + expect(carousel.to).toHaveBeenCalledWith(1) + resolve() + }) }) }) }) @@ -1421,75 +1461,81 @@ describe('Carousel', () => { expect(Carousel.getInstance(carouselEl)).not.toBeNull() }) - it('should create carousel and go to the next slide on click (with real button controls)', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div id="item2" class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - ' <button class="carousel-control-prev" data-bs-target="#myCarousel" type="button" data-bs-slide="prev"></button>', - ' <button id="next" class="carousel-control-next" data-bs-target="#myCarousel" type="button" data-bs-slide="next"></button>', - '</div>' - ].join('') + it('should create carousel and go to the next slide on click (with real button controls)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + ' <button class="carousel-control-prev" data-bs-target="#myCarousel" type="button" data-bs-slide="prev"></button>', + ' <button id="next" class="carousel-control-next" data-bs-target="#myCarousel" type="button" data-bs-slide="next"></button>', + '</div>' + ].join('') - const next = fixtureEl.querySelector('#next') - const item2 = fixtureEl.querySelector('#item2') + const next = fixtureEl.querySelector('#next') + const item2 = fixtureEl.querySelector('#item2') - next.click() + next.click() - setTimeout(() => { - expect(item2).toHaveClass('active') - done() - }, 10) + setTimeout(() => { + expect(item2).toHaveClass('active') + resolve() + }, 10) + }) }) - it('should create carousel and go to the next slide on click (using links as controls)', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div id="item2" class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - ' <a class="carousel-control-prev" href="#myCarousel" role="button" data-bs-slide="prev"></a>', - ' <a id="next" class="carousel-control-next" href="#myCarousel" role="button" data-bs-slide="next"></a>', - '</div>' - ].join('') + it('should create carousel and go to the next slide on click (using links as controls)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + ' <a class="carousel-control-prev" href="#myCarousel" role="button" data-bs-slide="prev"></a>', + ' <a id="next" class="carousel-control-next" href="#myCarousel" role="button" data-bs-slide="next"></a>', + '</div>' + ].join('') - const next = fixtureEl.querySelector('#next') - const item2 = fixtureEl.querySelector('#item2') + const next = fixtureEl.querySelector('#next') + const item2 = fixtureEl.querySelector('#item2') - next.click() + next.click() - setTimeout(() => { - expect(item2).toHaveClass('active') - done() - }, 10) + setTimeout(() => { + expect(item2).toHaveClass('active') + resolve() + }, 10) + }) }) - it('should create carousel and go to the next slide on click with data-bs-slide-to', done => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div id="item2" class="carousel-item">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - ' <div id="next" data-bs-target="#myCarousel" data-bs-slide-to="1"></div>', - '</div>' - ].join('') + it('should create carousel and go to the next slide on click with data-bs-slide-to', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + ' <div id="next" data-bs-target="#myCarousel" data-bs-slide-to="1"></div>', + '</div>' + ].join('') - const next = fixtureEl.querySelector('#next') - const item2 = fixtureEl.querySelector('#item2') + const next = fixtureEl.querySelector('#next') + const item2 = fixtureEl.querySelector('#item2') - next.click() + next.click() - setTimeout(() => { - expect(item2).toHaveClass('active') - done() - }, 10) + setTimeout(() => { + expect(item2).toHaveClass('active') + resolve() + }, 10) + }) }) it('should do nothing if no selector on click on arrows', () => { diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 327a68449b..e2d2864905 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -134,37 +134,39 @@ describe('Collapse', () => { expect(collapse.hide).toHaveBeenCalled() }) - it('should find collapse children if they have collapse class too not only data-bs-parent', done => { - fixtureEl.innerHTML = [ - '<div class="my-collapse">', - ' <div class="item">', - ' <a data-bs-toggle="collapse" href="#">Toggle item 1</a>', - ' <div id="collapse1" class="collapse show">Lorem ipsum 1</div>', - ' </div>', - ' <div class="item">', - ' <a id="triggerCollapse2" data-bs-toggle="collapse" href="#">Toggle item 2</a>', - ' <div id="collapse2" class="collapse">Lorem ipsum 2</div>', - ' </div>', - '</div>' - ].join('') - - const parent = fixtureEl.querySelector('.my-collapse') - const collapseEl1 = fixtureEl.querySelector('#collapse1') - const collapseEl2 = fixtureEl.querySelector('#collapse2') - - const collapseList = [].concat(...fixtureEl.querySelectorAll('.collapse')) - .map(el => new Collapse(el, { - parent, - toggle: false - })) + it('should find collapse children if they have collapse class too not only data-bs-parent', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="my-collapse">', + ' <div class="item">', + ' <a data-bs-toggle="collapse" href="#">Toggle item 1</a>', + ' <div id="collapse1" class="collapse show">Lorem ipsum 1</div>', + ' </div>', + ' <div class="item">', + ' <a id="triggerCollapse2" data-bs-toggle="collapse" href="#">Toggle item 2</a>', + ' <div id="collapse2" class="collapse">Lorem ipsum 2</div>', + ' </div>', + '</div>' + ].join('') + + const parent = fixtureEl.querySelector('.my-collapse') + const collapseEl1 = fixtureEl.querySelector('#collapse1') + const collapseEl2 = fixtureEl.querySelector('#collapse2') + + const collapseList = [].concat(...fixtureEl.querySelectorAll('.collapse')) + .map(el => new Collapse(el, { + parent, + toggle: false + })) + + collapseEl2.addEventListener('shown.bs.collapse', () => { + expect(collapseEl2).toHaveClass('show') + expect(collapseEl1).not.toHaveClass('show') + resolve() + }) - collapseEl2.addEventListener('shown.bs.collapse', () => { - expect(collapseEl2).toHaveClass('show') - expect(collapseEl1).not.toHaveClass('show') - done() + collapseList[1].toggle() }) - - collapseList[1].toggle() }) }) @@ -200,203 +202,215 @@ describe('Collapse', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should show a collapsed element', done => { - fixtureEl.innerHTML = '<div class="collapse" style="height: 0px;"></div>' + it('should show a collapsed element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="collapse" style="height: 0px;"></div>' - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) - collapseEl.addEventListener('show.bs.collapse', () => { - expect(collapseEl.style.height).toEqual('0px') - }) - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl).toHaveClass('show') - expect(collapseEl.style.height).toEqual('') - done() - }) + collapseEl.addEventListener('show.bs.collapse', () => { + expect(collapseEl.style.height).toEqual('0px') + }) + collapseEl.addEventListener('shown.bs.collapse', () => { + expect(collapseEl).toHaveClass('show') + expect(collapseEl.style.height).toEqual('') + resolve() + }) - collapse.show() + collapse.show() + }) }) - it('should show a collapsed element on width', done => { - fixtureEl.innerHTML = '<div class="collapse collapse-horizontal" style="width: 0px;"></div>' + it('should show a collapsed element on width', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="collapse collapse-horizontal" style="width: 0px;"></div>' - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) - collapseEl.addEventListener('show.bs.collapse', () => { - expect(collapseEl.style.width).toEqual('0px') - }) - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl).toHaveClass('show') - expect(collapseEl.style.width).toEqual('') - done() - }) + collapseEl.addEventListener('show.bs.collapse', () => { + expect(collapseEl.style.width).toEqual('0px') + }) + collapseEl.addEventListener('shown.bs.collapse', () => { + expect(collapseEl).toHaveClass('show') + expect(collapseEl.style.width).toEqual('') + resolve() + }) - collapse.show() + collapse.show() + }) }) - it('should collapse only the first collapse', done => { - fixtureEl.innerHTML = [ - '<div class="card" id="accordion1">', - ' <div id="collapse1" class="collapse"></div>', - '</div>', - '<div class="card" id="accordion2">', - ' <div id="collapse2" class="collapse show"></div>', - '</div>' - ].join('') + it('should collapse only the first collapse', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="card" id="accordion1">', + ' <div id="collapse1" class="collapse"></div>', + '</div>', + '<div class="card" id="accordion2">', + ' <div id="collapse2" class="collapse show"></div>', + '</div>' + ].join('') + + const el1 = fixtureEl.querySelector('#collapse1') + const el2 = fixtureEl.querySelector('#collapse2') + const collapse = new Collapse(el1, { + toggle: false + }) - const el1 = fixtureEl.querySelector('#collapse1') - const el2 = fixtureEl.querySelector('#collapse2') - const collapse = new Collapse(el1, { - toggle: false - }) + el1.addEventListener('shown.bs.collapse', () => { + expect(el1).toHaveClass('show') + expect(el2).toHaveClass('show') + resolve() + }) - el1.addEventListener('shown.bs.collapse', () => { - expect(el1).toHaveClass('show') - expect(el2).toHaveClass('show') - done() + collapse.show() }) - - collapse.show() }) - it('should be able to handle toggling of other children siblings', done => { - fixtureEl.innerHTML = [ - '<div id="parentGroup" class="accordion">', - ' <div id="parentHeader" class="accordion-header">', - ' <button data-bs-target="#parentContent" data-bs-toggle="collapse" role="button" class="accordion-toggle">Parent</button>', - ' </div>', - ' <div id="parentContent" class="accordion-collapse collapse" aria-labelledby="parentHeader" data-bs-parent="#parentGroup">', - ' <div class="accordion-body">', - ' <div id="childGroup" class="accordion">', - ' <div class="accordion-item">', - ' <div id="childHeader1" class="accordion-header">', - ' <button data-bs-target="#childContent1" data-bs-toggle="collapse" role="button" class="accordion-toggle">Child 1</button>', - ' </div>', - ' <div id="childContent1" class="accordion-collapse collapse" aria-labelledby="childHeader1" data-bs-parent="#childGroup">', - ' <div>content</div>', - ' </div>', - ' </div>', - ' <div class="accordion-item">', - ' <div id="childHeader2" class="accordion-header">', - ' <button data-bs-target="#childContent2" data-bs-toggle="collapse" role="button" class="accordion-toggle">Child 2</button>', - ' </div>', - ' <div id="childContent2" class="accordion-collapse collapse" aria-labelledby="childHeader2" data-bs-parent="#childGroup">', - ' <div>content</div>', - ' </div>', - ' </div>', - ' </div>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const el = selector => fixtureEl.querySelector(selector) - - const parentBtn = el('[data-bs-target="#parentContent"]') - const childBtn1 = el('[data-bs-target="#childContent1"]') - const childBtn2 = el('[data-bs-target="#childContent2"]') - - const parentCollapseEl = el('#parentContent') - const childCollapseEl1 = el('#childContent1') - const childCollapseEl2 = el('#childContent2') + it('should be able to handle toggling of other children siblings', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="parentGroup" class="accordion">', + ' <div id="parentHeader" class="accordion-header">', + ' <button data-bs-target="#parentContent" data-bs-toggle="collapse" role="button" class="accordion-toggle">Parent</button>', + ' </div>', + ' <div id="parentContent" class="accordion-collapse collapse" aria-labelledby="parentHeader" data-bs-parent="#parentGroup">', + ' <div class="accordion-body">', + ' <div id="childGroup" class="accordion">', + ' <div class="accordion-item">', + ' <div id="childHeader1" class="accordion-header">', + ' <button data-bs-target="#childContent1" data-bs-toggle="collapse" role="button" class="accordion-toggle">Child 1</button>', + ' </div>', + ' <div id="childContent1" class="accordion-collapse collapse" aria-labelledby="childHeader1" data-bs-parent="#childGroup">', + ' <div>content</div>', + ' </div>', + ' </div>', + ' <div class="accordion-item">', + ' <div id="childHeader2" class="accordion-header">', + ' <button data-bs-target="#childContent2" data-bs-toggle="collapse" role="button" class="accordion-toggle">Child 2</button>', + ' </div>', + ' <div id="childContent2" class="accordion-collapse collapse" aria-labelledby="childHeader2" data-bs-parent="#childGroup">', + ' <div>content</div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const el = selector => fixtureEl.querySelector(selector) + + const parentBtn = el('[data-bs-target="#parentContent"]') + const childBtn1 = el('[data-bs-target="#childContent1"]') + const childBtn2 = el('[data-bs-target="#childContent2"]') + + const parentCollapseEl = el('#parentContent') + const childCollapseEl1 = el('#childContent1') + const childCollapseEl2 = el('#childContent2') + + parentCollapseEl.addEventListener('shown.bs.collapse', () => { + expect(parentCollapseEl).toHaveClass('show') + childBtn1.click() + }) + childCollapseEl1.addEventListener('shown.bs.collapse', () => { + expect(childCollapseEl1).toHaveClass('show') + childBtn2.click() + }) + childCollapseEl2.addEventListener('shown.bs.collapse', () => { + expect(childCollapseEl2).toHaveClass('show') + expect(childCollapseEl1).not.toHaveClass('show') + resolve() + }) - parentCollapseEl.addEventListener('shown.bs.collapse', () => { - expect(parentCollapseEl).toHaveClass('show') - childBtn1.click() + parentBtn.click() }) - childCollapseEl1.addEventListener('shown.bs.collapse', () => { - expect(childCollapseEl1).toHaveClass('show') - childBtn2.click() - }) - childCollapseEl2.addEventListener('shown.bs.collapse', () => { - expect(childCollapseEl2).toHaveClass('show') - expect(childCollapseEl1).not.toHaveClass('show') - done() - }) - - parentBtn.click() }) - it('should not change tab tabpanels descendants on accordion', done => { - fixtureEl.innerHTML = [ - '<div class="accordion" id="accordionExample">', - ' <div class="accordion-item">', - ' <h2 class="accordion-header" id="headingOne">', - ' <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">', - ' Accordion Item #1', - ' </button>', - ' </h2>', - ' <div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionExample">', - ' <div class="accordion-body">', - ' <nav>', - ' <div class="nav nav-tabs" id="nav-tab" role="tablist">', - ' <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>', - ' <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>', - ' </div>', - ' </nav>', - ' <div class="tab-content" id="nav-tabContent">', - ' <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">Home</div>', - ' <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">Profile</div>', - ' </div>', - ' </div>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const el = fixtureEl.querySelector('#collapseOne') - const activeTabPane = fixtureEl.querySelector('#nav-home') - const collapse = new Collapse(el) - let times = 1 + it('should not change tab tabpanels descendants on accordion', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="accordion" id="accordionExample">', + ' <div class="accordion-item">', + ' <h2 class="accordion-header" id="headingOne">', + ' <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">', + ' Accordion Item #1', + ' </button>', + ' </h2>', + ' <div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionExample">', + ' <div class="accordion-body">', + ' <nav>', + ' <div class="nav nav-tabs" id="nav-tab" role="tablist">', + ' <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>', + ' <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>', + ' </div>', + ' </nav>', + ' <div class="tab-content" id="nav-tabContent">', + ' <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">Home</div>', + ' <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">Profile</div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const el = fixtureEl.querySelector('#collapseOne') + const activeTabPane = fixtureEl.querySelector('#nav-home') + const collapse = new Collapse(el) + let times = 1 + + el.addEventListener('hidden.bs.collapse', () => { + collapse.show() + }) - el.addEventListener('hidden.bs.collapse', () => { - collapse.show() - }) + el.addEventListener('shown.bs.collapse', () => { + expect(activeTabPane).toHaveClass('show') + times++ + if (times === 2) { + resolve() + } - el.addEventListener('shown.bs.collapse', () => { - expect(activeTabPane).toHaveClass('show') - times++ - if (times === 2) { - done() - } + collapse.hide() + }) - collapse.hide() + collapse.show() }) - - collapse.show() }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '<div class="collapse"></div>' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="collapse"></div>' - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) - const expectEnd = () => { - setTimeout(() => { - expect().nothing() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + } - collapseEl.addEventListener('show.bs.collapse', event => { - event.preventDefault() - expectEnd() - }) + collapseEl.addEventListener('show.bs.collapse', event => { + event.preventDefault() + expectEnd() + }) - collapseEl.addEventListener('shown.bs.collapse', () => { - throw new Error('should not fire shown event') - }) + collapseEl.addEventListener('shown.bs.collapse', () => { + throw new Error('should not fire shown event') + }) - collapse.show() + collapse.show() + }) }) }) @@ -432,48 +446,52 @@ describe('Collapse', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should hide a collapsed element', done => { - fixtureEl.innerHTML = '<div class="collapse show"></div>' + it('should hide a collapsed element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="collapse show"></div>' - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) - collapseEl.addEventListener('hidden.bs.collapse', () => { - expect(collapseEl).not.toHaveClass('show') - expect(collapseEl.style.height).toEqual('') - done() - }) + collapseEl.addEventListener('hidden.bs.collapse', () => { + expect(collapseEl).not.toHaveClass('show') + expect(collapseEl.style.height).toEqual('') + resolve() + }) - collapse.hide() + collapse.hide() + }) }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '<div class="collapse show"></div>' + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="collapse show"></div>' - const collapseEl = fixtureEl.querySelector('div') - const collapse = new Collapse(collapseEl, { - toggle: false - }) + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) - const expectEnd = () => { - setTimeout(() => { - expect().nothing() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + } - collapseEl.addEventListener('hide.bs.collapse', event => { - event.preventDefault() - expectEnd() - }) + collapseEl.addEventListener('hide.bs.collapse', event => { + event.preventDefault() + expectEnd() + }) - collapseEl.addEventListener('hidden.bs.collapse', () => { - throw new Error('should not fire hidden event') - }) + collapseEl.addEventListener('hidden.bs.collapse', () => { + throw new Error('should not fire hidden event') + }) - collapse.hide() + collapse.hide() + }) }) }) @@ -495,411 +513,433 @@ describe('Collapse', () => { }) describe('data-api', () => { - it('should prevent url change if click on nested elements', done => { - fixtureEl.innerHTML = [ - '<a role="button" data-bs-toggle="collapse" class="collapsed" href="#collapse">', - ' <span id="nested"></span>', - '</a>', - '<div id="collapse" class="collapse"></div>' - ].join('') - - const triggerEl = fixtureEl.querySelector('a') - const nestedTriggerEl = fixtureEl.querySelector('#nested') - - spyOn(Event.prototype, 'preventDefault').and.callThrough() + it('should prevent url change if click on nested elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a role="button" data-bs-toggle="collapse" class="collapsed" href="#collapse">', + ' <span id="nested"></span>', + '</a>', + '<div id="collapse" class="collapse"></div>' + ].join('') + + const triggerEl = fixtureEl.querySelector('a') + const nestedTriggerEl = fixtureEl.querySelector('#nested') + + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + triggerEl.addEventListener('click', event => { + expect(event.target.isEqualNode(nestedTriggerEl)).toBeTrue() + expect(event.delegateTarget.isEqualNode(triggerEl)).toBeTrue() + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - triggerEl.addEventListener('click', event => { - expect(event.target.isEqualNode(nestedTriggerEl)).toBeTrue() - expect(event.delegateTarget.isEqualNode(triggerEl)).toBeTrue() - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + nestedTriggerEl.click() }) - - nestedTriggerEl.click() }) - it('should show multiple collapsed elements', done => { - fixtureEl.innerHTML = [ - '<a role="button" data-bs-toggle="collapse" class="collapsed" href=".multi"></a>', - '<div id="collapse1" class="collapse multi"></div>', - '<div id="collapse2" class="collapse multi"></div>' - ].join('') - - const trigger = fixtureEl.querySelector('a') - const collapse1 = fixtureEl.querySelector('#collapse1') - const collapse2 = fixtureEl.querySelector('#collapse2') + it('should show multiple collapsed elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a role="button" data-bs-toggle="collapse" class="collapsed" href=".multi"></a>', + '<div id="collapse1" class="collapse multi"></div>', + '<div id="collapse2" class="collapse multi"></div>' + ].join('') + + const trigger = fixtureEl.querySelector('a') + const collapse1 = fixtureEl.querySelector('#collapse1') + const collapse2 = fixtureEl.querySelector('#collapse2') + + collapse2.addEventListener('shown.bs.collapse', () => { + expect(trigger.getAttribute('aria-expanded')).toEqual('true') + expect(trigger).not.toHaveClass('collapsed') + expect(collapse1).toHaveClass('show') + expect(collapse1).toHaveClass('show') + resolve() + }) - collapse2.addEventListener('shown.bs.collapse', () => { - expect(trigger.getAttribute('aria-expanded')).toEqual('true') - expect(trigger).not.toHaveClass('collapsed') - expect(collapse1).toHaveClass('show') - expect(collapse1).toHaveClass('show') - done() + trigger.click() }) - - trigger.click() }) - it('should hide multiple collapsed elements', done => { - fixtureEl.innerHTML = [ - '<a role="button" data-bs-toggle="collapse" href=".multi"></a>', - '<div id="collapse1" class="collapse multi show"></div>', - '<div id="collapse2" class="collapse multi show"></div>' - ].join('') - - const trigger = fixtureEl.querySelector('a') - const collapse1 = fixtureEl.querySelector('#collapse1') - const collapse2 = fixtureEl.querySelector('#collapse2') + it('should hide multiple collapsed elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a role="button" data-bs-toggle="collapse" href=".multi"></a>', + '<div id="collapse1" class="collapse multi show"></div>', + '<div id="collapse2" class="collapse multi show"></div>' + ].join('') + + const trigger = fixtureEl.querySelector('a') + const collapse1 = fixtureEl.querySelector('#collapse1') + const collapse2 = fixtureEl.querySelector('#collapse2') + + collapse2.addEventListener('hidden.bs.collapse', () => { + expect(trigger.getAttribute('aria-expanded')).toEqual('false') + expect(trigger).toHaveClass('collapsed') + expect(collapse1).not.toHaveClass('show') + expect(collapse1).not.toHaveClass('show') + resolve() + }) - collapse2.addEventListener('hidden.bs.collapse', () => { - expect(trigger.getAttribute('aria-expanded')).toEqual('false') - expect(trigger).toHaveClass('collapsed') - expect(collapse1).not.toHaveClass('show') - expect(collapse1).not.toHaveClass('show') - done() + trigger.click() }) - - trigger.click() }) - it('should remove "collapsed" class from target when collapse is shown', done => { - fixtureEl.innerHTML = [ - '<a id="link1" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>', - '<a id="link2" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>', - '<div id="test1"></div>' - ].join('') - - const link1 = fixtureEl.querySelector('#link1') - const link2 = fixtureEl.querySelector('#link2') - const collapseTest1 = fixtureEl.querySelector('#test1') + it('should remove "collapsed" class from target when collapse is shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a id="link1" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>', + '<a id="link2" role="button" data-bs-toggle="collapse" class="collapsed" href="#" data-bs-target="#test1"></a>', + '<div id="test1"></div>' + ].join('') + + const link1 = fixtureEl.querySelector('#link1') + const link2 = fixtureEl.querySelector('#link2') + const collapseTest1 = fixtureEl.querySelector('#test1') + + collapseTest1.addEventListener('shown.bs.collapse', () => { + expect(link1.getAttribute('aria-expanded')).toEqual('true') + expect(link2.getAttribute('aria-expanded')).toEqual('true') + expect(link1).not.toHaveClass('collapsed') + expect(link2).not.toHaveClass('collapsed') + resolve() + }) - collapseTest1.addEventListener('shown.bs.collapse', () => { - expect(link1.getAttribute('aria-expanded')).toEqual('true') - expect(link2.getAttribute('aria-expanded')).toEqual('true') - expect(link1).not.toHaveClass('collapsed') - expect(link2).not.toHaveClass('collapsed') - done() + link1.click() }) - - link1.click() }) - it('should add "collapsed" class to target when collapse is hidden', done => { - fixtureEl.innerHTML = [ - '<a id="link1" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>', - '<a id="link2" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>', - '<div id="test1" class="show"></div>' - ].join('') - - const link1 = fixtureEl.querySelector('#link1') - const link2 = fixtureEl.querySelector('#link2') - const collapseTest1 = fixtureEl.querySelector('#test1') + it('should add "collapsed" class to target when collapse is hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a id="link1" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>', + '<a id="link2" role="button" data-bs-toggle="collapse" href="#" data-bs-target="#test1"></a>', + '<div id="test1" class="show"></div>' + ].join('') + + const link1 = fixtureEl.querySelector('#link1') + const link2 = fixtureEl.querySelector('#link2') + const collapseTest1 = fixtureEl.querySelector('#test1') + + collapseTest1.addEventListener('hidden.bs.collapse', () => { + expect(link1.getAttribute('aria-expanded')).toEqual('false') + expect(link2.getAttribute('aria-expanded')).toEqual('false') + expect(link1).toHaveClass('collapsed') + expect(link2).toHaveClass('collapsed') + resolve() + }) - collapseTest1.addEventListener('hidden.bs.collapse', () => { - expect(link1.getAttribute('aria-expanded')).toEqual('false') - expect(link2.getAttribute('aria-expanded')).toEqual('false') - expect(link1).toHaveClass('collapsed') - expect(link2).toHaveClass('collapsed') - done() + link1.click() }) - - link1.click() }) - it('should allow accordion to use children other than card', done => { - fixtureEl.innerHTML = [ - '<div id="accordion">', - ' <div class="item">', - ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', - ' <div id="collapseOne" class="collapse" role="tabpanel" aria-labelledby="headingThree" data-bs-parent="#accordion"></div>', - ' </div>', - ' <div class="item">', - ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', - ' <div id="collapseTwo" class="collapse show" role="tabpanel" aria-labelledby="headingTwo" data-bs-parent="#accordion"></div>', - ' </div>', - '</div>' - ].join('') - - const trigger = fixtureEl.querySelector('#linkTrigger') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOne = fixtureEl.querySelector('#collapseOne') - const collapseTwo = fixtureEl.querySelector('#collapseTwo') - - collapseOne.addEventListener('shown.bs.collapse', () => { - expect(collapseOne).toHaveClass('show') - expect(collapseTwo).not.toHaveClass('show') + it('should allow accordion to use children other than card', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="accordion">', + ' <div class="item">', + ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', + ' <div id="collapseOne" class="collapse" role="tabpanel" aria-labelledby="headingThree" data-bs-parent="#accordion"></div>', + ' </div>', + ' <div class="item">', + ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', + ' <div id="collapseTwo" class="collapse show" role="tabpanel" aria-labelledby="headingTwo" data-bs-parent="#accordion"></div>', + ' </div>', + '</div>' + ].join('') + + const trigger = fixtureEl.querySelector('#linkTrigger') + const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') + const collapseOne = fixtureEl.querySelector('#collapseOne') + const collapseTwo = fixtureEl.querySelector('#collapseTwo') + + collapseOne.addEventListener('shown.bs.collapse', () => { + expect(collapseOne).toHaveClass('show') + expect(collapseTwo).not.toHaveClass('show') + + collapseTwo.addEventListener('shown.bs.collapse', () => { + expect(collapseOne).not.toHaveClass('show') + expect(collapseTwo).toHaveClass('show') + resolve() + }) - collapseTwo.addEventListener('shown.bs.collapse', () => { - expect(collapseOne).not.toHaveClass('show') - expect(collapseTwo).toHaveClass('show') - done() + triggerTwo.click() }) - triggerTwo.click() + trigger.click() }) - - trigger.click() }) - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '<input type="checkbox" data-bs-toggle="collapse" data-bs-target="#collapsediv1">', - '<div id="collapsediv1"></div>' - ].join('') + it('should not prevent event for input', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<input type="checkbox" data-bs-toggle="collapse" data-bs-target="#collapsediv1">', + '<div id="collapsediv1"></div>' + ].join('') - const target = fixtureEl.querySelector('input') - const collapseEl = fixtureEl.querySelector('#collapsediv1') + const target = fixtureEl.querySelector('input') + const collapseEl = fixtureEl.querySelector('#collapsediv1') - collapseEl.addEventListener('shown.bs.collapse', () => { - expect(collapseEl).toHaveClass('show') - expect(target.checked).toBeTrue() - done() - }) + collapseEl.addEventListener('shown.bs.collapse', () => { + expect(collapseEl).toHaveClass('show') + expect(target.checked).toBeTrue() + resolve() + }) - target.click() + target.click() + }) }) - it('should allow accordion to contain nested elements', done => { - fixtureEl.innerHTML = [ - '<div id="accordion">', - ' <div class="row">', - ' <div class="col-lg-6">', - ' <div class="item">', - ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', - ' <div id="collapseOne" class="collapse" role="tabpanel" aria-labelledby="headingThree" data-bs-parent="#accordion"></div>', - ' </div>', - ' </div>', - ' <div class="col-lg-6">', - ' <div class="item">', - ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', - ' <div id="collapseTwo" class="collapse show" role="tabpanel" aria-labelledby="headingTwo" data-bs-parent="#accordion"></div>', - ' </div>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const triggerEl = fixtureEl.querySelector('#linkTrigger') - const triggerTwoEl = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOneEl = fixtureEl.querySelector('#collapseOne') - const collapseTwoEl = fixtureEl.querySelector('#collapseTwo') - - collapseOneEl.addEventListener('shown.bs.collapse', () => { - expect(collapseOneEl).toHaveClass('show') - expect(triggerEl).not.toHaveClass('collapsed') - expect(triggerEl.getAttribute('aria-expanded')).toEqual('true') - - expect(collapseTwoEl).not.toHaveClass('show') - expect(triggerTwoEl).toHaveClass('collapsed') - expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false') - - collapseTwoEl.addEventListener('shown.bs.collapse', () => { - expect(collapseOneEl).not.toHaveClass('show') - expect(triggerEl).toHaveClass('collapsed') - expect(triggerEl.getAttribute('aria-expanded')).toEqual('false') + it('should allow accordion to contain nested elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="accordion">', + ' <div class="row">', + ' <div class="col-lg-6">', + ' <div class="item">', + ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', + ' <div id="collapseOne" class="collapse" role="tabpanel" aria-labelledby="headingThree" data-bs-parent="#accordion"></div>', + ' </div>', + ' </div>', + ' <div class="col-lg-6">', + ' <div class="item">', + ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', + ' <div id="collapseTwo" class="collapse show" role="tabpanel" aria-labelledby="headingTwo" data-bs-parent="#accordion"></div>', + ' </div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const triggerEl = fixtureEl.querySelector('#linkTrigger') + const triggerTwoEl = fixtureEl.querySelector('#linkTriggerTwo') + const collapseOneEl = fixtureEl.querySelector('#collapseOne') + const collapseTwoEl = fixtureEl.querySelector('#collapseTwo') + + collapseOneEl.addEventListener('shown.bs.collapse', () => { + expect(collapseOneEl).toHaveClass('show') + expect(triggerEl).not.toHaveClass('collapsed') + expect(triggerEl.getAttribute('aria-expanded')).toEqual('true') + + expect(collapseTwoEl).not.toHaveClass('show') + expect(triggerTwoEl).toHaveClass('collapsed') + expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false') + + collapseTwoEl.addEventListener('shown.bs.collapse', () => { + expect(collapseOneEl).not.toHaveClass('show') + expect(triggerEl).toHaveClass('collapsed') + expect(triggerEl.getAttribute('aria-expanded')).toEqual('false') + + expect(collapseTwoEl).toHaveClass('show') + expect(triggerTwoEl).not.toHaveClass('collapsed') + expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - expect(collapseTwoEl).toHaveClass('show') - expect(triggerTwoEl).not.toHaveClass('collapsed') - expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true') - done() + triggerTwoEl.click() }) - triggerTwoEl.click() + triggerEl.click() }) - - triggerEl.click() }) - it('should allow accordion to target multiple elements', done => { - fixtureEl.innerHTML = [ - '<div id="accordion">', - ' <a id="linkTriggerOne" data-bs-toggle="collapse" data-bs-target=".collapseOne" href="#" aria-expanded="false" aria-controls="collapseOne"></a>', - ' <a id="linkTriggerTwo" data-bs-toggle="collapse" data-bs-target=".collapseTwo" href="#" aria-expanded="false" aria-controls="collapseTwo"></a>', - ' <div id="collapseOneOne" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>', - ' <div id="collapseOneTwo" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>', - ' <div id="collapseTwoOne" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>', - ' <div id="collapseTwoTwo" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>', - '</div>' - ].join('') - - const trigger = fixtureEl.querySelector('#linkTriggerOne') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const collapseOneOne = fixtureEl.querySelector('#collapseOneOne') - const collapseOneTwo = fixtureEl.querySelector('#collapseOneTwo') - const collapseTwoOne = fixtureEl.querySelector('#collapseTwoOne') - const collapseTwoTwo = fixtureEl.querySelector('#collapseTwoTwo') - const collapsedElements = { - one: false, - two: false - } - - function firstTest() { - expect(collapseOneOne).toHaveClass('show') - expect(collapseOneTwo).toHaveClass('show') - - expect(collapseTwoOne).not.toHaveClass('show') - expect(collapseTwoTwo).not.toHaveClass('show') - - triggerTwo.click() - } - - function secondTest() { - expect(collapseOneOne).not.toHaveClass('show') - expect(collapseOneTwo).not.toHaveClass('show') - - expect(collapseTwoOne).toHaveClass('show') - expect(collapseTwoTwo).toHaveClass('show') - done() - } - - collapseOneOne.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.one) { - firstTest() - } else { - collapsedElements.one = true + it('should allow accordion to target multiple elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="accordion">', + ' <a id="linkTriggerOne" data-bs-toggle="collapse" data-bs-target=".collapseOne" href="#" aria-expanded="false" aria-controls="collapseOne"></a>', + ' <a id="linkTriggerTwo" data-bs-toggle="collapse" data-bs-target=".collapseTwo" href="#" aria-expanded="false" aria-controls="collapseTwo"></a>', + ' <div id="collapseOneOne" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>', + ' <div id="collapseOneTwo" class="collapse collapseOne" role="tabpanel" data-bs-parent="#accordion"></div>', + ' <div id="collapseTwoOne" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>', + ' <div id="collapseTwoTwo" class="collapse collapseTwo" role="tabpanel" data-bs-parent="#accordion"></div>', + '</div>' + ].join('') + + const trigger = fixtureEl.querySelector('#linkTriggerOne') + const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') + const collapseOneOne = fixtureEl.querySelector('#collapseOneOne') + const collapseOneTwo = fixtureEl.querySelector('#collapseOneTwo') + const collapseTwoOne = fixtureEl.querySelector('#collapseTwoOne') + const collapseTwoTwo = fixtureEl.querySelector('#collapseTwoTwo') + const collapsedElements = { + one: false, + two: false } - }) - collapseOneTwo.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.one) { - firstTest() - } else { - collapsedElements.one = true - } - }) + function firstTest() { + expect(collapseOneOne).toHaveClass('show') + expect(collapseOneTwo).toHaveClass('show') - collapseTwoOne.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.two) { - secondTest() - } else { - collapsedElements.two = true - } - }) + expect(collapseTwoOne).not.toHaveClass('show') + expect(collapseTwoTwo).not.toHaveClass('show') - collapseTwoTwo.addEventListener('shown.bs.collapse', () => { - if (collapsedElements.two) { - secondTest() - } else { - collapsedElements.two = true + triggerTwo.click() } - }) - trigger.click() - }) + function secondTest() { + expect(collapseOneOne).not.toHaveClass('show') + expect(collapseOneTwo).not.toHaveClass('show') - it('should collapse accordion children but not nested accordion children', done => { - fixtureEl.innerHTML = [ - '<div id="accordion">', - ' <div class="item">', - ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', - ' <div id="collapseOne" data-bs-parent="#accordion" class="collapse" role="tabpanel" aria-labelledby="headingThree">', - ' <div id="nestedAccordion">', - ' <div class="item">', - ' <a id="nestedLinkTrigger" data-bs-toggle="collapse" href="#nestedCollapseOne" aria-expanded="false" aria-controls="nestedCollapseOne"></a>', - ' <div id="nestedCollapseOne" data-bs-parent="#nestedAccordion" class="collapse" role="tabpanel" aria-labelledby="headingThree"></div>', - ' </div>', - ' </div>', - ' </div>', - ' </div>', - ' <div class="item">', - ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', - ' <div id="collapseTwo" data-bs-parent="#accordion" class="collapse show" role="tabpanel" aria-labelledby="headingTwo"></div>', - ' </div>', - '</div>' - ].join('') + expect(collapseTwoOne).toHaveClass('show') + expect(collapseTwoTwo).toHaveClass('show') + resolve() + } - const trigger = fixtureEl.querySelector('#linkTrigger') - const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') - const nestedTrigger = fixtureEl.querySelector('#nestedLinkTrigger') - const collapseOne = fixtureEl.querySelector('#collapseOne') - const collapseTwo = fixtureEl.querySelector('#collapseTwo') - const nestedCollapseOne = fixtureEl.querySelector('#nestedCollapseOne') - - function handlerCollapseOne() { - expect(collapseOne).toHaveClass('show') - expect(collapseTwo).not.toHaveClass('show') - expect(nestedCollapseOne).not.toHaveClass('show') - - nestedCollapseOne.addEventListener('shown.bs.collapse', handlerNestedCollapseOne) - nestedTrigger.click() - collapseOne.removeEventListener('shown.bs.collapse', handlerCollapseOne) - } + collapseOneOne.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.one) { + firstTest() + } else { + collapsedElements.one = true + } + }) - function handlerNestedCollapseOne() { - expect(collapseOne).toHaveClass('show') - expect(collapseTwo).not.toHaveClass('show') - expect(nestedCollapseOne).toHaveClass('show') + collapseOneTwo.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.one) { + firstTest() + } else { + collapsedElements.one = true + } + }) - collapseTwo.addEventListener('shown.bs.collapse', () => { - expect(collapseOne).not.toHaveClass('show') - expect(collapseTwo).toHaveClass('show') - expect(nestedCollapseOne).toHaveClass('show') - done() + collapseTwoOne.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.two) { + secondTest() + } else { + collapsedElements.two = true + } }) - triggerTwo.click() - nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) - } + collapseTwoTwo.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.two) { + secondTest() + } else { + collapsedElements.two = true + } + }) - collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) - trigger.click() + trigger.click() + }) }) - it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', done => { - fixtureEl.innerHTML = [ - '<a id="trigger1" role="button" data-bs-toggle="collapse" href="#test1"></a>', - '<a id="trigger2" role="button" data-bs-toggle="collapse" href="#test2"></a>', - '<a id="trigger3" role="button" data-bs-toggle="collapse" href=".multi"></a>', - '<div id="test1" class="multi"></div>', - '<div id="test2" class="multi"></div>' - ].join('') + it('should collapse accordion children but not nested accordion children', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="accordion">', + ' <div class="item">', + ' <a id="linkTrigger" data-bs-toggle="collapse" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"></a>', + ' <div id="collapseOne" data-bs-parent="#accordion" class="collapse" role="tabpanel" aria-labelledby="headingThree">', + ' <div id="nestedAccordion">', + ' <div class="item">', + ' <a id="nestedLinkTrigger" data-bs-toggle="collapse" href="#nestedCollapseOne" aria-expanded="false" aria-controls="nestedCollapseOne"></a>', + ' <div id="nestedCollapseOne" data-bs-parent="#nestedAccordion" class="collapse" role="tabpanel" aria-labelledby="headingThree"></div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>', + ' <div class="item">', + ' <a id="linkTriggerTwo" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"></a>', + ' <div id="collapseTwo" data-bs-parent="#accordion" class="collapse show" role="tabpanel" aria-labelledby="headingTwo"></div>', + ' </div>', + '</div>' + ].join('') + + const trigger = fixtureEl.querySelector('#linkTrigger') + const triggerTwo = fixtureEl.querySelector('#linkTriggerTwo') + const nestedTrigger = fixtureEl.querySelector('#nestedLinkTrigger') + const collapseOne = fixtureEl.querySelector('#collapseOne') + const collapseTwo = fixtureEl.querySelector('#collapseTwo') + const nestedCollapseOne = fixtureEl.querySelector('#nestedCollapseOne') + + function handlerCollapseOne() { + expect(collapseOne).toHaveClass('show') + expect(collapseTwo).not.toHaveClass('show') + expect(nestedCollapseOne).not.toHaveClass('show') + + nestedCollapseOne.addEventListener('shown.bs.collapse', handlerNestedCollapseOne) + nestedTrigger.click() + collapseOne.removeEventListener('shown.bs.collapse', handlerCollapseOne) + } - const trigger1 = fixtureEl.querySelector('#trigger1') - const trigger2 = fixtureEl.querySelector('#trigger2') - const trigger3 = fixtureEl.querySelector('#trigger3') - const target1 = fixtureEl.querySelector('#test1') - const target2 = fixtureEl.querySelector('#test2') + function handlerNestedCollapseOne() { + expect(collapseOne).toHaveClass('show') + expect(collapseTwo).not.toHaveClass('show') + expect(nestedCollapseOne).toHaveClass('show') - const target2Shown = () => { - expect(trigger1).not.toHaveClass('collapsed') - expect(trigger1.getAttribute('aria-expanded')).toEqual('true') + collapseTwo.addEventListener('shown.bs.collapse', () => { + expect(collapseOne).not.toHaveClass('show') + expect(collapseTwo).toHaveClass('show') + expect(nestedCollapseOne).toHaveClass('show') + resolve() + }) - expect(trigger2).not.toHaveClass('collapsed') - expect(trigger2.getAttribute('aria-expanded')).toEqual('true') + triggerTwo.click() + nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) + } - expect(trigger3).not.toHaveClass('collapsed') - expect(trigger3.getAttribute('aria-expanded')).toEqual('true') + collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) + trigger.click() + }) + }) - target2.addEventListener('hidden.bs.collapse', () => { + it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a id="trigger1" role="button" data-bs-toggle="collapse" href="#test1"></a>', + '<a id="trigger2" role="button" data-bs-toggle="collapse" href="#test2"></a>', + '<a id="trigger3" role="button" data-bs-toggle="collapse" href=".multi"></a>', + '<div id="test1" class="multi"></div>', + '<div id="test2" class="multi"></div>' + ].join('') + + const trigger1 = fixtureEl.querySelector('#trigger1') + const trigger2 = fixtureEl.querySelector('#trigger2') + const trigger3 = fixtureEl.querySelector('#trigger3') + const target1 = fixtureEl.querySelector('#test1') + const target2 = fixtureEl.querySelector('#test2') + + const target2Shown = () => { expect(trigger1).not.toHaveClass('collapsed') expect(trigger1.getAttribute('aria-expanded')).toEqual('true') - expect(trigger2).toHaveClass('collapsed') - expect(trigger2.getAttribute('aria-expanded')).toEqual('false') + expect(trigger2).not.toHaveClass('collapsed') + expect(trigger2.getAttribute('aria-expanded')).toEqual('true') expect(trigger3).not.toHaveClass('collapsed') expect(trigger3.getAttribute('aria-expanded')).toEqual('true') - target1.addEventListener('hidden.bs.collapse', () => { - expect(trigger1).toHaveClass('collapsed') - expect(trigger1.getAttribute('aria-expanded')).toEqual('false') + target2.addEventListener('hidden.bs.collapse', () => { + expect(trigger1).not.toHaveClass('collapsed') + expect(trigger1.getAttribute('aria-expanded')).toEqual('true') expect(trigger2).toHaveClass('collapsed') expect(trigger2.getAttribute('aria-expanded')).toEqual('false') - expect(trigger3).toHaveClass('collapsed') - expect(trigger3.getAttribute('aria-expanded')).toEqual('false') - done() - }) + expect(trigger3).not.toHaveClass('collapsed') + expect(trigger3.getAttribute('aria-expanded')).toEqual('true') - trigger1.click() - }) + target1.addEventListener('hidden.bs.collapse', () => { + expect(trigger1).toHaveClass('collapsed') + expect(trigger1.getAttribute('aria-expanded')).toEqual('false') - trigger2.click() - } + expect(trigger2).toHaveClass('collapsed') + expect(trigger2.getAttribute('aria-expanded')).toEqual('false') + + expect(trigger3).toHaveClass('collapsed') + expect(trigger3.getAttribute('aria-expanded')).toEqual('false') + resolve() + }) + + trigger1.click() + }) - target2.addEventListener('shown.bs.collapse', target2Shown) - trigger3.click() + trigger2.click() + } + + target2.addEventListener('shown.bs.collapse', target2Shown) + trigger3.click() + }) }) }) diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js index 601af7409e..e5887ce708 100644 --- a/js/tests/unit/dom/event-handler.spec.js +++ b/js/tests/unit/dom/event-handler.spec.js @@ -1,5 +1,6 @@ import EventHandler from '../../../src/dom/event-handler' -import { getFixture, clearFixture } from '../../helpers/fixture' +import { clearFixture, getFixture } from '../../helpers/fixture' +import { noop } from '../../../src/util' describe('EventHandler', () => { let fixtureEl @@ -18,176 +19,190 @@ describe('EventHandler', () => { const div = fixtureEl.querySelector('div') - EventHandler.on(div, null, () => {}) - EventHandler.on(null, 'click', () => {}) + EventHandler.on(div, null, noop) + EventHandler.on(null, 'click', noop) expect().nothing() }) - it('should add event listener', done => { - fixtureEl.innerHTML = '<div></div>' + it('should add event listener', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('div') - EventHandler.on(div, 'click', () => { - expect().nothing() - done() - }) + EventHandler.on(div, 'click', () => { + expect().nothing() + resolve() + }) - div.click() + div.click() + }) }) - it('should add namespaced event listener', done => { - fixtureEl.innerHTML = '<div></div>' + it('should add namespaced event listener', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('div') - EventHandler.on(div, 'bs.namespace', () => { - expect().nothing() - done() - }) + EventHandler.on(div, 'bs.namespace', () => { + expect().nothing() + resolve() + }) - EventHandler.trigger(div, 'bs.namespace') + EventHandler.trigger(div, 'bs.namespace') + }) }) - it('should add native namespaced event listener', done => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') + it('should add native namespaced event listener', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - EventHandler.on(div, 'click.namespace', () => { - expect().nothing() - done() - }) + const div = fixtureEl.querySelector('div') - EventHandler.trigger(div, 'click') - }) + EventHandler.on(div, 'click.namespace', () => { + expect().nothing() + resolve() + }) - it('should handle event delegation', done => { - EventHandler.on(document, 'click', '.test', () => { - expect().nothing() - done() + EventHandler.trigger(div, 'click') }) + }) - fixtureEl.innerHTML = '<div class="test"></div>' + it('should handle event delegation', () => { + return new Promise(resolve => { + EventHandler.on(document, 'click', '.test', () => { + expect().nothing() + resolve() + }) - const div = fixtureEl.querySelector('div') + fixtureEl.innerHTML = '<div class="test"></div>' - div.click() - }) + const div = fixtureEl.querySelector('div') - it('should handle mouseenter/mouseleave like the native counterpart', done => { - fixtureEl.innerHTML = [ - '<div class="outer">', - '<div class="inner">', - '<div class="nested">', - '<div class="deep"></div>', - '</div>', - '</div>', - '<div class="sibling"></div>', - '</div>' - ].join('') - - const outer = fixtureEl.querySelector('.outer') - const inner = fixtureEl.querySelector('.inner') - const nested = fixtureEl.querySelector('.nested') - const deep = fixtureEl.querySelector('.deep') - const sibling = fixtureEl.querySelector('.sibling') - - const enterSpy = jasmine.createSpy('mouseenter') - const leaveSpy = jasmine.createSpy('mouseleave') - const delegateEnterSpy = jasmine.createSpy('mouseenter') - const delegateLeaveSpy = jasmine.createSpy('mouseleave') - - EventHandler.on(inner, 'mouseenter', enterSpy) - EventHandler.on(inner, 'mouseleave', leaveSpy) - EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy) - EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy) - - EventHandler.on(sibling, 'mouseenter', () => { - expect(enterSpy.calls.count()).toEqual(2) - expect(leaveSpy.calls.count()).toEqual(2) - expect(delegateEnterSpy.calls.count()).toEqual(2) - expect(delegateLeaveSpy.calls.count()).toEqual(2) - done() + div.click() }) + }) - const moveMouse = (from, to) => { - from.dispatchEvent(new MouseEvent('mouseout', { - bubbles: true, - relatedTarget: to - })) - - to.dispatchEvent(new MouseEvent('mouseover', { - bubbles: true, - relatedTarget: from - })) - } + it('should handle mouseenter/mouseleave like the native counterpart', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="outer">', + '<div class="inner">', + '<div class="nested">', + '<div class="deep"></div>', + '</div>', + '</div>', + '<div class="sibling"></div>', + '</div>' + ].join('') + + const outer = fixtureEl.querySelector('.outer') + const inner = fixtureEl.querySelector('.inner') + const nested = fixtureEl.querySelector('.nested') + const deep = fixtureEl.querySelector('.deep') + const sibling = fixtureEl.querySelector('.sibling') + + const enterSpy = jasmine.createSpy('mouseenter') + const leaveSpy = jasmine.createSpy('mouseleave') + const delegateEnterSpy = jasmine.createSpy('mouseenter') + const delegateLeaveSpy = jasmine.createSpy('mouseleave') + + EventHandler.on(inner, 'mouseenter', enterSpy) + EventHandler.on(inner, 'mouseleave', leaveSpy) + EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy) + EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy) + + EventHandler.on(sibling, 'mouseenter', () => { + expect(enterSpy.calls.count()).toEqual(2) + expect(leaveSpy.calls.count()).toEqual(2) + expect(delegateEnterSpy.calls.count()).toEqual(2) + expect(delegateLeaveSpy.calls.count()).toEqual(2) + resolve() + }) + + const moveMouse = (from, to) => { + from.dispatchEvent(new MouseEvent('mouseout', { + bubbles: true, + relatedTarget: to + })) + + to.dispatchEvent(new MouseEvent('mouseover', { + bubbles: true, + relatedTarget: from + })) + } - // from outer to deep and back to outer (nested) - moveMouse(outer, inner) - moveMouse(inner, nested) - moveMouse(nested, deep) - moveMouse(deep, nested) - moveMouse(nested, inner) - moveMouse(inner, outer) - - setTimeout(() => { - expect(enterSpy.calls.count()).toEqual(1) - expect(leaveSpy.calls.count()).toEqual(1) - expect(delegateEnterSpy.calls.count()).toEqual(1) - expect(delegateLeaveSpy.calls.count()).toEqual(1) - - // from outer to inner to sibling (adjacent) + // from outer to deep and back to outer (nested) moveMouse(outer, inner) - moveMouse(inner, sibling) - }, 20) + moveMouse(inner, nested) + moveMouse(nested, deep) + moveMouse(deep, nested) + moveMouse(nested, inner) + moveMouse(inner, outer) + + setTimeout(() => { + expect(enterSpy.calls.count()).toEqual(1) + expect(leaveSpy.calls.count()).toEqual(1) + expect(delegateEnterSpy.calls.count()).toEqual(1) + expect(delegateLeaveSpy.calls.count()).toEqual(1) + + // from outer to inner to sibling (adjacent) + moveMouse(outer, inner) + moveMouse(inner, sibling) + }, 20) + }) }) }) describe('one', () => { - it('should call listener just once', done => { - fixtureEl.innerHTML = '<div></div>' - - let called = 0 - const div = fixtureEl.querySelector('div') - const obj = { - oneListener() { - called++ + it('should call listener just once', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + + let called = 0 + const div = fixtureEl.querySelector('div') + const obj = { + oneListener() { + called++ + } } - } - EventHandler.one(div, 'bootstrap', obj.oneListener) + EventHandler.one(div, 'bootstrap', obj.oneListener) - EventHandler.trigger(div, 'bootstrap') - EventHandler.trigger(div, 'bootstrap') + EventHandler.trigger(div, 'bootstrap') + EventHandler.trigger(div, 'bootstrap') - setTimeout(() => { - expect(called).toEqual(1) - done() - }, 20) + setTimeout(() => { + expect(called).toEqual(1) + resolve() + }, 20) + }) }) - it('should call delegated listener just once', done => { - fixtureEl.innerHTML = '<div></div>' + it('should call delegated listener just once', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - let called = 0 - const div = fixtureEl.querySelector('div') - const obj = { - oneListener() { - called++ + let called = 0 + const div = fixtureEl.querySelector('div') + const obj = { + oneListener() { + called++ + } } - } - EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener) + EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener) - EventHandler.trigger(div, 'bootstrap') - EventHandler.trigger(div, 'bootstrap') + EventHandler.trigger(div, 'bootstrap') + EventHandler.trigger(div, 'bootstrap') - setTimeout(() => { - expect(called).toEqual(1) - done() - }, 20) + setTimeout(() => { + expect(called).toEqual(1) + resolve() + }, 20) + }) }) }) @@ -196,171 +211,185 @@ describe('EventHandler', () => { fixtureEl.innerHTML = '<div></div>' const div = fixtureEl.querySelector('div') - EventHandler.off(div, null, () => {}) - EventHandler.off(null, 'click', () => {}) + EventHandler.off(div, null, noop) + EventHandler.off(null, 'click', noop) expect().nothing() }) - it('should remove a listener', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + it('should remove a listener', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') - let called = 0 - const handler = () => { - called++ - } + let called = 0 + const handler = () => { + called++ + } - EventHandler.on(div, 'foobar', handler) - EventHandler.trigger(div, 'foobar') + EventHandler.on(div, 'foobar', handler) + EventHandler.trigger(div, 'foobar') - EventHandler.off(div, 'foobar', handler) - EventHandler.trigger(div, 'foobar') + EventHandler.off(div, 'foobar', handler) + EventHandler.trigger(div, 'foobar') - setTimeout(() => { - expect(called).toEqual(1) - done() - }, 20) + setTimeout(() => { + expect(called).toEqual(1) + resolve() + }, 20) + }) }) - it('should remove all the events', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + it('should remove all the events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') - let called = 0 + let called = 0 - EventHandler.on(div, 'foobar', () => { - called++ - }) - EventHandler.on(div, 'foobar', () => { - called++ - }) - EventHandler.trigger(div, 'foobar') + EventHandler.on(div, 'foobar', () => { + called++ + }) + EventHandler.on(div, 'foobar', () => { + called++ + }) + EventHandler.trigger(div, 'foobar') - EventHandler.off(div, 'foobar') - EventHandler.trigger(div, 'foobar') + EventHandler.off(div, 'foobar') + EventHandler.trigger(div, 'foobar') - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) + setTimeout(() => { + expect(called).toEqual(2) + resolve() + }, 20) + }) }) - it('should remove all the namespaced listeners if namespace is passed', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + it('should remove all the namespaced listeners if namespace is passed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') - let called = 0 + let called = 0 - EventHandler.on(div, 'foobar.namespace', () => { - called++ - }) - EventHandler.on(div, 'foofoo.namespace', () => { - called++ + EventHandler.on(div, 'foobar.namespace', () => { + called++ + }) + EventHandler.on(div, 'foofoo.namespace', () => { + called++ + }) + EventHandler.trigger(div, 'foobar.namespace') + EventHandler.trigger(div, 'foofoo.namespace') + + EventHandler.off(div, '.namespace') + EventHandler.trigger(div, 'foobar.namespace') + EventHandler.trigger(div, 'foofoo.namespace') + + setTimeout(() => { + expect(called).toEqual(2) + resolve() + }, 20) }) - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.trigger(div, 'foofoo.namespace') - - EventHandler.off(div, '.namespace') - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.trigger(div, 'foofoo.namespace') - - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) }) - it('should remove the namespaced listeners', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + it('should remove the namespaced listeners', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') - let calledCallback1 = 0 - let calledCallback2 = 0 + let calledCallback1 = 0 + let calledCallback2 = 0 - EventHandler.on(div, 'foobar.namespace', () => { - calledCallback1++ - }) - EventHandler.on(div, 'foofoo.namespace', () => { - calledCallback2++ - }) + EventHandler.on(div, 'foobar.namespace', () => { + calledCallback1++ + }) + EventHandler.on(div, 'foofoo.namespace', () => { + calledCallback2++ + }) - EventHandler.trigger(div, 'foobar.namespace') - EventHandler.off(div, 'foobar.namespace') - EventHandler.trigger(div, 'foobar.namespace') + EventHandler.trigger(div, 'foobar.namespace') + EventHandler.off(div, 'foobar.namespace') + EventHandler.trigger(div, 'foobar.namespace') - EventHandler.trigger(div, 'foofoo.namespace') + EventHandler.trigger(div, 'foofoo.namespace') - setTimeout(() => { - expect(calledCallback1).toEqual(1) - expect(calledCallback2).toEqual(1) - done() - }, 20) + setTimeout(() => { + expect(calledCallback1).toEqual(1) + expect(calledCallback2).toEqual(1) + resolve() + }, 20) + }) }) - it('should remove the all the namespaced listeners for native events', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + it('should remove the all the namespaced listeners for native events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') - let called = 0 + let called = 0 - EventHandler.on(div, 'click.namespace', () => { - called++ - }) - EventHandler.on(div, 'click.namespace2', () => { - called++ - }) + EventHandler.on(div, 'click.namespace', () => { + called++ + }) + EventHandler.on(div, 'click.namespace2', () => { + called++ + }) - EventHandler.trigger(div, 'click') - EventHandler.off(div, 'click') - EventHandler.trigger(div, 'click') + EventHandler.trigger(div, 'click') + EventHandler.off(div, 'click') + EventHandler.trigger(div, 'click') - setTimeout(() => { - expect(called).toEqual(2) - done() - }, 20) + setTimeout(() => { + expect(called).toEqual(2) + resolve() + }, 20) + }) }) - it('should remove the specified namespaced listeners for native events', done => { - fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') - - let called1 = 0 - let called2 = 0 - - EventHandler.on(div, 'click.namespace', () => { - called1++ + it('should remove the specified namespaced listeners for native events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + const div = fixtureEl.querySelector('div') + + let called1 = 0 + let called2 = 0 + + EventHandler.on(div, 'click.namespace', () => { + called1++ + }) + EventHandler.on(div, 'click.namespace2', () => { + called2++ + }) + EventHandler.trigger(div, 'click') + + EventHandler.off(div, 'click.namespace') + EventHandler.trigger(div, 'click') + + setTimeout(() => { + expect(called1).toEqual(1) + expect(called2).toEqual(2) + resolve() + }, 20) }) - EventHandler.on(div, 'click.namespace2', () => { - called2++ - }) - EventHandler.trigger(div, 'click') - - EventHandler.off(div, 'click.namespace') - EventHandler.trigger(div, 'click') - - setTimeout(() => { - expect(called1).toEqual(1) - expect(called2).toEqual(2) - done() - }, 20) }) - it('should remove a listener registered by .one', done => { - fixtureEl.innerHTML = '<div></div>' + it('should remove a listener registered by .one', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') - const handler = () => { - throw new Error('called') - } + const div = fixtureEl.querySelector('div') + const handler = () => { + throw new Error('called') + } - EventHandler.one(div, 'foobar', handler) - EventHandler.off(div, 'foobar', handler) + EventHandler.one(div, 'foobar', handler) + EventHandler.off(div, 'foobar', handler) - EventHandler.trigger(div, 'foobar') - setTimeout(() => { - expect().nothing() - done() - }, 20) + EventHandler.trigger(div, 'foobar') + setTimeout(() => { + expect().nothing() + resolve() + }, 20) + }) }) it('should remove the correct delegated event listener', () => { diff --git a/js/tests/unit/dom/manipulator.spec.js b/js/tests/unit/dom/manipulator.spec.js index 963ee5989f..95af902ff5 100644 --- a/js/tests/unit/dom/manipulator.spec.js +++ b/js/tests/unit/dom/manipulator.spec.js @@ -1,5 +1,5 @@ import Manipulator from '../../../src/dom/manipulator' -import { getFixture, clearFixture } from '../../helpers/fixture' +import { clearFixture, getFixture } from '../../helpers/fixture' describe('Manipulator', () => { let fixtureEl @@ -134,42 +134,44 @@ describe('Manipulator', () => { }) }) - it('should not change offset when viewport is scrolled', done => { - const top = 500 - const left = 1000 - const scrollY = 200 - const scrollX = 400 + it('should not change offset when viewport is scrolled', () => { + return new Promise(resolve => { + const top = 500 + const left = 1000 + const scrollY = 200 + const scrollX = 400 - fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>` + fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>` - const div = fixtureEl.querySelector('div') - const offset = Manipulator.offset(div) + const div = fixtureEl.querySelector('div') + const offset = Manipulator.offset(div) - // append an element that forces scrollbars on the window so we can scroll - const { defaultView: win, body } = fixtureEl.ownerDocument - const forceScrollBars = document.createElement('div') - forceScrollBars.style.cssText = 'position:absolute;top:5000px;left:5000px;width:1px;height:1px' - body.append(forceScrollBars) + // append an element that forces scrollbars on the window so we can scroll + const { defaultView: win, body } = fixtureEl.ownerDocument + const forceScrollBars = document.createElement('div') + forceScrollBars.style.cssText = 'position:absolute;top:5000px;left:5000px;width:1px;height:1px' + body.append(forceScrollBars) - const scrollHandler = () => { - expect(window.pageYOffset).toEqual(scrollY) - expect(window.pageXOffset).toEqual(scrollX) + const scrollHandler = () => { + expect(window.pageYOffset).toEqual(scrollY) + expect(window.pageXOffset).toEqual(scrollX) - const newOffset = Manipulator.offset(div) + const newOffset = Manipulator.offset(div) - expect(newOffset).toEqual({ - top: offset.top, - left: offset.left - }) + expect(newOffset).toEqual({ + top: offset.top, + left: offset.left + }) - win.removeEventListener('scroll', scrollHandler) - forceScrollBars.remove() - win.scrollTo(0, 0) - done() - } + win.removeEventListener('scroll', scrollHandler) + forceScrollBars.remove() + win.scrollTo(0, 0) + resolve() + } - win.addEventListener('scroll', scrollHandler) - win.scrollTo(scrollX, scrollY) + win.addEventListener('scroll', scrollHandler) + win.scrollTo(scrollX, scrollY) + }) }) }) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index 6ee58fe71a..f24b59ed5c 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -57,36 +57,38 @@ describe('Dropdown', () => { expect(dropdownByElement._element).toEqual(btnDropdown) }) - it('should create offset modifier correctly when offset option is a function', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - offset: getOffset, - popperConfig: { - onFirstUpdate: state => { - expect(getOffset).toHaveBeenCalledWith({ - popper: state.rects.popper, - reference: state.rects.reference, - placement: state.placement - }, btnDropdown) - done() + it('should create offset modifier correctly when offset option is a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + offset: getOffset, + popperConfig: { + onFirstUpdate: state => { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, btnDropdown) + resolve() + } } - } - }) - const offset = dropdown._getOffset() + }) + const offset = dropdown._getOffset() - expect(typeof offset).toEqual('function') + expect(typeof offset).toEqual('function') - dropdown.show() + dropdown.show() + }) }) it('should create offset modifier correctly when offset option is a string into data attribute', () => { @@ -151,761 +153,817 @@ describe('Dropdown', () => { }) describe('toggle', () => { - it('should toggle a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + it('should toggle a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() + dropdown.toggle() }) - - dropdown.toggle() }) - it('should destroy old popper references on toggle', done => { - fixtureEl.innerHTML = [ - '<div class="first dropdown">', - ' <button class="firstBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>', - '<div class="second dropdown">', - ' <button class="secondBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should destroy old popper references on toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="first dropdown">', + ' <button class="firstBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>', + '<div class="second dropdown">', + ' <button class="secondBtn btn" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown1 = fixtureEl.querySelector('.firstBtn') + const btnDropdown2 = fixtureEl.querySelector('.secondBtn') + const firstDropdownEl = fixtureEl.querySelector('.first') + const secondDropdownEl = fixtureEl.querySelector('.second') + const dropdown1 = new Dropdown(btnDropdown1) + + firstDropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown1).toHaveClass('show') + spyOn(dropdown1._popper, 'destroy') + btnDropdown2.click() + }) - const btnDropdown1 = fixtureEl.querySelector('.firstBtn') - const btnDropdown2 = fixtureEl.querySelector('.secondBtn') - const firstDropdownEl = fixtureEl.querySelector('.first') - const secondDropdownEl = fixtureEl.querySelector('.second') - const dropdown1 = new Dropdown(btnDropdown1) + secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(dropdown1._popper.destroy).toHaveBeenCalled() + resolve() + })) - firstDropdownEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown1).toHaveClass('show') - spyOn(dropdown1._popper, 'destroy') - btnDropdown2.click() + dropdown1.toggle() }) - - secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(dropdown1._popper.destroy).toHaveBeenCalled() - done() - })) - - dropdown1.toggle() }) - it('should toggle a dropdown and add/remove event listener on mobile', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown and add/remove event listener on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const defaultValueOnTouchStart = document.documentElement.ontouchstart - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'on') - spyOn(EventHandler, 'off') + document.documentElement.ontouchstart = noop + spyOn(EventHandler, 'on') + spyOn(EventHandler, 'off') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - dropdown.toggle() - }) + dropdown.toggle() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown).not.toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown at the right', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu dropdown-menu-end">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown at the right', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu dropdown-menu-end">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropup', done => { - fixtureEl.innerHTML = [ - '<div class="dropup">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropup at the right', done => { - fixtureEl.innerHTML = [ - '<div class="dropup">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu dropdown-menu-end">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropup at the right', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu dropdown-menu-end">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropupEl = fixtureEl.querySelector('.dropup') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropend', done => { - fixtureEl.innerHTML = [ - '<div class="dropend">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropend', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropend">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropendEl = fixtureEl.querySelector('.dropend') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropendEl = fixtureEl.querySelector('.dropend') + const dropdown = new Dropdown(btnDropdown) - dropendEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropendEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropstart', done => { - fixtureEl.innerHTML = [ - '<div class="dropstart">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropstart', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropstart">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropstartEl = fixtureEl.querySelector('.dropstart') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropstartEl = fixtureEl.querySelector('.dropstart') + const dropdown = new Dropdown(btnDropdown) - dropstartEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + dropstartEl.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with parent reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with parent reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: 'parent' - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: 'parent' + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a dom node reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with a dom node reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: fixtureEl - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: fixtureEl + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a jquery object reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should toggle a dropdown with a jquery object reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown, { - reference: { 0: fixtureEl, jquery: 'jQuery' } - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: { 0: fixtureEl, jquery: 'jQuery' } + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - dropdown.toggle() + dropdown.toggle() + }) }) - it('should toggle a dropdown with a valid virtual element reference', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const virtualElement = { - nodeType: 1, - getBoundingClientRect() { - return { - width: 0, - height: 0, - top: 0, - right: 0, - bottom: 0, - left: 0 + it('should toggle a dropdown with a valid virtual element reference', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const virtualElement = { + nodeType: 1, + getBoundingClientRect() { + return { + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0 + } } } - } - expect(() => new Dropdown(btnDropdown, { - reference: {} - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + expect(() => new Dropdown(btnDropdown, { + reference: {} + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') - expect(() => new Dropdown(btnDropdown, { - reference: { - getBoundingClientRect: 'not-a-function' - } - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') - - // use onFirstUpdate as Poppers internal update is executed async - const dropdown = new Dropdown(btnDropdown, { - reference: virtualElement, - popperConfig: { - onFirstUpdate() { - expect(virtualElement.getBoundingClientRect).toHaveBeenCalled() - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() + expect(() => new Dropdown(btnDropdown, { + reference: { + getBoundingClientRect: 'not-a-function' } - } - }) + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + + // use onFirstUpdate as Poppers internal update is executed async + const dropdown = new Dropdown(btnDropdown, { + reference: virtualElement, + popperConfig: { + onFirstUpdate() { + expect(virtualElement.getBoundingClientRect).toHaveBeenCalled() + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + } + }) - spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() - dropdown.toggle() + dropdown.toggle() + }) }) - it('should not toggle a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the element is disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the element contains .disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if the menu is shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) - it('should not toggle a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not toggle a dropdown if show event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('show.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('show.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.toggle() + dropdown.toggle() - setTimeout(() => { - expect().nothing() - done() + setTimeout(() => { + expect().nothing() + resolve() + }) }) }) }) describe('show', () => { - it('should show a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + it('should show a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + resolve() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') - done() + dropdown.show() }) - - dropdown.show() }) - it('should not show a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the element is disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the element contains .disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if the menu is shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if the menu is shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not show a dropdown if show event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not show a dropdown if show event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('show.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('show.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - throw new Error('should not throw shown.bs.dropdown event') - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) - dropdown.show() + dropdown.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) }) describe('hide', () => { - it('should hide a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + it('should hide a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + resolve() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu).not.toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - done() + dropdown.hide() }) - - dropdown.hide() }) - it('should hide a dropdown and destroy popper', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should hide a dropdown and destroy popper', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - spyOn(dropdown._popper, 'destroy') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + spyOn(dropdown._popper, 'destroy') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdown._popper.destroy).toHaveBeenCalled() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdown._popper.destroy).toHaveBeenCalled() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not hide a dropdown if the element is disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the element is disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu).toHaveClass('show') - done() - }, 10) + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }, 10) + }) }) - it('should not hide a dropdown if the element contains .disabled', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the element contains .disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle disabled" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu).toHaveClass('show') - done() - }, 10) + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }, 10) + }) }) - it('should not hide a dropdown if the menu is not shown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if the menu is not shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should not hide a dropdown if hide event is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu show">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not hide a dropdown if hide event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu show">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('hide.bs.dropdown', event => { - event.preventDefault() - }) + btnDropdown.addEventListener('hide.bs.dropdown', event => { + event.preventDefault() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - throw new Error('should not throw hidden.bs.dropdown event') - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu).toHaveClass('show') - done() + setTimeout(() => { + expect(dropdownMenu).toHaveClass('show') + resolve() + }) }) }) - it('should remove event listener on touch-enabled device that was added in show method', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdwon item</a>', - ' </div>', - '</div>' - ].join('') + it('should remove event listener on touch-enabled device that was added in show method', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdwon item</a>', + ' </div>', + '</div>' + ].join('') - const defaultValueOnTouchStart = document.documentElement.ontouchstart - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(btnDropdown) + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'off') + document.documentElement.ontouchstart = noop + spyOn(EventHandler, 'off') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown).not.toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalled() + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(EventHandler.off).toHaveBeenCalled() - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) - dropdown.show() + dropdown.show() + }) }) }) @@ -1013,903 +1071,957 @@ describe('Dropdown', () => { }) describe('data-api', () => { - it('should show and hide a dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') - - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - let showEventTriggered = false - let hideEventTriggered = false + it('should show and hide a dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + let showEventTriggered = false + let hideEventTriggered = false + + btnDropdown.addEventListener('show.bs.dropdown', () => { + showEventTriggered = true + }) - btnDropdown.addEventListener('show.bs.dropdown', () => { - showEventTriggered = true - }) + btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(showEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + document.body.click() + })) - btnDropdown.addEventListener('shown.bs.dropdown', event => setTimeout(() => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(showEventTriggered).toBeTrue() - expect(event.relatedTarget).toEqual(btnDropdown) - document.body.click() - })) + btnDropdown.addEventListener('hide.bs.dropdown', () => { + hideEventTriggered = true + }) - btnDropdown.addEventListener('hide.bs.dropdown', () => { - hideEventTriggered = true - }) + btnDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(hideEventTriggered).toBeTrue() + expect(event.relatedTarget).toEqual(btnDropdown) + resolve() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', event => { - expect(btnDropdown).not.toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(hideEventTriggered).toBeTrue() - expect(event.relatedTarget).toEqual(btnDropdown) - done() + btnDropdown.click() }) - - btnDropdown.click() }) - it('should not use "static" Popper in navbar', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar navbar-expand-md navbar-light bg-light">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - ' </div>', - '</nav>' - ].join('') + it('should not use "static" Popper in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar navbar-expand-md navbar-light bg-light">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + ' </div>', + '</nav>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdown._popper).not.toBeNull() - expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown._popper).not.toBeNull() + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not collapse the dropdown when clicking a select option nested in the dropdown', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <select>', - ' <option selected>Open this select menu</option>', - ' <option value="1">One</option>', - ' </select>', - ' </div>', - '</div>' - ].join('') + it('should not collapse the dropdown when clicking a select option nested in the dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <select>', + ' <option selected>Open this select menu</option>', + ' <option value="1">One</option>', + ' </select>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - const hideSpy = spyOn(dropdown, '_completeHide') + const hideSpy = spyOn(dropdown, '_completeHide') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - const clickEvent = new MouseEvent('click', { - bubbles: true + btnDropdown.addEventListener('shown.bs.dropdown', () => { + const clickEvent = new MouseEvent('click', { + bubbles: true + }) + + dropdownMenu.querySelector('option').dispatchEvent(clickEvent) }) - dropdownMenu.querySelector('option').dispatchEvent(clickEvent) - }) + dropdownMenu.addEventListener('click', event => { + expect(event.target.tagName).toMatch(/select|option/i) - dropdownMenu.addEventListener('click', event => { - expect(event.target.tagName).toMatch(/select|option/i) + Dropdown.clearMenus(event) - Dropdown.clearMenus(event) + setTimeout(() => { + expect(hideSpy).not.toHaveBeenCalled() + resolve() + }, 10) + }) - setTimeout(() => { - expect(hideSpy).not.toHaveBeenCalled() - done() - }, 10) + dropdown.show() }) - - dropdown.show() }) - it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar navbar-expand-md navbar-light bg-light">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - ' </div>', - '</nav>' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when dropdown is in navbar', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar navbar-expand-md navbar-light bg-light">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + ' </div>', + '</nav>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should not use Popper if display set to static', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should not use Popper if display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - // Popper adds this attribute when we use it - expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() - done() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + // Popper adds this attribute when we use it + expect(dropdownMenu.getAttribute('data-popper-placement')).toBeNull() + resolve() + }) - btnDropdown.click() + btnDropdown.click() + }) }) - it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should manage bs attribute `data-bs-popper`="static" when display set to static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static') + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should remove "show" class if tabbing outside of menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if tabbing outside of menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown).toHaveClass('show') + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') - const keyup = createEvent('keyup') + const keyup = createEvent('keyup') - keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + keyup.key = 'Tab' + document.dispatchEvent(keyup) + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown).not.toHaveClass('show') - done() - }) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + resolve() + }) - btnDropdown.click() + btnDropdown.click() + }) }) - it('should remove "show" class if body is clicked, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '<div class="nav">', - ' <div class="dropdown" id="testmenu">', - ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' </div>', - ' </div>', - '</div>', - '<div class="btn-group">', - ' <button class="btn">Actions</button>', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Action 1</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if body is clicked, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <div class="dropdown" id="testmenu">', + ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + ' </div>', + '</div>', + '<div class="btn-group">', + ' <button class="btn">Actions</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Action 1</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') - expect(triggerDropdownList).toHaveSize(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst).toHaveClass('show') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - document.body.click() - }) + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) - triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) - triggerDropdownLast.click() - }) + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) - triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownLast).toHaveClass('show') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - document.body.click() - }) + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + document.body.click() + }) - triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) - done() - }) + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) - triggerDropdownFirst.click() + triggerDropdownFirst.click() + }) }) - it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' </div>', - '</div>', - '<div class="btn-group">', - ' <button class="btn">Actions</button>', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Action 1</a>', - ' </div>', - '</div>' - ].join('') + it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <a class="dropdown-toggle" data-bs-toggle="dropdown" href="#testmenu">Test menu</a>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + '</div>', + '<div class="btn-group">', + ' <button class="btn">Actions</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Action 1</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') + const triggerDropdownList = fixtureEl.querySelectorAll('[data-bs-toggle="dropdown"]') - expect(triggerDropdownList).toHaveSize(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst).toHaveClass('show') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownFirst).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - const keyup = createEvent('keyup') - keyup.key = 'Tab' + const keyup = createEvent('keyup') + keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + document.dispatchEvent(keyup) + }) - triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) - triggerDropdownLast.click() - }) + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) - triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownLast).toHaveClass('show') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) + triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdownLast).toHaveClass('show') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(1) - const keyup = createEvent('keyup') - keyup.key = 'Tab' + const keyup = createEvent('keyup') + keyup.key = 'Tab' - document.dispatchEvent(keyup) - }) + document.dispatchEvent(keyup) + }) - triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { - expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) - done() - }) + triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + resolve() + }) - triggerDropdownFirst.click() + triggerDropdownFirst.click() + }) }) - it('should fire hide and hidden event without a clickEvent if event type is not click', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' </div>', - '</div>' - ].join('') + it('should fire hide and hidden event without a clickEvent if event type is not click', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('hide.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - }) + triggerDropdown.addEventListener('hide.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + }) - triggerDropdown.addEventListener('hidden.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - done() - }) + triggerDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') - keydown.key = 'Escape' - triggerDropdown.dispatchEvent(keydown) - }) + keydown.key = 'Escape' + triggerDropdown.dispatchEvent(keydown) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should bubble up the events to the parent elements', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', - ' </div>', - '</div>' - ].join('') + it('should bubble up the events to the parent elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownParent = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(triggerDropdown) + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) - const showFunction = jasmine.createSpy('showFunction') - dropdownParent.addEventListener('show.bs.dropdown', showFunction) + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) - const shownFunction = jasmine.createSpy('shownFunction') - dropdownParent.addEventListener('shown.bs.dropdown', () => { - shownFunction() - dropdown.hide() - }) + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) - const hideFunction = jasmine.createSpy('hideFunction') - dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) - dropdownParent.addEventListener('hidden.bs.dropdown', () => { - expect(showFunction).toHaveBeenCalled() - expect(shownFunction).toHaveBeenCalled() - expect(hideFunction).toHaveBeenCalled() - done() - }) + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) - dropdown.show() + dropdown.show() + }) }) - it('should ignore keyboard events within <input>s and <textarea>s', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' <input type="text">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + it('should ignore keyboard events within <input>s and <textarea>s', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' <input type="text">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - expect(document.activeElement).toEqual(input, 'input still focused') + expect(document.activeElement).toEqual(input, 'input still focused') - textarea.focus() - textarea.dispatchEvent(keydown) + textarea.focus() + textarea.dispatchEvent(keydown) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() - }) + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', - ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', + ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement).not.toHaveClass('disabled') - expect(document.activeElement.hasAttribute('disabled')).toBeFalse() - done() - }) + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<style>', - ' .d-none {', - ' display: none;', - ' }', - '</style>', - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <button class="dropdown-item d-none" type="button">Hidden button by class</button>', - ' <a class="dropdown-item" href="#sub1" style="display: none">Hidden link</a>', - ' <a class="dropdown-item" href="#sub1" style="visibility: hidden">Hidden link</a>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<style>', + ' .d-none {', + ' display: none;', + ' }', + '</style>', + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <button class="dropdown-item d-none" type="button">Hidden button by class</button>', + ' <a class="dropdown-item" href="#sub1" style="display: none">Hidden link</a>', + ' <a class="dropdown-item" href="#sub1" style="visibility: hidden">Hidden link</a>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement).not.toHaveClass('d-none') - expect(document.activeElement.style.display).not.toEqual('none') - expect(document.activeElement.style.visibility).not.toEqual('hidden') + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') - done() - }) + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') - done() - }) + resolve() + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const lastItem = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a id="item1" class="dropdown-item" href="#">A link</a>', - ' <a id="item2" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a id="item1" class="dropdown-item" href="#">A link</a>', + ' <a id="item2" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const firstItem = fixtureEl.querySelector('#item1') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(firstItem, 'item1 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <input type="text">', - ' </div>', - '</div>' - ].join('') + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <input type="text">', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - input.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - input.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - textarea.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' </div>', - '</div>', - '<input type="text">' - ].join('') + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' </div>', + '</div>', + '<input type="text">' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true - })) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) - triggerDropdown.click() - }) + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for <input>s and <textarea>s within dropdown-menu, except for escape key', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', + ' <input type="text">', + ' <textarea></textarea>', + ' </div>', + '</div>' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) + } - it('should ignore keyboard events for <input>s and <textarea>s within dropdown-menu, except for escape key', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#sub1">Submenu 1</a>', - ' <input type="text">', - ' <textarea></textarea>', - ' </div>', - '</div>' - ].join('') + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - const test = (eventKey, elementToDispatch) => { - const event = createEvent('keydown') - event.key = eventKey - elementToDispatch.focus() - elementToDispatch.dispatchEvent(event) - expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) - } - - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - test('Space', input) + test('Space', textarea) - test('Space', textarea) + // Key ArrowUp + test('ArrowUp', input) - // Key ArrowUp - test('ArrowUp', input) + test('ArrowUp', textarea) - test('ArrowUp', textarea) + // Key ArrowDown + test('ArrowDown', input) - // Key ArrowDown - test('ArrowDown', input) + test('ArrowDown', textarea) - test('ArrowDown', textarea) + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - expect(triggerDropdown).not.toHaveClass('show') - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '<div class="tabs">', - ' <div class="dropdown">', - ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Secondary link</a>', - ' <a class="dropdown-item" href="#">Something else here</a>', - ' <div class="divider"></div>', - ' <a class="dropdown-item" href="#">Another link</a>', - ' </div>', - ' </div>', - '</div>' - ].join('') + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="tabs">', + ' <div class="dropdown">', + ' <button disabled class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Secondary link</a>', + ' <a class="dropdown-item" href="#">Something else here</a>', + ' <div class="divider"></div>', + ' <a class="dropdown-item" href="#">Another link</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') - spyOn(dropdown, 'toggle') + spyOn(dropdown, 'toggle') - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown).not.toHaveClass('show') - done() - }, 20) - }) - - it('should propagate escape key events if dropdown is closed', done => { - fixtureEl.innerHTML = [ - '<div class="parent">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Some Item</a>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const parent = fixtureEl.querySelector('.parent') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + setTimeout(() => { + expect(dropdown.toggle).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) + + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="parent">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) }) - - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' - - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) }) - it('should close dropdown using `escape` button, and return focus to its trigger', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Some Item</a>', - ' </div>', - '</div>' - ].join('') + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Some Item</a>', + ' </div>', + '</div>' + ].join('') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - toggle.addEventListener('shown.bs.dropdown', () => { - const keydownEvent = createEvent('keydown', { bubbles: true }) - keydownEvent.key = 'ArrowDown' - toggle.dispatchEvent(keydownEvent) - keydownEvent.key = 'Escape' - toggle.dispatchEvent(keydownEvent) - }) + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) - toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(document.activeElement).toEqual(toggle) - done() - })) + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) - toggle.click() + toggle.click() + }) }) - it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', - '</div>' - ].join('') + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - dropdownMenu.click() - }, 150) + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle).not.toHaveClass('show') - done() - })) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) - dropdownToggle.click() + dropdownToggle.click() + }) }) - it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', - '</div>' - ].join('') + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - document.documentElement.click() - }, 150) + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle).not.toHaveClass('show') - done() - }) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) - dropdownToggle.click() + dropdownToggle.click() + }) }) - it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#">Dropdown item</a>', - ' </div>', - '</div>' - ].join('') + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + '</div>' + ].join('') - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() - } + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } - expectDropdownToBeOpened(false) - }, 150) + expectDropdownToBeOpened(false) + }, 150) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - dropdownToggle.click() + dropdownToggle.click() + }) }) }) @@ -2030,52 +2142,54 @@ describe('Dropdown', () => { }) }) - it('should open dropdown when pressing keydown or keyup', done => { - fixtureEl.innerHTML = [ - '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', - ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', - ' <a id="item1" class="dropdown-item" href="#">Another link</a>', - ' </div>', - '</div>' - ].join('') + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>', + ' <button class="dropdown-item" type="button" disabled>Disabled button</button>', + ' <a id="item1" class="dropdown-item" href="#">Another link</a>', + ' </div>', + '</div>' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - const keyup = createEvent('keyup') - keyup.key = 'ArrowUp' + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' - const handleArrowDown = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - } - - dropdown.addEventListener('shown.bs.dropdown', event => { - if (event.target.key === 'ArrowDown') { - handleArrowDown() - } else { - handleArrowUp() + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) } - }) - triggerDropdown.dispatchEvent(keydown) + const handleArrowUp = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) }) it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { @@ -2101,27 +2215,29 @@ describe('Dropdown', () => { expect(delegatedClickListener).toHaveBeenCalled() }) - it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { - fixtureEl.innerHTML = [ - '<div class="container">', - ' <div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>', - ' <div class="dropdown-menu">', - ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', - ' </div>', - ' </div>', - '</div>' - ].join('') + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="container">', + ' <div class="dropdown">', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#subMenu">Sub menu</a>', + ' </div>', + ' </div>', + '</div>' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const childElement = fixtureEl.querySelector('#childElement') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') - btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - })) + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) - childElement.click() + childElement.click() + }) }) }) diff --git a/js/tests/unit/jquery.spec.js b/js/tests/unit/jquery.spec.js index 16781a3518..7da39d6303 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -12,7 +12,7 @@ import ScrollSpy from '../../src/scrollspy' import Tab from '../../src/tab' import Toast from '../../src/toast' import Tooltip from '../../src/tooltip' -import { getFixture, clearFixture } from '../helpers/fixture' +import { clearFixture, getFixture } from '../helpers/fixture' describe('jQuery', () => { let fixtureEl @@ -40,19 +40,21 @@ describe('jQuery', () => { expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip) }) - it('should use jQuery event system', done => { - fixtureEl.innerHTML = [ - '<div class="alert">', - ' <button type="button" data-bs-dismiss="alert">x</button>', - '</div>' - ].join('') - - $(fixtureEl).find('.alert') - .one('closed.bs.alert', () => { - expect($(fixtureEl).find('.alert')).toHaveSize(0) - done() - }) - - $(fixtureEl).find('button').trigger('click') + it('should use jQuery event system', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="alert">', + ' <button type="button" data-bs-dismiss="alert">x</button>', + '</div>' + ].join('') + + $(fixtureEl).find('.alert') + .one('closed.bs.alert', () => { + expect($(fixtureEl).find('.alert')).toHaveSize(0) + resolve() + }) + + $(fixtureEl).find('button').trigger('click') + }) }) }) diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 84a95c86ad..bf796411b5 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -56,93 +56,101 @@ describe('Modal', () => { }) describe('toggle', () => { - it('should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + modal.toggle() + }) - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() modal.toggle() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - - modal.toggle() }) }) describe('show', () => { - it('should show a modal', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should show a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal without backdrop', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should show a modal without backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal and append the element', done => { - const modalEl = document.createElement('div') - const id = 'dynamicModal' + it('should show a modal and append the element', () => { + return new Promise(resolve => { + const modalEl = document.createElement('div') + const id = 'dynamicModal' - modalEl.setAttribute('id', id) - modalEl.classList.add('modal') - modalEl.innerHTML = '<div class="modal-dialog"></div>' + modalEl.setAttribute('id', id) + modalEl.classList.add('modal') + modalEl.innerHTML = '<div class="modal-dialog"></div>' - const modal = new Modal(modalEl) + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const dynamicModal = document.getElementById(id) - expect(dynamicModal).not.toBeNull() - dynamicModal.remove() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + const dynamicModal = document.getElementById(id) + expect(dynamicModal).not.toBeNull() + dynamicModal.remove() + resolve() + }) - modal.show() + modal.show() + }) }) it('should do nothing if a modal is shown', () => { @@ -173,511 +181,551 @@ describe('Modal', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should not fire shown event when show is prevented', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should not fire shown event when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() - const expectedDone = () => { - expect().nothing() - done() - } + const expectedDone = () => { + expect().nothing() + resolve() + } - setTimeout(expectedDone, 10) - }) + setTimeout(expectedDone, 10) + }) - modalEl.addEventListener('shown.bs.modal', () => { - throw new Error('shown event triggered') - }) + modalEl.addEventListener('shown.bs.modal', () => { + throw new Error('shown event triggered') + }) - modal.show() + modal.show() + }) }) - it('should be shown after the first call to show() has been prevented while fading is enabled ', done => { - fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' + it('should be shown after the first call to show() has been prevented while fading is enabled ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - let prevented = false - modalEl.addEventListener('show.bs.modal', event => { - if (!prevented) { - event.preventDefault() - prevented = true + let prevented = false + modalEl.addEventListener('show.bs.modal', event => { + if (!prevented) { + event.preventDefault() + prevented = true - setTimeout(() => { - modal.show() - }) - } - }) + setTimeout(() => { + modal.show() + }) + } + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(prevented).toBeTrue() - expect(modal._isAnimated()).toBeTrue() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(prevented).toBeTrue() + expect(modal._isAnimated()).toBeTrue() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should set is transitioning if fade class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' - it('should set is transitioning if fade class is present', done => { - fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('show.bs.modal', () => { + setTimeout(() => { + expect(modal._isTransitioning).toBeTrue() + }) + }) - modalEl.addEventListener('show.bs.modal', () => { - setTimeout(() => { - expect(modal._isTransitioning).toBeTrue() + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._isTransitioning).toBeFalse() + resolve() }) - }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._isTransitioning).toBeFalse() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', done => { - fixtureEl.innerHTML = [ - '<div class="modal fade">', - ' <div class="modal-dialog">', - ' <div class="modal-header">', - ' <button type="button" data-bs-dismiss="modal"></button>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) - - spyOn(modal, 'hide').and.callThrough() + it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal fade">', + ' <div class="modal-dialog">', + ' <div class="modal-header">', + ' <button type="button" data-bs-dismiss="modal"></button>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) + + spyOn(modal, 'hide').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', done => { - fixtureEl.innerHTML = [ - '<button type="button" data-bs-dismiss="modal" data-bs-target="#modal1"></button>', - '<div id="modal1" class="modal fade">', - ' <div class="modal-dialog"></div>', - '</div>' - ].join('') + it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button type="button" data-bs-dismiss="modal" data-bs-target="#modal1"></button>', + '<div id="modal1" class="modal fade">', + ' <div class="modal-dialog"></div>', + '</div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set .modal\'s scroll top to 0', done => { - fixtureEl.innerHTML = [ - '<div class="modal fade">', - ' <div class="modal-dialog"></div>', - '</div>' - ].join('') + it('should set .modal\'s scroll top to 0', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal fade">', + ' <div class="modal-dialog"></div>', + '</div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.scrollTop).toEqual(0) - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.scrollTop).toEqual(0) + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set modal body scroll top to 0 if modal body do not exists', done => { - fixtureEl.innerHTML = [ - '<div class="modal fade">', - ' <div class="modal-dialog">', - ' <div class="modal-body"></div>', - ' </div>', - '</div>' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modalBody = modalEl.querySelector('.modal-body') - const modal = new Modal(modalEl) + it('should set modal body scroll top to 0 if modal body do not exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal fade">', + ' <div class="modal-dialog">', + ' <div class="modal-body"></div>', + ' </div>', + '</div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modalBody = modalEl.querySelector('.modal-body') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalBody.scrollTop).toEqual(0) + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalBody.scrollTop).toEqual(0) - done() + modal.show() }) - - modal.show() }) - it('should not trap focus if focus equal to false', done => { - fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' + it('should not trap focus if focus equal to false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - focus: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + focus: false + }) - spyOn(modal._focustrap, 'activate').and.callThrough() + spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should add listener when escape touch is pressed', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should add listener when escape touch is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - }) + modalEl.dispatchEvent(keydownEscape) + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should do nothing when the pressed key is not escape', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should do nothing when the pressed key is not escape', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide') + spyOn(modal, 'hide') - const expectDone = () => { - expect(modal.hide).not.toHaveBeenCalled() + const expectDone = () => { + expect(modal.hide).not.toHaveBeenCalled() - done() - } + resolve() + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownTab = createEvent('keydown') - keydownTab.key = 'Tab' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownTab = createEvent('keydown') + keydownTab.key = 'Tab' - modalEl.dispatchEvent(keydownTab) - setTimeout(expectDone, 30) - }) + modalEl.dispatchEvent(keydownTab) + setTimeout(expectDone, 30) + }) - modal.show() + modal.show() + }) }) - it('should adjust dialog on resize', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should adjust dialog on resize', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + spyOn(modal, '_adjustDialog').and.callThrough() - spyOn(modal, '_adjustDialog').and.callThrough() + const expectDone = () => { + expect(modal._adjustDialog).toHaveBeenCalled() - const expectDone = () => { - expect(modal._adjustDialog).toHaveBeenCalled() + resolve() + } - done() - } + modalEl.addEventListener('shown.bs.modal', () => { + const resizeEvent = createEvent('resize') - modalEl.addEventListener('shown.bs.modal', () => { - const resizeEvent = createEvent('resize') + window.dispatchEvent(resizeEvent) + setTimeout(expectDone, 10) + }) - window.dispatchEvent(resizeEvent) - setTimeout(expectDone, 10) + modal.show() }) - - modal.show() }) - it('should not close modal when clicking on modal-content', done => { - fixtureEl.innerHTML = [ - '<div class="modal">', - ' <div class="modal-dialog">', - ' <div class="modal-content"></div>', - ' </div>', - '</div>' - ].join('') + it('should not close modal when clicking on modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal">', + ' <div class="modal-dialog">', + ' <div class="modal-content"></div>', + ' </div>', + '</div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } - - modalEl.addEventListener('shown.bs.modal', () => { - fixtureEl.querySelector('.modal-dialog').click() - fixtureEl.querySelector('.modal-content').click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toEqual(true) + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + fixtureEl.querySelector('.modal-dialog').click() + fixtureEl.querySelector('.modal-content').click() + shownCallback() + }) - it('should not close modal when clicking outside of modal-content if backdrop = false', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false + modal.show() }) + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - it('should not close modal when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' + modal.show() }) + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - it('should close modal when escape key is pressed with keyboard = true and backdrop is static', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static', - keyboard: true + modal.show() }) + }) + it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static', + keyboard: true + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeFalse() - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeFalse() + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) - modal.show() + modal.show() + }) }) - it('should not close modal when escape key is pressed with keyboard = false', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should not close modal when escape key is pressed with keyboard = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - keyboard: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + keyboard: false + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - modal.show() + modal.show() + }) }) - it('should not overflow when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>' + it('should not overflow when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - setTimeout(() => { - expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) - done() - }, 20) - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + setTimeout(() => { + expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) + resolve() + }, 20) + }) - modal.show() + modal.show() + }) }) - it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>' + it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('shown.bs.modal', () => { - const spy = spyOn(modal, '_queueCallback').and.callThrough() + modalEl.addEventListener('shown.bs.modal', () => { + const spy = spyOn(modal, '_queueCallback').and.callThrough() - modalEl.click() - modalEl.click() + modalEl.click() + modalEl.click() - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1) - done() - }, 20) - }) + setTimeout(() => { + expect(spy).toHaveBeenCalledTimes(1) + resolve() + }, 20) + }) - modal.show() + modal.show() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'activate').and.callThrough() + spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) describe('hide', () => { - it('should hide a modal', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should hide a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hide.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('hide.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(backdropSpy).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(backdropSpy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should close modal when clicking outside of modal-content', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should close modal when clicking outside of modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) it('should do nothing is the modal is not shown', () => { @@ -703,52 +751,56 @@ describe('Modal', () => { expect().nothing() }) - it('should not hide a modal if hide is prevented', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should not hide a modal if hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - const hideCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + const hideCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('hide.bs.modal', event => { - event.preventDefault() - hideCallback() - }) + modalEl.addEventListener('hide.bs.modal', event => { + event.preventDefault() + hideCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('should not trigger hidden') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('should not trigger hidden') + }) - modal.show() + modal.show() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'deactivate').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + spyOn(modal._focustrap, 'deactivate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal._focustrap.deactivate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) @@ -789,246 +841,260 @@ describe('Modal', () => { }) describe('data-api', () => { - it('should toggle modal', done => { - fixtureEl.innerHTML = [ - '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>', - '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' - ].join('') + it('should toggle modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>', + '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + setTimeout(() => trigger.click(), 10) + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - setTimeout(() => trigger.click(), 10) - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() + trigger.click() }) - - trigger.click() }) - it('should not recreate a new modal', done => { - fixtureEl.innerHTML = [ - '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>', - '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' - ].join('') + it('should not recreate a new modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>', + '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(modal, 'show').and.callThrough() + spyOn(modal, 'show').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal.show).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal.show).toHaveBeenCalled() + resolve() + }) - trigger.click() + trigger.click() + }) }) - it('should prevent default when the trigger is <a> or <area>', done => { - fixtureEl.innerHTML = [ - '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', - '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' - ].join('') + it('should prevent default when the trigger is <a> or <area>', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', + '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + trigger.click() }) - - trigger.click() }) - it('should focus the trigger on hide', done => { - fixtureEl.innerHTML = [ - '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', - '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' - ].join('') + it('should focus the trigger on hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', + '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(trigger, 'focus') + spyOn(trigger, 'focus') - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modal.hide() - }) + modal.hide() + }) - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 20) - } + const hideListener = () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 20) + } - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) - trigger.click() + trigger.click() + }) }) + it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than <a> or <area>', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal">', + ' <div class="modal-dialog">', + ' <button type="button" data-bs-dismiss="modal"></button>', + ' </div>', + '</div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) + + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than <a> or <area>', done => { - fixtureEl.innerHTML = [ - '<div class="modal">', - ' <div class="modal-dialog">', - ' <button type="button" data-bs-dismiss="modal"></button>', - ' </div>', - '</div>' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) - - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).not.toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).not.toHaveBeenCalled() - done() + modal.show() }) - - modal.show() }) - it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is <a> or <area>', done => { - fixtureEl.innerHTML = [ - '<div class="modal">', - ' <div class="modal-dialog">', - ' <a type="button" data-bs-dismiss="modal"></a>', - ' </div>', - '</div>' - ].join('') + it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is <a> or <area>', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal">', + ' <div class="modal-dialog">', + ' <a type="button" data-bs-dismiss="modal"></a>', + ' </div>', + '</div>' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should not focus the trigger if the modal is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal" style="display: none;"></a>', + '<div id="exampleModal" class="modal" style="display: none;"><div class="modal-dialog"></div></div>' + ].join('') - it('should not focus the trigger if the modal is not visible', done => { - fixtureEl.innerHTML = [ - '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal" style="display: none;"></a>', - '<div id="exampleModal" class="modal" style="display: none;"><div class="modal-dialog"></div></div>' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modal.hide() + }) - modal.hide() - }) + const hideListener = () => { + setTimeout(() => { + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 20) + } - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 20) - } + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + trigger.click() }) - - trigger.click() }) + it('should not focus the trigger if the modal is not shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', + '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' + ].join('') - it('should not focus the trigger if the modal is not shown', done => { - fixtureEl.innerHTML = [ - '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>', - '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + const showListener = () => { + setTimeout(() => { + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 10) + } - const showListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 10) - } + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() + showListener() + }) - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() - showListener() + trigger.click() }) - - trigger.click() }) - it('should call hide first, if another modal is open', done => { - fixtureEl.innerHTML = [ - '<button data-bs-toggle="modal" data-bs-target="#modal2"></button>', - '<div id="modal1" class="modal fade"><div class="modal-dialog"></div></div>', - '<div id="modal2" class="modal"><div class="modal-dialog"></div></div>' - ].join('') - - const trigger2 = fixtureEl.querySelector('button') - const modalEl1 = document.querySelector('#modal1') - const modalEl2 = document.querySelector('#modal2') - const modal1 = new Modal(modalEl1) - - modalEl1.addEventListener('shown.bs.modal', () => { - trigger2.click() - }) - modalEl1.addEventListener('hidden.bs.modal', () => { - expect(Modal.getInstance(modalEl2)).not.toBeNull() - expect(modalEl2).toHaveClass('show') - done() + it('should call hide first, if another modal is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button data-bs-toggle="modal" data-bs-target="#modal2"></button>', + '<div id="modal1" class="modal fade"><div class="modal-dialog"></div></div>', + '<div id="modal2" class="modal"><div class="modal-dialog"></div></div>' + ].join('') + + const trigger2 = fixtureEl.querySelector('button') + const modalEl1 = document.querySelector('#modal1') + const modalEl2 = document.querySelector('#modal2') + const modal1 = new Modal(modalEl1) + + modalEl1.addEventListener('shown.bs.modal', () => { + trigger2.click() + }) + modalEl1.addEventListener('hidden.bs.modal', () => { + expect(Modal.getInstance(modalEl2)).not.toBeNull() + expect(modalEl2).toHaveClass('show') + resolve() + }) + modal1.show() }) - modal1.show() }) }) - describe('jQueryInterface', () => { it('should create a modal', () => { fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 36ef45dceb..852ffa5560 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -147,83 +147,91 @@ describe('Offcanvas', () => { }) describe('options', () => { - it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() - offCanvas.hide() - }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvas.show() }) - it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() - offCanvas.hide() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - offCanvas.show() }) - it('should hide a shown element if user click on backdrop', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should hide a shown element if user click on backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) - const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) - spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) - offCanvas._backdrop._getElement().dispatchEvent(clickEvent) - }) + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) - it('should not trap focus if scroll is allowed', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should not trap focus if scroll is allowed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { - scroll: true - }) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true + }) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) @@ -241,44 +249,48 @@ describe('Offcanvas', () => { expect(offCanvas.show).toHaveBeenCalled() }) - it('should call hide method if show class is present', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should call hide method if show class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + spyOn(offCanvas, 'hide') - offCanvas.toggle() + offCanvas.toggle() - expect(offCanvas.hide).toHaveBeenCalled() - done() - }) + expect(offCanvas.hide).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('show', () => { - it('should add `showing` class during opening and `show` class on end', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `showing` class during opening and `show` class on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('show.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') - }) + offCanvasEl.addEventListener('show.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + resolve() + }) - offCanvas.show() - expect(offCanvasEl).toHaveClass('showing') + offCanvas.show() + expect(offCanvasEl).toHaveClass('showing') + }) }) it('should do nothing if already shown', () => { @@ -298,104 +310,114 @@ describe('Offcanvas', () => { expect(offCanvas._backdrop.show).not.toHaveBeenCalled() }) - it('should show a hidden element', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should show a hidden element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(offCanvas._backdrop.show).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(offCanvas._backdrop.show).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.show).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('show.bs.offcanvas', event => { - event.preventDefault() - expectEnd() - }) + offCanvasEl.addEventListener('show.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - throw new Error('should not fire shown event') - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + throw new Error('should not fire shown event') + }) - offCanvas.show() + offCanvas.show() + }) }) - it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => { - fixtureEl.innerHTML = '<div class="offcanvas show"></div>' + it('on window load, should make visible an offcanvas element, if its markup contains class "show"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas show"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + spyOn(Offcanvas.prototype, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + resolve() + }) - window.dispatchEvent(createEvent('load')) + window.dispatchEvent(createEvent('load')) - const instance = Offcanvas.getInstance(offCanvasEl) - expect(instance).not.toBeNull() - expect(Offcanvas.prototype.show).toHaveBeenCalled() + const instance = Offcanvas.getInstance(offCanvasEl) + expect(instance).not.toBeNull() + expect(Offcanvas.prototype.show).toHaveBeenCalled() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('hide', () => { - it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('hide.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('hiding') - expect(offCanvasEl).not.toHaveClass('show') - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('hiding') + expect(offCanvasEl).not.toHaveClass('show') + resolve() + }) - offCanvas.show() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - offCanvas.hide() - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('hiding') + offCanvas.show() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + offCanvas.hide() + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('hiding') + }) }) }) @@ -413,65 +435,71 @@ describe('Offcanvas', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should hide a shown element', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should hide a shown element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') - expect(offCanvas._backdrop.hide).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(offCanvas._backdrop.hide).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + offCanvas.show() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('hide.bs.offcanvas', event => { - event.preventDefault() - expectEnd() - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - throw new Error('should not fire hidden event') - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + throw new Error('should not fire hidden event') + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) }) @@ -501,22 +529,24 @@ describe('Offcanvas', () => { }) describe('data-api', () => { - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />', - '<div id="offcanvasdiv1" class="offcanvas"></div>' - ].join('') + it('should not prevent event for input', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />', + '<div id="offcanvasdiv1" class="offcanvas"></div>' + ].join('') - const target = fixtureEl.querySelector('input') - const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') + const target = fixtureEl.querySelector('input') + const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(target.checked).toBeTrue() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(target.checked).toBeTrue() + resolve() + }) - target.click() + target.click() + }) }) it('should not call toggle on disabled elements', () => { @@ -534,76 +564,82 @@ describe('Offcanvas', () => { expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled() }) - it('should call hide first, if another offcanvas is open', done => { - fixtureEl.innerHTML = [ - '<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2"></button>', - '<div id="offcanvas1" class="offcanvas"></div>', - '<div id="offcanvas2" class="offcanvas"></div>' - ].join('') - - const trigger2 = fixtureEl.querySelector('#btn2') - const offcanvasEl1 = document.querySelector('#offcanvas1') - const offcanvasEl2 = document.querySelector('#offcanvas2') - const offcanvas1 = new Offcanvas(offcanvasEl1) - - offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { - trigger2.click() + it('should call hide first, if another offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2"></button>', + '<div id="offcanvas1" class="offcanvas"></div>', + '<div id="offcanvas2" class="offcanvas"></div>' + ].join('') + + const trigger2 = fixtureEl.querySelector('#btn2') + const offcanvasEl1 = document.querySelector('#offcanvas1') + const offcanvasEl2 = document.querySelector('#offcanvas2') + const offcanvas1 = new Offcanvas(offcanvasEl1) + + offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { + trigger2.click() + }) + offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { + expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() + resolve() + }) + offcanvas1.show() + }) + }) + + it('should focus on trigger element after closing offcanvas', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>', + '<div id="offcanvas" class="offcanvas"></div>' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() + }) + }) + + it('should not focus on trigger element after closing offcanvas, if it is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>', + '<div id="offcanvas" class="offcanvas"></div>' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + trigger.style.display = 'none' + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(isVisible(trigger)).toBeFalse() + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { - expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() - done() - }) - offcanvas1.show() - }) - - it('should focus on trigger element after closing offcanvas', done => { - fixtureEl.innerHTML = [ - '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>', - '<div id="offcanvas" class="offcanvas"></div>' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - offcanvas.hide() - }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() - }) - - it('should not focus on trigger element after closing offcanvas, if it is not visible', done => { - fixtureEl.innerHTML = [ - '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas"></button>', - '<div id="offcanvas" class="offcanvas"></div>' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - trigger.style.display = 'none' - offcanvas.hide() - }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(isVisible(trigger)).toBeFalse() - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() }) }) diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js index a04bd21c60..7bbd52b1db 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -62,113 +62,125 @@ describe('Popover', () => { }) describe('show', () => { - it('should show a popover', done => { - fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>' + it('should show a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - expect(document.querySelector('.popover')).not.toBeNull() - done() - }) + popoverEl.addEventListener('shown.bs.popover', () => { + expect(document.querySelector('.popover')).not.toBeNull() + resolve() + }) - popover.show() + popover.show() + }) }) - it('should set title and content from functions', done => { - fixtureEl.innerHTML = '<a href="#">BS twitter</a>' + it('should set title and content from functions', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#">BS twitter</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: () => 'Bootstrap', - content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' - }) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: () => 'Bootstrap', + content: () => 'loves writing tests (╯°□°)╯︵ ┻━┻' + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') - done() - }) + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Bootstrap') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('loves writing tests (╯°□°)╯︵ ┻━┻') + resolve() + }) - popover.show() + popover.show() + }) }) - it('should show a popover with just content without having header', done => { - fixtureEl.innerHTML = '<a href="#">Nice link</a>' + it('should show a popover with just content without having header', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#">Nice link</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Some beautiful content :)' - }) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Some beautiful content :)' + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') - done() - }) + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Some beautiful content :)') + resolve() + }) - popover.show() + popover.show() + }) }) - it('should show a popover with just title without having body', done => { - fixtureEl.innerHTML = '<a href="#">Nice link</a>' + it('should show a popover with just title without having body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#">Nice link</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - title: 'Title which does not require content' - }) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title: 'Title which does not require content' + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') - done() - }) + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) - popover.show() + popover.show() + }) }) - it('should show a popover with just title without having body using data-attribute to get config', done => { - fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="Title which does not require content">Nice link</a>' + it('should show a popover with just title without having body using data-attribute to get config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="Title which does not require content">Nice link</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() - expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') - done() - }) + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body')).toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('Title which does not require content') + resolve() + }) - popover.show() + popover.show() + }) }) - it('should NOT show a popover without `title` and `content`', done => { - fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="">Nice link</a>' + it('should NOT show a popover without `title` and `content`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="">Nice link</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { animation: false }) - spyOn(EventHandler, 'trigger').and.callThrough() + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { animation: false }) + spyOn(EventHandler, 'trigger').and.callThrough() - setTimeout(() => { - expect(EventHandler.trigger).not.toHaveBeenCalled() - expect(document.querySelector('.popover')).toBeNull() - done() - }) + setTimeout(() => { + expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) - popover.show() + popover.show() + }) }) it('"setContent" should keep the initial template', () => { @@ -187,72 +199,78 @@ describe('Popover', () => { expect(tip.querySelector('.popover-body')).not.toBeNull() }) - it('should call setContent once', done => { - fixtureEl.innerHTML = '<a href="#">BS twitter</a>' - - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Popover content' - }) - expect(popover._templateFactory).toBeNull() - let spy = null - let times = 1 - - popoverEl.addEventListener('hidden.bs.popover', () => { + it('should call setContent once', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#">BS twitter</a>' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Popover content' + }) + expect(popover._templateFactory).toBeNull() + let spy = null + let times = 1 + + popoverEl.addEventListener('hidden.bs.popover', () => { + popover.show() + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough() + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') + expect(spy).toHaveBeenCalledTimes(0) + if (times > 1) { + resolve() + } + + times++ + popover.hide() + }) popover.show() }) - - popoverEl.addEventListener('shown.bs.popover', () => { - spy = spy || spyOn(popover._templateFactory, 'constructor').and.callThrough() - const popoverDisplayed = document.querySelector('.popover') - - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - expect(spy).toHaveBeenCalledTimes(0) - if (times > 1) { - done() - } - - times++ - popover.hide() - }) - popover.show() }) - it('should show a popover with provided custom class', done => { - fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>' + it('should show a popover with provided custom class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - const tip = document.querySelector('.popover') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - done() - }) + popoverEl.addEventListener('shown.bs.popover', () => { + const tip = document.querySelector('.popover') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) - popover.show() + popover.show() + }) }) }) describe('hide', () => { - it('should hide a popover', done => { - fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>' + it('should hide a popover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap">BS twitter</a>' - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl) + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) - popoverEl.addEventListener('shown.bs.popover', () => { - popover.hide() - }) + popoverEl.addEventListener('shown.bs.popover', () => { + popover.hide() + }) - popoverEl.addEventListener('hidden.bs.popover', () => { - expect(document.querySelector('.popover')).toBeNull() - done() - }) + popoverEl.addEventListener('hidden.bs.popover', () => { + expect(document.querySelector('.popover')).toBeNull() + resolve() + }) - popover.show() + popover.show() + }) }) }) diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index 5c044e697a..291654d9be 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -1,6 +1,6 @@ import ScrollSpy from '../../src/scrollspy' import Manipulator from '../../src/dom/manipulator' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('ScrollSpy', () => { let fixtureEl @@ -85,418 +85,436 @@ describe('ScrollSpy', () => { expect(scrollSpy._targets).toHaveSize(2) }) - it('should only switch "active" class on current target', done => { - fixtureEl.innerHTML = [ - '<div id="root" class="active" style="display: block">', - ' <div class="topbar">', - ' <div class="topbar-inner">', - ' <div class="container" id="ss-target">', - ' <ul class="nav">', - ' <li class="nav-item"><a href="#masthead">Overview</a></li>', - ' <li class="nav-item"><a href="#detail">Detail</a></li>', - ' </ul>', - ' </div>', - ' </div>', - ' </div>', - ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', - ' <div style="height: 200px;">', - ' <h4 id="masthead">Overview</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' <div style="height: 200px;">', - ' <h4 id="detail">Detail</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: 'ss-target' - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl).toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() + it('should only switch "active" class on current target', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="root" class="active" style="display: block">', + ' <div class="topbar">', + ' <div class="topbar-inner">', + ' <div class="container" id="ss-target">', + ' <ul class="nav">', + ' <li class="nav-item"><a href="#masthead">Overview</a></li>', + ' <li class="nav-item"><a href="#detail">Detail</a></li>', + ' </ul>', + ' </div>', + ' </div>', + ' </div>', + ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', + ' <div style="height: 200px;">', + ' <h4 id="masthead">Overview</h4>', + ' <p style="height: 200px;"></p>', + ' </div>', + ' <div style="height: 200px;">', + ' <h4 id="detail">Detail</h4>', + ' <p style="height: 200px;"></p>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') + const rootEl = fixtureEl.querySelector('#root') + const scrollSpy = new ScrollSpy(scrollSpyEl, { + target: 'ss-target' + }) + + spyOn(scrollSpy, '_process').and.callThrough() + + scrollSpyEl.addEventListener('scroll', () => { + expect(rootEl).toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) + + scrollSpyEl.scrollTop = 350 }) - - scrollSpyEl.scrollTop = 350 }) - it('should only switch "active" class on current target specified w element', done => { - fixtureEl.innerHTML = [ - '<div id="root" class="active" style="display: block">', - ' <div class="topbar">', - ' <div class="topbar-inner">', - ' <div class="container" id="ss-target">', - ' <ul class="nav">', - ' <li class="nav-item"><a href="#masthead">Overview</a></li>', - ' <li class="nav-item"><a href="#detail">Detail</a></li>', - ' </ul>', - ' </div>', - ' </div>', - ' </div>', - ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', - ' <div style="height: 200px;">', - ' <h4 id="masthead">Overview</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' <div style="height: 200px;">', - ' <h4 id="detail">Detail</h4>', - ' <p style="height: 200px;"></p>', - ' </div>', - ' </div>', - '</div>' - ].join('') - - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: fixtureEl.querySelector('#ss-target') - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl).toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() + it('should only switch "active" class on current target specified w element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="root" class="active" style="display: block">', + ' <div class="topbar">', + ' <div class="topbar-inner">', + ' <div class="container" id="ss-target">', + ' <ul class="nav">', + ' <li class="nav-item"><a href="#masthead">Overview</a></li>', + ' <li class="nav-item"><a href="#detail">Detail</a></li>', + ' </ul>', + ' </div>', + ' </div>', + ' </div>', + ' <div id="scrollspy-example" style="height: 100px; overflow: auto;">', + ' <div style="height: 200px;">', + ' <h4 id="masthead">Overview</h4>', + ' <p style="height: 200px;"></p>', + ' </div>', + ' <div style="height: 200px;">', + ' <h4 id="detail">Detail</h4>', + ' <p style="height: 200px;"></p>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') + const rootEl = fixtureEl.querySelector('#root') + const scrollSpy = new ScrollSpy(scrollSpyEl, { + target: fixtureEl.querySelector('#ss-target') + }) + + spyOn(scrollSpy, '_process').and.callThrough() + + scrollSpyEl.addEventListener('scroll', () => { + expect(rootEl).toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) + + scrollSpyEl.scrollTop = 350 }) - - scrollSpyEl.scrollTop = 350 }) - it('should correctly select middle navigation option when large offset is used', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>', - ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="one" style="height: 500px;"></div>', - ' <div id="two" style="height: 300px;"></div>', - ' <div id="three" style="height: 10px;"></div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - - spyOn(scrollSpy, '_process').and.callThrough() - - contentEl.addEventListener('scroll', () => { - expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active') - expect(fixtureEl.querySelector('#two-link')).toHaveClass('active') - expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active') - expect(scrollSpy._process).toHaveBeenCalled() - done() + it('should correctly select middle navigation option when large offset is used', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="header" style="height: 500px;"></div>', + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">', + ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>', + ' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>', + ' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>', + ' </ul>', + '</nav>', + '<div id="content" style="height: 200px; overflow-y: auto;">', + ' <div id="one" style="height: 500px;"></div>', + ' <div id="two" style="height: 300px;"></div>', + ' <div id="three" style="height: 10px;"></div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: Manipulator.position(contentEl).top + }) + + spyOn(scrollSpy, '_process').and.callThrough() + + contentEl.addEventListener('scroll', () => { + expect(fixtureEl.querySelector('#one-link')).not.toHaveClass('active') + expect(fixtureEl.querySelector('#two-link')).toHaveClass('active') + expect(fixtureEl.querySelector('#three-link')).not.toHaveClass('active') + expect(scrollSpy._process).toHaveBeenCalled() + resolve() + }) + + contentEl.scrollTop = 550 }) - - contentEl.scrollTop = 550 }) - it('should add the active class to the correct element', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>', - ' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + it('should add the active class to the correct element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar">', + ' <ul class="nav">', + ' <li class="nav-item"><a class="nav-link" id="a-1" href="#div-1">div 1</a></li>', + ' <li class="nav-item"><a class="nav-link" id="a-2" href="#div-2">div 2</a></li>', + ' </ul>', + '</nav>', + '<div class="content" style="overflow: auto; height: 50px">', + ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', + ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) }) - it('should add the active class to the correct element (nav markup)', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <nav class="nav">', - ' <a class="nav-link" id="a-1" href="#div-1">div 1</a>', - ' <a class="nav-link" id="a-2" href="#div-2">div 2</a>', - ' </nav>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + it('should add the active class to the correct element (nav markup)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar">', + ' <nav class="nav">', + ' <a class="nav-link" id="a-1" href="#div-1">div 1</a>', + ' <a class="nav-link" id="a-2" href="#div-2">div 2</a>', + ' </nav>', + '</nav>', + '<div class="content" style="overflow: auto; height: 50px">', + ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', + ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) }) - it('should add the active class to the correct element (list-group markup)', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <div class="list-group">', - ' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>', - ' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>', - ' </div>', - '</nav>', - '<div class="content" style="overflow: auto; height: 50px">', - ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#a-1', - targetSelector: '#div-1', - contentEl, - scrollSpy, - spy, - cb: () => { - testElementIsActiveAfterScroll({ - elementSelector: '#a-2', - targetSelector: '#div-2', - contentEl, - scrollSpy, - spy, - cb: () => done() - }) - } + it('should add the active class to the correct element (list-group markup)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar">', + ' <div class="list-group">', + ' <a class="list-group-item" id="a-1" href="#div-1">div 1</a>', + ' <a class="list-group-item" id="a-2" href="#div-2">div 2</a>', + ' </div>', + '</nav>', + '<div class="content" style="overflow: auto; height: 50px">', + ' <div id="div-1" style="height: 100px; padding: 0; margin: 0">div 1</div>', + ' <div id="div-2" style="height: 200px; padding: 0; margin: 0">div 2</div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + spy, + cb: () => { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + spy, + cb: resolve + }) + } + }) }) }) - it('should clear selection if above the first section', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', - ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="spacer" style="height: 100px;"></div>', - ' <div id="one" style="height: 100px;"></div>', - ' <div id="two" style="height: 100px;"></div>', - ' <div id="three" style="height: 100px;"></div>', - ' <div id="spacer" style="height: 100px;"></div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: Manipulator.position(contentEl).top - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = 0 - } else { - expect(active).toBeNull() - done() - } + it('should clear selection if above the first section', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="header" style="height: 500px;"></div>', + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">', + ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', + ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', + ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', + ' </ul>', + '</nav>', + '<div id="content" style="height: 200px; overflow-y: auto;">', + ' <div id="spacer" style="height: 100px;"></div>', + ' <div id="one" style="height: 100px;"></div>', + ' <div id="two" style="height: 100px;"></div>', + ' <div id="three" style="height: 100px;"></div>', + ' <div id="spacer" style="height: 100px;"></div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: Manipulator.position(contentEl).top + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + let firstTime = true + + contentEl.addEventListener('scroll', () => { + const active = fixtureEl.querySelector('.active') + + expect(spy).toHaveBeenCalled() + spy.calls.reset() + if (firstTime) { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('two-link') + firstTime = false + contentEl.scrollTop = 0 + } else { + expect(active).toBeNull() + resolve() + } + }) + + contentEl.scrollTop = 201 }) - - contentEl.scrollTop = 201 }) - it('should not clear selection if above the first section and first section is at the top', done => { - fixtureEl.innerHTML = [ - '<div id="header" style="height: 500px;"></div>', - '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', - ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', - ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', - ' </ul>', - '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="one" style="height: 100px;"></div>', - ' <div id="two" style="height: 100px;"></div>', - ' <div id="three" style="height: 100px;"></div>', - ' <div id="spacer" style="height: 100px;"></div>', - '</div>' - ].join('') - - const negativeHeight = -10 - const startOfSectionTwo = 101 - const contentEl = fixtureEl.querySelector('#content') - const scrollSpy = new ScrollSpy(contentEl, { - target: '#navigation', - offset: contentEl.offsetTop - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - let firstTime = true - - contentEl.addEventListener('scroll', () => { - const active = fixtureEl.querySelector('.active') - - expect(spy).toHaveBeenCalled() - spy.calls.reset() - if (firstTime) { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = negativeHeight - } else { - expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) - expect(active.getAttribute('id')).toEqual('one-link') - done() - } + it('should not clear selection if above the first section and first section is at the top', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="header" style="height: 500px;"></div>', + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">', + ' <li class="nav-item"><a id="one-link" class="nav-link active" href="#one">One</a></li>', + ' <li class="nav-item"><a id="two-link" class="nav-link" href="#two">Two</a></li>', + ' <li class="nav-item"><a id="three-link" class="nav-link" href="#three">Three</a></li>', + ' </ul>', + '</nav>', + '<div id="content" style="height: 200px; overflow-y: auto;">', + ' <div id="one" style="height: 100px;"></div>', + ' <div id="two" style="height: 100px;"></div>', + ' <div id="three" style="height: 100px;"></div>', + ' <div id="spacer" style="height: 100px;"></div>', + '</div>' + ].join('') + + const negativeHeight = -10 + const startOfSectionTwo = 101 + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: contentEl.offsetTop + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + let firstTime = true + + contentEl.addEventListener('scroll', () => { + const active = fixtureEl.querySelector('.active') + + expect(spy).toHaveBeenCalled() + spy.calls.reset() + if (firstTime) { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('two-link') + firstTime = false + contentEl.scrollTop = negativeHeight + } else { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active.getAttribute('id')).toEqual('one-link') + resolve() + } + }) + + contentEl.scrollTop = startOfSectionTwo }) - - contentEl.scrollTop = startOfSectionTwo }) - it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>', - ' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>', - ' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>', - ' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>', - ' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>', - ' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>', - ' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const scrollSpy = new ScrollSpy(contentEl, { - offset: 0, - target: '.navbar' - }) - const spy = spyOn(scrollSpy, '_process').and.callThrough() - - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-5', - targetSelector: '#div-100-5', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-4', - targetSelector: '#div-100-4', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-3', - targetSelector: '#div-100-3', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-2', - targetSelector: '#div-100-2', - scrollSpy, - spy, - contentEl, - cb() { - contentEl.scrollTop = 0 - testElementIsActiveAfterScroll({ - elementSelector: '#li-100-1', - targetSelector: '#div-100-1', - scrollSpy, - spy, - contentEl, - cb: done - }) - } - }) - } - }) - } - }) - } + it('should correctly select navigation element on backward scrolling when each target section height is 100%', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="navbar">', + ' <ul class="nav">', + ' <li class="nav-item"><a id="li-100-1" class="nav-link" href="#div-100-1">div 1</a></li>', + ' <li class="nav-item"><a id="li-100-2" class="nav-link" href="#div-100-2">div 2</a></li>', + ' <li class="nav-item"><a id="li-100-3" class="nav-link" href="#div-100-3">div 3</a></li>', + ' <li class="nav-item"><a id="li-100-4" class="nav-link" href="#div-100-4">div 4</a></li>', + ' <li class="nav-item"><a id="li-100-5" class="nav-link" href="#div-100-5">div 5</a></li>', + ' </ul>', + '</nav>', + '<div class="content" style="position: relative; overflow: auto; height: 100px">', + ' <div id="div-100-1" style="position: relative; height: 100%; padding: 0; margin: 0">div 1</div>', + ' <div id="div-100-2" style="position: relative; height: 100%; padding: 0; margin: 0">div 2</div>', + ' <div id="div-100-3" style="position: relative; height: 100%; padding: 0; margin: 0">div 3</div>', + ' <div id="div-100-4" style="position: relative; height: 100%; padding: 0; margin: 0">div 4</div>', + ' <div id="div-100-5" style="position: relative; height: 100%; padding: 0; margin: 0">div 5</div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(contentEl, { + offset: 0, + target: '.navbar' + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-5', + targetSelector: '#div-100-5', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-4', + targetSelector: '#div-100-4', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-3', + targetSelector: '#div-100-3', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-2', + targetSelector: '#div-100-2', + scrollSpy, + spy, + contentEl, + cb() { + contentEl.scrollTop = 0 + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-1', + targetSelector: '#div-100-1', + scrollSpy, + spy, + contentEl, + cb: resolve + }) + } + }) + } + }) + } + }) + } + }) }) }) diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index bafa085526..43adee53b2 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -1,5 +1,5 @@ import Tab from '../../src/tab' -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' describe('Tab', () => { let fixtureEl @@ -39,314 +39,336 @@ describe('Tab', () => { }) describe('show', () => { - it('should activate element by tab id (using buttons, the preferred semantic way)', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>', - '</ul>', - '<ul>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ul>' - ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + it('should activate element by tab id (using buttons, the preferred semantic way)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>', + '<ul>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ul>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + tab.show() }) - - tab.show() }) - it('should activate element by tab id (using links for tabs - not ideal, but still supported)', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><a href="#home" role="tab">Home</a></li>', - ' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>', - '</ul>', - '<ul>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ul>' - ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + it('should activate element by tab id (using links for tabs - not ideal, but still supported)', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><a href="#home" role="tab">Home</a></li>', + ' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>', + '</ul>', + '<ul>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ul>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + tab.show() }) - - tab.show() }) - it('should activate element by tab id in ordered list', done => { - fixtureEl.innerHTML = [ - '<ol class="nav nav-pills">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>', - '</ol>', - '<ol>', - ' <li id="home" role="tabpanel"></li>', - ' <li id="profile" role="tabpanel"></li>', - '</ol>' - ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + it('should activate element by tab id in ordered list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ol class="nav nav-pills">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>', + '</ol>', + '<ol>', + ' <li id="home" role="tabpanel"></li>', + ' <li id="profile" role="tabpanel"></li>', + '</ol>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + tab.show() }) - - tab.show() }) - it('should activate element by tab id in nav list', done => { - fixtureEl.innerHTML = [ - '<nav class="nav">', - ' <button type="button" data-bs-target="#home" role="tab">Home</button>', - ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', - '</nav>', - '<div>', - ' <div id="home" role="tabpanel"></div>', - ' <div id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + it('should activate element by tab id in nav list', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="nav">', + ' <button type="button" data-bs-target="#home" role="tab">Home</button>', + ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', + '</nav>', + '<div>', + ' <div id="home" role="tabpanel"></div>', + ' <div id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + tab.show() }) - - tab.show() }) - it('should activate element by tab id in list group', done => { - fixtureEl.innerHTML = [ - '<div class="list-group" role="tablist">', - ' <button type="button" data-bs-target="#home" role="tab">Home</button>', - ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', - '</div>', - '<div>', - ' <div id="home" role="tabpanel"></div>', - ' <div id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + it('should activate element by tab id in list group', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="list-group" role="tablist">', + ' <button type="button" data-bs-target="#home" role="tab">Home</button>', + ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>', + '</div>', + '<div>', + ' <div id="home" role="tabpanel"></div>', + ' <div id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + tab.show() }) - - tab.show() }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '<div class="nav"></div>' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="nav"></div>' - const navEl = fixtureEl.querySelector('div') - const tab = new Tab(navEl) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + const navEl = fixtureEl.querySelector('div') + const tab = new Tab(navEl) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } - navEl.addEventListener('show.bs.tab', ev => { - ev.preventDefault() - expectDone() - }) + navEl.addEventListener('show.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) - navEl.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) + navEl.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) - tab.show() + tab.show() + }) }) - it('should not fire shown when tab is already active', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane active" id="home" role="tabpanel"></div>', - ' <div class="tab-pane" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const triggerActive = fixtureEl.querySelector('button.active') - const tab = new Tab(triggerActive) + it('should not fire shown when tab is already active', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerActive = fixtureEl.querySelector('button.active') + const tab = new Tab(triggerActive) + + triggerActive.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) - triggerActive.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + tab.show() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) }) - - tab.show() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) - it('show and shown events should reference correct relatedTarget', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane active" id="home" role="tabpanel"></div>', - ' <div class="tab-pane" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') - const secondTab = new Tab(secondTabTrigger) + it('show and shown events should reference correct relatedTarget', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + const secondTab = new Tab(secondTabTrigger) + + secondTabTrigger.addEventListener('show.bs.tab', ev => { + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') + }) - secondTabTrigger.addEventListener('show.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - }) + secondTabTrigger.addEventListener('shown.bs.tab', ev => { + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') + expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') + expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') + resolve() + }) - secondTabTrigger.addEventListener('shown.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true') - expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false') - done() + secondTab.show() }) - - secondTab.show() }) - it('should fire hide and hidden events', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" data-bs-target="#profile">Profile</button></li>', - '</ul>' - ].join('') + it('should fire hide and hidden events', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" data-bs-target="#profile">Profile</button></li>', + '</ul>' + ].join('') - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) + const triggerList = fixtureEl.querySelectorAll('button') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) - let hideCalled = false - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + let hideCalled = false + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) - triggerList[0].addEventListener('hide.bs.tab', ev => { - hideCalled = true - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - }) + triggerList[0].addEventListener('hide.bs.tab', ev => { + hideCalled = true + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') + }) - triggerList[0].addEventListener('hidden.bs.tab', ev => { - expect(hideCalled).toBeTrue() - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - done() - }) + triggerList[0].addEventListener('hidden.bs.tab', ev => { + expect(hideCalled).toBeTrue() + expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') + resolve() + }) - firstTab.show() + firstTab.show() + }) }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', - ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>', - '</ul>' - ].join('') - - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>', + ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>', + '</ul>' + ].join('') + + const triggerList = fixtureEl.querySelectorAll('button') + const firstTab = new Tab(triggerList[0]) + const secondTab = new Tab(triggerList[1]) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } + + triggerList[0].addEventListener('shown.bs.tab', () => { + secondTab.show() + }) - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + triggerList[0].addEventListener('hide.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) - triggerList[0].addEventListener('hide.bs.tab', ev => { - ev.preventDefault() - expectDone() - }) + triggerList[0].addEventListener('hidden.bs.tab', () => { + throw new Error('should not trigger hidden') + }) - triggerList[0].addEventListener('hidden.bs.tab', () => { - throw new Error('should not trigger hidden') + firstTab.show() }) - - firstTab.show() }) - it('should handle removed tabs', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation">', - ' <a class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">', - ' <button class="btn-close" aria-label="Close"></button>', - ' </a>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <a id="secondNav" class="nav-link nav-tab" href="#buzz" role="tab" data-bs-toggle="tab">', - ' <button class="btn-close" aria-label="Close"></button>', - ' </a>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <a class="nav-link nav-tab" href="#references" role="tab" data-bs-toggle="tab">', - ' <button id="btnClose" class="btn-close" aria-label="Close"></button>', - ' </a>', - ' </li>', - '</ul>', - '<div class="tab-content">', - ' <div role="tabpanel" class="tab-pane fade show active" id="profile">test 1</div>', - ' <div role="tabpanel" class="tab-pane fade" id="buzz">test 2</div>', - ' <div role="tabpanel" class="tab-pane fade" id="references">test 3</div>', - '</div>' - ].join('') - - const secondNavEl = fixtureEl.querySelector('#secondNav') - const btnCloseEl = fixtureEl.querySelector('#btnClose') - const secondNavTab = new Tab(secondNavEl) + it('should handle removed tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <a class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">', + ' <button class="btn-close" aria-label="Close"></button>', + ' </a>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <a id="secondNav" class="nav-link nav-tab" href="#buzz" role="tab" data-bs-toggle="tab">', + ' <button class="btn-close" aria-label="Close"></button>', + ' </a>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <a class="nav-link nav-tab" href="#references" role="tab" data-bs-toggle="tab">', + ' <button id="btnClose" class="btn-close" aria-label="Close"></button>', + ' </a>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane fade show active" id="profile">test 1</div>', + ' <div role="tabpanel" class="tab-pane fade" id="buzz">test 2</div>', + ' <div role="tabpanel" class="tab-pane fade" id="references">test 3</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + const btnCloseEl = fixtureEl.querySelector('#btnClose') + const secondNavTab = new Tab(secondNavEl) + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.nav-tab')).toHaveSize(2) + resolve() + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.nav-tab')).toHaveSize(2) - done() - }) + btnCloseEl.addEventListener('click', () => { + const linkEl = btnCloseEl.parentNode + const liEl = linkEl.parentNode + const tabId = linkEl.getAttribute('href') + const tabIdEl = fixtureEl.querySelector(tabId) - btnCloseEl.addEventListener('click', () => { - const linkEl = btnCloseEl.parentNode - const liEl = linkEl.parentNode - const tabId = linkEl.getAttribute('href') - const tabIdEl = fixtureEl.querySelector(tabId) + liEl.remove() + tabIdEl.remove() + secondNavTab.show() + }) - liEl.remove() - tabIdEl.remove() - secondNavTab.show() + btnCloseEl.click() }) - - btnCloseEl.click() }) }) @@ -464,27 +486,29 @@ describe('Tab', () => { }) describe('data-api', () => { - it('should create dynamically a tab', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane active" id="home" role="tabpanel"></div>', - ' <div class="tab-pane" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + it('should create dynamically a tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') + + secondTabTrigger.addEventListener('shown.bs.tab', () => { + expect(secondTabTrigger).toHaveClass('active') + expect(fixtureEl.querySelector('#profile')).toHaveClass('active') + resolve() + }) - secondTabTrigger.addEventListener('shown.bs.tab', () => { - expect(secondTabTrigger).toHaveClass('active') - expect(fixtureEl.querySelector('#profile')).toHaveClass('active') - done() + secondTabTrigger.click() }) - - secondTabTrigger.click() }) it('selected tab should deactivate previous selected link in dropdown', () => { @@ -567,202 +591,216 @@ describe('Tab', () => { expect(fixtureEl.querySelector('.nav-link')).not.toHaveClass('active') }) - it('should handle nested tabs', done => { - fixtureEl.innerHTML = [ - '<nav class="nav nav-tabs" role="tablist">', - ' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>', - ' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>', - ' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>', - '</nav>', - '<div class="tab-content">', - ' <div class="tab-pane" id="x-tab1" role="tabpanel">', - ' <nav class="nav nav-tabs" role="tablist">', - ' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>', - ' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>', - ' </nav>', - ' <div class="tab-content">', - ' <div class="tab-pane active" id="nested-tab1" role="tabpanel">Nested Tab1 Content</div>', - ' <div class="tab-pane" id="nested-tab2" role="tabpanel">Nested Tab2 Content</div>', - ' </div>', - ' </div>', - ' <div class="tab-pane active" id="x-tab2" role="tabpanel">Tab2 Content</div>', - ' <div class="tab-pane" id="x-tab3" role="tabpanel">Tab3 Content</div>', - '</div>' - ].join('') - - const tab1El = fixtureEl.querySelector('#tab1') - const tabNested2El = fixtureEl.querySelector('#tabNested2') - const xTab1El = fixtureEl.querySelector('#x-tab1') + it('should handle nested tabs', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<nav class="nav nav-tabs" role="tablist">', + ' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>', + ' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>', + ' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>', + '</nav>', + '<div class="tab-content">', + ' <div class="tab-pane" id="x-tab1" role="tabpanel">', + ' <nav class="nav nav-tabs" role="tablist">', + ' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>', + ' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>', + ' </nav>', + ' <div class="tab-content">', + ' <div class="tab-pane active" id="nested-tab1" role="tabpanel">Nested Tab1 Content</div>', + ' <div class="tab-pane" id="nested-tab2" role="tabpanel">Nested Tab2 Content</div>', + ' </div>', + ' </div>', + ' <div class="tab-pane active" id="x-tab2" role="tabpanel">Tab2 Content</div>', + ' <div class="tab-pane" id="x-tab3" role="tabpanel">Tab3 Content</div>', + '</div>' + ].join('') + + const tab1El = fixtureEl.querySelector('#tab1') + const tabNested2El = fixtureEl.querySelector('#tabNested2') + const xTab1El = fixtureEl.querySelector('#x-tab1') + + tabNested2El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + resolve() + }) - tabNested2El.addEventListener('shown.bs.tab', () => { - expect(xTab1El).toHaveClass('active') - done() - }) + tab1El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + tabNested2El.click() + }) - tab1El.addEventListener('shown.bs.tab', () => { - expect(xTab1El).toHaveClass('active') - tabNested2El.click() + tab1El.click() }) - - tab1El.click() }) - it('should not remove fade class if no active pane is present', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane fade" id="home" role="tabpanel"></div>', - ' <div class="tab-pane fade" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') - - const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') - const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') - const tabProfileEl = fixtureEl.querySelector('#profile') - const tabHomeEl = fixtureEl.querySelector('#home') + it('should not remove fade class if no active pane is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane fade" id="home" role="tabpanel"></div>', + ' <div class="tab-pane fade" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerTabProfileEl = fixtureEl.querySelector('#tab-profile') + const triggerTabHomeEl = fixtureEl.querySelector('#tab-home') + const tabProfileEl = fixtureEl.querySelector('#profile') + const tabHomeEl = fixtureEl.querySelector('#home') + + triggerTabProfileEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).toHaveClass('show') - triggerTabProfileEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl).toHaveClass('fade') - expect(tabProfileEl).toHaveClass('show') + triggerTabHomeEl.addEventListener('shown.bs.tab', () => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).not.toHaveClass('show') - triggerTabHomeEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl).toHaveClass('fade') - expect(tabProfileEl).not.toHaveClass('show') + expect(tabHomeEl).toHaveClass('fade') + expect(tabHomeEl).toHaveClass('show') - expect(tabHomeEl).toHaveClass('fade') - expect(tabHomeEl).toHaveClass('show') + resolve() + }) - done() + triggerTabHomeEl.click() }) - triggerTabHomeEl.click() + triggerTabProfileEl.click() }) - - triggerTabProfileEl.click() }) - it('should not add show class to tab panes if there is no `.fade` class', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation">', - ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', - ' </li>', - '</ul>', - '<div class="tab-content">', - ' <div role="tabpanel" class="tab-pane" id="home">test 1</div>', - ' <div role="tabpanel" class="tab-pane" id="profile">test 2</div>', - '</div>' - ].join('') - - const secondNavEl = fixtureEl.querySelector('#secondNav') + it('should not add show class to tab panes if there is no `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane" id="home">test 1</div>', + ' <div role="tabpanel" class="tab-pane" id="profile">test 2</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(0) + resolve() + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show')).toHaveSize(0) - done() + secondNavEl.click() }) - - secondNavEl.click() }) - it('should add show class to tab panes if there is a `.fade` class', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation">', - ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', - ' </li>', - ' <li class="nav-item" role="presentation">', - ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', - ' </li>', - '</ul>', - '<div class="tab-content">', - ' <div role="tabpanel" class="tab-pane fade" id="home">test 1</div>', - ' <div role="tabpanel" class="tab-pane fade" id="profile">test 2</div>', - '</div>' - ].join('') - - const secondNavEl = fixtureEl.querySelector('#secondNav') + it('should add show class to tab panes if there is a `.fade` class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation">', + ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>', + ' </li>', + ' <li class="nav-item" role="presentation">', + ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>', + ' </li>', + '</ul>', + '<div class="tab-content">', + ' <div role="tabpanel" class="tab-pane fade" id="home">test 1</div>', + ' <div role="tabpanel" class="tab-pane fade" id="profile">test 2</div>', + '</div>' + ].join('') + + const secondNavEl = fixtureEl.querySelector('#secondNav') + + secondNavEl.addEventListener('shown.bs.tab', () => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) + resolve() + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) - done() + secondNavEl.click() }) - - secondNavEl.click() }) - it('should prevent default when the trigger is <a> or <area>', done => { - fixtureEl.innerHTML = [ - '<ul class="nav" role="tablist">', - ' <li><a type="button" href="#test" class="active" role="tab" data-bs-toggle="tab">Home</a></li>', - ' <li><a type="button" href="#test2" role="tab" data-bs-toggle="tab">Home</a></li>', - '</ul>' - ].join('') + it('should prevent default when the trigger is <a> or <area>', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><a type="button" href="#test" class="active" role="tab" data-bs-toggle="tab">Home</a></li>', + ' <li><a type="button" href="#test2" role="tab" data-bs-toggle="tab">Home</a></li>', + '</ul>' + ].join('') - const tabEl = fixtureEl.querySelector('[href="#test2"]') - spyOn(Event.prototype, 'preventDefault').and.callThrough() + const tabEl = fixtureEl.querySelector('[href="#test2"]') + spyOn(Event.prototype, 'preventDefault').and.callThrough() - tabEl.addEventListener('shown.bs.tab', () => { - expect(tabEl).toHaveClass('active') - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() - }) + tabEl.addEventListener('shown.bs.tab', () => { + expect(tabEl).toHaveClass('active') + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - tabEl.click() + tabEl.click() + }) }) - it('should not fire shown when tab has disabled attribute', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', - ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" disabled role="tab">Profile</button></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane active" id="home" role="tabpanel"></div>', - ' <div class="tab-pane" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') + it('should not fire shown when tab has disabled attribute', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>', + ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" disabled role="tab">Profile</button></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') + + const triggerDisabled = fixtureEl.querySelector('button[disabled]') + triggerDisabled.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) - const triggerDisabled = fixtureEl.querySelector('button[disabled]') - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) }) - - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) - it('should not fire shown when tab has disabled class', done => { - fixtureEl.innerHTML = [ - '<ul class="nav nav-tabs" role="tablist">', - ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab" aria-selected="true">Home</a></li>', - ' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>', - '</ul>', - '<div class="tab-content">', - ' <div class="tab-pane active" id="home" role="tabpanel"></div>', - ' <div class="tab-pane" id="profile" role="tabpanel"></div>', - '</div>' - ].join('') + it('should not fire shown when tab has disabled class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav nav-tabs" role="tablist">', + ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab" aria-selected="true">Home</a></li>', + ' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>', + '</ul>', + '<div class="tab-content">', + ' <div class="tab-pane active" id="home" role="tabpanel"></div>', + ' <div class="tab-pane" id="profile" role="tabpanel"></div>', + '</div>' + ].join('') - const triggerDisabled = fixtureEl.querySelector('a.disabled') + const triggerDisabled = fixtureEl.querySelector('a.disabled') - triggerDisabled.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) + triggerDisabled.addEventListener('shown.bs.tab', () => { + throw new Error('should not trigger shown event') + }) - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) + triggerDisabled.click() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + }) }) }) }) diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js index c4ea43808c..9134a8410d 100644 --- a/js/tests/unit/toast.spec.js +++ b/js/tests/unit/toast.spec.js @@ -1,5 +1,5 @@ import Toast from '../../src/toast' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' describe('Toast', () => { let fixtureEl @@ -36,52 +36,56 @@ describe('Toast', () => { expect(toastByElement._element).toEqual(toastEl) }) - it('should allow to config in js', done => { - fixtureEl.innerHTML = [ - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') + it('should allow to config in js', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl, { + delay: 1 + }) - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl, { - delay: 1 - }) + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).toHaveClass('show') + resolve() + }) - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') - done() + toast.show() }) - - toast.show() }) - it('should close toast when close element with data-bs-dismiss attribute is set', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">', - ' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>', - '</div>' - ].join('') + it('should close toast when close element with data-bs-dismiss attribute is set', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1" data-bs-autohide="false" data-bs-animation="false">', + ' <button type="button" class="ms-2 mb-1 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>', + '</div>' + ].join('') - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl) - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).toHaveClass('show') + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).toHaveClass('show') - const button = toastEl.querySelector('.btn-close') + const button = toastEl.querySelector('.btn-close') - button.click() - }) + button.click() + }) - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() - }) + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) - toast.show() + toast.show() + }) }) }) @@ -111,304 +115,324 @@ describe('Toast', () => { }) describe('show', () => { - it('should auto hide', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() - }) - - toast.show() - }) - - it('should not add fade class', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1" data-bs-animation="false">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + it('should auto hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) - toastEl.addEventListener('shown.bs.toast', () => { - expect(toastEl).not.toHaveClass('fade') - done() + toast.show() }) - - toast.show() }) - it('should not trigger shown if show is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1" data-bs-animation="false">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') + it('should not add fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1" data-bs-animation="false">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - const assertDone = () => { - setTimeout(() => { - expect(toastEl).not.toHaveClass('show') - done() - }, 20) - } + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - toastEl.addEventListener('show.bs.toast', event => { - event.preventDefault() - assertDone() - }) + toastEl.addEventListener('shown.bs.toast', () => { + expect(toastEl).not.toHaveClass('fade') + resolve() + }) - toastEl.addEventListener('shown.bs.toast', () => { - throw new Error('shown event should not be triggered if show is prevented') + toast.show() }) - - toast.show() }) - it('should clear timeout if toast is shown again before it is hidden', done => { - fixtureEl.innerHTML = [ - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + it('should not trigger shown if show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1" data-bs-animation="false">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + const assertDone = () => { + setTimeout(() => { + expect(toastEl).not.toHaveClass('show') + resolve() + }, 20) + } + + toastEl.addEventListener('show.bs.toast', event => { + event.preventDefault() + assertDone() + }) - setTimeout(() => { - toast._config.autohide = false toastEl.addEventListener('shown.bs.toast', () => { - expect(toast._clearTimeout).toHaveBeenCalled() - expect(toast._timeout).toBeNull() - done() + throw new Error('shown event should not be triggered if show is prevented') }) - toast.show() - }, toast._config.delay / 2) - spyOn(toast, '_clearTimeout').and.callThrough() - - toast.show() + toast.show() + }) }) - it('should clear timeout if toast is interacted with mouse', done => { - fixtureEl.innerHTML = [ - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() + it('should clear timeout if toast is shown again before it is hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') - setTimeout(() => { - spy.calls.reset() + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - toastEl.addEventListener('mouseover', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - done() - }) + setTimeout(() => { + toast._config.autohide = false + toastEl.addEventListener('shown.bs.toast', () => { + expect(toast._clearTimeout).toHaveBeenCalled() + expect(toast._timeout).toBeNull() + resolve() + }) + toast.show() + }, toast._config.delay / 2) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + spyOn(toast, '_clearTimeout').and.callThrough() - toast.show() + toast.show() + }) }) - it('should clear timeout if toast is interacted with keyboard', done => { - fixtureEl.innerHTML = [ - '<button id="outside-focusable">outside focusable</button>', - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' <button>with a button</button>', - ' </div>', - '</div>' - ].join('') + it('should clear timeout if toast is interacted with mouse', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - const spy = spyOn(toast, '_clearTimeout').and.callThrough() + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const spy = spyOn(toast, '_clearTimeout').and.callThrough() - setTimeout(() => { - spy.calls.reset() + setTimeout(() => { + spy.calls.reset() - toastEl.addEventListener('focusin', () => { - expect(toast._clearTimeout).toHaveBeenCalledTimes(1) - expect(toast._timeout).toBeNull() - done() - }) + toastEl.addEventListener('mouseover', () => { + expect(toast._clearTimeout).toHaveBeenCalledTimes(1) + expect(toast._timeout).toBeNull() + resolve() + }) - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }, toast._config.delay / 2) + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) - toast.show() + toast.show() + }) }) - it('should still auto hide after being interacted with mouse and keyboard', done => { - fixtureEl.innerHTML = [ - '<button id="outside-focusable">outside focusable</button>', - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' <button>with a button</button>', - ' </div>', - '</div>' - ].join('') + it('should clear timeout if toast is interacted with keyboard', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="outside-focusable">outside focusable</button>', + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' <button>with a button</button>', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + const spy = spyOn(toast, '_clearTimeout').and.callThrough() - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + setTimeout(() => { + spy.calls.reset() + + toastEl.addEventListener('focusin', () => { + expect(toast._clearTimeout).toHaveBeenCalledTimes(1) + expect(toast._timeout).toBeNull() + resolve() + }) - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { const insideFocusable = toastEl.querySelector('button') insideFocusable.focus() - }) + }, toast._config.delay / 2) - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) + toast.show() + }) + }) - toastEl.addEventListener('mouseout', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) + it('should still auto hide after being interacted with mouse and keyboard', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="outside-focusable">outside focusable</button>', + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' <button>with a button</button>', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).not.toBeNull() - done() - }) - - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) + + toastEl.addEventListener('focusin', () => { + const mouseOutEvent = createEvent('mouseout') + toastEl.dispatchEvent(mouseOutEvent) + }) + + toastEl.addEventListener('mouseout', () => { + const outsideFocusable = document.getElementById('outside-focusable') + outsideFocusable.focus() + }) + + toastEl.addEventListener('focusout', () => { + expect(toast._timeout).not.toBeNull() + resolve() + }) + + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) - toast.show() + toast.show() + }) }) - it('should not auto hide if focus leaves but mouse pointer remains inside', done => { - fixtureEl.innerHTML = [ - '<button id="outside-focusable">outside focusable</button>', - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' <button>with a button</button>', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + it('should not auto hide if focus leaves but mouse pointer remains inside', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="outside-focusable">outside focusable</button>', + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' <button>with a button</button>', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) - toastEl.addEventListener('focusin', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) + toastEl.addEventListener('focusin', () => { + const outsideFocusable = document.getElementById('outside-focusable') + outsideFocusable.focus() + }) - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).toBeNull() - done() - }) + toastEl.addEventListener('focusout', () => { + expect(toast._timeout).toBeNull() + resolve() + }) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) - toast.show() + toast.show() + }) }) - it('should not auto hide if mouse pointer leaves but focus remains inside', done => { - fixtureEl.innerHTML = [ - '<button id="outside-focusable">outside focusable</button>', - '<div class="toast">', - ' <div class="toast-body">', - ' a simple toast', - ' <button>with a button</button>', - ' </div>', - '</div>' - ].join('') + it('should not auto hide if mouse pointer leaves but focus remains inside', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button id="outside-focusable">outside focusable</button>', + '<div class="toast">', + ' <div class="toast-body">', + ' a simple toast', + ' <button>with a button</button>', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - setTimeout(() => { - toastEl.addEventListener('mouseover', () => { - const insideFocusable = toastEl.querySelector('button') - insideFocusable.focus() - }) + setTimeout(() => { + toastEl.addEventListener('mouseover', () => { + const insideFocusable = toastEl.querySelector('button') + insideFocusable.focus() + }) - toastEl.addEventListener('focusin', () => { - const mouseOutEvent = createEvent('mouseout') - toastEl.dispatchEvent(mouseOutEvent) - }) + toastEl.addEventListener('focusin', () => { + const mouseOutEvent = createEvent('mouseout') + toastEl.dispatchEvent(mouseOutEvent) + }) - toastEl.addEventListener('mouseout', () => { - expect(toast._timeout).toBeNull() - done() - }) + toastEl.addEventListener('mouseout', () => { + expect(toast._timeout).toBeNull() + resolve() + }) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + const mouseOverEvent = createEvent('mouseover') + toastEl.dispatchEvent(mouseOverEvent) + }, toast._config.delay / 2) - toast.show() + toast.show() + }) }) }) describe('hide', () => { - it('should allow to hide toast manually', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1" data-bs-autohide="false">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') + it('should allow to hide toast manually', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1" data-bs-autohide="false">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + toastEl.addEventListener('shown.bs.toast', () => { + toast.hide() + }) - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() - }) + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) - toastEl.addEventListener('hidden.bs.toast', () => { - expect(toastEl).not.toHaveClass('show') - done() + toast.show() }) - - toast.show() }) it('should do nothing when we call hide on a non shown toast', () => { @@ -424,39 +448,41 @@ describe('Toast', () => { expect(toastEl.classList.contains).toHaveBeenCalled() }) - it('should not trigger hidden if hide is prevented', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="1" data-bs-animation="false">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + it('should not trigger hidden if hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="1" data-bs-animation="false">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) + + const assertDone = () => { + setTimeout(() => { + expect(toastEl).toHaveClass('show') + resolve() + }, 20) + } - const assertDone = () => { - setTimeout(() => { - expect(toastEl).toHaveClass('show') - done() - }, 20) - } + toastEl.addEventListener('shown.bs.toast', () => { + toast.hide() + }) - toastEl.addEventListener('shown.bs.toast', () => { - toast.hide() - }) + toastEl.addEventListener('hide.bs.toast', event => { + event.preventDefault() + assertDone() + }) - toastEl.addEventListener('hide.bs.toast', event => { - event.preventDefault() - assertDone() - }) + toastEl.addEventListener('hidden.bs.toast', () => { + throw new Error('hidden event should not be triggered if hide is prevented') + }) - toastEl.addEventListener('hidden.bs.toast', () => { - throw new Error('hidden event should not be triggered if hide is prevented') + toast.show() }) - - toast.show() }) }) @@ -475,34 +501,36 @@ describe('Toast', () => { expect(Toast.getInstance(toastEl)).toBeNull() }) - it('should allow to destroy toast and hide it before that', done => { - fixtureEl.innerHTML = [ - '<div class="toast" data-bs-delay="0" data-bs-autohide="false">', - ' <div class="toast-body">', - ' a simple toast', - ' </div>', - '</div>' - ].join('') + it('should allow to destroy toast and hide it before that', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="toast" data-bs-delay="0" data-bs-autohide="false">', + ' <div class="toast-body">', + ' a simple toast', + ' </div>', + '</div>' + ].join('') + + const toastEl = fixtureEl.querySelector('div') + const toast = new Toast(toastEl) + const expected = () => { + expect(toastEl).toHaveClass('show') + expect(Toast.getInstance(toastEl)).not.toBeNull() - const toastEl = fixtureEl.querySelector('div') - const toast = new Toast(toastEl) - const expected = () => { - expect(toastEl).toHaveClass('show') - expect(Toast.getInstance(toastEl)).not.toBeNull() + toast.dispose() - toast.dispose() + expect(Toast.getInstance(toastEl)).toBeNull() + expect(toastEl).not.toHaveClass('show') - expect(Toast.getInstance(toastEl)).toBeNull() - expect(toastEl).not.toHaveClass('show') + resolve() + } - done() - } + toastEl.addEventListener('shown.bs.toast', () => { + setTimeout(expected, 1) + }) - toastEl.addEventListener('shown.bs.toast', () => { - setTimeout(expected, 1) + toast.show() }) - - toast.show() }) }) diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 3feacf7b44..4a9a6d3bb0 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -94,52 +94,56 @@ describe('Tooltip', () => { expect(tooltip._config.content).toEqual('7') }) - it('should enable selector delegation', done => { - fixtureEl.innerHTML = '<div></div>' + it('should enable selector delegation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' + + const containerEl = fixtureEl.querySelector('div') + const tooltipContainer = new Tooltip(containerEl, { + selector: 'a[rel="tooltip"]', + trigger: 'click' + }) - const containerEl = fixtureEl.querySelector('div') - const tooltipContainer = new Tooltip(containerEl, { - selector: 'a[rel="tooltip"]', - trigger: 'click' - }) + containerEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - containerEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + const tooltipInContainerEl = containerEl.querySelector('a') - const tooltipInContainerEl = containerEl.querySelector('a') + tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + tooltipContainer.dispose() + resolve() + }) - tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - tooltipContainer.dispose() - done() + tooltipInContainerEl.click() }) - - tooltipInContainerEl.click() }) - it('should create offset modifier when offset is passed as a function', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Offset from function">' + it('should create offset modifier when offset is passed as a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Offset from function">' - const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - offset: getOffset, - popperConfig: { - onFirstUpdate: state => { - expect(getOffset).toHaveBeenCalledWith({ - popper: state.rects.popper, - reference: state.rects.reference, - placement: state.placement - }, tooltipEl) - done() + const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20]) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + offset: getOffset, + popperConfig: { + onFirstUpdate: state => { + expect(getOffset).toHaveBeenCalledWith({ + popper: state.rects.popper, + reference: state.rects.reference, + placement: state.placement + }, tooltipEl) + resolve() + } } - } - }) + }) - const offset = tooltip._getOffset() + const offset = tooltip._getOffset() - expect(offset).toEqual(jasmine.any(Function)) + expect(offset).toEqual(jasmine.any(Function)) - tooltip.show() + tooltip.show() + }) }) it('should create offset modifier when offset option is passed in data attribute', () => { @@ -192,42 +196,46 @@ describe('Tooltip', () => { }) describe('enable', () => { - it('should enable a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should enable a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.enable() + tooltip.enable() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) }) describe('disable', () => { - it('should disable tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should disable tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') - }) + tooltipEl.addEventListener('show.bs.tooltip', () => { + throw new Error('should not show a disabled tooltip') + }) - tooltip.show() + tooltip.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) }) @@ -247,96 +255,106 @@ describe('Tooltip', () => { }) describe('toggle', () => { - it('should do nothing if disabled', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should do nothing if disabled', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') - }) + tooltipEl.addEventListener('show.bs.tooltip', () => { + throw new Error('should not show a disabled tooltip') + }) - tooltip.toggle() + tooltip.toggle() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) - it('should show a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.toggle() + tooltip.toggle() + }) }) - it('should call toggle and show the tooltip when trigger is "click"', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should call toggle and show the tooltip when trigger is "click"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - trigger: 'click' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + trigger: 'click' + }) - spyOn(tooltip, 'toggle').and.callThrough() + spyOn(tooltip, 'toggle').and.callThrough() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltip.toggle).toHaveBeenCalled() + resolve() + }) - tooltipEl.click() + tooltipEl.click() + }) }) - it('should hide a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should hide a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltip.toggle() + }) + + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltip.toggle() }) + }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - done() - }) + it('should call toggle and hide the tooltip when trigger is "click"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - tooltip.toggle() - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + trigger: 'click' + }) - it('should call toggle and hide the tooltip when trigger is "click"', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + spyOn(tooltip, 'toggle').and.callThrough() - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - trigger: 'click' - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltipEl.click() + }) - spyOn(tooltip, 'toggle').and.callThrough() + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(tooltip.toggle).toHaveBeenCalled() + resolve() + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipEl.click() }) - - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() - }) - - tooltipEl.click() }) }) @@ -367,239 +385,263 @@ describe('Tooltip', () => { expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs) }) - it('should destroy a tooltip after it is shown and hidden', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should destroy a tooltip after it is shown and hidden', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - tooltip.hide() - }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - tooltip.dispose() - expect(tooltip.tip).toBeNull() - expect(Tooltip.getInstance(tooltipEl)).toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltip.hide() + }) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + tooltip.dispose() + expect(tooltip.tip).toBeNull() + expect(Tooltip.getInstance(tooltipEl)).toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should destroy a tooltip and remove it from the dom', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should destroy a tooltip and remove it from the dom', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() - tooltip.dispose() + tooltip.dispose() - expect(document.querySelector('.tooltip')).toBeNull() - done() - }) + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) }) describe('show', () => { - it('should show a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toEqual(tooltipShown.getAttribute('id')) - expect(tooltipShown.getAttribute('id')).toContain('tooltip') - done() - }) + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-describedby')).toEqual(tooltipShown.getAttribute('id')) + expect(tooltipShown.getAttribute('id')).toContain('tooltip') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip when hovering a child element', done => { - fixtureEl.innerHTML = [ - '<a href="#" rel="tooltip" title="Another tooltip">', - ' <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 100 100">', - ' <rect width="100%" fill="#563d7c"/>', - ' <circle cx="50" cy="50" r="30" fill="#fff"/>', - ' </svg>', - '</a>' - ].join('') + it('should show a tooltip when hovering a child element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" rel="tooltip" title="Another tooltip">', + ' <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 100 100">', + ' <rect width="100%" fill="#563d7c"/>', + ' <circle cx="50" cy="50" r="30" fill="#fff"/>', + ' </svg>', + '</a>' + ].join('') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - spyOn(tooltip, 'show') + spyOn(tooltip, 'show') - tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) + tooltipEl.querySelector('rect').dispatchEvent(createEvent('mouseover', { bubbles: true })) - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - done() - }, 0) + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + resolve() + }, 0) + }) }) - it('should show a tooltip on mobile', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - document.documentElement.ontouchstart = noop + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'on').and.callThrough() + spyOn(EventHandler, 'on').and.callThrough() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = undefined - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + document.documentElement.ontouchstart = undefined + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip relative to placement option', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip relative to placement option', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: 'bottom' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + placement: 'bottom' + }) - tooltipEl.addEventListener('inserted.bs.tooltip', () => { - expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') - }) + tooltipEl.addEventListener('inserted.bs.tooltip', () => { + expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') - expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom') - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') + expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('bottom') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should not error when trying to show a tooltip that has been removed from the dom', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should not error when trying to show a tooltip that has been removed from the dom', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - const firstCallback = () => { - tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) - let tooltipShown = document.querySelector('.tooltip') + const firstCallback = () => { + tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) + let tooltipShown = document.querySelector('.tooltip') - tooltipShown.remove() + tooltipShown.remove() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - done() - }) + expect(tooltipShown).not.toBeNull() + resolve() + }) - tooltip.show() - } + tooltip.show() + } - tooltipEl.addEventListener('shown.bs.tooltip', firstCallback) + tooltipEl.addEventListener('shown.bs.tooltip', firstCallback) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with a dom element container', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with a dom element container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: fixtureEl - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: fixtureEl + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with a jquery element container', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with a jquery element container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: { - 0: fixtureEl, - jquery: 'jQuery' - } - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: { + 0: fixtureEl, + jquery: 'jQuery' + } + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with a selector in container', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with a selector in container', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - container: '#fixture' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + container: '#fixture' + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(fixtureEl.querySelector('.tooltip')).not.toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with placement as a function', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with placement as a function', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const spy = jasmine.createSpy('placement').and.returnValue('top') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: spy - }) + const spy = jasmine.createSpy('placement').and.returnValue('top') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + placement: spy + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - expect(spy).toHaveBeenCalled() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + expect(spy).toHaveBeenCalled() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip without the animation', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip without the animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip).not.toHaveClass('fade') - done() - }) + expect(tip).not.toBeNull() + expect(tip).not.toHaveClass('fade') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) it('should throw an error the element is not visible', () => { @@ -615,352 +657,382 @@ describe('Tooltip', () => { } }) - it('should not show a tooltip if show.bs.tooltip is prevented', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - const expectedDone = () => { - setTimeout(() => { - expect(document.querySelector('.tooltip')).toBeNull() - done() - }, 10) - } + it('should not show a tooltip if show.bs.tooltip is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - tooltipEl.addEventListener('show.bs.tooltip', ev => { - ev.preventDefault() - expectedDone() - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - throw new Error('Tooltip should not be shown') - }) + const expectedDone = () => { + setTimeout(() => { + expect(document.querySelector('.tooltip')).toBeNull() + resolve() + }, 10) + } - tooltip.show() - }) + tooltipEl.addEventListener('show.bs.tooltip', ev => { + ev.preventDefault() + expectedDone() + }) - it('should show tooltip if leave event hasn\'t occurred before delay expires', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + tooltipEl.addEventListener('shown.bs.tooltip', () => { + throw new Error('Tooltip should not be shown') + }) - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: 150 + tooltip.show() }) - - spyOn(tooltip, 'show') - - setTimeout(() => { - expect(tooltip.show).not.toHaveBeenCalled() - }, 100) - - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - done() - }, 200) - - tooltipEl.dispatchEvent(createEvent('mouseover')) }) - it('should not show tooltip if leave event occurs before delay expires', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show tooltip if leave event hasn\'t occurred before delay expires', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: 150 - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: 150 + }) - spyOn(tooltip, 'show') + spyOn(tooltip, 'show') - setTimeout(() => { - expect(tooltip.show).not.toHaveBeenCalled() - tooltipEl.dispatchEvent(createEvent('mouseover')) - }, 100) + setTimeout(() => { + expect(tooltip.show).not.toHaveBeenCalled() + }, 100) - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - expect(document.querySelectorAll('.tooltip')).toHaveSize(0) - done() - }, 200) + setTimeout(() => { + expect(tooltip.show).toHaveBeenCalled() + resolve() + }, 200) - tooltipEl.dispatchEvent(createEvent('mouseover')) + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) }) - it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should not show tooltip if leave event occurs before delay expires', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - delay: { - show: 0, - hide: 150 - } - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: 150 + }) - setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') - tooltipEl.dispatchEvent(createEvent('mouseout')) + spyOn(tooltip, 'show') setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') + expect(tooltip.show).not.toHaveBeenCalled() tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) setTimeout(() => { - expect(tooltip._getTipElement()).toHaveClass('show') - expect(document.querySelectorAll('.tooltip')).toHaveSize(1) - done() + expect(tooltip.show).toHaveBeenCalled() + expect(document.querySelectorAll('.tooltip')).toHaveSize(0) + resolve() }, 200) - }, 10) - tooltipEl.dispatchEvent(createEvent('mouseover')) + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) }) - it('should not hide tooltip if leave event occurs and interaction remains inside trigger', done => { - fixtureEl.innerHTML = [ - '<a href="#" rel="tooltip" title="Another tooltip">', - '<b>Trigger</b>', - 'the tooltip', - '</a>' - ].join('') - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - const triggerChild = tooltipEl.querySelector('b') - - spyOn(tooltip, 'hide').and.callThrough() + it('should not hide tooltip if leave event occurs and enter event occurs within the hide delay', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - tooltipEl.addEventListener('mouseover', () => { - const moveMouseToChildEvent = createEvent('mouseout') - Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { - value: triggerChild + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + delay: { + show: 0, + hide: 150 + } }) - tooltipEl.dispatchEvent(moveMouseToChildEvent) - }) + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseout')) + + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseover')) + }, 100) + + setTimeout(() => { + expect(tooltip._getTipElement()).toHaveClass('show') + expect(document.querySelectorAll('.tooltip')).toHaveSize(1) + resolve() + }, 200) + }, 10) - tooltipEl.addEventListener('mouseout', () => { - expect(tooltip.hide).not.toHaveBeenCalled() - done() + tooltipEl.dispatchEvent(createEvent('mouseover')) }) - - tooltipEl.dispatchEvent(createEvent('mouseover')) }) - it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => { - // Style this tooltip to give it plenty of room for popper to do what it wants - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-placement="top" style="position:fixed;left:50%;top:50%;">Trigger</a>' + it('should not hide tooltip if leave event occurs and interaction remains inside trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" rel="tooltip" title="Another tooltip">', + '<b>Trigger</b>', + 'the tooltip', + '</a>' + ].join('') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + const triggerChild = tooltipEl.querySelector('b') + + spyOn(tooltip, 'hide').and.callThrough() + + tooltipEl.addEventListener('mouseover', () => { + const moveMouseToChildEvent = createEvent('mouseout') + Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { + value: triggerChild + }) + + tooltipEl.dispatchEvent(moveMouseToChildEvent) + }) - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.15s', - transitionDelay: '0s' + tooltipEl.addEventListener('mouseout', () => { + expect(tooltip.hide).not.toHaveBeenCalled() + resolve() + }) + + tooltipEl.dispatchEvent(createEvent('mouseover')) }) + }) - setTimeout(() => { - expect(tooltip._popper).not.toBeNull() - expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') - tooltipEl.dispatchEvent(createEvent('mouseout')) + it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', () => { + return new Promise(resolve => { + // Style this tooltip to give it plenty of room for popper to do what it wants + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-placement="top" style="position:fixed;left:50%;top:50%;">Trigger</a>' - setTimeout(() => { - expect(tooltip._getTipElement()).not.toHaveClass('show') - tooltipEl.dispatchEvent(createEvent('mouseover')) - }, 100) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) setTimeout(() => { expect(tooltip._popper).not.toBeNull() expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') - done() - }, 200) - }, 10) + tooltipEl.dispatchEvent(createEvent('mouseout')) + + setTimeout(() => { + expect(tooltip._getTipElement()).not.toHaveClass('show') + tooltipEl.dispatchEvent(createEvent('mouseover')) + }, 100) + + setTimeout(() => { + expect(tooltip._popper).not.toBeNull() + expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') + resolve() + }, 200) + }, 10) - tooltipEl.dispatchEvent(createEvent('mouseover')) + tooltipEl.dispatchEvent(createEvent('mouseover')) + }) }) - it('should only trigger inserted event if a new tooltip element was created', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + it('should only trigger inserted event if a new tooltip element was created', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.15s', - transitionDelay: '0s' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - const insertedFunc = jasmine.createSpy() - tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) - - setTimeout(() => { - expect(insertedFunc).toHaveBeenCalledTimes(1) - tooltip.hide() + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) - setTimeout(() => { - tooltip.show() - }, 100) + const insertedFunc = jasmine.createSpy() + tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc) setTimeout(() => { expect(insertedFunc).toHaveBeenCalledTimes(1) - done() - }, 200) - }, 0) + tooltip.hide() - tooltip.show() + setTimeout(() => { + tooltip.show() + }, 100) + + setTimeout(() => { + expect(insertedFunc).toHaveBeenCalledTimes(1) + resolve() + }, 200) + }, 0) + + tooltip.show() + }) }) - it('should show a tooltip with custom class provided in data attributes', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-custom-class="custom-class">' + it('should show a tooltip with custom class provided in data attributes', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-custom-class="custom-class">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with custom class provided as a string in config', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with custom class provided as a string in config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - customClass: 'custom-class custom-class-2' - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: 'custom-class custom-class-2' + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip).toHaveClass('custom-class') - expect(tip).toHaveClass('custom-class-2') - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + expect(tip).toHaveClass('custom-class-2') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should show a tooltip with custom class provided as a function in config', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should show a tooltip with custom class provided as a function in config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - customClass: spy - }) + const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + customClass: spy + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(spy).toHaveBeenCalled() - expect(tip).toHaveClass('custom-class') - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') + expect(tip).not.toBeNull() + expect(spy).toHaveBeenCalled() + expect(tip).toHaveClass('custom-class') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should remove `title` attribute if exists', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' + it('should remove `title` attribute if exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltipEl.getAttribute('title')).toBeNull() - done() + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltipEl.getAttribute('title')).toBeNull() + resolve() + }) + tooltip.show() }) - tooltip.show() }) }) describe('hide', () => { - it('should hide a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should hide a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should hide a tooltip on mobile', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should hide a tooltip on mobile', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'off') - tooltip.hide() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + document.documentElement.ontouchstart = noop + spyOn(EventHandler, 'off') + tooltip.hide() + }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = undefined - done() - }) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + document.documentElement.ontouchstart = undefined + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should hide a tooltip without animation', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should hide a tooltip without animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).toBeNull() - expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).toBeNull() + expect(tooltipEl.getAttribute('aria-describedby')).toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should not hide a tooltip if hide event is prevented', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should not hide a tooltip if hide event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const assertDone = () => { - setTimeout(() => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }, 20) - } + const assertDone = () => { + setTimeout(() => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }, 20) + } - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) - tooltipEl.addEventListener('hide.bs.tooltip', event => { - event.preventDefault() - assertDone() - }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - throw new Error('should not trigger hidden event') - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => tooltip.hide()) + tooltipEl.addEventListener('hide.bs.tooltip', event => { + event.preventDefault() + assertDone() + }) + tooltipEl.addEventListener('hidden.bs.tooltip', () => { + throw new Error('should not trigger hidden event') + }) - tooltip.show() + tooltip.show() + }) }) it('should not throw error running hide if popper hasn\'t been shown', () => { @@ -979,22 +1051,24 @@ describe('Tooltip', () => { }) describe('update', () => { - it('should call popper update', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should call popper update', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - spyOn(tooltip._popper, 'update') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + spyOn(tooltip._popper, 'update') - tooltip.update() + tooltip.update() - expect(tooltip._popper.update).toHaveBeenCalled() - done() - }) + expect(tooltip._popper.update).toHaveBeenCalled() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) it('should do nothing if the tooltip is not shown', () => { @@ -1272,55 +1346,61 @@ describe('Tooltip', () => { }) describe('aria-label', () => { - it('should add the aria-label attribute for referencing original title', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' + it('should add the aria-label attribute for referencing original title', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') - done() - }) + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Another tooltip') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should not add the aria-label attribute if the attribute already exists', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" aria-label="Different label" title="Another tooltip"></a>' + it('should not add the aria-label attribute if the attribute already exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" aria-label="Different label" title="Another tooltip"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') - done() - }) + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toEqual('Different label') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) - it('should not add the aria-label attribute if the element has text content', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">text content</a>' + it('should not add the aria-label attribute if the element has text content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">text content</a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tooltipShown = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toBeNull() - done() - }) + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toBeNull() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) }) diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index f9903c8327..73384fc90b 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -23,267 +23,297 @@ describe('Backdrop', () => { }) describe('show', () => { - it('should append the backdrop html once on show and include the "show" class if it is "shown"', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: false - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + it('should append the backdrop html once on show and include the "show" class if it is "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) + expect(getElements()).toHaveSize(0) - instance.show() - instance.show(() => { - expect(getElements()).toHaveSize(1) - for (const el of getElements()) { - expect(el).toHaveClass(CLASS_NAME_SHOW) - } + instance.show() + instance.show(() => { + expect(getElements()).toHaveSize(1) + for (const el of getElements()) { + expect(el).toHaveClass(CLASS_NAME_SHOW) + } - done() + resolve() + }) }) }) - it('should not append the backdrop html if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + it('should not append the backdrop html if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) - instance.show(() => { expect(getElements()).toHaveSize(0) - done() + instance.show(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) - it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + it('should append the backdrop html once and include the "fade" class if it is "shown" and "animated"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) + expect(getElements()).toHaveSize(0) - instance.show(() => { - expect(getElements()).toHaveSize(1) - for (const el of getElements()) { - expect(el).toHaveClass(CLASS_NAME_FADE) - } + instance.show(() => { + expect(getElements()).toHaveSize(1) + for (const el of getElements()) { + expect(el).toHaveClass(CLASS_NAME_FADE) + } - done() + resolve() + }) }) }) }) describe('hide', () => { - it('should remove the backdrop html', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) + it('should remove the backdrop html', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) - const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP) - expect(getElements()).toHaveSize(0) - instance.show(() => { - expect(getElements()).toHaveSize(1) - instance.hide(() => { - expect(getElements()).toHaveSize(0) - done() + expect(getElements()).toHaveSize(0) + instance.show(() => { + expect(getElements()).toHaveSize(1) + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) - it('should remove the "show" class', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) - const elem = instance._getElement() + it('should remove the "show" class', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const elem = instance._getElement() - instance.show() - instance.hide(() => { - expect(elem).not.toHaveClass(CLASS_NAME_SHOW) - done() + instance.show() + instance.hide(() => { + expect(elem).not.toHaveClass(CLASS_NAME_SHOW) + resolve() + }) }) }) - it('should not try to remove Node on remove method if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true - }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - const spy = spyOn(instance, 'dispose').and.callThrough() + it('should not try to remove Node on remove method if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + const spy = spyOn(instance, 'dispose').and.callThrough() - expect(getElements()).toHaveSize(0) - expect(instance._isAppended).toBeFalse() - instance.show(() => { - instance.hide(() => { - expect(getElements()).toHaveSize(0) - expect(spy).not.toHaveBeenCalled() - expect(instance._isAppended).toBeFalse() - done() + expect(getElements()).toHaveSize(0) + expect(instance._isAppended).toBeFalse() + instance.show(() => { + instance.hide(() => { + expect(getElements()).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + expect(instance._isAppended).toBeFalse() + resolve() + }) }) }) }) - it('should not error if the backdrop no longer has a parent', done => { - fixtureEl.innerHTML = '<div id="wrapper"></div>' + it('should not error if the backdrop no longer has a parent', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div id="wrapper"></div>' - const wrapper = fixtureEl.querySelector('#wrapper') - const instance = new Backdrop({ - isVisible: true, - isAnimated: true, - rootElement: wrapper - }) + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + isAnimated: true, + rootElement: wrapper + }) - const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) - instance.show(() => { - wrapper.remove() - instance.hide(() => { - expect(getElements()).toHaveSize(0) - done() + instance.show(() => { + wrapper.remove() + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) }) describe('click callback', () => { - it('should execute callback on click', done => { - const spy = jasmine.createSpy('spy') + it('should execute callback on click', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy') - const instance = new Backdrop({ - isVisible: true, - isAnimated: false, - clickCallback: () => spy() - }) - const endTest = () => { - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }, 10) - } - - instance.show(() => { - const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) - document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) - endTest() - }) - }) - }) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false, + clickCallback: () => spy() + }) + const endTest = () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 10) + } - describe('animation callbacks', () => { - it('should show and hide backdrop after counting transition duration if it is animated', done => { - const instance = new Backdrop({ - isVisible: true, - isAnimated: true - }) - const spy2 = jasmine.createSpy('spy2') - - const execDone = () => { - setTimeout(() => { - expect(spy2).toHaveBeenCalledTimes(2) - done() - }, 10) - } - - instance.show(spy2) - instance.hide(() => { - spy2() - execDone() + instance.show(() => { + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) + endTest() + }) }) - expect(spy2).not.toHaveBeenCalled() }) - it('should show and hide backdrop without a delay if it is not animated', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - const instance = new Backdrop({ - isVisible: true, - isAnimated: false + describe('animation callbacks', () => { + it('should show and hide backdrop after counting transition duration if it is animated', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + isAnimated: true + }) + const spy2 = jasmine.createSpy('spy2') + + const execDone = () => { + setTimeout(() => { + expect(spy2).toHaveBeenCalledTimes(2) + resolve() + }, 10) + } + + instance.show(spy2) + instance.hide(() => { + spy2() + execDone() + }) + expect(spy2).not.toHaveBeenCalled() + }) }) - const spy2 = jasmine.createSpy('spy2') - instance.show(spy2) - instance.hide(spy2) - - setTimeout(() => { - expect(spy2).toHaveBeenCalled() - expect(spy).not.toHaveBeenCalled() - done() - }, 10) - }) - - it('should not call delay callbacks if it is not "shown"', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true + it('should show and hide backdrop without a delay if it is not animated', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false + }) + const spy2 = jasmine.createSpy('spy2') + + instance.show(spy2) + instance.hide(spy2) + + setTimeout(() => { + expect(spy2).toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + }) }) - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - instance.show() - instance.hide(() => { - expect(spy).not.toHaveBeenCalled() - done() + it('should not call delay callbacks if it is not "shown"', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: false, + isAnimated: true + }) + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + + instance.show() + instance.hide(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) + }) }) }) - }) - describe('Config', () => { - describe('rootElement initialization', () => { - it('should be appended on "document.body" by default', done => { - const instance = new Backdrop({ - isVisible: true - }) - const getElement = () => document.querySelector(CLASS_BACKDROP) - instance.show(() => { - expect(getElement().parentElement).toEqual(document.body) - done() + describe('Config', () => { + describe('rootElement initialization', () => { + it('should be appended on "document.body" by default', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(document.body) + resolve() + }) + }) }) - }) - it('should find the rootElement if passed as a string', done => { - const instance = new Backdrop({ - isVisible: true, - rootElement: 'body' - }) - const getElement = () => document.querySelector(CLASS_BACKDROP) - instance.show(() => { - expect(getElement().parentElement).toEqual(document.body) - done() + it('should find the rootElement if passed as a string', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + rootElement: 'body' + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(document.body) + resolve() + }) + }) }) - }) - - it('should be appended on any element given by the proper config', done => { - fixtureEl.innerHTML = '<div id="wrapper"></div>' - const wrapper = fixtureEl.querySelector('#wrapper') - const instance = new Backdrop({ - isVisible: true, - rootElement: wrapper - }) - const getElement = () => document.querySelector(CLASS_BACKDROP) - instance.show(() => { - expect(getElement().parentElement).toEqual(wrapper) - done() + it('should be appended on any element given by the proper config', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div id="wrapper"></div>' + + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + rootElement: wrapper + }) + const getElement = () => document.querySelector(CLASS_BACKDROP) + instance.show(() => { + expect(getElement().parentElement).toEqual(wrapper) + resolve() + }) + }) }) }) - }) - describe('ClassName', () => { - it('should allow configuring className', done => { - const instance = new Backdrop({ - isVisible: true, - className: 'foo' - }) - const getElement = () => document.querySelector('.foo') - instance.show(() => { - expect(getElement()).toEqual(instance._getElement()) - instance.dispose() - done() + describe('ClassName', () => { + it('should allow configuring className', () => { + return new Promise(resolve => { + const instance = new Backdrop({ + isVisible: true, + className: 'foo' + }) + const getElement = () => document.querySelector('.foo') + instance.show(() => { + expect(getElement()).toEqual(instance._getElement()) + instance.dispose() + resolve() + }) + }) }) }) }) diff --git a/js/tests/unit/util/focustrap.spec.js b/js/tests/unit/util/focustrap.spec.js index 52a7573971..55f6a23a7e 100644 --- a/js/tests/unit/util/focustrap.spec.js +++ b/js/tests/unit/util/focustrap.spec.js @@ -1,7 +1,7 @@ import FocusTrap from '../../../src/util/focustrap' import EventHandler from '../../../src/dom/event-handler' import SelectorEngine from '../../../src/dom/selector-engine' -import { clearFixture, getFixture, createEvent } from '../../helpers/fixture' +import { clearFixture, createEvent, getFixture } from '../../helpers/fixture' describe('FocusTrap', () => { let fixtureEl @@ -41,140 +41,148 @@ describe('FocusTrap', () => { expect(trapElement.focus).not.toHaveBeenCalled() }) - it('should force focus inside focus trap if it can', done => { - fixtureEl.innerHTML = [ - '<a href="#" id="outside">outside</a>', - '<div id="focustrap" tabindex="-1">', - ' <a href="#" id="inside">inside</a>', - '</div>' - ].join('') + it('should force focus inside focus trap if it can', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" id="outside">outside</a>', + '<div id="focustrap" tabindex="-1">', + ' <a href="#" id="inside">inside</a>', + '</div>' + ].join('') - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() - const inside = document.getElementById('inside') + const inside = document.getElementById('inside') - const focusInListener = () => { - expect(inside.focus).toHaveBeenCalled() - document.removeEventListener('focusin', focusInListener) - done() - } + const focusInListener = () => { + expect(inside.focus).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } - spyOn(inside, 'focus') - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) + spyOn(inside, 'focus') + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) - document.addEventListener('focusin', focusInListener) + document.addEventListener('focusin', focusInListener) - const focusInEvent = createEvent('focusin', { bubbles: true }) - Object.defineProperty(focusInEvent, 'target', { - value: document.getElementById('outside') - }) + const focusInEvent = createEvent('focusin', { bubbles: true }) + Object.defineProperty(focusInEvent, 'target', { + value: document.getElementById('outside') + }) - document.dispatchEvent(focusInEvent) + document.dispatchEvent(focusInEvent) + }) }) - it('should wrap focus around forward on tab', done => { - fixtureEl.innerHTML = [ - '<a href="#" id="outside">outside</a>', - '<div id="focustrap" tabindex="-1">', - ' <a href="#" id="first">first</a>', - ' <a href="#" id="inside">inside</a>', - ' <a href="#" id="last">last</a>', - '</div>' - ].join('') - - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() - - const first = document.getElementById('first') - const inside = document.getElementById('inside') - const last = document.getElementById('last') - const outside = document.getElementById('outside') - - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) - spyOn(first, 'focus').and.callThrough() - - const focusInListener = () => { - expect(first.focus).toHaveBeenCalled() - first.removeEventListener('focusin', focusInListener) - done() - } - - first.addEventListener('focusin', focusInListener) - - const keydown = createEvent('keydown') - keydown.key = 'Tab' - - document.dispatchEvent(keydown) - outside.focus() + it('should wrap focus around forward on tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" id="outside">outside</a>', + '<div id="focustrap" tabindex="-1">', + ' <a href="#" id="first">first</a>', + ' <a href="#" id="inside">inside</a>', + ' <a href="#" id="last">last</a>', + '</div>' + ].join('') + + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() + + const first = document.getElementById('first') + const inside = document.getElementById('inside') + const last = document.getElementById('last') + const outside = document.getElementById('outside') + + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) + spyOn(first, 'focus').and.callThrough() + + const focusInListener = () => { + expect(first.focus).toHaveBeenCalled() + first.removeEventListener('focusin', focusInListener) + resolve() + } + + first.addEventListener('focusin', focusInListener) + + const keydown = createEvent('keydown') + keydown.key = 'Tab' + + document.dispatchEvent(keydown) + outside.focus() + }) }) - it('should wrap focus around backwards on shift-tab', done => { - fixtureEl.innerHTML = [ - '<a href="#" id="outside">outside</a>', - '<div id="focustrap" tabindex="-1">', - ' <a href="#" id="first">first</a>', - ' <a href="#" id="inside">inside</a>', - ' <a href="#" id="last">last</a>', - '</div>' - ].join('') - - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() - - const first = document.getElementById('first') - const inside = document.getElementById('inside') - const last = document.getElementById('last') - const outside = document.getElementById('outside') - - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) - spyOn(last, 'focus').and.callThrough() - - const focusInListener = () => { - expect(last.focus).toHaveBeenCalled() - last.removeEventListener('focusin', focusInListener) - done() - } - - last.addEventListener('focusin', focusInListener) - - const keydown = createEvent('keydown') - keydown.key = 'Tab' - keydown.shiftKey = true - - document.dispatchEvent(keydown) - outside.focus() + it('should wrap focus around backwards on shift-tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" id="outside">outside</a>', + '<div id="focustrap" tabindex="-1">', + ' <a href="#" id="first">first</a>', + ' <a href="#" id="inside">inside</a>', + ' <a href="#" id="last">last</a>', + '</div>' + ].join('') + + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() + + const first = document.getElementById('first') + const inside = document.getElementById('inside') + const last = document.getElementById('last') + const outside = document.getElementById('outside') + + spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last]) + spyOn(last, 'focus').and.callThrough() + + const focusInListener = () => { + expect(last.focus).toHaveBeenCalled() + last.removeEventListener('focusin', focusInListener) + resolve() + } + + last.addEventListener('focusin', focusInListener) + + const keydown = createEvent('keydown') + keydown.key = 'Tab' + keydown.shiftKey = true + + document.dispatchEvent(keydown) + outside.focus() + }) }) - it('should force focus on itself if there is no focusable content', done => { - fixtureEl.innerHTML = [ - '<a href="#" id="outside">outside</a>', - '<div id="focustrap" tabindex="-1"></div>' - ].join('') + it('should force focus on itself if there is no focusable content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<a href="#" id="outside">outside</a>', + '<div id="focustrap" tabindex="-1"></div>' + ].join('') - const trapElement = fixtureEl.querySelector('div') - const focustrap = new FocusTrap({ trapElement }) - focustrap.activate() + const trapElement = fixtureEl.querySelector('div') + const focustrap = new FocusTrap({ trapElement }) + focustrap.activate() - const focusInListener = () => { - expect(focustrap._config.trapElement.focus).toHaveBeenCalled() - document.removeEventListener('focusin', focusInListener) - done() - } + const focusInListener = () => { + expect(focustrap._config.trapElement.focus).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } - spyOn(focustrap._config.trapElement, 'focus') + spyOn(focustrap._config.trapElement, 'focus') - document.addEventListener('focusin', focusInListener) + document.addEventListener('focusin', focusInListener) - const focusInEvent = createEvent('focusin', { bubbles: true }) - Object.defineProperty(focusInEvent, 'target', { - value: document.getElementById('outside') - }) + const focusInEvent = createEvent('focusin', { bubbles: true }) + Object.defineProperty(focusInEvent, 'target', { + value: document.getElementById('outside') + }) - document.dispatchEvent(focusInEvent) + document.dispatchEvent(focusInEvent) + }) }) }) diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index 9d8c5ed986..0d60ca9894 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -1,5 +1,6 @@ import * as Util from '../../../src/util/index' import { clearFixture, getFixture } from '../../helpers/fixture' +import { noop } from '../../../src/util/index' describe('Util', () => { let fixtureEl @@ -154,18 +155,20 @@ describe('Util', () => { }) describe('triggerTransitionEnd', () => { - it('should trigger transitionend event', done => { - fixtureEl.innerHTML = '<div></div>' + it('should trigger transitionend event', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const el = fixtureEl.querySelector('div') - const spy = spyOn(el, 'dispatchEvent').and.callThrough() + const el = fixtureEl.querySelector('div') + const spy = spyOn(el, 'dispatchEvent').and.callThrough() - el.addEventListener('transitionend', () => { - expect(spy).toHaveBeenCalled() - done() - }) + el.addEventListener('transitionend', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - Util.triggerTransitionEnd(el) + Util.triggerTransitionEnd(el) + }) }) }) @@ -611,9 +614,9 @@ describe('Util', () => { }) it('should define a plugin on the jQuery instance', () => { - const pluginMock = function () {} + const pluginMock = Util.noop pluginMock.NAME = 'test' - pluginMock.jQueryInterface = function () {} + pluginMock.jQueryInterface = Util.noop Util.defineJQueryPlugin(pluginMock) expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface) @@ -658,96 +661,104 @@ describe('Util', () => { expect(callbackSpy).toHaveBeenCalled() }) - it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', done => { - const el = document.createElement('div') - const callbackSpy = jasmine.createSpy('callback spy') + it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', () => { + return new Promise(resolve => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' - }) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) - Util.executeAfterTransition(callbackSpy, el) + Util.executeAfterTransition(callbackSpy, el) - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalled() - done() - }, 70) + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + resolve() + }, 70) + }) }) - it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', done => { - const el = document.createElement('div') - const callbackSpy = jasmine.createSpy('callback spy') + it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', () => { + return new Promise(resolve => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' - }) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) - Util.executeAfterTransition(callbackSpy, el) + Util.executeAfterTransition(callbackSpy, el) - setTimeout(() => { - el.dispatchEvent(new TransitionEvent('transitionend')) - }, 50) + setTimeout(() => { + el.dispatchEvent(new TransitionEvent('transitionend')) + }, 50) - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalledTimes(1) - done() - }, 70) + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalledTimes(1) + resolve() + }, 70) + }) }) - it('should not trigger a transitionend event if another transitionend event had already happened', done => { - const el = document.createElement('div') + it('should not trigger a transitionend event if another transitionend event had already happened', () => { + return new Promise(resolve => { + const el = document.createElement('div') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' - }) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) - Util.executeAfterTransition(() => {}, el) + Util.executeAfterTransition(noop, el) - // simulate a event dispatched by the browser - el.dispatchEvent(new TransitionEvent('transitionend')) + // simulate a event dispatched by the browser + el.dispatchEvent(new TransitionEvent('transitionend')) - const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() + const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() - setTimeout(() => { - // setTimeout should not have triggered another transitionend event. - expect(dispatchSpy).not.toHaveBeenCalled() - done() - }, 70) + setTimeout(() => { + // setTimeout should not have triggered another transitionend event. + expect(dispatchSpy).not.toHaveBeenCalled() + resolve() + }, 70) + }) }) - it('should ignore transitionend events from nested elements', done => { - fixtureEl.innerHTML = [ - '<div class="outer">', - ' <div class="nested"></div>', - '</div>' - ].join('') + it('should ignore transitionend events from nested elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="outer">', + ' <div class="nested"></div>', + '</div>' + ].join('') - const outer = fixtureEl.querySelector('.outer') - const nested = fixtureEl.querySelector('.nested') - const callbackSpy = jasmine.createSpy('callback spy') + const outer = fixtureEl.querySelector('.outer') + const nested = fixtureEl.querySelector('.nested') + const callbackSpy = jasmine.createSpy('callback spy') - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.05s', - transitionDelay: '0s' - }) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) - Util.executeAfterTransition(callbackSpy, outer) + Util.executeAfterTransition(callbackSpy, outer) - nested.dispatchEvent(new TransitionEvent('transitionend', { - bubbles: true - })) + nested.dispatchEvent(new TransitionEvent('transitionend', { + bubbles: true + })) - setTimeout(() => { - expect(callbackSpy).not.toHaveBeenCalled() - }, 20) + setTimeout(() => { + expect(callbackSpy).not.toHaveBeenCalled() + }, 20) - setTimeout(() => { - expect(callbackSpy).toHaveBeenCalled() - done() - }, 70) + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + resolve() + }, 70) + }) }) }) diff --git a/js/tests/unit/util/scrollbar.spec.js b/js/tests/unit/util/scrollbar.spec.js index fc3a6f4e87..6fcf5718bb 100644 --- a/js/tests/unit/util/scrollbar.spec.js +++ b/js/tests/unit/util/scrollbar.spec.js @@ -101,7 +101,7 @@ describe('ScrollBar', () => { }) describe('hide - reset', () => { - it('should adjust the inline padding of fixed elements which are full-width', done => { + it('should adjust the inline padding of fixed elements which are full-width', () => { fixtureEl.innerHTML = [ '<div style="height: 110vh; width: 100%">', ' <div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>', @@ -134,10 +134,9 @@ describe('ScrollBar', () => { expect(getPaddingAttr(fixedEl2)).toBeNull() expect(currentPadding).toEqual(originalPadding) expect(currentPadding2).toEqual(originalPadding2) - done() }) - it('should remove padding & margin if not existed before adjustment', done => { + it('should remove padding & margin if not existed before adjustment', () => { fixtureEl.innerHTML = [ '<div style="height: 110vh; width: 100%">', ' <div class="fixed" id="fixed" style="width: 100vw;"></div>', @@ -155,10 +154,9 @@ describe('ScrollBar', () => { expect(fixedEl.getAttribute('style').includes('padding-right')).toBeFalse() expect(stickyEl.getAttribute('style').includes('margin-right')).toBeFalse() - done() }) - it('should adjust the inline margin and padding of sticky elements', done => { + it('should adjust the inline margin and padding of sticky elements', () => { fixtureEl.innerHTML = [ '<div style="height: 110vh">', ' <div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>', @@ -184,7 +182,6 @@ describe('ScrollBar', () => { expect(getMarginX(stickyTopEl)).toEqual(originalMargin) expect(getPaddingAttr(stickyTopEl)).toBeNull() expect(getPaddingX(stickyTopEl)).toEqual(originalPadding) - done() }) it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => { diff --git a/js/tests/unit/util/swipe.spec.js b/js/tests/unit/util/swipe.spec.js index 93131b8fdd..054aab6958 100644 --- a/js/tests/unit/util/swipe.spec.js +++ b/js/tests/unit/util/swipe.spec.js @@ -78,74 +78,80 @@ describe('Swipe', () => { }) describe('Config', () => { - it('Test leftCallback', done => { - const spyRight = jasmine.createSpy('spy') - clearPointerEvents() - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - leftCallback: () => { - expect(spyRight).not.toHaveBeenCalled() - restorePointerEvents() - done() - }, - rightCallback: spyRight - }) - - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 + it('Test leftCallback', () => { + return new Promise(resolve => { + const spyRight = jasmine.createSpy('spy') + clearPointerEvents() + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + leftCallback: () => { + expect(spyRight).not.toHaveBeenCalled() + restorePointerEvents() + resolve() + }, + rightCallback: spyRight + }) + + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }) }) }) - it('Test rightCallback', done => { - const spyLeft = jasmine.createSpy('spy') - clearPointerEvents() - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - rightCallback: () => { - expect(spyLeft).not.toHaveBeenCalled() - restorePointerEvents() - done() - }, - leftCallback: spyLeft - }) - - mockSwipeGesture(swipeEl, { - pos: [10, 10], - deltaX: 300 + it('Test rightCallback', () => { + return new Promise(resolve => { + const spyLeft = jasmine.createSpy('spy') + clearPointerEvents() + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + rightCallback: () => { + expect(spyLeft).not.toHaveBeenCalled() + restorePointerEvents() + resolve() + }, + leftCallback: spyLeft + }) + + mockSwipeGesture(swipeEl, { + pos: [10, 10], + deltaX: 300 + }) }) }) - it('Test endCallback', done => { - clearPointerEvents() - defineDocumentElementOntouchstart() - let isFirstTime = true - - const callback = () => { - if (isFirstTime) { - isFirstTime = false - return - } + it('Test endCallback', () => { + return new Promise(resolve => { + clearPointerEvents() + defineDocumentElementOntouchstart() + let isFirstTime = true - expect().nothing() - restorePointerEvents() - done() - } + const callback = () => { + if (isFirstTime) { + isFirstTime = false + return + } - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - endCallback: callback - }) - mockSwipeGesture(swipeEl, { - pos: [10, 10], - deltaX: 300 - }) + expect().nothing() + restorePointerEvents() + resolve() + } - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + endCallback: callback + }) + mockSwipeGesture(swipeEl, { + pos: [10, 10], + deltaX: 300 + }) + + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }) }) }) }) @@ -170,53 +176,57 @@ describe('Swipe', () => { expect(swipe._handleSwipe).not.toHaveBeenCalled() }) - it('should allow swipeRight and call "rightCallback" with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } - - const style = '#fixture .pointer-event { touch-action: none !important; }' - fixtureEl.innerHTML += style - - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - rightCallback: () => { - deleteDocumentElementOntouchstart() + it('should allow swipeRight and call "rightCallback" with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { expect().nothing() - done() + resolve() + return } - }) - mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer') - }) + const style = '#fixture .pointer-event { touch-action: none !important; }' + fixtureEl.innerHTML += style - it('should allow swipeLeft and call "leftCallback" with pointer events', done => { - if (!supportPointerEvent) { - expect().nothing() - done() - return - } + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + rightCallback: () => { + deleteDocumentElementOntouchstart() + expect().nothing() + resolve() + } + }) - const style = '#fixture .pointer-event { touch-action: none !important; }' - fixtureEl.innerHTML += style + mockSwipeGesture(swipeEl, { deltaX: 300 }, 'pointer') + }) + }) - defineDocumentElementOntouchstart() - // eslint-disable-next-line no-new - new Swipe(swipeEl, { - leftCallback: () => { + it('should allow swipeLeft and call "leftCallback" with pointer events', () => { + return new Promise(resolve => { + if (!supportPointerEvent) { expect().nothing() - deleteDocumentElementOntouchstart() - done() + resolve() + return } - }) - mockSwipeGesture(swipeEl, { - pos: [300, 10], - deltaX: -300 - }, 'pointer') + const style = '#fixture .pointer-event { touch-action: none !important; }' + fixtureEl.innerHTML += style + + defineDocumentElementOntouchstart() + // eslint-disable-next-line no-new + new Swipe(swipeEl, { + leftCallback: () => { + expect().nothing() + deleteDocumentElementOntouchstart() + resolve() + } + }) + + mockSwipeGesture(swipeEl, { + pos: [300, 10], + deltaX: -300 + }, 'pointer') + }) }) }) |