From c8c207465043d940aa031570f0bce5e8fff9ffcf Mon Sep 17 00:00:00 2001 From: Johann-S Date: Wed, 13 Mar 2019 16:23:50 +0200 Subject: Switch from QUnit to Jasmine. --- .babelrc.js | 7 +- build/build-plugins.js | 10 +- js/index.esm.js | 2 +- js/index.umd.js | 2 +- js/src/.eslintrc.json | 14 + js/src/alert.js | 189 -------------- js/src/alert/alert.js | 191 ++++++++++++++ js/src/alert/alert.spec.js | 127 +++++++++ js/src/dom/data.spec.js | 131 ++++++++++ js/src/dom/event-handler.js | 4 +- js/src/dom/event-handler.spec.js | 327 +++++++++++++++++++++++ js/src/util/index.spec.js | 297 +++++++++++++++++++++ js/src/util/sanitizer.spec.js | 70 +++++ js/tests/README.md | 76 +++--- js/tests/helpers/fixture.js | 20 ++ js/tests/karma.conf.js | 118 ++++----- js/tests/unit/alert.js | 123 --------- js/tests/unit/dom/data.js | 83 ------ js/tests/unit/dom/event-handler.js | 340 ------------------------ js/tests/unit/util/index.js | 191 -------------- js/tests/unit/util/sanitizer.js | 51 ---- package-lock.json | 516 ++++++++++++++++++++++++------------- package.json | 14 +- 23 files changed, 1623 insertions(+), 1280 deletions(-) create mode 100644 js/src/.eslintrc.json delete mode 100644 js/src/alert.js create mode 100644 js/src/alert/alert.js create mode 100644 js/src/alert/alert.spec.js create mode 100644 js/src/dom/data.spec.js create mode 100644 js/src/dom/event-handler.spec.js create mode 100644 js/src/util/index.spec.js create mode 100644 js/src/util/sanitizer.spec.js create mode 100644 js/tests/helpers/fixture.js delete mode 100644 js/tests/unit/alert.js delete mode 100644 js/tests/unit/dom/data.js delete mode 100644 js/tests/unit/dom/event-handler.js delete mode 100644 js/tests/unit/util/index.js delete mode 100644 js/tests/unit/util/sanitizer.js diff --git a/.babelrc.js b/.babelrc.js index 44a509954..6fdc98a4e 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -11,10 +11,5 @@ module.exports = { ], plugins: [ '@babel/plugin-proposal-object-rest-spread' - ], - env: { - test: { - plugins: [ 'istanbul' ] - } - } + ] }; diff --git a/build/build-plugins.js b/build/build-plugins.js index 20f7547fe..471707035 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -12,7 +12,6 @@ const rollup = require('rollup') const babel = require('rollup-plugin-babel') const banner = require('./banner.js') -const TEST = process.env.NODE_ENV === 'test' const plugins = [ babel({ // Only transpile our source code @@ -33,7 +32,7 @@ const bsPlugins = { Manipulator: path.resolve(__dirname, '../js/src/dom/manipulator.js'), Polyfill: path.resolve(__dirname, '../js/src/dom/polyfill.js'), SelectorEngine: path.resolve(__dirname, '../js/src/dom/selector-engine.js'), - Alert: path.resolve(__dirname, '../js/src/alert.js'), + Alert: path.resolve(__dirname, '../js/src/alert/alert.js'), Button: path.resolve(__dirname, '../js/src/button.js'), Carousel: path.resolve(__dirname, '../js/src/carousel.js'), Collapse: path.resolve(__dirname, '../js/src/collapse.js'), @@ -45,12 +44,7 @@ const bsPlugins = { Toast: path.resolve(__dirname, '../js/src/toast.js'), Tooltip: path.resolve(__dirname, '../js/src/tooltip.js') } -const rootPath = TEST ? '../js/coverage/dist/' : '../js/dist/' - -if (TEST) { - bsPlugins.Util = path.resolve(__dirname, '../js/src/util/index.js') - bsPlugins.Sanitizer = path.resolve(__dirname, '../js/src/util/sanitizer.js') -} +const rootPath = '../js/dist/' const defaultPluginConfig = { external: [ diff --git a/js/index.esm.js b/js/index.esm.js index e49218a1e..e3a851537 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -5,7 +5,7 @@ * -------------------------------------------------------------------------- */ -import Alert from './src/alert' +import Alert from './src/alert/alert' import Button from './src/button' import Carousel from './src/carousel' import Collapse from './src/collapse' diff --git a/js/index.umd.js b/js/index.umd.js index 0a1d5ac0b..039e6d1bb 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -5,7 +5,7 @@ * -------------------------------------------------------------------------- */ -import Alert from './src/alert' +import Alert from './src/alert/alert' import Button from './src/button' import Carousel from './src/carousel' import Collapse from './src/collapse' diff --git a/js/src/.eslintrc.json b/js/src/.eslintrc.json new file mode 100644 index 000000000..18ffdc003 --- /dev/null +++ b/js/src/.eslintrc.json @@ -0,0 +1,14 @@ +{ + "root": true, + "extends": [ + "../../.eslintrc.json" + ], + "overrides": [ + { + "files": ["**/*.spec.js"], + "env": { + "jasmine": true + } + } + ] +} diff --git a/js/src/alert.js b/js/src/alert.js deleted file mode 100644 index 23ac9f25d..000000000 --- a/js/src/alert.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * -------------------------------------------------------------------------- - * Bootstrap (v4.3.1): alert.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * -------------------------------------------------------------------------- - */ - -import { - jQuery as $, - TRANSITION_END, - emulateTransitionEnd, - getSelectorFromElement, - getTransitionDurationFromElement -} from './util/index' -import Data from './dom/data' -import EventHandler from './dom/event-handler' -import SelectorEngine from './dom/selector-engine' - -/** - * ------------------------------------------------------------------------ - * Constants - * ------------------------------------------------------------------------ - */ - -const NAME = 'alert' -const VERSION = '4.3.1' -const DATA_KEY = 'bs.alert' -const EVENT_KEY = `.${DATA_KEY}` -const DATA_API_KEY = '.data-api' - -const Selector = { - DISMISS: '[data-dismiss="alert"]' -} - -const Event = { - CLOSE: `close${EVENT_KEY}`, - CLOSED: `closed${EVENT_KEY}`, - CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` -} - -const ClassName = { - ALERT: 'alert', - FADE: 'fade', - SHOW: 'show' -} - -/** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ - */ - -class Alert { - constructor(element) { - this._element = element - if (this._element) { - Data.setData(element, DATA_KEY, this) - } - } - - // Getters - - static get VERSION() { - return VERSION - } - - // Public - - close(element) { - let rootElement = this._element - if (element) { - rootElement = this._getRootElement(element) - } - - const customEvent = this._triggerCloseEvent(rootElement) - - if (customEvent === null || customEvent.defaultPrevented) { - return - } - - this._removeElement(rootElement) - } - - dispose() { - Data.removeData(this._element, DATA_KEY) - this._element = null - } - - // Private - - _getRootElement(element) { - const selector = getSelectorFromElement(element) - let parent = false - - if (selector) { - parent = SelectorEngine.findOne(selector) - } - - if (!parent) { - parent = SelectorEngine.closest(element, `.${ClassName.ALERT}`) - } - - return parent - } - - _triggerCloseEvent(element) { - return EventHandler.trigger(element, Event.CLOSE) - } - - _removeElement(element) { - element.classList.remove(ClassName.SHOW) - - if (!element.classList.contains(ClassName.FADE)) { - this._destroyElement(element) - return - } - - const transitionDuration = getTransitionDurationFromElement(element) - - EventHandler - .one(element, TRANSITION_END, event => this._destroyElement(element, event)) - emulateTransitionEnd(element, transitionDuration) - } - - _destroyElement(element) { - if (element.parentNode) { - element.parentNode.removeChild(element) - } - - EventHandler.trigger(element, Event.CLOSED) - } - - // Static - - static _jQueryInterface(config) { - return this.each(function () { - let data = Data.getData(this, DATA_KEY) - - if (!data) { - data = new Alert(this) - } - - if (config === 'close') { - data[config](this) - } - }) - } - - static _handleDismiss(alertInstance) { - return function (event) { - if (event) { - event.preventDefault() - } - - alertInstance.close(this) - } - } - - static _getInstance(element) { - return Data.getData(element, DATA_KEY) - } -} - -/** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ - */ -EventHandler - .on(document, Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())) - -/** - * ------------------------------------------------------------------------ - * jQuery - * ------------------------------------------------------------------------ - * add .alert to jQuery only if jQuery is present - */ - -if (typeof $ !== 'undefined') { - const JQUERY_NO_CONFLICT = $.fn[NAME] - $.fn[NAME] = Alert._jQueryInterface - $.fn[NAME].Constructor = Alert - $.fn[NAME].noConflict = () => { - $.fn[NAME] = JQUERY_NO_CONFLICT - return Alert._jQueryInterface - } -} - -export default Alert diff --git a/js/src/alert/alert.js b/js/src/alert/alert.js new file mode 100644 index 000000000..99164bd19 --- /dev/null +++ b/js/src/alert/alert.js @@ -0,0 +1,191 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.1): alert.js + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * -------------------------------------------------------------------------- + */ + +import { + jQuery as $, + TRANSITION_END, + emulateTransitionEnd, + getSelectorFromElement, + getTransitionDurationFromElement +} from '../util/index' +import Data from '../dom/data' +import EventHandler from '../dom/event-handler' +import SelectorEngine from '../dom/selector-engine' + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = 'alert' +const VERSION = '4.3.1' +const DATA_KEY = 'bs.alert' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' + +const Selector = { + DISMISS: '[data-dismiss="alert"]' +} + +const Event = { + CLOSE: `close${EVENT_KEY}`, + CLOSED: `closed${EVENT_KEY}`, + CLICK_DATA_API: `click${EVENT_KEY}${DATA_API_KEY}` +} + +const ClassName = { + ALERT: 'alert', + FADE: 'fade', + SHOW: 'show' +} + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Alert { + constructor(element) { + this._element = element + + if (this._element) { + Data.setData(element, DATA_KEY, this) + } + } + + // Getters + + static get VERSION() { + return VERSION + } + + // Public + + close(element) { + let rootElement = this._element + if (element) { + rootElement = this._getRootElement(element) + } + + const customEvent = this._triggerCloseEvent(rootElement) + + if (customEvent === null || customEvent.defaultPrevented) { + return + } + + this._removeElement(rootElement) + } + + dispose() { + Data.removeData(this._element, DATA_KEY) + this._element = null + } + + // Private + + _getRootElement(element) { + const selector = getSelectorFromElement(element) + let parent = false + + if (selector) { + parent = SelectorEngine.findOne(selector) + } + + if (!parent) { + parent = SelectorEngine.closest(element, `.${ClassName.ALERT}`) + } + + return parent + } + + _triggerCloseEvent(element) { + return EventHandler.trigger(element, Event.CLOSE) + } + + _removeElement(element) { + element.classList.remove(ClassName.SHOW) + + if (!element.classList.contains(ClassName.FADE)) { + this._destroyElement(element) + return + } + + const transitionDuration = getTransitionDurationFromElement(element) + + EventHandler + .one(element, TRANSITION_END, () => this._destroyElement(element)) + emulateTransitionEnd(element, transitionDuration) + } + + _destroyElement(element) { + if (element.parentNode) { + element.parentNode.removeChild(element) + } + + EventHandler.trigger(element, Event.CLOSED) + } + + // Static + + static _jQueryInterface(config) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY) + + if (!data) { + data = new Alert(this) + } + + if (config === 'close') { + data[config](this) + } + }) + } + + static _handleDismiss(alertInstance) { + return function (event) { + if (event) { + event.preventDefault() + } + + alertInstance.close(this) + } + } + + static _getInstance(element) { + return Data.getData(element, DATA_KEY) + } +} + +/** + * ------------------------------------------------------------------------ + * Data Api implementation + * ------------------------------------------------------------------------ + */ +EventHandler + .on(document, Event.CLICK_DATA_API, Selector.DISMISS, Alert._handleDismiss(new Alert())) + +/** + * ------------------------------------------------------------------------ + * jQuery + * ------------------------------------------------------------------------ + * add .alert to jQuery only if jQuery is present + */ + +/* istanbul ignore if */ +if (typeof $ !== 'undefined') { + const JQUERY_NO_CONFLICT = $.fn[NAME] + $.fn[NAME] = Alert._jQueryInterface + $.fn[NAME].Constructor = Alert + $.fn[NAME].noConflict = () => { + $.fn[NAME] = JQUERY_NO_CONFLICT + return Alert._jQueryInterface + } +} + +export default Alert diff --git a/js/src/alert/alert.spec.js b/js/src/alert/alert.spec.js new file mode 100644 index 000000000..cb7b57b7f --- /dev/null +++ b/js/src/alert/alert.spec.js @@ -0,0 +1,127 @@ +import Alert from './alert' +import { makeArray, getTransitionDurationFromElement } from '../util/index' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../tests/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 instanciate 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 instanciate 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() + }) + }) +}) diff --git a/js/src/dom/data.spec.js b/js/src/dom/data.spec.js new file mode 100644 index 000000000..46018dd5c --- /dev/null +++ b/js/src/dom/data.spec.js @@ -0,0 +1,131 @@ +import Data from './data' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../tests/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/src/dom/event-handler.js b/js/src/dom/event-handler.js index 2dff88535..3310b41ab 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -192,7 +192,9 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { } const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, '')) - const fn = delegation ? bootstrapDelegationHandler(element, handler, delegationFn) : bootstrapHandler(element, handler) + const fn = delegation ? + bootstrapDelegationHandler(element, handler, delegationFn) : + bootstrapHandler(element, handler) fn.delegationSelector = delegation ? handler : null fn.originalHandler = originalHandler diff --git a/js/src/dom/event-handler.spec.js b/js/src/dom/event-handler.spec.js new file mode 100644 index 000000000..dc5c5c30c --- /dev/null +++ b/js/src/dom/event-handler.spec.js @@ -0,0 +1,327 @@ +import EventHandler from './event-handler' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../tests/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/src/util/index.spec.js b/js/src/util/index.spec.js new file mode 100644 index 000000000..4b2d41685 --- /dev/null +++ b/js/src/util/index.spec.js @@ -0,0 +1,297 @@ +import * as Util from './index' + +/** Test helpers */ +import { getFixture, clearFixture } from '../../tests/helpers/fixture' + +describe('Util', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('getUID', () => { + it('should generate uid', () => { + const uid = Util.getUID('bs') + const uid2 = Util.getUID('bs') + + expect(uid).not.toEqual(uid2) + }) + }) + + describe('getSelectorFromElement', () => { + it('should get selector from data-target', () => { + fixtureEl.innerHTML = [ + '
', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(Util.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if no data-target set', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(Util.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should get selector from href if data-target equal to #', () => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const testEl = fixtureEl.querySelector('#test') + + expect(Util.getSelectorFromElement(testEl)).toEqual('.target') + }) + + it('should return null if selector not found', () => { + fixtureEl.innerHTML = '' + + const testEl = fixtureEl.querySelector('#test') + + expect(Util.getSelectorFromElement(testEl)).toBeNull() + }) + }) + + describe('getTransitionDurationFromElement', () => { + it('should get transition from element', () => { + fixtureEl.innerHTML = '
' + + expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(300) + }) + + it('should return 0 if the element is undefined or null', () => { + expect(Util.getTransitionDurationFromElement(null)).toEqual(0) + expect(Util.getTransitionDurationFromElement(undefined)).toEqual(0) + }) + + it('should return 0 if the element do not possess transition', () => { + fixtureEl.innerHTML = '
' + + expect(Util.getTransitionDurationFromElement(fixtureEl.querySelector('div'))).toEqual(0) + }) + }) + + describe('triggerTransitionEnd', () => { + it('should trigger transitionend event', done => { + fixtureEl.innerHTML = '
' + + const el = fixtureEl.querySelector('div') + + el.addEventListener('transitionend', () => { + expect().nothing() + done() + }) + + Util.triggerTransitionEnd(el) + }) + }) + + describe('isElement', () => { + it('should detect if the parameter is an element or not', () => { + fixtureEl.innerHTML = '
' + + const el = document.querySelector('div') + + expect(Util.isElement(el)).toEqual(el.nodeType) + expect(Util.isElement({})).toEqual(undefined) + }) + + it('should detect jQuery element', () => { + fixtureEl.innerHTML = '
' + + const el = document.querySelector('div') + const fakejQuery = { + 0: el + } + + expect(Util.isElement(fakejQuery)).toEqual(el.nodeType) + }) + }) + + describe('emulateTransitionEnd', () => { + it('should emulate transition end', () => { + fixtureEl.innerHTML = '
' + + const el = document.querySelector('div') + const spy = spyOn(window, 'setTimeout') + + Util.emulateTransitionEnd(el, 10) + expect(spy).toHaveBeenCalled() + }) + + it('should not emulate transition end if already triggered', done => { + fixtureEl.innerHTML = '
' + + const el = fixtureEl.querySelector('div') + const spy = spyOn(el, 'removeEventListener') + + Util.emulateTransitionEnd(el, 10) + Util.triggerTransitionEnd(el) + + setTimeout(() => { + expect(spy).toHaveBeenCalled() + done() + }, 20) + }) + }) + + describe('typeCheckConfig', () => { + it('should check type of the config object', () => { + const namePlugin = 'collapse' + const defaultType = { + toggle: 'boolean', + parent: '(string|element)' + } + const config = { + toggle: true, + parent: 777 + } + + expect(() => { + Util.typeCheckConfig(namePlugin, config, defaultType) + }).toThrow(new Error('COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".')) + }) + }) + + describe('makeArray', () => { + it('should convert node list to array', () => { + const nodeList = document.querySelectorAll('div') + + expect(Array.isArray(nodeList)).toEqual(false) + expect(Array.isArray(Util.makeArray(nodeList))).toEqual(true) + }) + + it('should return an empty array if the nodeList is undefined', () => { + expect(Util.makeArray(null)).toEqual([]) + expect(Util.makeArray(undefined)).toEqual([]) + }) + }) + + describe('isVisible', () => { + it('should return false if the element is not defined', () => { + expect(Util.isVisible(null)).toEqual(false) + expect(Util.isVisible(undefined)).toEqual(false) + }) + + it('should return false if the element provided is not a dom element', () => { + expect(Util.isVisible({})).toEqual(false) + }) + + it('should return false if the element is not visible with display none', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Util.isVisible(div)).toEqual(false) + }) + + it('should return false if the element is not visible with visibility hidden', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Util.isVisible(div)).toEqual(false) + }) + + it('should return false if the parent element is not visible', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Util.isVisible(div)).toEqual(false) + }) + + it('should return true if the element is visible', () => { + fixtureEl.innerHTML = [ + '
', + '
', + '
' + ].join('') + + const div = fixtureEl.querySelector('#element') + + expect(Util.isVisible(div)).toEqual(true) + }) + }) + + describe('findShadowRoot', () => { + it('should return null if shadow dom is not available', () => { + // Only for newer browsers + if (!document.documentElement.attachShadow) { + expect().nothing() + return + } + + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + spyOn(document.documentElement, 'attachShadow').and.returnValue(null) + + expect(Util.findShadowRoot(div)).toEqual(null) + }) + + it('should return null when we do not find a shadow root', () => { + // Only for newer browsers + if (!document.documentElement.attachShadow) { + expect().nothing() + return + } + + spyOn(document, 'getRootNode').and.returnValue(undefined) + + expect(Util.findShadowRoot(document)).toEqual(null) + }) + + it('should return the shadow root when found', () => { + // Only for newer browsers + if (!document.documentElement.attachShadow) { + expect().nothing() + return + } + + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + const shadowRoot = div.attachShadow({ + mode: 'open' + }) + + expect(Util.findShadowRoot(shadowRoot)).toEqual(shadowRoot) + + shadowRoot.innerHTML = '' + + expect(Util.findShadowRoot(shadowRoot.firstChild)).toEqual(shadowRoot) + }) + }) + + describe('noop', () => { + it('should return a function', () => { + expect(typeof Util.noop()).toEqual('function') + }) + }) + + describe('reflow', () => { + it('should return element offset height to force the reflow', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + + expect(Util.reflow(div)).toEqual(0) + }) + }) +}) diff --git a/js/src/util/sanitizer.spec.js b/js/src/util/sanitizer.spec.js new file mode 100644 index 000000000..6dadd29a5 --- /dev/null +++ b/js/src/util/sanitizer.spec.js @@ -0,0 +1,70 @@ +import { DefaultWhitelist, sanitizeHtml } from './sanitizer' + +describe('Sanitizer', () => { + describe('sanitizeHtml', () => { + it('should return the same on empty string', () => { + const empty = '' + + const result = sanitizeHtml(empty, DefaultWhitelist, null) + + expect(result).toEqual(empty) + }) + + it('should sanitize template by removing tags with XSS', () => { + const template = [ + '
', + ' Click me', + ' Some content', + '
' + ].join('') + + const result = sanitizeHtml(template, DefaultWhitelist, null) + + expect(result.indexOf('script') === -1).toEqual(true) + }) + + it('should allow aria attributes and safe attributes', () => { + const template = [ + '
', + ' Some content', + '
' + ].join('') + + const result = sanitizeHtml(template, DefaultWhitelist, null) + + expect(result.indexOf('aria-pressed') !== -1).toEqual(true) + expect(result.indexOf('class="test"') !== -1).toEqual(true) + }) + + it('should remove not whitelist tags', () => { + const template = [ + '
', + ' ', + '
' + ].join('') + + const result = sanitizeHtml(template, DefaultWhitelist, null) + + expect(result.indexOf('