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('