diff options
| author | Bobby <[email protected]> | 2024-08-16 20:47:33 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-08-16 20:47:33 -0400 |
| commit | 6b28433d9cfde435be8ec2bd6cf91e6324d08865 (patch) | |
| tree | 8343c27b8b95ff5639233e81cf157f92e5688466 /js/tests/unit | |
| parent | d53094ec16ba385faae2973ddee648698b32ab24 (diff) | |
| parent | 048f56f51460df75e92a2f7b472e1c56baeb68f7 (diff) | |
| download | bootstrap-main.tar.xz bootstrap-main.zip | |
Diffstat (limited to 'js/tests/unit')
28 files changed, 9145 insertions, 7026 deletions
diff --git a/js/tests/unit/.eslintrc.json b/js/tests/unit/.eslintrc.json deleted file mode 100644 index 6362a1acf..000000000 --- a/js/tests/unit/.eslintrc.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": [ - "../../../.eslintrc.json" - ], - "env": { - "jasmine": true - }, - "rules": { - "unicorn/consistent-function-scoping": "off", - "unicorn/no-useless-undefined": "off", - "unicorn/prefer-add-event-listener": "off" - } -} diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index cdda997c9..97cc3cc53 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -1,6 +1,6 @@ -import Alert from '../../src/alert' -import { getTransitionDurationFromElement } from '../../src/util/index' -import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' +import Alert from '../../src/alert.js' +import { getTransitionDurationFromElement } from '../../src/util/index.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Alert', () => { let fixtureEl @@ -25,7 +25,7 @@ describe('Alert', () => { }) it('should return version', () => { - expect(typeof Alert.VERSION).toEqual('string') + expect(Alert.VERSION).toEqual(jasmine.any(String)) }) describe('DATA_KEY', () => { @@ -45,7 +45,7 @@ describe('Alert', () => { const button = document.querySelector('button') button.click() - expect(document.querySelectorAll('.alert').length).toEqual(0) + expect(document.querySelectorAll('.alert')).toHaveSize(0) }) it('should close an alert without instantiating it manually with the parent selector', () => { @@ -58,65 +58,71 @@ describe('Alert', () => { const button = document.querySelector('button') button.click() - expect(document.querySelectorAll('.alert').length).toEqual(0) + expect(document.querySelectorAll('.alert')).toHaveSize(0) }) }) 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').length).toEqual(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').length).toEqual(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, reject) => { + 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', () => { + reject(new Error('should not fire closed event')) + }) - alert.close() + alert.close() + }) }) }) @@ -142,14 +148,14 @@ describe('Alert', () => { const alertEl = fixtureEl.querySelector('.alert') const alert = new Alert(alertEl) - spyOn(alert, 'close') + const spy = spyOn(alert, 'close') jQueryMock.fn.alert = Alert.jQueryInterface jQueryMock.elements = [alertEl] jQueryMock.fn.alert.call(jQueryMock, 'close') - expect(alert.close).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create new alert instance and call close', () => { @@ -179,6 +185,34 @@ describe('Alert', () => { expect(Alert.getInstance(alertEl)).not.toBeNull() expect(fixtureEl.querySelector('.alert')).not.toBeNull() }) + + it('should throw an error on undefined method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.alert.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw an error on protected method', () => { + fixtureEl.innerHTML = '<div></div>' + + const div = fixtureEl.querySelector('div') + const action = '_getConfig' + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.alert.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) }) describe('getInstance', () => { @@ -197,7 +231,7 @@ describe('Alert', () => { const div = fixtureEl.querySelector('div') - expect(Alert.getInstance(div)).toEqual(null) + expect(Alert.getInstance(div)).toBeNull() }) }) @@ -218,7 +252,7 @@ describe('Alert', () => { const div = fixtureEl.querySelector('div') - expect(Alert.getInstance(div)).toEqual(null) + expect(Alert.getInstance(div)).toBeNull() expect(Alert.getOrCreateInstance(div)).toBeInstanceOf(Alert) }) }) diff --git a/js/tests/unit/base-component.spec.js b/js/tests/unit/base-component.spec.js index b8ec83f12..5b7d52e23 100644 --- a/js/tests/unit/base-component.spec.js +++ b/js/tests/unit/base-component.spec.js @@ -1,7 +1,7 @@ -import BaseComponent from '../../src/base-component' -import { clearFixture, getFixture } from '../helpers/fixture' -import EventHandler from '../../src/dom/event-handler' -import { noop } from '../../src/util' +import BaseComponent from '../../src/base-component.js' +import EventHandler from '../../src/dom/event-handler.js' +import { noop } from '../../src/util/index.js' +import { clearFixture, getFixture } from '../helpers/fixture.js' class DummyClass extends BaseComponent { constructor(element) { @@ -37,7 +37,7 @@ describe('Base Component', () => { describe('Static Methods', () => { describe('VERSION', () => { it('should return version', () => { - expect(typeof DummyClass.VERSION).toEqual('string') + expect(DummyClass.VERSION).toEqual(jasmine.any(String)) }) }) @@ -48,6 +48,13 @@ describe('Base Component', () => { }) describe('NAME', () => { + it('should throw an Error if it is not initialized', () => { + expect(() => { + // eslint-disable-next-line no-unused-expressions + BaseComponent.NAME + }).toThrowError(Error) + }) + it('should return plugin NAME', () => { expect(DummyClass.NAME).toEqual(name) }) @@ -59,6 +66,7 @@ describe('Base Component', () => { }) }) }) + describe('Public Methods', () => { describe('constructor', () => { it('should accept element, either passed as a CSS selector or DOM element', () => { @@ -74,7 +82,19 @@ describe('Base Component', () => { expect(elInstance._element).toEqual(el) expect(selectorInstance._element).toEqual(fixtureEl.querySelector('#bar')) }) + + it('should not initialize and add element record to Data (caching), if argument `element` is not an HTML element', () => { + fixtureEl.innerHTML = '' + + const el = fixtureEl.querySelector('#foo') + const elInstance = new DummyClass(el) + const selectorInstance = new DummyClass('#bar') + + expect(elInstance._element).not.toBeDefined() + expect(selectorInstance._element).not.toBeDefined() + }) }) + describe('dispose', () => { it('should dispose an component', () => { createInstance() @@ -88,11 +108,11 @@ describe('Base Component', () => { it('should de-register element event listeners', () => { createInstance() - spyOn(EventHandler, 'off') + const spy = spyOn(EventHandler, 'off') instance.dispose() - expect(EventHandler.off).toHaveBeenCalledWith(element, DummyClass.EVENT_KEY) + expect(spy).toHaveBeenCalledWith(element, DummyClass.EVENT_KEY) }) }) @@ -123,9 +143,10 @@ describe('Base Component', () => { const div = fixtureEl.querySelector('div') - expect(DummyClass.getInstance(div)).toEqual(null) + expect(DummyClass.getInstance(div)).toBeNull() }) }) + describe('getOrCreateInstance', () => { it('should return an instance', () => { createInstance() @@ -139,7 +160,7 @@ describe('Base Component', () => { fixtureEl.innerHTML = '<div id="foo"></div>' element = fixtureEl.querySelector('#foo') - expect(DummyClass.getInstance(element)).toEqual(null) + expect(DummyClass.getInstance(element)).toBeNull() expect(DummyClass.getOrCreateInstance(element)).toBeInstanceOf(DummyClass) }) }) diff --git a/js/tests/unit/button.spec.js b/js/tests/unit/button.spec.js index e24ff5cb0..6624fee7c 100644 --- a/js/tests/unit/button.spec.js +++ b/js/tests/unit/button.spec.js @@ -1,5 +1,5 @@ -import Button from '../../src/button' -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import Button from '../../src/button.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Button', () => { let fixtureEl @@ -45,19 +45,19 @@ describe('Button', () => { const divTest = fixtureEl.querySelector('.test') const btnTestParent = fixtureEl.querySelector('.testParent') - expect(btn.classList.contains('active')).toEqual(false) + expect(btn).not.toHaveClass('active') btn.click() - expect(btn.classList.contains('active')).toEqual(true) + expect(btn).toHaveClass('active') btn.click() - expect(btn.classList.contains('active')).toEqual(false) + expect(btn).not.toHaveClass('active') divTest.click() - expect(btnTestParent.classList.contains('active')).toEqual(true) + expect(btnTestParent).toHaveClass('active') }) }) @@ -69,12 +69,12 @@ describe('Button', () => { const button = new Button(btnEl) expect(btnEl.getAttribute('aria-pressed')).toEqual('false') - expect(btnEl.classList.contains('active')).toEqual(false) + expect(btnEl).not.toHaveClass('active') button.toggle() expect(btnEl.getAttribute('aria-pressed')).toEqual('true') - expect(btnEl.classList.contains('active')).toEqual(true) + expect(btnEl).toHaveClass('active') }) }) @@ -100,14 +100,14 @@ describe('Button', () => { const btnEl = fixtureEl.querySelector('.btn') const button = new Button(btnEl) - spyOn(button, 'toggle') + const spy = spyOn(button, 'toggle') jQueryMock.fn.button = Button.jQueryInterface jQueryMock.elements = [btnEl] jQueryMock.fn.button.call(jQueryMock, 'toggle') - expect(button.toggle).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create new button instance and call toggle', () => { @@ -121,7 +121,7 @@ describe('Button', () => { jQueryMock.fn.button.call(jQueryMock, 'toggle') expect(Button.getInstance(btnEl)).not.toBeNull() - expect(btnEl.classList.contains('active')).toEqual(true) + expect(btnEl).toHaveClass('active') }) it('should just create a button instance without calling toggle', () => { @@ -135,7 +135,7 @@ describe('Button', () => { jQueryMock.fn.button.call(jQueryMock) expect(Button.getInstance(btnEl)).not.toBeNull() - expect(btnEl.classList.contains('active')).toEqual(false) + expect(btnEl).not.toHaveClass('active') }) }) @@ -155,7 +155,7 @@ describe('Button', () => { const div = fixtureEl.querySelector('div') - expect(Button.getInstance(div)).toEqual(null) + expect(Button.getInstance(div)).toBeNull() }) }) @@ -176,7 +176,7 @@ describe('Button', () => { const div = fixtureEl.querySelector('div') - expect(Button.getInstance(div)).toEqual(null) + expect(Button.getInstance(div)).toBeNull() expect(Button.getOrCreateInstance(div)).toBeInstanceOf(Button) }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index a138f3ad5..2960eb5ce 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -1,8 +1,10 @@ -import Carousel from '../../src/carousel' -import EventHandler from '../../src/dom/event-handler' -import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' -import { isRTL, noop } from '../../src/util/index' -import Swipe from '../../src/util/swipe' +import Carousel from '../../src/carousel.js' +import EventHandler from '../../src/dom/event-handler.js' +import { isRTL, noop } from '../../src/util/index.js' +import Swipe from '../../src/util/swipe.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Carousel', () => { const { Simulator, PointerEvent } = window @@ -63,94 +65,148 @@ 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 start cycling if `ride`===`carousel`', () => { + fixtureEl.innerHTML = '<div id="myCarousel" class="carousel slide" data-bs-ride="carousel"></div>' - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).not.toBeNull() + }) - spyOn(carousel, '_keydown').and.callThrough() + it('should not start cycling if `ride`!==`carousel`', () => { + fixtureEl.innerHTML = '<div id="myCarousel" class="carousel slide" data-bs-ride="true"></div>' - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) + const carousel = new Carousel('#myCarousel') + expect(carousel._interval).toBeNull() + }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowRight' + 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 + }) + + const spy = spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(spy).toHaveBeenCalled() + resolve() + }) - 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 => { + it('should ignore keyboard events if data-bs-keyboard=false', () => { fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', + '<div id="myCarousel" class="carousel slide" data-bs-keyboard="false">', ' <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 class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', ' </div>', '</div>' ].join('') + const spy = spyOn(EventHandler, 'trigger').and.callThrough() 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('#item1')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowLeft' - - carouselEl.dispatchEvent(keydown) + // eslint-disable-next-line no-new + new Carousel('#myCarousel') + expect(spy).not.toHaveBeenCalledWith(carouselEl, 'keydown.bs.carousel', jasmine.any(Function)) }) - it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { + it('should ignore mouse events if data-bs-pause=false', () => { fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', + '<div id="myCarousel" class="carousel slide" data-bs-pause="false">', ' <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 id="item2" class="carousel-item">item 2</div>', ' </div>', '</div>' ].join('') + const spy = spyOn(EventHandler, 'trigger').and.callThrough() const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + // eslint-disable-next-line no-new + new Carousel('#myCarousel') + expect(spy).not.toHaveBeenCalledWith(carouselEl, 'hover.bs.carousel', jasmine.any(Function)) + }) + + 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 spy = spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(spy).toHaveBeenCalled() + resolve() + }) - spyOn(carousel, '_keydown').and.callThrough() + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' - carouselEl.addEventListener('keydown', event => { - expect(carousel._keydown).toHaveBeenCalled() - expect(event.defaultPrevented).toEqual(false) - done() + carouselEl.dispatchEvent(keydown) }) + }) + + 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 spy = spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('keydown', event => { + expect(spy).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', () => { @@ -208,7 +264,7 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - spyOn(carousel, '_triggerSlideEvent') + const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true @@ -219,75 +275,77 @@ describe('Carousel', () => { carouselEl.dispatchEvent(keydown) } - expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() + expect(spy).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 = () => { - return 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, reject) => { + 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', () => { + reject(new Error('carousel slid when it should not have slid')) + }) - carousel.prev() + carousel.prev() - setTimeout(() => { - expect(firstElement.classList.contains('active')).toEqual(true) - done() - }, 10) + setTimeout(() => { + expect(firstElement).toHaveClass('active') + resolve() + }, 10) + }) }) it('should not add touch event listeners if touch = false', () => { @@ -295,13 +353,13 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') - spyOn(Carousel.prototype, '_addTouchEventListeners') + const spy = spyOn(Carousel.prototype, '_addTouchEventListeners') const carousel = new Carousel(carouselEl, { touch: false }) - expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() expect(carousel._swipeHelper).toBeNull() }) @@ -314,11 +372,11 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl) EventHandler.off(carouselEl, Carousel.EVENT_KEY) - spyOn(carousel, '_addTouchEventListeners') + const spy = spyOn(carousel, '_addTouchEventListeners') carousel._addEventListeners() - expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() expect(carousel._swipeHelper).toBeNull() }) @@ -337,274 +395,292 @@ 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.classList.contains('active')).toEqual(true) - 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">', + ' <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) + + const spy = spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).toHaveClass('active') + expect(spy).toHaveBeenCalledWith('prev') + 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.classList.contains('active')).toEqual(false) - 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">', + ' <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) + + const spy = spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).not.toHaveClass('active') + expect(spy).toHaveBeenCalledWith('next') + 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.classList.contains('active')).toEqual(true) - 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">', + ' <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) + + const spy = spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).toHaveClass('active') + expect(spy).toHaveBeenCalledWith('prev') + 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.classList.contains('active')).toEqual(false) - 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">', + ' <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) + + const spy = spyOn(carousel, '_slide').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', event => { + expect(item).not.toHaveClass('active') + expect(spy).toHaveBeenCalledWith('next') + 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('') - - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) - carousel._isSliding = true - - spyOn(carousel, '_triggerSlideEvent') + 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">', + ' <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 + + const spy = spyOn(EventHandler, 'trigger') + + Simulator.gestures.swipe(carouselEl, { + deltaX: 300, + deltaY: 0 + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) - Simulator.gestures.swipe(carouselEl, { - deltaX: 300, - deltaY: 0 - }) - - Simulator.gestures.swipe(carouselEl, { - pos: [300, 10], - deltaX: -300, - deltaY: 0 + setTimeout(() => { + expect(spy).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"></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') + const spy = 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(spy).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 `maybeEnableCycle` on mouse out with pause equal to hover', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="carousel" data-bs-ride="true"></div>' - const carouselEl = fixtureEl.querySelector('.carousel') - const carousel = new Carousel(carouselEl) + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) - spyOn(carousel, 'cycle') + const spyEnable = spyOn(carousel, '_maybeEnableCycle').and.callThrough() + const spyCycle = 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(spyEnable).toHaveBeenCalled() + expect(spyCycle).toHaveBeenCalled() + resolve() + }, 10) + }) }) }) @@ -615,108 +691,114 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - spyOn(carousel, '_triggerSlideEvent') + const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true carousel.next() - expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() + expect(spy).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).toEqual(false) - 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.classList.contains('carousel-item')).toEqual(true) - 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.classList.contains('carousel-item')).toEqual(true) - 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', () => { @@ -739,36 +821,60 @@ describe('Carousel', () => { expect(carousel._activeElement).toEqual(secondItemEl) }) - it('should update indicators if present', done => { + it('should continue cycling if it was already', () => { 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 class="carousel-item">item 2</div>', ' </div>', '</div>' ].join('') const carouselEl = fixtureEl.querySelector('#myCarousel') - const firstIndicator = fixtureEl.querySelector('#firstIndicator') - const secondIndicator = fixtureEl.querySelector('#secondIndicator') const carousel = new Carousel(carouselEl) + const spy = spyOn(carousel, 'cycle') - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(firstIndicator.classList.contains('active')).toEqual(false) - expect(firstIndicator.hasAttribute('aria-current')).toEqual(false) - expect(secondIndicator.classList.contains('active')).toEqual(true) - expect(secondIndicator.getAttribute('aria-current')).toEqual('true') - done() - }) + carousel.next() + expect(spy).not.toHaveBeenCalled() + carousel.cycle() carousel.next() + expect(spy).toHaveBeenCalledTimes(1) + }) + + 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() + }) }) it('should call next()/prev() instance methods when clicking the respective direction buttons', () => { @@ -791,12 +897,14 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl) const nextSpy = spyOn(carousel, 'next') const prevSpy = spyOn(carousel, 'prev') + const spyEnable = spyOn(carousel, '_maybeEnableCycle') nextBtnEl.click() prevBtnEl.click() expect(nextSpy).toHaveBeenCalled() expect(prevSpy).toHaveBeenCalled() + expect(spyEnable).toHaveBeenCalled() }) }) @@ -804,18 +912,18 @@ describe('Carousel', () => { it('should not call next when the page is not visible', () => { fixtureEl.innerHTML = [ '<div style="display: none;">', - ' <div class="carousel" data-bs-interval="false"></div>', + ' <div class="carousel"></div>', '</div>' ].join('') const carouselEl = fixtureEl.querySelector('.carousel') const carousel = new Carousel(carouselEl) - spyOn(carousel, 'next') + const spy = spyOn(carousel, 'next') carousel.nextWhenVisible() - expect(carousel.next).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) }) @@ -826,91 +934,42 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - spyOn(carousel, '_triggerSlideEvent') + const spy = spyOn(EventHandler, 'trigger') carousel._isSliding = true carousel.prev() - expect(carousel._triggerSlideEvent).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) }) describe('pause', () => { - it('should call cycle if the carousel have carousel-item-next and carousel-item-prev class', () => { - fixtureEl.innerHTML = [ - '<div id="myCarousel" class="carousel slide">', - ' <div class="carousel-inner">', - ' <div class="carousel-item active">item 1</div>', - ' <div class="carousel-item carousel-item-next">item 2</div>', - ' <div class="carousel-item">item 3</div>', - ' </div>', - ' <div class="carousel-control-prev"></div>', - ' <div class="carousel-control-next"></div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(window, 'clearInterval') - - carousel.pause() - - expect(carousel.cycle).toHaveBeenCalledWith(true) - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(true) - }) - - it('should not call cycle if nothing is in transition', () => { - 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 class="carousel-control-prev"></div>', - ' <div class="carousel-control-next"></div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(carousel, 'cycle') - spyOn(window, 'clearInterval') - - carousel.pause() - - expect(carousel.cycle).not.toHaveBeenCalled() - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(true) - }) - - it('should not set is paused at true if an event is passed', () => { - 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 class="carousel-control-prev"></div>', - ' <div class="carousel-control-next"></div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - const event = createEvent('mouseenter') - - spyOn(window, 'clearInterval') - - carousel.pause(event) - - expect(window.clearInterval).toHaveBeenCalled() - expect(carousel._isPaused).toEqual(false) + it('should trigger transitionend if the carousel have carousel-item-next or carousel-item-prev class, cause 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 carousel-item-next">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + ' <div class="carousel-control-prev"></div>', + ' <div class="carousel-control-next"></div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) + const spy = spyOn(carousel, '_clearInterval') + + carouselEl.addEventListener('transitionend', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) + + carousel._slide('next') + carousel.pause() + }) }) }) @@ -931,35 +990,11 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl) - spyOn(window, 'setInterval').and.callThrough() + const spy = spyOn(window, 'setInterval').and.callThrough() carousel.cycle() - expect(window.setInterval).toHaveBeenCalled() - }) - - it('should not set interval if the carousel is paused', () => { - 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 class="carousel-control-prev"></div>', - ' <div class="carousel-control-next"></div>', - '</div>' - ].join('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl) - - spyOn(window, 'setInterval').and.callThrough() - - carousel._isPaused = true - carousel.cycle(true) - - expect(window.setInterval).not.toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should clear interval if there is one', () => { @@ -980,13 +1015,13 @@ describe('Carousel', () => { carousel._interval = setInterval(noop, 10) - spyOn(window, 'setInterval').and.callThrough() - spyOn(window, 'clearInterval').and.callThrough() + const spySet = spyOn(window, 'setInterval').and.callThrough() + const spyClear = spyOn(window, 'clearInterval').and.callThrough() carousel.cycle() - expect(window.setInterval).toHaveBeenCalled() - expect(window.clearInterval).toHaveBeenCalled() + expect(spySet).toHaveBeenCalled() + expect(spyClear).toHaveBeenCalled() }) it('should get interval from data attribute on the active item element', () => { @@ -1020,51 +1055,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('') - - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, {}) + 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, {}) - 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() + }) }) }) @@ -1095,7 +1134,7 @@ describe('Carousel', () => { expect(spy).not.toHaveBeenCalled() }) - it('should call pause and cycle is the provided is the same compare to the current one', () => { + it('should not continue if the provided is the same compare to the current one', () => { fixtureEl.innerHTML = [ '<div id="myCarousel" class="carousel slide">', ' <div class="carousel-inner">', @@ -1109,50 +1148,49 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('#myCarousel') const carousel = new Carousel(carouselEl, {}) - spyOn(carousel, '_slide') - spyOn(carousel, 'pause') - spyOn(carousel, 'cycle') + const spy = spyOn(carousel, '_slide') carousel.to(0) - expect(carousel._slide).not.toHaveBeenCalled() - expect(carousel.pause).toHaveBeenCalled() - expect(carousel.cycle).toHaveBeenCalled() + expect(spy).not.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') + const spyOne = spyOn(EventHandler, 'one').and.callThrough() + const spySlide = 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(spySlide).not.toHaveBeenCalled() + expect(spyOne).toHaveBeenCalled() - spyOn(carousel, 'to') + const spyTo = spyOn(carousel, 'to') - EventHandler.trigger(carouselEl, 'slid.bs.carousel') + EventHandler.trigger(carouselEl, 'slid.bs.carousel') - setTimeout(() => { - expect(carousel.to).toHaveBeenCalledWith(1) - done() + setTimeout(() => { + expect(spyTo).toHaveBeenCalledWith(1) + resolve() + }) }) }) }) + describe('rtl function', () => { it('"_directionToOrder" and "_orderToDirection" must return the right results', () => { fixtureEl.innerHTML = '<div></div>' @@ -1161,9 +1199,7 @@ describe('Carousel', () => { const carousel = new Carousel(carouselEl, {}) expect(carousel._directionToOrder('left')).toEqual('next') - expect(carousel._directionToOrder('prev')).toEqual('prev') expect(carousel._directionToOrder('right')).toEqual('prev') - expect(carousel._directionToOrder('next')).toEqual('next') expect(carousel._orderToDirection('next')).toEqual('left') expect(carousel._orderToDirection('prev')).toEqual('right') @@ -1175,12 +1211,10 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - expect(isRTL()).toEqual(true, 'rtl has to be true') + expect(isRTL()).toBeTrue() expect(carousel._directionToOrder('left')).toEqual('prev') - expect(carousel._directionToOrder('prev')).toEqual('prev') expect(carousel._directionToOrder('right')).toEqual('next') - expect(carousel._directionToOrder('next')).toEqual('next') expect(carousel._orderToDirection('next')).toEqual('right') expect(carousel._orderToDirection('prev')).toEqual('left') @@ -1192,16 +1226,14 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - const spy = spyOn(carousel, '_directionToOrder').and.callThrough() - const spy2 = spyOn(carousel, '_orderToDirection').and.callThrough() - carousel._slide('left') - expect(spy).toHaveBeenCalledWith('left') - expect(spy2).toHaveBeenCalledWith('next') + const spy = spyOn(carousel, '_orderToDirection').and.callThrough() - carousel._slide('right') - expect(spy).toHaveBeenCalledWith('right') - expect(spy2).toHaveBeenCalledWith('prev') + carousel._slide(carousel._directionToOrder('left')) + expect(spy).toHaveBeenCalledWith('next') + + carousel._slide(carousel._directionToOrder('right')) + expect(spy).toHaveBeenCalledWith('prev') }) it('"_slide" has to call "_directionToOrder" and "_orderToDirection" when rtl=true', () => { @@ -1210,16 +1242,13 @@ describe('Carousel', () => { const carouselEl = fixtureEl.querySelector('div') const carousel = new Carousel(carouselEl, {}) - const spy = spyOn(carousel, '_directionToOrder').and.callThrough() - const spy2 = spyOn(carousel, '_orderToDirection').and.callThrough() + const spy = spyOn(carousel, '_orderToDirection').and.callThrough() - carousel._slide('left') - expect(spy).toHaveBeenCalledWith('left') - expect(spy2).toHaveBeenCalledWith('prev') + carousel._slide(carousel._directionToOrder('left')) + expect(spy).toHaveBeenCalledWith('prev') - carousel._slide('right') - expect(spy).toHaveBeenCalledWith('right') - expect(spy2).toHaveBeenCalledWith('next') + carousel._slide(carousel._directionToOrder('right')) + expect(spy).toHaveBeenCalledWith('next') document.documentElement.dir = 'ltl' }) @@ -1292,7 +1321,7 @@ describe('Carousel', () => { const div = fixtureEl.querySelector('div') - expect(Carousel.getInstance(div)).toEqual(null) + expect(Carousel.getInstance(div)).toBeNull() }) }) @@ -1313,7 +1342,7 @@ describe('Carousel', () => { const div = fixtureEl.querySelector('div') - expect(Carousel.getInstance(div)).toEqual(null) + expect(Carousel.getInstance(div)).toBeNull() expect(Carousel.getOrCreateInstance(div)).toBeInstanceOf(Carousel) }) @@ -1322,7 +1351,7 @@ describe('Carousel', () => { const div = fixtureEl.querySelector('div') - expect(Carousel.getInstance(div)).toEqual(null) + expect(Carousel.getInstance(div)).toBeNull() const carousel = Carousel.getOrCreateInstance(div, { interval: 1 }) @@ -1385,14 +1414,14 @@ describe('Carousel', () => { const carousel = new Carousel(div) const slideTo = 2 - spyOn(carousel, 'to') + const spy = spyOn(carousel, 'to') jQueryMock.fn.carousel = Carousel.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.carousel.call(jQueryMock, slideTo) - expect(carousel.to).toHaveBeenCalledWith(slideTo) + expect(spy).toHaveBeenCalledWith(slideTo) }) it('should throw error on undefined method', () => { @@ -1418,79 +1447,86 @@ describe('Carousel', () => { const loadEvent = createEvent('load') window.dispatchEvent(loadEvent) - - expect(Carousel.getInstance(carouselEl)).not.toBeNull() + const carousel = Carousel.getInstance(carouselEl) + expect(carousel._interval).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"></div>', - '</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.classList.contains('active')).toEqual(true) - 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"></button>', - ' <a id="next" class="carousel-control-next" href="#myCarousel" role="button" data-bs-slide="next"></div>', - '</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.classList.contains('active')).toEqual(true) - 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" data-bs-ride="true">', + ' <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.classList.contains('active')).toEqual(true) - done() - }, 10) + setTimeout(() => { + expect(item2).toHaveClass('active') + expect(Carousel.getInstance('#myCarousel')._interval).not.toBeNull() + resolve() + }, 10) + }) }) it('should do nothing if no selector on click on arrows', () => { @@ -1521,8 +1557,8 @@ describe('Carousel', () => { ' <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"></div>', - ' <button id="next" class="carousel-control-next" data-bs-target="#myCarousel" type="button" data-bs-slide="next"></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('') diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 89d20a6d8..58c536752 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -1,6 +1,6 @@ -import Collapse from '../../src/collapse' -import EventHandler from '../../src/dom/event-handler' -import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' +import Collapse from '../../src/collapse.js' +import EventHandler from '../../src/dom/event-handler.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Collapse', () => { let fixtureEl @@ -112,11 +112,11 @@ describe('Collapse', () => { const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl) - spyOn(collapse, 'show') + const spy = spyOn(collapse, 'show') collapse.toggle() - expect(collapse.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should call hide method if show class is present', () => { @@ -127,44 +127,46 @@ describe('Collapse', () => { toggle: false }) - spyOn(collapse, 'hide') + const spy = spyOn(collapse, 'hide') collapse.toggle() - expect(collapse.hide).toHaveBeenCalled() + expect(spy).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.classList.contains('show')).toEqual(true) - expect(collapseEl1.classList.contains('show')).toEqual(false) - done() + collapseList[1].toggle() }) - - collapseList[1].toggle() }) }) @@ -172,7 +174,7 @@ describe('Collapse', () => { it('should do nothing if is transitioning', () => { fixtureEl.innerHTML = '<div></div>' - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { @@ -182,13 +184,13 @@ describe('Collapse', () => { collapse._isTransitioning = true collapse.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should do nothing if already shown', () => { fixtureEl.innerHTML = '<div class="show"></div>' - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { @@ -197,205 +199,218 @@ describe('Collapse', () => { collapse.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).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.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - expect(el2.classList.contains('show')).toEqual(true) - 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 class="accordion-header">', + ' <button data-bs-target="#parentContent" data-bs-toggle="collapse" class="accordion-toggle">Parent</button>', + ' </div>', + ' <div id="parentContent" class="accordion-collapse collapse" data-bs-parent="#parentGroup">', + ' <div class="accordion-body">', + ' <div id="childGroup" class="accordion">', + ' <div class="accordion-item">', + ' <div class="accordion-header">', + ' <button data-bs-target="#childContent1" data-bs-toggle="collapse" class="accordion-toggle">Child 1</button>', + ' </div>', + ' <div id="childContent1" class="accordion-collapse collapse" data-bs-parent="#childGroup">', + ' <div>content</div>', + ' </div>', + ' </div>', + ' <div class="accordion-item">', + ' <div class="accordion-header">', + ' <button data-bs-target="#childContent2" data-bs-toggle="collapse" class="accordion-toggle">Child 2</button>', + ' </div>', + ' <div id="childContent2" class="accordion-collapse collapse" 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.classList.contains('show')).toEqual(true) - childBtn1.click() + parentBtn.click() }) - childCollapseEl1.addEventListener('shown.bs.collapse', () => { - expect(childCollapseEl1.classList.contains('show')).toEqual(true) - childBtn2.click() - }) - childCollapseEl2.addEventListener('shown.bs.collapse', () => { - expect(childCollapseEl2.classList.contains('show')).toEqual(true) - expect(childCollapseEl1.classList.contains('show')).toEqual(false) - 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">', + ' <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" 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.classList.contains('show')).toEqual(true) - 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, reject) => { + 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', () => { + reject(new Error('should not fire shown event')) + }) - collapse.show() + collapse.show() + }) }) }) @@ -403,7 +418,7 @@ describe('Collapse', () => { it('should do nothing if is transitioning', () => { fixtureEl.innerHTML = '<div></div>' - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { @@ -413,13 +428,13 @@ describe('Collapse', () => { collapse._isTransitioning = true collapse.hide() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should do nothing if already shown', () => { fixtureEl.innerHTML = '<div></div>' - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') const collapseEl = fixtureEl.querySelector('div') const collapse = new Collapse(collapseEl, { @@ -428,51 +443,55 @@ describe('Collapse', () => { collapse.hide() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).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.classList.contains('show')).toEqual(false) - 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, reject) => { + 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', () => { + reject(new Error('should not fire hidden event')) + }) - collapse.hide() + collapse.hide() + }) }) }) @@ -489,416 +508,438 @@ describe('Collapse', () => { collapse.dispose() - expect(Collapse.getInstance(collapseEl)).toEqual(null) + expect(Collapse.getInstance(collapseEl)).toBeNull() }) }) 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') + + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + triggerEl.addEventListener('click', event => { + expect(event.target.isEqualNode(nestedTriggerEl)).toBeTrue() + expect(event.delegateTarget.isEqualNode(triggerEl)).toBeTrue() + expect(spy).toHaveBeenCalled() + resolve() + }) - triggerEl.addEventListener('click', event => { - expect(event.target.isEqualNode(nestedTriggerEl)).toEqual(true) - expect(event.delegateTarget.isEqualNode(triggerEl)).toEqual(true) - 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.classList.contains('collapsed')).toEqual(false) - expect(collapse1.classList.contains('show')).toEqual(true) - expect(collapse1.classList.contains('show')).toEqual(true) - 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.classList.contains('collapsed')).toEqual(true) - expect(collapse1.classList.contains('show')).toEqual(false) - expect(collapse1.classList.contains('show')).toEqual(false) - 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.classList.contains('collapsed')).toEqual(false) - expect(link2.classList.contains('collapsed')).toEqual(false) - 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.classList.contains('collapsed')).toEqual(true) - expect(link2.classList.contains('collapsed')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) + 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" 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" 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.classList.contains('show')).toEqual(false) - expect(collapseTwo.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - expect(target.checked).toEqual(true) - 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.classList.contains('show')).toEqual(true) - expect(triggerEl.classList.contains('collapsed')).toEqual(false) - expect(triggerEl.getAttribute('aria-expanded')).toEqual('true') - - expect(collapseTwoEl.classList.contains('show')).toEqual(false) - expect(triggerTwoEl.classList.contains('collapsed')).toEqual(true) - expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('false') - - collapseTwoEl.addEventListener('shown.bs.collapse', () => { - expect(collapseOneEl.classList.contains('show')).toEqual(false) - expect(triggerEl.classList.contains('collapsed')).toEqual(true) - 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" 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" 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.classList.contains('show')).toEqual(true) - expect(triggerTwoEl.classList.contains('collapsed')).toEqual(false) - 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.classList.contains('show')).toEqual(true) - expect(collapseOneTwo.classList.contains('show')).toEqual(true) - - expect(collapseTwoOne.classList.contains('show')).toEqual(false) - expect(collapseTwoTwo.classList.contains('show')).toEqual(false) - - triggerTwo.click() - } - - function secondTest() { - expect(collapseOneOne.classList.contains('show')).toEqual(false) - expect(collapseOneTwo.classList.contains('show')).toEqual(false) - - expect(collapseTwoOne.classList.contains('show')).toEqual(true) - expect(collapseTwoTwo.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) - expect(nestedCollapseOne.classList.contains('show')).toEqual(false) - - 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.classList.contains('show')).toEqual(true) - expect(collapseTwo.classList.contains('show')).toEqual(false) - expect(nestedCollapseOne.classList.contains('show')).toEqual(true) + collapseOneTwo.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.one) { + firstTest() + } else { + collapsedElements.one = true + } + }) - collapseTwo.addEventListener('shown.bs.collapse', () => { - expect(collapseOne.classList.contains('show')).toEqual(false) - expect(collapseTwo.classList.contains('show')).toEqual(true) - expect(nestedCollapseOne.classList.contains('show')).toEqual(true) - 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">', + ' <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"></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"></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.classList.contains('collapsed')).toEqual(false) - 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.classList.contains('collapsed')).toEqual(false) - expect(trigger2.getAttribute('aria-expanded')).toEqual('true') + triggerTwo.click() + nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) + } - expect(trigger3.classList.contains('collapsed')).toEqual(false) - expect(trigger3.getAttribute('aria-expanded')).toEqual('true') + collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) + trigger.click() + }) + }) - target2.addEventListener('hidden.bs.collapse', () => { - expect(trigger1.classList.contains('collapsed')).toEqual(false) + 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="#0/my/id"></a>', + '<a id="trigger3" role="button" data-bs-toggle="collapse" href=".multi"></a>', + '<div id="test1" class="multi"></div>', + '<div id="0/my/id" 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(`#${CSS.escape('0/my/id')}`) + + const target2Shown = () => { + expect(trigger1).not.toHaveClass('collapsed') expect(trigger1.getAttribute('aria-expanded')).toEqual('true') - expect(trigger2.classList.contains('collapsed')).toEqual(true) - expect(trigger2.getAttribute('aria-expanded')).toEqual('false') + expect(trigger2).not.toHaveClass('collapsed') + expect(trigger2.getAttribute('aria-expanded')).toEqual('true') - expect(trigger3.classList.contains('collapsed')).toEqual(false) + expect(trigger3).not.toHaveClass('collapsed') expect(trigger3.getAttribute('aria-expanded')).toEqual('true') - target1.addEventListener('hidden.bs.collapse', () => { - expect(trigger1.classList.contains('collapsed')).toEqual(true) - 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.classList.contains('collapsed')).toEqual(true) + expect(trigger2).toHaveClass('collapsed') expect(trigger2.getAttribute('aria-expanded')).toEqual('false') - expect(trigger3.classList.contains('collapsed')).toEqual(true) - 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') - target2.addEventListener('shown.bs.collapse', target2Shown) - trigger3.click() + expect(trigger3).toHaveClass('collapsed') + expect(trigger3.getAttribute('aria-expanded')).toEqual('false') + resolve() + }) + + trigger1.click() + }) + + trigger2.click() + } + + target2.addEventListener('shown.bs.collapse', target2Shown) + trigger3.click() + }) }) }) @@ -961,7 +1002,7 @@ describe('Collapse', () => { const div = fixtureEl.querySelector('div') - expect(Collapse.getInstance(div)).toEqual(null) + expect(Collapse.getInstance(div)).toBeNull() }) }) @@ -982,7 +1023,7 @@ describe('Collapse', () => { const div = fixtureEl.querySelector('div') - expect(Collapse.getInstance(div)).toEqual(null) + expect(Collapse.getInstance(div)).toBeNull() expect(Collapse.getOrCreateInstance(div)).toBeInstanceOf(Collapse) }) @@ -991,13 +1032,13 @@ describe('Collapse', () => { const div = fixtureEl.querySelector('div') - expect(Collapse.getInstance(div)).toEqual(null) + expect(Collapse.getInstance(div)).toBeNull() const collapse = Collapse.getOrCreateInstance(div, { toggle: false }) expect(collapse).toBeInstanceOf(Collapse) - expect(collapse._config.toggle).toEqual(false) + expect(collapse._config.toggle).toBeFalse() }) it('should return the instance when exists without given configuration', () => { @@ -1015,7 +1056,7 @@ describe('Collapse', () => { expect(collapse).toBeInstanceOf(Collapse) expect(collapse2).toEqual(collapse) - expect(collapse2._config.toggle).toEqual(false) + expect(collapse2._config.toggle).toBeFalse() }) }) }) diff --git a/js/tests/unit/dom/data.spec.js b/js/tests/unit/dom/data.spec.js index 2560caff7..04e57a8bc 100644 --- a/js/tests/unit/dom/data.spec.js +++ b/js/tests/unit/dom/data.spec.js @@ -1,5 +1,5 @@ -import Data from '../../../src/dom/data' -import { getFixture, clearFixture } from '../../helpers/fixture' +import Data from '../../../src/dom/data.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Data', () => { const TEST_KEY = 'bs.test' @@ -50,7 +50,7 @@ describe('Data', () => { Data.set(div, TEST_KEY, data) - expect(Data.get(div, TEST_KEY)).toBe(data) + expect(Data.get(div, TEST_KEY)).toEqual(data) }) it('should overwrite data if something is already stored', () => { @@ -60,11 +60,12 @@ describe('Data', () => { Data.set(div, TEST_KEY, data) Data.set(div, TEST_KEY, copy) + // Using `toBe` since spread creates a shallow copy expect(Data.get(div, TEST_KEY)).not.toBe(data) expect(Data.get(div, TEST_KEY)).toBe(copy) }) - it('should do nothing when an element have nothing stored', () => { + it('should do nothing when an element has nothing stored', () => { Data.remove(div, TEST_KEY) expect().nothing() @@ -76,7 +77,7 @@ describe('Data', () => { Data.set(div, TEST_KEY, data) Data.remove(div, UNKNOWN_KEY) - expect(Data.get(div, TEST_KEY)).toBe(data) + expect(Data.get(div, TEST_KEY)).toEqual(data) }) it('should remove data for a given key', () => { @@ -88,7 +89,6 @@ describe('Data', () => { expect(Data.get(div, TEST_KEY)).toBeNull() }) - /* eslint-disable no-console */ it('should console.error a message if called with multiple keys', () => { console.error = jasmine.createSpy('console.error') @@ -99,7 +99,6 @@ describe('Data', () => { Data.set(div, UNKNOWN_KEY, copy) expect(console.error).toHaveBeenCalled() - expect(Data.get(div, UNKNOWN_KEY)).toBe(null) + expect(Data.get(div, UNKNOWN_KEY)).toBeNull() }) - /* eslint-enable no-console */ }) diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js index 19d63492b..7f99c4122 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 EventHandler from '../../../src/dom/event-handler.js' +import { noop } from '../../../src/util/index.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' 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>' + it('should add native namespaced event listener', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('div') - EventHandler.on(div, 'click.namespace', () => { - expect().nothing() - done() - }) + EventHandler.on(div, 'click.namespace', () => { + expect().nothing() + resolve() + }) - EventHandler.trigger(div, 'click') + EventHandler.trigger(div, 'click') + }) }) - it('should handle event delegation', done => { - EventHandler.on(document, 'click', '.test', () => { - expect().nothing() - done() - }) + it('should handle event delegation', () => { + return new Promise(resolve => { + EventHandler.on(document, 'click', '.test', () => { + expect().nothing() + resolve() + }) - fixtureEl.innerHTML = '<div class="test"></div>' + fixtureEl.innerHTML = '<div class="test"></div>' - const div = fixtureEl.querySelector('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>' - ] - - 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()).toBe(2) - expect(leaveSpy.calls.count()).toBe(2) - expect(delegateEnterSpy.calls.count()).toBe(2) - expect(delegateLeaveSpy.calls.count()).toBe(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()).toBe(1) - expect(leaveSpy.calls.count()).toBe(1) - expect(delegateEnterSpy.calls.count()).toBe(1) - expect(delegateLeaveSpy.calls.count()).toBe(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++ - }) - EventHandler.on(div, 'click.namespace2', () => { - called2++ + 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.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, reject) => { + fixtureEl.innerHTML = '<div></div>' - const div = fixtureEl.querySelector('div') - const handler = () => { - throw new Error('called') - } + const div = fixtureEl.querySelector('div') + const handler = () => { + reject(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', () => { @@ -412,4 +441,40 @@ describe('EventHandler', () => { expect(i).toEqual(5) }) }) + + describe('general functionality', () => { + it('should hydrate properties, and make them configurable', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div id="div1">', + ' <div id="div2"></div>', + ' <div id="div3"></div>', + '</div>' + ].join('') + + const div1 = fixtureEl.querySelector('#div1') + const div2 = fixtureEl.querySelector('#div2') + + EventHandler.on(div1, 'click', event => { + expect(event.currentTarget).toBe(div2) + expect(event.delegateTarget).toBe(div1) + expect(event.originalTarget).toBeNull() + + Object.defineProperty(event, 'currentTarget', { + configurable: true, + get() { + return div1 + } + }) + + expect(event.currentTarget).toBe(div1) + resolve() + }) + + expect(() => { + EventHandler.trigger(div1, 'click', { originalTarget: null, currentTarget: div2 }) + }).not.toThrowError(TypeError) + }) + }) + }) }) diff --git a/js/tests/unit/dom/manipulator.spec.js b/js/tests/unit/dom/manipulator.spec.js index 61ffe7455..9d0be3218 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 Manipulator from '../../../src/dom/manipulator.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Manipulator', () => { let fixtureEl @@ -70,6 +70,17 @@ describe('Manipulator', () => { target: '#element' }) }) + + it('should omit `bs-config` data attribute', () => { + fixtureEl.innerHTML = '<div data-bs-toggle="tabs" data-bs-target="#element" data-bs-config=\'{"testBool":false}\'></div>' + + const div = fixtureEl.querySelector('div') + + expect(Manipulator.getDataAttributes(div)).toEqual({ + toggle: 'tabs', + target: '#element' + }) + }) }) describe('getDataAttribute', () => { @@ -96,93 +107,29 @@ describe('Manipulator', () => { const div = fixtureEl.querySelector('div') - expect(Manipulator.getDataAttribute(div, 'test')).toEqual(false) + expect(Manipulator.getDataAttribute(div, 'test')).toBeFalse() div.setAttribute('data-bs-test', 'true') - expect(Manipulator.getDataAttribute(div, 'test')).toEqual(true) + expect(Manipulator.getDataAttribute(div, 'test')).toBeTrue() div.setAttribute('data-bs-test', '1') expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1) }) - }) - - describe('offset', () => { - it('should return an object with two properties top and left, both numbers', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - const offset = Manipulator.offset(div) - - expect(offset).toBeDefined() - expect(offset.top).toEqual(jasmine.any(Number)) - expect(offset.left).toEqual(jasmine.any(Number)) - }) - - it('should return offset relative to attached element\'s offset', () => { - const top = 500 - const left = 1000 - - fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>` - - const div = fixtureEl.querySelector('div') - const offset = Manipulator.offset(div) - const fixtureOffset = Manipulator.offset(fixtureEl) - - expect(offset).toEqual({ - top: fixtureOffset.top + top, - left: fixtureOffset.left + left - }) - }) - - it('should not change offset when viewport is scrolled', done => { - 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>` + it('should normalize json data', () => { + fixtureEl.innerHTML = '<div data-bs-test=\'{"delay":{"show":100,"hide":10}}\'></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) - - const scrollHandler = () => { - expect(window.pageYOffset).toBe(scrollY) - expect(window.pageXOffset).toBe(scrollX) - const newOffset = Manipulator.offset(div) + expect(Manipulator.getDataAttribute(div, 'test')).toEqual({ delay: { show: 100, hide: 10 } }) - expect(newOffset).toEqual({ - top: offset.top, - left: offset.left - }) - - win.removeEventListener('scroll', scrollHandler) - forceScrollBars.remove() - win.scrollTo(0, 0) - done() - } - - win.addEventListener('scroll', scrollHandler) - win.scrollTo(scrollX, scrollY) - }) - }) - - describe('position', () => { - it('should return an object with two properties top and left, both numbers', () => { - fixtureEl.innerHTML = '<div></div>' - - const div = fixtureEl.querySelector('div') - const position = Manipulator.position(div) + const objectData = { 'Super Hero': ['Iron Man', 'Super Man'], testNum: 90, url: 'http://localhost:8080/test?foo=bar' } + const dataStr = JSON.stringify(objectData) + div.setAttribute('data-bs-test', encodeURIComponent(dataStr)) + expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData) - expect(position).toBeDefined() - expect(position.top).toEqual(jasmine.any(Number)) - expect(position.left).toEqual(jasmine.any(Number)) + div.setAttribute('data-bs-test', dataStr) + expect(Manipulator.getDataAttribute(div, 'test')).toEqual(objectData) }) }) }) diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js index 09c85a88a..95d9bf8ec 100644 --- a/js/tests/unit/dom/selector-engine.spec.js +++ b/js/tests/unit/dom/selector-engine.spec.js @@ -1,5 +1,5 @@ -import SelectorEngine from '../../../src/dom/selector-engine' -import { getFixture, clearFixture } from '../../helpers/fixture' +import SelectorEngine from '../../../src/dom/selector-engine.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('SelectorEngine', () => { let fixtureEl @@ -21,7 +21,7 @@ describe('SelectorEngine', () => { expect(SelectorEngine.find('div', fixtureEl)).toEqual([div]) }) - it('should find elements globaly', () => { + it('should find elements globally', () => { fixtureEl.innerHTML = '<div id="test"></div>' const div = fixtureEl.querySelector('#test') @@ -30,13 +30,15 @@ describe('SelectorEngine', () => { }) it('should handle :scope selectors', () => { - fixtureEl.innerHTML = `<ul> - <li></li> - <li> - <a href="#" class="active">link</a> - </li> - <li></li> - </ul>` + fixtureEl.innerHTML = [ + '<ul>', + ' <li></li>', + ' <li>', + ' <a href="#" class="active">link</a>', + ' </li>', + ' <li></li>', + '</ul>' + ].join('') const listEl = fixtureEl.querySelector('ul') const aActive = fixtureEl.querySelector('.active') @@ -57,11 +59,13 @@ describe('SelectorEngine', () => { describe('children', () => { it('should find children', () => { - fixtureEl.innerHTML = `<ul> - <li></li> - <li></li> - <li></li> - </ul>` + fixtureEl.innerHTML = [ + '<ul>', + ' <li></li>', + ' <li></li>', + ' <li></li>', + '</ul>' + ].join('') const list = fixtureEl.querySelector('ul') const liList = [].concat(...fixtureEl.querySelectorAll('li')) @@ -73,7 +77,7 @@ describe('SelectorEngine', () => { describe('parents', () => { it('should return parents', () => { - expect(SelectorEngine.parents(fixtureEl, 'body').length).toEqual(1) + expect(SelectorEngine.parents(fixtureEl, 'body')).toHaveSize(1) }) }) @@ -162,7 +166,7 @@ describe('SelectorEngine', () => { '<span>lorem</span>', '<a>lorem</a>', '<button>lorem</button>', - '<input />', + '<input>', '<textarea></textarea>', '<select></select>', '<details>lorem</details>' @@ -197,9 +201,7 @@ describe('SelectorEngine', () => { }) it('should return not return elements with negative tab index', () => { - fixtureEl.innerHTML = [ - '<button tabindex="-1">lorem</button>' - ].join('') + fixtureEl.innerHTML = '<button tabindex="-1">lorem</button>' const expectedElements = [] @@ -207,9 +209,7 @@ describe('SelectorEngine', () => { }) it('should return contenteditable elements', () => { - fixtureEl.innerHTML = [ - '<div contenteditable="true">lorem</div>' - ].join('') + fixtureEl.innerHTML = '<div contenteditable="true">lorem</div>' const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')] @@ -217,9 +217,7 @@ describe('SelectorEngine', () => { }) it('should not return disabled elements', () => { - fixtureEl.innerHTML = [ - '<button disabled="true">lorem</button>' - ].join('') + fixtureEl.innerHTML = '<button disabled="true">lorem</button>' const expectedElements = [] @@ -227,14 +225,190 @@ describe('SelectorEngine', () => { }) it('should not return invisible elements', () => { - fixtureEl.innerHTML = [ - '<button style="display:none;">lorem</button>' - ].join('') + fixtureEl.innerHTML = '<button style="display:none;">lorem</button>' const expectedElements = [] expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements) }) }) -}) + describe('getSelectorFromElement', () => { + it('should get selector from data-bs-target', () => { + fixtureEl.innerHTML = [ + '<div id="test" data-bs-target=".target"></div>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '<a id="test" href=".target"></a>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if data-bs-target equal to #', () => { + fixtureEl.innerHTML = [ + '<a id="test" data-bs-target="#" href=".target"></a>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should return null if a selector from a href is a url without an anchor', () => { + fixtureEl.innerHTML = [ + '<a id="test" data-bs-target="#" href="foo/bar.html"></a>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return the anchor if a selector from a href is a url', () => { + fixtureEl.innerHTML = [ + '<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>', + '<div id="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toEqual('#target') + }) + + it('should return null if selector not found', () => { + fixtureEl.innerHTML = '<a id="test" href=".target"></a>' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '<div></div>' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getSelectorFromElement(testEl)).toBeNull() + }) + }) + + describe('getElementFromSelector', () => { + it('should get element from data-bs-target', () => { + fixtureEl.innerHTML = [ + '<div id="test" data-bs-target=".target"></div>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should get element from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '<a id="test" href=".target"></a>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) + }) + + it('should return null if element not found', () => { + fixtureEl.innerHTML = '<a id="test" href=".target"></a>' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + + it('should return null if no selector', () => { + fixtureEl.innerHTML = '<div></div>' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getElementFromSelector(testEl)).toBeNull() + }) + }) + + describe('getMultipleElementsFromSelector', () => { + it('should get elements from data-bs-target', () => { + fixtureEl.innerHTML = [ + '<div id="test" data-bs-target=".target"></div>', + '<div class="target"></div>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements if several ids are given', () => { + fixtureEl.innerHTML = [ + '<div id="test" data-bs-target="#target1,#target2"></div>', + '<div class="target" id="target1"></div>', + '<div class="target" id="target2"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements if several ids with special chars are given', () => { + fixtureEl.innerHTML = [ + '<div id="test" data-bs-target="#j_id11:exampleModal,#j_id22:exampleModal"></div>', + '<div class="target" id="j_id11:exampleModal"></div>', + '<div class="target" id="j_id22:exampleModal"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should get elements in array, from href if no data-bs-target set', () => { + fixtureEl.innerHTML = [ + '<a id="test" href=".target"></a>', + '<div class="target"></div>', + '<div class="target"></div>' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toEqual(Array.from(fixtureEl.querySelectorAll('.target'))) + }) + + it('should return empty array if elements not found', () => { + fixtureEl.innerHTML = '<a id="test" href=".target"></a>' + + const testEl = fixtureEl.querySelector('#test') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + + it('should return empty array if no selector', () => { + fixtureEl.innerHTML = '<div></div>' + + const testEl = fixtureEl.querySelector('div') + + expect(SelectorEngine.getMultipleElementsFromSelector(testEl)).toHaveSize(0) + }) + }) +}) diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js index f099e9990..63ae4bd10 100644 --- a/js/tests/unit/dropdown.spec.js +++ b/js/tests/unit/dropdown.spec.js @@ -1,7 +1,9 @@ -import Dropdown from '../../src/dropdown' -import EventHandler from '../../src/dom/event-handler' -import { noop } from '../../src/util/index' -import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Dropdown from '../../src/dropdown.js' +import { noop } from '../../src/util/index.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Dropdown', () => { let fixtureEl @@ -57,36 +59,61 @@ 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('') + it('should work on invalid markup', () => { + return new Promise(resolve => { + // TODO: REMOVE in v6 + fixtureEl.innerHTML = [ + '<div class="dropdown">', + ' <div class="dropdown-menu">', + ' <a class="dropdown-item" href="#">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() + const dropdownElem = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(dropdownElem) + + dropdownElem.addEventListener('shown.bs.dropdown', () => { + resolve() + }) + + expect().nothing() + dropdown.show() + }) + }) + + 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', () => { @@ -130,7 +157,7 @@ describe('Dropdown', () => { it('should allow to pass config to Popper with `popperConfig` as a function', () => { fixtureEl.innerHTML = [ '<div class="dropdown">', - ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right" >Dropdown</button>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right">Dropdown</button>', ' <div class="dropdown-menu">', ' <a class="dropdown-item" href="#">Secondary link</a>', ' </div>', @@ -145,767 +172,875 @@ describe('Dropdown', () => { const popperConfig = dropdown._getPopperConfig() - expect(getPopperConfig).toHaveBeenCalled() + // Ensure that the function was called with the default config. + expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({ + placement: jasmine.any(String) + })) expect(popperConfig.placement).toEqual('left') }) }) 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.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - spyOn(dropdown1._popper, 'destroy') - btnDropdown2.click() + dropdown1.toggle() }) + }) - secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(dropdown1._popper.destroy).toHaveBeenCalled() - done() - })) + 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('') - dropdown1.toggle() - }) + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - 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('') + document.documentElement.ontouchstart = noop + const spy = spyOn(EventHandler, 'on') + const spyOff = spyOn(EventHandler, 'off') - const defaultValueOnTouchStart = document.documentElement.ontouchstart - 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') + expect(spy).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'on') - spyOn(EventHandler, 'off') + dropdown.toggle() + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + btnDropdown.addEventListener('hidden.bs.dropdown', () => { + expect(btnDropdown).not.toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(spyOff).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) dropdown.toggle() }) + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop) + 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('') - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - dropdown.toggle() + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) + + 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 centered dropdown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropdown-center">', + ' <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) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) - 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 centered', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="dropup-center">', + ' <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-center') + const dropdown = new Dropdown(btnDropdown) - dropupEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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 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 dropendEl = fixtureEl.querySelector('.dropend') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) - dropendEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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 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 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 dropstartEl = fixtureEl.querySelector('.dropstart') - const dropdown = new Dropdown(btnDropdown) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropendEl = fixtureEl.querySelector('.dropend') + const dropdown = new Dropdown(btnDropdown) - dropstartEl.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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 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 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 dropdown = new Dropdown(btnDropdown, { - reference: 'parent' - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropstartEl = fixtureEl.querySelector('.dropstart') + const dropdown = new Dropdown(btnDropdown) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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 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 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: fixtureEl - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: 'parent' + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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 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: { 0: fixtureEl, jquery: 'jQuery' } - }) + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: fixtureEl + }) - btnDropdown.addEventListener('shown.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(true) - 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('') + 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 virtualElement = { - nodeType: 1, - getBoundingClientRect() { - return { - width: 0, - height: 0, - top: 0, - right: 0, - bottom: 0, - left: 0 - } - } - } + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + reference: { 0: fixtureEl, jquery: 'jQuery' } + }) - expect(() => new Dropdown(btnDropdown, { - reference: {} - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + btnDropdown.addEventListener('shown.bs.dropdown', () => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + }) - expect(() => new Dropdown(btnDropdown, { - reference: { - getBoundingClientRect: 'not-a-function' - } - })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') + dropdown.toggle() + }) + }) - // use onFirstUpdate as Poppers internal update is executed async - const dropdown = new Dropdown(btnDropdown, { - reference: virtualElement, - popperConfig: { - onFirstUpdate() { - expect(virtualElement.getBoundingClientRect).toHaveBeenCalled() - expect(btnDropdown.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() + 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 + } } } - }) - spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + expect(() => new Dropdown(btnDropdown, { + reference: {} + })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.') - dropdown.toggle() + 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(spy).toHaveBeenCalled() + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + } + } + }) + + const spy = spyOn(virtualElement, 'getBoundingClientRect').and.callThrough() + + 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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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.classList.contains('show')).toEqual(true) - 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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(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.classList.contains('show')).toEqual(false) - 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, reject) => { + 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', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - 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, reject) => { + 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', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - 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, reject) => { + 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', () => { + reject(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, reject) => { + 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', () => { + reject(new Error('should not throw hidden.bs.dropdown event')) + }) - dropdown.hide() + dropdown.hide() - setTimeout(() => { - expect(dropdownMenu.classList.contains('show')).toEqual(true) - 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="#">Dropdown 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 + const spy = spyOn(EventHandler, 'off') - btnDropdown.addEventListener('shown.bs.dropdown', () => { - dropdown.hide() - }) + btnDropdown.addEventListener('shown.bs.dropdown', () => { + dropdown.hide() + }) - btnDropdown.addEventListener('hidden.bs.dropdown', () => { - expect(btnDropdown.classList.contains('show')).toEqual(false) - 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(spy).toHaveBeenCalled() - document.documentElement.ontouchstart = defaultValueOnTouchStart - done() - }) + document.documentElement.ontouchstart = defaultValueOnTouchStart + resolve() + }) - dropdown.show() + dropdown.show() + }) }) }) @@ -927,13 +1062,13 @@ describe('Dropdown', () => { expect(dropdown._popper).toBeNull() expect(dropdown._menu).not.toBeNull() expect(dropdown._element).not.toBeNull() - spyOn(EventHandler, 'off') + const spy = spyOn(EventHandler, 'off') dropdown.dispose() expect(dropdown._menu).toBeNull() expect(dropdown._element).toBeNull() - expect(EventHandler.off).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) + expect(spy).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY) }) it('should dispose dropdown with Popper', () => { @@ -981,13 +1116,13 @@ describe('Dropdown', () => { expect(dropdown._popper).not.toBeNull() - spyOn(dropdown._popper, 'update') - spyOn(dropdown, '_detectNavbar') + const spyUpdate = spyOn(dropdown._popper, 'update') + const spyDetect = spyOn(dropdown, '_detectNavbar') dropdown.update() - expect(dropdown._popper.update).toHaveBeenCalled() - expect(dropdown._detectNavbar).toHaveBeenCalled() + expect(spyUpdate).toHaveBeenCalled() + expect(spyDetect).toHaveBeenCalled() }) it('should just detect navbar on update', () => { @@ -1003,904 +1138,1083 @@ describe('Dropdown', () => { const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdown = new Dropdown(btnDropdown) - spyOn(dropdown, '_detectNavbar') + const spy = spyOn(dropdown, '_detectNavbar') dropdown.update() expect(dropdown._popper).toBeNull() - expect(dropdown._detectNavbar).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) 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.classList.contains('show')).toEqual(true) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - expect(showEventTriggered).toEqual(true) - 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.classList.contains('show')).toEqual(false) - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') - expect(hideEventTriggered).toEqual(true) - expect(event.relatedTarget).toEqual(btnDropdown) - done() + btnDropdown.click() }) - - btnDropdown.click() }) - it('should not use 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 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).toBeNull() - expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper') - 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`="none" 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 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('none') - 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')).toEqual(null) - 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.classList.contains('show')).toEqual(true) + 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.classList.contains('show')).toEqual(false) - 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.length).toEqual(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(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').length).toEqual(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.classList.contains('show')).toEqual(true) - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(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').length).toEqual(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.length).toEqual(2) + expect(triggerDropdownList).toHaveSize(2) - const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList + const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList - triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownFirst.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + 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').length).toEqual(0, '"show" class removed') - triggerDropdownLast.click() - }) + triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show')).toHaveSize(0) + triggerDropdownLast.click() + }) - triggerDropdownLast.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdownLast.classList.contains('show')).toEqual(true, '"show" class added on click') - expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + 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').length).toEqual(0, '"show" class removed') - 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 => { + it('should be able to identify clicked dropdown, even with multiple dropdowns in the same tag', () => { 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>', + ' <button id="dropdown1" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu1" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + ' <button id="dropdown2" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu2" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', ' </div>', '</div>' ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - - triggerDropdown.addEventListener('hide.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - }) + const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') + const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') + const dropdownMenu1 = fixtureEl.querySelector('#menu1') + const dropdownMenu2 = fixtureEl.querySelector('#menu2') + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() - triggerDropdown.addEventListener('hidden.bs.dropdown', event => { - expect(event.clickEvent).toBeUndefined() - done() - }) + dropdownToggle1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') + dropdownToggle2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) - keydown.key = 'Escape' - triggerDropdown.dispatchEvent(keydown) - }) + dropdownMenu1.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle1) - triggerDropdown.click() + dropdownMenu2.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle2) }) - it('should bubble up the events to the parent elements', done => { + it('should be able to show the proper menu, even with multiple dropdowns in the same tag', () => { 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>', + ' <button id="dropdown1" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu1" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', + ' </div>', + ' <button id="dropdown2" class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', + ' <div id="menu2" class="dropdown-menu">', + ' <a class="dropdown-item" href="#">Dropdown item</a>', ' </div>', '</div>' ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownParent = fixtureEl.querySelector('.dropdown') - const dropdown = new Dropdown(triggerDropdown) + const dropdownToggle1 = fixtureEl.querySelector('#dropdown1') + const dropdownToggle2 = fixtureEl.querySelector('#dropdown2') + const dropdownMenu1 = fixtureEl.querySelector('#menu1') + const dropdownMenu2 = fixtureEl.querySelector('#menu2') - const showFunction = jasmine.createSpy('showFunction') - dropdownParent.addEventListener('show.bs.dropdown', showFunction) + dropdownToggle1.click() + expect(dropdownMenu1).toHaveClass('show') + expect(dropdownMenu2).not.toHaveClass('show') - const shownFunction = jasmine.createSpy('shownFunction') - dropdownParent.addEventListener('shown.bs.dropdown', () => { - shownFunction() - dropdown.hide() - }) + dropdownToggle2.click() + expect(dropdownMenu1).not.toHaveClass('show') + expect(dropdownMenu2).toHaveClass('show') + }) - const hideFunction = jasmine.createSpy('hideFunction') - dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) + 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('') - dropdownParent.addEventListener('hidden.bs.dropdown', () => { - expect(showFunction).toHaveBeenCalled() - expect(shownFunction).toHaveBeenCalled() - expect(hideFunction).toHaveBeenCalled() - done() - }) + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + triggerDropdown.addEventListener('hide.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + }) + + triggerDropdown.addEventListener('hidden.bs.dropdown', event => { + expect(event.clickEvent).toBeUndefined() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') - dropdown.show() + keydown.key = 'Escape' + triggerDropdown.dispatchEvent(keydown) + }) + + triggerDropdown.click() + }) }) - 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 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 input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) - expect(document.activeElement).toEqual(input, 'input still focused') + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) - textarea.focus() - textarea.dispatchEvent(keydown) + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() + dropdown.show() }) - - 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 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 triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') - expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') - done() - }) + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keydown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + 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 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) - expect(document.activeElement.classList.contains('d-none')).toEqual(false, '.d-none not focused') - expect(document.activeElement.style.display).not.toBe('none', '"display: none" not focused') - expect(document.activeElement.style.visibility).not.toBe('hidden', '"visibility: hidden" not focused') + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - done() + 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('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + 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('') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + triggerDropdown.dispatchEvent(keydown) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + resolve() + }) - done() + 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 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 lastItem = 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', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') + + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' + + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + resolve() }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.click() + }) }) - 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 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 firstItem = fixtureEl.querySelector('#item1') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') - 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(lastItem, 'item2 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + 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 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 input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') - input.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - input.dispatchEvent(createEvent('click')) + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) }) - - 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 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 textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - textarea.addEventListener('click', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.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">' - ] + 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 input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true - })) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - 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('') + 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 textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - const keydownSpace = createEvent('keydown') - keydownSpace.key = 'Space' + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + 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`) + } - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - input.focus() - input.dispatchEvent(keydownSpace) + test('Space', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key ArrowUp + test('ArrowUp', input) - textarea.focus() - textarea.dispatchEvent(keydownSpace) + test('ArrowUp', textarea) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + // Key ArrowDown + test('ArrowDown', input) - // Key ArrowUp - input.focus() - input.dispatchEvent(keydownArrowUp) + test('ArrowDown', textarea) - expect(document.activeElement).toEqual(input, 'input still focused') + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - textarea.focus() - textarea.dispatchEvent(keydownArrowUp) + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + triggerDropdown.click() + }) + }) - // Key ArrowDown - input.focus() - input.dispatchEvent(keydownArrowDown) + 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('') - expect(document.activeElement).toEqual(input, 'input still focused') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') - textarea.focus() - textarea.dispatchEvent(keydownArrowDown) + const spy = spyOn(dropdown, 'toggle') - expect(document.activeElement).toEqual(textarea, 'textarea still focused') + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + setTimeout(() => { + expect(spy).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() + }) - expect(triggerDropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') - done() - }) + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - triggerDropdown.click() + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) }) - 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>' - ] + it('should not propagate escape key events if dropdown is open', () => { + 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 triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - spyOn(dropdown, 'toggle') + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).not.toHaveBeenCalled() + resolve() + }) + + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown.classList.contains('show')).toEqual(false) - done() - }, 20) + toggle.click() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) + }) }) - 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>' - ] + 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 parent = fixtureEl.querySelector('.parent') - 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) + }) - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() + toggle.click() }) + }) - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' + 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('') - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - 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>' - ] + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - dropdownMenu.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - })) + 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('') - dropdownToggle.click() - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - 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>' - ] + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - document.documentElement.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle.classList.contains('show')).toEqual(false) - done() - }) + 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('') - dropdownToggle.click() + 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 { + resolve() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + 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 => { + it('should be able to identify clicked dropdown, no matter the markup order', () => { 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>', + ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown toggle</button>', '</div>' - ] + ].join('') const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle.classList.contains('show')).toEqual(true) - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() - } - - expectDropdownToBeOpened(false) - }, 150) - - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + dropdownMenu.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) }) }) @@ -1963,7 +2277,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() }) }) @@ -1984,7 +2298,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() expect(Dropdown.getOrCreateInstance(div)).toBeInstanceOf(Dropdown) }) @@ -1993,7 +2307,7 @@ describe('Dropdown', () => { const div = fixtureEl.querySelector('div') - expect(Dropdown.getInstance(div)).toEqual(null) + expect(Dropdown.getInstance(div)).toBeNull() const dropdown = Dropdown.getOrCreateInstance(div, { display: 'dynamic' }) @@ -2021,52 +2335,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.classList.contains('show')).toEqual(true) - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown.classList.contains('show')).toEqual(true) - 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', () => { @@ -2092,27 +2408,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.classList.contains('show')).toEqual(true) - 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 1c9258bd1..7d7f29dc7 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -1,18 +1,18 @@ /* eslint-env jquery */ -import Alert from '../../src/alert' -import Button from '../../src/button' -import Carousel from '../../src/carousel' -import Collapse from '../../src/collapse' -import Dropdown from '../../src/dropdown' -import Modal from '../../src/modal' -import Offcanvas from '../../src/offcanvas' -import Popover from '../../src/popover' -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 Alert from '../../src/alert.js' +import Button from '../../src/button.js' +import Carousel from '../../src/carousel.js' +import Collapse from '../../src/collapse.js' +import Dropdown from '../../src/dropdown.js' +import Modal from '../../src/modal.js' +import Offcanvas from '../../src/offcanvas.js' +import Popover from '../../src/popover.js' +import ScrollSpy from '../../src/scrollspy.js' +import Tab from '../../src/tab.js' +import Toast from '../../src/toast.js' +import Tooltip from '../../src/tooltip.js' +import { clearFixture, getFixture } from '../helpers/fixture.js' 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').length).toEqual(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 613b0a0a1..2aa0b7655 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -1,7 +1,9 @@ -import Modal from '../../src/modal' -import EventHandler from '../../src/dom/event-handler' -import ScrollBarHelper from '../../src/util/scrollbar' -import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Modal from '../../src/modal.js' +import ScrollBarHelper from '../../src/util/scrollbar.js' +import { + clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Modal', () => { let fixtureEl @@ -56,95 +58,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>' - ].join('') + it('should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(spyHide).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(spyReset).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', () => { @@ -153,12 +161,12 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') modal._isShown = true modal.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should do nothing if a modal is transitioning', () => { @@ -167,521 +175,595 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(EventHandler, 'trigger') + const spy = spyOn(EventHandler, 'trigger') modal._isTransitioning = true modal.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spy).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, reject) => { + 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', () => { + reject(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).toEqual(true) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._isTransitioning).toBeFalse() + resolve() }) - }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._isTransitioning).toEqual(false) - 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) + + const spy = 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(spy).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() + const spy = 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(spy).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() + const spy = spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).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() + const spy = 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(spy).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') + const spy = spyOn(modal, 'hide') - const expectDone = () => { - expect(modal.hide).not.toHaveBeenCalled() + const expectDone = () => { + expect(spy).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() + const spy = spyOn(modal, '_adjustDialog').and.callThrough() - const expectDone = () => { - expect(modal._adjustDialog).toHaveBeenCalled() + const expectDone = () => { + expect(spy).toHaveBeenCalled() - done() - } + resolve() + } - 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, reject) => { + 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 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() - }) - - 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', () => { + reject(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).toEqual(true) - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = false', () => { + return new Promise((resolve, reject) => { + 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', () => { + reject(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).toEqual(true) - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = static', () => { + return new Promise((resolve, reject) => { + 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', () => { + reject(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).toEqual(false) - 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, reject) => { + 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).toEqual(true) - 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', () => { + reject(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() + const mouseDown = createEvent('mousedown') - modalEl.click() - modalEl.click() + modalEl.dispatchEvent(mouseDown) + modalEl.click() + modalEl.dispatchEvent(mouseDown) + 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() + const spy = spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).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 dialogEl = modalEl.querySelector('.modal-dialog') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - }) + const spy = spyOn(modal, 'hide') + + modalEl.addEventListener('shown.bs.modal', () => { + const mouseDown = createEvent('mousedown') + + dialogEl.dispatchEvent(mouseDown) + modalEl.click() + expect(spy).not.toHaveBeenCalled() + + modalEl.dispatchEvent(mouseDown) + modalEl.click() + expect(spy).toHaveBeenCalled() + 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() + modal.show() }) + }) - modal.show() + it('should not close modal when clicking on an element removed from modal content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<div class="modal">', + ' <div class="modal-dialog">', + ' <button class="btn">BTN</button>', + ' </div>', + '</div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const buttonEl = modalEl.querySelector('.btn') + const modal = new Modal(modalEl) + + const spy = spyOn(modal, 'hide') + buttonEl.addEventListener('click', () => { + buttonEl.remove() + }) + + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.dispatchEvent(createEvent('mousedown')) + buttonEl.click() + expect(spy).not.toHaveBeenCalled() + resolve() + }) + + modal.show() + }) }) it('should do nothing is the modal is not shown', () => { @@ -707,52 +789,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, reject) => { + 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).toEqual(true) - 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', () => { + reject(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) + const spy = 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(spy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) @@ -763,17 +849,17 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) const focustrap = modal._focustrap - spyOn(focustrap, 'deactivate').and.callThrough() + const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Modal.getInstance(modalEl)).toEqual(modal) - spyOn(EventHandler, 'off') + const spyOff = spyOn(EventHandler, 'off') modal.dispose() expect(Modal.getInstance(modalEl)).toBeNull() - expect(EventHandler.off).toHaveBeenCalledTimes(3) - expect(focustrap.deactivate).toHaveBeenCalled() + expect(spyOff).toHaveBeenCalledTimes(3) + expect(spyDeactivate).toHaveBeenCalled() }) }) @@ -784,255 +870,298 @@ describe('Modal', () => { const modalEl = fixtureEl.querySelector('.modal') const modal = new Modal(modalEl) - spyOn(modal, '_adjustDialog') + const spy = spyOn(modal, '_adjustDialog') modal.handleUpdate() - expect(modal._adjustDialog).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) 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() + const spy = spyOn(modal, 'show').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal.show).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(spy).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"]') + + const spy = 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(spy).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') + const spy = 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(spy).toHaveBeenCalled() + resolve() + }, 20) + } - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) + + trigger.click() }) + }) + + it('should open modal, having special characters in its id', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#j_id22:exampleModal">', + ' Launch demo modal', + '</button>', + '<div class="modal fade" id="j_id22:exampleModal" aria-labelledby="exampleModalLabel" aria-hidden="true">', + ' <div class="modal-dialog">', + ' <div class="modal-content">', + ' <div class="modal-body">', + ' <p>modal body</p>', + ' </div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + modalEl.addEventListener('shown.bs.modal', () => { + resolve() + }) - 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>', done => { - fixtureEl.innerHTML = [ - '<div class="modal">', - ' <div class="modal-dialog">', - ' <button type="button" data-bs-dismiss="modal"></button>', - ' </div>', - '</div>' - ].join('') + 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) + 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() + const spy = 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).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - 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() + const spy = 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(spy).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"]') + const spy = 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(spy).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"]') + const spy = spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + const showListener = () => { + setTimeout(() => { + expect(spy).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.classList.contains('show')).toBeTrue() - 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>' @@ -1056,12 +1185,12 @@ describe('Modal', () => { jQueryMock.elements = [div] jQueryMock.fn.modal.call(jQueryMock, { keyboard: false }) - spyOn(Modal.prototype, 'constructor') - expect(Modal.prototype.constructor).not.toHaveBeenCalledWith(div, { keyboard: false }) + const spy = spyOn(Modal.prototype, 'constructor') + expect(spy).not.toHaveBeenCalledWith(div, { keyboard: false }) const modal = Modal.getInstance(div) expect(modal).not.toBeNull() - expect(modal._config.keyboard).toBe(false) + expect(modal._config.keyboard).toBeFalse() }) it('should not re create a modal', () => { @@ -1101,11 +1230,11 @@ describe('Modal', () => { jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] - spyOn(modal, 'show') + const spy = spyOn(modal, 'show') jQueryMock.fn.modal.call(jQueryMock, 'show') - expect(modal.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should not call show method', () => { @@ -1116,11 +1245,11 @@ describe('Modal', () => { jQueryMock.fn.modal = Modal.jQueryInterface jQueryMock.elements = [div] - spyOn(Modal.prototype, 'show') + const spy = spyOn(Modal.prototype, 'show') jQueryMock.fn.modal.call(jQueryMock) - expect(Modal.prototype.show).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) }) @@ -1161,7 +1290,7 @@ describe('Modal', () => { const div = fixtureEl.querySelector('div') - expect(Modal.getInstance(div)).toEqual(null) + expect(Modal.getInstance(div)).toBeNull() expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal) }) @@ -1170,13 +1299,13 @@ describe('Modal', () => { const div = fixtureEl.querySelector('div') - expect(Modal.getInstance(div)).toEqual(null) + expect(Modal.getInstance(div)).toBeNull() const modal = Modal.getOrCreateInstance(div, { backdrop: true }) expect(modal).toBeInstanceOf(Modal) - expect(modal._config.backdrop).toEqual(true) + expect(modal._config.backdrop).toBeTrue() }) it('should return the instance when exists without given configuration', () => { @@ -1194,7 +1323,7 @@ describe('Modal', () => { expect(modal).toBeInstanceOf(Modal) expect(modal2).toEqual(modal) - expect(modal2._config.backdrop).toEqual(true) + expect(modal2._config.backdrop).toBeTrue() }) }) }) diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index 3eda50520..3b6c98c10 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -1,8 +1,10 @@ -import Offcanvas from '../../src/offcanvas' -import EventHandler from '../../src/dom/event-handler' -import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' -import { isVisible } from '../../src/util/index' -import ScrollBarHelper from '../../src/util/scrollbar' +import EventHandler from '../../src/dom/event-handler.js' +import Offcanvas from '../../src/offcanvas.js' +import { isVisible } from '../../src/util/index.js' +import ScrollBarHelper from '../../src/util/scrollbar.js' +import { + clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Offcanvas', () => { let fixtureEl @@ -51,12 +53,12 @@ describe('Offcanvas', () => { const closeEl = fixtureEl.querySelector('a') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') closeEl.click() - expect(offCanvas._config.keyboard).toBe(true) - expect(offCanvas.hide).toHaveBeenCalled() + expect(offCanvas._config.keyboard).toBeTrue() + expect(spy).toHaveBeenCalled() }) it('should hide if esc is pressed', () => { @@ -67,11 +69,26 @@ describe('Offcanvas', () => { const keyDownEsc = createEvent('keydown') keyDownEsc.key = 'Escape' - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') offCanvasEl.dispatchEvent(keyDownEsc) - expect(offCanvas.hide).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() + }) + + it('should hide if esc is pressed and backdrop is static', () => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + const spy = spyOn(offCanvas, 'hide') + + offCanvasEl.dispatchEvent(keyDownEsc) + + expect(spy).toHaveBeenCalled() }) it('should not hide if esc is not pressed', () => { @@ -82,66 +99,115 @@ describe('Offcanvas', () => { const keydownTab = createEvent('keydown') keydownTab.key = 'Tab' - spyOn(offCanvas, 'hide') + const spy = spyOn(offCanvas, 'hide') - document.dispatchEvent(keydownTab) + offCanvasEl.dispatchEvent(keydownTab) - expect(offCanvas.hide).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should not hide if esc is pressed but with keyboard = false', () => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) - const keyDownEsc = createEvent('keydown') - keyDownEsc.key = 'Escape' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false }) + const keyDownEsc = createEvent('keydown') + keyDownEsc.key = 'Escape' + + const spy = spyOn(offCanvas, 'hide') + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._config.keyboard).toBeFalse() + offCanvasEl.dispatchEvent(keyDownEsc) + + expect(hidePreventedSpy).toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) + }) - spyOn(offCanvas, 'hide') + it('should not hide if user clicks on static backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - document.dispatchEvent(keyDownEsc) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: 'static' }) - expect(offCanvas._config.keyboard).toBe(false) - expect(offCanvas.hide).not.toHaveBeenCalled() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + const spyClick = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const hidePreventedSpy = jasmine.createSpy('hidePrevented') + offCanvasEl.addEventListener('hidePrevented.bs.offcanvas', hidePreventedSpy) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyClick).toEqual(jasmine.any(Function)) + + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + expect(hidePreventedSpy).toHaveBeenCalled() + expect(spyHide).not.toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) + }) + + it('should call `hide` on resize, if element\'s position is not fixed any more', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas-lg"></div>' + + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + + const spy = spyOn(offCanvas, 'hide').and.callThrough() + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + const resizeEvent = createEvent('resize') + offCanvasEl.style.removeProperty('position') + + window.dispatchEvent(resizeEvent) + expect(spy).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() + }) }) }) describe('config', () => { it('should have default values', () => { - fixtureEl.innerHTML = [ - '<div class="offcanvas">', - '</div>' - ].join('') + fixtureEl.innerHTML = '<div class="offcanvas"></div>' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - expect(offCanvas._config.backdrop).toEqual(true) - expect(offCanvas._backdrop._config.isVisible).toEqual(true) - expect(offCanvas._config.keyboard).toEqual(true) - expect(offCanvas._config.scroll).toEqual(false) + expect(offCanvas._config.backdrop).toBeTrue() + expect(offCanvas._backdrop._config.isVisible).toBeTrue() + expect(offCanvas._config.keyboard).toBeTrue() + expect(offCanvas._config.scroll).toBeFalse() }) it('should read data attributes and override default config', () => { - fixtureEl.innerHTML = [ - '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">', - '</div>' - ].join('') + fixtureEl.innerHTML = '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false"></div>' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - expect(offCanvas._config.backdrop).toEqual(false) - expect(offCanvas._backdrop._config.isVisible).toEqual(false) - expect(offCanvas._config.keyboard).toEqual(false) - expect(offCanvas._config.scroll).toEqual(true) + expect(offCanvas._config.backdrop).toBeFalse() + expect(offCanvas._backdrop._config.isVisible).toBeFalse() + expect(offCanvas._config.keyboard).toBeFalse() + expect(offCanvas._config.scroll).toBeTrue() }) it('given a config object must override data attributes', () => { - fixtureEl.innerHTML = [ - '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">', - '</div>' - ].join('') + fixtureEl.innerHTML = '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false"></div>' const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl, { @@ -149,90 +215,120 @@ describe('Offcanvas', () => { keyboard: true, scroll: false }) - expect(offCanvas._config.backdrop).toEqual(true) - expect(offCanvas._config.keyboard).toEqual(true) - expect(offCanvas._config.scroll).toEqual(false) + expect(offCanvas._config.backdrop).toBeTrue() + expect(offCanvas._config.keyboard).toBeTrue() + expect(offCanvas._config.scroll).toBeFalse() }) }) - describe('options', () => { - it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => { - 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 }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() - offCanvas.hide() + describe('options', () => { + it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyHide).not.toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spyReset).not.toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() - done() + }) + + it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' + + const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spyHide).toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spyReset).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('should hide a shown element if user click on backdrop', () => { + 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 }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() - offCanvas.hide() - }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + const spy = spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + }) + + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) + + offCanvas.show() }) - offCanvas.show() }) - it('should hide a shown element if user click on backdrop', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' - - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) + it('should not trap focus if scroll is allowed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const clickEvent = document.createEvent('MouseEvents') - clickEvent.initEvent('mousedown', true, true) - spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true, + backdrop: false + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(typeof offCanvas._backdrop._config.clickCallback).toBe('function') + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvas._backdrop._getElement().dispatchEvent(clickEvent) - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() - done() + offCanvas.show() }) - - offCanvas.show() }) - it('should not trap focus if scroll is allowed', done => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + it('should trap focus if scroll is allowed OR backdrop is enabled', () => { + 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, + backdrop: true + }) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) @@ -243,30 +339,57 @@ describe('Offcanvas', () => { const offCanvasEl = fixtureEl.querySelector('.offcanvas') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'show') + const spy = spyOn(offCanvas, 'show') offCanvas.toggle() - expect(offCanvas.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should call hide method if show class is present', () => { - fixtureEl.innerHTML = '<div class="offcanvas"></div>' + return new Promise(resolve => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) - offCanvas.show() - expect(offCanvasEl.classList.contains('show')).toBe(true) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + const spy = spyOn(offCanvas, 'hide') - offCanvas.toggle() + offCanvas.toggle() + + expect(spy).toHaveBeenCalled() + resolve() + }) - expect(offCanvas.hide).toHaveBeenCalled() + offCanvas.show() + }) }) }) describe('show', () => { + 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('shown.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + resolve() + }) + + offCanvas.show() + expect(offCanvasEl).toHaveClass('showing') + }) + }) + it('should do nothing if already shown', () => { fixtureEl.innerHTML = '<div class="offcanvas show"></div>' @@ -274,166 +397,206 @@ describe('Offcanvas', () => { const offCanvas = new Offcanvas(offCanvasEl) offCanvas.show() - expect(offCanvasEl.classList.contains('show')).toBe(true) + expect(offCanvasEl).toHaveClass('show') - spyOn(offCanvas._backdrop, 'show').and.callThrough() - spyOn(EventHandler, 'trigger').and.callThrough() + const spyShow = spyOn(offCanvas._backdrop, 'show').and.callThrough() + const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() offCanvas.show() - expect(EventHandler.trigger).not.toHaveBeenCalled() - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() + expect(spyTrigger).not.toHaveBeenCalled() + expect(spyShow).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) + const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(true) - expect(offCanvas._backdrop.show).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(spy).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, reject) => { + fixtureEl.innerHTML = '<div class="offcanvas"></div>' - 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) - } - - offCanvasEl.addEventListener('show.bs.offcanvas', event => { - event.preventDefault() - expectEnd() - }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + const spy = spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - throw new Error('should not fire shown event') - }) + const expectEnd = () => { + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvas.show() + offCanvasEl.addEventListener('show.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) + + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + reject(new Error('should not fire shown event')) + }) + + 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') + const spy = 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(spy).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() + const spy = spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('hide', () => { + 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('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') + }) + }) + }) + it('should do nothing if already shown', () => { fixtureEl.innerHTML = '<div class="offcanvas"></div>' - spyOn(EventHandler, 'trigger').and.callThrough() + const spyTrigger = spyOn(EventHandler, 'trigger').and.callThrough() const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const spyHide = spyOn(offCanvas._backdrop, 'hide').and.callThrough() offCanvas.hide() - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - expect(EventHandler.trigger).not.toHaveBeenCalled() + expect(spyHide).not.toHaveBeenCalled() + expect(spyTrigger).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) + const spy = spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(false) - expect(offCanvas._backdrop.hide).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(spy).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, reject) => { + 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) + const spy = 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(spy).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', () => { + reject(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) + const spy = 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(spy).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) }) @@ -444,41 +607,41 @@ describe('Offcanvas', () => { const offCanvasEl = fixtureEl.querySelector('div') const offCanvas = new Offcanvas(offCanvasEl) const backdrop = offCanvas._backdrop - spyOn(backdrop, 'dispose').and.callThrough() + const spyDispose = spyOn(backdrop, 'dispose').and.callThrough() const focustrap = offCanvas._focustrap - spyOn(focustrap, 'deactivate').and.callThrough() + const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough() expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas) - spyOn(EventHandler, 'off') - offCanvas.dispose() - expect(backdrop.dispose).toHaveBeenCalled() + expect(spyDispose).toHaveBeenCalled() expect(offCanvas._backdrop).toBeNull() - expect(focustrap.deactivate).toHaveBeenCalled() + expect(spyDeactivate).toHaveBeenCalled() expect(offCanvas._focustrap).toBeNull() - expect(Offcanvas.getInstance(offCanvasEl)).toEqual(null) + expect(Offcanvas.getInstance(offCanvasEl)).toBeNull() }) }) 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('') - - const target = fixtureEl.querySelector('input') - const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') - - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl.classList.contains('show')).toEqual(true) - expect(target.checked).toEqual(true) - done() + 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') + + 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', () => { @@ -489,83 +652,89 @@ describe('Offcanvas', () => { const target = fixtureEl.querySelector('a') - spyOn(Offcanvas.prototype, 'toggle') + const spy = spyOn(Offcanvas.prototype, 'toggle') target.click() - 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() + expect(spy).not.toHaveBeenCalled() + }) + + 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() }) - 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() + 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) + const spy = spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - 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)).toBe(false) - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 5) + 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) + const spy = 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(spy).not.toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - - trigger.click() }) }) @@ -644,13 +813,13 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show') + const spy = spyOn(Offcanvas.prototype, 'show') jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface jQueryMock.elements = [div] jQueryMock.fn.offcanvas.call(jQueryMock, 'show') - expect(Offcanvas.prototype.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should create a offcanvas with given config', () => { @@ -665,7 +834,7 @@ describe('Offcanvas', () => { const offcanvas = Offcanvas.getInstance(div) expect(offcanvas).not.toBeNull() - expect(offcanvas._config.scroll).toBe(true) + expect(offcanvas._config.scroll).toBeTrue() }) }) @@ -706,7 +875,7 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - expect(Offcanvas.getInstance(div)).toEqual(null) + expect(Offcanvas.getInstance(div)).toBeNull() expect(Offcanvas.getOrCreateInstance(div)).toBeInstanceOf(Offcanvas) }) @@ -715,13 +884,13 @@ describe('Offcanvas', () => { const div = fixtureEl.querySelector('div') - expect(Offcanvas.getInstance(div)).toEqual(null) + expect(Offcanvas.getInstance(div)).toBeNull() const offcanvas = Offcanvas.getOrCreateInstance(div, { scroll: true }) expect(offcanvas).toBeInstanceOf(Offcanvas) - expect(offcanvas._config.scroll).toEqual(true) + expect(offcanvas._config.scroll).toBeTrue() }) it('should return the instance when exists without given configuration', () => { @@ -739,7 +908,7 @@ describe('Offcanvas', () => { expect(offcanvas).toBeInstanceOf(Offcanvas) expect(offcanvas2).toEqual(offcanvas) - expect(offcanvas2._config.scroll).toEqual(true) + expect(offcanvas2._config.scroll).toBeTrue() }) }) }) diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js index b3bba3180..ba38ebe06 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -1,5 +1,6 @@ -import Popover from '../../src/popover' -import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Popover from '../../src/popover.js' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture.js' describe('Popover', () => { let fixtureEl @@ -42,12 +43,6 @@ describe('Popover', () => { }) }) - describe('Event', () => { - it('should return plugin events', () => { - expect(Popover.Event).toEqual(jasmine.any(Object)) - }) - }) - describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(Popover.EVENT_KEY).toEqual('.bs.popover') @@ -61,166 +56,264 @@ 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', done => { - fixtureEl.innerHTML = '<a href="#">BS twitter</a>' + it('should call content and title functions with trigger element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" data-foo="bar">BS twitter</a>' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title(el) { + return el.dataset.foo + }, + content(el) { + return el.dataset.foo + } + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('bar') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('bar') + resolve() + }) - const popoverEl = fixtureEl.querySelector('a') - const popover = new Popover(popoverEl, { - content: 'Popover content' + popover.show() }) + }) - popoverEl.addEventListener('shown.bs.popover', () => { - const popoverDisplayed = document.querySelector('.popover') + it('should call content and title functions with correct this value', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" data-foo="bar">BS twitter</a>' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + title() { + return this.dataset.foo + }, + content() { + return this.dataset.foo + } + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-header').textContent).toEqual('bar') + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('bar') + resolve() + }) - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - done() + 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 call setContent once', done => { - fixtureEl.innerHTML = '<a href="#">BS twitter</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, { - content: 'Popover content' - }) - expect(popover._templateFactory).toBeNull() - let spy = null - let times = 1 + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl) + + 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') + resolve() + }) - 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') + it('should NOT show a popover without `title` and `content`', () => { + fixtureEl.innerHTML = '<a href="#" data-bs-content="" title="">Nice link</a>' - expect(popoverDisplayed).not.toBeNull() - expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') - expect(spy).toHaveBeenCalledTimes(0) - if (times > 1) { - done() - } + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { animation: false }) + const spy = spyOn(EventHandler, 'trigger').and.callThrough() - times++ - popover.hide() - }) popover.show() + + expect(spy).not.toHaveBeenCalledWith(popoverEl, Popover.eventName('show')) + expect(document.querySelector('.popover')).toBeNull() }) - it('should show a popover with provided custom class', done => { + it('"setContent" should keep the initial template', () => { 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) - popoverEl.addEventListener('shown.bs.popover', () => { - const tip = document.querySelector('.popover') - expect(tip).not.toBeNull() - expect(tip.classList.contains('custom-class')).toBeTrue() - done() + popover.setContent({ '.tooltip-inner': 'foo' }) + const tip = popover._getTipElement() + + expect(tip).toHaveClass('popover') + expect(tip).toHaveClass('bs-popover-auto') + expect(tip.querySelector('.popover-arrow')).not.toBeNull() + expect(tip.querySelector('.popover-header')).not.toBeNull() + expect(tip.querySelector('.popover-body')).not.toBeNull() + }) + + 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() }) + }) - popover.show() + 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) + + popoverEl.addEventListener('shown.bs.popover', () => { + const tip = document.querySelector('.popover') + expect(tip).not.toBeNull() + expect(tip).toHaveClass('custom-class') + resolve() + }) + + 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() + }) }) }) @@ -290,11 +383,11 @@ describe('Popover', () => { jQueryMock.fn.popover = Popover.jQueryInterface jQueryMock.elements = [popoverEl] - spyOn(popover, 'show') + const spy = spyOn(popover, 'show') jQueryMock.fn.popover.call(jQueryMock, 'show') - expect(popover.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) @@ -314,7 +407,7 @@ describe('Popover', () => { const popoverEl = fixtureEl.querySelector('a') - expect(Popover.getInstance(popoverEl)).toEqual(null) + expect(Popover.getInstance(popoverEl)).toBeNull() }) }) @@ -335,7 +428,7 @@ describe('Popover', () => { const div = fixtureEl.querySelector('div') - expect(Popover.getInstance(div)).toEqual(null) + expect(Popover.getInstance(div)).toBeNull() expect(Popover.getOrCreateInstance(div)).toBeInstanceOf(Popover) }) @@ -344,7 +437,7 @@ describe('Popover', () => { const div = fixtureEl.querySelector('div') - expect(Popover.getInstance(div)).toEqual(null) + expect(Popover.getInstance(div)).toBeNull() const popover = Popover.getOrCreateInstance(div, { placement: 'top' }) diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js index f64b8f1dc..fc44471c4 100644 --- a/js/tests/unit/scrollspy.spec.js +++ b/js/tests/unit/scrollspy.spec.js @@ -1,28 +1,71 @@ -import ScrollSpy from '../../src/scrollspy' -import Manipulator from '../../src/dom/manipulator' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import ScrollSpy from '../../src/scrollspy.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('ScrollSpy', () => { let fixtureEl - const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => { + const getElementScrollSpy = element => element.scrollTo ? + spyOn(element, 'scrollTo').and.callThrough() : + spyOnProperty(element, 'scrollTop', 'set').and.callThrough() + + const scrollTo = (el, height) => { + el.scrollTop = height + } + + const onScrollStop = (callback, element, timeout = 30) => { + let handle = null + const onScroll = function () { + if (handle) { + window.clearTimeout(handle) + } + + handle = setTimeout(() => { + element.removeEventListener('scroll', onScroll) + callback() + }, timeout + 1) + } + + element.addEventListener('scroll', onScroll) + } + + const getDummyFixture = () => { + return [ + '<nav id="navBar" class="navbar">', + ' <ul class="nav">', + ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>', + ' </ul>', + '</nav>', + '<div class="content" data-bs-target="#navBar" style="overflow-y: auto">', + ' <div id="div-jsm-1">div 1</div>', + '</div>' + ].join('') + } + + const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, cb }) => { const element = fixtureEl.querySelector(elementSelector) const target = fixtureEl.querySelector(targetSelector) - // add top padding to fix Chrome on Android failures - const paddingTop = 5 - const scrollHeight = Math.ceil(contentEl.scrollTop + Manipulator.position(target).top) + paddingTop - - function listener() { - expect(element.classList.contains('active')).toEqual(true) - contentEl.removeEventListener('scroll', listener) - expect(scrollSpy._process).toHaveBeenCalled() - spy.calls.reset() + const paddingTop = 0 + const parentOffset = getComputedStyle(contentEl).getPropertyValue('position') === 'relative' ? 0 : contentEl.offsetTop + const scrollHeight = (target.offsetTop - parentOffset) + paddingTop + + contentEl.addEventListener('activate.bs.scrollspy', event => { + if (scrollSpy._activeTarget !== element) { + return + } + + expect(element).toHaveClass('active') + expect(scrollSpy._activeTarget).toEqual(element) + expect(event.relatedTarget).toEqual(element) cb() - } + }) - contentEl.addEventListener('scroll', listener) - contentEl.scrollTop = scrollHeight + setTimeout(() => { // in case we scroll something before the test + scrollTo(contentEl, scrollHeight) + }, 100) } beforeAll(() => { @@ -53,28 +96,25 @@ describe('ScrollSpy', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { - fixtureEl.innerHTML = '<nav id="navigation"></nav><div class="content"></div>' + fixtureEl.innerHTML = getDummyFixture() - const sSpyEl = fixtureEl.querySelector('#navigation') - const sSpyBySelector = new ScrollSpy('#navigation') + const sSpyEl = fixtureEl.querySelector('.content') + const sSpyBySelector = new ScrollSpy('.content') const sSpyByElement = new ScrollSpy(sSpyEl) expect(sSpyBySelector._element).toEqual(sSpyEl) expect(sSpyByElement._element).toEqual(sSpyEl) }) - it('should not process element without target', () => { + it('should null, if element is not scrollable', () => { fixtureEl.innerHTML = [ '<nav id="navigation" class="navbar">', - ' <ul class="navbar-nav">', - ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">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 class="navbar-nav">' + + ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">One</a></li>' + ' </ul>', '</nav>', - '<div id="content" style="height: 200px; overflow-y: auto;">', - ' <div id="two" style="height: 300px;"></div>', - ' <div id="three" style="height: 10px;"></div>', + '<div id="content">', + ' <div id="1" style="height: 300px;">test</div>', '</div>' ].join('') @@ -82,533 +122,567 @@ describe('ScrollSpy', () => { target: '#navigation' }) - expect(scrollSpy._targets.length).toEqual(2) + expect(scrollSpy._observer.root).toBeNull() + expect(scrollSpy._rootElement).toBeNull() }) - it('should only switch "active" class on current target', done => { + it('should respect threshold option', () => { 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>', + '<ul id="navigation" class="navbar">', + ' <a class="nav-link active" id="one-link" href="#">One</a>' + + '</ul>', + '<div id="content">', + ' <div id="one-link">test</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.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() + const scrollSpy = new ScrollSpy('#content', { + target: '#navigation', + threshold: [1] }) - scrollSpyEl.scrollTop = 350 + expect(scrollSpy._observer.thresholds).toEqual([1]) }) - it('should only switch "active" class on current target specified w element', done => { + it('should respect threshold option markup', () => { 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>', + '<ul id="navigation" class="navbar">', + ' <a class="nav-link active" id="one-link" href="#">One</a>' + + '</ul>', + '<div id="content" data-bs-threshold="0,0.2,1">', + ' <div id="one-link">test</div>', '</div>' ].join('') - const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') - const rootEl = fixtureEl.querySelector('#root') - const scrollSpy = new ScrollSpy(scrollSpyEl, { - target: fixtureEl.querySelector('#ss-target') + const scrollSpy = new ScrollSpy('#content', { + target: '#navigation' }) - spyOn(scrollSpy, '_process').and.callThrough() - - scrollSpyEl.addEventListener('scroll', () => { - expect(rootEl.classList.contains('active')).toEqual(true) - expect(scrollSpy._process).toHaveBeenCalled() - done() - }) + // See https://stackoverflow.com/a/45592926 + const expectToBeCloseToArray = (actual, expected) => { + expect(actual.length).toBe(expected.length) + for (const x of actual) { + const i = actual.indexOf(x) + expect(x).withContext(`[${i}]`).toBeCloseTo(expected[i]) + } + } - scrollSpyEl.scrollTop = 350 + expectToBeCloseToArray(scrollSpy._observer.thresholds, [0, 0.2, 1]) }) - it('should correctly select middle navigation option when large offset is used', done => { + it('should not take count to not visible sections', () => { 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>', + ' <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 id="one" style="height: 300px;">test</div>', + ' <div id="two" hidden style="height: 300px;">test</div>', + ' <div id="three" style="display: none;">test</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').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('#two-link').classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#three-link').classList.contains('active')).toEqual(false) - expect(scrollSpy._process).toHaveBeenCalled() - done() + const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { + target: '#navigation' }) - contentEl.scrollTop = 550 + expect(scrollSpy._observableSections.size).toBe(1) + expect(scrollSpy._targetLinks.size).toBe(1) }) - it('should add the active class to the correct element', done => { + it('should not process element without target', () => { 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>', + '<nav id="navigation" class="navbar">', + ' <ul class="navbar-nav">', + ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">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 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 id="content" style="height: 200px; overflow-y: auto;">', + ' <div id="two" style="height: 300px;">test</div>', + ' <div id="three" style="height: 10px;">test2</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() - }) - } + const scrollSpy = new ScrollSpy(fixtureEl.querySelector('#content'), { + target: '#navigation' }) - }) - 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('') + expect(scrollSpy._targetLinks).toHaveSize(2) + }) - 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 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;" id="masthead">Overview</div>', + ' <div style="height: 200px;" id="detail">Detail</div>', + ' </div>', + '</div>' + ].join('') + + const scrollSpyEl = fixtureEl.querySelector('#scrollspy-example') + const rootEl = fixtureEl.querySelector('#root') + const scrollSpy = new ScrollSpy(scrollSpyEl, { + target: 'ss-target' + }) + + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + onScrollStop(() => { + expect(rootEl).toHaveClass('active') + expect(spy).toHaveBeenCalled() + resolve() + }, scrollSpyEl) + + scrollTo(scrollSpyEl, 350) }) }) - 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 not process data if `activeTarget` is same as given target', () => { + return new Promise((resolve, reject) => { + 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 triggerSpy = spyOn(EventHandler, 'trigger').and.callThrough() + + scrollSpy._activeTarget = fixtureEl.querySelector('#a-1') + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + cb: reject + }) + + setTimeout(() => { + expect(triggerSpy).not.toHaveBeenCalled() + resolve() + }, 100) }) }) - 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 + 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;" id="masthead">Overview</div>', + ' <div style="height: 200px;" id="detail">Detail</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') + }) + + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + onScrollStop(() => { + expect(rootEl).toHaveClass('active') + expect(scrollSpy._activeTarget).toEqual(fixtureEl.querySelector('[href="#detail"]')) + expect(spy).toHaveBeenCalled() + resolve() + }, scrollSpyEl) + + scrollTo(scrollSpyEl, 350) }) - 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').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = 0 - } else { - expect(active).toBeNull() - 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' + }) + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + cb() { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + cb: resolve + }) + } + }) }) - - 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 + it('should add to nav 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' + }) + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + cb() { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + cb: resolve + }) + } + }) }) - 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').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('two-link') - firstTime = false - contentEl.scrollTop = negativeHeight - } else { - expect(fixtureEl.querySelectorAll('.active').length).toEqual(1) - expect(active.getAttribute('id')).toEqual('one-link') - done() - } + it('should add to list-group, 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' + }) + + testElementIsActiveAfterScroll({ + elementSelector: '#a-1', + targetSelector: '#div-1', + contentEl, + scrollSpy, + cb() { + testElementIsActiveAfterScroll({ + elementSelector: '#a-2', + targetSelector: '#div-2', + contentEl, + scrollSpy, + cb: resolve + }) + } + }) }) - - 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 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: 200px;"></div>', + ' <div id="one" style="height: 100px;">text</div>', + ' <div id="two" style="height: 100px;">text</div>', + ' <div id="three" style="height: 100px;">text</div>', + ' <div id="spacer" style="height: 100px;"></div>', + '</div>' + ].join('') + + const contentEl = fixtureEl.querySelector('#content') + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + offset: contentEl.offsetTop + }) + const spy = spyOn(scrollSpy, '_process').and.callThrough() + + onScrollStop(() => { + const active = () => fixtureEl.querySelector('.active') + expect(spy).toHaveBeenCalled() + + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(active().getAttribute('id')).toEqual('two-link') + onScrollStop(() => { + expect(active()).toBeNull() + resolve() + }, contentEl) + scrollTo(contentEl, 0) + }, contentEl) + + scrollTo(contentEl, 200) }) }) - it('should allow passed in option offset method: offset', () => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>', - ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>', - '</div>' - ].join('') - - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'offset' + 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: 150px; overflow-y: auto;">', + ' <div id="one" style="height: 100px;">test</div>', + ' <div id="two" style="height: 100px;">test</div>', + ' <div id="three" style="height: 100px;">test</div>', + ' <div id="spacer" style="height: 100px;">test</div>', + '</div>' + ].join('') + + const negativeHeight = 0 + const startOfSectionTwo = 101 + const contentEl = fixtureEl.querySelector('#content') + // eslint-disable-next-line no-unused-vars + const scrollSpy = new ScrollSpy(contentEl, { + target: '#navigation', + rootMargin: '0px 0px -50%' + }) + + onScrollStop(() => { + const activeId = () => fixtureEl.querySelector('.active').getAttribute('id') + + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(activeId()).toEqual('two-link') + scrollTo(contentEl, negativeHeight) + + onScrollStop(() => { + expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1) + expect(activeId()).toEqual('one-link') + resolve() + }, contentEl) + + scrollTo(contentEl, 0) + }, contentEl) + + scrollTo(contentEl, startOfSectionTwo) }) + }) - expect(scrollSpy._offsets[1]).toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.position(targetEl).top) + 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' + }) + + scrollTo(contentEl, 0) + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-5', + targetSelector: '#div-100-5', + contentEl, + scrollSpy, + cb() { + scrollTo(contentEl, 0) + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-2', + targetSelector: '#div-100-2', + contentEl, + scrollSpy, + cb() { + scrollTo(contentEl, 0) + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-3', + targetSelector: '#div-100-3', + contentEl, + scrollSpy, + cb() { + scrollTo(contentEl, 0) + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-2', + targetSelector: '#div-100-2', + contentEl, + scrollSpy, + cb() { + scrollTo(contentEl, 0) + testElementIsActiveAfterScroll({ + elementSelector: '#li-100-1', + targetSelector: '#div-100-1', + contentEl, + scrollSpy, + cb: resolve + }) + } + }) + } + }) + } + }) + } + }) + }) }) + }) - it('should allow passed in option offset method: position', () => { - fixtureEl.innerHTML = [ - '<nav class="navbar">', - ' <ul class="nav">', - ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#div-jsm-1">div 1</a></li>', - ' <li class="nav-item"><a id="li-jsm-2" class="nav-link" href="#div-jsm-2">div 2</a></li>', - ' <li class="nav-item"><a id="li-jsm-3" class="nav-link" href="#div-jsm-3">div 3</a></li>', - ' </ul>', - '</nav>', - '<div class="content" style="position: relative; overflow: auto; height: 100px">', - ' <div id="div-jsm-1" style="position: relative; height: 200px; padding: 0; margin: 0">div 1</div>', - ' <div id="div-jsm-2" style="position: relative; height: 150px; padding: 0; margin: 0">div 2</div>', - ' <div id="div-jsm-3" style="position: relative; height: 250px; padding: 0; margin: 0">div 3</div>', - '</div>' - ].join('') + describe('refresh', () => { + it('should disconnect existing observer', () => { + fixtureEl.innerHTML = getDummyFixture() - const contentEl = fixtureEl.querySelector('.content') - const targetEl = fixtureEl.querySelector('#div-jsm-2') - const scrollSpy = new ScrollSpy(contentEl, { - target: '.navbar', - offset: 0, - method: 'position' - }) + const el = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(el) + + const spy = spyOn(scrollSpy._observer, 'disconnect') + + scrollSpy.refresh() - expect(scrollSpy._offsets[1]).not.toEqual(Manipulator.offset(targetEl).top) - expect(scrollSpy._offsets[1]).toEqual(Manipulator.position(targetEl).top) + expect(spy).toHaveBeenCalled() }) }) describe('dispose', () => { it('should dispose a scrollspy', () => { - fixtureEl.innerHTML = '<div style="display: none;"></div>' + fixtureEl.innerHTML = getDummyFixture() - const divEl = fixtureEl.querySelector('div') - spyOn(divEl, 'addEventListener').and.callThrough() - spyOn(divEl, 'removeEventListener').and.callThrough() + const el = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(el) - const scrollSpy = new ScrollSpy(divEl) - expect(divEl.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) + expect(ScrollSpy.getInstance(el)).not.toBeNull() scrollSpy.dispose() - expect(divEl.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean)) + expect(ScrollSpy.getInstance(el)).toBeNull() }) }) describe('jQueryInterface', () => { it('should create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] - jQueryMock.fn.scrollspy.call(jQueryMock) + jQueryMock.fn.scrollspy.call(jQueryMock, { target: '#navBar' }) expect(ScrollSpy.getInstance(div)).not.toBeNull() }) it('should create a scrollspy with given config', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] - jQueryMock.fn.scrollspy.call(jQueryMock, { offset: 15 }) - spyOn(ScrollSpy.prototype, 'constructor') - expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { offset: 15 }) + jQueryMock.fn.scrollspy.call(jQueryMock, { rootMargin: '100px' }) + const spy = spyOn(ScrollSpy.prototype, 'constructor') + expect(spy).not.toHaveBeenCalledWith(div, { rootMargin: '100px' }) const scrollspy = ScrollSpy.getInstance(div) expect(scrollspy).not.toBeNull() - expect(scrollspy._config.offset).toBe(15) + expect(scrollspy._config.rootMargin).toEqual('100px') }) it('should not re create a scrollspy', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface @@ -620,12 +694,12 @@ describe('ScrollSpy', () => { }) it('should call a scrollspy method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollSpy = new ScrollSpy(div) - spyOn(scrollSpy, 'refresh') + const spy = spyOn(scrollSpy, 'refresh') jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface jQueryMock.elements = [div] @@ -633,13 +707,13 @@ describe('ScrollSpy', () => { jQueryMock.fn.scrollspy.call(jQueryMock, 'refresh') expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) - expect(scrollSpy.refresh).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const action = 'undefinedMethod' jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface @@ -649,29 +723,60 @@ describe('ScrollSpy', () => { jQueryMock.fn.scrollspy.call(jQueryMock, action) }).toThrowError(TypeError, `No method named "${action}"`) }) + + it('should throw error on protected method', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const action = '_getConfig' + + jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.scrollspy.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) + + it('should throw error if method "constructor" is being called', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const action = 'constructor' + + jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface + jQueryMock.elements = [div] + + expect(() => { + jQueryMock.fn.scrollspy.call(jQueryMock, action) + }).toThrowError(TypeError, `No method named "${action}"`) + }) }) describe('getInstance', () => { it('should return scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') - const scrollSpy = new ScrollSpy(div) + const div = fixtureEl.querySelector('.content') + const scrollSpy = new ScrollSpy(div, { target: fixtureEl.querySelector('#navBar') }) expect(ScrollSpy.getInstance(div)).toEqual(scrollSpy) expect(ScrollSpy.getInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return null if there is no instance', () => { - expect(ScrollSpy.getInstance(fixtureEl)).toEqual(null) + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + expect(ScrollSpy.getInstance(div)).toBeNull() }) }) describe('getOrCreateInstance', () => { it('should return scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div) expect(ScrollSpy.getOrCreateInstance(div)).toEqual(scrollspy) @@ -680,20 +785,20 @@ describe('ScrollSpy', () => { }) it('should return new instance when there is no scrollspy instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') - expect(ScrollSpy.getInstance(div)).toEqual(null) + expect(ScrollSpy.getInstance(div)).toBeNull() expect(ScrollSpy.getOrCreateInstance(div)).toBeInstanceOf(ScrollSpy) }) it('should return new instance when there is no scrollspy instance with given configuration', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') - expect(ScrollSpy.getInstance(div)).toEqual(null) + expect(ScrollSpy.getInstance(div)).toBeNull() const scrollspy = ScrollSpy.getOrCreateInstance(div, { offset: 1 }) @@ -703,9 +808,9 @@ describe('ScrollSpy', () => { }) it('should return the instance when exists without given configuration', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = getDummyFixture() - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.content') const scrollspy = new ScrollSpy(div, { offset: 1 }) @@ -723,13 +828,153 @@ describe('ScrollSpy', () => { describe('event handler', () => { it('should create scrollspy on window load event', () => { - fixtureEl.innerHTML = '<div data-bs-spy="scroll"></div>' + fixtureEl.innerHTML = [ + '<div id="nav"></div>' + + '<div id="wrapper" data-bs-spy="scroll" data-bs-target="#nav" style="overflow-y: auto"></div>' + ].join('') - const scrollSpyEl = fixtureEl.querySelector('div') + const scrollSpyEl = fixtureEl.querySelector('#wrapper') window.dispatchEvent(createEvent('load')) expect(ScrollSpy.getInstance(scrollSpyEl)).not.toBeNull() }) }) + + describe('SmoothScroll', () => { + it('should not enable smoothScroll', () => { + fixtureEl.innerHTML = getDummyFixture() + const offSpy = spyOn(EventHandler, 'off').and.callThrough() + const onSpy = spyOn(EventHandler, 'on').and.callThrough() + + const div = fixtureEl.querySelector('.content') + const target = fixtureEl.querySelector('#navBar') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1 + }) + + expect(offSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') + expect(onSpy).not.toHaveBeenCalledWith(target, 'click.bs.scrollspy') + }) + + it('should enable smoothScroll', () => { + fixtureEl.innerHTML = getDummyFixture() + const offSpy = spyOn(EventHandler, 'off').and.callThrough() + const onSpy = spyOn(EventHandler, 'on').and.callThrough() + + const div = fixtureEl.querySelector('.content') + const target = fixtureEl.querySelector('#navBar') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + expect(offSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy') + expect(onSpy).toHaveBeenCalledWith(target, 'click.bs.scrollspy', '[href]', jasmine.any(Function)) + }) + + it('should not smoothScroll to element if it not handles a scrollspy section', () => { + fixtureEl.innerHTML = [ + '<nav id="navBar" class="navbar">', + ' <ul class="nav">', + ' <a id="anchor-1" href="#div-jsm-1">div 1</a></li>', + ' <a id="anchor-2" href="#foo">div 2</a></li>', + ' </ul>', + '</nav>', + '<div class="content" data-bs-target="#navBar" style="overflow-y: auto">', + ' <div id="div-jsm-1">div 1</div>', + '</div>' + ].join('') + + const div = fixtureEl.querySelector('.content') + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + const clickSpy = getElementScrollSpy(div) + + fixtureEl.querySelector('#anchor-2').click() + expect(clickSpy).not.toHaveBeenCalled() + }) + + it('should call `scrollTop` if element doesn\'t not support `scrollTo`', () => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const link = fixtureEl.querySelector('[href="#div-jsm-1"]') + delete div.scrollTo + const clickSpy = getElementScrollSpy(div) + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + link.click() + expect(clickSpy).toHaveBeenCalled() + }) + + it('should smoothScroll to the proper observable element on anchor click', done => { + fixtureEl.innerHTML = getDummyFixture() + + const div = fixtureEl.querySelector('.content') + const link = fixtureEl.querySelector('[href="#div-jsm-1"]') + const observable = fixtureEl.querySelector('#div-jsm-1') + const clickSpy = getElementScrollSpy(div) + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + setTimeout(() => { + if (div.scrollTo) { + expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' }) + } else { + expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop) + } + + done() + }, 100) + link.click() + }) + + it('should smoothscroll to observable with anchor link that contains a french word as id', done => { + fixtureEl.innerHTML = [ + '<nav id="navBar" class="navbar">', + ' <ul class="nav">', + ' <li class="nav-item"><a id="li-jsm-1" class="nav-link" href="#présentation">div 1</a></li>', + ' </ul>', + '</nav>', + '<div class="content" data-bs-target="#navBar" style="overflow-y: auto">', + ' <div id="présentation">div 1</div>', + '</div>' + ].join('') + + const div = fixtureEl.querySelector('.content') + const link = fixtureEl.querySelector('[href="#présentation"]') + const observable = fixtureEl.querySelector('#présentation') + const clickSpy = getElementScrollSpy(div) + // eslint-disable-next-line no-new + new ScrollSpy(div, { + offset: 1, + smoothScroll: true + }) + + setTimeout(() => { + if (div.scrollTo) { + expect(clickSpy).toHaveBeenCalledWith({ top: observable.offsetTop - div.offsetTop, behavior: 'smooth' }) + } else { + expect(clickSpy).toHaveBeenCalledWith(observable.offsetTop - div.offsetTop) + } + + done() + }, 100) + link.click() + }) + }) }) diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index 05f9db2ec..4fcf8ee0f 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -1,5 +1,7 @@ -import Tab from '../../src/tab' -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import Tab from '../../src/tab.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Tab', () => { let fixtureEl @@ -21,8 +23,12 @@ describe('Tab', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { fixtureEl.innerHTML = [ - '<ul class="nav"><li><a href="#home" role="tab">Home</a></li></ul>', - '<ul><li id="home"></li></ul>' + '<ul class="nav">', + ' <li><a href="#home" role="tab">Home</a></li>', + '</ul>', + '<ul>', + ' <li id="home"></li>', + '</ul>' ].join('') const tabEl = fixtureEl.querySelector('[href="#home"]') @@ -32,334 +38,800 @@ describe('Tab', () => { expect(tabBySelector._element).toEqual(tabEl) expect(tabByElement._element).toEqual(tabEl) }) - }) - describe('show', () => { - it('should activate element by tab id (using buttons, the preferred semantic way)', done => { + it('Do not Throw exception if not parent', () => { 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>' + fixtureEl.innerHTML = '<div class=""><div class="nav-link"></div></div>' ].join('') + const navEl = fixtureEl.querySelector('.nav-link') - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + expect(() => { + new Tab(navEl) // eslint-disable-line no-new + }).not.toThrowError(TypeError) + }) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + describe('show', () => { + 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() + }) + + tab.show() }) + }) - tab.show() + 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() + }) + + 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('') + 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() + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + tab.show() + }) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true') - done() + 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() + }) + + tab.show() }) + }) - tab.show() + 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() + }) + + tab.show() + }) }) - it('should activate element by tab id in ordered list', done => { + it('should work with tab id being an int', 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>' + '<div class="card-header d-block d-inline-block">', + ' <ul class="nav nav-tabs card-header-tabs" id="page_tabs">', + ' <li class="nav-item">', + ' <a class="nav-link" draggable="false" data-toggle="tab" href="#tab1">', + ' Working Tab 1 (#tab1)', + ' </a>', + ' </li>', + ' <li class="nav-item">', + ' <a id="trigger2" class="nav-link" draggable="false" data-toggle="tab" href="#2">', + ' Tab with numeric ID should work (#2)', + ' </a>', + ' </li>', + ' </ul>', + '</div>', + '<div class="card-body">', + ' <div class="tab-content" id="page_content">', + ' <div class="tab-pane fade" id="tab1">', + ' Working Tab 1 (#tab1) Content Here', + ' </div>', + ' <div class="tab-pane fade" id="2">', + ' Working Tab 2 (#2) with numeric ID', + ' </div>', + '</div>' ].join('') - - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const profileTriggerEl = fixtureEl.querySelector('#trigger2') const tab = new Tab(profileTriggerEl) profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) + expect(fixtureEl.querySelector(`#${CSS.escape('2')}`)).toHaveClass('active') done() }) 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</a>', - '</nav>', - '<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>' - ].join('') + it('should not fire shown when show is prevented', () => { + return new Promise((resolve, reject) => { + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' + + const navEl = fixtureEl.querySelector('.nav > div') + const tab = new Tab(navEl) + const expectDone = () => { + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + } + + navEl.addEventListener('show.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + navEl.addEventListener('shown.bs.tab', () => { + reject(new Error('should not trigger shown event')) + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + tab.show() }) + }) - tab.show() + it('should not fire shown when tab is already active', () => { + return new Promise((resolve, reject) => { + 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', () => { + reject(new Error('should not trigger shown event')) + }) + + tab.show() + setTimeout(() => { + expect().nothing() + resolve() + }, 30) + }) }) - 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('') + 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') + }) - const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') - const tab = new Tab(profileTriggerEl) + 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() + }) - profileTriggerEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + secondTab.show() }) + }) - tab.show() + 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" role="tab">Profile</button></li>', + '</ul>' + ].join('') + + 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() + }) + + 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') + resolve() + }) + + firstTab.show() + }) }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '<div class="nav"></div>' + it('should not fire hidden when hide is prevented', () => { + return new Promise((resolve, reject) => { + 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() + }) - const navEl = fixtureEl.querySelector('div') - const tab = new Tab(navEl) - const expectDone = () => { - setTimeout(() => { - expect().nothing() - done() - }, 30) - } + triggerList[0].addEventListener('hide.bs.tab', ev => { + ev.preventDefault() + expectDone() + }) + + triggerList[0].addEventListener('hidden.bs.tab', () => { + reject(new Error('should not trigger hidden')) + }) - navEl.addEventListener('show.bs.tab', ev => { - ev.preventDefault() - expectDone() + firstTab.show() }) + }) + + 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() + }) - navEl.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') + 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() + }) + + btnCloseEl.click() }) + }) - tab.show() + it('should not focus on opened tab', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li><button type="button" id="home" 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 firstTab = fixtureEl.querySelector('#home') + firstTab.focus() + + const profileTriggerEl = fixtureEl.querySelector('#triggerProfile') + const tab = new Tab(profileTriggerEl) + + profileTriggerEl.addEventListener('shown.bs.tab', () => { + expect(document.activeElement).toBe(firstTab) + expect(document.activeElement).not.toBe(profileTriggerEl) + resolve() + }) + + tab.show() + }) }) + }) + + describe('dispose', () => { + it('should dispose a tab', () => { + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' + + const el = fixtureEl.querySelector('.nav > div') + const tab = new Tab(fixtureEl.querySelector('.nav > div')) + + expect(Tab.getInstance(el)).not.toBeNull() - it('should not fire shown when tab is already active', done => { + tab.dispose() + + expect(Tab.getInstance(el)).toBeNull() + }) + }) + + describe('_activate', () => { + it('should not be called if element argument is null', () => { + fixtureEl.innerHTML = [ + '<ul class="nav" role="tablist">', + ' <li class="nav-link"></li>', + '</ul>' + ].join('') + + const tabEl = fixtureEl.querySelector('.nav-link') + const tab = new Tab(tabEl) + const spy = jasmine.createSpy('spy') + + const spyQueue = spyOn(tab, '_queueCallback') + tab._activate(null, spy) + expect(spyQueue).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() + }) + }) + + describe('_setInitialAttributes', () => { + it('should put aria attributes', () => { 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 class="nav">', + ' <li class="nav-link" id="foo" data-bs-target="#panel"></li>', + ' <li class="nav-link" data-bs-target="#panel2"></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>' + '<div id="panel"></div>', + '<div id="panel2"></div>' ].join('') - const triggerActive = fixtureEl.querySelector('button.active') - const tab = new Tab(triggerActive) + const tabEl = fixtureEl.querySelector('.nav-link') + const parent = fixtureEl.querySelector('.nav') + const children = fixtureEl.querySelectorAll('.nav-link') + const tabPanel = fixtureEl.querySelector('#panel') + const tabPanel2 = fixtureEl.querySelector('#panel2') - triggerActive.addEventListener('shown.bs.tab', () => { - throw new Error('should not trigger shown event') - }) + expect(parent.getAttribute('role')).toEqual(null) + expect(tabEl.getAttribute('role')).toEqual(null) + expect(tabPanel.getAttribute('role')).toEqual(null) + const tab = new Tab(tabEl) + tab._setInitialAttributes(parent, children) - tab.show() - setTimeout(() => { - expect().nothing() - done() - }, 30) + expect(parent.getAttribute('role')).toEqual('tablist') + expect(tabEl.getAttribute('role')).toEqual('tab') + + expect(tabPanel.getAttribute('role')).toEqual('tabpanel') + expect(tabPanel2.getAttribute('role')).toEqual('tabpanel') + expect(tabPanel.hasAttribute('tabindex')).toBeFalse() + expect(tabPanel.hasAttribute('tabindex2')).toBeFalse() + + expect(tabPanel.getAttribute('aria-labelledby')).toEqual('foo') + expect(tabPanel2.hasAttribute('aria-labelledby')).toBeFalse() }) + }) - it('show and shown events should reference correct relatedTarget', done => { + describe('_keydown', () => { + it('if event is not one of left/right/up/down arrow, ignore it', () => { 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>', + '<ul class="nav">', + ' <li class="nav-link" data-bs-toggle="tab"></li>', + '</ul>' + ].join('') + + const tabEl = fixtureEl.querySelector('.nav-link') + const tab = new Tab(tabEl) + + const keydown = createEvent('keydown') + keydown.key = 'Enter' + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + const spyKeydown = spyOn(tab, '_keydown') + const spyGet = spyOn(tab, '_getChildren') + + tabEl.dispatchEvent(keydown) + expect(spyKeydown).toHaveBeenCalled() + expect(spyGet).not.toHaveBeenCalled() + + expect(spyStop).not.toHaveBeenCalled() + expect(spyPrevent).not.toHaveBeenCalled() + }) + + it('if keydown event is right/down arrow, handle it', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', '</div>' ].join('') - const secondTabTrigger = fixtureEl.querySelector('#triggerProfile') - const secondTab = new Tab(secondTabTrigger) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyShow3 = spyOn(tab3, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + tabEl1.dispatchEvent(keydown) + expect(spyShow2).toHaveBeenCalled() + expect(spyFocus2).toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + + tabEl2.dispatchEvent(keydown) + expect(spyShow3).toHaveBeenCalled() + expect(spyFocus3).toHaveBeenCalled() + + tabEl3.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + + expect(spyStop).toHaveBeenCalledTimes(3) + expect(spyPrevent).toHaveBeenCalledTimes(3) + }) - secondTabTrigger.addEventListener('show.bs.tab', ev => { - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home') - }) + it('if keydown event is left arrow, handle it', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') - 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() - }) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const spyShow1 = spyOn(tab1, 'show').and.callThrough() + const spyShow2 = spyOn(tab2, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + + const spyStop = spyOn(Event.prototype, 'stopPropagation').and.callThrough() + const spyPrevent = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + let keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' + + tabEl2.dispatchEvent(keydown) + expect(spyShow1).toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalled() + + keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + + tabEl1.dispatchEvent(keydown) + expect(spyShow2).toHaveBeenCalled() + expect(spyFocus2).toHaveBeenCalled() - secondTab.show() + expect(spyStop).toHaveBeenCalledTimes(2) + expect(spyPrevent).toHaveBeenCalledTimes(2) }) - it('should fire hide and hidden events', done => { + it('if keydown event is Home, handle it', () => { 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>' + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' ].join('') - const triggerList = fixtureEl.querySelectorAll('button') - const firstTab = new Tab(triggerList[0]) - const secondTab = new Tab(triggerList[1]) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl3 = fixtureEl.querySelector('#tab3') - let hideCalled = false - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + const tab3 = new Tab(tabEl3) + tab3.show() - triggerList[0].addEventListener('hide.bs.tab', ev => { - hideCalled = true - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - }) + const spyShown = jasmine.createSpy() + tabEl1.addEventListener('shown.bs.tab', spyShown) - triggerList[0].addEventListener('hidden.bs.tab', ev => { - expect(hideCalled).toEqual(true) - expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile') - done() - }) + const keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl3.dispatchEvent(keydown) - firstTab.show() + expect(spyShown).toHaveBeenCalled() }) - it('should not fire hidden when hide is prevented', done => { + it('if keydown event is End, handle it', () => { 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>' + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' ].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) - } + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl3 = fixtureEl.querySelector('#tab3') - triggerList[0].addEventListener('shown.bs.tab', () => { - secondTab.show() - }) + const tab1 = new Tab(tabEl1) + tab1.show() - triggerList[0].addEventListener('hide.bs.tab', ev => { - ev.preventDefault() - expectDone() - }) + const spyShown = jasmine.createSpy() + tabEl3.addEventListener('shown.bs.tab', spyShown) - triggerList[0].addEventListener('hidden.bs.tab', () => { - throw new Error('should not trigger hidden') - }) + const keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl1.dispatchEvent(keydown) - firstTab.show() + expect(spyShown).toHaveBeenCalled() }) - it('should handle removed tabs', done => { + it('if keydown event is right arrow and next element is disabled', () => { 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 class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab" disabled></span>', + ' <span id="tab3" class="nav-link disabled" data-bs-toggle="tab"></span>', + ' <span id="tab4" class="nav-link" data-bs-toggle="tab"></span>', '</div>' ].join('') - const secondNavEl = fixtureEl.querySelector('#secondNav') - const btnCloseEl = fixtureEl.querySelector('#btnClose') - const secondNavTab = new Tab(secondNavEl) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tabEl4 = fixtureEl.querySelector('#tab4') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const tab4 = new Tab(tabEl4) + const spy1 = spyOn(tab1, 'show').and.callThrough() + const spy2 = spyOn(tab2, 'show').and.callThrough() + const spy3 = spyOn(tab3, 'show').and.callThrough() + const spy4 = spyOn(tab4, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() + + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' + + tabEl1.dispatchEvent(keydown) + expect(spy1).not.toHaveBeenCalled() + expect(spy2).not.toHaveBeenCalled() + expect(spy3).not.toHaveBeenCalled() + expect(spy4).toHaveBeenCalledTimes(1) + expect(spyFocus1).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + expect(spyFocus4).toHaveBeenCalledTimes(1) + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.nav-tab').length).toEqual(2) - done() - }) + it('if keydown event is left arrow and next element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab" disabled></span>', + ' <span id="tab3" class="nav-link disabled" data-bs-toggle="tab"></span>', + ' <span id="tab4" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') - btnCloseEl.addEventListener('click', () => { - const linkEl = btnCloseEl.parentNode - const liEl = linkEl.parentNode - const tabId = linkEl.getAttribute('href') - const tabIdEl = fixtureEl.querySelector(tabId) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tabEl4 = fixtureEl.querySelector('#tab4') + const tab1 = new Tab(tabEl1) + const tab2 = new Tab(tabEl2) + const tab3 = new Tab(tabEl3) + const tab4 = new Tab(tabEl4) + const spy1 = spyOn(tab1, 'show').and.callThrough() + const spy2 = spyOn(tab2, 'show').and.callThrough() + const spy3 = spyOn(tab3, 'show').and.callThrough() + const spy4 = spyOn(tab4, 'show').and.callThrough() + const spyFocus1 = spyOn(tabEl1, 'focus').and.callThrough() + const spyFocus2 = spyOn(tabEl2, 'focus').and.callThrough() + const spyFocus3 = spyOn(tabEl3, 'focus').and.callThrough() + const spyFocus4 = spyOn(tabEl4, 'focus').and.callThrough() + + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' + + tabEl4.dispatchEvent(keydown) + expect(spy4).not.toHaveBeenCalled() + expect(spy3).not.toHaveBeenCalled() + expect(spy2).not.toHaveBeenCalled() + expect(spy1).toHaveBeenCalledTimes(1) + expect(spyFocus4).not.toHaveBeenCalled() + expect(spyFocus3).not.toHaveBeenCalled() + expect(spyFocus2).not.toHaveBeenCalled() + expect(spyFocus1).toHaveBeenCalledTimes(1) + }) - liEl.remove() - tabIdEl.remove() - secondNavTab.show() - }) + it('if keydown event is Home and first element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link disabled" data-bs-toggle="tab" disabled></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab"></span>', + '</div>' + ].join('') + + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab3 = new Tab(tabEl3) - btnCloseEl.click() + tab3.show() + + const spyShown1 = jasmine.createSpy() + const spyShown2 = jasmine.createSpy() + tabEl1.addEventListener('shown.bs.tab', spyShown1) + tabEl2.addEventListener('shown.bs.tab', spyShown2) + + const keydown = createEvent('keydown') + keydown.key = 'Home' + + tabEl3.dispatchEvent(keydown) + + expect(spyShown1).not.toHaveBeenCalled() + expect(spyShown2).toHaveBeenCalled() }) - }) - describe('dispose', () => { - it('should dispose a tab', () => { - fixtureEl.innerHTML = '<div></div>' + it('if keydown event is End and last element is disabled', () => { + fixtureEl.innerHTML = [ + '<div class="nav">', + ' <span id="tab1" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab2" class="nav-link" data-bs-toggle="tab"></span>', + ' <span id="tab3" class="nav-link" data-bs-toggle="tab" disabled></span>', + '</div>' + ].join('') - const el = fixtureEl.querySelector('div') - const tab = new Tab(fixtureEl.querySelector('div')) + const tabEl1 = fixtureEl.querySelector('#tab1') + const tabEl2 = fixtureEl.querySelector('#tab2') + const tabEl3 = fixtureEl.querySelector('#tab3') + const tab1 = new Tab(tabEl1) - expect(Tab.getInstance(el)).not.toBeNull() + tab1.show() - tab.dispose() + const spyShown2 = jasmine.createSpy() + const spyShown3 = jasmine.createSpy() + tabEl2.addEventListener('shown.bs.tab', spyShown2) + tabEl3.addEventListener('shown.bs.tab', spyShown3) - expect(Tab.getInstance(el)).toBeNull() + const keydown = createEvent('keydown') + keydown.key = 'End' + + tabEl1.dispatchEvent(keydown) + + expect(spyShown3).not.toHaveBeenCalled() + expect(spyShown2).toHaveBeenCalled() }) }) describe('jQueryInterface', () => { it('should create a tab', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] @@ -370,9 +842,9 @@ describe('Tab', () => { }) it('should not re create a tab', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) jQueryMock.fn.tab = Tab.jQueryInterface @@ -384,12 +856,12 @@ describe('Tab', () => { }) it('should call a tab method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const tab = new Tab(div) - spyOn(tab, 'show') + const spy = spyOn(tab, 'show') jQueryMock.fn.tab = Tab.jQueryInterface jQueryMock.elements = [div] @@ -397,13 +869,13 @@ describe('Tab', () => { jQueryMock.fn.tab.call(jQueryMock, 'show') expect(Tab.getInstance(div)).toEqual(tab) - expect(tab.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const div = fixtureEl.querySelector('div') + const div = fixtureEl.querySelector('.nav > div') const action = 'undefinedMethod' jQueryMock.fn.tab = Tab.jQueryInterface @@ -417,13 +889,13 @@ describe('Tab', () => { describe('getInstance', () => { it('should return null if there is no instance', () => { - expect(Tab.getInstance(fixtureEl)).toEqual(null) + expect(Tab.getInstance(fixtureEl)).toBeNull() }) it('should return this instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' - const divEl = fixtureEl.querySelector('div') + const divEl = fixtureEl.querySelector('.nav > div') const tab = new Tab(divEl) expect(Tab.getInstance(divEl)).toEqual(tab) @@ -433,7 +905,7 @@ describe('Tab', () => { describe('getOrCreateInstance', () => { it('should return tab instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' const div = fixtureEl.querySelector('div') const tab = new Tab(div) @@ -444,37 +916,39 @@ describe('Tab', () => { }) it('should return new instance when there is no tab instance', () => { - fixtureEl.innerHTML = '<div></div>' + fixtureEl.innerHTML = '<div class="nav"><div class="nav-link"></div></div>' const div = fixtureEl.querySelector('div') - expect(Tab.getInstance(div)).toEqual(null) + expect(Tab.getInstance(div)).toBeNull() expect(Tab.getOrCreateInstance(div)).toBeInstanceOf(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.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true) - done() + secondTabTrigger.click() }) - - secondTabTrigger.click() }) it('selected tab should deactivate previous selected link in dropdown', () => { @@ -495,9 +969,9 @@ describe('Tab', () => { const firstLiLinkEl = fixtureEl.querySelector('li:first-child a') firstLiLinkEl.click() - expect(firstLiLinkEl.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('li:last-child a').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child').classList.contains('active')).toEqual(false) + expect(firstLiLinkEl).toHaveClass('active') + expect(fixtureEl.querySelector('li:last-child a')).not.toHaveClass('active') + expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child')).not.toHaveClass('active') }) it('selecting a dropdown tab does not activate another', () => { @@ -529,10 +1003,10 @@ describe('Tab', () => { const firstDropItem = fixtureEl.querySelector('#nav1 .dropdown-item') firstDropItem.click() - expect(firstDropItem.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#nav1 .dropdown-toggle').classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('#nav2 .dropdown-toggle').classList.contains('active')).toEqual(false) - expect(fixtureEl.querySelector('#nav2 .dropdown-item').classList.contains('active')).toEqual(false) + expect(firstDropItem).toHaveClass('active') + expect(fixtureEl.querySelector('#nav1 .dropdown-toggle')).toHaveClass('active') + expect(fixtureEl.querySelector('#nav2 .dropdown-toggle')).not.toHaveClass('active') + expect(fixtureEl.querySelector('#nav2 .dropdown-item')).not.toHaveClass('active') }) it('should support li > .dropdown-item', () => { @@ -550,209 +1024,229 @@ describe('Tab', () => { '</ul>' ].join('') - const firstDropItem = fixtureEl.querySelector('.dropdown-item') + const dropItems = fixtureEl.querySelectorAll('.dropdown-item') - firstDropItem.click() - expect(firstDropItem.classList.contains('active')).toEqual(true) - expect(fixtureEl.querySelector('.nav-link').classList.contains('active')).toEqual(false) + dropItems[1].click() + expect(dropItems[0]).not.toHaveClass('active') + expect(dropItems[1]).toHaveClass('active') + 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.classList.contains('active')).toEqual(true) - done() - }) + tab1El.addEventListener('shown.bs.tab', () => { + expect(xTab1El).toHaveClass('active') + tabNested2El.click() + }) - tab1El.addEventListener('shown.bs.tab', () => { - expect(xTab1El.classList.contains('active')).toEqual(true) - 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') - - triggerTabProfileEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl.classList.contains('fade')).toEqual(true) - expect(tabProfileEl.classList.contains('show')).toEqual(true) + 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') triggerTabHomeEl.addEventListener('shown.bs.tab', () => { - expect(tabProfileEl.classList.contains('fade')).toEqual(true) - expect(tabProfileEl.classList.contains('show')).toEqual(false) + setTimeout(() => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).not.toHaveClass('show') - expect(tabHomeEl.classList.contains('fade')).toEqual(true) - expect(tabHomeEl.classList.contains('show')).toEqual(true) + expect(tabHomeEl).toHaveClass('fade') + expect(tabHomeEl).toHaveClass('show') - done() + resolve() + }, 10) }) - triggerTabHomeEl.click() - }) + triggerTabProfileEl.addEventListener('shown.bs.tab', () => { + setTimeout(() => { + expect(tabProfileEl).toHaveClass('fade') + expect(tabProfileEl).toHaveClass('show') + triggerTabHomeEl.click() + }, 10) + }) - 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 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('.tab-content .show')).toHaveSize(1) + resolve() + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show').length).toEqual(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', () => { + setTimeout(() => { + expect(fixtureEl.querySelectorAll('.show')).toHaveSize(1) + resolve() + }, 10) + }) - secondNavEl.addEventListener('shown.bs.tab', () => { - expect(fixtureEl.querySelectorAll('.show').length).toEqual(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('') - - const tabEl = fixtureEl.querySelector('[href="#test2"]') - spyOn(Event.prototype, 'preventDefault').and.callThrough() + 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"]') + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() + + tabEl.addEventListener('shown.bs.tab', () => { + expect(tabEl).toHaveClass('active') + expect(spy).toHaveBeenCalled() + resolve() + }) - tabEl.addEventListener('shown.bs.tab', () => { - expect(tabEl.classList.contains('active')).toEqual(true) - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + 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, reject) => { + 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', () => { + reject(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('') - - const triggerDisabled = fixtureEl.querySelector('a.disabled') + it('should not fire shown when tab has disabled class', () => { + return new Promise((resolve, reject) => { + 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') + + triggerDisabled.addEventListener('shown.bs.tab', () => { + reject(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() + resolve() + }, 30) }) - - triggerDisabled.click() - setTimeout(() => { - expect().nothing() - done() - }, 30) }) }) }) diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js index 4b84bf2c5..200fe3e40 100644 --- a/js/tests/unit/toast.spec.js +++ b/js/tests/unit/toast.spec.js @@ -1,5 +1,7 @@ -import Toast from '../../src/toast' -import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' +import Toast from '../../src/toast.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Toast', () => { let fixtureEl @@ -36,52 +38,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.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(true) + 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.classList.contains('show')).toEqual(false) - done() - }) + toastEl.addEventListener('hidden.bs.toast', () => { + expect(toastEl).not.toHaveClass('show') + resolve() + }) - toast.show() + toast.show() + }) }) }) @@ -111,304 +117,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.classList.contains('show')).toEqual(false) - 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.classList.contains('fade')).toEqual(false) - 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 toastEl = fixtureEl.querySelector('.toast') + const toast = new Toast(toastEl) - const assertDone = () => { - setTimeout(() => { - expect(toastEl.classList.contains('show')).toEqual(false) - done() - }, 20) - } - - 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, reject) => { + 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() + reject(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(spy).toHaveBeenCalled() + expect(toast._timeout).toBeNull() + resolve() + }) + toast.show() + }, toast._config.delay / 2) - const mouseOverEvent = createEvent('mouseover') - toastEl.dispatchEvent(mouseOverEvent) - }, toast._config.delay / 2) + const spy = 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) - }) - - toastEl.addEventListener('mouseout', () => { - const outsideFocusable = document.getElementById('outside-focusable') - outsideFocusable.focus() - }) + toast.show() + }) + }) - toastEl.addEventListener('focusout', () => { - expect(toast._timeout).not.toBeNull() - done() - }) + 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) - 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('') - - const toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) + 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) - 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.classList.contains('show')).toEqual(false) - done() + toast.show() }) - - toast.show() }) it('should do nothing when we call hide on a non shown toast', () => { @@ -417,46 +443,48 @@ describe('Toast', () => { const toastEl = fixtureEl.querySelector('div') const toast = new Toast(toastEl) - spyOn(toastEl.classList, 'contains') + const spy = spyOn(toastEl.classList, 'contains') toast.hide() - expect(toastEl.classList.contains).toHaveBeenCalled() + expect(spy).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('') + it('should not trigger hidden if hide is prevented', () => { + return new Promise((resolve, reject) => { + 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 toastEl = fixtureEl.querySelector('.toast') - const toast = new Toast(toastEl) - - const assertDone = () => { - setTimeout(() => { - expect(toastEl.classList.contains('show')).toEqual(true) - 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', () => { + reject(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 +503,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.classList.contains('show')).toEqual(true) - 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.classList.contains('show')).toEqual(false) + expect(Toast.getInstance(toastEl)).toBeNull() + expect(toastEl).not.toHaveClass('show') - done() - } + resolve() + } - toastEl.addEventListener('shown.bs.toast', () => { - setTimeout(expected, 1) - }) + toastEl.addEventListener('shown.bs.toast', () => { + setTimeout(expected, 1) + }) - toast.show() + toast.show() + }) }) }) @@ -540,7 +570,7 @@ describe('Toast', () => { const div = fixtureEl.querySelector('div') const toast = new Toast(div) - spyOn(toast, 'show') + const spy = spyOn(toast, 'show') jQueryMock.fn.toast = Toast.jQueryInterface jQueryMock.elements = [div] @@ -548,7 +578,7 @@ describe('Toast', () => { jQueryMock.fn.toast.call(jQueryMock, 'show') expect(Toast.getInstance(div)).toEqual(toast) - expect(toast.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { @@ -582,7 +612,7 @@ describe('Toast', () => { const div = fixtureEl.querySelector('div') - expect(Toast.getInstance(div)).toEqual(null) + expect(Toast.getInstance(div)).toBeNull() }) }) @@ -603,7 +633,7 @@ describe('Toast', () => { const div = fixtureEl.querySelector('div') - expect(Toast.getInstance(div)).toEqual(null) + expect(Toast.getInstance(div)).toBeNull() expect(Toast.getOrCreateInstance(div)).toBeInstanceOf(Toast) }) @@ -612,7 +642,7 @@ describe('Toast', () => { const div = fixtureEl.querySelector('div') - expect(Toast.getInstance(div)).toEqual(null) + expect(Toast.getInstance(div)).toBeNull() const toast = Toast.getOrCreateInstance(div, { delay: 1 }) diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 4a7022234..37f2c230d 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -1,7 +1,9 @@ -import Tooltip from '../../src/tooltip' -import EventHandler from '../../src/dom/event-handler' -import { noop } from '../../src/util/index' -import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture' +import EventHandler from '../../src/dom/event-handler.js' +import Tooltip from '../../src/tooltip.js' +import { noop } from '../../src/util/index.js' +import { + clearFixture, createEvent, getFixture, jQueryMock +} from '../helpers/fixture.js' describe('Tooltip', () => { let fixtureEl @@ -42,12 +44,6 @@ describe('Tooltip', () => { }) }) - describe('Event', () => { - it('should return plugin events', () => { - expect(Tooltip.Event).toEqual(jasmine.any(Object)) - }) - }) - describe('EVENT_KEY', () => { it('should return plugin event key', () => { expect(Tooltip.EVENT_KEY).toEqual('.bs.tooltip') @@ -62,7 +58,7 @@ describe('Tooltip', () => { describe('constructor', () => { it('should take care of element either passed as a CSS selector or DOM element', () => { - fixtureEl.innerHTML = '<a href="#" id="tooltipEl" rel="tooltip" title="Nice and short title">' + fixtureEl.innerHTML = '<a href="#" id="tooltipEl" rel="tooltip" title="Nice and short title"></a>' const tooltipEl = fixtureEl.querySelector('#tooltipEl') const tooltipBySelector = new Tooltip('#tooltipEl') @@ -73,16 +69,16 @@ describe('Tooltip', () => { }) it('should not take care of disallowed data attributes', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-sanitize="false" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-sanitize="false" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - expect(tooltip._config.sanitize).toEqual(true) + expect(tooltip._config.sanitize).toBeTrue() }) it('should convert title and content to string if numbers', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { @@ -94,56 +90,60 @@ 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"></a>' - const tooltipInContainerEl = containerEl.querySelector('a') + const tooltipInContainerEl = containerEl.querySelector('a') - tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - tooltipContainer.dispose() - done() - }) + tooltipInContainerEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + tooltipContainer.dispose() + resolve() + }) - 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"></a>' - 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(typeof offset).toEqual('function') + expect(offset).toEqual(jasmine.any(Function)) - tooltip.show() + tooltip.show() + }) }) it('should create offset modifier when offset option is passed in data attribute', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-offset="10,20" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-offset="10,20" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) @@ -152,7 +152,7 @@ describe('Tooltip', () => { }) it('should allow to pass config to Popper with `popperConfig`', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { @@ -167,7 +167,7 @@ describe('Tooltip', () => { }) it('should allow to pass config to Popper with `popperConfig` as a function', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' }) @@ -177,163 +177,189 @@ describe('Tooltip', () => { const popperConfig = tooltip._getPopperConfig('top') - expect(getPopperConfig).toHaveBeenCalled() + // Ensure that the function was called with the default config. + expect(getPopperConfig).toHaveBeenCalledWith(jasmine.objectContaining({ + placement: jasmine.any(String) + })) expect(popperConfig.placement).toEqual('left') }) - }) - describe('enable', () => { - it('should enable a tooltip', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('should use original title, if not "data-bs-title" is given', () => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - tooltip.enable() + expect(tooltip._getTitle()).toEqual('Another tooltip') + }) + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(document.querySelector('.tooltip')).not.toBeNull() - done() - }) + describe('enable', () => { + it('should enable a tooltip', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - tooltip.show() + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + + tooltip.enable() + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(document.querySelector('.tooltip')).not.toBeNull() + resolve() + }) + + 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, reject) => { + 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) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') - }) + tooltipEl.addEventListener('show.bs.tooltip', () => { + reject(new Error('should not show a disabled tooltip')) + }) - tooltip.show() + tooltip.show() - setTimeout(() => { - expect().nothing() - done() - }, 10) + setTimeout(() => { + expect().nothing() + resolve() + }, 10) + }) }) }) describe('toggleEnabled', () => { it('should toggle enabled', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - expect(tooltip._isEnabled).toEqual(true) + expect(tooltip._isEnabled).toBeTrue() tooltip.toggleEnabled() - expect(tooltip._isEnabled).toEqual(false) + expect(tooltip._isEnabled).toBeFalse() }) }) 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, reject) => { + 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) - tooltip.disable() + tooltip.disable() - tooltipEl.addEventListener('show.bs.tooltip', () => { - throw new Error('should not show a disabled tooltip') - }) + tooltipEl.addEventListener('show.bs.tooltip', () => { + reject(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"></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(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"></a>' - 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() + const spy = spyOn(tooltip, 'toggle').and.callThrough() - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(spy).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"></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', () => { + 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"></a>' - 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">' + const spy = 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(spy).toHaveBeenCalled() + resolve() + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipEl.click() }) - - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - expect(tooltip.toggle).toHaveBeenCalled() - done() - }) - - tooltipEl.click() }) }) describe('dispose', () => { it('should destroy a tooltip', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const addEventSpy = spyOn(tooltipEl, 'addEventListener').and.callThrough() @@ -354,246 +380,290 @@ describe('Tooltip', () => { tooltip.dispose() - expect(Tooltip.getInstance(tooltipEl)).toEqual(null) + expect(Tooltip.getInstance(tooltipEl)).toBeNull() 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"></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', () => { - tooltip.hide() - }) - tooltipEl.addEventListener('hidden.bs.tooltip', () => { - tooltip.dispose() - expect(tooltip.tip).toEqual(null) - expect(Tooltip.getInstance(tooltipEl)).toEqual(null) - 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"></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(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() + resolve() + }) - expect(document.querySelector('.tooltip')).toBeNull() - done() + tooltip.show() }) + }) - tooltip.show() + it('should destroy a tooltip and reset it\'s initial title', () => { + fixtureEl.innerHTML = [ + '<span id="tooltipWithTitle" rel="tooltip" title="tooltipTitle"></span>', + '<span id="tooltipWithoutTitle" rel="tooltip" data-bs-title="tooltipTitle"></span>' + ].join('') + + const tooltipWithTitleEl = fixtureEl.querySelector('#tooltipWithTitle') + const tooltip = new Tooltip('#tooltipWithTitle') + expect(tooltipWithTitleEl.getAttribute('title')).toBeNull() + tooltip.dispose() + expect(tooltipWithTitleEl.getAttribute('title')).toBe('tooltipTitle') + + const tooltipWithoutTitleEl = fixtureEl.querySelector('#tooltipWithoutTitle') + const tooltip2 = new Tooltip('#tooltipWithTitle') + expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull() + tooltip2.dispose() + expect(tooltipWithoutTitleEl.getAttribute('title')).toBeNull() }) }) 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"></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-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 children 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>' + 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') + const spy = 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(spy).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"></a>' - 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() + const spy = 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(spy).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"></a>' - 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().classList.contains('bs-tooltip-auto')).toEqual(true) - }) + tooltipEl.addEventListener('inserted.bs.tooltip', () => { + expect(tooltip._getTipElement()).toHaveClass('bs-tooltip-auto') + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - expect(tooltip.getTipElement().classList.contains('bs-tooltip-auto')).toEqual(true) - 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"></a>' - 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"></a>' - 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"></a>' - 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"></a>' - 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"></a>' - 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"></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).not.toHaveClass('fade') + resolve() + }) - tooltip.show() + tooltip.show() + }) }) it('should throw an error the element is not visible', () => { - fixtureEl.innerHTML = '<a href="#" style="display: none" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" style="display: none" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) @@ -605,339 +675,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, reject) => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - 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', () => { + reject(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"></a>' - 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') + const spy = spyOn(tooltip, 'show') - setTimeout(() => { - expect(tooltip.show).not.toHaveBeenCalled() - tooltipEl.dispatchEvent(createEvent('mouseover')) - }, 100) + setTimeout(() => { + expect(spy).not.toHaveBeenCalled() + }, 100) - setTimeout(() => { - expect(tooltip.show).toHaveBeenCalled() - expect(document.querySelectorAll('.tooltip').length).toEqual(0) - done() - }, 200) + setTimeout(() => { + expect(spy).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"></a>' - 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().classList.contains('show')).toEqual(true) - tooltipEl.dispatchEvent(createEvent('mouseout')) + const spy = spyOn(tooltip, 'show') setTimeout(() => { - expect(tooltip.getTipElement().classList.contains('show')).toEqual(true) + expect(spy).not.toHaveBeenCalled() tooltipEl.dispatchEvent(createEvent('mouseover')) }, 100) setTimeout(() => { - expect(tooltip.getTipElement().classList.contains('show')).toEqual(true) - expect(document.querySelectorAll('.tooltip').length).toEqual(1) - done() + expect(spy).toHaveBeenCalled() + expect(document.querySelectorAll('.tooltip')).toHaveSize(0) + resolve() }, 200) - }, 0) - 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>' - ] + 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" data-bs-delay=\'{"show":0,"hide":150}\'>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - const triggerChild = tooltipEl.querySelector('b') + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) - spyOn(tooltip, 'hide').and.callThrough() + expect(tooltip._config.delay).toEqual({ show: 0, hide: 150 }) - tooltipEl.addEventListener('mouseover', () => { - const moveMouseToChildEvent = createEvent('mouseout') - Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { - value: triggerChild - }) + 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.dispatchEvent(moveMouseToChildEvent) + tooltipEl.dispatchEvent(createEvent('mouseover')) }) + }) - tooltipEl.addEventListener('mouseout', () => { - expect(tooltip.hide).not.toHaveBeenCalled() - done() - }) + 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('') - tooltipEl.dispatchEvent(createEvent('mouseover')) - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + const triggerChild = tooltipEl.querySelector('b') - 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>' + const spy = spyOn(tooltip, 'hide').and.callThrough() - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + tooltipEl.addEventListener('mouseover', () => { + const moveMouseToChildEvent = createEvent('mouseout') + Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', { + value: triggerChild + }) + + tooltipEl.dispatchEvent(moveMouseToChildEvent) + }) + + tooltipEl.addEventListener('mouseout', () => { + expect(spy).not.toHaveBeenCalled() + resolve() + }) - spyOn(window, 'getComputedStyle').and.returnValue({ - transitionDuration: '0.15s', - transitionDelay: '0s' + tooltipEl.dispatchEvent(createEvent('mouseover')) }) + }) - setTimeout(() => { - expect(tooltip._popper).not.toBeNull() - expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('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().classList.contains('show')).toEqual(false) - 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')).toBe('top') - done() - }, 200) - }, 0) + expect(tooltip._getTipElement().getAttribute('data-popper-placement')).toEqual('top') + 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">' + 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"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) - - 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) + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.15s', + transitionDelay: '0s' + }) - setTimeout(() => { - expect(insertedFunc).toHaveBeenCalledTimes(1) - tooltip.hide() - - 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(2) + 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"></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 tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip.classList.contains('custom-class')).toBeTrue() - 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"></a>' - 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.classList.contains('custom-class')).toBeTrue() - expect(tip.classList.contains('custom-class-2')).toBeTrue() - 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" data-class-a="custom-class-a" data-class-b="custom-class-b"></a>' - const spy = jasmine.createSpy('customClass').and.returnValue('custom-class') - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - customClass: spy - }) + const tooltipEl = fixtureEl.querySelector('a') + const spy = jasmine.createSpy('customClass').and.callFake(function (el) { + return `${el.dataset.classA} ${this.dataset.classB}` + }) + 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-a') + expect(tip).toHaveClass('custom-class-b') + resolve() + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(spy).toHaveBeenCalled() - expect(tip.classList.contains('custom-class')).toBeTrue() - done() + tooltip.show() }) + }) - tooltip.show() + 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) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + expect(tooltipEl.getAttribute('title')).toBeNull() + resolve() + }) + 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"></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', () => 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"></a>' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl) + const spy = spyOn(EventHandler, 'off') - tooltipEl.addEventListener('shown.bs.tooltip', () => { - document.documentElement.ontouchstart = noop - spyOn(EventHandler, 'off') - tooltip.hide() - }) + tooltipEl.addEventListener('shown.bs.tooltip', () => { + document.documentElement.ontouchstart = noop + 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(spy).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"></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', () => 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, reject) => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' - 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', () => { + reject(new Error('should not trigger hidden event')) + }) - tooltip.show() + tooltip.show() + }) }) it('should not throw error running hide if popper hasn\'t been shown', () => { @@ -956,26 +1069,28 @@ 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"></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', () => { - spyOn(tooltip._popper, 'update') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const spy = spyOn(tooltip._popper, 'update') - tooltip.update() + tooltip.update() - expect(tooltip._popper.update).toHaveBeenCalled() - done() - }) + expect(spy).toHaveBeenCalled() + resolve() + }) - tooltip.show() + tooltip.show() + }) }) it('should do nothing if the tooltip is not shown', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) @@ -985,140 +1100,122 @@ describe('Tooltip', () => { }) }) - describe('isWithContent', () => { + describe('_isWithContent', () => { it('should return true if there is content', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - expect(tooltip.isWithContent()).toEqual(true) + expect(tooltip._isWithContent()).toBeTrue() }) it('should return false if there is no content', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title=""></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - expect(tooltip.isWithContent()).toEqual(false) + expect(tooltip._isWithContent()).toBeFalse() }) }) - describe('getTipElement', () => { + describe('_getTipElement', () => { it('should create the tip element and return it', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - spyOn(document, 'createElement').and.callThrough() + const spy = spyOn(document, 'createElement').and.callThrough() - expect(tooltip.getTipElement()).toBeDefined() - expect(document.createElement).toHaveBeenCalled() + expect(tooltip._getTipElement()).toBeDefined() + expect(spy).toHaveBeenCalled() }) it('should return the created tip element', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) const spy = spyOn(document, 'createElement').and.callThrough() - expect(tooltip.getTipElement()).toBeDefined() + expect(tooltip._getTipElement()).toBeDefined() expect(spy).toHaveBeenCalled() spy.calls.reset() - expect(tooltip.getTipElement()).toBeDefined() + expect(tooltip._getTipElement()).toBeDefined() expect(spy).not.toHaveBeenCalled() }) }) describe('setContent', () => { it('should set tip content', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { animation: false }) - const tip = tooltip.getTipElement() + const tip = tooltip._getTipElement() tooltip.setContent(tip) - expect(tip.classList.contains('show')).toEqual(false) - expect(tip.classList.contains('fade')).toEqual(false) + expect(tip).not.toHaveClass('show') + expect(tip).not.toHaveClass('fade') expect(tip.querySelector('.tooltip-inner').textContent).toEqual('Another tooltip') }) it('should re-show tip if it was already shown', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.show() - const tip = () => tooltip.getTipElement() + const tip = () => tooltip._getTipElement() - expect(tip().classList.contains('show')).toEqual(true) + expect(tip()).toHaveClass('show') tooltip.setContent({ '.tooltip-inner': 'foo' }) - expect(tip().classList.contains('show')).toEqual(true) + expect(tip()).toHaveClass('show') expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo') }) it('should keep tip hidden, if it was already hidden before', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - const tip = () => tooltip.getTipElement() + const tip = () => tooltip._getTipElement() - expect(tip().classList.contains('show')).toEqual(false) + expect(tip()).not.toHaveClass('show') tooltip.setContent({ '.tooltip-inner': 'foo' }) - expect(tip().classList.contains('show')).toEqual(false) - expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo') - }) - }) - - describe('updateAttachment', () => { - it('should use end class name when right placement specified', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' - - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: 'right' - }) - - tooltipEl.addEventListener('inserted.bs.tooltip', () => { - expect(tooltip.getTipElement().classList.contains('bs-tooltip-auto')).toEqual(true) - done() - }) - + expect(tip()).not.toHaveClass('show') tooltip.show() + expect(tip().querySelector('.tooltip-inner').textContent).toEqual('foo') }) - it('should use start class name when left placement specified', done => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + it('"setContent" should keep the initial template', () => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - placement: 'left' - }) + const tooltip = new Tooltip(tooltipEl) - tooltipEl.addEventListener('inserted.bs.tooltip', () => { - expect(tooltip.getTipElement().classList.contains('bs-tooltip-auto')).toEqual(true) - done() - }) + tooltip.setContent({ '.tooltip-inner': 'foo' }) + const tip = tooltip._getTipElement() - tooltip.show() + expect(tip).toHaveClass('tooltip') + expect(tip).toHaveClass('bs-tooltip-auto') + expect(tip.querySelector('.tooltip-arrow')).not.toBeNull() + expect(tip.querySelector('.tooltip-inner')).not.toBeNull() }) }) describe('setContent', () => { it('should do nothing if the element is null', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) @@ -1130,7 +1227,8 @@ describe('Tooltip', () => { it('should do nothing if the content is a child of the element', () => { fixtureEl.innerHTML = [ '<a href="#" rel="tooltip" title="Another tooltip">', - '<div id="childContent"></div>' + ' <div id="childContent"></div>', + '</a>' ].join('') const tooltipEl = fixtureEl.querySelector('a') @@ -1139,7 +1237,7 @@ describe('Tooltip', () => { html: true }) - tooltip.getTipElement().append(childContent) + tooltip._getTipElement().append(childContent) tooltip.setContent({ '.tooltip': childContent }) expect().nothing() @@ -1148,7 +1246,8 @@ describe('Tooltip', () => { it('should add the content as a child of the element for jQuery elements', () => { fixtureEl.innerHTML = [ '<a href="#" rel="tooltip" title="Another tooltip">', - '<div id="childContent"></div>' + ' <div id="childContent"></div>', + '</a>' ].join('') const tooltipEl = fixtureEl.querySelector('a') @@ -1158,14 +1257,16 @@ describe('Tooltip', () => { }) tooltip.setContent({ '.tooltip': { 0: childContent, jquery: 'jQuery' } }) + tooltip.show() - expect(childContent.parentNode).toEqual(tooltip.getTipElement()) + expect(childContent.parentNode).toEqual(tooltip._getTipElement()) }) it('should add the child text content in the element', () => { fixtureEl.innerHTML = [ '<a href="#" rel="tooltip" title="Another tooltip">', - '<div id="childContent">Tooltip</div>' + ' <div id="childContent">Tooltip</div>', + '</a>' ].join('') const tooltipEl = fixtureEl.querySelector('a') @@ -1174,11 +1275,11 @@ describe('Tooltip', () => { tooltip.setContent({ '.tooltip': childContent }) - expect(childContent.textContent).toEqual(tooltip.getTipElement().textContent) + expect(childContent.textContent).toEqual(tooltip._getTipElement().textContent) }) it('should add html without sanitize it', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { @@ -1188,11 +1289,11 @@ describe('Tooltip', () => { tooltip.setContent({ '.tooltip': '<div id="childContent">Tooltip</div>' }) - expect(tooltip.getTipElement().querySelector('div').id).toEqual('childContent') + expect(tooltip._getTipElement().querySelector('div').id).toEqual('childContent') }) it('should add html sanitized', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl, { @@ -1201,35 +1302,35 @@ describe('Tooltip', () => { const content = [ '<div id="childContent">', - ' <button type="button">test btn</button>', + ' <button type="button">test btn</button>', '</div>' ].join('') tooltip.setContent({ '.tooltip': content }) - expect(tooltip.getTipElement().querySelector('div').id).toEqual('childContent') - expect(tooltip.getTipElement().querySelector('button')).toEqual(null) + expect(tooltip._getTipElement().querySelector('div').id).toEqual('childContent') + expect(tooltip._getTipElement().querySelector('button')).toBeNull() }) it('should add text content', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) tooltip.setContent({ '.tooltip': 'test' }) - expect(tooltip.getTipElement().textContent).toEqual('test') + expect(tooltip._getTipElement().textContent).toEqual('test') }) }) - describe('getTitle', () => { + describe('_getTitle', () => { it('should return the title', () => { - fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">' + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip"></a>' const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - expect(tooltip.getTitle()).toEqual('Another tooltip') + expect(tooltip._getTitle()).toEqual('Another tooltip') }) it('should call title function', () => { @@ -1240,7 +1341,33 @@ describe('Tooltip', () => { title: () => 'test' }) - expect(tooltip.getTitle()).toEqual('test') + expect(tooltip._getTitle()).toEqual('test') + }) + + it('should call title function with trigger element', () => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-foo="bar"></a>' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + title(el) { + return el.dataset.foo + } + }) + + expect(tooltip._getTitle()).toEqual('bar') + }) + + it('should call title function with correct this value', () => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-foo="bar"></a>' + + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + title() { + return this.dataset.foo + } + }) + + expect(tooltip._getTitle()).toEqual('bar') }) }) @@ -1260,60 +1387,85 @@ describe('Tooltip', () => { const div = fixtureEl.querySelector('div') - expect(Tooltip.getInstance(div)).toEqual(null) + expect(Tooltip.getInstance(div)).toBeNull() }) }) 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 add the aria-label attribute when element text content is a whitespace string', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="A tooltip"><span> </span></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('A tooltip') + 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 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') + resolve() + }) - expect(tooltipShown).not.toBeNull() - expect(tooltipEl.getAttribute('aria-label')).toBeNull() - done() + tooltip.show() }) + }) - tooltip.show() + 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) + + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tooltipShown = document.querySelector('.tooltip') + + expect(tooltipShown).not.toBeNull() + expect(tooltipEl.getAttribute('aria-label')).toBeNull() + resolve() + }) + + tooltip.show() + }) }) }) @@ -1334,7 +1486,7 @@ describe('Tooltip', () => { const div = fixtureEl.querySelector('div') - expect(Tooltip.getInstance(div)).toEqual(null) + expect(Tooltip.getInstance(div)).toBeNull() expect(Tooltip.getOrCreateInstance(div)).toBeInstanceOf(Tooltip) }) @@ -1343,13 +1495,13 @@ describe('Tooltip', () => { const div = fixtureEl.querySelector('div') - expect(Tooltip.getInstance(div)).toEqual(null) + expect(Tooltip.getInstance(div)).toBeNull() const tooltip = Tooltip.getOrCreateInstance(div, { title: () => 'test' }) expect(tooltip).toBeInstanceOf(Tooltip) - expect(tooltip.getTitle()).toEqual('test') + expect(tooltip._getTitle()).toEqual('test') }) it('should return the instance when exists without given configuration', () => { @@ -1367,7 +1519,7 @@ describe('Tooltip', () => { expect(tooltip).toBeInstanceOf(Tooltip) expect(tooltip2).toEqual(tooltip) - expect(tooltip2.getTitle()).toEqual('nothing') + expect(tooltip2._getTitle()).toEqual('nothing') }) }) @@ -1405,7 +1557,7 @@ describe('Tooltip', () => { const div = fixtureEl.querySelector('div') const tooltip = new Tooltip(div) - spyOn(tooltip, 'show') + const spy = spyOn(tooltip, 'show') jQueryMock.fn.tooltip = Tooltip.jQueryInterface jQueryMock.elements = [div] @@ -1413,7 +1565,7 @@ describe('Tooltip', () => { jQueryMock.fn.tooltip.call(jQueryMock, 'show') expect(Tooltip.getInstance(div)).toEqual(tooltip) - expect(tooltip.show).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('should throw error on undefined method', () => { diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index 818ddf221..0faaac6a5 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -1,6 +1,6 @@ -import Backdrop from '../../../src/util/backdrop' -import { getTransitionDurationFromElement } from '../../../src/util/index' -import { clearFixture, getFixture } from '../../helpers/fixture' +import Backdrop from '../../../src/util/backdrop.js' +import { getTransitionDurationFromElement } from '../../../src/util/index.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' const CLASS_BACKDROP = '.modal-backdrop' const CLASS_NAME_FADE = 'fade' @@ -23,270 +23,297 @@ describe('Backdrop', () => { }) describe('show', () => { - it('if it is "shown", should append the backdrop html once, on show, and contain "show" class', 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().length).toEqual(0) + expect(getElements()).toHaveSize(0) - instance.show() - instance.show(() => { - expect(getElements().length).toEqual(1) - for (const el of getElements()) { - expect(el.classList.contains(CLASS_NAME_SHOW)).toEqual(true) - } + instance.show() + instance.show(() => { + expect(getElements()).toHaveSize(1) + for (const el of getElements()) { + expect(el).toHaveClass(CLASS_NAME_SHOW) + } - done() + resolve() + }) }) }) - it('if it is not "shown", should not append the backdrop html', 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().length).toEqual(0) - instance.show(() => { - expect(getElements().length).toEqual(0) - done() + expect(getElements()).toHaveSize(0) + instance.show(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) - it('if it is "shown" and "animated", should append the backdrop html once, and contain "fade" class', 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().length).toEqual(0) + expect(getElements()).toHaveSize(0) - instance.show(() => { - expect(getElements().length).toEqual(1) - for (const el of getElements()) { - expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true) - } + 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().length).toEqual(0) - instance.show(() => { - expect(getElements().length).toEqual(1) - instance.hide(() => { - expect(getElements().length).toEqual(0) - done() + expect(getElements()).toHaveSize(0) + instance.show(() => { + expect(getElements()).toHaveSize(1) + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) - it('should remove "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.classList.contains(CLASS_NAME_SHOW)).toEqual(false) - done() + instance.show() + instance.hide(() => { + expect(elem).not.toHaveClass(CLASS_NAME_SHOW) + resolve() + }) }) }) - it('if it is not "shown", should not try to remove Node on remove method', 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().length).toEqual(0) - expect(instance._isAppended).toEqual(false) - instance.show(() => { - instance.hide(() => { - expect(getElements().length).toEqual(0) - expect(spy).not.toHaveBeenCalled() - expect(instance._isAppended).toEqual(false) - 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().length).toEqual(0) - done() + instance.show(() => { + wrapper.remove() + instance.hide(() => { + expect(getElements()).toHaveSize(0) + resolve() + }) }) }) }) }) describe('click callback', () => { - it('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 = document.createEvent('MouseEvents') - clickEvent.initEvent('mousedown', true, true) - document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) - endTest() - }) - }) - }) - - describe('animation callbacks', () => { - it('if it is animated, should show and hide backdrop after counting transition duration', 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() - }) - expect(spy2).not.toHaveBeenCalled() - }) + const instance = new Backdrop({ + isVisible: true, + isAnimated: false, + clickCallback: () => spy() + }) + const endTest = () => { + setTimeout(() => { + expect(spy).toHaveBeenCalled() + resolve() + }, 10) + } - it('if it is not animated, should show and hide backdrop without delay', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - const instance = new Backdrop({ - isVisible: true, - isAnimated: false + instance.show(() => { + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent) + endTest() + }) }) - const spy2 = jasmine.createSpy('spy2') - - instance.show(spy2) - instance.hide(spy2) - - setTimeout(() => { - expect(spy2).toHaveBeenCalled() - expect(spy).not.toHaveBeenCalled() - done() - }, 10) }) - it('if it is not "shown", should not call delay callbacks', done => { - const instance = new Backdrop({ - isVisible: false, - isAnimated: true + 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 spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - instance.show() - instance.hide(() => { - expect(spy).not.toHaveBeenCalled() - done() - }) - }) - }) - 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() + 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) }) }) - 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 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() + }) }) }) + }) - it('Should appended on any element given by the proper config', done => { - fixtureEl.innerHTML = [ - '<div id="wrapper">', - '</div>' - ].join('') + 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() + }) + }) + }) - const wrapper = fixtureEl.querySelector('#wrapper') - const instance = new Backdrop({ - isVisible: true, - rootElement: wrapper + 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() + }) + }) }) - 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 be able to have different classNames than default', 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/component-functions.spec.js b/js/tests/unit/util/component-functions.spec.js index edaedd32e..ce83785e2 100644 --- a/js/tests/unit/util/component-functions.spec.js +++ b/js/tests/unit/util/component-functions.spec.js @@ -1,8 +1,6 @@ -/* Test helpers */ - -import { clearFixture, createEvent, getFixture } from '../../helpers/fixture' -import { enableDismissTrigger } from '../../../src/util/component-functions' -import BaseComponent from '../../../src/base-component' +import BaseComponent from '../../../src/base-component.js' +import { enableDismissTrigger } from '../../../src/util/component-functions.js' +import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js' class DummyClass2 extends BaseComponent { static get NAME() { @@ -33,12 +31,12 @@ describe('Plugin functions', () => { it('should get Plugin and execute the given method, when a click occurred on data-bs-dismiss="PluginName"', () => { fixtureEl.innerHTML = [ '<div id="foo" class="test">', - ' <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>', + ' <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>', '</div>' ].join('') - spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() - spyOn(DummyClass2.prototype, 'testMethod') + const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() + const spyTest = spyOn(DummyClass2.prototype, 'testMethod') const componentWrapper = fixtureEl.querySelector('#foo') const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') @@ -46,19 +44,19 @@ describe('Plugin functions', () => { enableDismissTrigger(DummyClass2, 'testMethod') btnClose.dispatchEvent(event) - expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper) - expect(DummyClass2.prototype.testMethod).toHaveBeenCalled() + expect(spyGet).toHaveBeenCalledWith(componentWrapper) + expect(spyTest).toHaveBeenCalled() }) it('if data-bs-dismiss="PluginName" hasn\'t got "data-bs-target", "getOrCreateInstance" has to be initialized by closest "plugin.Name" class', () => { fixtureEl.innerHTML = [ '<div id="foo" class="test">', - ' <button type="button" data-bs-dismiss="test"></button>', + ' <button type="button" data-bs-dismiss="test"></button>', '</div>' ].join('') - spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() - spyOn(DummyClass2.prototype, 'hide') + const spyGet = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() + const spyHide = spyOn(DummyClass2.prototype, 'hide') const componentWrapper = fixtureEl.querySelector('#foo') const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') @@ -66,31 +64,31 @@ describe('Plugin functions', () => { enableDismissTrigger(DummyClass2) btnClose.dispatchEvent(event) - expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper) - expect(DummyClass2.prototype.hide).toHaveBeenCalled() + expect(spyGet).toHaveBeenCalledWith(componentWrapper) + expect(spyHide).toHaveBeenCalled() }) it('if data-bs-dismiss="PluginName" is disabled, must not trigger function', () => { fixtureEl.innerHTML = [ '<div id="foo" class="test">', - ' <button type="button" disabled data-bs-dismiss="test"></button>', + ' <button type="button" disabled data-bs-dismiss="test"></button>', '</div>' ].join('') - spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() + const spy = spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough() const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]') const event = createEvent('click') enableDismissTrigger(DummyClass2) btnClose.dispatchEvent(event) - expect(DummyClass2.getOrCreateInstance).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should prevent default when the trigger is <a> or <area>', () => { fixtureEl.innerHTML = [ '<div id="foo" class="test">', - ' <a type="button" data-bs-dismiss="test"></a>', + ' <a type="button" data-bs-dismiss="test"></a>', '</div>' ].join('') @@ -98,11 +96,11 @@ describe('Plugin functions', () => { const event = createEvent('click') enableDismissTrigger(DummyClass2) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough() btnClose.dispatchEvent(event) - expect(Event.prototype.preventDefault).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) }) }) diff --git a/js/tests/unit/util/config.spec.js b/js/tests/unit/util/config.spec.js new file mode 100644 index 000000000..93987a74a --- /dev/null +++ b/js/tests/unit/util/config.spec.js @@ -0,0 +1,166 @@ +import Config from '../../../src/util/config.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' + +class DummyConfigClass extends Config { + static get NAME() { + return 'dummy' + } +} + +describe('Config', () => { + let fixtureEl + const name = 'dummy' + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('NAME', () => { + it('should return plugin NAME', () => { + expect(DummyConfigClass.NAME).toEqual(name) + }) + }) + + describe('DefaultType', () => { + it('should return plugin default type', () => { + expect(DummyConfigClass.DefaultType).toEqual(jasmine.any(Object)) + }) + }) + + describe('Default', () => { + it('should return plugin defaults', () => { + expect(DummyConfigClass.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('mergeConfigObj', () => { + it('should parse element\'s data attributes and merge it with default config. Element\'s data attributes must excel Defaults', () => { + fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string1="bar"></div>' + + spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ + testBool: true, + testString: 'foo', + testString1: 'foo', + testInt: 7 + }) + const instance = new DummyConfigClass() + const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test')) + + expect(configResult.testBool).toEqual(false) + expect(configResult.testString).toEqual('foo') + expect(configResult.testString1).toEqual('bar') + expect(configResult.testInt).toEqual(8) + }) + + it('should parse element\'s data attributes and merge it with default config, plug these given during method call. The programmatically given should excel all', () => { + fixtureEl.innerHTML = '<div id="test" data-bs-test-bool="false" data-bs-test-int="8" data-bs-test-string-1="bar"></div>' + + spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ + testBool: true, + testString: 'foo', + testString1: 'foo', + testInt: 7 + }) + const instance = new DummyConfigClass() + const configResult = instance._mergeConfigObj({ + testString1: 'test', + testInt: 3 + }, fixtureEl.querySelector('#test')) + + expect(configResult.testBool).toEqual(false) + expect(configResult.testString).toEqual('foo') + expect(configResult.testString1).toEqual('test') + expect(configResult.testInt).toEqual(3) + }) + + it('should parse element\'s data attribute `config` and any rest attributes. The programmatically given should excel all. Data attribute `config` should excel only Defaults', () => { + fixtureEl.innerHTML = '<div id="test" data-bs-config=\'{"testBool":false,"testInt":50,"testInt2":100}\' data-bs-test-int="8" data-bs-test-string-1="bar"></div>' + + spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ + testBool: true, + testString: 'foo', + testString1: 'foo', + testInt: 7, + testInt2: 600 + }) + const instance = new DummyConfigClass() + const configResult = instance._mergeConfigObj({ + testString1: 'test' + }, fixtureEl.querySelector('#test')) + + expect(configResult.testBool).toEqual(false) + expect(configResult.testString).toEqual('foo') + expect(configResult.testString1).toEqual('test') + expect(configResult.testInt).toEqual(8) + expect(configResult.testInt2).toEqual(100) + }) + + it('should omit element\'s data attribute `config` if is not an object', () => { + fixtureEl.innerHTML = '<div id="test" data-bs-config="foo" data-bs-test-int="8"></div>' + + spyOnProperty(DummyConfigClass, 'Default', 'get').and.returnValue({ + testInt: 7, + testInt2: 79 + }) + const instance = new DummyConfigClass() + const configResult = instance._mergeConfigObj({}, fixtureEl.querySelector('#test')) + + expect(configResult.testInt).toEqual(8) + expect(configResult.testInt2).toEqual(79) + }) + }) + + describe('typeCheckConfig', () => { + it('should check type of the config object', () => { + spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ + toggle: 'boolean', + parent: '(string|element)' + }) + const config = { + toggle: true, + parent: 777 + } + + const obj = new DummyConfigClass() + expect(() => { + obj._typeCheckConfig(config) + }).toThrowError(TypeError, `${obj.constructor.NAME.toUpperCase()}: Option "parent" provided type "number" but expected type "(string|element)".`) + }) + + it('should return null stringified when null is passed', () => { + spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ + toggle: 'boolean', + parent: '(null|element)' + }) + + const obj = new DummyConfigClass() + const config = { + toggle: true, + parent: null + } + + obj._typeCheckConfig(config) + expect().nothing() + }) + + it('should return undefined stringified when undefined is passed', () => { + spyOnProperty(DummyConfigClass, 'DefaultType', 'get').and.returnValue({ + toggle: 'boolean', + parent: '(undefined|element)' + }) + + const obj = new DummyConfigClass() + const config = { + toggle: true, + parent: undefined + } + + obj._typeCheckConfig(config) + expect().nothing() + }) + }) +}) diff --git a/js/tests/unit/util/focustrap.spec.js b/js/tests/unit/util/focustrap.spec.js index 99bc95fca..0a20017d5 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 EventHandler from '../../../src/dom/event-handler.js' +import SelectorEngine from '../../../src/dom/selector-engine.js' +import FocusTrap from '../../../src/util/focustrap.js' +import { clearFixture, createEvent, getFixture } from '../../helpers/fixture.js' describe('FocusTrap', () => { let fixtureEl @@ -20,12 +20,12 @@ describe('FocusTrap', () => { const trapElement = fixtureEl.querySelector('div') - spyOn(trapElement, 'focus') + const spy = spyOn(trapElement, 'focus') const focustrap = new FocusTrap({ trapElement }) focustrap.activate() - expect(trapElement.focus).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('if configured not to autofocus, should not autofocus itself', () => { @@ -33,148 +33,156 @@ describe('FocusTrap', () => { const trapElement = fixtureEl.querySelector('div') - spyOn(trapElement, 'focus') + const spy = spyOn(trapElement, 'focus') const focustrap = new FocusTrap({ trapElement, autofocus: false }) focustrap.activate() - expect(trapElement.focus).not.toHaveBeenCalled() + expect(spy).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(spy).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } - spyOn(inside, 'focus') - spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside]) + const spy = 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]) + const spy = spyOn(first, 'focus').and.callThrough() + + const focusInListener = () => { + expect(spy).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]) + const spy = spyOn(last, 'focus').and.callThrough() + + const focusInListener = () => { + expect(spy).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(spy).toHaveBeenCalled() + document.removeEventListener('focusin', focusInListener) + resolve() + } - spyOn(focustrap._config.trapElement, 'focus') + const spy = 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) + }) }) }) @@ -182,29 +190,29 @@ describe('FocusTrap', () => { it('should flag itself as no longer active', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) focustrap.activate() - expect(focustrap._isActive).toBe(true) + expect(focustrap._isActive).toBeTrue() focustrap.deactivate() - expect(focustrap._isActive).toBe(false) + expect(focustrap._isActive).toBeFalse() }) it('should remove all event listeners', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) focustrap.activate() - spyOn(EventHandler, 'off') + const spy = spyOn(EventHandler, 'off') focustrap.deactivate() - expect(EventHandler.off).toHaveBeenCalled() + expect(spy).toHaveBeenCalled() }) it('doesn\'t try removing event listeners unless it needs to (in case it hasn\'t been activated)', () => { const focustrap = new FocusTrap({ trapElement: fixtureEl }) - spyOn(EventHandler, 'off') + const spy = spyOn(EventHandler, 'off') focustrap.deactivate() - expect(EventHandler.off).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) }) }) diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js index ccfe5e2c2..9e154818f 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 * as Util from '../../../src/util/index.js' +import { noop } from '../../../src/util/index.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Util', () => { let fixtureEl @@ -21,119 +22,6 @@ describe('Util', () => { }) }) - describe('getSelectorFromElement', () => { - it('should get selector from data-bs-target', () => { - fixtureEl.innerHTML = [ - '<div id="test" data-bs-target=".target"></div>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should get selector from href if no data-bs-target set', () => { - fixtureEl.innerHTML = [ - '<a id="test" href=".target"></a>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should get selector from href if data-bs-target equal to #', () => { - fixtureEl.innerHTML = [ - '<a id="test" data-bs-target="#" href=".target"></a>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('.target') - }) - - it('should return null if a selector from a href is a url without an anchor', () => { - fixtureEl.innerHTML = [ - '<a id="test" data-bs-target="#" href="foo/bar.html"></a>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - - it('should return the anchor if a selector from a href is a url', () => { - fixtureEl.innerHTML = [ - '<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>', - '<div id="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toEqual('#target') - }) - - it('should return null if selector not found', () => { - fixtureEl.innerHTML = '<a id="test" href=".target"></a>' - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - - it('should return null if no selector', () => { - fixtureEl.innerHTML = '<div></div>' - - const testEl = fixtureEl.querySelector('div') - - expect(Util.getSelectorFromElement(testEl)).toBeNull() - }) - }) - - describe('getElementFromSelector', () => { - it('should get element from data-bs-target', () => { - fixtureEl.innerHTML = [ - '<div id="test" data-bs-target=".target"></div>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) - }) - - it('should get element from href if no data-bs-target set', () => { - fixtureEl.innerHTML = [ - '<a id="test" href=".target"></a>', - '<div class="target"></div>' - ].join('') - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toEqual(fixtureEl.querySelector('.target')) - }) - - it('should return null if element not found', () => { - fixtureEl.innerHTML = '<a id="test" href=".target"></a>' - - const testEl = fixtureEl.querySelector('#test') - - expect(Util.getElementFromSelector(testEl)).toBeNull() - }) - - it('should return null if no selector', () => { - fixtureEl.innerHTML = '<div></div>' - - const testEl = fixtureEl.querySelector('div') - - expect(Util.getElementFromSelector(testEl)).toBeNull() - }) - }) - describe('getTransitionDurationFromElement', () => { it('should get transition from element', () => { fixtureEl.innerHTML = '<div style="transition: all 300ms ease-out;"></div>' @@ -154,34 +42,35 @@ 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) + }) }) }) describe('isElement', () => { it('should detect if the parameter is an element or not and return Boolean', () => { - fixtureEl.innerHTML = - [ - '<div id="foo" class="test"></div>', - '<div id="bar" class="test"></div>' - ].join('') + fixtureEl.innerHTML = [ + '<div id="foo" class="test"></div>', + '<div id="bar" class="test"></div>' + ].join('') const el = fixtureEl.querySelector('#foo') - expect(Util.isElement(el)).toEqual(true) - expect(Util.isElement({})).toEqual(false) - expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toEqual(false) + expect(Util.isElement(el)).toBeTrue() + expect(Util.isElement({})).toBeFalse() + expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toBeFalse() }) it('should detect jQuery element', () => { @@ -193,17 +82,16 @@ describe('Util', () => { jquery: 'foo' } - expect(Util.isElement(fakejQuery)).toEqual(true) + expect(Util.isElement(fakejQuery)).toBeTrue() }) }) describe('getElement', () => { it('should try to parse element', () => { - fixtureEl.innerHTML = - [ - '<div id="foo" class="test"></div>', - '<div id="bar" class="test"></div>' - ].join('') + fixtureEl.innerHTML = [ + '<div id="foo" class="test"></div>', + '<div id="bar" class="test"></div>' + ].join('') const el = fixtureEl.querySelector('div') @@ -225,61 +113,14 @@ describe('Util', () => { }) }) - describe('typeCheckConfig', () => { - const namePlugin = 'collapse' - - it('should check type of the config object', () => { - const defaultType = { - toggle: 'boolean', - parent: '(string|element)' - } - const config = { - toggle: true, - parent: 777 - } - - expect(() => { - Util.typeCheckConfig(namePlugin, config, defaultType) - }).toThrowError(TypeError, 'COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".') - }) - - it('should return null stringified when null is passed', () => { - const defaultType = { - toggle: 'boolean', - parent: '(null|element)' - } - const config = { - toggle: true, - parent: null - } - - Util.typeCheckConfig(namePlugin, config, defaultType) - expect().nothing() - }) - - it('should return undefined stringified when undefined is passed', () => { - const defaultType = { - toggle: 'boolean', - parent: '(undefined|element)' - } - const config = { - toggle: true, - parent: undefined - } - - Util.typeCheckConfig(namePlugin, config, defaultType) - expect().nothing() - }) - }) - describe('isVisible', () => { it('should return false if the element is not defined', () => { - expect(Util.isVisible(null)).toEqual(false) - expect(Util.isVisible(undefined)).toEqual(false) + expect(Util.isVisible(null)).toBeFalse() + expect(Util.isVisible(undefined)).toBeFalse() }) it('should return false if the element provided is not a dom element', () => { - expect(Util.isVisible({})).toEqual(false) + expect(Util.isVisible({})).toBeFalse() }) it('should return false if the element is not visible with display none', () => { @@ -287,7 +128,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('div') - expect(Util.isVisible(div)).toEqual(false) + expect(Util.isVisible(div)).toBeFalse() }) it('should return false if the element is not visible with visibility hidden', () => { @@ -295,7 +136,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('div') - expect(Util.isVisible(div)).toEqual(false) + expect(Util.isVisible(div)).toBeFalse() }) it('should return false if an ancestor element is display none', () => { @@ -311,7 +152,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('.content') - expect(Util.isVisible(div)).toEqual(false) + expect(Util.isVisible(div)).toBeFalse() }) it('should return false if an ancestor element is visibility hidden', () => { @@ -327,7 +168,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('.content') - expect(Util.isVisible(div)).toEqual(false) + expect(Util.isVisible(div)).toBeFalse() }) it('should return true if an ancestor element is visibility hidden, but reverted', () => { @@ -343,7 +184,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('.content') - expect(Util.isVisible(div)).toEqual(true) + expect(Util.isVisible(div)).toBeTrue() }) it('should return true if the element is visible', () => { @@ -355,7 +196,7 @@ describe('Util', () => { const div = fixtureEl.querySelector('#element') - expect(Util.isVisible(div)).toEqual(true) + expect(Util.isVisible(div)).toBeTrue() }) it('should return false if the element is hidden, but not via display or visibility', () => { @@ -367,20 +208,56 @@ describe('Util', () => { const div = fixtureEl.querySelector('#element') - expect(Util.isVisible(div)).toEqual(false) + expect(Util.isVisible(div)).toBeFalse() + }) + + it('should return true if its a closed details element', () => { + fixtureEl.innerHTML = '<details id="element"></details>' + + const div = fixtureEl.querySelector('#element') + + expect(Util.isVisible(div)).toBeTrue() + }) + + it('should return true if the element is visible inside an open details element', () => { + fixtureEl.innerHTML = [ + '<details open>', + ' <div id="element"></div>', + '</details>' + ].join('') + + const div = fixtureEl.querySelector('#element') + + expect(Util.isVisible(div)).toBeTrue() + }) + + it('should return true if the element is a visible summary in a closed details element', () => { + fixtureEl.innerHTML = [ + '<details>', + ' <summary id="element-1">', + ' <span id="element-2"></span>', + ' </summary>', + '</details>' + ].join('') + + const element1 = fixtureEl.querySelector('#element-1') + const element2 = fixtureEl.querySelector('#element-2') + + expect(Util.isVisible(element1)).toBeTrue() + expect(Util.isVisible(element2)).toBeTrue() }) }) describe('isDisabled', () => { it('should return true if the element is not defined', () => { - expect(Util.isDisabled(null)).toEqual(true) - expect(Util.isDisabled(undefined)).toEqual(true) - expect(Util.isDisabled()).toEqual(true) + expect(Util.isDisabled(null)).toBeTrue() + expect(Util.isDisabled(undefined)).toBeTrue() + expect(Util.isDisabled()).toBeTrue() }) it('should return true if the element provided is not a dom element', () => { - expect(Util.isDisabled({})).toEqual(true) - expect(Util.isDisabled('test')).toEqual(true) + expect(Util.isDisabled({})).toBeTrue() + expect(Util.isDisabled('test')).toBeTrue() }) it('should return true if the element has disabled attribute', () => { @@ -396,9 +273,9 @@ describe('Util', () => { const div1 = fixtureEl.querySelector('#element1') const div2 = fixtureEl.querySelector('#element2') - expect(Util.isDisabled(div)).toEqual(true) - expect(Util.isDisabled(div1)).toEqual(true) - expect(Util.isDisabled(div2)).toEqual(true) + expect(Util.isDisabled(div)).toBeTrue() + expect(Util.isDisabled(div1)).toBeTrue() + expect(Util.isDisabled(div2)).toBeTrue() }) it('should return false if the element has disabled attribute with "false" value, or doesn\'t have attribute', () => { @@ -412,8 +289,8 @@ describe('Util', () => { const div = fixtureEl.querySelector('#element') const div1 = fixtureEl.querySelector('#element1') - expect(Util.isDisabled(div)).toEqual(false) - expect(Util.isDisabled(div1)).toEqual(false) + expect(Util.isDisabled(div)).toBeFalse() + expect(Util.isDisabled(div1)).toBeFalse() }) it('should return false if the element is not disabled ', () => { @@ -427,15 +304,16 @@ describe('Util', () => { const el = selector => fixtureEl.querySelector(selector) - expect(Util.isDisabled(el('#button'))).toEqual(false) - expect(Util.isDisabled(el('#select'))).toEqual(false) - expect(Util.isDisabled(el('#input'))).toEqual(false) + expect(Util.isDisabled(el('#button'))).toBeFalse() + expect(Util.isDisabled(el('#select'))).toBeFalse() + expect(Util.isDisabled(el('#input'))).toBeFalse() }) + it('should return true if the element has disabled attribute', () => { fixtureEl.innerHTML = [ '<div>', - ' <input id="input" disabled="disabled"/>', - ' <input id="input1" disabled="disabled"/>', + ' <input id="input" disabled="disabled">', + ' <input id="input1" disabled="disabled">', ' <button id="button" disabled="true"></button>', ' <button id="button1" disabled="disabled"></button>', ' <button id="button2" disabled></button>', @@ -446,12 +324,12 @@ describe('Util', () => { const el = selector => fixtureEl.querySelector(selector) - expect(Util.isDisabled(el('#input'))).toEqual(true) - expect(Util.isDisabled(el('#input1'))).toEqual(true) - expect(Util.isDisabled(el('#button'))).toEqual(true) - expect(Util.isDisabled(el('#button1'))).toEqual(true) - expect(Util.isDisabled(el('#button2'))).toEqual(true) - expect(Util.isDisabled(el('#input'))).toEqual(true) + expect(Util.isDisabled(el('#input'))).toBeTrue() + expect(Util.isDisabled(el('#input1'))).toBeTrue() + expect(Util.isDisabled(el('#button'))).toBeTrue() + expect(Util.isDisabled(el('#button1'))).toBeTrue() + expect(Util.isDisabled(el('#button2'))).toBeTrue() + expect(Util.isDisabled(el('#input'))).toBeTrue() }) it('should return true if the element has class "disabled"', () => { @@ -463,19 +341,19 @@ describe('Util', () => { const div = fixtureEl.querySelector('#element') - expect(Util.isDisabled(div)).toEqual(true) + expect(Util.isDisabled(div)).toBeTrue() }) it('should return true if the element has class "disabled" but disabled attribute is false', () => { fixtureEl.innerHTML = [ '<div>', - ' <input id="input" class="disabled" disabled="false"/>', + ' <input id="input" class="disabled" disabled="false">', '</div>' ].join('') const div = fixtureEl.querySelector('#input') - expect(Util.isDisabled(div)).toEqual(true) + expect(Util.isDisabled(div)).toBeTrue() }) }) @@ -493,7 +371,7 @@ describe('Util', () => { spyOn(document.documentElement, 'attachShadow').and.returnValue(null) - expect(Util.findShadowRoot(div)).toEqual(null) + expect(Util.findShadowRoot(div)).toBeNull() }) it('should return null when we do not find a shadow root', () => { @@ -505,7 +383,7 @@ describe('Util', () => { spyOn(document, 'getRootNode').and.returnValue(undefined) - expect(Util.findShadowRoot(document)).toEqual(null) + expect(Util.findShadowRoot(document)).toBeNull() }) it('should return the shadow root when found', () => { @@ -532,7 +410,7 @@ describe('Util', () => { describe('noop', () => { it('should be a function', () => { - expect(typeof Util.noop).toEqual('function') + expect(Util.noop).toEqual(jasmine.any(Function)) }) }) @@ -569,14 +447,14 @@ describe('Util', () => { document.body.setAttribute('data-bs-no-jquery', '') expect(window.jQuery).toEqual(fakejQuery) - expect(Util.getjQuery()).toEqual(null) + expect(Util.getjQuery()).toBeNull() document.body.removeAttribute('data-bs-no-jquery') }) it('should not return jQuery if not present', () => { window.jQuery = undefined - expect(Util.getjQuery()).toEqual(null) + expect(Util.getjQuery()).toBeNull() }) }) @@ -585,7 +463,7 @@ describe('Util', () => { const spy = jasmine.createSpy() const spy2 = jasmine.createSpy() - spyOn(document, 'addEventListener').and.callThrough() + const spyAdd = spyOn(document, 'addEventListener').and.callThrough() spyOnProperty(document, 'readyState').and.returnValue('loading') Util.onDOMContentLoaded(spy) @@ -598,7 +476,7 @@ describe('Util', () => { expect(spy).toHaveBeenCalled() expect(spy2).toHaveBeenCalled() - expect(document.addEventListener).toHaveBeenCalledTimes(1) + expect(spyAdd).toHaveBeenCalledTimes(1) }) it('should execute callback if readyState is not "loading"', () => { @@ -623,14 +501,14 @@ 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).toBe(pluginMock.jQueryInterface) - expect(fakejQuery.fn.test.Constructor).toBe(pluginMock) - expect(typeof fakejQuery.fn.test.noConflict).toEqual('function') + expect(fakejQuery.fn.test).toEqual(pluginMock.jQueryInterface) + expect(fakejQuery.fn.test.Constructor).toEqual(pluginMock) + expect(fakejQuery.fn.test.noConflict).toEqual(jasmine.any(Function)) }) }) @@ -640,6 +518,25 @@ describe('Util', () => { Util.execute(spy) expect(spy).toHaveBeenCalled() }) + + it('should execute if arg is function & return the result', () => { + const functionFoo = (num1, num2 = 10) => num1 + num2 + const resultFoo = Util.execute(functionFoo, [undefined, 4, 5]) + expect(resultFoo).toBe(9) + + const resultFoo1 = Util.execute(functionFoo, [undefined, 4]) + expect(resultFoo1).toBe(14) + + const functionBar = () => 'foo' + const resultBar = Util.execute(functionBar) + expect(resultBar).toBe('foo') + }) + + it('should not execute if arg is not function & return default argument', () => { + const foo = 'bar' + expect(Util.execute(foo)).toBe('bar') + expect(Util.execute(foo, [], 4)).toBe(4) + }) }) describe('executeAfterTransition', () => { @@ -670,96 +567,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/sanitizer.spec.js b/js/tests/unit/util/sanitizer.spec.js index 28d624c87..2b21ef2e1 100644 --- a/js/tests/unit/util/sanitizer.spec.js +++ b/js/tests/unit/util/sanitizer.spec.js @@ -1,4 +1,4 @@ -import { DefaultAllowlist, sanitizeHtml } from '../../../src/util/sanitizer' +import { DefaultAllowlist, sanitizeHtml } from '../../../src/util/sanitizer.js' describe('Sanitizer', () => { describe('sanitizeHtml', () => { @@ -10,17 +10,75 @@ describe('Sanitizer', () => { expect(result).toEqual(empty) }) - it('should sanitize template by removing tags with XSS', () => { - const template = [ - '<div>', - ' <a href="javascript:alert(7)">Click me</a>', - ' <span>Some content</span>', - '</div>' - ].join('') - - const result = sanitizeHtml(template, DefaultAllowlist, null) + it('should retain tags with valid URLs', () => { + const validUrls = [ + '', + 'http://abc', + 'HTTP://abc', + 'https://abc', + 'HTTPS://abc', + 'ftp://abc', + 'FTP://abc', + 'mailto:[email protected]', + 'MAILTO:[email protected]', + 'tel:123-123-1234', + 'TEL:123-123-1234', + 'sip:[email protected]', + 'SIP:[email protected]', + '#anchor', + '/page1.md', + 'http://JavaScript/my.js', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', // Truncated. + 'data:video/webm;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'data:audio/opus;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/', + 'unknown-scheme:abc' + ] + + for (const url of validUrls) { + const template = [ + '<div>', + ` <a href="${url}">Click me</a>`, + ' <span>Some content</span>', + '</div>' + ].join('') + + const result = sanitizeHtml(template, DefaultAllowlist, null) + + expect(result).toContain(`href="${url}"`) + } + }) - expect(result).not.toContain('href="javascript:alert(7)') + it('should sanitize template by removing tags with XSS', () => { + const invalidUrls = [ + // eslint-disable-next-line no-script-url + 'javascript:alert(7)', + // eslint-disable-next-line no-script-url + 'javascript:evil()', + // eslint-disable-next-line no-script-url + 'JavaScript:abc', + ' javascript:abc', + ' \n Java\n Script:abc', + 'javascript:', + 'javascript:', + 'j avascript:', + 'javascript:', + 'javascript:', + 'jav	ascript:alert();', + 'jav\u0000ascript:alert();' + ] + + for (const url of invalidUrls) { + const template = [ + '<div>', + ` <a href="${url}">Click me</a>`, + ' <span>Some content</span>', + '</div>' + ].join('') + + const result = sanitizeHtml(template, DefaultAllowlist, null) + + expect(result).not.toContain(`href="${url}"`) + } }) it('should sanitize template and work with multiple regex', () => { @@ -84,12 +142,12 @@ describe('Sanitizer', () => { return htmlUnsafe } - spyOn(DOMParser.prototype, 'parseFromString') + const spy = spyOn(DOMParser.prototype, 'parseFromString') const result = sanitizeHtml(template, DefaultAllowlist, mySanitize) expect(result).toEqual(template) - expect(DOMParser.prototype.parseFromString).not.toHaveBeenCalled() + expect(spy).not.toHaveBeenCalled() }) it('should allow multiple sanitation passes of the same template', () => { diff --git a/js/tests/unit/util/scrollbar.spec.js b/js/tests/unit/util/scrollbar.spec.js index 280adb8e5..6dadfcdd1 100644 --- a/js/tests/unit/util/scrollbar.spec.js +++ b/js/tests/unit/util/scrollbar.spec.js @@ -1,13 +1,13 @@ -import { clearBodyAndDocument, clearFixture, getFixture } from '../../helpers/fixture' -import Manipulator from '../../../src/dom/manipulator' -import ScrollBarHelper from '../../../src/util/scrollbar' +import Manipulator from '../../../src/dom/manipulator.js' +import ScrollBarHelper from '../../../src/util/scrollbar.js' +import { clearBodyAndDocument, clearFixture, getFixture } from '../../helpers/fixture.js' describe('ScrollBar', () => { let fixtureEl const doc = document.documentElement - const parseInt = arg => Number.parseInt(arg, 10) - const getPaddingX = el => parseInt(window.getComputedStyle(el).paddingRight) - const getMarginX = el => parseInt(window.getComputedStyle(el).marginRight) + const parseIntDecimal = arg => Number.parseInt(arg, 10) + const getPaddingX = el => parseIntDecimal(window.getComputedStyle(el).paddingRight) + const getMarginX = el => parseIntDecimal(window.getComputedStyle(el).marginRight) const getOverFlow = el => el.style.overflow const getPaddingAttr = el => Manipulator.getDataAttribute(el, 'padding-right') const getMarginAttr = el => Manipulator.getDataAttribute(el, 'margin-right') @@ -24,7 +24,9 @@ describe('ScrollBar', () => { } } - const isScrollBarHidden = () => { // IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling. So the tests for scrollbar would fail + // iOS, Android devices and macOS browsers hide scrollbar by default and show it only while scrolling. + // So the tests for scrollbar would fail + const isScrollBarHidden = () => { const calc = windowCalculations() return calc.htmlClient === calc.htmlOffset && calc.htmlClient === calc.window } @@ -52,28 +54,24 @@ describe('ScrollBar', () => { it('should return true if body is overflowing', () => { document.documentElement.style.overflowY = 'scroll' document.body.style.overflowY = 'scroll' - fixtureEl.innerHTML = [ - '<div style="height: 110vh; width: 100%"></div>' - ].join('') + fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>' const result = new ScrollBarHelper().isOverflowing() if (isScrollBarHidden()) { - expect(result).toEqual(false) + expect(result).toBeFalse() } else { - expect(result).toEqual(true) + expect(result).toBeTrue() } }) it('should return false if body is not overflowing', () => { doc.style.overflowY = 'hidden' document.body.style.overflowY = 'hidden' - fixtureEl.innerHTML = [ - '<div style="height: 110vh; width: 100%"></div>' - ].join('') + fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>' const scrollBar = new ScrollBarHelper() const result = scrollBar.isOverflowing() - expect(result).toEqual(false) + expect(result).toBeFalse() }) }) @@ -81,13 +79,11 @@ describe('ScrollBar', () => { it('should return an integer greater than zero, if body is overflowing', () => { doc.style.overflowY = 'scroll' document.body.style.overflowY = 'scroll' - fixtureEl.innerHTML = [ - '<div style="height: 110vh; width: 100%"></div>' - ].join('') + fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>' const result = new ScrollBarHelper().getWidth() if (isScrollBarHidden()) { - expect(result).toBe(0) + expect(result).toEqual(0) } else { expect(result).toBeGreaterThan(1) } @@ -96,9 +92,7 @@ describe('ScrollBar', () => { it('should return 0 if body is not overflowing', () => { document.documentElement.style.overflowY = 'hidden' document.body.style.overflowY = 'hidden' - fixtureEl.innerHTML = [ - '<div style="height: 110vh; width: 100%"></div>' - ].join('') + fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>' const result = new ScrollBarHelper().getWidth() @@ -107,11 +101,11 @@ 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>', - '<div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>', + '<div style="height: 110vh; width: 100%">', + ' <div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>', + ' <div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>', '</div>' ].join('') doc.style.overflowY = 'scroll' @@ -128,25 +122,44 @@ describe('ScrollBar', () => { let currentPadding = getPaddingX(fixedEl) let currentPadding2 = getPaddingX(fixedEl2) - expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`, 'original fixed element padding should be stored in data-bs-padding-right') - expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}px`, 'original fixed element padding should be stored in data-bs-padding-right') - expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening') - expect(currentPadding2).toEqual(expectedPadding2, 'fixed element padding should be adjusted while opening') + expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`) + expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}px`) + expect(currentPadding).toEqual(expectedPadding) + expect(currentPadding2).toEqual(expectedPadding2) scrollBar.reset() currentPadding = getPaddingX(fixedEl) currentPadding2 = getPaddingX(fixedEl2) - expect(getPaddingAttr(fixedEl)).toEqual(null, 'data-bs-padding-right should be cleared after closing') - expect(getPaddingAttr(fixedEl2)).toEqual(null, 'data-bs-padding-right should be cleared after closing') - expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing') - expect(currentPadding2).toEqual(originalPadding2, 'fixed element padding should be reset after closing') - done() + expect(getPaddingAttr(fixedEl)).toBeNull() + expect(getPaddingAttr(fixedEl2)).toBeNull() + expect(currentPadding).toEqual(originalPadding) + expect(currentPadding2).toEqual(originalPadding2) }) - it('should adjust the inline margin and padding of sticky elements', done => { + it('should remove padding & margin if not existed before adjustment', () => { fixtureEl.innerHTML = [ - '<div style="height: 110vh">' + - '<div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>', + '<div style="height: 110vh; width: 100%">', + ' <div class="fixed" id="fixed" style="width: 100vw;"></div>', + ' <div class="sticky-top" id="sticky" style=" width: 100vw;"></div>', + '</div>' + ].join('') + doc.style.overflowY = 'scroll' + + const fixedEl = fixtureEl.querySelector('#fixed') + const stickyEl = fixtureEl.querySelector('#sticky') + const scrollBar = new ScrollBarHelper() + + scrollBar.hide() + scrollBar.reset() + + expect(fixedEl.getAttribute('style').includes('padding-right')).toBeFalse() + expect(stickyEl.getAttribute('style').includes('margin-right')).toBeFalse() + }) + + 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>', '</div>' ].join('') doc.style.overflowY = 'scroll' @@ -159,23 +172,20 @@ describe('ScrollBar', () => { const expectedPadding = originalPadding + scrollBar.getWidth() scrollBar.hide() - expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`, 'original sticky element margin should be stored in data-bs-margin-right') - expect(getMarginX(stickyTopEl)).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening') - expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`, 'original sticky element margin should be stored in data-bs-margin-right') - expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding, 'sticky element margin should be adjusted while opening') + expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`) + expect(getMarginX(stickyTopEl)).toEqual(expectedMargin) + expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`) + expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding) scrollBar.reset() - expect(getMarginAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing') - expect(getMarginX(stickyTopEl)).toEqual(originalMargin, 'sticky element margin should be reset after closing') - expect(getPaddingAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing') - expect(getPaddingX(stickyTopEl)).toEqual(originalPadding, 'sticky element margin should be reset after closing') - done() + expect(getMarginAttr(stickyTopEl)).toBeNull() + expect(getMarginX(stickyTopEl)).toEqual(originalMargin) + expect(getPaddingAttr(stickyTopEl)).toBeNull() + expect(getPaddingX(stickyTopEl)).toEqual(originalPadding) }) it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => { - fixtureEl.innerHTML = [ - '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>' - ].join('') + fixtureEl.innerHTML = '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>' const stickyTopEl = fixtureEl.querySelector('.sticky-top') const originalMargin = getMarginX(stickyTopEl) @@ -187,16 +197,16 @@ describe('ScrollBar', () => { const currentMargin = getMarginX(stickyTopEl) const currentPadding = getPaddingX(stickyTopEl) - expect(currentMargin).toEqual(originalMargin, 'sticky element\'s margin should not be adjusted while opening') - expect(currentPadding).toEqual(originalPadding, 'sticky element\'s padding should not be adjusted while opening') + expect(currentMargin).toEqual(originalMargin) + expect(currentPadding).toEqual(originalPadding) scrollBar.reset() }) it('should not put data-attribute if element doesn\'t have the proper style property, should just remove style property if element didn\'t had one', () => { fixtureEl.innerHTML = [ - '<div style="height: 110vh; width: 100%">' + - '<div class="sticky-top" id="sticky" style="width: 100vw"></div>', + '<div style="height: 110vh; width: 100%">', + ' <div class="sticky-top" id="sticky" style="width: 100vw"></div>', '</div>' ].join('') @@ -232,8 +242,8 @@ describe('ScrollBar', () => { const scrollBarWidth = scrollBar.getWidth() scrollBar.hide() - expect(getPaddingX(document.body)).toEqual(scrollBarWidth, 'body does not have inline padding set') - expect(document.body.style.color).toEqual('red', 'body still has other inline styles set') + expect(getPaddingX(document.body)).toEqual(scrollBarWidth) + expect(document.body.style.color).toEqual('red') scrollBar.reset() }) @@ -243,7 +253,7 @@ describe('ScrollBar', () => { fixtureEl.innerHTML = [ '<style>', ' body {', - ` padding-right: ${styleSheetPadding} }`, + ` padding-right: ${styleSheetPadding}`, ' }', '</style>' ].join('') @@ -253,7 +263,7 @@ describe('ScrollBar', () => { el.style.paddingRight = inlineStylePadding const originalPadding = getPaddingX(el) - expect(originalPadding).toEqual(parseInt(inlineStylePadding)) // Respect only the inline style as it has prevails this of css + expect(originalPadding).toEqual(parseIntDecimal(inlineStylePadding)) // Respect only the inline style as it has prevails this of css const originalOverFlow = 'auto' el.style.overflow = originalOverFlow const scrollBar = new ScrollBarHelper() @@ -264,7 +274,7 @@ describe('ScrollBar', () => { const currentPadding = getPaddingX(el) expect(currentPadding).toEqual(scrollBarWidth + originalPadding) - expect(currentPadding).toEqual(scrollBarWidth + parseInt(inlineStylePadding)) + expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(inlineStylePadding)) expect(getPaddingAttr(el)).toEqual(inlineStylePadding) expect(getOverFlow(el)).toEqual('hidden') expect(getOverFlowAttr(el)).toEqual(originalOverFlow) @@ -273,9 +283,9 @@ describe('ScrollBar', () => { const currentPadding1 = getPaddingX(el) expect(currentPadding1).toEqual(originalPadding) - expect(getPaddingAttr(el)).toEqual(null) + expect(getPaddingAttr(el)).toBeNull() expect(getOverFlow(el)).toEqual(originalOverFlow) - expect(getOverFlowAttr(el)).toEqual(null) + expect(getOverFlowAttr(el)).toBeNull() }) it('should hide scrollbar and reset it to its initial value - respecting css rules', () => { @@ -283,7 +293,7 @@ describe('ScrollBar', () => { fixtureEl.innerHTML = [ '<style>', ' body {', - ` padding-right: ${styleSheetPadding} }`, + ` padding-right: ${styleSheetPadding}`, ' }', '</style>' ].join('') @@ -299,7 +309,7 @@ describe('ScrollBar', () => { const currentPadding = getPaddingX(el) expect(currentPadding).toEqual(scrollBarWidth + originalPadding) - expect(currentPadding).toEqual(scrollBarWidth + parseInt(styleSheetPadding)) + expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(styleSheetPadding)) expect(getPaddingAttr(el)).toBeNull() // We do not have to keep css padding expect(getOverFlow(el)).toEqual('hidden') expect(getOverFlowAttr(el)).toEqual(originalOverFlow) @@ -308,9 +318,9 @@ describe('ScrollBar', () => { const currentPadding1 = getPaddingX(el) expect(currentPadding1).toEqual(originalPadding) - expect(getPaddingAttr(el)).toEqual(null) + expect(getPaddingAttr(el)).toBeNull() expect(getOverFlow(el)).toEqual(originalOverFlow) - expect(getOverFlowAttr(el)).toEqual(null) + expect(getOverFlowAttr(el)).toBeNull() }) it('should not adjust the inline body padding when it does not overflow', () => { @@ -324,7 +334,7 @@ describe('ScrollBar', () => { scrollBar.hide() const currentPadding = getPaddingX(document.body) - expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted') + expect(currentPadding).toEqual(originalPadding) scrollBar.reset() }) @@ -344,7 +354,7 @@ describe('ScrollBar', () => { const currentPadding = getPaddingX(document.body) - expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted') + expect(currentPadding).toEqual(originalPadding) scrollBar.reset() }) diff --git a/js/tests/unit/util/swipe.spec.js b/js/tests/unit/util/swipe.spec.js index 474e34f65..9252d312b 100644 --- a/js/tests/unit/util/swipe.spec.js +++ b/js/tests/unit/util/swipe.spec.js @@ -1,7 +1,7 @@ -import { clearFixture, getFixture } from '../../helpers/fixture' -import EventHandler from '../../../src/dom/event-handler' -import Swipe from '../../../src/util/swipe' -import { noop } from '../../../src/util' +import EventHandler from '../../../src/dom/event-handler.js' +import { noop } from '../../../src/util/index.js' +import Swipe from '../../../src/util/swipe.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('Swipe', () => { const { Simulator, PointerEvent } = window @@ -39,17 +39,17 @@ describe('Swipe', () => { fixtureEl = getFixture() const cssStyle = [ '<style>', - ' #fixture .pointer-event {', - ' touch-action: pan-y;', + ' #fixture .pointer-event {', + ' touch-action: pan-y;', ' }', - ' #fixture div {', - ' width: 300px;', - ' height: 300px;', + ' #fixture div {', + ' width: 300px;', + ' height: 300px;', ' }', '</style>' ].join('') - fixtureEl.innerHTML = '<div id="swipeEl"></div>' + cssStyle + fixtureEl.innerHTML = `<div id="swipeEl"></div>${cssStyle}` swipeEl = fixtureEl.querySelector('div') }) @@ -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 + }) }) }) }) @@ -157,7 +163,7 @@ describe('Swipe', () => { deleteDocumentElementOntouchstart() const swipe = new Swipe(swipeEl) - spyOn(swipe, '_handleSwipe') + const spy = spyOn(swipe, '_handleSwipe') mockSwipeGesture(swipeEl, { pos: [300, 10], @@ -167,56 +173,60 @@ describe('Swipe', () => { }) restorePointerEvents() - expect(swipe._handleSwipe).not.toHaveBeenCalled() + expect(spy).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') + }) }) }) @@ -266,7 +276,7 @@ describe('Swipe', () => { expect(Swipe.isSupported()).toBeTrue() }) - it('should return "false" if "touchstart" not exists in document element and "navigator.maxTouchPoints" are zero (0)', () => { + it('should return "false" if "touchstart" not exists in document element and "navigator.maxTouchPoints" are zero (0)', () => { Object.defineProperty(window.navigator, 'maxTouchPoints', () => 0) deleteDocumentElementOntouchstart() diff --git a/js/tests/unit/util/template-factory.spec.js b/js/tests/unit/util/template-factory.spec.js index 842c480c2..07f4d91c7 100644 --- a/js/tests/unit/util/template-factory.spec.js +++ b/js/tests/unit/util/template-factory.spec.js @@ -1,5 +1,5 @@ -import { clearFixture, getFixture } from '../../helpers/fixture' -import TemplateFactory from '../../../src/util/template-factory' +import TemplateFactory from '../../../src/util/template-factory.js' +import { clearFixture, getFixture } from '../../helpers/fixture.js' describe('TemplateFactory', () => { let fixtureEl @@ -86,26 +86,26 @@ describe('TemplateFactory', () => { const factory = new TemplateFactory({ extraClass: 'testClass' }) - expect(factory.toHtml().classList.contains('testClass')).toBeTrue() + expect(factory.toHtml()).toHaveClass('testClass') }) it('should add extra classes', () => { const factory = new TemplateFactory({ extraClass: 'testClass testClass2' }) - expect(factory.toHtml().classList.contains('testClass')).toBeTrue() - expect(factory.toHtml().classList.contains('testClass2')).toBeTrue() + expect(factory.toHtml()).toHaveClass('testClass') + expect(factory.toHtml()).toHaveClass('testClass2') }) it('should resolve class if function is given', () => { const factory = new TemplateFactory({ - extraClass: arg => { + extraClass(arg) { expect(arg).toEqual(factory) return 'testClass' } }) - expect(factory.toHtml().classList.contains('testClass')).toBeTrue() + expect(factory.toHtml()).toHaveClass('testClass') }) }) }) @@ -113,11 +113,11 @@ describe('TemplateFactory', () => { describe('Content', () => { it('add simple text content', () => { const template = [ - '<div>' + - '<div class="foo"></div>' + - '<div class="foo2"></div>' + + '<div>', + ' <div class="foo"></div>', + ' <div class="foo2"></div>', '</div>' - ].join(' ') + ].join('') const factory = new TemplateFactory({ template, @@ -128,8 +128,8 @@ describe('TemplateFactory', () => { }) const html = factory.toHtml() - expect(html.querySelector('.foo').textContent).toBe('bar') - expect(html.querySelector('.foo2').textContent).toBe('bar2') + expect(html.querySelector('.foo').textContent).toEqual('bar') + expect(html.querySelector('.foo2').textContent).toEqual('bar2') }) it('should not fill template if selector not exists', () => { @@ -140,7 +140,7 @@ describe('TemplateFactory', () => { content: { '#bar': 'test' } }) - expect(factory.toHtml().outerHTML).toBe('<div id="foo"></div>') + expect(factory.toHtml().outerHTML).toEqual('<div id="foo"></div>') }) it('should remove template selector, if content is null', () => { @@ -151,7 +151,7 @@ describe('TemplateFactory', () => { content: { '#foo': null } }) - expect(factory.toHtml().outerHTML).toBe('<div></div>') + expect(factory.toHtml().outerHTML).toEqual('<div></div>') }) it('should resolve content if is function', () => { @@ -162,7 +162,7 @@ describe('TemplateFactory', () => { content: { '#foo': () => null } }) - expect(factory.toHtml().outerHTML).toBe('<div></div>') + expect(factory.toHtml().outerHTML).toEqual('<div></div>') }) it('if content is element and "config.html=false", should put content\'s textContent', () => { @@ -176,9 +176,9 @@ describe('TemplateFactory', () => { }) const fooEl = factory.toHtml().querySelector('#foo') - expect(fooEl.innerHTML).not.toBe(contentElement.innerHTML) - expect(fooEl.textContent).toBe(contentElement.textContent) - expect(fooEl.textContent).toBe('foobar') + expect(fooEl.innerHTML).not.toEqual(contentElement.innerHTML) + expect(fooEl.textContent).toEqual(contentElement.textContent) + expect(fooEl.textContent).toEqual('foobar') }) it('if content is element and "config.html=true", should put content\'s outerHtml as child', () => { @@ -192,8 +192,8 @@ describe('TemplateFactory', () => { }) const fooEl = factory.toHtml().querySelector('#foo') - expect(fooEl.innerHTML).toBe(contentElement.outerHTML) - expect(fooEl.textContent).toBe(contentElement.textContent) + expect(fooEl.innerHTML).toEqual(contentElement.outerHTML) + expect(fooEl.textContent).toEqual(contentElement.textContent) }) }) @@ -245,14 +245,15 @@ describe('TemplateFactory', () => { expect(factory.hasContent()).toBeFalse() }) }) + describe('changeContent', () => { it('should change Content', () => { const template = [ - '<div>' + - '<div class="foo"></div>' + - '<div class="foo2"></div>' + + '<div>', + ' <div class="foo"></div>', + ' <div class="foo2"></div>', '</div>' - ].join(' ') + ].join('') const factory = new TemplateFactory({ template, @@ -276,11 +277,11 @@ describe('TemplateFactory', () => { it('should change only the given, content', () => { const template = [ - '<div>' + - '<div class="foo"></div>' + - '<div class="foo2"></div>' + + '<div>', + ' <div class="foo"></div>', + ' <div class="foo2"></div>', '</div>' - ].join(' ') + ].join('') const factory = new TemplateFactory({ template, |
