diff options
Diffstat (limited to 'js/src/carousel')
| -rw-r--r-- | js/src/carousel/carousel.js | 641 | ||||
| -rw-r--r-- | js/src/carousel/carousel.spec.js | 1197 |
2 files changed, 1838 insertions, 0 deletions
diff --git a/js/src/carousel/carousel.js b/js/src/carousel/carousel.js new file mode 100644 index 000000000..0e1bad14a --- /dev/null +++ b/js/src/carousel/carousel.js @@ -0,0 +1,641 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): carousel.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement, + isVisible, + makeArray, + 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' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'carousel' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.carousel' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key +const ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key +const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch +const SWIPE_THRESHOLD = 40 + +const Default = { + interval: 5000, + keyboard: true, + slide: false, + pause: 'hover', + wrap: true, + touch: true +} + +const DefaultType = { + interval: '(number|boolean)', + keyboard: 'boolean', + slide: '(boolean|string)', + pause: '(string|boolean)', + wrap: 'boolean', + touch: 'boolean' +} + +const Direction = { + NEXT: 'next', + PREV: 'prev', + LEFT: 'left', + RIGHT: 'right' +} + +const Event = { + SLIDE: `slide${EVENT_KEY}`, + SLID: `slid${EVENT_KEY}`, + KEYDOWN: `keydown${EVENT_KEY}`, + MOUSEENTER: `mouseenter${EVENT_KEY}`, + MOUSELEAVE: `mouseleave${EVENT_KEY}`, + TOUCHSTART: `touchstart${EVENT_KEY}`, + TOUCHMOVE: `touchmove${EVENT_KEY}`, + TOUCHEND: `touchend${EVENT_KEY}`, + POINTERDOWN: `pointerdown${EVENT_KEY}`, + POINTERUP: `pointerup${EVENT_KEY}`, + DRAG_START: `dragstart${EVENT_KEY}`, + LOAD_DATA_API: `load${EVENT_KEY}${DATA_API_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + CAROUSEL: 'carousel', + ACTIVE: 'active', + SLIDE: 'slide', + RIGHT: 'carousel-item-right', + LEFT: 'carousel-item-left', + NEXT: 'carousel-item-next', + PREV: 'carousel-item-prev', + ITEM: 'carousel-item', + POINTER_EVENT: 'pointer-event' +} + +const Selector = { + ACTIVE: '.active', + ACTIVE_ITEM: '.active.carousel-item', + ITEM: '.carousel-item', + ITEM_IMG: '.carousel-item img', + NEXT_PREV: '.carousel-item-next, .carousel-item-prev', + INDICATORS: '.carousel-indicators', + DATA_SLIDE: '[data-slide], [data-slide-to]', + DATA_RIDE: '[data-ride="carousel"]' +} + +const PointerType = { + TOUCH: 'touch', + PEN: 'pen' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ +class Carousel { + constructor(element, config) { + this._items = null + this._interval = null + this._activeElement = null + this._isPaused = false + this._isSliding = false + this.touchTimeout = null + this.touchStartX = 0 + this.touchDeltaX = 0 + + this._config = this._getConfig(config) + this._element = element + this._indicatorsElement = SelectorEngine.findOne(Selector.INDICATORS, this._element) + this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0 + this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent) + + this._addEventListeners() + Data.setData(element, DATA_KEY, this) + } + + // Getters + + static get VERSION() { + return VERSION + } + + static get Default() { + return Default + } + + // Public + + next() { + if (!this._isSliding) { + this._slide(Direction.NEXT) + } + } + + nextWhenVisible() { + // Don't call next when the page isn't visible + // or the carousel or its parent isn't visible + if (!document.hidden && isVisible(this._element)) { + this.next() + } + } + + prev() { + if (!this._isSliding) { + this._slide(Direction.PREV) + } + } + + pause(event) { + if (!event) { + this._isPaused = true + } + + if (SelectorEngine.findOne(Selector.NEXT_PREV, this._element)) { + triggerTransitionEnd(this._element) + this.cycle(true) + } + + clearInterval(this._interval) + this._interval = null + } + + cycle(event) { + if (!event) { + this._isPaused = false + } + + if (this._interval) { + clearInterval(this._interval) + this._interval = null + } + + if (this._config && this._config.interval && !this._isPaused) { + this._interval = setInterval( + (document.visibilityState ? this.nextWhenVisible : this.next).bind(this), + this._config.interval + ) + } + } + + to(index) { + this._activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) + const activeIndex = this._getItemIndex(this._activeElement) + + if (index > this._items.length - 1 || index < 0) { + return + } + + if (this._isSliding) { + EventHandler.one(this._element, Event.SLID, () => this.to(index)) + return + } + + if (activeIndex === index) { + this.pause() + this.cycle() + return + } + + const direction = index > activeIndex ? + Direction.NEXT : + Direction.PREV + + this._slide(direction, this._items[index]) + } + + dispose() { + EventHandler.off(this._element, EVENT_KEY) + Data.removeData(this._element, DATA_KEY) + + this._items = null + this._config = null + this._element = null + this._interval = null + this._isPaused = null + this._isSliding = null + this._activeElement = null + this._indicatorsElement = null + } + + // Private + + _getConfig(config) { + config = { + ...Default, + ...config + } + typeCheckConfig(NAME, config, DefaultType) + return config + } + + _handleSwipe() { + const absDeltax = Math.abs(this.touchDeltaX) + + if (absDeltax <= SWIPE_THRESHOLD) { + return + } + + const direction = absDeltax / this.touchDeltaX + + this.touchDeltaX = 0 + + // swipe left + if (direction > 0) { + this.prev() + } + + // swipe right + if (direction < 0) { + this.next() + } + } + + _addEventListeners() { + if (this._config.keyboard) { + EventHandler + .on(this._element, Event.KEYDOWN, event => this._keydown(event)) + } + + if (this._config.pause === 'hover') { + EventHandler + .on(this._element, Event.MOUSEENTER, event => this.pause(event)) + EventHandler + .on(this._element, Event.MOUSELEAVE, event => this.cycle(event)) + } + + if (this._config.touch && this._touchSupported) { + this._addTouchEventListeners() + } + } + + _addTouchEventListeners() { + const start = event => { + if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { + this.touchStartX = event.clientX + } else if (!this._pointerEvent) { + this.touchStartX = event.touches[0].clientX + } + } + + const move = event => { + // ensure swiping with one touch and not pinching + if (event.touches && event.touches.length > 1) { + this.touchDeltaX = 0 + } else { + this.touchDeltaX = event.touches[0].clientX - this.touchStartX + } + } + + const end = event => { + if (this._pointerEvent && PointerType[event.pointerType.toUpperCase()]) { + this.touchDeltaX = event.clientX - this.touchStartX + } + + this._handleSwipe() + if (this._config.pause === 'hover') { + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling + + this.pause() + if (this.touchTimeout) { + clearTimeout(this.touchTimeout) + } + + this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) + } + } + + makeArray(SelectorEngine.find(Selector.ITEM_IMG, this._element)).forEach(itemImg => { + EventHandler.on(itemImg, Event.DRAG_START, e => e.preventDefault()) + }) + + if (this._pointerEvent) { + EventHandler.on(this._element, Event.POINTERDOWN, event => start(event)) + EventHandler.on(this._element, Event.POINTERUP, event => end(event)) + + this._element.classList.add(ClassName.POINTER_EVENT) + } else { + EventHandler.on(this._element, Event.TOUCHSTART, event => start(event)) + EventHandler.on(this._element, Event.TOUCHMOVE, event => move(event)) + EventHandler.on(this._element, Event.TOUCHEND, event => end(event)) + } + } + + _keydown(event) { + if (/input|textarea/i.test(event.target.tagName)) { + return + } + + switch (event.which) { + case ARROW_LEFT_KEYCODE: + event.preventDefault() + this.prev() + break + case ARROW_RIGHT_KEYCODE: + event.preventDefault() + this.next() + break + default: + } + } + + _getItemIndex(element) { + this._items = element && element.parentNode ? + makeArray(SelectorEngine.find(Selector.ITEM, element.parentNode)) : + [] + + return this._items.indexOf(element) + } + + _getItemByDirection(direction, activeElement) { + const isNextDirection = direction === Direction.NEXT + const isPrevDirection = direction === Direction.PREV + const activeIndex = this._getItemIndex(activeElement) + const lastItemIndex = this._items.length - 1 + const isGoingToWrap = isPrevDirection && activeIndex === 0 || + isNextDirection && activeIndex === lastItemIndex + + if (isGoingToWrap && !this._config.wrap) { + return activeElement + } + + const delta = direction === Direction.PREV ? -1 : 1 + const itemIndex = (activeIndex + delta) % this._items.length + + return itemIndex === -1 ? + this._items[this._items.length - 1] : + this._items[itemIndex] + } + + _triggerSlideEvent(relatedTarget, eventDirectionName) { + const targetIndex = this._getItemIndex(relatedTarget) + const fromIndex = this._getItemIndex(SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element)) + + return EventHandler.trigger(this._element, Event.SLIDE, { + relatedTarget, + direction: eventDirectionName, + from: fromIndex, + to: targetIndex + }) + } + + _setActiveIndicatorElement(element) { + if (this._indicatorsElement) { + const indicators = SelectorEngine.find(Selector.ACTIVE, this._indicatorsElement) + for (let i = 0; i < indicators.length; i++) { + indicators[i].classList.remove(ClassName.ACTIVE) + } + + const nextIndicator = this._indicatorsElement.children[ + this._getItemIndex(element) + ] + + if (nextIndicator) { + nextIndicator.classList.add(ClassName.ACTIVE) + } + } + } + + _slide(direction, element) { + const activeElement = SelectorEngine.findOne(Selector.ACTIVE_ITEM, this._element) + const activeElementIndex = this._getItemIndex(activeElement) + const nextElement = element || activeElement && + this._getItemByDirection(direction, activeElement) + + const nextElementIndex = this._getItemIndex(nextElement) + const isCycling = Boolean(this._interval) + + let directionalClassName + let orderClassName + let eventDirectionName + + if (direction === Direction.NEXT) { + directionalClassName = ClassName.LEFT + orderClassName = ClassName.NEXT + eventDirectionName = Direction.LEFT + } else { + directionalClassName = ClassName.RIGHT + orderClassName = ClassName.PREV + eventDirectionName = Direction.RIGHT + } + + if (nextElement && nextElement.classList.contains(ClassName.ACTIVE)) { + this._isSliding = false + return + } + + const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName) + if (slideEvent.defaultPrevented) { + return + } + + if (!activeElement || !nextElement) { + // Some weirdness is happening, so we bail + return + } + + this._isSliding = true + + if (isCycling) { + this.pause() + } + + this._setActiveIndicatorElement(nextElement) + + if (this._element.classList.contains(ClassName.SLIDE)) { + nextElement.classList.add(orderClassName) + + reflow(nextElement) + + activeElement.classList.add(directionalClassName) + nextElement.classList.add(directionalClassName) + + const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10) + if (nextElementInterval) { + this._config.defaultInterval = this._config.defaultInterval || this._config.interval + this._config.interval = nextElementInterval + } else { + this._config.interval = this._config.defaultInterval || this._config.interval + } + + const transitionDuration = getTransitionDurationFromElement(activeElement) + + EventHandler + .one(activeElement, TRANSITION_END, () => { + nextElement.classList.remove(directionalClassName) + nextElement.classList.remove(orderClassName) + nextElement.classList.add(ClassName.ACTIVE) + + activeElement.classList.remove(ClassName.ACTIVE) + activeElement.classList.remove(orderClassName) + activeElement.classList.remove(directionalClassName) + + this._isSliding = false + + setTimeout(() => { + EventHandler.trigger(this._element, Event.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }) + }, 0) + }) + + emulateTransitionEnd(activeElement, transitionDuration) + } else { + activeElement.classList.remove(ClassName.ACTIVE) + nextElement.classList.add(ClassName.ACTIVE) + + this._isSliding = false + EventHandler.trigger(this._element, Event.SLID, { + relatedTarget: nextElement, + direction: eventDirectionName, + from: activeElementIndex, + to: nextElementIndex + }) + } + + if (isCycling) { + this.cycle() + } + } + + // Static + + static _carouselInterface(element, config) { + let data = Data.getData(element, DATA_KEY) + let _config = { + ...Default, + ...Manipulator.getDataAttributes(element) + } + + if (typeof config === 'object') { + _config = { + ..._config, + ...config + } + } + + const action = typeof config === 'string' ? config : _config.slide + + if (!data) { + data = new Carousel(element, _config) + } + + if (typeof config === 'number') { + data.to(config) + } else if (typeof action === 'string') { + if (typeof data[action] === 'undefined') { + throw new TypeError(`No method named "${action}"`) + } + + data[action]() + } else if (_config.interval && _config.ride) { + data.pause() + data.cycle() + } + } + + static _jQueryInterface(config) { + return this.each(function () { + Carousel._carouselInterface(this, config) + }) + } + + static _dataApiClickHandler(event) { + const selector = getSelectorFromElement(this) + + if (!selector) { + return + } + + const target = SelectorEngine.findOne(selector) + + if (!target || !target.classList.contains(ClassName.CAROUSEL)) { + return + } + + const config = { + ...Manipulator.getDataAttributes(target), + ...Manipulator.getDataAttributes(this) + } + const slideIndex = this.getAttribute('data-slide-to') + + if (slideIndex) { + config.interval = false + } + + Carousel._carouselInterface(target, config) + + if (slideIndex) { + Data.getData(target, DATA_KEY).to(slideIndex) + } + + event.preventDefault() + } + + static _getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ + +EventHandler + .on(document, Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler) + +EventHandler.on(window, Event.LOAD_DATA_API, () => { + const carousels = makeArray(SelectorEngine.find(Selector.DATA_RIDE)) + for (let i = 0, len = carousels.length; i < len; i++) { + Carousel._carouselInterface(carousels[i], Data.getData(carousels[i], DATA_KEY)) + } +}) + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * 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 + $.fn[NAME].Constructor = Carousel + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Carousel._jQueryInterface + } +} + +export default Carousel diff --git a/js/src/carousel/carousel.spec.js b/js/src/carousel/carousel.spec.js new file mode 100644 index 000000000..9f897110d --- /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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div id="item2" class="carousel-item">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].join('') + + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) + + spyOn(carousel, '_keydown').and.callThrough() + + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) + expect(carousel._keydown).toHaveBeenCalled() + 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 = [ + '<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 + }) + + 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 = [ + '<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 + }) + + 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 <input>s and <textarea>s', () => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">', + ' <input type="text" />', + ' <textarea></textarea>', + ' </div>', + ' <div class="carousel-item"></div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].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 = [ + '<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', 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 = [ + '<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 }) + + 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 = '<div></div>' + + 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 = '<div></div>' + + 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 = '<div></div>' + + 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 = [ + '<div class="carousel" data-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, '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 = [ + '<div class="carousel" data-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, '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 = [ + '<div class="carousel" data-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, '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 = [ + '<div class="carousel" data-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, '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 = '<div class="carousel" data-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.touchDeltaX).toEqual(0) + done() + }) + }) + + it('should call pause method on mouse over with pause equal to hover', done => { + fixtureEl.innerHTML = '<div class="carousel"></div>' + + 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 = '<div class="carousel"></div>' + + 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 = '<div></div>' + + 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 = '<div></div>' + + 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 = [ + '<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 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 = [ + '<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 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-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, { + 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <ol class="carousel-indicators">', + ' <li data-target="#myCarousel" data-slide-to="0" class="active"></li>', + ' <li id="secondIndicator" data-target="#myCarousel" data-slide-to="1"></li>', + ' <li data-target="#myCarousel" data-slide-to="2"></li>', + ' </ol>', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-interval="7">item 2</div>', + ' <div class="carousel-item">item 3</div>', + ' </div>', + '</div>' + ].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 = '<div class="carousel" data-interval="false"></div>' + + 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 = '<div></div>' + + 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 = [ + '<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) + }) + }) + + describe('cycle', () => { + it('should set an interval', () => { + 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.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() + }) + + it('should clear interval if there is one', () => { + 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) + + 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 = [ + '<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')) + + 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 = [ + '<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, {}) + + 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-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 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-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, {}) + + 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-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, {}) + + 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 = [ + '<div id="myCarousel" class="carousel slide">', + ' <div class="carousel-inner">', + ' <div class="carousel-item active">item 1</div>', + ' <div class="carousel-item" data-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) + + spyOn(EventHandler, 'off').and.callThrough() + + carousel.dispose() + + expect(EventHandler.off).toHaveBeenCalled() + }) + }) + + describe('_jQueryInterface', () => { + it('should create a carousel', () => { + fixtureEl.innerHTML = '<div></div>' + + 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 = '<div></div>' + + 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 = '<div></div>' + + 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 = '<div></div>' + + 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 = '<div data-ride="carousel"></div>' + + 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 = [ + '<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 class="carousel-control-prev" data-target="#myCarousel" role="button" data-slide="prev"></div>', + ' <div id="next" class="carousel-control-next" data-target="#myCarousel" role="button" data-slide="next"></div>', + '</div>' + ].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 = [ + '<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-target="#myCarousel" data-slide-to="1"></div>', + '</div>' + ].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 = [ + '<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" data-target="#myCarousel" role="button" data-slide="prev"></div>', + ' <div id="next" class="carousel-control-next" role="button" data-slide="next"></div>', + '</div>' + ].join('') + + const next = fixtureEl.querySelector('#next') + + next.click() + + expect().nothing() + }) + + it('should do nothing if no carousel class on click on arrows', () => { + fixtureEl.innerHTML = [ + '<div id="myCarousel" class="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 class="carousel-control-prev" data-target="#myCarousel" role="button" data-slide="prev"></div>', + ' <div id="next" class="carousel-control-next" data-target="#myCarousel" role="button" data-slide="next"></div>', + '</div>' + ].join('') + + const next = fixtureEl.querySelector('#next') + + next.click() + + expect().nothing() + }) + }) +}) |
