diff --git a/build/build-plugins.js b/build/build-plugins.js index 5d8c429635..f380d77dc1 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -34,7 +34,7 @@ const bsPlugins = { SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), Alert: path.resolve(__dirname, '../js/src/alert/alert.js'), Button: path.resolve(__dirname, '../js/src/button/button.js'), - Carousel: path.resolve(__dirname, '../js/src/carousel.js'), + Carousel: path.resolve(__dirname, '../js/src/carousel/carousel.js'), Collapse: path.resolve(__dirname, '../js/src/collapse.js'), Dropdown: path.resolve(__dirname, '../js/src/dropdown.js'), Modal: path.resolve(__dirname, '../js/src/modal.js'), diff --git a/js/index.esm.js b/js/index.esm.js index ca47d7405e..4f5058560d 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -7,7 +7,7 @@ import Alert from './src/alert/alert' import Button from './src/button/button' -import Carousel from './src/carousel' +import Carousel from './src/carousel/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' import Modal from './src/modal' diff --git a/js/index.umd.js b/js/index.umd.js index 2cb90696da..f3b81377e5 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -7,7 +7,7 @@ import Alert from './src/alert/alert' import Button from './src/button/button' -import Carousel from './src/carousel' +import Carousel from './src/carousel/carousel' import Collapse from './src/collapse' import Dropdown from './src/dropdown' import Modal from './src/modal' diff --git a/js/src/carousel.js b/js/src/carousel/carousel.js similarity index 98% rename from js/src/carousel.js rename to js/src/carousel/carousel.js index 7cd790f85d..0e1bad14ac 100644 --- a/js/src/carousel.js +++ b/js/src/carousel/carousel.js @@ -16,11 +16,11 @@ import { reflow, triggerTransitionEnd, typeCheckConfig -} from './util/index' -import Data from './dom/data' -import EventHandler from './dom/event-handler' -import Manipulator from './dom/manipulator' -import SelectorEngine from './dom/selector-engine' +} from '../util/index' +import Data from '../dom/data' +import EventHandler from '../dom/event-handler' +import Manipulator from '../dom/manipulator' +import SelectorEngine from '../dom/selector-engine' /** * ------------------------------------------------------------------------ @@ -283,16 +283,12 @@ class Carousel { .on(this._element, Event.MOUSELEAVE, event => this.cycle(event)) } - if (this._config.touch) { + if (this._config.touch && this._touchSupported) { this._addTouchEventListeners() } } _addTouchEventListeners() { - if (!this._touchSupported) { - return - } - const start = event => { if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { this.touchStartX = event.clientX @@ -631,7 +627,7 @@ EventHandler.on(window, Event.LOAD_DATA_API, () => { * ------------------------------------------------------------------------ * add .carousel to jQuery only if jQuery is present */ - +/* istanbul ignore if */ if (typeof $ !== 'undefined') { const JQUERY_NO_CONFLICT = $.fn[NAME] $.fn[NAME] = Carousel._jQueryInterface diff --git a/js/src/carousel/carousel.spec.js b/js/src/carousel/carousel.spec.js new file mode 100644 index 0000000000..9f897110d5 --- /dev/null +++ b/js/src/carousel/carousel.spec.js @@ -0,0 +1,1197 @@ +import Carousel from './carousel' +import EventHandler from '../dom/event-handler' + +/** Test helpers */ +import { getFixture, clearFixture, createEvent, jQueryMock } from '../../tests/helpers/fixture' + +describe('Carousel', () => { + const { Simulator, PointerEvent, MSPointerEvent } = window + const originWinPointerEvent = PointerEvent || MSPointerEvent + const supportPointerEvent = Boolean(PointerEvent || MSPointerEvent) + + window.MSPointerEvent = null + const cssStyleCarousel = '.carousel.pointer-event { -ms-touch-action: none; touch-action: none; }' + + const stylesCarousel = document.createElement('style') + stylesCarousel.type = 'text/css' + stylesCarousel.appendChild(document.createTextNode(cssStyleCarousel)) + + const clearPointerEvents = () => { + window.PointerEvent = null + } + + const restorePointerEvents = () => { + window.PointerEvent = originWinPointerEvent + } + + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Carousel.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(Carousel.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('constructor', () => { + it('should go to next item if right arrow key is pressed', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(carousel._keydown).toHaveBeenCalled() + done() + }) + + const keyDown = createEvent('keydown') + keyDown.which = 39 + + carouselEl.dispatchEvent(keyDown) + }) + + it('should go to previous item if left arrow key is pressed', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(carousel._keydown).toHaveBeenCalled() + done() + }) + + const keyDown = createEvent('keydown') + keyDown.which = 37 + + carouselEl.dispatchEvent(keyDown) + }) + + it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('keydown', event => { + expect(carousel._keydown).toHaveBeenCalled() + expect(event.defaultPrevented).toEqual(false) + done() + }) + + const keyDown = createEvent('keydown') + keyDown.which = 40 + + carouselEl.dispatchEvent(keyDown) + }) + + it('should ignore keyboard events within s and ', + ' ', + ' ', + ' ', + ' ', + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + const spyKeyDown = spyOn(carousel, '_keydown').and.callThrough() + const spyPrev = spyOn(carousel, 'prev') + const spyNext = spyOn(carousel, 'next') + + const keyDown = createEvent('keydown', { bubbles: true, cancelable: true }) + keyDown.which = 39 + Object.defineProperty(keyDown, 'target', { + value: input, + writable: true, + configurable: true + }) + + input.dispatchEvent(keyDown) + + expect(spyKeyDown).toHaveBeenCalled() + expect(spyPrev).not.toHaveBeenCalled() + expect(spyNext).not.toHaveBeenCalled() + + spyKeyDown.calls.reset() + spyPrev.calls.reset() + spyNext.calls.reset() + + Object.defineProperty(keyDown, 'target', { + value: textarea + }) + textarea.dispatchEvent(keyDown) + + expect(spyKeyDown).toHaveBeenCalled() + expect(spyPrev).not.toHaveBeenCalled() + expect(spyNext).not.toHaveBeenCalled() + }) + + it('should wrap around from end to start when wrap option is true', done => { + fixtureEl.innerHTML = [ + '' + ].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', e => { + 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(e.from + 1).toEqual(3) + done() + } + }) + + carousel.next() + }) + + it('should stay at the start when the prev method is called and wrap is false', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + 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') + }) + + carousel.prev() + + setTimeout(() => { + expect(firstElement.classList.contains('active')).toEqual(true) + done() + }, 10) + }) + + it('should not add touch event listeners if touch = false', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + + spyOn(Carousel.prototype, '_addTouchEventListeners') + + const carousel = new Carousel(carouselEl, { + touch: false + }) + + expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() + }) + + it('should not add touch event listeners if touch supported = false', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + + const carousel = new Carousel(carouselEl) + + EventHandler.off(carouselEl, '.bs-carousel') + carousel._touchSupported = false + + spyOn(carousel, '_addTouchEventListeners') + + carousel._addEventListeners() + + expect(carousel._addTouchEventListeners).not.toHaveBeenCalled() + }) + + it('should add touch event listeners by default', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + + spyOn(Carousel.prototype, '_addTouchEventListeners') + + document.documentElement.ontouchstart = () => {} + const carousel = new Carousel(carouselEl) + + expect(carousel._addTouchEventListeners).toHaveBeenCalled() + }) + + it('should allow swiperight and call prev with pointer events', done => { + if (!supportPointerEvent) { + expect().nothing() + done() + return + } + + document.documentElement.ontouchstart = () => {} + document.head.appendChild(stylesCarousel) + Simulator.setType('pointer') + + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'prev').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(item.classList.contains('active')).toEqual(true) + expect(carousel.prev).toHaveBeenCalled() + document.head.removeChild(stylesCarousel) + delete document.documentElement.ontouchstart + done() + }) + + 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 = () => {} + document.head.appendChild(stylesCarousel) + Simulator.setType('pointer') + + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'next').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(item.classList.contains('active')).toEqual(false) + expect(carousel.next).toHaveBeenCalled() + document.head.removeChild(stylesCarousel) + delete document.documentElement.ontouchstart + done() + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) + }) + + it('should allow swiperight and call prev with touch events', done => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = () => {} + + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'prev').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(item.classList.contains('active')).toEqual(true) + expect(carousel.prev).toHaveBeenCalled() + delete document.documentElement.ontouchstart + restorePointerEvents() + done() + }) + + Simulator.gestures.swipe(carouselEl, { + deltaX: 300, + deltaY: 0 + }) + }) + + it('should allow swipeleft and call next with touch events', done => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = () => {} + + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + const item = fixtureEl.querySelector('#item') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'next').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(item.classList.contains('active')).toEqual(false) + expect(carousel.next).toHaveBeenCalled() + delete document.documentElement.ontouchstart + restorePointerEvents() + done() + }) + + Simulator.gestures.swipe(carouselEl, { + pos: [300, 10], + deltaX: -300, + deltaY: 0 + }) + }) + + it('should not allow pinch with touch events', done => { + Simulator.setType('touch') + clearPointerEvents() + document.documentElement.ontouchstart = () => {} + + fixtureEl.innerHTML = '' + + 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.touchDeltaX).toEqual(0) + done() + }) + }) + + it('should call pause method on mouse over with pause equal to hover', done => { + fixtureEl.innerHTML = '' + + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'pause') + + const mouseOverEvent = createEvent('mouseover') + carouselEl.dispatchEvent(mouseOverEvent) + + setTimeout(() => { + expect(carousel.pause).toHaveBeenCalled() + done() + }, 10) + }) + + it('should call cycle on mouse out with pause equal to hover', done => { + fixtureEl.innerHTML = '' + + const carouselEl = fixtureEl.querySelector('.carousel') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'cycle') + + const mouseOutEvent = createEvent('mouseout') + carouselEl.dispatchEvent(mouseOutEvent) + + setTimeout(() => { + expect(carousel.cycle).toHaveBeenCalled() + done() + }, 10) + }) + }) + + describe('next', () => { + it('should not slide if the carousel is sliding', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + const carousel = new Carousel(carouselEl, {}) + + spyOn(carousel, '_slide') + + carousel._isSliding = true + carousel.next() + + expect(carousel._slide).not.toHaveBeenCalled() + }) + + it('should not fire slid when slide is prevented', done => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + const carousel = new Carousel(carouselEl, {}) + let slidEvent = false + + const doneTest = () => { + setTimeout(() => { + expect(slidEvent).toEqual(false) + done() + }, 20) + } + + carouselEl.addEventListener('slide.bs.carousel', e => { + e.preventDefault() + doneTest() + }) + + carouselEl.addEventListener('slid.bs.carousel', () => { + slidEvent = true + }) + + carousel.next() + }) + + it('should fire slide event with: direction, relatedTarget, from and to', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + const onSlide = e => { + expect(e.direction).toEqual('left') + expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) + expect(e.from).toEqual(0) + expect(e.to).toEqual(1) + + carouselEl.removeEventListener('slide.bs.carousel', onSlide) + carouselEl.addEventListener('slide.bs.carousel', onSlide2) + + carousel.prev() + } + + const onSlide2 = e => { + expect(e.direction).toEqual('right') + done() + } + + carouselEl.addEventListener('slide.bs.carousel', onSlide) + carousel.next() + }) + + it('should fire slid event with: direction, relatedTarget, from and to', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + const onSlid = e => { + expect(e.direction).toEqual('left') + expect(e.relatedTarget.classList.contains('carousel-item')).toEqual(true) + expect(e.from).toEqual(0) + expect(e.to).toEqual(1) + + carouselEl.removeEventListener('slid.bs.carousel', onSlid) + carouselEl.addEventListener('slid.bs.carousel', onSlid2) + + carousel.prev() + } + + const onSlid2 = e => { + expect(e.direction).toEqual('right') + done() + } + + carouselEl.addEventListener('slid.bs.carousel', onSlid) + carousel.next() + }) + + it('should get interval from data attribute in individual item', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + interval: 1814 + }) + + expect(carousel._config.interval).toEqual(1814) + + carousel.next() + + expect(carousel._config.interval).toEqual(7) + }) + + it('should update indicators if present', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const secondIndicator = fixtureEl.querySelector('#secondIndicator') + const carousel = new Carousel(carouselEl) + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(secondIndicator.classList.contains('active')).toEqual(true) + done() + }) + + carousel.next() + }) + }) + + describe('nextWhenVisible', () => { + it('should not call next when the page is not visible', () => { + fixtureEl.innerHTML = '' + + const carouselEl = fixtureEl.querySelector('div') + const carousel = new Carousel(carouselEl) + + spyOn(carousel, 'next') + + carousel.nextWhenVisible() + + expect(carousel.next).not.toHaveBeenCalled() + }) + }) + + describe('prev', () => { + it('should not slide if the carousel is sliding', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + const carousel = new Carousel(carouselEl, {}) + + spyOn(carousel, '_slide') + + carousel._isSliding = true + carousel.prev() + + expect(carousel._slide).not.toHaveBeenCalled() + }) + }) + + describe('pause', () => { + it('should call cycle if the carousel have carousel-item-next and carousel-item-prev class', () => { + fixtureEl.innerHTML = [ + '' + ].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 = [ + '' + ].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 = [ + '' + ].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) + }) + }) + + describe('cycle', () => { + it('should set an interval', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) + + spyOn(window, 'setInterval').and.callThrough() + + carousel.cycle() + + expect(window.setInterval).toHaveBeenCalled() + }) + + it('should not set interval if the carousel is paused', () => { + fixtureEl.innerHTML = [ + '' + ].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() + }) + + it('should clear interval if there is one', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) + + carousel._interval = setInterval(() => {}, 10) + + spyOn(window, 'setInterval').and.callThrough() + spyOn(window, 'clearInterval').and.callThrough() + + carousel.cycle() + + expect(window.setInterval).toHaveBeenCalled() + expect(window.clearInterval).toHaveBeenCalled() + }) + }) + + describe('to', () => { + it('should go directement to the provided index', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + + carousel.to(2) + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) + done() + }) + }) + + it('should return to a previous slide if the provided index is lower than the current', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item3')) + + carousel.to(1) + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + done() + }) + }) + + it('should do nothing if a wrong index is provided', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + const spy = spyOn(carousel, '_slide') + + carousel.to(25) + + expect(spy).not.toHaveBeenCalled() + + spy.calls.reset() + + carousel.to(-5) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should call pause and cycle is the provided is the same compare to the current one', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + spyOn(carousel, '_slide') + spyOn(carousel, 'pause') + spyOn(carousel, 'cycle') + + carousel.to(0) + + expect(carousel._slide).not.toHaveBeenCalled() + expect(carousel.pause).toHaveBeenCalled() + expect(carousel.cycle).toHaveBeenCalled() + }) + + it('should wait before performing to if a slide is sliding', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, {}) + + spyOn(EventHandler, 'one').and.callThrough() + spyOn(carousel, '_slide') + + carousel._isSliding = true + carousel.to(1) + + expect(carousel._slide).not.toHaveBeenCalled() + expect(EventHandler.one).toHaveBeenCalled() + + spyOn(carousel, 'to') + + EventHandler.trigger(carouselEl, 'slid.bs.carousel') + + setTimeout(() => { + expect(carousel.to).toHaveBeenCalledWith(1) + done() + }) + }) + }) + + describe('dispose', () => { + it('should destroy a carousel', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl) + + spyOn(EventHandler, 'off').and.callThrough() + + carousel.dispose() + + expect(EventHandler.off).toHaveBeenCalled() + }) + }) + + describe('_jQueryInterface', () => { + it('should create a carousel', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.carousel = Carousel._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.carousel.call(jQueryMock) + + expect(Carousel._getInstance(div)).toBeDefined() + }) + + it('should not re create a carousel', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const carousel = new Carousel(div) + + jQueryMock.fn.carousel = Carousel._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.carousel.call(jQueryMock) + + expect(Carousel._getInstance(div)).toEqual(carousel) + }) + + it('should call to if the config is a number', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const carousel = new Carousel(div) + const slideTo = 2 + + spyOn(carousel, 'to') + + jQueryMock.fn.carousel = Carousel._jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.carousel.call(jQueryMock, slideTo) + + expect(carousel.to).toHaveBeenCalledWith(slideTo) + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.carousel = Carousel._jQueryInterface + jQueryMock.elements = [div] + + try { + jQueryMock.fn.carousel.call(jQueryMock, action) + } catch (error) { + expect(error.message).toEqual(`No method named "${action}"`) + } + }) + }) + + describe('data-api', () => { + it('should init carousels with data-ride="carousel" on load', () => { + fixtureEl.innerHTML = '
' + + const carouselEl = fixtureEl.querySelector('div') + const loadEvent = createEvent('load') + + window.dispatchEvent(loadEvent) + + expect(Carousel._getInstance(carouselEl)).toBeDefined() + }) + + it('should create carousel and go to the next slide on click', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const next = fixtureEl.querySelector('#next') + const item2 = fixtureEl.querySelector('#item2') + + next.click() + + setTimeout(() => { + expect(item2.classList.contains('active')).toEqual(true) + done() + }, 10) + }) + + it('should create carousel and go to the next slide on click with data-slide-to', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const next = fixtureEl.querySelector('#next') + const item2 = fixtureEl.querySelector('#item2') + + next.click() + + setTimeout(() => { + expect(item2.classList.contains('active')).toEqual(true) + done() + }, 10) + }) + + it('should do nothing if no selector on click on arrows', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const next = fixtureEl.querySelector('#next') + + next.click() + + expect().nothing() + }) + + it('should do nothing if no carousel class on click on arrows', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const next = fixtureEl.querySelector('#next') + + next.click() + + expect().nothing() + }) + }) +}) diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index b5b9fb3736..00bf5d8d35 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -74,7 +74,9 @@ const rollupPreprocessor = { } } -let files = [] +let files = [ + 'node_modules/hammer-simulator/index.js' +] const conf = { basePath: '../..', diff --git a/js/tests/unit/carousel.js b/js/tests/unit/carousel.js deleted file mode 100644 index f92d87c612..0000000000 --- a/js/tests/unit/carousel.js +++ /dev/null @@ -1,1370 +0,0 @@ -$(function () { - 'use strict' - - window.Carousel = typeof bootstrap === 'undefined' ? Carousel : bootstrap.Carousel - - var originWinPointerEvent = window.PointerEvent - window.MSPointerEvent = null - var supportPointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) - - function clearPointerEvents() { - window.PointerEvent = null - } - - function restorePointerEvents() { - window.PointerEvent = originWinPointerEvent - } - - var stylesCarousel = [ - '' - ].join('') - - QUnit.module('carousel plugin') - - QUnit.test('should be defined on jQuery object', function (assert) { - assert.expect(1) - assert.ok($(document.body).carousel, 'carousel method is defined') - }) - - QUnit.module('carousel', { - beforeEach: function () { - // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode - $.fn.bootstrapCarousel = $.fn.carousel.noConflict() - }, - afterEach: function () { - $('.carousel').bootstrapCarousel('dispose') - $.fn.carousel = $.fn.bootstrapCarousel - delete $.fn.bootstrapCarousel - $('#qunit-fixture').html('') - } - }) - - QUnit.test('should provide no conflict', function (assert) { - assert.expect(1) - assert.strictEqual(typeof $.fn.carousel, 'undefined', 'carousel was set back to undefined (orig value)') - }) - - QUnit.test('should return the version', function (assert) { - assert.expect(1) - assert.strictEqual(typeof Carousel.VERSION, 'string') - }) - - QUnit.test('should return default parameters', function (assert) { - assert.expect(1) - - var defaultConfig = Carousel.Default - - assert.strictEqual(defaultConfig.touch, true) - }) - - QUnit.test('should throw explicit error on undefined method', function (assert) { - assert.expect(1) - var $el = $('
') - $el.bootstrapCarousel() - try { - $el.bootstrapCarousel('noMethod') - } catch (error) { - assert.strictEqual(error.message, 'No method named "noMethod"') - } - }) - - QUnit.test('should return jquery collection containing the element', function (assert) { - assert.expect(2) - var $el = $('
') - var $carousel = $el.bootstrapCarousel() - assert.ok($carousel instanceof $, 'returns jquery collection') - assert.strictEqual($carousel[0], $el[0], 'collection contains element') - }) - - QUnit.test('should type check config options', function (assert) { - assert.expect(2) - - var message - var expectedMessage = 'CAROUSEL: Option "interval" provided type "string" but expected type "(number|boolean)".' - var config = { - interval: 'fat sux' - } - - try { - $('
').bootstrapCarousel(config) - } catch (error) { - message = error.message - } - - assert.ok(message === expectedMessage, 'correct error message') - - config = { - keyboard: document.createElement('div') - } - expectedMessage = 'CAROUSEL: Option "keyboard" provided type "element" but expected type "boolean".' - - try { - $('
').bootstrapCarousel(config) - } catch (error) { - message = error.message - } - - assert.ok(message === expectedMessage, 'correct error message') - }) - - QUnit.test('should not fire slid when slide is prevented', function (assert) { - assert.expect(1) - var done = assert.async() - var $carousel = $('