diff --git a/.babelrc.js b/.babelrc.js
index 44a5099541..6fdc98a4e7 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 20f7547fef..471707035b 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 e49218a1e8..e3a851537c 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 0a1d5ac0b4..039e6d1bb4 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 0000000000..18ffdc003d
--- /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/alert.js
similarity index 94%
rename from js/src/alert.js
rename to js/src/alert/alert.js
index 23ac9f25d4..99164bd195 100644
--- a/js/src/alert.js
+++ b/js/src/alert/alert.js
@@ -11,10 +11,10 @@ import {
emulateTransitionEnd,
getSelectorFromElement,
getTransitionDurationFromElement
-} from './util/index'
-import Data from './dom/data'
-import EventHandler from './dom/event-handler'
-import SelectorEngine from './dom/selector-engine'
+} from '../util/index'
+import Data from '../dom/data'
+import EventHandler from '../dom/event-handler'
+import SelectorEngine from '../dom/selector-engine'
/**
* ------------------------------------------------------------------------
@@ -53,6 +53,7 @@ const ClassName = {
class Alert {
constructor(element) {
this._element = element
+
if (this._element) {
Data.setData(element, DATA_KEY, this)
}
@@ -118,7 +119,7 @@ class Alert {
const transitionDuration = getTransitionDurationFromElement(element)
EventHandler
- .one(element, TRANSITION_END, event => this._destroyElement(element, event))
+ .one(element, TRANSITION_END, () => this._destroyElement(element))
emulateTransitionEnd(element, transitionDuration)
}
@@ -176,6 +177,7 @@ EventHandler
* 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
diff --git a/js/src/alert/alert.spec.js b/js/src/alert/alert.spec.js
new file mode 100644
index 0000000000..cb7b57b7f3
--- /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 0000000000..46018dd5ca
--- /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 2dff885354..3310b41abd 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 0000000000..dc5c5c30ce
--- /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 0000000000..4b2d416856
--- /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 0000000000..6dadd29a5a
--- /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('