From 3d12b541c488ea09efced2fb987fcbf384c656bb Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 2 Oct 2019 11:43:54 +0200 Subject: return to the original file structure to avoid breaking modularity --- js/tests/units/.eslintrc.json | 14 + js/tests/units/alert.spec.js | 173 +++ js/tests/units/button.spec.js | 292 ++++++ js/tests/units/carousel.spec.js | 1201 +++++++++++++++++++++ js/tests/units/collapse.spec.js | 826 +++++++++++++++ js/tests/units/dom/data.spec.js | 131 +++ js/tests/units/dom/event-handler.spec.js | 327 ++++++ js/tests/units/dom/manipulator.spec.js | 158 +++ js/tests/units/dom/selector-engine.spec.js | 115 ++ js/tests/units/dropdown.spec.js | 1564 ++++++++++++++++++++++++++++ js/tests/units/modal.spec.js | 987 ++++++++++++++++++ js/tests/units/popover.spec.js | 251 +++++ js/tests/units/scrollspy.spec.js | 653 ++++++++++++ js/tests/units/tab.spec.js | 593 +++++++++++ js/tests/units/toast.spec.js | 374 +++++++ js/tests/units/tooltip.spec.js | 1020 ++++++++++++++++++ js/tests/units/util/index.spec.js | 382 +++++++ js/tests/units/util/sanitizer.spec.js | 70 ++ 18 files changed, 9131 insertions(+) create mode 100644 js/tests/units/.eslintrc.json create mode 100644 js/tests/units/alert.spec.js create mode 100644 js/tests/units/button.spec.js create mode 100644 js/tests/units/carousel.spec.js create mode 100644 js/tests/units/collapse.spec.js create mode 100644 js/tests/units/dom/data.spec.js create mode 100644 js/tests/units/dom/event-handler.spec.js create mode 100644 js/tests/units/dom/manipulator.spec.js create mode 100644 js/tests/units/dom/selector-engine.spec.js create mode 100644 js/tests/units/dropdown.spec.js create mode 100644 js/tests/units/modal.spec.js create mode 100644 js/tests/units/popover.spec.js create mode 100644 js/tests/units/scrollspy.spec.js create mode 100644 js/tests/units/tab.spec.js create mode 100644 js/tests/units/toast.spec.js create mode 100644 js/tests/units/tooltip.spec.js create mode 100644 js/tests/units/util/index.spec.js create mode 100644 js/tests/units/util/sanitizer.spec.js (limited to 'js/tests/units') diff --git a/js/tests/units/.eslintrc.json b/js/tests/units/.eslintrc.json new file mode 100644 index 000000000..a8c1a6ae3 --- /dev/null +++ b/js/tests/units/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": [ + "../../../.eslintrc.json" + ], + "overrides": [ + { + "files": ["**/*.spec.js"], + "env": { + "jasmine": true + } + } + ] +} diff --git a/js/tests/units/alert.spec.js b/js/tests/units/alert.spec.js new file mode 100644 index 000000000..32f11b618 --- /dev/null +++ b/js/tests/units/alert.spec.js @@ -0,0 +1,173 @@ +import Alert from '../../src/alert' +import { makeArray, getTransitionDurationFromElement } from '../../src/util/index' + +/** Test helpers */ +import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' + +describe('Alert', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + it('should return version', () => { + expect(typeof Alert.VERSION).toEqual('string') + }) + + describe('data-api', () => { + it('should close an alert without instantiate it manually', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const button = document.querySelector('button') + + button.click() + expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) + }) + + it('should close an alert without instantiate it manually with the parent selector', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const button = document.querySelector('button') + + button.click() + expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) + }) + }) + + describe('close', () => { + it('should close an alert', done => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + fixtureEl.innerHTML = '
' + + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) + + alertEl.addEventListener('closed.bs.alert', () => { + expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) + expect(spy).not.toHaveBeenCalled() + done() + }) + + alert.close() + }) + + it('should close alert with fade class', done => { + fixtureEl.innerHTML = '
' + + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) + + alertEl.addEventListener('transitionend', () => { + expect().nothing() + }) + + alertEl.addEventListener('closed.bs.alert', () => { + expect(makeArray(document.querySelectorAll('.alert')).length).toEqual(0) + done() + }) + + alert.close() + }) + + it('should not remove alert if close event is prevented', done => { + fixtureEl.innerHTML = '
' + + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) + + const endTest = () => { + setTimeout(() => { + expect(alert._removeElement).not.toHaveBeenCalled() + done() + }, 10) + } + + spyOn(alert, '_removeElement') + + alertEl.addEventListener('close.bs.alert', event => { + event.preventDefault() + endTest() + }) + + alertEl.addEventListener('closed.bs.alert', () => { + endTest() + }) + + alert.close() + }) + }) + + describe('dispose', () => { + it('should dispose an alert', () => { + fixtureEl.innerHTML = '
' + + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) + + expect(Alert.getInstance(alertEl)).toBeDefined() + + alert.dispose() + + expect(Alert.getInstance(alertEl)).toBeNull() + }) + }) + + describe('jQueryInterface', () => { + it('should handle config passed and toggle existing alert', () => { + fixtureEl.innerHTML = '
' + + const alertEl = fixtureEl.querySelector('.alert') + const alert = new Alert(alertEl) + + spyOn(alert, 'close') + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [alertEl] + + jQueryMock.fn.alert.call(jQueryMock, 'close') + + expect(alert.close).toHaveBeenCalled() + }) + + it('should create new alert instance and call close', () => { + fixtureEl.innerHTML = '
' + + const alertEl = fixtureEl.querySelector('.alert') + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [alertEl] + + jQueryMock.fn.alert.call(jQueryMock, 'close') + + expect(Alert.getInstance(alertEl)).toBeDefined() + expect(fixtureEl.querySelector('.alert')).toBeNull() + }) + + it('should just create an alert instance without calling close', () => { + fixtureEl.innerHTML = '
' + + const alertEl = fixtureEl.querySelector('.alert') + + jQueryMock.fn.alert = Alert.jQueryInterface + jQueryMock.elements = [alertEl] + + jQueryMock.fn.alert.call(jQueryMock) + + expect(Alert.getInstance(alertEl)).toBeDefined() + expect(fixtureEl.querySelector('.alert')).not.toBeNull() + }) + }) +}) diff --git a/js/tests/units/button.spec.js b/js/tests/units/button.spec.js new file mode 100644 index 000000000..a3c95be1b --- /dev/null +++ b/js/tests/units/button.spec.js @@ -0,0 +1,292 @@ +import Button from '../../src/button' +import EventHandler from '../../src/dom/event-handler' + +/** Test helpers */ +import { + getFixture, + clearFixture, + createEvent, + jQueryMock +} from '../helpers/fixture' + +describe('Button', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Button.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('data-api', () => { + it('should toggle active class on click', () => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + const btnTestParent = fixtureEl.querySelector('.testParent') + + expect(btn.classList.contains('active')).toEqual(false) + + btn.click() + + expect(btn.classList.contains('active')).toEqual(true) + + btn.click() + + expect(btn.classList.contains('active')).toEqual(false) + + divTest.click() + + expect(btnTestParent.classList.contains('active')).toEqual(true) + }) + + it('should trigger input change event when toggled button has input field', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const input = fixtureEl.querySelector('input') + const label = fixtureEl.querySelector('label') + + input.addEventListener('change', () => { + expect().nothing() + done() + }) + + label.click() + }) + + it('should not trigger input change event when input already checked and button is active', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const button = fixtureEl.querySelector('button') + + spyOn(EventHandler, 'trigger') + + button.click() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should remove active when an other radio button is clicked', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const option1 = fixtureEl.querySelector('#option1') + const option2 = fixtureEl.querySelector('#option2') + + expect(option1.checked).toEqual(true) + expect(option1.parentElement.classList.contains('active')).toEqual(true) + + const clickEvent = createEvent('click') + + option2.dispatchEvent(clickEvent) + + expect(option1.checked).toEqual(false) + expect(option1.parentElement.classList.contains('active')).toEqual(false) + expect(option2.checked).toEqual(true) + expect(option2.parentElement.classList.contains('active')).toEqual(true) + }) + + it('should do nothing if the child is not an input', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const option2 = fixtureEl.querySelector('#option2') + const clickEvent = createEvent('click') + + option2.dispatchEvent(clickEvent) + + expect().nothing() + }) + + it('should add focus class on focus event', () => { + fixtureEl.innerHTML = '' + + const btn = fixtureEl.querySelector('.btn') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('focus') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(true) + }) + + it('should not add focus class', () => { + fixtureEl.innerHTML = '' + + const btn = fixtureEl.querySelector('button') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('focus') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(false) + }) + + it('should remove focus class on blur event', () => { + fixtureEl.innerHTML = '' + + const btn = fixtureEl.querySelector('.btn') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('blur') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(false) + }) + + it('should not remove focus class on blur event', () => { + fixtureEl.innerHTML = '' + + const btn = fixtureEl.querySelector('button') + const input = fixtureEl.querySelector('input') + + const focusEvent = createEvent('blur') + input.dispatchEvent(focusEvent) + + expect(btn.classList.contains('focus')).toEqual(true) + }) + }) + + describe('toggle', () => { + it('should toggle aria-pressed', () => { + fixtureEl.innerHTML = '' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + expect(btnEl.getAttribute('aria-pressed')).toEqual('false') + expect(btnEl.classList.contains('active')).toEqual(false) + + button.toggle() + + expect(btnEl.getAttribute('aria-pressed')).toEqual('true') + expect(btnEl.classList.contains('active')).toEqual(true) + }) + + it('should handle disabled attribute on non-button elements', () => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const btnGroupEl = fixtureEl.querySelector('.btn-group') + const btnDanger = fixtureEl.querySelector('.btn-danger') + const input = fixtureEl.querySelector('input') + + const button = new Button(btnGroupEl) + + button.toggle() + + expect(btnDanger.hasAttribute('disabled')).toEqual(true) + expect(input.checked).toEqual(false) + }) + }) + + describe('dispose', () => { + it('should dispose a button', () => { + fixtureEl.innerHTML = '' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + expect(Button.getInstance(btnEl)).toBeDefined() + + button.dispose() + + expect(Button.getInstance(btnEl)).toBeNull() + }) + }) + + describe('jQueryInterface', () => { + it('should handle config passed and toggle existing button', () => { + fixtureEl.innerHTML = '' + + const btnEl = fixtureEl.querySelector('.btn') + const button = new Button(btnEl) + + spyOn(button, 'toggle') + + jQueryMock.fn.button = Button.jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock, 'toggle') + + expect(button.toggle).toHaveBeenCalled() + }) + + it('should create new button instance and call toggle', () => { + fixtureEl.innerHTML = '' + + const btnEl = fixtureEl.querySelector('.btn') + + jQueryMock.fn.button = Button.jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock, 'toggle') + + expect(Button.getInstance(btnEl)).toBeDefined() + expect(btnEl.classList.contains('active')).toEqual(true) + }) + + it('should just create a button instance without calling toggle', () => { + fixtureEl.innerHTML = '' + + const btnEl = fixtureEl.querySelector('.btn') + + jQueryMock.fn.button = Button.jQueryInterface + jQueryMock.elements = [btnEl] + + jQueryMock.fn.button.call(jQueryMock) + + expect(Button.getInstance(btnEl)).toBeDefined() + expect(btnEl.classList.contains('active')).toEqual(false) + }) + }) +}) diff --git a/js/tests/units/carousel.spec.js b/js/tests/units/carousel.spec.js new file mode 100644 index 000000000..a163f9ae4 --- /dev/null +++ b/js/tests/units/carousel.spec.js @@ -0,0 +1,1201 @@ +import Carousel from '../../src/carousel' +import EventHandler from '../../src/dom/event-handler' + +/** Test helpers */ +import { getFixture, clearFixture, createEvent, jQueryMock } from '../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 = [ + '
', + ' ', + '
' + ].join('') + + const carouselEl = fixtureEl.querySelector('.carousel') + 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/units/collapse.spec.js b/js/tests/units/collapse.spec.js new file mode 100644 index 000000000..3122ae6f4 --- /dev/null +++ b/js/tests/units/collapse.spec.js @@ -0,0 +1,826 @@ +import Collapse from '../../src/collapse' +import EventHandler from '../../src/dom/event-handler' +import { makeArray } from '../../src/util/index' + +/** Test helpers */ +import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' + +describe('Collapse', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Collapse.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(Collapse.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('constructor', () => { + it('should allow jquery object in parent config', () => { + fixtureEl.innerHTML = [ + '
', + '
', + ' Toggle item', + '
Lorem ipsum
', + '
', + '
' + ].join('') + + const collapseEl = fixtureEl.querySelector('div.collapse') + const myCollapseEl = fixtureEl.querySelector('.my-collapse') + const fakejQueryObject = { + 0: myCollapseEl + } + const collapse = new Collapse(collapseEl, { + parent: fakejQueryObject + }) + + expect(collapse._config.parent).toEqual(fakejQueryObject) + expect(collapse._getParent()).toEqual(myCollapseEl) + }) + + it('should allow non jquery object in parent config', () => { + fixtureEl.innerHTML = [ + '
', + '
', + ' Toggle item', + '
Lorem ipsum
', + '
', + '
' + ].join('') + + const collapseEl = fixtureEl.querySelector('div.collapse') + const myCollapseEl = fixtureEl.querySelector('.my-collapse') + const collapse = new Collapse(collapseEl, { + parent: myCollapseEl + }) + + expect(collapse._config.parent).toEqual(myCollapseEl) + }) + + it('should allow string selector in parent config', () => { + fixtureEl.innerHTML = [ + '
', + '
', + ' Toggle item', + '
Lorem ipsum
', + '
', + '
' + ].join('') + + const collapseEl = fixtureEl.querySelector('div.collapse') + const myCollapseEl = fixtureEl.querySelector('.my-collapse') + const collapse = new Collapse(collapseEl, { + parent: 'div.my-collapse' + }) + + expect(collapse._config.parent).toEqual('div.my-collapse') + expect(collapse._getParent()).toEqual(myCollapseEl) + }) + }) + + describe('toggle', () => { + it('should call show method if show class is not present', () => { + fixtureEl.innerHTML = '
' + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl) + + spyOn(collapse, 'show') + + collapse.toggle() + + expect(collapse.show).toHaveBeenCalled() + }) + + it('should call hide method if show class is present', () => { + fixtureEl.innerHTML = '
' + + const collapseEl = fixtureEl.querySelector('.show') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + spyOn(collapse, 'hide') + + collapse.toggle() + + expect(collapse.hide).toHaveBeenCalled() + }) + + it('should find collapse children if they have collapse class too not only data-parent', done => { + fixtureEl.innerHTML = [ + '
', + '
', + ' Toggle item 1', + '
Lorem ipsum 1
', + '
', + '
', + ' Toggle item 2', + '
Lorem ipsum 2
', + '
', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.my-collapse') + const collapseEl1 = fixtureEl.querySelector('#collapse1') + const collapseEl2 = fixtureEl.querySelector('#collapse2') + + const collapseList = makeArray(fixtureEl.querySelectorAll('.collapse')) + .map(el => new Collapse(el, { + parent, + toggle: false + })) + + collapseEl2.addEventListener('shown.bs.collapse', () => { + expect(collapseEl2.classList.contains('show')).toEqual(true) + expect(collapseEl1.classList.contains('show')).toEqual(false) + done() + }) + + collapseList[1].toggle() + }) + }) + + describe('show', () => { + it('should do nothing if is transitioning', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + collapse._isTransitioning = true + collapse.show() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should do nothing if already shown', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + collapse.show() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should show a collapsed element', done => { + fixtureEl.innerHTML = '
' + + 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() + }) + + collapse.show() + }) + + it('should show a collapsed element on width', done => { + fixtureEl.innerHTML = '
' + + 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() + }) + + collapse.show() + }) + + it('should collapse only the first collapse', done => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + '
', + '
' + ].join('') + + const el1 = fixtureEl.querySelector('#collapse1') + const el2 = fixtureEl.querySelector('#collapse2') + const collapse = new Collapse(el1, { + toggle: false + }) + + el1.addEventListener('shown.bs.collapse', () => { + expect(el1.classList.contains('show')).toEqual(true) + expect(el2.classList.contains('show')).toEqual(true) + done() + }) + + collapse.show() + }) + + it('should not fire shown when show is prevented', done => { + fixtureEl.innerHTML = '
' + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + done() + }, 10) + } + + collapseEl.addEventListener('show.bs.collapse', e => { + e.preventDefault() + expectEnd() + }) + + collapseEl.addEventListener('shown.bs.collapse', () => { + throw new Error('should not fire shown event') + }) + + collapse.show() + }) + }) + + describe('hide', () => { + it('should do nothing if is transitioning', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + collapse._isTransitioning = true + collapse.hide() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should do nothing if already shown', () => { + fixtureEl.innerHTML = '
' + + spyOn(EventHandler, 'trigger') + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + collapse.hide() + + expect(EventHandler.trigger).not.toHaveBeenCalled() + }) + + it('should hide a collapsed element', done => { + fixtureEl.innerHTML = '
' + + 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() + }) + + collapse.hide() + }) + + it('should not fire hidden when hide is prevented', done => { + fixtureEl.innerHTML = '
' + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + const expectEnd = () => { + setTimeout(() => { + expect().nothing() + done() + }, 10) + } + + collapseEl.addEventListener('hide.bs.collapse', e => { + e.preventDefault() + expectEnd() + }) + + collapseEl.addEventListener('hidden.bs.collapse', () => { + throw new Error('should not fire hidden event') + }) + + collapse.hide() + }) + }) + + describe('dispose', () => { + it('should destroy a collapse', () => { + fixtureEl.innerHTML = '
' + + const collapseEl = fixtureEl.querySelector('div') + const collapse = new Collapse(collapseEl, { + toggle: false + }) + + expect(Collapse.getInstance(collapseEl)).toEqual(collapse) + + collapse.dispose() + + expect(Collapse.getInstance(collapseEl)).toEqual(null) + }) + }) + + describe('data-api', () => { + it('should show multiple collapsed elements', done => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].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.classList.contains('collapsed')).toEqual(false) + expect(collapse1.classList.contains('show')).toEqual(true) + expect(collapse1.classList.contains('show')).toEqual(true) + done() + }) + + trigger.click() + }) + + it('should hide multiple collapsed elements', done => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].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.classList.contains('collapsed')).toEqual(true) + expect(collapse1.classList.contains('show')).toEqual(false) + expect(collapse1.classList.contains('show')).toEqual(false) + done() + }) + + trigger.click() + }) + + it('should remove "collapsed" class from target when collapse is shown', done => { + fixtureEl.innerHTML = [ + '', + '', + '
' + ].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.classList.contains('collapsed')).toEqual(true) + expect(link2.classList.contains('collapsed')).toEqual(true) + done() + }) + + link1.click() + }) + + it('should allow accordion to use children other than card', done => { + fixtureEl.innerHTML = [ + '
', + '
', + ' ', + '
', + '
', + '
', + ' ', + '
', + '
', + '
' + ].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) + + collapseTwo.addEventListener('shown.bs.collapse', () => { + expect(collapseOne.classList.contains('show')).toEqual(false) + expect(collapseTwo.classList.contains('show')).toEqual(true) + done() + }) + + triggerTwo.click() + }) + + trigger.click() + }) + + it('should not prevent event for input', done => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + 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() + }) + + target.click() + }) + + it('should allow accordion to contain nested elements', done => { + fixtureEl.innerHTML = [ + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
', + '
' + ].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') + + expect(collapseTwoEl.classList.contains('show')).toEqual(true) + expect(triggerTwoEl.classList.contains('collapsed')).toEqual(false) + expect(triggerTwoEl.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + triggerTwoEl.click() + }) + + triggerEl.click() + }) + + it('should allow accordion to target multiple elements', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
', + '
', + '
', + '
', + '
' + ].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 + } + }) + + collapseOneTwo.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.one) { + firstTest() + } else { + collapsedElements.one = true + } + }) + + collapseTwoOne.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.two) { + secondTest() + } else { + collapsedElements.two = true + } + }) + + collapseTwoTwo.addEventListener('shown.bs.collapse', () => { + if (collapsedElements.two) { + secondTest() + } else { + collapsedElements.two = true + } + }) + + trigger.click() + }) + + it('should collapse accordion children but not nested accordion children', done => { + fixtureEl.innerHTML = [ + '
', + '
', + ' ', + '
', + '
', + '
', + ' ', + '
', + '
', + '
', + '
', + '
', + '
', + ' ', + '
', + '
', + '
' + ].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.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) + } + + function handlerNestedCollapseOne() { + expect(collapseOne.classList.contains('show')).toEqual(true) + expect(collapseTwo.classList.contains('show')).toEqual(false) + expect(nestedCollapseOne.classList.contains('show')).toEqual(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() + }) + + triggerTwo.click() + nestedCollapseOne.removeEventListener('shown.bs.collapse', handlerNestedCollapseOne) + } + + collapseOne.addEventListener('shown.bs.collapse', handlerCollapseOne) + trigger.click() + }) + + it('should add "collapsed" class and set aria-expanded to triggers only when all the targeted collapse are hidden', done => { + fixtureEl.innerHTML = [ + '', + '', + '', + '
', + '
' + ].join('') + + const trigger1 = fixtureEl.querySelector('#trigger1') + const trigger2 = fixtureEl.querySelector('#trigger2') + const trigger3 = fixtureEl.querySelector('#trigger3') + const target1 = fixtureEl.querySelector('#test1') + const target2 = fixtureEl.querySelector('#test2') + + const target2Shown = () => { + expect(trigger1.classList.contains('collapsed')).toEqual(false) + expect(trigger1.getAttribute('aria-expanded')).toEqual('true') + + expect(trigger2.classList.contains('collapsed')).toEqual(false) + expect(trigger2.getAttribute('aria-expanded')).toEqual('true') + + expect(trigger3.classList.contains('collapsed')).toEqual(false) + expect(trigger3.getAttribute('aria-expanded')).toEqual('true') + + target2.addEventListener('hidden.bs.collapse', () => { + expect(trigger1.classList.contains('collapsed')).toEqual(false) + expect(trigger1.getAttribute('aria-expanded')).toEqual('true') + + expect(trigger2.classList.contains('collapsed')).toEqual(true) + expect(trigger2.getAttribute('aria-expanded')).toEqual('false') + + expect(trigger3.classList.contains('collapsed')).toEqual(false) + 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') + + expect(trigger2.classList.contains('collapsed')).toEqual(true) + expect(trigger2.getAttribute('aria-expanded')).toEqual('false') + + expect(trigger3.classList.contains('collapsed')).toEqual(true) + expect(trigger3.getAttribute('aria-expanded')).toEqual('false') + done() + }) + + trigger1.click() + }) + + trigger2.click() + } + + target2.addEventListener('shown.bs.collapse', target2Shown) + trigger3.click() + }) + }) + + describe('jQueryInterface', () => { + it('should create a collapse', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.collapse = Collapse.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.collapse.call(jQueryMock) + + expect(Collapse.getInstance(div)).toBeDefined() + }) + + it('should not re create a collapse', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const collapse = new Collapse(div) + + jQueryMock.fn.collapse = Collapse.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.collapse.call(jQueryMock) + + expect(Collapse.getInstance(div)).toEqual(collapse) + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.collapse = Collapse.jQueryInterface + jQueryMock.elements = [div] + + try { + jQueryMock.fn.collapse.call(jQueryMock, action) + } catch (error) { + expect(error.message).toEqual(`No method named "${action}"`) + } + }) + }) + + describe('getInstance', () => { + it('should return collapse instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const collapse = new Collapse(div) + + expect(Collapse.getInstance(div)).toEqual(collapse) + }) + + it('should return null when there is no collapse instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Collapse.getInstance(div)).toEqual(null) + }) + }) +}) diff --git a/js/tests/units/dom/data.spec.js b/js/tests/units/dom/data.spec.js new file mode 100644 index 000000000..ab3240b9b --- /dev/null +++ b/js/tests/units/dom/data.spec.js @@ -0,0 +1,131 @@ +import Data from '../../../src/dom/data' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../helpers/fixture' + +describe('Data', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('setData', () => { + it('should set data in an element by adding a key attribute', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + expect(div.key).toBeDefined() + }) + + it('should change data if something is already stored', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + + data.test = 'bsData2' + Data.setData(div, 'test', data) + + expect(div.key).toBeDefined() + }) + }) + + describe('getData', () => { + it('should return stored data', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + expect(Data.getData(div, 'test')).toEqual(data) + }) + + it('should return null on undefined element', () => { + expect(Data.getData(null)).toEqual(null) + expect(Data.getData(undefined)).toEqual(null) + }) + + it('should return null when an element have nothing stored', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Data.getData(div, 'test')).toEqual(null) + }) + + it('should return null when an element have nothing stored with the provided key', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + + expect(Data.getData(div, 'test2')).toEqual(null) + }) + }) + + describe('removeData', () => { + it('should do nothing when an element have nothing stored', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Data.removeData(div, 'test') + expect().nothing() + }) + + it('should should do nothing if it\'s not a valid key provided', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + + expect(div.key).toBeDefined() + + Data.removeData(div, 'test2') + + expect(div.key).toBeDefined() + }) + + it('should remove data if something is stored', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const data = { + test: 'bsData' + } + + Data.setData(div, 'test', data) + + expect(div.key).toBeDefined() + + Data.removeData(div, 'test') + + expect(div.key).toBeUndefined() + }) + }) +}) diff --git a/js/tests/units/dom/event-handler.spec.js b/js/tests/units/dom/event-handler.spec.js new file mode 100644 index 000000000..5551ddaa3 --- /dev/null +++ b/js/tests/units/dom/event-handler.spec.js @@ -0,0 +1,327 @@ +import EventHandler from '../../../src/dom/event-handler' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../helpers/fixture' + +describe('EventHandler', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('on', () => { + it('should not add event listener if the event is not a string', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + EventHandler.on(div, null, () => {}) + EventHandler.on(null, 'click', () => {}) + + expect().nothing() + }) + + it('should add event listener', done => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + EventHandler.on(div, 'click', () => { + expect().nothing() + done() + }) + + div.click() + }) + + it('should add namespaced event listener', done => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + EventHandler.on(div, 'bs.namespace', () => { + expect().nothing() + done() + }) + + EventHandler.trigger(div, 'bs.namespace') + }) + + it('should add native namespaced event listener', done => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + EventHandler.on(div, 'click.namespace', () => { + expect().nothing() + done() + }) + + EventHandler.trigger(div, 'click') + }) + + it('should handle event delegation', done => { + EventHandler.on(document, 'click', '.test', () => { + expect().nothing() + done() + }) + + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + div.click() + }) + }) + + describe('one', () => { + it('should call listener just one', done => { + fixtureEl.innerHTML = '
' + + let called = 0 + const div = fixtureEl.querySelector('div') + const obj = { + oneListener() { + called++ + } + } + + EventHandler.one(div, 'bootstrap', obj.oneListener) + + EventHandler.trigger(div, 'bootstrap') + EventHandler.trigger(div, 'bootstrap') + + setTimeout(() => { + expect(called).toEqual(1) + done() + }, 20) + }) + }) + + describe('off', () => { + it('should not remove a listener', () => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + EventHandler.off(div, null, () => {}) + EventHandler.off(null, 'click', () => {}) + expect().nothing() + }) + + it('should remove a listener', done => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + let called = 0 + const handler = () => { + called++ + } + + EventHandler.on(div, 'foobar', handler) + EventHandler.trigger(div, 'foobar') + + EventHandler.off(div, 'foobar', handler) + EventHandler.trigger(div, 'foobar') + + setTimeout(() => { + expect(called).toEqual(1) + done() + }, 20) + }) + + it('should remove all the events', done => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + let called = 0 + + EventHandler.on(div, 'foobar', () => { + called++ + }) + EventHandler.on(div, 'foobar', () => { + called++ + }) + EventHandler.trigger(div, 'foobar') + + EventHandler.off(div, 'foobar') + EventHandler.trigger(div, 'foobar') + + setTimeout(() => { + expect(called).toEqual(2) + done() + }, 20) + }) + + it('should remove all the namespaced listeners if namespace is passed', done => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + let called = 0 + + 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) + done() + }, 20) + }) + + it('should remove the namespaced listeners', done => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + let calledCallback1 = 0 + let calledCallback2 = 0 + + 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, 'foofoo.namespace') + + setTimeout(() => { + expect(calledCallback1).toEqual(1) + expect(calledCallback2).toEqual(1) + done() + }, 20) + }) + + it('should remove the all the namespaced listeners for native events', done => { + fixtureEl.innerHTML = '
' + const div = fixtureEl.querySelector('div') + + let called = 0 + + EventHandler.on(div, 'click.namespace', () => { + called++ + }) + EventHandler.on(div, 'click.namespace2', () => { + called++ + }) + + EventHandler.trigger(div, 'click') + EventHandler.off(div, 'click') + EventHandler.trigger(div, 'click') + + setTimeout(() => { + expect(called).toEqual(2) + done() + }, 20) + }) + + it('should remove the specified namespaced listeners for native events', done => { + fixtureEl.innerHTML = '
' + 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) + done() + }, 20) + }) + + it('should remove a listener registered by .one', done => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const handler = () => { + throw new Error('called') + } + + EventHandler.one(div, 'foobar', handler) + EventHandler.off(div, 'foobar', handler) + + EventHandler.trigger(div, 'foobar') + setTimeout(() => { + expect().nothing() + done() + }, 20) + }) + + it('should remove the correct delegated event listener', () => { + const element = document.createElement('div') + const subelement = document.createElement('span') + element.appendChild(subelement) + + const anchor = document.createElement('a') + element.appendChild(anchor) + + let i = 0 + const handler = () => { + i++ + } + + EventHandler.on(element, 'click', 'a', handler) + EventHandler.on(element, 'click', 'span', handler) + + fixtureEl.appendChild(element) + + EventHandler.trigger(anchor, 'click') + EventHandler.trigger(subelement, 'click') + + // first listeners called + expect(i === 2).toEqual(true) + + EventHandler.off(element, 'click', 'span', handler) + EventHandler.trigger(subelement, 'click') + + // removed listener not called + expect(i === 2).toEqual(true) + + EventHandler.trigger(anchor, 'click') + + // not removed listener called + expect(i === 3).toEqual(true) + + EventHandler.on(element, 'click', 'span', handler) + EventHandler.trigger(anchor, 'click') + EventHandler.trigger(subelement, 'click') + + // listener re-registered + expect(i === 5).toEqual(true) + + EventHandler.off(element, 'click', 'span') + EventHandler.trigger(subelement, 'click') + + // listener removed again + expect(i === 5).toEqual(true) + }) + }) +}) diff --git a/js/tests/units/dom/manipulator.spec.js b/js/tests/units/dom/manipulator.spec.js new file mode 100644 index 000000000..986f69298 --- /dev/null +++ b/js/tests/units/dom/manipulator.spec.js @@ -0,0 +1,158 @@ +import Manipulator from '../../../src/dom/manipulator' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../helpers/fixture' + +describe('Manipulator', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('setDataAttribute', () => { + it('should set data attribute', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.setDataAttribute(div, 'key', 'value') + expect(div.getAttribute('data-key')).toEqual('value') + }) + + it('should set data attribute in lower case', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.setDataAttribute(div, 'tEsT', 'value') + expect(div.getAttribute('data-test')).toEqual('value') + }) + }) + + describe('removeDataAttribute', () => { + it('should remove data attribute', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.removeDataAttribute(div, 'key') + expect(div.getAttribute('data-key')).toBeNull() + }) + + it('should remove data attribute in lower case', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.removeDataAttribute(div, 'tEStKeY') + expect(div.getAttribute('data-testkey')).toBeNull() + }) + }) + + describe('getDataAttributes', () => { + it('should return empty object for null', () => { + expect(Manipulator.getDataAttributes(null), {}) + expect().nothing() + }) + + it('should get all data attributes', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Manipulator.getDataAttributes(div)).toEqual({ + test: 'js', + test2: 'js2' + }) + }) + }) + + describe('getDataAttribute', () => { + it('should get data attribute', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Manipulator.getDataAttribute(div, 'test')).toBeNull() + }) + + it('should get data attribute in lower case', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Manipulator.getDataAttribute(div, 'tEsT')).toEqual('value') + }) + + it('should normalize data', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Manipulator.getDataAttribute(div, 'test')).toEqual(false) + + div.setAttribute('data-test', 'true') + expect(Manipulator.getDataAttribute(div, 'test')).toEqual(true) + + div.setAttribute('data-test', '1') + expect(Manipulator.getDataAttribute(div, 'test')).toEqual(1) + }) + }) + + describe('offset', () => { + it('should return object with two properties top and left, both numbers', () => { + fixtureEl.innerHTML = '
' + + 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)) + }) + }) + + describe('position', () => { + it('should return object with two properties top and left, both numbers', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const position = Manipulator.position(div) + + expect(position).toBeDefined() + expect(position.top).toEqual(jasmine.any(Number)) + expect(position.left).toEqual(jasmine.any(Number)) + }) + }) + + describe('toggleClass', () => { + it('should not error out if element is null or undefined', () => { + Manipulator.toggleClass(null, 'test') + Manipulator.toggleClass(undefined, 'test') + expect().nothing() + }) + + it('should add class if it is missing', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.toggleClass(div, 'test') + expect(div.classList.contains('test')).toEqual(true) + }) + + it('should remove class if it is set', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + Manipulator.toggleClass(div, 'test') + expect(div.classList.contains('test')).toEqual(false) + }) + }) +}) diff --git a/js/tests/units/dom/selector-engine.spec.js b/js/tests/units/dom/selector-engine.spec.js new file mode 100644 index 000000000..e13438e6f --- /dev/null +++ b/js/tests/units/dom/selector-engine.spec.js @@ -0,0 +1,115 @@ +import SelectorEngine from '../../../src/dom/selector-engine' +import { makeArray } from '../../../src/util/index' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../helpers/fixture' + +describe('SelectorEngine', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('matches', () => { + it('should return matched elements', () => { + fixtureEl.innerHTML = '
' + + expect(SelectorEngine.matches(fixtureEl, 'div')).toEqual(true) + }) + }) + + describe('find', () => { + it('should find elements', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(makeArray(SelectorEngine.find('div', fixtureEl))).toEqual([div]) + }) + + it('should find elements globaly', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('#test') + + expect(makeArray(SelectorEngine.find('#test'))).toEqual([div]) + }) + + it('should handle :scope selectors', () => { + fixtureEl.innerHTML = `
` + + const listEl = fixtureEl.querySelector('ul') + const aActive = fixtureEl.querySelector('.active') + + expect(makeArray(SelectorEngine.find(':scope > li > .active', listEl))).toEqual([aActive]) + }) + }) + + describe('findOne', () => { + it('should return one element', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('#test') + + expect(SelectorEngine.findOne('#test')).toEqual(div) + }) + }) + + describe('children', () => { + it('should find children', () => { + fixtureEl.innerHTML = `
    +
  • +
  • +
  • +
` + + const list = fixtureEl.querySelector('ul') + const liList = makeArray(fixtureEl.querySelectorAll('li')) + const result = makeArray(SelectorEngine.children(list, 'li')) + + expect(result).toEqual(liList) + }) + }) + + describe('parents', () => { + it('should return parents', () => { + expect(SelectorEngine.parents(fixtureEl, 'body').length).toEqual(1) + }) + }) + + describe('prev', () => { + it('should return previous element', () => { + fixtureEl.innerHTML = '
' + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + + expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) + }) + + it('should return previous element with an extra element between', () => { + fixtureEl.innerHTML = [ + '
', + '', + '' + ].join('') + + const btn = fixtureEl.querySelector('.btn') + const divTest = fixtureEl.querySelector('.test') + + expect(SelectorEngine.prev(btn, '.test')).toEqual([divTest]) + }) + }) +}) + diff --git a/js/tests/units/dropdown.spec.js b/js/tests/units/dropdown.spec.js new file mode 100644 index 000000000..0046cf680 --- /dev/null +++ b/js/tests/units/dropdown.spec.js @@ -0,0 +1,1564 @@ +import Popper from 'popper.js' + +import Dropdown from '../../src/dropdown' +import EventHandler from '../../src/dom/event-handler' + +/** Test helpers */ +import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' + +describe('Dropdown', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Dropdown.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(Dropdown.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('DefaultType', () => { + it('should return plugin default type config', () => { + expect(Dropdown.DefaultType).toEqual(jasmine.any(Object)) + }) + }) + + describe('constructor', () => { + it('should create offset modifier correctly when offset option is a function', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const getOffset = offsets => offsets + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + offset: getOffset + }) + + const offset = dropdown._getOffset() + + expect(offset.offset).toBeUndefined() + expect(typeof offset.fn).toEqual('function') + }) + + it('should create offset modifier correctly when offset option is not a function', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const myOffset = 7 + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + offset: myOffset + }) + + const offset = dropdown._getOffset() + + expect(offset.offset).toEqual(myOffset) + expect(offset.fn).toBeUndefined() + }) + + it('should add a listener on trigger which do not have data-toggle="dropdown"', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('.btn') + const dropdown = new Dropdown(btnDropdown) + + spyOn(dropdown, 'toggle') + + btnDropdown.click() + + expect(dropdown.toggle).toHaveBeenCalled() + }) + + it('should allow to pass config to popper.js with `popperConfig`', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown, { + popperConfig: { + placement: 'left' + } + }) + + const popperConfig = dropdown._getPopperConfig() + + expect(popperConfig.placement).toEqual('left') + }) + }) + + describe('toggle', () => { + it('should toggle a dropdown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should destroy old popper references on toggle', done => { + fixtureEl.innerHTML = [ + '', + '' + ].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) + const dropdown2 = new Dropdown(btnDropdown2) + + firstDropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(firstDropdownEl.classList.contains('show')).toEqual(true) + spyOn(dropdown1._popper, 'destroy') + dropdown2.toggle() + }) + + secondDropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdown1._popper.destroy).toHaveBeenCalled() + done() + }) + + dropdown1.toggle() + }) + + it('should toggle a dropdown and add/remove event listener on mobile', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const defaultValueOnTouchStart = document.documentElement.ontouchstart + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + document.documentElement.ontouchstart = () => {} + spyOn(EventHandler, 'on') + spyOn(EventHandler, 'off') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(EventHandler.on).toHaveBeenCalled() + + dropdown.toggle() + }) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(false) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(EventHandler.off).toHaveBeenCalled() + + document.documentElement.ontouchstart = defaultValueOnTouchStart + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropdown at the right', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropup', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) + + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(dropupEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropup at the right', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropupEl = fixtureEl.querySelector('.dropup') + const dropdown = new Dropdown(btnDropdown) + + dropupEl.addEventListener('shown.bs.dropdown', () => { + expect(dropupEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropright', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const droprightEl = fixtureEl.querySelector('.dropright') + const dropdown = new Dropdown(btnDropdown) + + droprightEl.addEventListener('shown.bs.dropdown', () => { + expect(droprightEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropleft', done => { + fixtureEl.innerHTML = [ + '
', + ' ', + ' ', + '
' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropleftEl = fixtureEl.querySelector('.dropleft') + const dropdown = new Dropdown(btnDropdown) + + dropleftEl.addEventListener('shown.bs.dropdown', () => { + expect(dropleftEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropdown with parent reference', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown, { + reference: 'parent' + }) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropdown with a dom node reference', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown, { + reference: fixtureEl + }) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should toggle a dropdown with a jquery object reference', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown, { + reference: { 0: fixtureEl, jquery: 'jQuery' } + }) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + done() + }) + + dropdown.toggle() + }) + + it('should not toggle a dropdown if the element is disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.toggle() + + setTimeout(() => { + expect().nothing() + done() + }) + }) + + it('should not toggle a dropdown if the element contains .disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.toggle() + + setTimeout(() => { + expect().nothing() + done() + }) + }) + + it('should not toggle a dropdown if the menu is shown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.toggle() + + setTimeout(() => { + expect().nothing() + done() + }) + }) + + it('should not toggle a dropdown if show event is prevented', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('show.bs.dropdown', e => { + e.preventDefault() + }) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.toggle() + + setTimeout(() => { + expect().nothing() + done() + }) + }) + }) + + describe('show', () => { + it('should show a dropdown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + done() + }) + + dropdown.show() + }) + + it('should not show a dropdown if the element is disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.show() + + setTimeout(() => { + expect().nothing() + done() + }, 10) + }) + + it('should not show a dropdown if the element contains .disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.show() + + setTimeout(() => { + expect().nothing() + done() + }, 10) + }) + + it('should not show a dropdown if the menu is shown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.show() + + setTimeout(() => { + expect().nothing() + done() + }, 10) + }) + + it('should not show a dropdown if show event is prevented', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('show.bs.dropdown', e => { + e.preventDefault() + }) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + throw new Error('should not throw shown.bs.dropdown event') + }) + + dropdown.show() + + setTimeout(() => { + expect().nothing() + done() + }, 10) + }) + }) + + describe('hide', () => { + it('should hide a dropdown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownMenu.classList.contains('show')).toEqual(false) + done() + }) + + dropdown.hide() + }) + + it('should hide a dropdown and destroy popper', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + spyOn(dropdown._popper, 'destroy') + dropdown.hide() + }) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + expect(dropdown._popper.destroy).toHaveBeenCalled() + done() + }) + + dropdown.show() + }) + + it('should not hide a dropdown if the element is disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) + + dropdown.hide() + + setTimeout(() => { + expect(dropdownMenu.classList.contains('show')).toEqual(true) + done() + }, 10) + }) + + it('should not hide a dropdown if the element contains .disabled', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) + + dropdown.hide() + + setTimeout(() => { + expect(dropdownMenu.classList.contains('show')).toEqual(true) + done() + }, 10) + }) + + it('should not hide a dropdown if the menu is not shown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) + + dropdown.hide() + + setTimeout(() => { + expect().nothing() + done() + }, 10) + }) + + it('should not hide a dropdown if hide event is prevented', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + const dropdown = new Dropdown(btnDropdown) + + dropdownEl.addEventListener('hide.bs.dropdown', e => { + e.preventDefault() + }) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + throw new Error('should not throw hidden.bs.dropdown event') + }) + + dropdown.hide() + + setTimeout(() => { + expect(dropdownMenu.classList.contains('show')).toEqual(true) + done() + }) + }) + }) + + describe('dispose', () => { + it('should dispose dropdown', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + expect(dropdown._popper).toBeNull() + expect(dropdown._menu).toBeDefined() + expect(dropdown._element).toBeDefined() + + dropdown.dispose() + + expect(dropdown._menu).toBeNull() + expect(dropdown._element).toBeNull() + }) + + it('should dispose dropdown with popper.js', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + dropdown.toggle() + + expect(dropdown._popper).toBeDefined() + expect(dropdown._menu).toBeDefined() + expect(dropdown._element).toBeDefined() + + spyOn(Popper.prototype, 'destroy') + + dropdown.dispose() + + expect(dropdown._popper).toBeNull() + expect(dropdown._menu).toBeNull() + expect(dropdown._element).toBeNull() + expect(Popper.prototype.destroy).toHaveBeenCalled() + }) + }) + + describe('update', () => { + it('should call popper.js and detect navbar on update', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + dropdown.toggle() + + expect(dropdown._popper).toBeDefined() + + spyOn(dropdown._popper, 'scheduleUpdate') + spyOn(dropdown, '_detectNavbar') + + dropdown.update() + + expect(dropdown._popper.scheduleUpdate).toHaveBeenCalled() + expect(dropdown._detectNavbar).toHaveBeenCalled() + }) + + it('should just detect navbar on update', () => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(btnDropdown) + + spyOn(dropdown, '_detectNavbar') + + dropdown.update() + + expect(dropdown._popper).toBeNull() + expect(dropdown._detectNavbar).toHaveBeenCalled() + }) + }) + + describe('data-api', () => { + it('should not add class position-static to dropdown if boundary not set', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('position-static')).toEqual(false) + done() + }) + + btnDropdown.click() + }) + + it('should add class position-static to dropdown if boundary not scrollParent', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('position-static')).toEqual(true) + done() + }) + + btnDropdown.click() + }) + + it('should show and hide a dropdown', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + let showEventTriggered = false + let hideEventTriggered = false + + dropdownEl.addEventListener('show.bs.dropdown', () => { + showEventTriggered = true + }) + + dropdownEl.addEventListener('shown.bs.dropdown', e => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + expect(showEventTriggered).toEqual(true) + expect(e.relatedTarget).toEqual(btnDropdown) + document.body.click() + }) + + dropdownEl.addEventListener('hide.bs.dropdown', () => { + hideEventTriggered = true + }) + + dropdownEl.addEventListener('hidden.bs.dropdown', e => { + expect(dropdownEl.classList.contains('show')).toEqual(false) + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false') + expect(hideEventTriggered).toEqual(true) + expect(e.relatedTarget).toEqual(btnDropdown) + done() + }) + + btnDropdown.click() + }) + + it('should not use popper.js in navbar', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by popper.js') + done() + }) + + btnDropdown.click() + }) + + it('should not use popper.js if display set to static', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + // popper.js add this attribute when we use it + expect(dropdownMenu.getAttribute('x-placement')).toEqual(null) + done() + }) + + btnDropdown.click() + }) + + it('should remove "show" class if tabbing outside of menu', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const btnDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdownEl = fixtureEl.querySelector('.dropdown') + + dropdownEl.addEventListener('shown.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(true) + + const keyUp = createEvent('keyup') + + keyUp.which = 9 // Tab + document.dispatchEvent(keyUp) + }) + + dropdownEl.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownEl.classList.contains('show')).toEqual(false) + done() + }) + + btnDropdown.click() + }) + + it('should remove "show" class if body is clicked, with multiple dropdowns', done => { + fixtureEl.innerHTML = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const triggerDropdownList = fixtureEl.querySelectorAll('[data-toggle="dropdown"]') + + expect(triggerDropdownList.length).toEqual(2) + + const first = triggerDropdownList[0] + const last = triggerDropdownList[1] + const dropdownTestMenu = first.parentNode + const btnGroup = last.parentNode + + dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { + expect(dropdownTestMenu.classList.contains('show')).toEqual(true) + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) + document.body.click() + }) + + dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) + last.click() + }) + + btnGroup.addEventListener('shown.bs.dropdown', () => { + expect(btnGroup.classList.contains('show')).toEqual(true) + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1) + document.body.click() + }) + + btnGroup.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0) + done() + }) + + first.click() + }) + + it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => { + fixtureEl.innerHTML = [ + '', + '
', + ' ', + ' ', + ' ', + '
' + ].join('') + + const triggerDropdownList = fixtureEl.querySelectorAll('[data-toggle="dropdown"]') + + expect(triggerDropdownList.length).toEqual(2) + + const first = triggerDropdownList[0] + const last = triggerDropdownList[1] + const dropdownTestMenu = first.parentNode + const btnGroup = last.parentNode + + dropdownTestMenu.addEventListener('shown.bs.dropdown', () => { + expect(dropdownTestMenu.classList.contains('show')).toEqual(true, '"show" class added on click') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + + const keyUp = createEvent('keyup') + keyUp.which = 9 // Tab + + document.dispatchEvent(keyUp) + }) + + dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') + last.click() + }) + + btnGroup.addEventListener('shown.bs.dropdown', () => { + expect(btnGroup.classList.contains('show')).toEqual(true, '"show" class added on click') + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown') + + const keyUp = createEvent('keyup') + keyUp.which = 9 // Tab + + document.dispatchEvent(keyUp) + }) + + btnGroup.addEventListener('hidden.bs.dropdown', () => { + expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed') + done() + }) + + first.click() + }) + + it('should fire hide and hidden event without a clickEvent if event type is not click', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + + dropdown.addEventListener('hide.bs.dropdown', e => { + expect(e.clickEvent).toBeUndefined() + }) + + dropdown.addEventListener('hidden.bs.dropdown', e => { + expect(e.clickEvent).toBeUndefined() + done() + }) + + dropdown.addEventListener('shown.bs.dropdown', () => { + const keyDown = createEvent('keydown') + + keyDown.which = 27 + triggerDropdown.dispatchEvent(keyDown) + }) + + triggerDropdown.click() + }) + + it('should ignore keyboard events within s and ', + '
', + '
' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + dropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keyDown = createEvent('keydown') + + keyDown.which = 38 + input.dispatchEvent(keyDown) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keyDown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + done() + }) + + triggerDropdown.click() + }) + + it('should skip disabled element when using keyboard navigation', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + + dropdown.addEventListener('shown.bs.dropdown', () => { + const keyDown = createEvent('keydown') + keyDown.which = 40 + + triggerDropdown.dispatchEvent(keyDown) + triggerDropdown.dispatchEvent(keyDown) + + expect(document.activeElement.classList.contains('disabled')).toEqual(false, '.disabled not focused') + expect(document.activeElement.hasAttribute('disabled')).toEqual(false, ':disabled not focused') + done() + }) + + triggerDropdown.click() + }) + + it('should focus next/previous element when using keyboard navigation', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') + + dropdown.addEventListener('shown.bs.dropdown', () => { + const keyDown40 = createEvent('keydown') + keyDown40.which = 40 + + triggerDropdown.dispatchEvent(keyDown40) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + document.activeElement.dispatchEvent(keyDown40) + expect(document.activeElement).toEqual(item2, 'item2 is focused') + + const keyDown38 = createEvent('keydown') + keyDown38.which = 38 + + document.activeElement.dispatchEvent(keyDown38) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + done() + }) + + triggerDropdown.click() + }) + + it('should not close the dropdown if the user clicks on a text field', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + const input = fixtureEl.querySelector('input') + + input.addEventListener('click', () => { + expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + done() + }) + + dropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + input.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + + it('should not close the dropdown if the user clicks on a textarea', done => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + const textarea = fixtureEl.querySelector('textarea') + + textarea.addEventListener('click', () => { + expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + done() + }) + + dropdown.addEventListener('shown.bs.dropdown', () => { + expect(dropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown') + textarea.dispatchEvent(createEvent('click')) + }) + + triggerDropdown.click() + }) + + it('should ignore keyboard events for s and ', + '
', + '
' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + // Space key + const keyDownSpace = createEvent('keydown') + keyDownSpace.which = 32 + + // Key up + const keyDownUp = createEvent('keydown') + keyDownSpace.which = 38 + + // Key down + const keyDown = createEvent('keydown') + keyDownSpace.which = 40 + + // Key escape + const keyDownEscape = createEvent('keydown') + keyDownEscape.which = 27 + + dropdown.addEventListener('shown.bs.dropdown', () => { + // Space key + input.focus() + input.dispatchEvent(keyDownSpace) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keyDownSpace) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + + // Key up + input.focus() + input.dispatchEvent(keyDownUp) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keyDownUp) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + + // Key down + input.focus() + input.dispatchEvent(keyDown) + + expect(document.activeElement).toEqual(input, 'input still focused') + + textarea.focus() + textarea.dispatchEvent(keyDown) + + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + + // Key escape + input.focus() + input.dispatchEvent(keyDownEscape) + + expect(dropdown.classList.contains('show')).toEqual(false, 'dropdown menu is not shown') + done() + }) + + triggerDropdown.click() + }) + + it('should not open dropdown if escape key was pressed on the toggle', done => { + fixtureEl.innerHTML = [ + '
', + ' ' + ] + + const triggerDropdown = fixtureEl.querySelector('[data-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-toggle="dropdown"]') + + spyOn(dropdown, 'toggle') + + // Key escape + button.focus() + // Key escape + const keyDownEscape = createEvent('keydown') + keyDownEscape.which = 27 + button.dispatchEvent(keyDownEscape) + + setTimeout(() => { + expect(dropdown.toggle).not.toHaveBeenCalled() + expect(triggerDropdown.parentNode.classList.contains('show')).toEqual(false) + done() + }, 20) + }) + }) + + describe('jQueryInterface', () => { + it('should create a dropdown', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.dropdown.call(jQueryMock) + + expect(Dropdown.getInstance(div)).toBeDefined() + }) + + it('should not re create a dropdown', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div) + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + jQueryMock.fn.dropdown.call(jQueryMock) + + expect(Dropdown.getInstance(div)).toEqual(dropdown) + }) + + it('should throw error on undefined method', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const action = 'undefinedMethod' + + jQueryMock.fn.dropdown = Dropdown.jQueryInterface + jQueryMock.elements = [div] + + try { + jQueryMock.fn.dropdown.call(jQueryMock, action) + } catch (error) { + expect(error.message).toEqual(`No method named "${action}"`) + } + }) + }) + + describe('getInstance', () => { + it('should return dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const dropdown = new Dropdown(div) + + expect(Dropdown.getInstance(div)).toEqual(dropdown) + }) + + it('should return null when there is no dropdown instance', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Dropdown.getInstance(div)).toEqual(null) + }) + }) +}) diff --git a/js/tests/units/modal.spec.js b/js/tests/units/modal.spec.js new file mode 100644 index 000000000..604934785 --- /dev/null +++ b/js/tests/units/modal.spec.js @@ -0,0 +1,987 @@ +import Modal from '../../src/modal' +import EventHandler from '../../src/dom/event-handler' +import { makeArray } from '../../src/util/index' + +/** Test helpers */ +import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture' + +describe('Modal', () => { + let fixtureEl + let style + + beforeAll(() => { + fixtureEl = getFixture() + + // Enable the scrollbar measurer + const css = '.modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; }' + + style = document.createElement('style') + style.type = 'text/css' + style.appendChild(document.createTextNode(css)) + + document.head.appendChild(style) + + // Simulate scrollbars + document.documentElement.style.paddingRight = '16px' + }) + + afterEach(() => { + clearFixture() + + document.body.classList.remove('modal-open') + document.body.removeAttribute('style') + document.body.removeAttribute('data-padding-right') + const backdropList = makeArray(document.querySelectorAll('.modal-backdrop')) + + backdropList.forEach(backdrop => { + document.body.removeChild(backdrop) + }) + + document.body.style.paddingRight = '0px' + }) + + afterAll(() => { + document.head.removeChild(style) + document.documentElement.style.paddingRight = '0px' + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(Modal.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(Modal.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('toggle', () => { + it('should toggle a modal', done => { + fixtureEl.innerHTML = '