diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 34adc7bd47..dcc19277cf 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -30,7 +30,7 @@ }, { "path": "./dist/css/boosted.min.css", - "maxSize": "26.4 kB" + "maxSize": "26.5 kB" }, { "path": "./dist/js/boosted.bundle.js", diff --git a/.cspell.json b/.cspell.json index 77c36fa831..1c7afc6c9f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -12,6 +12,7 @@ "Blockquotes", "Bootstrappers", "borderless", + "Brotli", "browserslist", "browserslistrc", "btncheck", @@ -49,6 +50,7 @@ "française", "fullscreen", "getbootstrap", + "Gaël", "Grayscale", "Helv", "Hoverable", @@ -76,8 +78,10 @@ "offcanvas", "offcanvases", "opensource", + "Packagist", "pinterest", "popperjs", + "Poupard", "prebuild", "precompiled", "preconnect", @@ -127,13 +131,17 @@ "بالعالم", "مرحبًا" ], - "language": "en, en-US", + "language": "en-US", "files": [ - "site/**/*.md" + "**/*.md" ], "ignorePaths": [ ".cspell.json", - "*.min.*" + "dist/", + "*.min.*", + "**/*rtl*", + "**/tests/**", + "CHANGELOG.md" ], "useGitignore": true } \ No newline at end of file diff --git a/.github/workflows/cspell.yml b/.github/workflows/cspell.yml new file mode 100644 index 0000000000..3b214575d0 --- /dev/null +++ b/.github/workflows/cspell.yml @@ -0,0 +1,34 @@ +name: cspell + +on: + push: + branches-ignore: + - "dependabot/**" + pull_request: + workflow_dispatch: + +env: + FORCE_COLOR: 2 + NODE: 16 + +jobs: + cspell: + runs-on: ubuntu-latest + + steps: + - name: Bail early + if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'skip:ci') + run: exit 0 + + - name: Clone repository + uses: actions/checkout@v2 + if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) + + - name: Run cspell + uses: streetsidesoftware/cspell-action@v1 + with: + config: ".cspell.json" + files: "**/*.md" + inline: error + incremental_files_only: false + if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7a6b71775b..02a50f0116 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -50,6 +50,19 @@ jobs: run: npm ci if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) - - name: Test docs - run: npm run docs + - name: Build docs + run: npm run docs-build + if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) + + - name: Validate HTML + run: npm run docs-vnu + if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) + + - name: Run linkinator + uses: JustinBeckwith/linkinator-action@v1 + with: + paths: _site + recurse: true + verbosity: error + skip: "^(?!http://localhost)" if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci')) diff --git a/.github/workflows/pa11y.yml b/.github/workflows/pa11y.yml index 50fb553519..513d3afd3b 100644 --- a/.github/workflows/pa11y.yml +++ b/.github/workflows/pa11y.yml @@ -62,7 +62,7 @@ jobs: - name: Update sri run: npm run release-sri if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci') && !contains(github.event.pull_request.labels.*.name, 'skip:pa11y')) - + - name: Test docs run: npm run docs-build if: github.event_name == 'push' || (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip:ci') && !contains(github.event.pull_request.labels.*.name, 'skip:pa11y')) diff --git a/README.md b/README.md index e7ff23c000..af16cb4345 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Read the [Getting started page](https://boosted.orange.com/docs/5.1/getting-star [![JS gzip size](https://img.badgesize.io/Orange-OpenSource/Orange-Boosted-Bootstrap/main/dist/js/boosted.min.js?compression=gzip&label=JS%20gzip%20size)](https://github.com/Orange-OpenSource/Orange-Boosted-Bootstrap/blob/main/dist/js/boosted.min.js) [![JS Brotli size](https://img.badgesize.io/Orange-OpenSource/Orange-Boosted-Bootstrap/main/dist/js/boosted.min.js?compression=brotli&label=JS%20Brotli%20size)](https://github.com/Orange-OpenSource/Orange-Boosted-Bootstrap/blob/main/dist/js/boosted.min.js) [![BrowserStack Status](https://automate.browserstack.com/badge.svg?badge_key=ZGR2VnBvQ1F6ajhoNDIwWEF3Wm1RcjJlak1rdFZpRXJSdFZjTjJxQU9udz0tLU5wS2lrODNRTkswdzZZVUxjSHFpTHc9PQ==--714e60c953cf25bd52082a2b494eb881db1736c0)](https://automate.browserstack.com/public-build/ZGR2VnBvQ1F6ajhoNDIwWEF3Wm1RcjJlak1rdFZpRXJSdFZjTjJxQU9udz0tLU5wS2lrODNRTkswdzZZVUxjSHFpTHc9PQ==--714e60c953cf25bd52082a2b494eb881db1736c0) -[![JS Delivr](https://data.jsdelivr.com/v1/package/npm/boosted/badge)](https://www.jsdelivr.com/package/npm/boosted) +[![JSDelivr](https://data.jsdelivr.com/v1/package/npm/boosted/badge)](https://www.jsdelivr.com/package/npm/boosted) ## What's included diff --git a/build/build-plugins.js b/build/build-plugins.js index 401c676017..8fa0e0b1f3 100644 --- a/build/build-plugins.js +++ b/build/build-plugins.js @@ -15,8 +15,8 @@ const globby = require('globby') const { babel } = require('@rollup/plugin-babel') const banner = require('./banner.js') -const srcPath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/') -const jsFiles = globby.sync(srcPath + '/**/*.js') +const sourcePath = path.resolve(__dirname, '../js/src/').replace(/\\/g, '/') +const jsFiles = globby.sync(sourcePath + '/**/*.js') // Array which holds the resolved plugins const resolvedPlugins = [] @@ -31,7 +31,7 @@ for (const file of jsFiles) { dist: file.replace('src', 'dist'), fileName: path.basename(file), className: filenameToEntity(path.basename(file)) - // safeClassName: filenameToEntity(path.relative(srcPath, file)) + // safeClassName: filenameToEntity(path.relative(sourcePath, file)) }) } diff --git a/build/generate-sri.js b/build/generate-sri.js index 1ce940c37c..678fa4e3b0 100644 --- a/build/generate-sri.js +++ b/build/generate-sri.js @@ -60,9 +60,9 @@ const files = [ ] for (const file of files) { - fs.readFile(file.file, 'utf8', (err, data) => { - if (err) { - throw err + fs.readFile(file.file, 'utf8', (error, data) => { + if (error) { + throw error } const algo = 'sha384' diff --git a/build/rollup.config.js b/build/rollup.config.js index 417d5ec560..2e67663c42 100644 --- a/build/rollup.config.js +++ b/build/rollup.config.js @@ -9,7 +9,7 @@ const banner = require('./banner.js') const BUNDLE = process.env.BUNDLE === 'true' const ESM = process.env.ESM === 'true' -let fileDest = `boosted${ESM ? '.esm' : ''}` +let fileDestination = `boosted${ESM ? '.esm' : ''}` const external = ['@popperjs/core'] const plugins = [ babel({ @@ -24,7 +24,7 @@ const globals = { } if (BUNDLE) { - fileDest += '.bundle' + fileDestination += '.bundle' // Remove last entry in external array to bundle Popper external.pop() delete globals['@popperjs/core'] @@ -41,7 +41,7 @@ const rollupConfig = { input: path.resolve(__dirname, `../js/index.${ESM ? 'esm' : 'umd'}.js`), output: { banner, - file: path.resolve(__dirname, `../dist/js/${fileDest}.js`), + file: path.resolve(__dirname, `../dist/js/${fileDestination}.js`), format: ESM ? 'esm' : 'umd', globals, generatedCode: 'es2015' diff --git a/js/index.esm.js b/js/index.esm.js index 4a8bf422bd..8a7b17dd14 100644 --- a/js/index.esm.js +++ b/js/index.esm.js @@ -14,6 +14,7 @@ export { default as Collapse } from './src/collapse' export { default as Dropdown } from './src/dropdown' export { default as Modal } from './src/modal' export { default as Offcanvas } from './src/offcanvas' +export { default as OrangeNavbar } from './src/orange-navbar' export { default as Popover } from './src/popover' export { default as ScrollSpy } from './src/scrollspy' export { default as Tab } from './src/tab' diff --git a/js/index.umd.js b/js/index.umd.js index b5ec086c67..e2f4bb70c0 100644 --- a/js/index.umd.js +++ b/js/index.umd.js @@ -12,6 +12,7 @@ import Collapse from './src/collapse' import Dropdown from './src/dropdown' import Modal from './src/modal' import Offcanvas from './src/offcanvas' +import OrangeNavbar from './src/orange-navbar' import Popover from './src/popover' import ScrollSpy from './src/scrollspy' import Tab from './src/tab' @@ -27,6 +28,7 @@ export default { Dropdown, Modal, Offcanvas, + OrangeNavbar, Popover, ScrollSpy, Tab, diff --git a/js/src/carousel.js b/js/src/carousel.js index 548af85144..b6c144367a 100644 --- a/js/src/carousel.js +++ b/js/src/carousel.js @@ -245,27 +245,29 @@ class Carousel extends BaseComponent { } _addTouchEventListeners() { - for (const itemImg of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { - EventHandler.on(itemImg, EVENT_DRAG_START, event => event.preventDefault()) + for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) { + EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault()) } const endCallBack = () => { - if (this._config.pause === 'hover') { - // If it's a touch-enabled device, mouseenter/leave are fired as - // part of the mouse compatibility events on first tap - the carousel - // would stop cycling until user tapped out of it; - // here, we listen for touchend, explicitly pause the carousel - // (as if it's the second time we tap on it, mouseenter compat event - // is NOT fired) and after a timeout (to allow for mouse compatibility - // events to fire) we explicitly restart cycling - - this.pause() - if (this.touchTimeout) { - clearTimeout(this.touchTimeout) - } + if (this._config.pause !== 'hover') { + return + } + + // If it's a touch-enabled device, mouseenter/leave are fired as + // part of the mouse compatibility events on first tap - the carousel + // would stop cycling until user tapped out of it; + // here, we listen for touchend, explicitly pause the carousel + // (as if it's the second time we tap on it, mouseenter compat event + // is NOT fired) and after a timeout (to allow for mouse compatibility + // events to fire) we explicitly restart cycling - this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) + this.pause() + if (this.touchTimeout) { + clearTimeout(this.touchTimeout) } + + this.touchTimeout = setTimeout(event => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval) } const swipeConfig = { diff --git a/js/src/collapse.js b/js/src/collapse.js index 56d4f51c2d..b1088e106f 100644 --- a/js/src/collapse.js +++ b/js/src/collapse.js @@ -70,7 +70,7 @@ class Collapse extends BaseComponent { for (const elem of toggleList) { const selector = getSelectorFromElement(elem) const filterElement = SelectorEngine.find(selector) - .filter(foundElem => foundElem === this._element) + .filter(foundElement => foundElement === this._element) if (selector !== null && filterElement.length) { this._triggerArray.push(elem) @@ -185,9 +185,9 @@ class Collapse extends BaseComponent { this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW) for (const trigger of this._triggerArray) { - const elem = getElementFromSelector(trigger) + const element = getElementFromSelector(trigger) - if (elem && !this._isShown(elem)) { + if (element && !this._isShown(element)) { this._addAriaAndCollapsedClass([trigger], false) } } @@ -240,7 +240,7 @@ class Collapse extends BaseComponent { _getFirstLevelChildren(selector) { const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent) // remove children if greater depth - return SelectorEngine.find(selector, this._config.parent).filter(elem => !children.includes(elem)) + return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element)) } _addAriaAndCollapsedClass(triggerArray, isOpen) { @@ -248,25 +248,20 @@ class Collapse extends BaseComponent { return } - for (const elem of triggerArray) { - if (isOpen) { - elem.classList.remove(CLASS_NAME_COLLAPSED) - } else { - elem.classList.add(CLASS_NAME_COLLAPSED) - } - - elem.setAttribute('aria-expanded', isOpen) + for (const element of triggerArray) { + element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen) + element.setAttribute('aria-expanded', isOpen) } } // Static static jQueryInterface(config) { - return this.each(function () { - const _config = {} - if (typeof config === 'string' && /show|hide/.test(config)) { - _config.toggle = false - } + const _config = {} + if (typeof config === 'string' && /show|hide/.test(config)) { + _config.toggle = false + } + return this.each(function () { const data = Collapse.getOrCreateInstance(this, _config) if (typeof config === 'string') { diff --git a/js/src/dom/event-handler.js b/js/src/dom/event-handler.js index b9ebce3244..a31ed333c9 100644 --- a/js/src/dom/event-handler.js +++ b/js/src/dom/event-handler.js @@ -104,65 +104,54 @@ function bootstrapDelegationHandler(element, selector, fn) { const domElements = element.querySelectorAll(selector) for (let { target } = event; target && target !== this; target = target.parentNode) { - for (let i = domElements.length; i--;) { - if (domElements[i] === target) { - event.delegateTarget = target + for (const domElement of domElements) { + if (domElement !== target) { + continue + } - if (handler.oneOff) { - EventHandler.off(element, event.type, selector, fn) - } + event.delegateTarget = target - return fn.apply(target, [event]) + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn) } + + return fn.apply(target, [event]) } } - - // To please ESLint - return null } } function findHandler(events, handler, delegationSelector = null) { - const uidEventList = Object.keys(events) - - for (const uidEvent of uidEventList) { - const event = events[uidEvent] - - if (event.originalHandler === handler && event.delegationSelector === delegationSelector) { - return event - } - } - - return null + return Object.values(events) + .find(event => event.originalHandler === handler && event.delegationSelector === delegationSelector) } -function normalizeParams(originalTypeEvent, handler, delegationFn) { +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { const delegation = typeof handler === 'string' - const originalHandler = delegation ? delegationFn : handler + const originalHandler = delegation ? delegationFunction : handler let typeEvent = getTypeEvent(originalTypeEvent) - const isNative = nativeEvents.has(typeEvent) - if (!isNative) { + if (!nativeEvents.has(typeEvent)) { typeEvent = originalTypeEvent } return [delegation, originalHandler, typeEvent] } -function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { +function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) { if (typeof originalTypeEvent !== 'string' || !element) { return } if (!handler) { - handler = delegationFn - delegationFn = null + handler = delegationFunction + delegationFunction = null } // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position // this prevents the handler from being dispatched the same way as mouseover or mouseout does if (customEventsRegex.test(originalTypeEvent)) { - const wrapFn = fn => { + const wrapFunction = fn => { return function (event) { if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) { return fn.call(this, event) @@ -170,27 +159,27 @@ function addHandler(element, originalTypeEvent, handler, delegationFn, oneOff) { } } - if (delegationFn) { - delegationFn = wrapFn(delegationFn) + if (delegationFunction) { + delegationFunction = wrapFunction(delegationFunction) } else { - handler = wrapFn(handler) + handler = wrapFunction(handler) } } - const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) + const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction) const events = getEvent(element) const handlers = events[typeEvent] || (events[typeEvent] = {}) - const previousFn = findHandler(handlers, originalHandler, delegation ? handler : null) + const previousFunction = findHandler(handlers, originalHandler, delegation ? handler : null) - if (previousFn) { - previousFn.oneOff = previousFn.oneOff && oneOff + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff return } const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, '')) const fn = delegation ? - bootstrapDelegationHandler(element, handler, delegationFn) : + bootstrapDelegationHandler(element, handler, delegationFunction) : bootstrapHandler(element, handler) fn.delegationSelector = delegation ? handler : null @@ -231,20 +220,20 @@ function getTypeEvent(event) { } const EventHandler = { - on(element, event, handler, delegationFn) { - addHandler(element, event, handler, delegationFn, false) + on(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, false) }, - one(element, event, handler, delegationFn) { - addHandler(element, event, handler, delegationFn, true) + one(element, event, handler, delegationFunction) { + addHandler(element, event, handler, delegationFunction, true) }, - off(element, originalTypeEvent, handler, delegationFn) { + off(element, originalTypeEvent, handler, delegationFunction) { if (typeof originalTypeEvent !== 'string' || !element) { return } - const [delegation, originalHandler, typeEvent] = normalizeParams(originalTypeEvent, handler, delegationFn) + const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction) const inNamespace = typeEvent !== originalTypeEvent const events = getEvent(element) const isNamespace = originalTypeEvent.startsWith('.') @@ -284,13 +273,11 @@ const EventHandler = { const $ = getjQuery() const typeEvent = getTypeEvent(event) const inNamespace = event !== typeEvent - const isNative = nativeEvents.has(typeEvent) - let jQueryEvent + let jQueryEvent = null let bubbles = true let nativeDispatch = true let defaultPrevented = false - let evt = null if (inNamespace && $) { jQueryEvent = $.Event(event, args) @@ -301,12 +288,7 @@ const EventHandler = { defaultPrevented = jQueryEvent.isDefaultPrevented() } - if (isNative) { - evt = document.createEvent('HTMLEvents') - evt.initEvent(typeEvent, bubbles, true) - } else { - evt = new CustomEvent(event, { bubbles, cancelable: true }) - } + const evt = new Event(event, { bubbles, cancelable: true }) // merge custom information in our event if (typeof args !== 'undefined') { @@ -327,7 +309,7 @@ const EventHandler = { element.dispatchEvent(evt) } - if (evt.defaultPrevented && typeof jQueryEvent !== 'undefined') { + if (evt.defaultPrevented && jQueryEvent) { jQueryEvent.preventDefault() } diff --git a/js/src/dom/manipulator.js b/js/src/dom/manipulator.js index a3e9e192ae..e3ee293c7d 100644 --- a/js/src/dom/manipulator.js +++ b/js/src/dom/manipulator.js @@ -5,24 +5,24 @@ * -------------------------------------------------------------------------- */ -function normalizeData(val) { - if (val === 'true') { +function normalizeData(value) { + if (value === 'true') { return true } - if (val === 'false') { + if (value === 'false') { return false } - if (val === Number(val).toString()) { - return Number(val) + if (value === Number(value).toString()) { + return Number(value) } - if (val === '' || val === 'null') { + if (value === '' || value === 'null') { return null } - return val + return value } function normalizeDataKey(key) { diff --git a/js/src/dom/selector-engine.js b/js/src/dom/selector-engine.js index af27dc3795..9214335bd5 100644 --- a/js/src/dom/selector-engine.js +++ b/js/src/dom/selector-engine.js @@ -11,8 +11,6 @@ import { isDisabled, isVisible } from '../util/index' * Constants */ -const NODE_TEXT = 3 - const SelectorEngine = { find(selector, element = document.documentElement) { return [].concat(...Element.prototype.querySelectorAll.call(element, selector)) @@ -28,14 +26,11 @@ const SelectorEngine = { parents(element, selector) { const parents = [] - let ancestor = element.parentNode - - while (ancestor && ancestor.nodeType === Node.ELEMENT_NODE && ancestor.nodeType !== NODE_TEXT) { - if (ancestor.matches(selector)) { - parents.push(ancestor) - } + let ancestor = element.parentNode.closest(selector) - ancestor = ancestor.parentNode + while (ancestor) { + parents.push(ancestor) + ancestor = ancestor.parentNode.closest(selector) } return parents @@ -55,6 +50,7 @@ const SelectorEngine = { return [] }, + // TODO: this is now unused; remove later along with prev() next(element, selector) { let next = element.nextElementSibling @@ -79,7 +75,7 @@ const SelectorEngine = { 'details', '[tabindex]', '[contenteditable="true"]' - ].map(selector => `${selector}:not([tabindex^="-"])`).join(', ') + ].map(selector => `${selector}:not([tabindex^="-"])`).join(',') return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el)) } diff --git a/js/src/dropdown.js b/js/src/dropdown.js index 674150e016..5635ec96ec 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -9,7 +9,6 @@ import * as Popper from '@popperjs/core' import { defineJQueryPlugin, getElement, - getElementFromSelector, getNextActiveElement, isDisabled, isElement, @@ -90,7 +89,8 @@ class Dropdown extends BaseComponent { super(element, config) this._popper = null - this._menu = this._getMenuElement() + this._parent = this._element.parentNode // dropdown wrapper + this._menu = SelectorEngine.findOne(SELECTOR_MENU, this._parent) this._inNavbar = this._detectNavbar() } @@ -113,7 +113,7 @@ class Dropdown extends BaseComponent { } show() { - if (isDisabled(this._element) || this._isShown(this._menu)) { + if (isDisabled(this._element) || this._isShown()) { return } @@ -127,17 +127,15 @@ class Dropdown extends BaseComponent { return } - const parent = getElementFromSelector(this._element) || this._element.parentNode - - this._createPopper(parent) + this._createPopper() // If this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html - if ('ontouchstart' in document.documentElement && !parent.closest(SELECTOR_NAVBAR_NAV)) { - for (const elem of [].concat(...document.body.children)) { - EventHandler.on(elem, 'mouseover', noop) + if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) { + for (const element of [].concat(...document.body.children)) { + EventHandler.on(element, 'mouseover', noop) } } @@ -150,7 +148,7 @@ class Dropdown extends BaseComponent { } hide() { - if (isDisabled(this._element) || !this._isShown(this._menu)) { + if (isDisabled(this._element) || !this._isShown()) { return } @@ -186,8 +184,8 @@ class Dropdown extends BaseComponent { // If this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) { - for (const elem of [].concat(...document.body.children)) { - EventHandler.off(elem, 'mouseover', noop) + for (const element of [].concat(...document.body.children)) { + EventHandler.off(element, 'mouseover', noop) } } @@ -215,7 +213,7 @@ class Dropdown extends BaseComponent { return config } - _createPopper(parent) { + _createPopper() { if (typeof Popper === 'undefined') { throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)') } @@ -223,7 +221,7 @@ class Dropdown extends BaseComponent { let referenceElement = this._element if (this._config.reference === 'parent') { - referenceElement = parent + referenceElement = this._parent } else if (isElement(this._config.reference)) { referenceElement = getElement(this._config.reference) } else if (typeof this._config.reference === 'object') { @@ -234,16 +232,12 @@ class Dropdown extends BaseComponent { this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig) } - _isShown(element = this._element) { - return element.classList.contains(CLASS_NAME_SHOW) - } - - _getMenuElement() { - return SelectorEngine.next(this._element, SELECTOR_MENU)[0] + _isShown() { + return this._menu.classList.contains(CLASS_NAME_SHOW) } _getPlacement() { - const parentDropdown = this._element.parentNode + const parentDropdown = this._parent if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) { return PLACEMENT_RIGHT @@ -271,7 +265,7 @@ class Dropdown extends BaseComponent { const { offset } = this._config if (typeof offset === 'string') { - return offset.split(',').map(val => Number.parseInt(val, 10)) + return offset.split(',').map(value => Number.parseInt(value, 10)) } if (typeof offset === 'function') { @@ -314,7 +308,7 @@ class Dropdown extends BaseComponent { } _selectMenuItem({ key, target }) { - const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(el => isVisible(el)) + const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element)) if (!items.length) { return @@ -359,10 +353,6 @@ class Dropdown extends BaseComponent { continue } - const relatedTarget = { - relatedTarget: context._element - } - const composedPath = event.composedPath() const isMenuTarget = composedPath.includes(context._menu) if ( @@ -378,6 +368,8 @@ class Dropdown extends BaseComponent { continue } + const relatedTarget = { relatedTarget: context._element } + if (event.type === 'click') { relatedTarget.clickEvent = event } @@ -393,9 +385,10 @@ class Dropdown extends BaseComponent { // - If key is not UP or DOWN => not a dropdown command // - If trigger inside the menu => not a dropdown command - const isInput = /input|textarea/i.test(event.target.tagName) - const isEscapeEvent = event.key === ESCAPE_KEY - const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key) + const { target, key, delegateTarget } = event + const isInput = /input|textarea/i.test(target.tagName) + const isEscapeEvent = key === ESCAPE_KEY + const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(key) if (!isInput && !(isUpOrDownEvent || isEscapeEvent)) { return @@ -403,12 +396,12 @@ class Dropdown extends BaseComponent { if (isInput && !isEscapeEvent) { // eslint-disable-next-line unicorn/no-lonely-if - if (!isUpOrDownEvent || event.target.closest(SELECTOR_MENU)) { + if (!isUpOrDownEvent || target.closest(SELECTOR_MENU)) { return } } - const isActive = this.classList.contains(CLASS_NAME_SHOW) + const isActive = delegateTarget.classList.contains(CLASS_NAME_SHOW) if (!isActive && isEscapeEvent) { return @@ -421,7 +414,7 @@ class Dropdown extends BaseComponent { return } - const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] + const getToggleButton = SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, delegateTarget.parentNode) const instance = Dropdown.getOrCreateInstance(getToggleButton) if (isEscapeEvent) { diff --git a/js/src/modal.js b/js/src/modal.js index cc158d6ce5..054750c5f7 100644 --- a/js/src/modal.js +++ b/js/src/modal.js @@ -30,7 +30,6 @@ const EVENT_HIDDEN = `hidden${EVENT_KEY}` const EVENT_SHOW = `show${EVENT_KEY}` const EVENT_SHOWN = `shown${EVENT_KEY}` const EVENT_RESIZE = `resize${EVENT_KEY}` -const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}` const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}` const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}` @@ -115,7 +114,7 @@ class Modal extends BaseComponent { this._toggleEscapeEventListener(true) this._toggleResizeEventListener(true) - this._showBackdrop(() => this._showElement(relatedTarget)) + this._backdrop.show(() => this._showElement(relatedTarget)) } hide() { @@ -158,9 +157,22 @@ class Modal extends BaseComponent { // Private _initializeBackDrop() { + const clickCallback = () => { + if (this._config.backdrop === 'static') { + this._triggerBackdropTransition() + return + } + + this.hide() + } + + // 'static' option will be translated to true, and booleans will keep their value + const isVisible = Boolean(this._config.backdrop) + return new Backdrop({ - isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value - isAnimated: this._isAnimated() + isVisible, + isAnimated: this._isAnimated(), + clickCallback: isVisible ? clickCallback : null }) } @@ -250,25 +262,6 @@ class Modal extends BaseComponent { }) } - _showBackdrop(callback) { - EventHandler.on(this._element, EVENT_CLICK_DISMISS, event => { - if (event.target !== event.currentTarget) { - return - } - - if (this._config.backdrop === true) { - this.hide() - return - } - - if (this._config.backdrop === 'static') { - this._triggerBackdropTransition() - } - }) - - this._backdrop.show(callback) - } - _isAnimated() { return this._element.classList.contains(CLASS_NAME_FADE) } @@ -279,23 +272,22 @@ class Modal extends BaseComponent { return } - const { classList, scrollHeight, style } = this._element - const isModalOverflowing = scrollHeight > document.documentElement.clientHeight - const initialOverflowY = style.overflowY + const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight + const initialOverflowY = this._element.style.overflowY // return if the following background transition hasn't yet completed - if (initialOverflowY === 'hidden' || classList.contains(CLASS_NAME_STATIC)) { + if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) { return } if (!isModalOverflowing) { - style.overflowY = 'hidden' + this._element.style.overflowY = 'hidden' } - classList.add(CLASS_NAME_STATIC) + this._element.classList.add(CLASS_NAME_STATIC) this._queueCallback(() => { - classList.remove(CLASS_NAME_STATIC) + this._element.classList.remove(CLASS_NAME_STATIC) this._queueCallback(() => { - style.overflowY = initialOverflowY + this._element.style.overflowY = initialOverflowY }, this._dialog) }, this._dialog) @@ -370,9 +362,9 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( }) // avoid conflict when clicking modal toggler while another one is open - const allReadyOpen = SelectorEngine.findOne(OPEN_SELECTOR) - if (allReadyOpen) { - Modal.getInstance(allReadyOpen).hide() + const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR) + if (alreadyOpen) { + Modal.getInstance(alreadyOpen).hide() } const data = Modal.getOrCreateInstance(target) diff --git a/js/src/offcanvas.js b/js/src/offcanvas.js index db65340391..2735a9c2ae 100644 --- a/js/src/offcanvas.js +++ b/js/src/offcanvas.js @@ -238,8 +238,8 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( }) EventHandler.on(window, EVENT_LOAD_DATA_API, () => { - for (const el of SelectorEngine.find(OPEN_SELECTOR)) { - Offcanvas.getOrCreateInstance(el).show() + for (const selector of SelectorEngine.find(OPEN_SELECTOR)) { + Offcanvas.getOrCreateInstance(selector).show() } }) diff --git a/js/src/orange-navbar.js b/js/src/orange-navbar.js new file mode 100644 index 0000000000..5248d639e9 --- /dev/null +++ b/js/src/orange-navbar.js @@ -0,0 +1,40 @@ +import EventHandler from './dom/event-handler' +import BaseComponent from './base-component' +import SelectorEngine from './dom/selector-engine' + +const NAME = 'orangenavbar' +const DATA_KEY = 'bs.orangenavbar' +const EVENT_KEY = `.${DATA_KEY}` +const DATA_API_KEY = '.data-api' +const EVENT_SCROLL_DATA_API = `scroll${EVENT_KEY}${DATA_API_KEY}` +const SELECTOR_STICKY_TOP = 'header.sticky-top' + +class OrangeNavbar extends BaseComponent { + // Getters + static get NAME() { + return NAME + } + + enableMinimizing(el) { + // The minimized behaviour works only if your header has .sticky-top (fixed-top will be sticky without minimizing) + const Scroll = window.scrollY + const headerChildren = [...el.children] + const globalHeaderChild = headerChildren.find(element => !element.classList.contains('supra')) + if (globalHeaderChild) { + if (Scroll > 0) { + // Consider first element not having .supra in array is the first header + globalHeaderChild.classList.add('header-minimized') + } else { + globalHeaderChild.classList.remove('header-minimized') + } + } + } +} + +EventHandler.on(window, EVENT_SCROLL_DATA_API, () => { + for (const el of SelectorEngine.find(SELECTOR_STICKY_TOP)) { + OrangeNavbar.getOrCreateInstance(el).enableMinimizing(el) + } +}) + +export default OrangeNavbar diff --git a/js/src/tooltip.js b/js/src/tooltip.js index aa54371e7e..ef5b9fa825 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -178,14 +178,16 @@ class Tooltip extends BaseComponent { } else { context._leave() } - } else { - if (this._getTipElement().classList.contains(CLASS_NAME_SHOW)) { - this._leave() - return - } - this._enter() + return + } + + if (this._isShown()) { + this._leave() + return } + + this._enter() } dispose() { @@ -234,11 +236,7 @@ class Tooltip extends BaseComponent { if (this._popper) { this._popper.update() } else { - const placement = typeof this._config.placement === 'function' ? - this._config.placement.call(this, tip, this._element) : - this._config.placement - const attachment = AttachmentMap[placement.toUpperCase()] - this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) + this._createPopper(tip) } tip.classList.add(CLASS_NAME_SHOW) @@ -254,12 +252,12 @@ class Tooltip extends BaseComponent { } const complete = () => { - const prevHoverState = this._isHovered + const previousHoverState = this._isHovered this._isHovered = false EventHandler.trigger(this._element, this.constructor.Event.SHOWN) - if (prevHoverState) { + if (previousHoverState) { this._leave() } } @@ -268,7 +266,7 @@ class Tooltip extends BaseComponent { } hide() { - if (!this._popper) { + if (!this._isShown()) { return } @@ -291,6 +289,7 @@ class Tooltip extends BaseComponent { this._activeTrigger[TRIGGER_CLICK] = false this._activeTrigger[TRIGGER_FOCUS] = false this._activeTrigger[TRIGGER_HOVER] = false + this._isHovered = false const complete = () => { if (this._isWithActiveTrigger()) { @@ -308,7 +307,6 @@ class Tooltip extends BaseComponent { } this._queueCallback(complete, this.tip, this._isAnimated()) - this._isHovered = false } update() { @@ -356,7 +354,7 @@ class Tooltip extends BaseComponent { setContent(content) { let isShown = false if (this.tip) { - isShown = this.tip.classList.contains(CLASS_NAME_SHOW) + isShown = this._isShown() this.tip.remove() this.tip = null } @@ -404,11 +402,23 @@ class Tooltip extends BaseComponent { return this._config.animation || (this.tip && this.tip.classList.contains(CLASS_NAME_FADE)) } + _isShown() { + return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW) + } + + _createPopper(tip) { + const placement = typeof this._config.placement === 'function' ? + this._config.placement.call(this, tip, this._element) : + this._config.placement + const attachment = AttachmentMap[placement.toUpperCase()] + this._popper = Popper.createPopper(this._element, tip, this._getPopperConfig(attachment)) + } + _getOffset() { const { offset } = this._config if (typeof offset === 'string') { - return offset.split(',').map(val => Number.parseInt(val, 10)) + return offset.split(',').map(value => Number.parseInt(value, 10)) } if (typeof offset === 'function') { @@ -532,7 +542,7 @@ class Tooltip extends BaseComponent { } _enter() { - if (this._getTipElement().classList.contains(CLASS_NAME_SHOW) || this._isHovered) { + if (this._isShown() || this._isHovered) { this._isHovered = true return } @@ -572,9 +582,9 @@ class Tooltip extends BaseComponent { _getConfig(config) { const dataAttributes = Manipulator.getDataAttributes(this._element) - for (const dataAttr of Object.keys(dataAttributes)) { - if (DISALLOWED_ATTRIBUTES.has(dataAttr)) { - delete dataAttributes[dataAttr] + for (const dataAttribute of Object.keys(dataAttributes)) { + if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) { + delete dataAttributes[dataAttribute] } } diff --git a/js/src/util/backdrop.js b/js/src/util/backdrop.js index 63f2b581c6..8f121e5bd2 100644 --- a/js/src/util/backdrop.js +++ b/js/src/util/backdrop.js @@ -68,11 +68,12 @@ class Backdrop extends Config { this._append() + const element = this._getElement() if (this._config.isAnimated) { - reflow(this._getElement()) + reflow(element) } - this._getElement().classList.add(CLASS_NAME_SHOW) + element.classList.add(CLASS_NAME_SHOW) this._emulateAnimation(() => { execute(callback) @@ -130,9 +131,10 @@ class Backdrop extends Config { return } - this._config.rootElement.append(this._getElement()) + const element = this._getElement() + this._config.rootElement.append(element) - EventHandler.on(this._getElement(), EVENT_MOUSEDOWN, () => { + EventHandler.on(element, EVENT_MOUSEDOWN, () => { execute(this._config.clickCallback) }) diff --git a/js/src/util/focustrap.js b/js/src/util/focustrap.js index 46727ecf8a..88fd16b104 100644 --- a/js/src/util/focustrap.js +++ b/js/src/util/focustrap.js @@ -60,14 +60,12 @@ class FocusTrap extends Config { // Public activate() { - const { trapElement, autofocus } = this._config - if (this._isActive) { return } - if (autofocus) { - trapElement.focus() + if (this._config.autofocus) { + this._config.trapElement.focus() } EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop @@ -88,10 +86,9 @@ class FocusTrap extends Config { // Private _handleFocusin(event) { - const { target } = event const { trapElement } = this._config - if (target === document || target === trapElement || trapElement.contains(target)) { + if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) { return } diff --git a/js/src/util/sanitizer.js b/js/src/util/sanitizer.js index 5a7a680355..1db61ae707 100644 --- a/js/src/util/sanitizer.js +++ b/js/src/util/sanitizer.js @@ -82,13 +82,13 @@ export const DefaultAllowlist = { ul: [] } -export function sanitizeHtml(unsafeHtml, allowList, sanitizeFn) { +export function sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) { if (!unsafeHtml.length) { return unsafeHtml } - if (sanitizeFn && typeof sanitizeFn === 'function') { - return sanitizeFn(unsafeHtml) + if (sanitizeFunction && typeof sanitizeFunction === 'function') { + return sanitizeFunction(unsafeHtml) } const domParser = new window.DOMParser() diff --git a/js/src/util/scrollbar.js b/js/src/util/scrollbar.js index b81d4b2372..86a2bca01f 100644 --- a/js/src/util/scrollbar.js +++ b/js/src/util/scrollbar.js @@ -61,39 +61,39 @@ class ScrollBarHelper { this._element.style.overflow = 'hidden' } - _setElementAttributes(selector, styleProp, callback) { + _setElementAttributes(selector, styleProperty, callback) { const scrollbarWidth = this.getWidth() const manipulationCallBack = element => { if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) { return } - this._saveInitialAttribute(element, styleProp) - const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProp) - element.style.setProperty(styleProp, `${callback(Number.parseFloat(calculatedValue))}px`) + this._saveInitialAttribute(element, styleProperty) + const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty) + element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`) } this._applyManipulationCallback(selector, manipulationCallBack) } - _saveInitialAttribute(element, styleProp) { - const actualValue = element.style.getPropertyValue(styleProp) + _saveInitialAttribute(element, styleProperty) { + const actualValue = element.style.getPropertyValue(styleProperty) if (actualValue) { - Manipulator.setDataAttribute(element, styleProp, actualValue) + Manipulator.setDataAttribute(element, styleProperty, actualValue) } } - _resetElementAttributes(selector, styleProp) { + _resetElementAttributes(selector, styleProperty) { const manipulationCallBack = element => { - const value = Manipulator.getDataAttribute(element, styleProp) + const value = Manipulator.getDataAttribute(element, styleProperty) // We only want to remove the property if the value is `null`; the value can also be zero if (value === null) { - element.style.removeProperty(styleProp) + element.style.removeProperty(styleProperty) return } - Manipulator.removeDataAttribute(element, styleProp) - element.style.setProperty(styleProp, value) + Manipulator.removeDataAttribute(element, styleProperty) + element.style.setProperty(styleProperty, value) } this._applyManipulationCallback(selector, manipulationCallBack) diff --git a/js/tests/README.md b/js/tests/README.md index ca99c0ede0..79d05d444f 100644 --- a/js/tests/README.md +++ b/js/tests/README.md @@ -50,22 +50,24 @@ describe('getInstance', () => { }) // Asynchronous test -it('should show a tooltip without the animation', done => { - fixtureEl.innerHTML = '' +it('should show a tooltip without the animation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const tooltipEl = fixtureEl.querySelector('a') - const tooltip = new Tooltip(tooltipEl, { - animation: false - }) + const tooltipEl = fixtureEl.querySelector('a') + const tooltip = new Tooltip(tooltipEl, { + animation: false + }) - tooltipEl.addEventListener('shown.bs.tooltip', () => { - const tip = document.querySelector('.tooltip') + tooltipEl.addEventListener('shown.bs.tooltip', () => { + const tip = document.querySelector('.tooltip') - expect(tip).not.toBeNull() - expect(tip.classList.contains('fade')).toEqual(false) - done() - }) + expect(tip).not.toBeNull() + expect(tip.classList.contains('fade')).toEqual(false) + resolve() + }) - tooltip.show() + tooltip.show() + }) }) ``` diff --git a/js/tests/helpers/fixture.js b/js/tests/helpers/fixture.js index 02915af44c..5ad14e1db9 100644 --- a/js/tests/helpers/fixture.js +++ b/js/tests/helpers/fixture.js @@ -1,41 +1,38 @@ const fixtureId = 'fixture' export const getFixture = () => { - let fixtureEl = document.getElementById(fixtureId) - - if (!fixtureEl) { - fixtureEl = document.createElement('div') - fixtureEl.setAttribute('id', fixtureId) - fixtureEl.style.position = 'absolute' - fixtureEl.style.top = '-10000px' - fixtureEl.style.left = '-10000px' - fixtureEl.style.width = '10000px' - fixtureEl.style.height = '10000px' - document.body.append(fixtureEl) + let fixtureElement = document.getElementById(fixtureId) + + if (!fixtureElement) { + fixtureElement = document.createElement('div') + fixtureElement.setAttribute('id', fixtureId) + fixtureElement.style.position = 'absolute' + fixtureElement.style.top = '-10000px' + fixtureElement.style.left = '-10000px' + fixtureElement.style.width = '10000px' + fixtureElement.style.height = '10000px' + document.body.append(fixtureElement) } - return fixtureEl + return fixtureElement } export const clearFixture = () => { - const fixtureEl = getFixture() + const fixtureElement = getFixture() - fixtureEl.innerHTML = '' + fixtureElement.innerHTML = '' } -export const createEvent = (eventName, params = {}) => { - const event = document.createEvent('Event') - - event.initEvent(eventName, Boolean(params.bubbles), Boolean(params.cancelable)) - return event +export const createEvent = (eventName, parameters = {}) => { + return new Event(eventName, parameters) } export const jQueryMock = { elements: undefined, fn: {}, each(fn) { - for (const el of this.elements) { - fn.call(el) + for (const element of this.elements) { + fn.call(element) } } } @@ -43,8 +40,8 @@ export const jQueryMock = { export const clearBodyAndDocument = () => { const attributes = ['data-bs-padding-right', 'style'] - for (const attr of attributes) { - document.documentElement.removeAttribute(attr) - document.body.removeAttribute(attr) + for (const attribute of attributes) { + document.documentElement.removeAttribute(attribute) + document.body.removeAttribute(attribute) } } diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index f803bf8670..839adf083b 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -34,6 +34,10 @@ const detectBrowsers = { return ['ChromeHeadless'] } + if (availableBrowser.includes('Firefox')) { + return DEBUG ? ['Firefox'] : ['FirefoxHeadless'] + } + if (availableBrowser.includes('Chrome')) { return DEBUG ? ['Chrome'] : ['ChromeHeadless'] } @@ -42,16 +46,13 @@ const detectBrowsers = { return DEBUG ? ['Chromium'] : ['ChromiumHeadless'] } - if (availableBrowser.includes('Firefox')) { - return DEBUG ? ['Firefox'] : ['FirefoxHeadless'] - } - throw new Error('Please install Chrome, Chromium or Firefox') } } -const conf = { +const config = { basePath: '../..', + hostname: 'localhost', port: 9876, colors: true, autoWatch: false, @@ -101,8 +102,8 @@ const conf = { } if (BROWSERSTACK) { - conf.hostname = ip.address() - conf.browserStack = { + config.hostname = ip.address() + config.browserStack = { username: ENV.BROWSER_STACK_USERNAME, accessKey: ENV.BROWSER_STACK_ACCESS_KEY, build: `boosted-v5-${ENV.GITHUB_SHA ? ENV.GITHUB_SHA.slice(0, 7) + '-' : ''}${new Date().toISOString()}`, @@ -110,8 +111,8 @@ if (BROWSERSTACK) { retryLimit: 2 } plugins.push('karma-browserstack-launcher', 'karma-jasmine-html-reporter') - conf.customLaunchers = browsers - conf.browsers = Object.keys(browsers) + config.customLaunchers = browsers + config.browsers = Object.keys(browsers) reporters.push('BrowserStack', 'kjhtml') } else if (JQUERY_TEST) { frameworks.push('detectBrowsers') @@ -120,8 +121,8 @@ if (BROWSERSTACK) { 'karma-firefox-launcher', 'karma-detect-browsers' ) - conf.detectBrowsers = detectBrowsers - conf.files = [ + config.detectBrowsers = detectBrowsers + config.files = [ 'node_modules/jquery/dist/jquery.slim.min.js', { pattern: 'js/tests/unit/jquery.spec.js', @@ -137,8 +138,8 @@ if (BROWSERSTACK) { 'karma-coverage-istanbul-reporter' ) reporters.push('coverage-istanbul') - conf.detectBrowsers = detectBrowsers - conf.coverageIstanbulReporter = { + config.detectBrowsers = detectBrowsers + config.coverageIstanbulReporter = { dir: path.resolve(__dirname, '../coverage/'), reports: ['lcov', 'text-summary'], thresholds: { @@ -153,19 +154,19 @@ if (BROWSERSTACK) { } if (DEBUG) { - conf.hostname = ip.address() + config.hostname = 'localhost' plugins.push('karma-jasmine-html-reporter') reporters.push('kjhtml') - conf.singleRun = false - conf.autoWatch = true + config.singleRun = false + config.autoWatch = true } } -conf.frameworks = frameworks -conf.plugins = plugins -conf.reporters = reporters +config.frameworks = frameworks +config.plugins = plugins +config.reporters = reporters module.exports = karmaConfig => { - conf.logLevel = karmaConfig.LOG_ERROR - karmaConfig.set(conf) + config.logLevel = karmaConfig.LOG_ERROR + karmaConfig.set(config) } diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js index 210ae9a25e..e2fe49246a 100644 --- a/js/tests/unit/alert.spec.js +++ b/js/tests/unit/alert.spec.js @@ -63,60 +63,66 @@ describe('Alert', () => { }) describe('close', () => { - it('should close an alert', done => { - const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) - fixtureEl.innerHTML = '
' + it('should close an alert', () => { + return new Promise(resolve => { + const spy = jasmine.createSpy('spy', getTransitionDurationFromElement) + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - expect(spy).not.toHaveBeenCalled() - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + expect(spy).not.toHaveBeenCalled() + resolve() + }) - alert.close() + alert.close() + }) }) - it('should close alert with fade class', done => { - fixtureEl.innerHTML = '
' + it('should close alert with fade class', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const alertEl = document.querySelector('.alert') - const alert = new Alert(alertEl) + const alertEl = document.querySelector('.alert') + const alert = new Alert(alertEl) - alertEl.addEventListener('transitionend', () => { - expect().nothing() - }) + alertEl.addEventListener('transitionend', () => { + expect().nothing() + }) - alertEl.addEventListener('closed.bs.alert', () => { - expect(document.querySelectorAll('.alert')).toHaveSize(0) - done() - }) + alertEl.addEventListener('closed.bs.alert', () => { + expect(document.querySelectorAll('.alert')).toHaveSize(0) + resolve() + }) - alert.close() + alert.close() + }) }) - it('should not remove alert if close event is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not remove alert if close event is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const getAlert = () => document.querySelector('.alert') - const alertEl = getAlert() - const alert = new Alert(alertEl) + const getAlert = () => document.querySelector('.alert') + const alertEl = getAlert() + const alert = new Alert(alertEl) - alertEl.addEventListener('close.bs.alert', event => { - event.preventDefault() - setTimeout(() => { - expect(getAlert()).not.toBeNull() - done() - }, 10) - }) + alertEl.addEventListener('close.bs.alert', event => { + event.preventDefault() + setTimeout(() => { + expect(getAlert()).not.toBeNull() + resolve() + }, 10) + }) - alertEl.addEventListener('closed.bs.alert', () => { - throw new Error('should not fire closed event') - }) + alertEl.addEventListener('closed.bs.alert', () => { + throw new Error('should not fire closed event') + }) - alert.close() + alert.close() + }) }) }) diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 9c3f69e608..ddd2df96af 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -63,94 +63,100 @@ describe('Carousel', () => { expect(carouselByElement._element).toEqual(carouselEl) }) - 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 - }) + it('should go to next item if right arrow key is pressed', () => { + return new Promise(resolve => { + 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() + resolve() + }) - spyOn(carousel, '_keydown').and.callThrough() + const keydown = createEvent('keydown') + keydown.key = 'ArrowRight' - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item2')) - expect(carousel._keydown).toHaveBeenCalled() - done() + carouselEl.dispatchEvent(keydown) }) - - const keydown = createEvent('keydown') - keydown.key = 'ArrowRight' - - carouselEl.dispatchEvent(keydown) }) - it('should go to previous item if left arrow key is pressed', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should go to previous item if left arrow key is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) - spyOn(carousel, '_keydown').and.callThrough() + spyOn(carousel, '_keydown').and.callThrough() - carouselEl.addEventListener('slid.bs.carousel', () => { - expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) - expect(carousel._keydown).toHaveBeenCalled() - done() - }) + carouselEl.addEventListener('slid.bs.carousel', () => { + expect(fixtureEl.querySelector('.active')).toEqual(fixtureEl.querySelector('#item1')) + expect(carousel._keydown).toHaveBeenCalled() + resolve() + }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowLeft' + const keydown = createEvent('keydown') + keydown.key = 'ArrowLeft' - carouselEl.dispatchEvent(keydown) + carouselEl.dispatchEvent(keydown) + }) }) - it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not prevent keydown if key is not ARROW_LEFT or ARROW_RIGHT', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const carouselEl = fixtureEl.querySelector('#myCarousel') - const carousel = new Carousel(carouselEl, { - keyboard: true - }) + const carouselEl = fixtureEl.querySelector('#myCarousel') + const carousel = new Carousel(carouselEl, { + keyboard: true + }) - spyOn(carousel, '_keydown').and.callThrough() + spyOn(carousel, '_keydown').and.callThrough() - carouselEl.addEventListener('keydown', event => { - expect(carousel._keydown).toHaveBeenCalled() - expect(event.defaultPrevented).toBeFalse() - done() - }) + carouselEl.addEventListener('keydown', event => { + expect(carousel._keydown).toHaveBeenCalled() + expect(event.defaultPrevented).toBeFalse() + resolve() + }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - carouselEl.dispatchEvent(keydown) + carouselEl.dispatchEvent(keydown) + }) }) it('should ignore keyboard events within s and ', - ' ', - '' - ].join('') + it('should bubble up the events to the parent elements', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownParent = fixtureEl.querySelector('.dropdown') + const dropdown = new Dropdown(triggerDropdown) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.focus() - const keydown = createEvent('keydown') + const showFunction = jasmine.createSpy('showFunction') + dropdownParent.addEventListener('show.bs.dropdown', showFunction) - keydown.key = 'ArrowUp' - input.dispatchEvent(keydown) + const shownFunction = jasmine.createSpy('shownFunction') + dropdownParent.addEventListener('shown.bs.dropdown', () => { + shownFunction() + dropdown.hide() + }) - expect(document.activeElement).toEqual(input, 'input still focused') + const hideFunction = jasmine.createSpy('hideFunction') + dropdownParent.addEventListener('hide.bs.dropdown', hideFunction) - textarea.focus() - textarea.dispatchEvent(keydown) + dropdownParent.addEventListener('hidden.bs.dropdown', () => { + expect(showFunction).toHaveBeenCalled() + expect(shownFunction).toHaveBeenCalled() + expect(hideFunction).toHaveBeenCalled() + resolve() + }) - expect(document.activeElement).toEqual(textarea, 'textarea still focused') - done() + dropdown.show() }) - - triggerDropdown.click() }) - it('should skip disabled element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should ignore keyboard events within s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.focus() + const keydown = createEvent('keydown') + + keydown.key = 'ArrowUp' + input.dispatchEvent(keydown) - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + expect(document.activeElement).toEqual(input, 'input still focused') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + textarea.focus() + textarea.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) - triggerDropdown.dispatchEvent(keydown) + expect(document.activeElement).toEqual(textarea, 'textarea still focused') + resolve() + }) - expect(document.activeElement).not.toHaveClass('disabled') - expect(document.activeElement.hasAttribute('disabled')).toBeFalse() - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should skip hidden element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should skip disabled element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) + triggerDropdown.dispatchEvent(keydown) - expect(document.activeElement).not.toHaveClass('d-none') - expect(document.activeElement.style.display).not.toEqual('none') - expect(document.activeElement.style.visibility).not.toEqual('hidden') + expect(document.activeElement).not.toHaveClass('disabled') + expect(document.activeElement.hasAttribute('disabled')).toBeFalse() + resolve() + }) - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should focus next/previous element when using keyboard navigation', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const item1 = fixtureEl.querySelector('#item1') - const item2 = fixtureEl.querySelector('#item2') + it('should skip hidden element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - const keydownArrowDown = createEvent('keydown') - keydownArrowDown.key = 'ArrowDown' + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - triggerDropdown.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - document.activeElement.dispatchEvent(keydownArrowDown) - expect(document.activeElement).toEqual(item2, 'item2 is focused') + triggerDropdown.dispatchEvent(keydown) - const keydownArrowUp = createEvent('keydown') - keydownArrowUp.key = 'ArrowUp' + expect(document.activeElement).not.toHaveClass('d-none') + expect(document.activeElement.style.display).not.toEqual('none') + expect(document.activeElement.style.visibility).not.toEqual('hidden') - document.activeElement.dispatchEvent(keydownArrowUp) - expect(document.activeElement).toEqual(item1, 'item1 is focused') + resolve() + }) - done() + triggerDropdown.click() }) - - triggerDropdown.click() }) - it('should open the dropdown and focus on the last item when using ArrowUp for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should focus next/previous element when using keyboard navigation', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const lastItem = fixtureEl.querySelector('#item2') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const item1 = fixtureEl.querySelector('#item1') + const item2 = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(lastItem, 'item2 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + const keydownArrowDown = createEvent('keydown') + keydownArrowDown.key = 'ArrowDown' + + triggerDropdown.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + document.activeElement.dispatchEvent(keydownArrowDown) + expect(document.activeElement).toEqual(item2, 'item2 is focused') + + const keydownArrowUp = createEvent('keydown') + keydownArrowUp.key = 'ArrowUp' + + document.activeElement.dispatchEvent(keydownArrowUp) + expect(document.activeElement).toEqual(item1, 'item1 is focused') + + resolve() }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keydown) + triggerDropdown.click() + }) }) - it('should open the dropdown and focus on the first item when using ArrowDown for the first time', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open the dropdown and focus on the last item when using ArrowUp for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const firstItem = fixtureEl.querySelector('#item1') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const lastItem = fixtureEl.querySelector('#item2') - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - setTimeout(() => { - expect(document.activeElement).toEqual(firstItem, 'item1 is focused') - done() + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(lastItem, 'item2 is focused') + resolve() + }) }) - }) - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' - triggerDropdown.dispatchEvent(keydown) + const keydown = createEvent('keydown') + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keydown) + }) }) - it('should not close the dropdown if the user clicks on a text field within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open the dropdown and focus on the first item when using ArrowDown for the first time', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const firstItem = fixtureEl.querySelector('#item1') - input.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + setTimeout(() => { + expect(document.activeElement).toEqual(firstItem, 'item1 is focused') + resolve() + }) + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - input.dispatchEvent(createEvent('click')) + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' + triggerDropdown.dispatchEvent(keydown) }) - - triggerDropdown.click() }) - it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close the dropdown if the user clicks on a text field within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const textarea = fixtureEl.querySelector('textarea') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - textarea.addEventListener('click', () => { - expect(triggerDropdown).toHaveClass('show') - done() - }) + input.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - expect(triggerDropdown).toHaveClass('show') - textarea.dispatchEvent(createEvent('click')) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + input.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should not close the dropdown if the user clicks on a textarea within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const textarea = fixtureEl.querySelector('textarea') - triggerDropdown.addEventListener('hidden.bs.dropdown', () => { - expect().nothing() - done() - }) + textarea.addEventListener('click', () => { + expect(triggerDropdown).toHaveClass('show') + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - input.dispatchEvent(createEvent('click', { - bubbles: true - })) - }) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + expect(triggerDropdown).toHaveClass('show') + textarea.dispatchEvent(createEvent('click')) + }) - triggerDropdown.click() + triggerDropdown.click() + }) }) - it('should ignore keyboard events for s and ', - ' ', - '' - ].join('') + it('should close the dropdown if the user clicks on a text field that is not contained within dropdown-menu', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const input = fixtureEl.querySelector('input') - const textarea = fixtureEl.querySelector('textarea') - - const test = (eventKey, elementToDispatch) => { - const event = createEvent('keydown') - event.key = eventKey - elementToDispatch.focus() - elementToDispatch.dispatchEvent(event) - expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) - } + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + triggerDropdown.addEventListener('hidden.bs.dropdown', () => { + expect().nothing() + resolve() + }) - triggerDropdown.addEventListener('shown.bs.dropdown', () => { - // Key Space - test('Space', input) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + input.dispatchEvent(createEvent('click', { + bubbles: true + })) + }) - test('Space', textarea) + triggerDropdown.click() + }) + }) + + it('should ignore keyboard events for s and ', + ' ', + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const input = fixtureEl.querySelector('input') + const textarea = fixtureEl.querySelector('textarea') + + const test = (eventKey, elementToDispatch) => { + const event = createEvent('keydown') + event.key = eventKey + elementToDispatch.focus() + elementToDispatch.dispatchEvent(event) + expect(document.activeElement).toEqual(elementToDispatch, `${elementToDispatch.tagName} still focused`) + } - // Key ArrowUp - test('ArrowUp', input) + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - test('ArrowUp', textarea) + triggerDropdown.addEventListener('shown.bs.dropdown', () => { + // Key Space + test('Space', input) - // Key ArrowDown - test('ArrowDown', input) + test('Space', textarea) - test('ArrowDown', textarea) + // Key ArrowUp + test('ArrowUp', input) - // Key Escape - input.focus() - input.dispatchEvent(keydownEscape) + test('ArrowUp', textarea) - expect(triggerDropdown).not.toHaveClass('show') - done() - }) + // Key ArrowDown + test('ArrowDown', input) - triggerDropdown.click() - }) + test('ArrowDown', textarea) - it('should not open dropdown if escape key was pressed on the toggle', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + // Key Escape + input.focus() + input.dispatchEvent(keydownEscape) - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = new Dropdown(triggerDropdown) - const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }) - spyOn(dropdown, 'toggle') + triggerDropdown.click() + }) + }) - // Key escape - button.focus() - // Key escape - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' - button.dispatchEvent(keydownEscape) + it('should not open dropdown if escape key was pressed on the toggle', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - setTimeout(() => { - expect(dropdown.toggle).not.toHaveBeenCalled() - expect(triggerDropdown).not.toHaveClass('show') - done() - }, 20) - }) + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = new Dropdown(triggerDropdown) + const button = fixtureEl.querySelector('button[data-bs-toggle="dropdown"]') - it('should propagate escape key events if dropdown is closed', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + spyOn(dropdown, 'toggle') - const parent = fixtureEl.querySelector('.parent') - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + // Key escape + button.focus() + // Key escape + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' + button.dispatchEvent(keydownEscape) - const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + setTimeout(() => { + expect(dropdown.toggle).not.toHaveBeenCalled() + expect(triggerDropdown).not.toHaveClass('show') + resolve() + }, 20) + }) + }) + + it('should propagate escape key events if dropdown is closed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + const parent = fixtureEl.querySelector('.parent') + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + + const parentKeyHandler = jasmine.createSpy('parentKeyHandler') + + parent.addEventListener('keydown', parentKeyHandler) + parent.addEventListener('keyup', () => { + expect(parentKeyHandler).toHaveBeenCalled() + resolve() + }) - parent.addEventListener('keydown', parentKeyHandler) - parent.addEventListener('keyup', () => { - expect(parentKeyHandler).toHaveBeenCalled() - done() + const keydownEscape = createEvent('keydown', { bubbles: true }) + keydownEscape.key = 'Escape' + const keyupEscape = createEvent('keyup', { bubbles: true }) + keyupEscape.key = 'Escape' + + toggle.focus() + toggle.dispatchEvent(keydownEscape) + toggle.dispatchEvent(keyupEscape) }) + }) - const keydownEscape = createEvent('keydown', { bubbles: true }) - keydownEscape.key = 'Escape' - const keyupEscape = createEvent('keyup', { bubbles: true }) - keyupEscape.key = 'Escape' + it('should close dropdown using `escape` button, and return focus to its trigger', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - toggle.focus() - toggle.dispatchEvent(keydownEscape) - toggle.dispatchEvent(keyupEscape) - }) + const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - it('should close dropdown using `escape` button, and return focus to its trigger', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + toggle.addEventListener('shown.bs.dropdown', () => { + const keydownEvent = createEvent('keydown', { bubbles: true }) + keydownEvent.key = 'ArrowDown' + toggle.dispatchEvent(keydownEvent) + keydownEvent.key = 'Escape' + toggle.dispatchEvent(keydownEvent) + }) - const toggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(document.activeElement).toEqual(toggle) + resolve() + })) - toggle.addEventListener('shown.bs.dropdown', () => { - const keydownEvent = createEvent('keydown', { bubbles: true }) - keydownEvent.key = 'ArrowDown' - toggle.dispatchEvent(keydownEvent) - keydownEvent.key = 'Escape' - toggle.dispatchEvent(keydownEvent) + toggle.click() }) + }) - toggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(document.activeElement).toEqual(toggle) - done() - })) + it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - toggle.click() - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + dropdownMenu.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + document.documentElement.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - dropdownMenu.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + })) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - document.documentElement.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => { - expect(dropdownToggle).not.toHaveClass('show') - done() - })) + it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - dropdownToggle.click() - }) + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + const expectDropdownToBeOpened = () => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + document.documentElement.click() + }, 150) - const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) - const expectDropdownToBeOpened = () => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - document.documentElement.click() - }, 150) + dropdownToggle.addEventListener('hidden.bs.dropdown', () => { + expect(dropdownToggle).not.toHaveClass('show') + resolve() + }) - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() + dropdownToggle.click() }) + }) - dropdownToggle.addEventListener('hidden.bs.dropdown', () => { - expect(dropdownToggle).not.toHaveClass('show') - done() - }) + it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - dropdownToggle.click() + const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') + + const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { + expect(dropdownToggle).toHaveClass('show') + if (shouldTriggerClick) { + document.documentElement.click() + } else { + resolve() + } + + expectDropdownToBeOpened(false) + }, 150) + + dropdownToggle.addEventListener('shown.bs.dropdown', () => { + dropdownMenu.click() + expectDropdownToBeOpened() + }) + + dropdownToggle.click() + }) }) - it('should not close dropdown by clicking inside or outside the dropdown menu when it has data-attribute `data-bs-auto-close="false"`', done => { + it('should be able to identify clicked dropdown, no matter the markup order', () => { fixtureEl.innerHTML = [ '', + ' ', '' ].join('') const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') const dropdownMenu = fixtureEl.querySelector('.dropdown-menu') - - const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => { - expect(dropdownToggle).toHaveClass('show') - if (shouldTriggerClick) { - document.documentElement.click() - } else { - done() - } - - expectDropdownToBeOpened(false) - }, 150) - - dropdownToggle.addEventListener('shown.bs.dropdown', () => { - dropdownMenu.click() - expectDropdownToBeOpened() - }) + const spy = spyOn(Dropdown, 'getOrCreateInstance').and.callThrough() dropdownToggle.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) + dropdownMenu.click() + expect(spy).toHaveBeenCalledWith(dropdownToggle) }) }) @@ -2030,52 +2162,54 @@ describe('Dropdown', () => { }) }) - it('should open dropdown when pressing keydown or keyup', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should open dropdown when pressing keydown or keyup', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const dropdown = fixtureEl.querySelector('.dropdown') - const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const dropdown = fixtureEl.querySelector('.dropdown') + const keydown = createEvent('keydown') + keydown.key = 'ArrowDown' - const keydown = createEvent('keydown') - keydown.key = 'ArrowDown' + const keyup = createEvent('keyup') + keyup.key = 'ArrowUp' - const keyup = createEvent('keyup') - keyup.key = 'ArrowUp' + const handleArrowDown = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + setTimeout(() => { + dropdown.hide() + keydown.key = 'ArrowUp' + triggerDropdown.dispatchEvent(keyup) + }, 20) + } - const handleArrowDown = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - setTimeout(() => { - dropdown.hide() - keydown.key = 'ArrowUp' - triggerDropdown.dispatchEvent(keyup) - }, 20) - } - - const handleArrowUp = () => { - expect(triggerDropdown).toHaveClass('show') - expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - } - - dropdown.addEventListener('shown.bs.dropdown', event => { - if (event.target.key === 'ArrowDown') { - handleArrowDown() - } else { - handleArrowUp() + const handleArrowUp = () => { + expect(triggerDropdown).toHaveClass('show') + expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() } - }) - triggerDropdown.dispatchEvent(keydown) + dropdown.addEventListener('shown.bs.dropdown', event => { + if (event.target.key === 'ArrowDown') { + handleArrowDown() + } else { + handleArrowUp() + } + }) + + triggerDropdown.dispatchEvent(keydown) + }) }) it('should allow `data-bs-toggle="dropdown"` click events to bubble up', () => { @@ -2101,27 +2235,29 @@ describe('Dropdown', () => { expect(delegatedClickListener).toHaveBeenCalled() }) - it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') + it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') - const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') - const childElement = fixtureEl.querySelector('#childElement') + const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]') + const childElement = fixtureEl.querySelector('#childElement') - btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { - expect(btnDropdown).toHaveClass('show') - expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') - done() - })) + btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => { + expect(btnDropdown).toHaveClass('show') + expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true') + resolve() + })) - childElement.click() + childElement.click() + }) }) }) diff --git a/js/tests/unit/jquery.spec.js b/js/tests/unit/jquery.spec.js index 16781a3518..7da39d6303 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -12,7 +12,7 @@ import ScrollSpy from '../../src/scrollspy' import Tab from '../../src/tab' import Toast from '../../src/toast' import Tooltip from '../../src/tooltip' -import { getFixture, clearFixture } from '../helpers/fixture' +import { clearFixture, getFixture } from '../helpers/fixture' describe('jQuery', () => { let fixtureEl @@ -40,19 +40,21 @@ describe('jQuery', () => { expect(Tooltip.jQueryInterface).toEqual(jQuery.fn.tooltip) }) - it('should use jQuery event system', done => { - fixtureEl.innerHTML = [ - '
', - ' ', - '
' - ].join('') - - $(fixtureEl).find('.alert') - .one('closed.bs.alert', () => { - expect($(fixtureEl).find('.alert')).toHaveSize(0) - done() - }) - - $(fixtureEl).find('button').trigger('click') + it('should use jQuery event system', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '
', + ' ', + '
' + ].join('') + + $(fixtureEl).find('.alert') + .one('closed.bs.alert', () => { + expect($(fixtureEl).find('.alert')).toHaveSize(0) + resolve() + }) + + $(fixtureEl).find('button').trigger('click') + }) }) }) diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index 84a95c86ad..0471a1b9f7 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -56,93 +56,101 @@ describe('Modal', () => { }) describe('toggle', () => { - it('should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '' + it('should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + modal.toggle() + }) - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() modal.toggle() }) - - modalEl.addEventListener('hidden.bs.modal', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - - modal.toggle() }) }) describe('show', () => { - it('should show a modal', done => { - fixtureEl.innerHTML = '' + it('should show a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal without backdrop', done => { - fixtureEl.innerHTML = '' + it('should show a modal without backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - modalEl.addEventListener('show.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('show.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should show a modal and append the element', done => { - const modalEl = document.createElement('div') - const id = 'dynamicModal' + it('should show a modal and append the element', () => { + return new Promise(resolve => { + const modalEl = document.createElement('div') + const id = 'dynamicModal' - modalEl.setAttribute('id', id) - modalEl.classList.add('modal') - modalEl.innerHTML = '' + modalEl.setAttribute('id', id) + modalEl.classList.add('modal') + modalEl.innerHTML = '' - const modal = new Modal(modalEl) + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const dynamicModal = document.getElementById(id) - expect(dynamicModal).not.toBeNull() - dynamicModal.remove() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + const dynamicModal = document.getElementById(id) + expect(dynamicModal).not.toBeNull() + dynamicModal.remove() + resolve() + }) - modal.show() + modal.show() + }) }) it('should do nothing if a modal is shown', () => { @@ -173,511 +181,550 @@ describe('Modal', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should not fire shown event when show is prevented', done => { - fixtureEl.innerHTML = '' + it('should not fire shown event when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() - const expectedDone = () => { - expect().nothing() - done() - } + const expectedDone = () => { + expect().nothing() + resolve() + } - setTimeout(expectedDone, 10) - }) + setTimeout(expectedDone, 10) + }) - modalEl.addEventListener('shown.bs.modal', () => { - throw new Error('shown event triggered') - }) + modalEl.addEventListener('shown.bs.modal', () => { + throw new Error('shown event triggered') + }) - modal.show() + modal.show() + }) }) - it('should be shown after the first call to show() has been prevented while fading is enabled ', done => { - fixtureEl.innerHTML = '' + it('should be shown after the first call to show() has been prevented while fading is enabled ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - let prevented = false - modalEl.addEventListener('show.bs.modal', event => { - if (!prevented) { - event.preventDefault() - prevented = true + let prevented = false + modalEl.addEventListener('show.bs.modal', event => { + if (!prevented) { + event.preventDefault() + prevented = true - setTimeout(() => { - modal.show() - }) - } - }) + setTimeout(() => { + modal.show() + }) + } + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(prevented).toBeTrue() - expect(modal._isAnimated()).toBeTrue() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(prevented).toBeTrue() + expect(modal._isAnimated()).toBeTrue() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should set is transitioning if fade class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - it('should set is transitioning if fade class is present', done => { - fixtureEl.innerHTML = '' + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + modalEl.addEventListener('show.bs.modal', () => { + setTimeout(() => { + expect(modal._isTransitioning).toBeTrue() + }) + }) - modalEl.addEventListener('show.bs.modal', () => { - setTimeout(() => { - expect(modal._isTransitioning).toBeTrue() + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._isTransitioning).toBeFalse() + resolve() }) - }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._isTransitioning).toBeFalse() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) - - spyOn(modal, 'hide').and.callThrough() + it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) + + spyOn(modal, 'hide').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() + modal.show() }) - - modal.show() }) - it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set .modal\'s scroll top to 0', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should set .modal\'s scroll top to 0', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.scrollTop).toEqual(0) - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.scrollTop).toEqual(0) + resolve() + }) - modal.show() + modal.show() + }) }) - it('should set modal body scroll top to 0 if modal body do not exists', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const modalBody = modalEl.querySelector('.modal-body') - const modal = new Modal(modalEl) + it('should set modal body scroll top to 0 if modal body do not exists', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const modalBody = modalEl.querySelector('.modal-body') + const modal = new Modal(modalEl) + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalBody.scrollTop).toEqual(0) + resolve() + }) - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalBody.scrollTop).toEqual(0) - done() + modal.show() }) - - modal.show() }) - it('should not trap focus if focus equal to false', done => { - fixtureEl.innerHTML = '' + it('should not trap focus if focus equal to false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - focus: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + focus: false + }) - spyOn(modal._focustrap, 'activate').and.callThrough() + spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).not.toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should add listener when escape touch is pressed', done => { - fixtureEl.innerHTML = '' + it('should add listener when escape touch is pressed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide').and.callThrough() + spyOn(modal, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - }) + modalEl.dispatchEvent(keydownEscape) + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal.hide).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal.hide).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should do nothing when the pressed key is not escape', done => { - fixtureEl.innerHTML = '' + it('should do nothing when the pressed key is not escape', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal, 'hide') + spyOn(modal, 'hide') - const expectDone = () => { - expect(modal.hide).not.toHaveBeenCalled() + const expectDone = () => { + expect(modal.hide).not.toHaveBeenCalled() - done() - } + resolve() + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownTab = createEvent('keydown') - keydownTab.key = 'Tab' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownTab = createEvent('keydown') + keydownTab.key = 'Tab' - modalEl.dispatchEvent(keydownTab) - setTimeout(expectDone, 30) - }) + modalEl.dispatchEvent(keydownTab) + setTimeout(expectDone, 30) + }) - modal.show() + modal.show() + }) }) - it('should adjust dialog on resize', done => { - fixtureEl.innerHTML = '' + it('should adjust dialog on resize', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + + spyOn(modal, '_adjustDialog').and.callThrough() - spyOn(modal, '_adjustDialog').and.callThrough() + const expectDone = () => { + expect(modal._adjustDialog).toHaveBeenCalled() - const expectDone = () => { - expect(modal._adjustDialog).toHaveBeenCalled() + resolve() + } - done() - } + modalEl.addEventListener('shown.bs.modal', () => { + const resizeEvent = createEvent('resize') - modalEl.addEventListener('shown.bs.modal', () => { - const resizeEvent = createEvent('resize') + window.dispatchEvent(resizeEvent) + setTimeout(expectDone, 10) + }) - window.dispatchEvent(resizeEvent) - setTimeout(expectDone, 10) + modal.show() }) - - modal.show() }) - it('should not close modal when clicking on modal-content', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should not close modal when clicking on modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toEqual(true) - done() - }, 10) - } - - modalEl.addEventListener('shown.bs.modal', () => { - fixtureEl.querySelector('.modal-dialog').click() - fixtureEl.querySelector('.modal-content').click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toEqual(true) + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + fixtureEl.querySelector('.modal-dialog').click() + fixtureEl.querySelector('.modal-content').click() + shownCallback() + }) - it('should not close modal when clicking outside of modal-content if backdrop = false', done => { - fixtureEl.innerHTML = '' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: false + modal.show() }) + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: false + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - it('should not close modal when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' + modal.show() }) + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + it('should not close modal when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - shownCallback() - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modal.show() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + shownCallback() + }) - it('should close modal when escape key is pressed with keyboard = true and backdrop is static', done => { - fixtureEl.innerHTML = '' + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static', - keyboard: true + modal.show() }) + }) + it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static', + keyboard: true + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeFalse() - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeFalse() + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) - modal.show() + modal.show() + }) }) - it('should not close modal when escape key is pressed with keyboard = false', done => { - fixtureEl.innerHTML = '' + it('should not close modal when escape key is pressed with keyboard = false', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - keyboard: false - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + keyboard: false + }) - const shownCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + const shownCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('shown.bs.modal', () => { - const keydownEscape = createEvent('keydown') - keydownEscape.key = 'Escape' + modalEl.addEventListener('shown.bs.modal', () => { + const keydownEscape = createEvent('keydown') + keydownEscape.key = 'Escape' - modalEl.dispatchEvent(keydownEscape) - shownCallback() - }) + modalEl.dispatchEvent(keydownEscape) + shownCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('Should not hide a modal') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('Should not hide a modal') + }) - modal.show() + modal.show() + }) }) - it('should not overflow when clicking outside of modal-content if backdrop = static', done => { - fixtureEl.innerHTML = '' + it('should not overflow when clicking outside of modal-content if backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - setTimeout(() => { - expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) - done() - }, 20) - }) + modalEl.addEventListener('shown.bs.modal', () => { + modalEl.click() + setTimeout(() => { + expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight) + resolve() + }, 20) + }) - modal.show() + modal.show() + }) }) - it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', done => { - fixtureEl.innerHTML = '' + it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl, { - backdrop: 'static' - }) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl, { + backdrop: 'static' + }) - modalEl.addEventListener('shown.bs.modal', () => { - const spy = spyOn(modal, '_queueCallback').and.callThrough() + modalEl.addEventListener('shown.bs.modal', () => { + const spy = spyOn(modal, '_queueCallback').and.callThrough() - modalEl.click() - modalEl.click() + const mouseOverEvent = createEvent('mousedown') + const backdrop = document.querySelector('.modal-backdrop') - setTimeout(() => { - expect(spy).toHaveBeenCalledTimes(1) - done() - }, 20) - }) + backdrop.dispatchEvent(mouseOverEvent) + backdrop.dispatchEvent(mouseOverEvent) - modal.show() + setTimeout(() => { + expect(spy).toHaveBeenCalledTimes(1) + resolve() + }, 20) + }) + + modal.show() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'activate').and.callThrough() + spyOn(modal._focustrap, 'activate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal._focustrap.activate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal._focustrap.activate).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) describe('hide', () => { - it('should hide a modal', done => { - fixtureEl.innerHTML = '' + it('should hide a modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hide.bs.modal', event => { - expect(event).toBeDefined() - }) + modalEl.addEventListener('hide.bs.modal', event => { + expect(event).toBeDefined() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(backdropSpy).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(backdropSpy).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) - it('should close modal when clicking outside of modal-content', done => { - fixtureEl.innerHTML = '' + it('should close modal when clicking outside of modal-content', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + modalEl.addEventListener('shown.bs.modal', () => { + const mouseOverEvent = createEvent('mousedown') + document.querySelector('.modal-backdrop').dispatchEvent(mouseOverEvent) + }) - modalEl.addEventListener('shown.bs.modal', () => { - modalEl.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() + modal.show() }) - - modal.show() }) it('should do nothing is the modal is not shown', () => { @@ -703,52 +750,56 @@ describe('Modal', () => { expect().nothing() }) - it('should not hide a modal if hide is prevented', done => { - fixtureEl.innerHTML = '' + it('should not hide a modal if hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - const hideCallback = () => { - setTimeout(() => { - expect(modal._isShown).toBeTrue() - done() - }, 10) - } + const hideCallback = () => { + setTimeout(() => { + expect(modal._isShown).toBeTrue() + resolve() + }, 10) + } - modalEl.addEventListener('hide.bs.modal', event => { - event.preventDefault() - hideCallback() - }) + modalEl.addEventListener('hide.bs.modal', event => { + event.preventDefault() + hideCallback() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - throw new Error('should not trigger hidden') - }) + modalEl.addEventListener('hidden.bs.modal', () => { + throw new Error('should not trigger hidden') + }) - modal.show() + modal.show() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - spyOn(modal._focustrap, 'deactivate').and.callThrough() + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + spyOn(modal._focustrap, 'deactivate').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - modal.hide() - }) + modalEl.addEventListener('shown.bs.modal', () => { + modal.hide() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modal._focustrap.deactivate).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modal._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) }) @@ -789,246 +840,260 @@ describe('Modal', () => { }) describe('data-api', () => { - it('should toggle modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should toggle modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + setTimeout(() => trigger.click(), 10) + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - setTimeout(() => trigger.click(), 10) - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toBeNull() + expect(modalEl.getAttribute('role')).toBeNull() + expect(modalEl.getAttribute('aria-hidden')).toEqual('true') + expect(modalEl.style.display).toEqual('none') + expect(document.querySelector('.modal-backdrop')).toBeNull() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toBeNull() - expect(modalEl.getAttribute('role')).toBeNull() - expect(modalEl.getAttribute('aria-hidden')).toEqual('true') - expect(modalEl.style.display).toEqual('none') - expect(document.querySelector('.modal-backdrop')).toBeNull() - done() + trigger.click() }) - - trigger.click() }) - it('should not recreate a new modal', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should not recreate a new modal', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const modal = new Modal(modalEl) - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const modal = new Modal(modalEl) + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(modal, 'show').and.callThrough() + spyOn(modal, 'show').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - expect(modal.show).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('shown.bs.modal', () => { + expect(modal.show).toHaveBeenCalled() + resolve() + }) - trigger.click() + trigger.click() + }) }) - it('should prevent default when the trigger is or ', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should prevent default when the trigger is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + expect(modalEl.getAttribute('aria-modal')).toEqual('true') + expect(modalEl.getAttribute('role')).toEqual('dialog') + expect(modalEl.getAttribute('aria-hidden')).toBeNull() + expect(modalEl.style.display).toEqual('block') + expect(document.querySelector('.modal-backdrop')).not.toBeNull() + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - expect(modalEl.getAttribute('aria-modal')).toEqual('true') - expect(modalEl.getAttribute('role')).toEqual('dialog') - expect(modalEl.getAttribute('aria-hidden')).toBeNull() - expect(modalEl.style.display).toEqual('block') - expect(document.querySelector('.modal-backdrop')).not.toBeNull() - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() + trigger.click() }) - - trigger.click() }) - it('should focus the trigger on hide', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + it('should focus the trigger on hide', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - spyOn(trigger, 'focus') + spyOn(trigger, 'focus') - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modal.hide() - }) + modal.hide() + }) - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 20) - } + const hideListener = () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 20) + } - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) - trigger.click() + trigger.click() + }) }) + it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') + + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) + + spyOn(Event.prototype, 'preventDefault').and.callThrough() + + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') - - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) - - spyOn(Event.prototype, 'preventDefault').and.callThrough() - - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).not.toHaveBeenCalled() + resolve() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).not.toHaveBeenCalled() - done() + modal.show() }) - - modal.show() }) - it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', done => { - fixtureEl.innerHTML = [ - '' - ].join('') + it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is or ', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '' + ].join('') - const modalEl = fixtureEl.querySelector('.modal') - const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') - const modal = new Modal(modalEl) + const modalEl = fixtureEl.querySelector('.modal') + const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]') + const modal = new Modal(modalEl) - spyOn(Event.prototype, 'preventDefault').and.callThrough() + spyOn(Event.prototype, 'preventDefault').and.callThrough() - modalEl.addEventListener('shown.bs.modal', () => { - btnClose.click() - }) + modalEl.addEventListener('shown.bs.modal', () => { + btnClose.click() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - expect(Event.prototype.preventDefault).toHaveBeenCalled() - done() - }) + modalEl.addEventListener('hidden.bs.modal', () => { + expect(Event.prototype.preventDefault).toHaveBeenCalled() + resolve() + }) - modal.show() + modal.show() + }) }) + it('should not focus the trigger if the modal is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - it('should not focus the trigger if the modal is not visible', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + modalEl.addEventListener('shown.bs.modal', () => { + const modal = Modal.getInstance(modalEl) - modalEl.addEventListener('shown.bs.modal', () => { - const modal = Modal.getInstance(modalEl) + modal.hide() + }) - modal.hide() - }) + const hideListener = () => { + setTimeout(() => { + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 20) + } - const hideListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 20) - } + modalEl.addEventListener('hidden.bs.modal', () => { + hideListener() + }) - modalEl.addEventListener('hidden.bs.modal', () => { - hideListener() + trigger.click() }) - - trigger.click() }) + it('should not focus the trigger if the modal is not shown', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '' + ].join('') - it('should not focus the trigger if the modal is not shown', done => { - fixtureEl.innerHTML = [ - '', - '' - ].join('') + const modalEl = fixtureEl.querySelector('.modal') + const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') - const modalEl = fixtureEl.querySelector('.modal') - const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]') + spyOn(trigger, 'focus') - spyOn(trigger, 'focus') + const showListener = () => { + setTimeout(() => { + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 10) + } - const showListener = () => { - setTimeout(() => { - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 10) - } + modalEl.addEventListener('show.bs.modal', event => { + event.preventDefault() + showListener() + }) - modalEl.addEventListener('show.bs.modal', event => { - event.preventDefault() - showListener() + trigger.click() }) - - trigger.click() }) - it('should call hide first, if another modal is open', done => { - fixtureEl.innerHTML = [ - '', - '', - '' - ].join('') - - const trigger2 = fixtureEl.querySelector('button') - const modalEl1 = document.querySelector('#modal1') - const modalEl2 = document.querySelector('#modal2') - const modal1 = new Modal(modalEl1) - - modalEl1.addEventListener('shown.bs.modal', () => { - trigger2.click() - }) - modalEl1.addEventListener('hidden.bs.modal', () => { - expect(Modal.getInstance(modalEl2)).not.toBeNull() - expect(modalEl2).toHaveClass('show') - done() + it('should call hide first, if another modal is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '', + '' + ].join('') + + const trigger2 = fixtureEl.querySelector('button') + const modalEl1 = document.querySelector('#modal1') + const modalEl2 = document.querySelector('#modal2') + const modal1 = new Modal(modalEl1) + + modalEl1.addEventListener('shown.bs.modal', () => { + trigger2.click() + }) + modalEl1.addEventListener('hidden.bs.modal', () => { + expect(Modal.getInstance(modalEl2)).not.toBeNull() + expect(modalEl2).toHaveClass('show') + resolve() + }) + modal1.show() }) - modal1.show() }) }) - describe('jQueryInterface', () => { it('should create a modal', () => { fixtureEl.innerHTML = '' diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js index f87527fb2f..852ffa5560 100644 --- a/js/tests/unit/offcanvas.spec.js +++ b/js/tests/unit/offcanvas.spec.js @@ -147,84 +147,91 @@ describe('Offcanvas', () => { }) describe('options', () => { - it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => { - fixtureEl.innerHTML = '
' + it('if scroll is enabled, should allow body to scroll while offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: true }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() - offCanvas.hide() - }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() - done() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).not.toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).not.toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvas.show() }) - it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', done => { - fixtureEl.innerHTML = '
' + it('if scroll is disabled, should call ScrollBarHelper to handle scrollBar on body', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() - spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) + spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough() + spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { scroll: false }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() - offCanvas.hide() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.hide).toHaveBeenCalled() + offCanvas.hide() + }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() + resolve() + }) + offCanvas.show() }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(ScrollBarHelper.prototype.reset).toHaveBeenCalled() - done() - }) - offCanvas.show() }) - it('should hide a shown element if user click on backdrop', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element if user click on backdrop', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl, { backdrop: true }) - const clickEvent = document.createEvent('MouseEvents') - clickEvent.initEvent('mousedown', true, true) - spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() + const clickEvent = new Event('mousedown', { bubbles: true, cancelable: true }) + spyOn(offCanvas._backdrop._config, 'clickCallback').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toEqual(jasmine.any(Function)) - offCanvas._backdrop._getElement().dispatchEvent(clickEvent) - }) + offCanvas._backdrop._getElement().dispatchEvent(clickEvent) + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) - it('should not trap focus if scroll is allowed', done => { - fixtureEl.innerHTML = '
' + it('should not trap focus if scroll is allowed', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl, { - scroll: true - }) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl, { + scroll: true + }) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).not.toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) @@ -242,44 +249,48 @@ describe('Offcanvas', () => { expect(offCanvas.show).toHaveBeenCalled() }) - it('should call hide method if show class is present', done => { - fixtureEl.innerHTML = '
' + it('should call hide method if show class is present', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - spyOn(offCanvas, 'hide') + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + spyOn(offCanvas, 'hide') - offCanvas.toggle() + offCanvas.toggle() - expect(offCanvas.hide).toHaveBeenCalled() - done() - }) + expect(offCanvas.hide).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('show', () => { - it('should add `showing` class during opening and `show` class on end', done => { - fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `showing` class during opening and `show` class on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('show.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') - }) + offCanvasEl.addEventListener('show.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + resolve() + }) - offCanvas.show() - expect(offCanvasEl).toHaveClass('showing') + offCanvas.show() + expect(offCanvasEl).toHaveClass('showing') + }) }) it('should do nothing if already shown', () => { @@ -299,104 +310,114 @@ describe('Offcanvas', () => { expect(offCanvas._backdrop.show).not.toHaveBeenCalled() }) - it('should show a hidden element', done => { - fixtureEl.innerHTML = '
' + it('should show a hidden element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(offCanvas._backdrop.show).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(offCanvas._backdrop.show).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) - it('should not fire shown when show is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire shown when show is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'show').and.callThrough() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.show).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.show).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('show.bs.offcanvas', event => { - event.preventDefault() - expectEnd() - }) + offCanvasEl.addEventListener('show.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - throw new Error('should not fire shown event') - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + throw new Error('should not fire shown event') + }) - offCanvas.show() + offCanvas.show() + }) }) - it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => { - fixtureEl.innerHTML = '
' + it('on window load, should make visible an offcanvas element, if its markup contains class "show"', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - spyOn(Offcanvas.prototype, 'show').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + spyOn(Offcanvas.prototype, 'show').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + resolve() + }) - window.dispatchEvent(createEvent('load')) + window.dispatchEvent(createEvent('load')) - const instance = Offcanvas.getInstance(offCanvasEl) - expect(instance).not.toBeNull() - expect(Offcanvas.prototype.show).toHaveBeenCalled() + const instance = Offcanvas.getInstance(offCanvasEl) + expect(instance).not.toBeNull() + expect(Offcanvas.prototype.show).toHaveBeenCalled() + }) }) - it('should trap focus', done => { - fixtureEl.innerHTML = '
' + it('should trap focus', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'activate').and.callThrough() + spyOn(offCanvas._focustrap, 'activate').and.callThrough() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvas._focustrap.activate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvas._focustrap.activate).toHaveBeenCalled() + resolve() + }) - offCanvas.show() + offCanvas.show() + }) }) }) describe('hide', () => { - it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', done => { - fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('.offcanvas') - const offCanvas = new Offcanvas(offCanvasEl) + it('should add `hiding` class during closing and remover `show` & `hiding` classes on end', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' + const offCanvasEl = fixtureEl.querySelector('.offcanvas') + const offCanvas = new Offcanvas(offCanvasEl) - offCanvasEl.addEventListener('hide.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('show') - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('show') + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('hiding') - expect(offCanvasEl).not.toHaveClass('show') - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('hiding') + expect(offCanvasEl).not.toHaveClass('show') + resolve() + }) - offCanvas.show() - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - offCanvas.hide() - expect(offCanvasEl).not.toHaveClass('showing') - expect(offCanvasEl).toHaveClass('hiding') + offCanvas.show() + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + offCanvas.hide() + expect(offCanvasEl).not.toHaveClass('showing') + expect(offCanvasEl).toHaveClass('hiding') + }) }) }) @@ -414,65 +435,71 @@ describe('Offcanvas', () => { expect(EventHandler.trigger).not.toHaveBeenCalled() }) - it('should hide a shown element', done => { - fixtureEl.innerHTML = '
' + it('should hide a shown element', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvasEl).not.toHaveClass('show') - expect(offCanvas._backdrop.hide).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvasEl).not.toHaveClass('show') + expect(offCanvas._backdrop.hide).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should not fire hidden when hide is prevented', done => { - fixtureEl.innerHTML = '
' + it('should not fire hidden when hide is prevented', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._backdrop, 'hide').and.callThrough() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._backdrop, 'hide').and.callThrough() - offCanvas.show() + offCanvas.show() - const expectEnd = () => { - setTimeout(() => { - expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() - done() - }, 10) - } + const expectEnd = () => { + setTimeout(() => { + expect(offCanvas._backdrop.hide).not.toHaveBeenCalled() + resolve() + }, 10) + } - offCanvasEl.addEventListener('hide.bs.offcanvas', event => { - event.preventDefault() - expectEnd() - }) + offCanvasEl.addEventListener('hide.bs.offcanvas', event => { + event.preventDefault() + expectEnd() + }) - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - throw new Error('should not fire hidden event') - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + throw new Error('should not fire hidden event') + }) - offCanvas.hide() + offCanvas.hide() + }) }) - it('should release focus trap', done => { - fixtureEl.innerHTML = '
' + it('should release focus trap', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '
' - const offCanvasEl = fixtureEl.querySelector('div') - const offCanvas = new Offcanvas(offCanvasEl) - spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() - offCanvas.show() + const offCanvasEl = fixtureEl.querySelector('div') + const offCanvas = new Offcanvas(offCanvasEl) + spyOn(offCanvas._focustrap, 'deactivate').and.callThrough() + offCanvas.show() - offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { - expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() - done() - }) + offCanvasEl.addEventListener('hidden.bs.offcanvas', () => { + expect(offCanvas._focustrap.deactivate).toHaveBeenCalled() + resolve() + }) - offCanvas.hide() + offCanvas.hide() + }) }) }) @@ -502,22 +529,24 @@ describe('Offcanvas', () => { }) describe('data-api', () => { - it('should not prevent event for input', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') + it('should not prevent event for input', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') - const target = fixtureEl.querySelector('input') - const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') + const target = fixtureEl.querySelector('input') + const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1') - offCanvasEl.addEventListener('shown.bs.offcanvas', () => { - expect(offCanvasEl).toHaveClass('show') - expect(target.checked).toBeTrue() - done() - }) + offCanvasEl.addEventListener('shown.bs.offcanvas', () => { + expect(offCanvasEl).toHaveClass('show') + expect(target.checked).toBeTrue() + resolve() + }) - target.click() + target.click() + }) }) it('should not call toggle on disabled elements', () => { @@ -535,76 +564,82 @@ describe('Offcanvas', () => { expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled() }) - it('should call hide first, if another offcanvas is open', done => { - fixtureEl.innerHTML = [ - '', - '
', - '
' - ].join('') - - const trigger2 = fixtureEl.querySelector('#btn2') - const offcanvasEl1 = document.querySelector('#offcanvas1') - const offcanvasEl2 = document.querySelector('#offcanvas2') - const offcanvas1 = new Offcanvas(offcanvasEl1) - - offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { - trigger2.click() + it('should call hide first, if another offcanvas is open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
', + '
' + ].join('') + + const trigger2 = fixtureEl.querySelector('#btn2') + const offcanvasEl1 = document.querySelector('#offcanvas1') + const offcanvasEl2 = document.querySelector('#offcanvas2') + const offcanvas1 = new Offcanvas(offcanvasEl1) + + offcanvasEl1.addEventListener('shown.bs.offcanvas', () => { + trigger2.click() + }) + offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { + expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() + resolve() + }) + offcanvas1.show() + }) + }) + + it('should focus on trigger element after closing offcanvas', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(trigger.focus).toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() + }) + }) + + it('should not focus on trigger element after closing offcanvas, if it is not visible', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = [ + '', + '
' + ].join('') + + const trigger = fixtureEl.querySelector('#btn') + const offcanvasEl = fixtureEl.querySelector('#offcanvas') + const offcanvas = new Offcanvas(offcanvasEl) + spyOn(trigger, 'focus') + + offcanvasEl.addEventListener('shown.bs.offcanvas', () => { + trigger.style.display = 'none' + offcanvas.hide() + }) + offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { + setTimeout(() => { + expect(isVisible(trigger)).toBeFalse() + expect(trigger.focus).not.toHaveBeenCalled() + resolve() + }, 5) + }) + + trigger.click() }) - offcanvasEl1.addEventListener('hidden.bs.offcanvas', () => { - expect(Offcanvas.getInstance(offcanvasEl2)).not.toBeNull() - done() - }) - offcanvas1.show() - }) - - it('should focus on trigger element after closing offcanvas', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - offcanvas.hide() - }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(trigger.focus).toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() - }) - - it('should not focus on trigger element after closing offcanvas, if it is not visible', done => { - fixtureEl.innerHTML = [ - '', - '
' - ].join('') - - const trigger = fixtureEl.querySelector('#btn') - const offcanvasEl = fixtureEl.querySelector('#offcanvas') - const offcanvas = new Offcanvas(offcanvasEl) - spyOn(trigger, 'focus') - - offcanvasEl.addEventListener('shown.bs.offcanvas', () => { - trigger.style.display = 'none' - offcanvas.hide() - }) - offcanvasEl.addEventListener('hidden.bs.offcanvas', () => { - setTimeout(() => { - expect(isVisible(trigger)).toBeFalse() - expect(trigger.focus).not.toHaveBeenCalled() - done() - }, 5) - }) - - trigger.click() }) }) diff --git a/js/tests/unit/orange-navbar.spec.js b/js/tests/unit/orange-navbar.spec.js new file mode 100644 index 0000000000..1d2ba9e939 --- /dev/null +++ b/js/tests/unit/orange-navbar.spec.js @@ -0,0 +1,115 @@ +import OrangeNavbar from '../../src/orange-navbar' +import { clearFixture, getFixture, createEvent } from '../helpers/fixture' + +describe('OrangeNavbar', () => { + let fixtureEl + + beforeAll(() => { + fixtureEl = getFixture() + }) + + afterEach(() => { + clearFixture() + }) + + describe('VERSION', () => { + it('should return plugin version', () => { + expect(OrangeNavbar.VERSION).toEqual(jasmine.any(String)) + }) + }) + + describe('Default', () => { + it('should return plugin default config', () => { + expect(OrangeNavbar.Default).toEqual(jasmine.any(Object)) + }) + }) + + describe('DefaultType', () => { + it('should return plugin default type config', () => { + expect(OrangeNavbar.DefaultType).toEqual(jasmine.any(Object)) + }) + }) + + describe('DATA_KEY', () => { + it('should return plugin data key', () => { + expect(OrangeNavbar.DATA_KEY).toEqual('bs.orangenavbar') + }) + }) + + it('should add .header-minimized to the global header non-supra first