aboutsummaryrefslogtreecommitdiff
path: root/js/tests
diff options
context:
space:
mode:
Diffstat (limited to 'js/tests')
-rw-r--r--js/tests/browsers.js14
-rw-r--r--js/tests/helpers/fixture.js9
-rw-r--r--js/tests/integration/bundle-modularity.js2
-rw-r--r--js/tests/integration/bundle.js1
-rw-r--r--js/tests/integration/index.html10
-rw-r--r--js/tests/integration/rollup.bundle.js5
-rw-r--r--js/tests/karma.conf.js55
-rw-r--r--js/tests/unit/.eslintrc.json14
-rw-r--r--js/tests/unit/alert.spec.js17
-rw-r--r--js/tests/unit/button.spec.js16
-rw-r--r--js/tests/unit/carousel.spec.js228
-rw-r--r--js/tests/unit/collapse.spec.js46
-rw-r--r--js/tests/unit/dom/data.spec.js151
-rw-r--r--js/tests/unit/dom/event-handler.spec.js92
-rw-r--r--js/tests/unit/dom/selector-engine.spec.js8
-rw-r--r--js/tests/unit/dropdown.spec.js773
-rw-r--r--js/tests/unit/jquery.spec.js2
-rw-r--r--js/tests/unit/modal.spec.js159
-rw-r--r--js/tests/unit/offcanvas.spec.js705
-rw-r--r--js/tests/unit/popover.spec.js6
-rw-r--r--js/tests/unit/scrollspy.spec.js53
-rw-r--r--js/tests/unit/tab.spec.js292
-rw-r--r--js/tests/unit/toast.spec.js28
-rw-r--r--js/tests/unit/tooltip.spec.js221
-rw-r--r--js/tests/unit/util/backdrop.spec.js241
-rw-r--r--js/tests/unit/util/index.spec.js169
-rw-r--r--js/tests/unit/util/sanitizer.spec.js10
-rw-r--r--js/tests/unit/util/scrollbar.spec.js261
-rw-r--r--js/tests/visual/alert.html1
-rw-r--r--js/tests/visual/button.html1
-rw-r--r--js/tests/visual/carousel.html19
-rw-r--r--js/tests/visual/collapse.html1
-rw-r--r--js/tests/visual/dropdown.html20
-rw-r--r--js/tests/visual/modal.html5
-rw-r--r--js/tests/visual/popover.html3
-rw-r--r--js/tests/visual/scrollspy.html1
-rw-r--r--js/tests/visual/tab.html68
-rw-r--r--js/tests/visual/toast.html1
-rw-r--r--js/tests/visual/tooltip.html3
39 files changed, 3086 insertions, 625 deletions
diff --git a/js/tests/browsers.js b/js/tests/browsers.js
index 8df94cc53..b8e47a364 100644
--- a/js/tests/browsers.js
+++ b/js/tests/browsers.js
@@ -5,21 +5,21 @@ const browsers = {
safariMac: {
base: 'BrowserStack',
os: 'OS X',
- os_version: 'High Sierra',
+ os_version: 'Catalina',
browser: 'Safari',
browser_version: 'latest'
},
chromeMac: {
base: 'BrowserStack',
os: 'OS X',
- os_version: 'High Sierra',
+ os_version: 'Catalina',
browser: 'Chrome',
browser_version: 'latest'
},
firefoxMac: {
base: 'BrowserStack',
os: 'OS X',
- os_version: 'High Sierra',
+ os_version: 'Catalina',
browser: 'Firefox',
browser_version: 'latest'
},
@@ -40,15 +40,15 @@ const browsers = {
iphone7: {
base: 'BrowserStack',
os: 'ios',
- os_version: '10.0',
+ os_version: '12.0',
device: 'iPhone 7',
real_mobile: true
},
- iphone11: {
+ iphone12: {
base: 'BrowserStack',
os: 'ios',
- os_version: '13.0',
- device: 'iPhone 11',
+ os_version: '14.0',
+ device: 'iPhone 12',
real_mobile: true
},
pixel2: {
diff --git a/js/tests/helpers/fixture.js b/js/tests/helpers/fixture.js
index 0bfc26f46..3d6f395e8 100644
--- a/js/tests/helpers/fixture.js
+++ b/js/tests/helpers/fixture.js
@@ -39,3 +39,12 @@ export const jQueryMock = {
})
}
}
+
+export const clearBodyAndDocument = () => {
+ const attributes = ['data-bs-padding-right', 'style']
+
+ attributes.forEach(attr => {
+ document.documentElement.removeAttribute(attr)
+ document.body.removeAttribute(attr)
+ })
+}
diff --git a/js/tests/integration/bundle-modularity.js b/js/tests/integration/bundle-modularity.js
index 003f84021..8546141b1 100644
--- a/js/tests/integration/bundle-modularity.js
+++ b/js/tests/integration/bundle-modularity.js
@@ -1,5 +1,5 @@
-import 'popper.js'
import Tooltip from '../../dist/tooltip'
+import '../../dist/carousel'
window.addEventListener('load', () => {
[].concat(...document.querySelectorAll('[data-bs-toggle="tooltip"]'))
diff --git a/js/tests/integration/bundle.js b/js/tests/integration/bundle.js
index 75982f76f..452088a7d 100644
--- a/js/tests/integration/bundle.js
+++ b/js/tests/integration/bundle.js
@@ -1,4 +1,3 @@
-import 'popper.js'
import { Tooltip } from '../../../dist/js/bootstrap.esm.js'
window.addEventListener('load', () => {
diff --git a/js/tests/integration/index.html b/js/tests/integration/index.html
index 9855d5d34..4c71bad91 100644
--- a/js/tests/integration/index.html
+++ b/js/tests/integration/index.html
@@ -20,11 +20,11 @@
</button>
<div id="carouselExampleIndicators" class="carousel slide mt-2" data-bs-ride="carousel">
- <ol class="carousel-indicators">
- <li data-bs-target="#carouselExampleIndicators" data-bs-slide-to="0"></li>
- <li data-bs-target="#carouselExampleIndicators" data-bs-slide-to="1" class="active"></li>
- <li data-bs-target="#carouselExampleIndicators" data-bs-slide-to="2"></li>
- </ol>
+ <div class="carousel-indicators">
+ <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="0" aria-label="Slide 1"></button>
+ <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="1" class="active" aria-current="true" aria-label="Slide 2"></button>
+ <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="2" aria-label="Slide 3"></button>
+ </div>
<div class="carousel-inner">
<div class="carousel-item">
diff --git a/js/tests/integration/rollup.bundle.js b/js/tests/integration/rollup.bundle.js
index 9e2ed26c1..caddcab48 100644
--- a/js/tests/integration/rollup.bundle.js
+++ b/js/tests/integration/rollup.bundle.js
@@ -2,6 +2,7 @@
const { babel } = require('@rollup/plugin-babel')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
+const replace = require('@rollup/plugin-replace')
module.exports = {
input: 'js/tests/integration/bundle.js',
@@ -10,6 +11,10 @@ module.exports = {
format: 'iife'
},
plugins: [
+ replace({
+ 'process.env.NODE_ENV': '"production"',
+ preventAssignment: true
+ }),
nodeResolve(),
babel({
exclude: 'node_modules/**',
diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js
index 0728a8cfa..67b60f15e 100644
--- a/js/tests/karma.conf.js
+++ b/js/tests/karma.conf.js
@@ -1,20 +1,24 @@
/* eslint-env node */
+'use strict'
+
const path = require('path')
const ip = require('ip')
const { babel } = require('@rollup/plugin-babel')
const istanbul = require('rollup-plugin-istanbul')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
+const replace = require('@rollup/plugin-replace')
const {
browsers,
browsersKeys
} = require('./browsers')
-const { env } = process
-const browserStack = env.BROWSER === 'true'
-const debug = env.DEBUG === 'true'
-const jQueryTest = env.JQUERY === 'true'
+const ENV = process.env
+const BROWSERSTACK = Boolean(ENV.BROWSERSTACK)
+const DEBUG = Boolean(ENV.DEBUG)
+const JQUERY_TEST = Boolean(ENV.JQUERY)
+
const frameworks = [
'jasmine'
]
@@ -29,36 +33,34 @@ const reporters = ['dots']
const detectBrowsers = {
usePhantomJS: false,
postDetection(availableBrowser) {
- if (env.CI === true || availableBrowser.includes('Chrome')) {
- return debug ? ['Chrome'] : ['ChromeHeadless']
+ // On CI just use Chrome
+ if (ENV.CI === true) {
+ return ['ChromeHeadless']
+ }
+
+ if (availableBrowser.includes('Chrome')) {
+ return DEBUG ? ['Chrome'] : ['ChromeHeadless']
}
if (availableBrowser.includes('Chromium')) {
- return debug ? ['Chromium'] : ['ChromiumHeadless']
+ return DEBUG ? ['Chromium'] : ['ChromiumHeadless']
}
if (availableBrowser.includes('Firefox')) {
- return debug ? ['Firefox'] : ['FirefoxHeadless']
+ return DEBUG ? ['Firefox'] : ['FirefoxHeadless']
}
throw new Error('Please install Chrome, Chromium or Firefox')
}
}
-const customLaunchers = {
- FirefoxHeadless: {
- base: 'Firefox',
- flags: ['-headless']
- }
-}
-
const conf = {
basePath: '../..',
port: 9876,
colors: true,
autoWatch: false,
singleRun: true,
- concurrency: Infinity,
+ concurrency: Number.POSITIVE_INFINITY,
client: {
clearContext: false
},
@@ -66,7 +68,7 @@ const conf = {
'node_modules/hammer-simulator/index.js',
{
pattern: 'js/tests/unit/**/!(jquery).spec.js',
- watched: !browserStack
+ watched: !BROWSERSTACK
}
],
preprocessors: {
@@ -74,6 +76,10 @@ const conf = {
},
rollupPreprocessor: {
plugins: [
+ replace({
+ 'process.env.NODE_ENV': '"dev"',
+ preventAssignment: true
+ }),
istanbul({
exclude: [
'node_modules/**',
@@ -97,11 +103,11 @@ const conf = {
}
}
-if (browserStack) {
+if (BROWSERSTACK) {
conf.hostname = ip.address()
conf.browserStack = {
- username: env.BROWSER_STACK_USERNAME,
- accessKey: env.BROWSER_STACK_ACCESS_KEY,
+ username: ENV.BROWSER_STACK_USERNAME,
+ accessKey: ENV.BROWSER_STACK_ACCESS_KEY,
build: `bootstrap-${new Date().toISOString()}`,
project: 'Bootstrap',
retryLimit: 2
@@ -110,14 +116,13 @@ if (browserStack) {
conf.customLaunchers = browsers
conf.browsers = browsersKeys
reporters.push('BrowserStack', 'kjhtml')
-} else if (jQueryTest) {
+} else if (JQUERY_TEST) {
frameworks.push('detectBrowsers')
plugins.push(
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-detect-browsers'
)
- conf.customLaunchers = customLaunchers
conf.detectBrowsers = detectBrowsers
conf.files = [
'node_modules/jquery/dist/jquery.slim.min.js',
@@ -135,7 +140,6 @@ if (browserStack) {
'karma-coverage-istanbul-reporter'
)
reporters.push('coverage-istanbul')
- conf.customLaunchers = customLaunchers
conf.detectBrowsers = detectBrowsers
conf.coverageIstanbulReporter = {
dir: path.resolve(__dirname, '../coverage/'),
@@ -151,7 +155,7 @@ if (browserStack) {
}
}
- if (debug) {
+ if (DEBUG) {
conf.hostname = ip.address()
plugins.push('karma-jasmine-html-reporter')
reporters.push('kjhtml')
@@ -165,7 +169,6 @@ conf.plugins = plugins
conf.reporters = reporters
module.exports = karmaConfig => {
- // possible values: karmaConfig.LOG_DISABLE || karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN || karmaConfig.LOG_INFO || karmaConfig.LOG_DEBUG
- conf.logLevel = karmaConfig.LOG_ERROR || karmaConfig.LOG_WARN
+ conf.logLevel = karmaConfig.LOG_ERROR
karmaConfig.set(conf)
}
diff --git a/js/tests/unit/.eslintrc.json b/js/tests/unit/.eslintrc.json
index e7f8d5d2a..adde3105d 100644
--- a/js/tests/unit/.eslintrc.json
+++ b/js/tests/unit/.eslintrc.json
@@ -1,16 +1,8 @@
{
- "root": true,
"extends": [
"../../../.eslintrc.json"
],
- "overrides": [
- {
- "files": [
- "**/*.spec.js"
- ],
- "env": {
- "jasmine": true
- }
- }
- ]
+ "env": {
+ "jasmine": true
+ }
}
diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js
index a1322f1c7..194da420e 100644
--- a/js/tests/unit/alert.spec.js
+++ b/js/tests/unit/alert.spec.js
@@ -15,10 +15,27 @@ describe('Alert', () => {
clearFixture()
})
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<div class="alert"></div>'
+
+ const alertEl = fixtureEl.querySelector('.alert')
+ const alertBySelector = new Alert('.alert')
+ const alertByElement = new Alert(alertEl)
+
+ expect(alertBySelector._element).toEqual(alertEl)
+ expect(alertByElement._element).toEqual(alertEl)
+ })
+
it('should return version', () => {
expect(typeof Alert.VERSION).toEqual('string')
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Alert.DATA_KEY).toEqual('bs.alert')
+ })
+ })
+
describe('data-api', () => {
it('should close an alert without instantiating it manually', () => {
fixtureEl.innerHTML = [
diff --git a/js/tests/unit/button.spec.js b/js/tests/unit/button.spec.js
index 51aa73774..e7d92cb6d 100644
--- a/js/tests/unit/button.spec.js
+++ b/js/tests/unit/button.spec.js
@@ -18,12 +18,28 @@ describe('Button', () => {
clearFixture()
})
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<button data-bs-toggle="button">Placeholder</button>'
+ const buttonEl = fixtureEl.querySelector('[data-bs-toggle="button"]')
+ const buttonBySelector = new Button('[data-bs-toggle="button"]')
+ const buttonByElement = new Button(buttonEl)
+
+ expect(buttonBySelector._element).toEqual(buttonEl)
+ expect(buttonByElement._element).toEqual(buttonEl)
+ })
+
describe('VERSION', () => {
it('should return plugin version', () => {
expect(Button.VERSION).toEqual(jasmine.any(String))
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Button.DATA_KEY).toEqual('bs.button')
+ })
+ })
+
describe('data-api', () => {
it('should toggle active class on click', () => {
fixtureEl.innerHTML = [
diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js
index 07b8fc311..75c3bbd6d 100644
--- a/js/tests/unit/carousel.spec.js
+++ b/js/tests/unit/carousel.spec.js
@@ -2,7 +2,8 @@ import Carousel from '../../src/carousel'
import EventHandler from '../../src/dom/event-handler'
/** Test helpers */
-import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
+import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
+import * as util from '../../src/util'
describe('Carousel', () => {
const { Simulator, PointerEvent } = window
@@ -45,7 +46,24 @@ describe('Carousel', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Carousel.DATA_KEY).toEqual('bs.carousel')
+ })
+ })
+
describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<div id="myCarousel" class="carousel slide"></div>'
+
+ const carouselEl = fixtureEl.querySelector('#myCarousel')
+ const carouselBySelector = new Carousel('#myCarousel')
+ const carouselByElement = new Carousel(carouselEl)
+
+ expect(carouselBySelector._element).toEqual(carouselEl)
+ expect(carouselByElement._element).toEqual(carouselEl)
+ })
+
it('should go to next item if right arrow key is pressed', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
@@ -158,8 +176,7 @@ describe('Carousel', () => {
})
const spyKeydown = spyOn(carousel, '_keydown').and.callThrough()
- const spyPrev = spyOn(carousel, 'prev')
- const spyNext = spyOn(carousel, 'next')
+ const spySlide = spyOn(carousel, '_slide')
const keydown = createEvent('keydown', { bubbles: true, cancelable: true })
keydown.key = 'ArrowRight'
@@ -172,12 +189,10 @@ describe('Carousel', () => {
input.dispatchEvent(keydown)
expect(spyKeydown).toHaveBeenCalled()
- expect(spyPrev).not.toHaveBeenCalled()
- expect(spyNext).not.toHaveBeenCalled()
+ expect(spySlide).not.toHaveBeenCalled()
spyKeydown.calls.reset()
- spyPrev.calls.reset()
- spyNext.calls.reset()
+ spySlide.calls.reset()
Object.defineProperty(keydown, 'target', {
value: textarea
@@ -185,8 +200,7 @@ describe('Carousel', () => {
textarea.dispatchEvent(keydown)
expect(spyKeydown).toHaveBeenCalled()
- expect(spyPrev).not.toHaveBeenCalled()
- expect(spyNext).not.toHaveBeenCalled()
+ expect(spySlide).not.toHaveBeenCalled()
})
it('should wrap around from end to start when wrap option is true', done => {
@@ -295,13 +309,15 @@ describe('Carousel', () => {
spyOn(Carousel.prototype, '_addTouchEventListeners')
+ // Headless browser does not support touch events, so need to fake it
+ // to test that touch events are add properly.
document.documentElement.ontouchstart = () => {}
const carousel = new Carousel(carouselEl)
expect(carousel._addTouchEventListeners).toHaveBeenCalled()
})
- it('should allow swiperight and call prev with pointer events', done => {
+ it('should allow swiperight and call _slide (prev) with pointer events', done => {
if (!supportPointerEvent) {
expect().nothing()
done()
@@ -329,11 +345,12 @@ describe('Carousel', () => {
const item = fixtureEl.querySelector('#item')
const carousel = new Carousel(carouselEl)
- spyOn(carousel, 'prev').and.callThrough()
+ spyOn(carousel, '_slide').and.callThrough()
- carouselEl.addEventListener('slid.bs.carousel', () => {
+ carouselEl.addEventListener('slid.bs.carousel', event => {
expect(item.classList.contains('active')).toEqual(true)
- expect(carousel.prev).toHaveBeenCalled()
+ expect(carousel._slide).toHaveBeenCalledWith('right')
+ expect(event.direction).toEqual('right')
document.head.removeChild(stylesCarousel)
delete document.documentElement.ontouchstart
done()
@@ -373,11 +390,12 @@ describe('Carousel', () => {
const item = fixtureEl.querySelector('#item')
const carousel = new Carousel(carouselEl)
- spyOn(carousel, 'next').and.callThrough()
+ spyOn(carousel, '_slide').and.callThrough()
- carouselEl.addEventListener('slid.bs.carousel', () => {
+ carouselEl.addEventListener('slid.bs.carousel', event => {
expect(item.classList.contains('active')).toEqual(false)
- expect(carousel.next).toHaveBeenCalled()
+ expect(carousel._slide).toHaveBeenCalledWith('left')
+ expect(event.direction).toEqual('left')
document.head.removeChild(stylesCarousel)
delete document.documentElement.ontouchstart
done()
@@ -390,7 +408,7 @@ describe('Carousel', () => {
})
})
- it('should allow swiperight and call prev with touch events', done => {
+ it('should allow swiperight and call _slide (prev) with touch events', done => {
Simulator.setType('touch')
clearPointerEvents()
document.documentElement.ontouchstart = () => {}
@@ -412,11 +430,12 @@ describe('Carousel', () => {
const item = fixtureEl.querySelector('#item')
const carousel = new Carousel(carouselEl)
- spyOn(carousel, 'prev').and.callThrough()
+ spyOn(carousel, '_slide').and.callThrough()
- carouselEl.addEventListener('slid.bs.carousel', () => {
+ carouselEl.addEventListener('slid.bs.carousel', event => {
expect(item.classList.contains('active')).toEqual(true)
- expect(carousel.prev).toHaveBeenCalled()
+ expect(carousel._slide).toHaveBeenCalledWith('right')
+ expect(event.direction).toEqual('right')
delete document.documentElement.ontouchstart
restorePointerEvents()
done()
@@ -428,7 +447,7 @@ describe('Carousel', () => {
})
})
- it('should allow swipeleft and call next with touch events', done => {
+ it('should allow swipeleft and call _slide (next) with touch events', done => {
Simulator.setType('touch')
clearPointerEvents()
document.documentElement.ontouchstart = () => {}
@@ -450,11 +469,12 @@ describe('Carousel', () => {
const item = fixtureEl.querySelector('#item')
const carousel = new Carousel(carouselEl)
- spyOn(carousel, 'next').and.callThrough()
+ spyOn(carousel, '_slide').and.callThrough()
- carouselEl.addEventListener('slid.bs.carousel', () => {
+ carouselEl.addEventListener('slid.bs.carousel', event => {
expect(item.classList.contains('active')).toEqual(false)
- expect(carousel.next).toHaveBeenCalled()
+ expect(carousel._slide).toHaveBeenCalledWith('left')
+ expect(event.direction).toEqual('left')
delete document.documentElement.ontouchstart
restorePointerEvents()
done()
@@ -659,11 +679,11 @@ describe('Carousel', () => {
it('should update indicators if present', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
- ' <ol class="carousel-indicators">',
- ' <li data-bs-target="#myCarousel" data-bs-slide-to="0" class="active"></li>',
- ' <li id="secondIndicator" data-bs-target="#myCarousel" data-bs-slide-to="1"></li>',
- ' <li data-bs-target="#myCarousel" data-bs-slide-to="2"></li>',
- ' </ol>',
+ ' <div class="carousel-indicators">',
+ ' <button type="button" id="firstIndicator" data-bs-target="myCarousel" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>',
+ ' <button type="button" id="secondIndicator" data-bs-target="myCarousel" data-bs-slide-to="1" aria-label="Slide 2"></button>',
+ ' <button type="button" data-bs-target="myCarousel" data-bs-slide-to="2" aria-label="Slide 3"></button>',
+ ' </div>',
' <div class="carousel-inner">',
' <div class="carousel-item active">item 1</div>',
' <div class="carousel-item" data-bs-interval="7">item 2</div>',
@@ -673,11 +693,15 @@ describe('Carousel', () => {
].join('')
const carouselEl = fixtureEl.querySelector('#myCarousel')
+ const firstIndicator = fixtureEl.querySelector('#firstIndicator')
const secondIndicator = fixtureEl.querySelector('#secondIndicator')
const carousel = new Carousel(carouselEl)
carouselEl.addEventListener('slid.bs.carousel', () => {
+ expect(firstIndicator.classList.contains('active')).toEqual(false)
+ expect(firstIndicator.hasAttribute('aria-current')).toEqual(false)
expect(secondIndicator.classList.contains('active')).toEqual(true)
+ expect(secondIndicator.getAttribute('aria-current')).toEqual('true')
done()
})
@@ -905,7 +929,7 @@ describe('Carousel', () => {
})
describe('to', () => {
- it('should go directement to the provided index', done => {
+ it('should go directly to the provided index', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
' <div class="carousel-inner">',
@@ -1038,6 +1062,77 @@ describe('Carousel', () => {
})
})
})
+ describe('rtl function', () => {
+ it('"_directionToOrder" and "_orderToDirection" must return the right results', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const carouselEl = fixtureEl.querySelector('div')
+ const carousel = new Carousel(carouselEl, {})
+
+ expect(carousel._directionToOrder('left')).toEqual('next')
+ expect(carousel._directionToOrder('prev')).toEqual('prev')
+ expect(carousel._directionToOrder('right')).toEqual('prev')
+ expect(carousel._directionToOrder('next')).toEqual('next')
+
+ expect(carousel._orderToDirection('next')).toEqual('left')
+ expect(carousel._orderToDirection('prev')).toEqual('right')
+ })
+
+ it('"_directionToOrder" and "_orderToDirection" must return the right results when rtl=true', () => {
+ document.documentElement.dir = 'rtl'
+ fixtureEl.innerHTML = '<div></div>'
+
+ const carouselEl = fixtureEl.querySelector('div')
+ const carousel = new Carousel(carouselEl, {})
+ expect(util.isRTL()).toEqual(true, 'rtl has to be true')
+
+ expect(carousel._directionToOrder('left')).toEqual('prev')
+ expect(carousel._directionToOrder('prev')).toEqual('prev')
+ expect(carousel._directionToOrder('right')).toEqual('next')
+ expect(carousel._directionToOrder('next')).toEqual('next')
+
+ expect(carousel._orderToDirection('next')).toEqual('right')
+ expect(carousel._orderToDirection('prev')).toEqual('left')
+ document.documentElement.dir = 'ltl'
+ })
+
+ it('"_slide" has to call _directionToOrder and "_orderToDirection"', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const carouselEl = fixtureEl.querySelector('div')
+ const carousel = new Carousel(carouselEl, {})
+ const spy = spyOn(carousel, '_directionToOrder').and.callThrough()
+ const spy2 = spyOn(carousel, '_orderToDirection').and.callThrough()
+
+ carousel._slide('left')
+ expect(spy).toHaveBeenCalledWith('left')
+ expect(spy2).toHaveBeenCalledWith('next')
+
+ carousel._slide('right')
+ expect(spy).toHaveBeenCalledWith('right')
+ expect(spy2).toHaveBeenCalledWith('prev')
+ })
+
+ it('"_slide" has to call "_directionToOrder" and "_orderToDirection" when rtl=true', () => {
+ document.documentElement.dir = 'rtl'
+ fixtureEl.innerHTML = '<div></div>'
+
+ const carouselEl = fixtureEl.querySelector('div')
+ const carousel = new Carousel(carouselEl, {})
+ const spy = spyOn(carousel, '_directionToOrder').and.callThrough()
+ const spy2 = spyOn(carousel, '_orderToDirection').and.callThrough()
+
+ carousel._slide('left')
+ expect(spy).toHaveBeenCalledWith('left')
+ expect(spy2).toHaveBeenCalledWith('prev')
+
+ carousel._slide('right')
+ expect(spy).toHaveBeenCalledWith('right')
+ expect(spy2).toHaveBeenCalledWith('next')
+
+ document.documentElement.dir = 'ltl'
+ })
+ })
describe('dispose', () => {
it('should destroy a carousel', () => {
@@ -1052,13 +1147,38 @@ describe('Carousel', () => {
].join('')
const carouselEl = fixtureEl.querySelector('#myCarousel')
+ const addEventSpy = spyOn(carouselEl, 'addEventListener').and.callThrough()
+ const removeEventSpy = spyOn(carouselEl, 'removeEventListener').and.callThrough()
+
+ // Headless browser does not support touch events, so need to fake it
+ // to test that touch events are add/removed properly.
+ document.documentElement.ontouchstart = () => {}
+
const carousel = new Carousel(carouselEl)
- spyOn(EventHandler, 'off').and.callThrough()
+ const expectedArgs = [
+ ['keydown', jasmine.any(Function), jasmine.any(Boolean)],
+ ['mouseover', jasmine.any(Function), jasmine.any(Boolean)],
+ ['mouseout', jasmine.any(Function), jasmine.any(Boolean)],
+ ...(carousel._pointerEvent ?
+ [
+ ['pointerdown', jasmine.any(Function), jasmine.any(Boolean)],
+ ['pointerup', jasmine.any(Function), jasmine.any(Boolean)]
+ ] :
+ [
+ ['touchstart', jasmine.any(Function), jasmine.any(Boolean)],
+ ['touchmove', jasmine.any(Function), jasmine.any(Boolean)],
+ ['touchend', jasmine.any(Function), jasmine.any(Boolean)]
+ ])
+ ]
+
+ expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs)
carousel.dispose()
- expect(EventHandler.off).toHaveBeenCalled()
+ expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs)
+
+ delete document.documentElement.ontouchstart
})
})
@@ -1136,11 +1256,9 @@ describe('Carousel', () => {
jQueryMock.fn.carousel = Carousel.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.carousel.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
@@ -1156,7 +1274,31 @@ describe('Carousel', () => {
expect(Carousel.getInstance(carouselEl)).toBeDefined()
})
- it('should create carousel and go to the next slide on click', done => {
+ it('should create carousel and go to the next slide on click (with real button controls)', done => {
+ fixtureEl.innerHTML = [
+ '<div id="myCarousel" class="carousel slide">',
+ ' <div class="carousel-inner">',
+ ' <div class="carousel-item active">item 1</div>',
+ ' <div id="item2" class="carousel-item">item 2</div>',
+ ' <div class="carousel-item">item 3</div>',
+ ' </div>',
+ ' <button class="carousel-control-prev" data-bs-target="#myCarousel" type="button" data-bs-slide="prev"></button>',
+ ' <button id="next" class="carousel-control-next" data-bs-target="#myCarousel" type="button" data-bs-slide="next"></div>',
+ '</div>'
+ ].join('')
+
+ const next = fixtureEl.querySelector('#next')
+ const item2 = fixtureEl.querySelector('#item2')
+
+ next.click()
+
+ setTimeout(() => {
+ expect(item2.classList.contains('active')).toEqual(true)
+ done()
+ }, 10)
+ })
+
+ it('should create carousel and go to the next slide on click (using links as controls)', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
' <div class="carousel-inner">',
@@ -1164,8 +1306,8 @@ describe('Carousel', () => {
' <div id="item2" class="carousel-item">item 2</div>',
' <div class="carousel-item">item 3</div>',
' </div>',
- ' <div class="carousel-control-prev" data-bs-target="#myCarousel" role="button" data-bs-slide="prev"></div>',
- ' <div id="next" class="carousel-control-next" data-bs-target="#myCarousel" role="button" data-bs-slide="next"></div>',
+ ' <a class="carousel-control-prev" href="#myCarousel" role="button" data-bs-slide="prev"></button>',
+ ' <a id="next" class="carousel-control-next" href="#myCarousel" role="button" data-bs-slide="next"></div>',
'</div>'
].join('')
@@ -1211,8 +1353,8 @@ describe('Carousel', () => {
' <div class="carousel-item">item 2</div>',
' <div class="carousel-item">item 3</div>',
' </div>',
- ' <div class="carousel-control-prev" data-bs-target="#myCarousel" role="button" data-bs-slide="prev"></div>',
- ' <div id="next" class="carousel-control-next" role="button" data-bs-slide="next"></div>',
+ ' <button class="carousel-control-prev" data-bs-target="#myCarousel" type="button" data-bs-slide="prev"></button>',
+ ' <button id="next" class="carousel-control-next" type="button" data-bs-slide="next"></button>',
'</div>'
].join('')
@@ -1231,8 +1373,8 @@ describe('Carousel', () => {
' <div id="item2" class="carousel-item">item 2</div>',
' <div class="carousel-item">item 3</div>',
' </div>',
- ' <div class="carousel-control-prev" data-bs-target="#myCarousel" role="button" data-bs-slide="prev"></div>',
- ' <div id="next" class="carousel-control-next" data-bs-target="#myCarousel" role="button" data-bs-slide="next"></div>',
+ ' <button class="carousel-control-prev" data-bs-target="#myCarousel" type="button" data-bs-slide="prev"></div>',
+ ' <button id="next" class="carousel-control-next" data-bs-target="#myCarousel" type="button" data-bs-slide="next"></div>',
'</div>'
].join('')
diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js
index d53ab5964..bc7c15771 100644
--- a/js/tests/unit/collapse.spec.js
+++ b/js/tests/unit/collapse.spec.js
@@ -27,7 +27,24 @@ describe('Collapse', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Collapse.DATA_KEY).toEqual('bs.collapse')
+ })
+ })
+
describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<div class="my-collapse"></div>'
+
+ const collapseEl = fixtureEl.querySelector('div.my-collapse')
+ const collapseBySelector = new Collapse('div.my-collapse')
+ const collapseByElement = new Collapse(collapseEl)
+
+ expect(collapseBySelector._element).toEqual(collapseEl)
+ expect(collapseByElement._element).toEqual(collapseEl)
+ })
+
it('should allow jquery object in parent config', () => {
fixtureEl.innerHTML = [
'<div class="my-collapse">',
@@ -374,6 +391,29 @@ describe('Collapse', () => {
})
describe('data-api', () => {
+ it('should prevent url change if click on nested elements', done => {
+ fixtureEl.innerHTML = [
+ '<a role="button" data-bs-toggle="collapse" class="collapsed" href="#collapse">',
+ ' <span id="nested"></span>',
+ '</a>',
+ '<div id="collapse" class="collapse"></div>'
+ ].join('')
+
+ const triggerEl = fixtureEl.querySelector('a')
+ const nestedTriggerEl = fixtureEl.querySelector('#nested')
+
+ spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+ triggerEl.addEventListener('click', event => {
+ expect(event.target.isEqualNode(nestedTriggerEl)).toEqual(true)
+ expect(event.delegateTarget.isEqualNode(triggerEl)).toEqual(true)
+ expect(Event.prototype.preventDefault).toHaveBeenCalled()
+ done()
+ })
+
+ nestedTriggerEl.click()
+ })
+
it('should show multiple collapsed elements', done => {
fixtureEl.innerHTML = [
'<a role="button" data-bs-toggle="collapse" class="collapsed" href=".multi"></a>',
@@ -796,11 +836,9 @@ describe('Collapse', () => {
jQueryMock.fn.collapse = Collapse.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.collapse.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
diff --git a/js/tests/unit/dom/data.spec.js b/js/tests/unit/dom/data.spec.js
index c80f32db0..a00d3b734 100644
--- a/js/tests/unit/dom/data.spec.js
+++ b/js/tests/unit/dom/data.spec.js
@@ -4,128 +4,103 @@ import Data from '../../../src/dom/data'
import { getFixture, clearFixture } from '../../helpers/fixture'
describe('Data', () => {
+ const TEST_KEY = 'bs.test'
+ const UNKNOWN_KEY = 'bs.unknown'
+ const TEST_DATA = {
+ test: 'bsData'
+ }
+
let fixtureEl
+ let div
beforeAll(() => {
fixtureEl = getFixture()
})
+ beforeEach(() => {
+ fixtureEl.innerHTML = '<div></div>'
+ div = fixtureEl.querySelector('div')
+ })
+
afterEach(() => {
+ Data.remove(div, TEST_KEY)
clearFixture()
})
- describe('setData', () => {
- it('should set data in an element by adding a bsKey attribute', () => {
- fixtureEl.innerHTML = '<div></div>'
-
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
-
- Data.setData(div, 'test', data)
- expect(div.bsKey).toBeDefined()
- })
+ it('should return null for unknown elements', () => {
+ const data = { ...TEST_DATA }
- it('should change data if something is already stored', () => {
- fixtureEl.innerHTML = '<div></div>'
+ Data.set(div, TEST_KEY, data)
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
-
- Data.setData(div, 'test', data)
-
- data.test = 'bsData2'
- Data.setData(div, 'test', data)
-
- expect(div.bsKey).toBeDefined()
- })
+ expect(Data.get(null)).toBeNull()
+ expect(Data.get(undefined)).toBeNull()
+ expect(Data.get(document.createElement('div'), TEST_KEY)).toBeNull()
})
- describe('getData', () => {
- it('should return stored data', () => {
- fixtureEl.innerHTML = '<div></div>'
-
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
+ it('should return null for unknown keys', () => {
+ const data = { ...TEST_DATA }
- Data.setData(div, 'test', data)
- expect(Data.getData(div, 'test')).toEqual(data)
- })
+ Data.set(div, TEST_KEY, data)
- it('should return null on undefined element', () => {
- expect(Data.getData(null)).toEqual(null)
- expect(Data.getData(undefined)).toEqual(null)
- })
-
- it('should return null when an element have nothing stored', () => {
- fixtureEl.innerHTML = '<div></div>'
-
- const div = fixtureEl.querySelector('div')
-
- expect(Data.getData(div, 'test')).toEqual(null)
- })
-
- it('should return null when an element have nothing stored with the provided key', () => {
- fixtureEl.innerHTML = '<div></div>'
+ expect(Data.get(div, null)).toBeNull()
+ expect(Data.get(div, undefined)).toBeNull()
+ expect(Data.get(div, UNKNOWN_KEY)).toBeNull()
+ })
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
+ it('should store data for an element with a given key and return it', () => {
+ const data = { ...TEST_DATA }
- Data.setData(div, 'test', data)
+ Data.set(div, TEST_KEY, data)
- expect(Data.getData(div, 'test2')).toEqual(null)
- })
+ expect(Data.get(div, TEST_KEY)).toBe(data)
})
- describe('removeData', () => {
- it('should do nothing when an element have nothing stored', () => {
- fixtureEl.innerHTML = '<div></div>'
+ it('should overwrite data if something is already stored', () => {
+ const data = { ...TEST_DATA }
+ const copy = { ...data }
- const div = fixtureEl.querySelector('div')
+ Data.set(div, TEST_KEY, data)
+ Data.set(div, TEST_KEY, copy)
- Data.removeData(div, 'test')
- expect().nothing()
- })
+ expect(Data.get(div, TEST_KEY)).not.toBe(data)
+ expect(Data.get(div, TEST_KEY)).toBe(copy)
+ })
- it('should should do nothing if it\'s not a valid key provided', () => {
- fixtureEl.innerHTML = '<div></div>'
+ it('should do nothing when an element have nothing stored', () => {
+ Data.remove(div, TEST_KEY)
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
+ expect().nothing()
+ })
- Data.setData(div, 'test', data)
+ it('should remove nothing for an unknown key', () => {
+ const data = { ...TEST_DATA }
- expect(div.bsKey).toBeDefined()
+ Data.set(div, TEST_KEY, data)
+ Data.remove(div, UNKNOWN_KEY)
- Data.removeData(div, 'test2')
+ expect(Data.get(div, TEST_KEY)).toBe(data)
+ })
- expect(div.bsKey).toBeDefined()
- })
+ it('should remove data for a given key', () => {
+ const data = { ...TEST_DATA }
- it('should remove data if something is stored', () => {
- fixtureEl.innerHTML = '<div></div>'
+ Data.set(div, TEST_KEY, data)
+ Data.remove(div, TEST_KEY)
- const div = fixtureEl.querySelector('div')
- const data = {
- test: 'bsData'
- }
+ expect(Data.get(div, TEST_KEY)).toBeNull()
+ })
- Data.setData(div, 'test', data)
+ it('should console.error a message if called with multiple keys', () => {
+ /* eslint-disable no-console */
+ console.error = jasmine.createSpy('console.error')
- expect(div.bsKey).toBeDefined()
+ const data = { ...TEST_DATA }
+ const copy = { ...data }
- Data.removeData(div, 'test')
+ Data.set(div, TEST_KEY, data)
+ Data.set(div, UNKNOWN_KEY, copy)
- expect(div.bsKey).toBeUndefined()
- })
+ expect(console.error).toHaveBeenCalled()
+ expect(Data.get(div, UNKNOWN_KEY)).toBe(null)
})
})
diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js
index e596a49b5..4dc4ffc35 100644
--- a/js/tests/unit/dom/event-handler.spec.js
+++ b/js/tests/unit/dom/event-handler.spec.js
@@ -77,10 +77,78 @@ describe('EventHandler', () => {
div.click()
})
+
+ it('should handle mouseenter/mouseleave like the native counterpart', done => {
+ fixtureEl.innerHTML = [
+ '<div class="outer">',
+ '<div class="inner">',
+ '<div class="nested">',
+ '<div class="deep"></div>',
+ '</div>',
+ '</div>',
+ '<div class="sibling"></div>',
+ '</div>'
+ ]
+
+ const outer = fixtureEl.querySelector('.outer')
+ const inner = fixtureEl.querySelector('.inner')
+ const nested = fixtureEl.querySelector('.nested')
+ const deep = fixtureEl.querySelector('.deep')
+ const sibling = fixtureEl.querySelector('.sibling')
+
+ const enterSpy = jasmine.createSpy('mouseenter')
+ const leaveSpy = jasmine.createSpy('mouseleave')
+ const delegateEnterSpy = jasmine.createSpy('mouseenter')
+ const delegateLeaveSpy = jasmine.createSpy('mouseleave')
+
+ EventHandler.on(inner, 'mouseenter', enterSpy)
+ EventHandler.on(inner, 'mouseleave', leaveSpy)
+ EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
+ EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
+
+ EventHandler.on(sibling, 'mouseenter', () => {
+ expect(enterSpy.calls.count()).toBe(2)
+ expect(leaveSpy.calls.count()).toBe(2)
+ expect(delegateEnterSpy.calls.count()).toBe(2)
+ expect(delegateLeaveSpy.calls.count()).toBe(2)
+ done()
+ })
+
+ const moveMouse = (from, to) => {
+ from.dispatchEvent(new MouseEvent('mouseout', {
+ bubbles: true,
+ relatedTarget: to
+ }))
+
+ to.dispatchEvent(new MouseEvent('mouseover', {
+ bubbles: true,
+ relatedTarget: from
+ }))
+ }
+
+ // from outer to deep and back to outer (nested)
+ moveMouse(outer, inner)
+ moveMouse(inner, nested)
+ moveMouse(nested, deep)
+ moveMouse(deep, nested)
+ moveMouse(nested, inner)
+ moveMouse(inner, outer)
+
+ setTimeout(() => {
+ expect(enterSpy.calls.count()).toBe(1)
+ expect(leaveSpy.calls.count()).toBe(1)
+ expect(delegateEnterSpy.calls.count()).toBe(1)
+ expect(delegateLeaveSpy.calls.count()).toBe(1)
+
+ // from outer to inner to sibling (adjacent)
+ moveMouse(outer, inner)
+ moveMouse(inner, sibling)
+ }, 20)
+ })
})
describe('one', () => {
- it('should call listener just one', done => {
+ it('should call listener just once', done => {
fixtureEl.innerHTML = '<div></div>'
let called = 0
@@ -101,6 +169,28 @@ describe('EventHandler', () => {
done()
}, 20)
})
+
+ it('should call delegated listener just once', done => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ let called = 0
+ const div = fixtureEl.querySelector('div')
+ const obj = {
+ oneListener() {
+ called++
+ }
+ }
+
+ EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
+
+ EventHandler.trigger(div, 'bootstrap')
+ EventHandler.trigger(div, 'bootstrap')
+
+ setTimeout(() => {
+ expect(called).toEqual(1)
+ done()
+ }, 20)
+ })
})
describe('off', () => {
diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js
index 781d0ce1b..d108a2efb 100644
--- a/js/tests/unit/dom/selector-engine.spec.js
+++ b/js/tests/unit/dom/selector-engine.spec.js
@@ -14,14 +14,6 @@ describe('SelectorEngine', () => {
clearFixture()
})
- describe('matches', () => {
- it('should return matched elements', () => {
- fixtureEl.innerHTML = '<div></div>'
-
- expect(SelectorEngine.matches(fixtureEl, 'div')).toEqual(true)
- })
- })
-
describe('find', () => {
it('should find elements', () => {
fixtureEl.innerHTML = '<div></div>'
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js
index f6a5feb1b..b0f225140 100644
--- a/js/tests/unit/dropdown.spec.js
+++ b/js/tests/unit/dropdown.spec.js
@@ -1,7 +1,6 @@
-import Popper from 'popper.js'
-
import Dropdown from '../../src/dropdown'
import EventHandler from '../../src/dom/event-handler'
+import { noop } from '../../src/util'
/** Test helpers */
import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
@@ -35,30 +34,52 @@ describe('Dropdown', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Dropdown.DATA_KEY).toEqual('bs.dropdown')
+ })
+ })
+
describe('constructor', () => {
- it('should create offset modifier correctly when offset option is a function', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
' <div class="dropdown-menu">',
- ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' <a class="dropdown-item" href="#">Link</a>',
' </div>',
'</div>'
].join('')
- const getOffset = offsets => offsets
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = new Dropdown(btnDropdown, {
- offset: getOffset
- })
+ const dropdownBySelector = new Dropdown('[data-bs-toggle="dropdown"]')
+ const dropdownByElement = new Dropdown(btnDropdown)
- const offset = dropdown._getOffset()
+ expect(dropdownBySelector._element).toEqual(btnDropdown)
+ expect(dropdownByElement._element).toEqual(btnDropdown)
+ })
+
+ it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
- expect(offset.offset).toBeUndefined()
- expect(typeof offset.fn).toEqual('function')
+ const btnDropdown = fixtureEl.querySelector('.btn')
+ const dropdown = new Dropdown(btnDropdown)
+
+ spyOn(dropdown, 'toggle')
+
+ btnDropdown.click()
+
+ expect(dropdown.toggle).toHaveBeenCalled()
})
- it('should create offset modifier correctly when offset option is not a function', () => {
+ it('should create offset modifier correctly when offset option is a function', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
@@ -68,36 +89,42 @@ describe('Dropdown', () => {
'</div>'
].join('')
- const myOffset = 7
+ const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20])
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdown = new Dropdown(btnDropdown, {
- offset: myOffset
+ offset: getOffset,
+ popperConfig: {
+ onFirstUpdate: state => {
+ expect(getOffset).toHaveBeenCalledWith({
+ popper: state.rects.popper,
+ reference: state.rects.reference,
+ placement: state.placement
+ }, btnDropdown)
+ done()
+ }
+ }
})
-
const offset = dropdown._getOffset()
- expect(offset.offset).toEqual(myOffset)
- expect(offset.fn).toBeUndefined()
+ expect(typeof offset).toEqual('function')
+
+ dropdown.show()
})
- it('should add a listener on trigger which do not have data-bs-toggle="dropdown"', () => {
+ it('should create offset modifier correctly when offset option is a string into data attribute', () => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
- ' <button class="btn">Dropdown</button>',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-offset="10,20">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Secondary link</a>',
' </div>',
'</div>'
].join('')
- const btnDropdown = fixtureEl.querySelector('.btn')
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
const dropdown = new Dropdown(btnDropdown)
- spyOn(dropdown, 'toggle')
-
- btnDropdown.click()
-
- expect(dropdown.toggle).toHaveBeenCalled()
+ expect(dropdown._getOffset()).toEqual([10, 20])
})
it('should allow to pass config to Popper with `popperConfig`', () => {
@@ -121,6 +148,28 @@ describe('Dropdown', () => {
expect(popperConfig.placement).toEqual('left')
})
+
+ it('should allow to pass config to Popper with `popperConfig` as a function', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-placement="right" >Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' })
+ const dropdown = new Dropdown(btnDropdown, {
+ popperConfig: getPopperConfig
+ })
+
+ const popperConfig = dropdown._getPopperConfig()
+
+ expect(getPopperConfig).toHaveBeenCalled()
+ expect(popperConfig.placement).toEqual('left')
+ })
})
describe('toggle', () => {
@@ -135,10 +184,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
@@ -168,18 +216,17 @@ describe('Dropdown', () => {
const firstDropdownEl = fixtureEl.querySelector('.first')
const secondDropdownEl = fixtureEl.querySelector('.second')
const dropdown1 = new Dropdown(btnDropdown1)
- const dropdown2 = new Dropdown(btnDropdown2)
firstDropdownEl.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown1.classList.contains('show')).toEqual(true)
spyOn(dropdown1._popper, 'destroy')
- dropdown2.toggle()
+ btnDropdown2.click()
})
- secondDropdownEl.addEventListener('shown.bs.dropdown', () => {
+ secondDropdownEl.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
expect(dropdown1._popper.destroy).toHaveBeenCalled()
done()
- })
+ }))
dropdown1.toggle()
})
@@ -196,25 +243,24 @@ describe('Dropdown', () => {
const defaultValueOnTouchStart = document.documentElement.ontouchstart
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
document.documentElement.ontouchstart = () => {}
spyOn(EventHandler, 'on')
spyOn(EventHandler, 'off')
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
- expect(EventHandler.on).toHaveBeenCalled()
+ expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
dropdown.toggle()
})
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(false)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
- expect(EventHandler.off).toHaveBeenCalled()
+ expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
document.documentElement.ontouchstart = defaultValueOnTouchStart
done()
@@ -234,10 +280,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
@@ -349,12 +394,11 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown, {
reference: 'parent'
})
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
@@ -374,12 +418,11 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown, {
reference: fixtureEl
})
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
@@ -399,12 +442,11 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown, {
reference: { 0: fixtureEl, jquery: 'jQuery' }
})
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
done()
@@ -413,6 +455,58 @@ describe('Dropdown', () => {
dropdown.toggle()
})
+ it('should toggle a dropdown with a valid virtual element reference', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle visually-hidden" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const virtualElement = {
+ getBoundingClientRect() {
+ return {
+ width: 0,
+ height: 0,
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ }
+ }
+ }
+
+ expect(() => new Dropdown(btnDropdown, {
+ reference: {}
+ })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.')
+
+ expect(() => new Dropdown(btnDropdown, {
+ reference: {
+ getBoundingClientRect: 'not-a-function'
+ }
+ })).toThrowError(TypeError, 'DROPDOWN: Option "reference" provided type "object" without a required "getBoundingClientRect" method.')
+
+ // use onFirstUpdate as Poppers internal update is executed async
+ const dropdown = new Dropdown(btnDropdown, {
+ reference: virtualElement,
+ popperConfig: {
+ onFirstUpdate() {
+ expect(virtualElement.getBoundingClientRect).toHaveBeenCalled()
+ expect(btnDropdown.classList.contains('show')).toEqual(true)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
+ done()
+ }
+ }
+ })
+
+ spyOn(virtualElement, 'getBoundingClientRect').and.callThrough()
+
+ dropdown.toggle()
+ })
+
it('should not toggle a dropdown if the element is disabled', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
@@ -424,10 +518,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -450,10 +543,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -476,10 +568,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -502,14 +593,13 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('show.bs.dropdown', e => {
+ btnDropdown.addEventListener('show.bs.dropdown', e => {
e.preventDefault()
})
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -534,10 +624,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
done()
})
@@ -556,10 +645,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -582,10 +670,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -608,10 +695,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -634,14 +720,13 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('show.bs.dropdown', e => {
+ btnDropdown.addEventListener('show.bs.dropdown', e => {
e.preventDefault()
})
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
throw new Error('should not throw shown.bs.dropdown event')
})
@@ -658,7 +743,7 @@ describe('Dropdown', () => {
it('should hide a dropdown', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
- ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="true">Dropdown</button>',
' <div class="dropdown-menu show">',
' <a class="dropdown-item" href="#">Secondary link</a>',
' </div>',
@@ -666,12 +751,12 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(dropdownMenu.classList.contains('show')).toEqual(false)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
done()
})
@@ -689,15 +774,14 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
spyOn(dropdown._popper, 'destroy')
dropdown.hide()
})
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(dropdown._popper.destroy).toHaveBeenCalled()
done()
})
@@ -716,11 +800,10 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
throw new Error('should not throw hidden.bs.dropdown event')
})
@@ -743,11 +826,10 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
throw new Error('should not throw hidden.bs.dropdown event')
})
@@ -770,10 +852,9 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
throw new Error('should not throw hidden.bs.dropdown event')
})
@@ -796,15 +877,14 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('hide.bs.dropdown', e => {
+ btnDropdown.addEventListener('hide.bs.dropdown', e => {
e.preventDefault()
})
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
throw new Error('should not throw hidden.bs.dropdown event')
})
@@ -815,6 +895,39 @@ describe('Dropdown', () => {
done()
})
})
+
+ it('should remove event listener on touch-enabled device that was added in show method', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Dropdwon item</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const defaultValueOnTouchStart = document.documentElement.ontouchstart
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = new Dropdown(btnDropdown)
+
+ document.documentElement.ontouchstart = () => {}
+ spyOn(EventHandler, 'off')
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ dropdown.hide()
+ })
+
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
+ expect(btnDropdown.classList.contains('show')).toEqual(false)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
+ expect(EventHandler.off).toHaveBeenCalled()
+
+ document.documentElement.ontouchstart = defaultValueOnTouchStart
+ done()
+ })
+
+ dropdown.show()
+ })
})
describe('dispose', () => {
@@ -829,16 +942,21 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ spyOn(btnDropdown, 'addEventListener').and.callThrough()
+ spyOn(btnDropdown, 'removeEventListener').and.callThrough()
+
const dropdown = new Dropdown(btnDropdown)
expect(dropdown._popper).toBeNull()
expect(dropdown._menu).toBeDefined()
expect(dropdown._element).toBeDefined()
+ expect(btnDropdown.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
dropdown.dispose()
expect(dropdown._menu).toBeNull()
expect(dropdown._element).toBeNull()
+ expect(btnDropdown.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
})
it('should dispose dropdown with Popper', () => {
@@ -860,14 +978,11 @@ describe('Dropdown', () => {
expect(dropdown._menu).toBeDefined()
expect(dropdown._element).toBeDefined()
- spyOn(Popper.prototype, 'destroy')
-
dropdown.dispose()
expect(dropdown._popper).toBeNull()
expect(dropdown._menu).toBeNull()
expect(dropdown._element).toBeNull()
- expect(Popper.prototype.destroy).toHaveBeenCalled()
})
})
@@ -889,12 +1004,12 @@ describe('Dropdown', () => {
expect(dropdown._popper).toBeDefined()
- spyOn(dropdown._popper, 'scheduleUpdate')
+ spyOn(dropdown._popper, 'update')
spyOn(dropdown, '_detectNavbar')
dropdown.update()
- expect(dropdown._popper.scheduleUpdate).toHaveBeenCalled()
+ expect(dropdown._popper.update).toHaveBeenCalled()
expect(dropdown._detectNavbar).toHaveBeenCalled()
})
@@ -921,10 +1036,10 @@ describe('Dropdown', () => {
})
describe('data-api', () => {
- it('should not add class position-static to dropdown if boundary not set', done => {
+ it('should show and hide a dropdown', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
- ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
' <div class="dropdown-menu">',
' <a class="dropdown-item" href="#">Secondary link</a>',
' </div>',
@@ -932,80 +1047,103 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
+ let showEventTriggered = false
+ let hideEventTriggered = false
+
+ btnDropdown.addEventListener('show.bs.dropdown', () => {
+ showEventTriggered = true
+ })
+
+ btnDropdown.addEventListener('shown.bs.dropdown', e => setTimeout(() => {
+ expect(btnDropdown.classList.contains('show')).toEqual(true)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
+ expect(showEventTriggered).toEqual(true)
+ expect(e.relatedTarget).toEqual(btnDropdown)
+ document.body.click()
+ }))
+
+ btnDropdown.addEventListener('hide.bs.dropdown', () => {
+ hideEventTriggered = true
+ })
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
- expect(dropdownEl.classList.contains('position-static')).toEqual(false)
+ btnDropdown.addEventListener('hidden.bs.dropdown', e => {
+ expect(btnDropdown.classList.contains('show')).toEqual(false)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
+ expect(hideEventTriggered).toEqual(true)
+ expect(e.relatedTarget).toEqual(btnDropdown)
done()
})
btnDropdown.click()
})
- it('should add class position-static to dropdown if boundary not scrollParent', done => {
+ it('should not use Popper in navbar', done => {
fixtureEl.innerHTML = [
- '<div class="dropdown">',
- ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-boundary="viewport">Dropdown</button>',
- ' <div class="dropdown-menu">',
- ' <a class="dropdown-item" href="#">Secondary link</a>',
+ '<nav class="navbar navbar-expand-md navbar-light bg-light">',
+ ' <div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
' </div>',
- '</div>'
+ '</nav>'
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
- expect(dropdownEl.classList.contains('position-static')).toEqual(true)
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdown._popper).toBeNull()
+ expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper')
done()
})
- btnDropdown.click()
+ dropdown.show()
})
- it('should show and hide a dropdown', done => {
+ it('should not collapse the dropdown when clicking a select option nested in the dropdown', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</button>',
' <div class="dropdown-menu">',
- ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' <select>',
+ ' <option selected>Open this select menu</option>',
+ ' <option value="1">One</option>',
+ ' </select>',
' </div>',
'</div>'
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
- let showEventTriggered = false
- let hideEventTriggered = false
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('show.bs.dropdown', () => {
- showEventTriggered = true
- })
+ const hideSpy = spyOn(dropdown, '_completeHide')
- dropdownEl.addEventListener('shown.bs.dropdown', e => {
- expect(btnDropdown.classList.contains('show')).toEqual(true)
- expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
- expect(showEventTriggered).toEqual(true)
- expect(e.relatedTarget).toEqual(btnDropdown)
- document.body.click()
- })
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ const clickEvent = new MouseEvent('click', {
+ bubbles: true
+ })
- dropdownEl.addEventListener('hide.bs.dropdown', () => {
- hideEventTriggered = true
+ dropdownMenu.querySelector('option').dispatchEvent(clickEvent)
})
- dropdownEl.addEventListener('hidden.bs.dropdown', e => {
- expect(btnDropdown.classList.contains('show')).toEqual(false)
- expect(btnDropdown.getAttribute('aria-expanded')).toEqual('false')
- expect(hideEventTriggered).toEqual(true)
- expect(e.relatedTarget).toEqual(btnDropdown)
- done()
+ dropdownMenu.addEventListener('click', event => {
+ expect(event.target.tagName).toMatch(/select|option/i)
+
+ Dropdown.clearMenus(event)
+
+ setTimeout(() => {
+ expect(hideSpy).not.toHaveBeenCalled()
+ done()
+ }, 10)
})
- btnDropdown.click()
+ dropdown.show()
})
- it('should not use Popper in navbar', done => {
+ it('should manage bs attribute `data-bs-popper`="none" when dropdown is in navbar', done => {
fixtureEl.innerHTML = [
'<nav class="navbar navbar-expand-md navbar-light bg-light">',
' <div class="dropdown">',
@@ -1018,15 +1156,20 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown)
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
- expect(dropdownMenu.getAttribute('style')).toEqual(null, 'no inline style applied by Popper')
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('none')
+ dropdown.hide()
+ })
+
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
+ expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
done()
})
- btnDropdown.click()
+ dropdown.show()
})
it('should not use Popper if display set to static', done => {
@@ -1040,18 +1183,44 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
// Popper adds this attribute when we use it
- expect(dropdownMenu.getAttribute('x-placement')).toEqual(null)
+ expect(dropdownMenu.getAttribute('data-popper-placement')).toEqual(null)
done()
})
btnDropdown.click()
})
+ it('should manage bs attribute `data-bs-popper`="static" when display set to static', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-display="static">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+ const dropdown = new Dropdown(btnDropdown)
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
+ expect(dropdownMenu.getAttribute('data-bs-popper')).toEqual('static')
+ dropdown.hide()
+ })
+
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
+ expect(dropdownMenu.getAttribute('data-bs-popper')).toBeNull()
+ done()
+ })
+
+ dropdown.show()
+ })
+
it('should remove "show" class if tabbing outside of menu', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
@@ -1063,9 +1232,8 @@ describe('Dropdown', () => {
].join('')
const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdownEl = fixtureEl.querySelector('.dropdown')
- dropdownEl.addEventListener('shown.bs.dropdown', () => {
+ btnDropdown.addEventListener('shown.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(true)
const keyup = createEvent('keyup')
@@ -1074,7 +1242,7 @@ describe('Dropdown', () => {
document.dispatchEvent(keyup)
})
- dropdownEl.addEventListener('hidden.bs.dropdown', () => {
+ btnDropdown.addEventListener('hidden.bs.dropdown', () => {
expect(btnDropdown.classList.contains('show')).toEqual(false)
done()
})
@@ -1105,34 +1273,31 @@ describe('Dropdown', () => {
expect(triggerDropdownList.length).toEqual(2)
- const first = triggerDropdownList[0]
- const last = triggerDropdownList[1]
- const dropdownTestMenu = first.parentNode
- const btnGroup = last.parentNode
+ const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList
- dropdownTestMenu.addEventListener('shown.bs.dropdown', () => {
- expect(first.classList.contains('show')).toEqual(true)
+ triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => {
+ expect(triggerDropdownFirst.classList.contains('show')).toEqual(true)
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1)
document.body.click()
})
- dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => {
+ triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => {
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0)
- last.click()
+ triggerDropdownLast.click()
})
- btnGroup.addEventListener('shown.bs.dropdown', () => {
- expect(last.classList.contains('show')).toEqual(true)
+ triggerDropdownLast.addEventListener('shown.bs.dropdown', () => {
+ expect(triggerDropdownLast.classList.contains('show')).toEqual(true)
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1)
document.body.click()
})
- btnGroup.addEventListener('hidden.bs.dropdown', () => {
+ triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => {
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0)
done()
})
- first.click()
+ triggerDropdownFirst.click()
})
it('should remove "show" class if body if tabbing outside of menu, with multiple dropdowns', done => {
@@ -1156,13 +1321,10 @@ describe('Dropdown', () => {
expect(triggerDropdownList.length).toEqual(2)
- const first = triggerDropdownList[0]
- const last = triggerDropdownList[1]
- const dropdownTestMenu = first.parentNode
- const btnGroup = last.parentNode
+ const [triggerDropdownFirst, triggerDropdownLast] = triggerDropdownList
- dropdownTestMenu.addEventListener('shown.bs.dropdown', () => {
- expect(first.classList.contains('show')).toEqual(true, '"show" class added on click')
+ triggerDropdownFirst.addEventListener('shown.bs.dropdown', () => {
+ expect(triggerDropdownFirst.classList.contains('show')).toEqual(true, '"show" class added on click')
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown')
const keyup = createEvent('keyup')
@@ -1171,13 +1333,13 @@ describe('Dropdown', () => {
document.dispatchEvent(keyup)
})
- dropdownTestMenu.addEventListener('hidden.bs.dropdown', () => {
+ triggerDropdownFirst.addEventListener('hidden.bs.dropdown', () => {
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed')
- last.click()
+ triggerDropdownLast.click()
})
- btnGroup.addEventListener('shown.bs.dropdown', () => {
- expect(last.classList.contains('show')).toEqual(true, '"show" class added on click')
+ triggerDropdownLast.addEventListener('shown.bs.dropdown', () => {
+ expect(triggerDropdownLast.classList.contains('show')).toEqual(true, '"show" class added on click')
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(1, 'only one dropdown is shown')
const keyup = createEvent('keyup')
@@ -1186,12 +1348,12 @@ describe('Dropdown', () => {
document.dispatchEvent(keyup)
})
- btnGroup.addEventListener('hidden.bs.dropdown', () => {
+ triggerDropdownLast.addEventListener('hidden.bs.dropdown', () => {
expect(fixtureEl.querySelectorAll('.dropdown-menu.show').length).toEqual(0, '"show" class removed')
done()
})
- first.click()
+ triggerDropdownFirst.click()
})
it('should fire hide and hidden event without a clickEvent if event type is not click', done => {
@@ -1205,18 +1367,17 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
- dropdown.addEventListener('hide.bs.dropdown', e => {
+ triggerDropdown.addEventListener('hide.bs.dropdown', e => {
expect(e.clickEvent).toBeUndefined()
})
- dropdown.addEventListener('hidden.bs.dropdown', e => {
+ triggerDropdown.addEventListener('hidden.bs.dropdown', e => {
expect(e.clickEvent).toBeUndefined()
done()
})
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydown = createEvent('keydown')
keydown.key = 'Escape'
@@ -1226,6 +1387,42 @@ describe('Dropdown', () => {
triggerDropdown.click()
})
+ it('should bubble up the events to the parent elements', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#subMenu">Sub menu</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdownParent = fixtureEl.querySelector('.dropdown')
+ const dropdown = new Dropdown(triggerDropdown)
+
+ const showFunction = jasmine.createSpy('showFunction')
+ dropdownParent.addEventListener('show.bs.dropdown', showFunction)
+
+ const shownFunction = jasmine.createSpy('shownFunction')
+ dropdownParent.addEventListener('shown.bs.dropdown', () => {
+ shownFunction()
+ dropdown.hide()
+ })
+
+ const hideFunction = jasmine.createSpy('hideFunction')
+ dropdownParent.addEventListener('hide.bs.dropdown', hideFunction)
+
+ dropdownParent.addEventListener('hidden.bs.dropdown', () => {
+ expect(showFunction).toHaveBeenCalled()
+ expect(shownFunction).toHaveBeenCalled()
+ expect(hideFunction).toHaveBeenCalled()
+ done()
+ })
+
+ dropdown.show()
+ })
+
it('should ignore keyboard events within <input>s and <textarea>s', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
@@ -1239,11 +1436,10 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const input = fixtureEl.querySelector('input')
const textarea = fixtureEl.querySelector('textarea')
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
input.focus()
const keydown = createEvent('keydown')
@@ -1275,9 +1471,8 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydown = createEvent('keydown')
keydown.key = 'ArrowDown'
@@ -1311,9 +1506,8 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydown = createEvent('keydown')
keydown.key = 'ArrowDown'
@@ -1341,11 +1535,10 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const item1 = fixtureEl.querySelector('#item1')
const item2 = fixtureEl.querySelector('#item2')
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydownArrowDown = createEvent('keydown')
keydownArrowDown.key = 'ArrowDown'
@@ -1379,10 +1572,9 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const item1 = fixtureEl.querySelector('#item1')
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
const keydown = createEvent('keydown')
keydown.key = 'ArrowUp'
@@ -1406,7 +1598,6 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const input = fixtureEl.querySelector('input')
input.addEventListener('click', () => {
@@ -1414,7 +1605,7 @@ describe('Dropdown', () => {
done()
})
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
input.dispatchEvent(createEvent('click'))
})
@@ -1433,7 +1624,6 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const textarea = fixtureEl.querySelector('textarea')
textarea.addEventListener('click', () => {
@@ -1441,7 +1631,7 @@ describe('Dropdown', () => {
done()
})
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
expect(triggerDropdown.classList.contains('show')).toEqual(true, 'dropdown menu is shown')
textarea.dispatchEvent(createEvent('click'))
})
@@ -1462,7 +1652,6 @@ describe('Dropdown', () => {
].join('')
const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
- const dropdown = fixtureEl.querySelector('.dropdown')
const input = fixtureEl.querySelector('input')
const textarea = fixtureEl.querySelector('textarea')
@@ -1478,7 +1667,7 @@ describe('Dropdown', () => {
const keydownEscape = createEvent('keydown')
keydownEscape.key = 'Escape'
- dropdown.addEventListener('shown.bs.dropdown', () => {
+ triggerDropdown.addEventListener('shown.bs.dropdown', () => {
// Key Space
input.focus()
input.dispatchEvent(keydownSpace)
@@ -1557,6 +1746,133 @@ describe('Dropdown', () => {
done()
}, 20)
})
+
+ it('should propagate escape key events if dropdown is closed', done => {
+ fixtureEl.innerHTML = [
+ '<div class="parent">',
+ ' <div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Some Item</a>',
+ ' </div>',
+ ' </div>',
+ '</div>'
+ ]
+
+ 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()
+ 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)
+ })
+
+ it('should close dropdown (only) by clicking inside the dropdown menu when it has data-attribute `data-bs-auto-close="inside"`', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="inside">Dropdown toggle</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Dropdown item</a>',
+ ' </div>',
+ '</div>'
+ ]
+
+ const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+ const expectDropdownToBeOpened = () => setTimeout(() => {
+ expect(dropdownToggle.classList.contains('show')).toEqual(true)
+ dropdownMenu.click()
+ }, 150)
+
+ dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+ document.documentElement.click()
+ expectDropdownToBeOpened()
+ })
+
+ dropdownToggle.addEventListener('hidden.bs.dropdown', () => setTimeout(() => {
+ expect(dropdownToggle.classList.contains('show')).toEqual(false)
+ done()
+ }))
+
+ dropdownToggle.click()
+ })
+
+ it('should close dropdown (only) by clicking outside the dropdown menu when it has data-attribute `data-bs-auto-close="outside"`', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="outside">Dropdown toggle</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Dropdown item</a>',
+ ' </div>',
+ '</div>'
+ ]
+
+ const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+ const expectDropdownToBeOpened = () => setTimeout(() => {
+ expect(dropdownToggle.classList.contains('show')).toEqual(true)
+ document.documentElement.click()
+ }, 150)
+
+ dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+ dropdownMenu.click()
+ expectDropdownToBeOpened()
+ })
+
+ dropdownToggle.addEventListener('hidden.bs.dropdown', () => {
+ expect(dropdownToggle.classList.contains('show')).toEqual(false)
+ done()
+ })
+
+ 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 => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown" data-bs-auto-close="false">Dropdown toggle</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Dropdown item</a>',
+ ' </div>',
+ '</div>'
+ ]
+
+ const dropdownToggle = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdownMenu = fixtureEl.querySelector('.dropdown-menu')
+
+ const expectDropdownToBeOpened = (shouldTriggerClick = true) => setTimeout(() => {
+ expect(dropdownToggle.classList.contains('show')).toEqual(true)
+ if (shouldTriggerClick) {
+ document.documentElement.click()
+ } else {
+ done()
+ }
+
+ expectDropdownToBeOpened(false)
+ }, 150)
+
+ dropdownToggle.addEventListener('shown.bs.dropdown', () => {
+ dropdownMenu.click()
+ expectDropdownToBeOpened()
+ })
+
+ dropdownToggle.click()
+ })
})
describe('jQueryInterface', () => {
@@ -1596,11 +1912,9 @@ describe('Dropdown', () => {
jQueryMock.fn.dropdown = Dropdown.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.dropdown.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
@@ -1623,4 +1937,99 @@ describe('Dropdown', () => {
expect(Dropdown.getInstance(div)).toEqual(null)
})
})
+
+ it('should open dropdown when pressing keydown or keyup', done => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item disabled" href="#sub1">Submenu 1</a>',
+ ' <button class="dropdown-item" type="button" disabled>Disabled button</button>',
+ ' <a id="item1" class="dropdown-item" href="#">Another link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const triggerDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const dropdown = fixtureEl.querySelector('.dropdown')
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'ArrowDown'
+
+ const keyup = createEvent('keyup')
+ keyup.key = 'ArrowUp'
+
+ const handleArrowDown = () => {
+ expect(triggerDropdown.classList.contains('show')).toEqual(true)
+ expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true')
+ setTimeout(() => {
+ dropdown.hide()
+ keydown.key = 'ArrowUp'
+ triggerDropdown.dispatchEvent(keyup)
+ }, 20)
+ }
+
+ const handleArrowUp = () => {
+ expect(triggerDropdown.classList.contains('show')).toEqual(true)
+ expect(triggerDropdown.getAttribute('aria-expanded')).toEqual('true')
+ done()
+ }
+
+ 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', () => {
+ fixtureEl.innerHTML = [
+ '<div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown">Dropdown</button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#">Secondary link</a>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const clickListener = jasmine.createSpy('clickListener')
+ const delegatedClickListener = jasmine.createSpy('delegatedClickListener')
+
+ btnDropdown.addEventListener('click', clickListener)
+ document.addEventListener('click', delegatedClickListener)
+
+ btnDropdown.click()
+
+ expect(clickListener).toHaveBeenCalled()
+ expect(delegatedClickListener).toHaveBeenCalled()
+ })
+
+ it('should open the dropdown when clicking the child element inside `data-bs-toggle="dropdown"`', done => {
+ fixtureEl.innerHTML = [
+ '<div class="container">',
+ ' <div class="dropdown">',
+ ' <button class="btn dropdown-toggle" data-bs-toggle="dropdown"><span id="childElement">Dropdown</span></button>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#subMenu">Sub menu</a>',
+ ' </div>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const btnDropdown = fixtureEl.querySelector('[data-bs-toggle="dropdown"]')
+ const childElement = fixtureEl.querySelector('#childElement')
+
+ btnDropdown.addEventListener('shown.bs.dropdown', () => setTimeout(() => {
+ expect(btnDropdown.classList.contains('show')).toEqual(true)
+ expect(btnDropdown.getAttribute('aria-expanded')).toEqual('true')
+ done()
+ }))
+
+ childElement.click()
+ })
})
diff --git a/js/tests/unit/jquery.spec.js b/js/tests/unit/jquery.spec.js
index 6198cdb85..289612df5 100644
--- a/js/tests/unit/jquery.spec.js
+++ b/js/tests/unit/jquery.spec.js
@@ -52,6 +52,6 @@ describe('jQuery', () => {
done()
})
- $(fixtureEl).find('button').click()
+ $(fixtureEl).find('button').trigger('click')
})
})
diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js
index f645e9892..a09711b34 100644
--- a/js/tests/unit/modal.spec.js
+++ b/js/tests/unit/modal.spec.js
@@ -1,47 +1,30 @@
import Modal from '../../src/modal'
import EventHandler from '../../src/dom/event-handler'
+import { getWidth as getScrollBarWidth } from '../../src/util/scrollbar'
/** Test helpers */
-import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
+import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
describe('Modal', () => {
let fixtureEl
- let style
beforeAll(() => {
fixtureEl = getFixture()
-
- // Enable the scrollbar measurer
- const css = '.modal-scrollbar-measure { position: absolute; top: -9999px; width: 50px; height: 50px; overflow: scroll; }'
-
- style = document.createElement('style')
- style.type = 'text/css'
- style.appendChild(document.createTextNode(css))
-
- document.head.appendChild(style)
-
- // Simulate scrollbars
- document.documentElement.style.paddingRight = '16px'
})
afterEach(() => {
clearFixture()
-
+ clearBodyAndDocument()
document.body.classList.remove('modal-open')
- document.body.removeAttribute('style')
- document.body.removeAttribute('data-bs-padding-right')
document.querySelectorAll('.modal-backdrop')
.forEach(backdrop => {
document.body.removeChild(backdrop)
})
-
- document.body.style.paddingRight = '0px'
})
- afterAll(() => {
- document.head.removeChild(style)
- document.documentElement.style.paddingRight = '0px'
+ beforeEach(() => {
+ clearBodyAndDocument()
})
describe('VERSION', () => {
@@ -56,10 +39,30 @@ describe('Modal', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Modal.DATA_KEY).toEqual('bs.modal')
+ })
+ })
+
+ describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
+
+ const modalEl = fixtureEl.querySelector('.modal')
+ const modalBySelector = new Modal('.modal')
+ const modalByElement = new Modal(modalEl)
+
+ expect(modalBySelector._element).toEqual(modalEl)
+ expect(modalByElement._element).toEqual(modalEl)
+ })
+ })
+
describe('toggle', () => {
it('should toggle a modal', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
+ document.documentElement.style.overflowY = 'scroll'
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl)
const originalPadding = '0px'
@@ -74,6 +77,7 @@ describe('Modal', () => {
modalEl.addEventListener('hidden.bs.modal', () => {
expect(document.body.getAttribute('data-bs-padding-right')).toBeNull()
expect().nothing()
+ document.documentElement.style.overflowY = 'auto'
done()
})
@@ -86,25 +90,28 @@ describe('Modal', () => {
'<div class="modal"><div class="modal-dialog"></div></div>'
].join('')
+ document.documentElement.style.overflowY = 'scroll'
const fixedEl = fixtureEl.querySelector('.fixed-top')
const originalPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl)
+ const scrollBarWidth = getScrollBarWidth()
modalEl.addEventListener('shown.bs.modal', () => {
- const expectedPadding = originalPadding + modal._getScrollbarWidth()
- const currentPadding = Number.parseInt(window.getComputedStyle(modalEl).paddingRight, 10)
+ const expectedPadding = originalPadding + scrollBarWidth
+ const currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
- expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual('0px', 'original fixed element padding should be stored in data-bs-padding-right')
+ expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual(`${originalPadding}px`, 'original fixed element padding should be stored in data-bs-padding-right')
expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening')
modal.toggle()
})
modalEl.addEventListener('hidden.bs.modal', () => {
- const currentPadding = Number.parseInt(window.getComputedStyle(modalEl).paddingRight, 10)
+ const currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
- expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing')
+ expect(fixedEl.hasAttribute('data-bs-padding-right')).toEqual(false, 'data-bs-padding-right should be cleared after closing')
expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing')
+ document.documentElement.style.overflowY = 'auto'
done()
})
@@ -117,16 +124,19 @@ describe('Modal', () => {
'<div class="modal"><div class="modal-dialog"></div></div>'
].join('')
+ document.documentElement.style.overflowY = 'scroll'
+
const stickyTopEl = fixtureEl.querySelector('.sticky-top')
const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl)
+ const scrollBarWidth = getScrollBarWidth()
modalEl.addEventListener('shown.bs.modal', () => {
- const expectedMargin = originalMargin - modal._getScrollbarWidth()
+ const expectedMargin = originalMargin - scrollBarWidth
const currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
- expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual('0px', 'original sticky element margin should be stored in data-bs-margin-right')
+ expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual(`${originalMargin}px`, 'original sticky element margin should be stored in data-bs-margin-right')
expect(currentMargin).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening')
modal.toggle()
})
@@ -134,14 +144,40 @@ describe('Modal', () => {
modalEl.addEventListener('hidden.bs.modal', () => {
const currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
- expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual(null, 'data-bs-margin-right should be cleared after closing')
+ expect(stickyTopEl.hasAttribute('data-bs-margin-right')).toEqual(false, 'data-bs-margin-right should be cleared after closing')
expect(currentMargin).toEqual(originalMargin, 'sticky element margin should be reset after closing')
+
+ document.documentElement.style.overflowY = 'auto'
done()
})
modal.toggle()
})
+ it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', done => {
+ fixtureEl.innerHTML = [
+ '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: calc(100vw - 50%)"></div>',
+ '<div class="modal"><div class="modal-dialog"></div></div>'
+ ].join('')
+
+ const stickyTopEl = fixtureEl.querySelector('.sticky-top')
+ const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ const originalPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
+ const modalEl = fixtureEl.querySelector('.modal')
+ const modal = new Modal(modalEl)
+
+ modalEl.addEventListener('shown.bs.modal', () => {
+ const currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ const currentPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
+
+ expect(currentMargin).toEqual(originalMargin, 'sticky element\'s margin should not be adjusted while opening')
+ expect(currentPadding).toEqual(originalPadding, 'sticky element\'s padding should not be adjusted while opening')
+ done()
+ })
+
+ modal.show()
+ })
+
it('should ignore values set via CSS when trying to restore body padding after closing', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const styleTest = document.createElement('style')
@@ -195,27 +231,6 @@ describe('Modal', () => {
modal.toggle()
})
-
- it('should properly restore non-pixel inline body padding after closing', done => {
- fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
- document.body.style.paddingRight = '5%'
-
- const modalEl = fixtureEl.querySelector('.modal')
- const modal = new Modal(modalEl)
-
- modalEl.addEventListener('shown.bs.modal', () => {
- modal.toggle()
- })
-
- modalEl.addEventListener('hidden.bs.modal', () => {
- expect(document.body.style.paddingRight).toEqual('5%')
- document.body.removeAttribute('style')
- done()
- })
-
- modal.toggle()
- })
})
describe('show', () => {
@@ -542,7 +557,7 @@ describe('Modal', () => {
})
it('should not close modal when clicking outside of modal-content if backdrop = static', done => {
- fixtureEl.innerHTML = '<div class="modal" data-bs-backdrop="static" ><div class="modal-dialog"></div></div>'
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
@@ -569,7 +584,7 @@ describe('Modal', () => {
})
it('should close modal when escape key is pressed with keyboard = true and backdrop is static', done => {
- fixtureEl.innerHTML = '<div class="modal" data-bs-backdrop="static"><div class="modal-dialog"></div></div>'
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
@@ -626,7 +641,7 @@ describe('Modal', () => {
})
it('should not overflow when clicking outside of modal-content if backdrop = static', done => {
- fixtureEl.innerHTML = '<div class="modal" data-bs-backdrop="static"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>'
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl, {
@@ -662,7 +677,6 @@ describe('Modal', () => {
// Restore scrollbars
document.body.style.overflow = 'auto'
- document.documentElement.style.paddingRight = '16px'
done()
})
@@ -694,7 +708,6 @@ describe('Modal', () => {
// Restore overridden css
document.body.style.removeProperty('margin')
document.body.style.removeProperty('overflow')
- document.documentElement.style.paddingRight = '16px'
done()
})
@@ -870,7 +883,7 @@ describe('Modal', () => {
})
describe('data-api', () => {
- it('should open modal', done => {
+ it('should toggle modal', done => {
fixtureEl.innerHTML = [
'<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
'<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
@@ -885,6 +898,15 @@ describe('Modal', () => {
expect(modalEl.getAttribute('aria-hidden')).toEqual(null)
expect(modalEl.style.display).toEqual('block')
expect(document.querySelector('.modal-backdrop')).toBeDefined()
+ setTimeout(() => trigger.click(), 10)
+ })
+
+ modalEl.addEventListener('hidden.bs.modal', () => {
+ expect(modalEl.getAttribute('aria-modal')).toEqual(null)
+ expect(modalEl.getAttribute('role')).toEqual(null)
+ expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
+ expect(modalEl.style.display).toEqual('none')
+ expect(document.querySelector('.modal-backdrop')).toEqual(null)
done()
})
@@ -1038,6 +1060,23 @@ describe('Modal', () => {
expect(Modal.getInstance(div)).toBeDefined()
})
+ it('should create a modal with given config', () => {
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ jQueryMock.fn.modal = Modal.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.modal.call(jQueryMock, { keyboard: false })
+ spyOn(Modal.prototype, 'constructor')
+ expect(Modal.prototype.constructor).not.toHaveBeenCalledWith(div, { keyboard: false })
+
+ const modal = Modal.getInstance(div)
+ expect(modal).toBeDefined()
+ expect(modal._config.keyboard).toBe(false)
+ })
+
it('should not re create a modal', () => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
@@ -1061,11 +1100,9 @@ describe('Modal', () => {
jQueryMock.fn.modal = Modal.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.modal.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
it('should call show method', () => {
diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js
new file mode 100644
index 000000000..30edc2913
--- /dev/null
+++ b/js/tests/unit/offcanvas.spec.js
@@ -0,0 +1,705 @@
+import Offcanvas from '../../src/offcanvas'
+import EventHandler from '../../src/dom/event-handler'
+
+/** Test helpers */
+import { clearBodyAndDocument, clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fixture'
+import { isVisible } from '../../src/util'
+
+describe('Offcanvas', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ document.body.classList.remove('offcanvas-open')
+ clearBodyAndDocument()
+ })
+
+ beforeEach(() => {
+ clearBodyAndDocument()
+ })
+
+ describe('VERSION', () => {
+ it('should return plugin version', () => {
+ expect(Offcanvas.VERSION).toEqual(jasmine.any(String))
+ })
+ })
+
+ describe('Default', () => {
+ it('should return plugin default config', () => {
+ expect(Offcanvas.Default).toEqual(jasmine.any(Object))
+ })
+ })
+
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Offcanvas.DATA_KEY).toEqual('bs.offcanvas')
+ })
+ })
+
+ describe('constructor', () => {
+ it('should call hide when a element with data-bs-dismiss="offcanvas" is clicked', () => {
+ fixtureEl.innerHTML = [
+ '<div class="offcanvas">',
+ ' <a href="#" data-bs-dismiss="offcanvas">Close</a>',
+ '</div>'
+ ].join('')
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const closeEl = fixtureEl.querySelector('a')
+ const offCanvas = new Offcanvas(offCanvasEl)
+
+ spyOn(offCanvas, 'hide')
+
+ closeEl.click()
+
+ expect(offCanvas._config.keyboard).toBe(true)
+ expect(offCanvas.hide).toHaveBeenCalled()
+ })
+
+ it('should hide if esc is pressed', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ const keyDownEsc = createEvent('keydown')
+ keyDownEsc.key = 'Escape'
+
+ spyOn(offCanvas, 'hide')
+
+ offCanvasEl.dispatchEvent(keyDownEsc)
+
+ expect(offCanvas.hide).toHaveBeenCalled()
+ })
+
+ it('should not hide if esc is not pressed', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ const keydownTab = createEvent('keydown')
+ keydownTab.key = 'Tab'
+
+ spyOn(offCanvas, 'hide')
+
+ document.dispatchEvent(keydownTab)
+
+ expect(offCanvas.hide).not.toHaveBeenCalled()
+ })
+
+ it('should not hide if esc is pressed but with keyboard = false', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl, { keyboard: false })
+ const keyDownEsc = createEvent('keydown')
+ keyDownEsc.key = 'Escape'
+
+ spyOn(offCanvas, 'hide')
+
+ document.dispatchEvent(keyDownEsc)
+
+ expect(offCanvas._config.keyboard).toBe(false)
+ expect(offCanvas.hide).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('config', () => {
+ it('should have default values', () => {
+ fixtureEl.innerHTML = [
+ '<div class="offcanvas">',
+ '</div>'
+ ].join('')
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+
+ expect(offCanvas._config.backdrop).toEqual(true)
+ expect(offCanvas._backdrop._config.isVisible).toEqual(true)
+ expect(offCanvas._config.keyboard).toEqual(true)
+ expect(offCanvas._config.scroll).toEqual(false)
+ })
+
+ it('should read data attributes and override default config', () => {
+ fixtureEl.innerHTML = [
+ '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">',
+ '</div>'
+ ].join('')
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+
+ expect(offCanvas._config.backdrop).toEqual(false)
+ expect(offCanvas._backdrop._config.isVisible).toEqual(false)
+ expect(offCanvas._config.keyboard).toEqual(false)
+ expect(offCanvas._config.scroll).toEqual(true)
+ })
+
+ it('given a config object must override data attributes', () => {
+ fixtureEl.innerHTML = [
+ '<div class="offcanvas" data-bs-scroll="true" data-bs-backdrop="false" data-bs-keyboard="false">',
+ '</div>'
+ ].join('')
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl, {
+ backdrop: true,
+ keyboard: true,
+ scroll: false
+ })
+ expect(offCanvas._config.backdrop).toEqual(true)
+ expect(offCanvas._config.keyboard).toEqual(true)
+ expect(offCanvas._config.scroll).toEqual(false)
+ })
+ })
+ describe('options', () => {
+ it('if scroll is enabled, should allow body to scroll while offcanvas is open', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl, { scroll: true })
+ const initialOverFlow = document.body.style.overflow
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(document.body.style.overflow).toEqual(initialOverFlow)
+
+ offCanvas.hide()
+ })
+ offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
+ expect(document.body.style.overflow).toEqual(initialOverFlow)
+ done()
+ })
+ offCanvas.show()
+ })
+
+ it('if scroll is disabled, should not allow body to scroll while offcanvas is open', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl, { scroll: false })
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(document.body.style.overflow).toEqual('hidden')
+
+ offCanvas.hide()
+ })
+ offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
+ expect(document.body.style.overflow).not.toEqual('hidden')
+ done()
+ })
+ offCanvas.show()
+ })
+
+ it('should hide a shown element if user click on backdrop', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ 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()
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(typeof offCanvas._backdrop._config.clickCallback).toBe('function')
+
+ offCanvas._backdrop._getElement().dispatchEvent(clickEvent)
+ })
+
+ offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
+ expect(offCanvas._backdrop._config.clickCallback).toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.show()
+ })
+
+ it('should not enforce focus if focus scroll is allowed', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl, {
+ scroll: true
+ })
+
+ spyOn(offCanvas, '_enforceFocusOnElement')
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(offCanvas._enforceFocusOnElement).not.toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.show()
+ })
+ })
+
+ describe('toggle', () => {
+ it('should call show method if show class is not present', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+
+ spyOn(offCanvas, 'show')
+
+ offCanvas.toggle()
+
+ expect(offCanvas.show).toHaveBeenCalled()
+ })
+
+ it('should call hide method if show class is present', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ offCanvas.show()
+ expect(offCanvasEl.classList.contains('show')).toBe(true)
+
+ spyOn(offCanvas, 'hide')
+
+ offCanvas.toggle()
+
+ expect(offCanvas.hide).toHaveBeenCalled()
+ })
+ })
+
+ describe('show', () => {
+ it('should do nothing if already shown', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ offCanvas.show()
+
+ expect(offCanvasEl.classList.contains('show')).toBe(true)
+
+ spyOn(offCanvas._backdrop, 'show').and.callThrough()
+ spyOn(EventHandler, 'trigger').and.callThrough()
+ offCanvas.show()
+
+ expect(EventHandler.trigger).not.toHaveBeenCalled()
+ expect(offCanvas._backdrop.show).not.toHaveBeenCalled()
+ })
+
+ it('should show a hidden element', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ spyOn(offCanvas._backdrop, 'show').and.callThrough()
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(offCanvasEl.classList.contains('show')).toEqual(true)
+ expect(offCanvas._backdrop.show).toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.show()
+ })
+
+ it('should not fire shown when show is prevented', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ 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)
+ }
+
+ offCanvasEl.addEventListener('show.bs.offcanvas', e => {
+ e.preventDefault()
+ expectEnd()
+ })
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ throw new Error('should not fire shown event')
+ })
+
+ offCanvas.show()
+ })
+
+ it('on window load, should make visible an offcanvas element, if its markup contains class "show"', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas show"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ spyOn(Offcanvas.prototype, 'show').and.callThrough()
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ done()
+ })
+
+ window.dispatchEvent(createEvent('load'))
+
+ const instance = Offcanvas.getInstance(offCanvasEl)
+ expect(instance).not.toBeNull()
+ expect(Offcanvas.prototype.show).toHaveBeenCalled()
+ })
+
+ it('should enforce focus', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('.offcanvas')
+ const offCanvas = new Offcanvas(offCanvasEl)
+
+ spyOn(offCanvas, '_enforceFocusOnElement')
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(offCanvas._enforceFocusOnElement).toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.show()
+ })
+ })
+
+ describe('hide', () => {
+ it('should do nothing if already shown', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ spyOn(EventHandler, 'trigger').and.callThrough()
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ spyOn(offCanvas._backdrop, 'hide').and.callThrough()
+
+ offCanvas.hide()
+ expect(offCanvas._backdrop.hide).not.toHaveBeenCalled()
+ expect(EventHandler.trigger).not.toHaveBeenCalled()
+ })
+
+ it('should hide a shown element', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ 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.classList.contains('show')).toEqual(false)
+ expect(offCanvas._backdrop.hide).toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.hide()
+ })
+
+ it('should not fire hidden when hide is prevented', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ spyOn(offCanvas._backdrop, 'hide').and.callThrough()
+
+ offCanvas.show()
+
+ const expectEnd = () => {
+ setTimeout(() => {
+ expect(offCanvas._backdrop.hide).not.toHaveBeenCalled()
+ done()
+ }, 10)
+ }
+
+ offCanvasEl.addEventListener('hide.bs.offcanvas', e => {
+ e.preventDefault()
+ expectEnd()
+ })
+
+ offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
+ throw new Error('should not fire hidden event')
+ })
+
+ offCanvas.hide()
+ })
+ })
+
+ describe('dispose', () => {
+ it('should dispose an offcanvas', () => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ const backdrop = offCanvas._backdrop
+ spyOn(backdrop, 'dispose').and.callThrough()
+
+ expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas)
+
+ spyOn(EventHandler, 'off')
+
+ offCanvas.dispose()
+
+ expect(backdrop.dispose).toHaveBeenCalled()
+ expect(offCanvas._backdrop).toBeNull()
+ expect(Offcanvas.getInstance(offCanvasEl)).toEqual(null)
+ })
+ })
+
+ describe('data-api', () => {
+ it('should not prevent event for input', done => {
+ fixtureEl.innerHTML = [
+ '<input type="checkbox" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" />',
+ '<div id="offcanvasdiv1" class="offcanvas"></div>'
+ ].join('')
+
+ const target = fixtureEl.querySelector('input')
+ const offCanvasEl = fixtureEl.querySelector('#offcanvasdiv1')
+
+ offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
+ expect(offCanvasEl.classList.contains('show')).toEqual(true)
+ expect(target.checked).toEqual(true)
+ done()
+ })
+
+ target.click()
+ })
+
+ it('should not call toggle on disabled elements', () => {
+ fixtureEl.innerHTML = [
+ '<a href="#" data-bs-toggle="offcanvas" data-bs-target="#offcanvasdiv1" class="disabled"></a>',
+ '<div id="offcanvasdiv1" class="offcanvas"></div>'
+ ].join('')
+
+ const target = fixtureEl.querySelector('a')
+
+ spyOn(Offcanvas.prototype, 'toggle')
+
+ target.click()
+
+ expect(Offcanvas.prototype.toggle).not.toHaveBeenCalled()
+ })
+
+ it('should call hide first, if another offcanvas is open', done => {
+ fixtureEl.innerHTML = [
+ '<button id="btn2" data-bs-toggle="offcanvas" data-bs-target="#offcanvas2" ></button>',
+ '<div id="offcanvas1" class="offcanvas"></div>',
+ '<div id="offcanvas2" class="offcanvas"></div>'
+ ].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()
+ done()
+ })
+ offcanvas1.show()
+ })
+
+ it('should focus on trigger element after closing offcanvas', done => {
+ fixtureEl.innerHTML = [
+ '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas" ></button>',
+ '<div id="offcanvas" class="offcanvas"></div>'
+ ].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 = [
+ '<button id="btn" data-bs-toggle="offcanvas" data-bs-target="#offcanvas" ></button>',
+ '<div id="offcanvas" class="offcanvas"></div>'
+ ].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)).toBe(false)
+ expect(trigger.focus).not.toHaveBeenCalled()
+ done()
+ }, 5)
+ })
+
+ trigger.click()
+ })
+ })
+
+ describe('jQueryInterface', () => {
+ it('should create an offcanvas', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.offcanvas.call(jQueryMock)
+
+ expect(Offcanvas.getInstance(div)).toBeDefined()
+ })
+
+ it('should not re create an offcanvas', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(div)
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.offcanvas.call(jQueryMock)
+
+ expect(Offcanvas.getInstance(div)).toEqual(offCanvas)
+ })
+
+ it('should throw error on undefined method', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const action = 'undefinedMethod'
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ expect(() => {
+ jQueryMock.fn.offcanvas.call(jQueryMock, action)
+ }).toThrowError(TypeError, `No method named "${action}"`)
+ })
+
+ it('should throw error on protected method', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const action = '_getConfig'
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ expect(() => {
+ jQueryMock.fn.offcanvas.call(jQueryMock, action)
+ }).toThrowError(TypeError, `No method named "${action}"`)
+ })
+
+ it('should throw error if method "constructor" is being called', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const action = 'constructor'
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ expect(() => {
+ jQueryMock.fn.offcanvas.call(jQueryMock, action)
+ }).toThrowError(TypeError, `No method named "${action}"`)
+ })
+
+ it('should throw error on protected method', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const action = '_getConfig'
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ try {
+ jQueryMock.fn.offcanvas.call(jQueryMock, action)
+ } catch (error) {
+ expect(error.message).toEqual(`No method named "${action}"`)
+ }
+ })
+
+ it('should throw error if method "constructor" is being called', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const action = 'constructor'
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ try {
+ jQueryMock.fn.offcanvas.call(jQueryMock, action)
+ } catch (error) {
+ expect(error.message).toEqual(`No method named "${action}"`)
+ }
+ })
+
+ it('should call offcanvas method', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ spyOn(Offcanvas.prototype, 'show')
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.offcanvas.call(jQueryMock, 'show')
+ expect(Offcanvas.prototype.show).toHaveBeenCalled()
+ })
+
+ it('should create a offcanvas with given config', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ jQueryMock.fn.offcanvas = Offcanvas.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.offcanvas.call(jQueryMock, { scroll: true })
+ spyOn(Offcanvas.prototype, 'constructor')
+ expect(Offcanvas.prototype.constructor).not.toHaveBeenCalledWith(div, { scroll: true })
+
+ const offcanvas = Offcanvas.getInstance(div)
+ expect(offcanvas).toBeDefined()
+ expect(offcanvas._config.scroll).toBe(true)
+ })
+ })
+
+ describe('getInstance', () => {
+ it('should return offcanvas instance', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(div)
+
+ expect(Offcanvas.getInstance(div)).toEqual(offCanvas)
+ expect(Offcanvas.getInstance(div)).toBeInstanceOf(Offcanvas)
+ })
+
+ it('should return null when there is no offcanvas instance', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ expect(Offcanvas.getInstance(div)).toEqual(null)
+ })
+ })
+})
diff --git a/js/tests/unit/popover.spec.js b/js/tests/unit/popover.spec.js
index 3c04e7ac1..e5c235e2a 100644
--- a/js/tests/unit/popover.spec.js
+++ b/js/tests/unit/popover.spec.js
@@ -206,11 +206,9 @@ describe('Popover', () => {
jQueryMock.fn.popover = Popover.jQueryInterface
jQueryMock.elements = [popoverEl]
- try {
+ expect(() => {
jQueryMock.fn.popover.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
it('should should call show method', () => {
diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js
index 45de56fbe..c4130a1aa 100644
--- a/js/tests/unit/scrollspy.spec.js
+++ b/js/tests/unit/scrollspy.spec.js
@@ -1,6 +1,5 @@
import ScrollSpy from '../../src/scrollspy'
import Manipulator from '../../src/dom/manipulator'
-import EventHandler from '../../src/dom/event-handler'
/** Test helpers */
import { getFixture, clearFixture, createEvent, jQueryMock } from '../helpers/fixture'
@@ -48,7 +47,24 @@ describe('ScrollSpy', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(ScrollSpy.DATA_KEY).toEqual('bs.scrollspy')
+ })
+ })
+
describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<nav id="navigation"></nav><div class="content"></div>'
+
+ const sSpyEl = fixtureEl.querySelector('#navigation')
+ const sSpyBySelector = new ScrollSpy('#navigation')
+ const sSpyByElement = new ScrollSpy(sSpyEl)
+
+ expect(sSpyBySelector._element).toEqual(sSpyEl)
+ expect(sSpyByElement._element).toEqual(sSpyEl)
+ })
+
it('should generate an id when there is not one', () => {
fixtureEl.innerHTML = [
'<nav></nav>',
@@ -68,7 +84,7 @@ describe('ScrollSpy', () => {
fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
- ' <li class="nav-item active"><a class="nav-link" id="one-link" href="#">One</a></li>',
+ ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#">One</a></li>',
' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
' </ul>',
@@ -177,7 +193,7 @@ describe('ScrollSpy', () => {
'<div id="header" style="height: 500px;"></div>',
'<nav id="navigation" class="navbar">',
' <ul class="navbar-nav">',
- ' <li class="nav-item active"><a class="nav-link" id="one-link" href="#one">One</a></li>',
+ ' <li class="nav-item"><a class="nav-link active" id="one-link" href="#one">One</a></li>',
' <li class="nav-item"><a class="nav-link" id="two-link" href="#two">Two</a></li>',
' <li class="nav-item"><a class="nav-link" id="three-link" href="#three">Three</a></li>',
' </ul>',
@@ -560,14 +576,18 @@ describe('ScrollSpy', () => {
describe('dispose', () => {
it('should dispose a scrollspy', () => {
- spyOn(EventHandler, 'off')
fixtureEl.innerHTML = '<div style="display: none;"></div>'
const divEl = fixtureEl.querySelector('div')
+ spyOn(divEl, 'addEventListener').and.callThrough()
+ spyOn(divEl, 'removeEventListener').and.callThrough()
+
const scrollSpy = new ScrollSpy(divEl)
+ expect(divEl.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean))
scrollSpy.dispose()
- expect(EventHandler.off).toHaveBeenCalledWith(divEl, '.bs.scrollspy')
+
+ expect(divEl.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), jasmine.any(Boolean))
})
})
@@ -585,6 +605,23 @@ describe('ScrollSpy', () => {
expect(ScrollSpy.getInstance(div)).toBeDefined()
})
+ it('should create a scrollspy with given config', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const div = fixtureEl.querySelector('div')
+
+ jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
+ jQueryMock.elements = [div]
+
+ jQueryMock.fn.scrollspy.call(jQueryMock, { offset: 15 })
+ spyOn(ScrollSpy.prototype, 'constructor')
+ expect(ScrollSpy.prototype.constructor).not.toHaveBeenCalledWith(div, { offset: 15 })
+
+ const scrollspy = ScrollSpy.getInstance(div)
+ expect(scrollspy).toBeDefined()
+ expect(scrollspy._config.offset).toBe(15)
+ })
+
it('should not re create a scrollspy', () => {
fixtureEl.innerHTML = '<div></div>'
@@ -625,11 +662,9 @@ describe('ScrollSpy', () => {
jQueryMock.fn.scrollspy = ScrollSpy.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.scrollspy.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js
index 67a85b2e4..c8e1475ad 100644
--- a/js/tests/unit/tab.spec.js
+++ b/js/tests/unit/tab.spec.js
@@ -20,16 +20,56 @@ describe('Tab', () => {
})
})
+ describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav"><li><a href="#home" role="tab">Home</a></li></ul>',
+ '<ul><li id="home"></li></ul>'
+ ].join('')
+
+ const tabEl = fixtureEl.querySelector('[href="#home"]')
+ const tabBySelector = new Tab('[href="#home"]')
+ const tabByElement = new Tab(tabEl)
+
+ expect(tabBySelector._element).toEqual(tabEl)
+ expect(tabByElement._element).toEqual(tabEl)
+ })
+ })
+
describe('show', () => {
- it('should activate element by tab id', done => {
+ it('should activate element by tab id (using buttons, the preferred semantic way)', done => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" role="tablist">',
+ ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
+ ' <li><button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button></li>',
+ '</ul>',
+ '<ul>',
+ ' <li id="home" role="tabpanel"></li>',
+ ' <li id="profile" role="tabpanel"></li>',
+ '</ul>'
+ ].join('')
+
+ const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
+ const tab = new Tab(profileTriggerEl)
+
+ profileTriggerEl.addEventListener('shown.bs.tab', () => {
+ expect(fixtureEl.querySelector('#profile').classList.contains('active')).toEqual(true)
+ expect(profileTriggerEl.getAttribute('aria-selected')).toEqual('true')
+ done()
+ })
+
+ tab.show()
+ })
+
+ it('should activate element by tab id (using links for tabs - not ideal, but still supported)', done => {
fixtureEl.innerHTML = [
- '<ul class="nav">',
+ '<ul class="nav" role="tablist">',
' <li><a href="#home" role="tab">Home</a></li>',
- ' <li><a id="triggerProfile" role="tab" href="#profile">Profile</a></li>',
+ ' <li><a id="triggerProfile" href="#profile" role="tab">Profile</a></li>',
'</ul>',
'<ul>',
- ' <li id="home"></li>',
- ' <li id="profile"></li>',
+ ' <li id="home" role="tabpanel"></li>',
+ ' <li id="profile" role="tabpanel"></li>',
'</ul>'
].join('')
@@ -48,12 +88,12 @@ describe('Tab', () => {
it('should activate element by tab id in ordered list', done => {
fixtureEl.innerHTML = [
'<ol class="nav nav-pills">',
- ' <li><a href="#home">Home</a></li>',
- ' <li><a id="triggerProfile" href="#profile">Profile</a></li>',
+ ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
+ ' <li><button type="button" id="triggerProfile" href="#profile" role="tab">Profile</button></li>',
'</ol>',
'<ol>',
- ' <li id="home"></li>',
- ' <li id="profile"></li>',
+ ' <li id="home" role="tabpanel"></li>',
+ ' <li id="profile" role="tabpanel"></li>',
'</ol>'
].join('')
@@ -71,10 +111,10 @@ describe('Tab', () => {
it('should activate element by tab id in nav list', done => {
fixtureEl.innerHTML = [
'<nav class="nav">',
- ' <a href="#home">Home</a>',
- ' <a id="triggerProfile" href="#profile">Profile</a>',
+ ' <button type="button" data-bs-target="#home" role="tab">Home</button>',
+ ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</a>',
'</nav>',
- '<nav><div id="home"></div><div id="profile"></div></nav>'
+ '<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>'
].join('')
const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
@@ -90,11 +130,11 @@ describe('Tab', () => {
it('should activate element by tab id in list group', done => {
fixtureEl.innerHTML = [
- '<div class="list-group">',
- ' <a href="#home">Home</a>',
- ' <a id="triggerProfile" href="#profile">Profile</a>',
+ '<div class="list-group" role="tablist">',
+ ' <button type="button" data-bs-target="#home" role="tab">Home</button>',
+ ' <button type="button" id="triggerProfile" data-bs-target="#profile" role="tab">Profile</button>',
'</div>',
- '<nav><div id="home"></div><div id="profile"></div></nav>'
+ '<div><div id="home" role="tabpanel"></div><div id="profile" role="tabpanel"></div></div>'
].join('')
const profileTriggerEl = fixtureEl.querySelector('#triggerProfile')
@@ -135,8 +175,8 @@ describe('Tab', () => {
it('should not fire shown when tab is already active', done => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
- ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
- ' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link" role="tab">Profile</a></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>',
'</ul>',
'<div class="tab-content">',
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
@@ -144,7 +184,7 @@ describe('Tab', () => {
'</div>'
].join('')
- const triggerActive = fixtureEl.querySelector('a.active')
+ const triggerActive = fixtureEl.querySelector('button.active')
const tab = new Tab(triggerActive)
triggerActive.addEventListener('shown.bs.tab', () => {
@@ -158,37 +198,11 @@ describe('Tab', () => {
}, 30)
})
- it('should not fire shown when tab is disabled', done => {
- fixtureEl.innerHTML = [
- '<ul class="nav nav-tabs" role="tablist">',
- ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
- ' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>',
- '</ul>',
- '<div class="tab-content">',
- ' <div class="tab-pane active" id="home" role="tabpanel"></div>',
- ' <div class="tab-pane" id="profile" role="tabpanel"></div>',
- '</div>'
- ].join('')
-
- const triggerDisabled = fixtureEl.querySelector('a.disabled')
- const tab = new Tab(triggerDisabled)
-
- triggerDisabled.addEventListener('shown.bs.tab', () => {
- throw new Error('should not trigger shown event')
- })
-
- tab.show()
- setTimeout(() => {
- expect().nothing()
- done()
- }, 30)
- })
-
it('show and shown events should reference correct relatedTarget', done => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
- ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
- ' <li class="nav-item" role="presentation"><a id="triggerProfile" href="#profile" class="nav-link" role="tab">Profile</a></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>',
'</ul>',
'<div class="tab-content">',
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
@@ -200,13 +214,13 @@ describe('Tab', () => {
const secondTab = new Tab(secondTabTrigger)
secondTabTrigger.addEventListener('show.bs.tab', ev => {
- expect(ev.relatedTarget.hash).toEqual('#home')
+ expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home')
})
secondTabTrigger.addEventListener('shown.bs.tab', ev => {
- expect(ev.relatedTarget.hash).toEqual('#home')
+ expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#home')
expect(secondTabTrigger.getAttribute('aria-selected')).toEqual('true')
- expect(fixtureEl.querySelector('a:not(.active)').getAttribute('aria-selected')).toEqual('false')
+ expect(fixtureEl.querySelector('button:not(.active)').getAttribute('aria-selected')).toEqual('false')
done()
})
@@ -215,13 +229,13 @@ describe('Tab', () => {
it('should fire hide and hidden events', done => {
fixtureEl.innerHTML = [
- '<ul class="nav">',
- ' <li><a href="#home">Home</a></li>',
- ' <li><a href="#profile">Profile</a></li>',
+ '<ul class="nav" role="tablist">',
+ ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
+ ' <li><button type="button" data-bs-target="#profile">Profile</button></li>',
'</ul>'
].join('')
- const triggerList = fixtureEl.querySelectorAll('a')
+ const triggerList = fixtureEl.querySelectorAll('button')
const firstTab = new Tab(triggerList[0])
const secondTab = new Tab(triggerList[1])
@@ -232,12 +246,12 @@ describe('Tab', () => {
triggerList[0].addEventListener('hide.bs.tab', ev => {
hideCalled = true
- expect(ev.relatedTarget.hash).toEqual('#profile')
+ expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile')
})
triggerList[0].addEventListener('hidden.bs.tab', ev => {
expect(hideCalled).toEqual(true)
- expect(ev.relatedTarget.hash).toEqual('#profile')
+ expect(ev.relatedTarget.getAttribute('data-bs-target')).toEqual('#profile')
done()
})
@@ -246,13 +260,13 @@ describe('Tab', () => {
it('should not fire hidden when hide is prevented', done => {
fixtureEl.innerHTML = [
- '<ul class="nav">',
- ' <li><a href="#home">Home</a></li>',
- ' <li><a href="#profile">Profile</a></li>',
+ '<ul class="nav" role="tablist">',
+ ' <li><button type="button" data-bs-target="#home" role="tab">Home</button></li>',
+ ' <li><button type="button" data-bs-target="#profile" role="tab">Profile</button></li>',
'</ul>'
].join('')
- const triggerList = fixtureEl.querySelectorAll('a')
+ const triggerList = fixtureEl.querySelectorAll('button')
const firstTab = new Tab(triggerList[0])
const secondTab = new Tab(triggerList[1])
const expectDone = () => {
@@ -397,11 +411,9 @@ describe('Tab', () => {
jQueryMock.fn.tab = Tab.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.tab.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
@@ -425,8 +437,8 @@ describe('Tab', () => {
it('should create dynamically a tab', done => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
- ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab">Home</a></li>',
- ' <li class="nav-item" role="presentation"><a id="triggerProfile" data-bs-toggle="tab" href="#profile" class="nav-link" role="tab">Profile</a></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" id="triggerProfile" data-bs-toggle="tab" data-bs-target="#profile" class="nav-link" role="tab">Profile</button></li>',
'</ul>',
'<div class="tab-content">',
' <div class="tab-pane active" id="home" role="tabpanel"></div>',
@@ -468,18 +480,75 @@ describe('Tab', () => {
expect(fixtureEl.querySelector('li:last-child .dropdown-menu a:first-child').classList.contains('active')).toEqual(false)
})
+ it('selecting a dropdown tab does not activate another', () => {
+ const nav1 = [
+ '<ul class="nav nav-tabs" id="nav1">',
+ ' <li class="nav-item active"><a class="nav-link" href="#home" data-bs-toggle="tab">Home</a></li>',
+ ' <li class="nav-item dropdown">',
+ ' <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">Dropdown</a>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#dropdown1" id="dropdown1-tab" data-bs-toggle="tab">@fat</a>',
+ ' </div>',
+ ' </li>',
+ '</ul>'
+ ].join('')
+ const nav2 = [
+ '<ul class="nav nav-tabs" id="nav2">',
+ ' <li class="nav-item active"><a class="nav-link" href="#home" data-bs-toggle="tab">Home</a></li>',
+ ' <li class="nav-item dropdown">',
+ ' <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">Dropdown</a>',
+ ' <div class="dropdown-menu">',
+ ' <a class="dropdown-item" href="#dropdown1" id="dropdown1-tab" data-bs-toggle="tab">@fat</a>',
+ ' </div>',
+ ' </li>',
+ '</ul>'
+ ].join('')
+
+ fixtureEl.innerHTML = nav1 + nav2
+
+ const firstDropItem = fixtureEl.querySelector('#nav1 .dropdown-item')
+
+ firstDropItem.click()
+ expect(firstDropItem.classList.contains('active')).toEqual(true)
+ expect(fixtureEl.querySelector('#nav1 .dropdown-toggle').classList.contains('active')).toEqual(true)
+ expect(fixtureEl.querySelector('#nav2 .dropdown-toggle').classList.contains('active')).toEqual(false)
+ expect(fixtureEl.querySelector('#nav2 .dropdown-item').classList.contains('active')).toEqual(false)
+ })
+
+ it('should support li > .dropdown-item', () => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav nav-tabs">',
+ ' <li class="nav-item"><a class="nav-link active" href="#home" data-bs-toggle="tab">Home</a></li>',
+ ' <li class="nav-item"><a class="nav-link" href="#profile" data-bs-toggle="tab">Profile</a></li>',
+ ' <li class="nav-item dropdown">',
+ ' <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">Dropdown</a>',
+ ' <ul class="dropdown-menu">',
+ ' <li><a class="dropdown-item" href="#dropdown1" id="dropdown1-tab" data-bs-toggle="tab">@fat</a></li>',
+ ' <li><a class="dropdown-item" href="#dropdown2" id="dropdown2-tab" data-bs-toggle="tab">@mdo</a></li>',
+ ' </ul>',
+ ' </li>',
+ '</ul>'
+ ].join('')
+
+ const firstDropItem = fixtureEl.querySelector('.dropdown-item')
+
+ firstDropItem.click()
+ expect(firstDropItem.classList.contains('active')).toEqual(true)
+ expect(fixtureEl.querySelector('.nav-link').classList.contains('active')).toEqual(false)
+ })
+
it('should handle nested tabs', done => {
fixtureEl.innerHTML = [
'<nav class="nav nav-tabs" role="tablist">',
- ' <a id="tab1" href="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</a>',
- ' <a href="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</a>',
- ' <a href="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</a>',
+ ' <button type="button" id="tab1" data-bs-target="#x-tab1" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab1">Tab 1</button>',
+ ' <button type="button" data-bs-target="#x-tab2" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab2" aria-selected="true">Tab 2</button>',
+ ' <button type="button" data-bs-target="#x-tab3" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-tab3">Tab 3</button>',
'</nav>',
'<div class="tab-content">',
' <div class="tab-pane" id="x-tab1" role="tabpanel">',
' <nav class="nav nav-tabs" role="tablist">',
- ' <a href="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</a>',
- ' <a id="tabNested2" href="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</a>',
+ ' <button type="button" data-bs-target="#nested-tab1" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="x-tab1" aria-selected="true">Nested Tab 1</button>',
+ ' <button type="button" id="tabNested2" data-bs-target="#nested-tab2" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="x-profile">Nested Tab2</button>',
' </nav>',
' <div class="tab-content">',
' <div class="tab-pane active" id="nested-tab1" role="tabpanel">Nested Tab1 Content</div>',
@@ -511,8 +580,8 @@ describe('Tab', () => {
it('should not remove fade class if no active pane is present', done => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
- ' <li class="nav-item" role="presentation"><a id="tab-home" href="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</a></li>',
- ' <li class="nav-item" role="presentation"><a id="tab-profile" href="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</a></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" id="tab-home" data-bs-target="#home" class="nav-link" data-bs-toggle="tab" role="tab">Home</button></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" id="tab-profile" data-bs-target="#profile" class="nav-link" data-bs-toggle="tab" role="tab">Profile</button></li>',
'</ul>',
'<div class="tab-content">',
' <div class="tab-pane fade" id="home" role="tabpanel"></div>',
@@ -549,10 +618,10 @@ describe('Tab', () => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
' <li class="nav-item" role="presentation">',
- ' <a class="nav-link nav-tab" href="#home" role="tab" data-bs-toggle="tab">Home</a>',
+ ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>',
' </li>',
' <li class="nav-item" role="presentation">',
- ' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">Profile</a>',
+ ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>',
' </li>',
'</ul>',
'<div class="tab-content">',
@@ -575,10 +644,10 @@ describe('Tab', () => {
fixtureEl.innerHTML = [
'<ul class="nav nav-tabs" role="tablist">',
' <li class="nav-item" role="presentation">',
- ' <a class="nav-link nav-tab" href="#home" role="tab" data-bs-toggle="tab">Home</a>',
+ ' <button type="button" class="nav-link nav-tab" data-bs-target="#home" role="tab" data-bs-toggle="tab">Home</button>',
' </li>',
' <li class="nav-item" role="presentation">',
- ' <a id="secondNav" class="nav-link nav-tab" href="#profile" role="tab" data-bs-toggle="tab">Profile</a>',
+ ' <button type="button" id="secondNav" class="nav-link nav-tab" data-bs-target="#profile" role="tab" data-bs-toggle="tab">Profile</button>',
' </li>',
'</ul>',
'<div class="tab-content">',
@@ -596,5 +665,74 @@ describe('Tab', () => {
secondNavEl.click()
})
+
+ it('should prevent default when the trigger is <a> or <area>', done => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav" role="tablist">',
+ ' <li><a type="button" href="#test" class="active" role="tab" data-bs-toggle="tab">Home</a></li>',
+ ' <li><a type="button" href="#test2" role="tab" data-bs-toggle="tab">Home</a></li>',
+ '</ul>'
+ ].join('')
+
+ const tabEl = fixtureEl.querySelector('[href="#test2"]')
+ spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+ tabEl.addEventListener('shown.bs.tab', () => {
+ expect(tabEl.classList.contains('active')).toEqual(true)
+ expect(Event.prototype.preventDefault).toHaveBeenCalled()
+ done()
+ })
+
+ tabEl.click()
+ })
+
+ it('should not fire shown when tab has disabled attribute', done => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav nav-tabs" role="tablist">',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#home" class="nav-link active" role="tab" aria-selected="true">Home</button></li>',
+ ' <li class="nav-item" role="presentation"><button type="button" data-bs-target="#profile" class="nav-link" disabled role="tab">Profile</button></li>',
+ '</ul>',
+ '<div class="tab-content">',
+ ' <div class="tab-pane active" id="home" role="tabpanel"></div>',
+ ' <div class="tab-pane" id="profile" role="tabpanel"></div>',
+ '</div>'
+ ].join('')
+
+ const triggerDisabled = fixtureEl.querySelector('button[disabled]')
+ triggerDisabled.addEventListener('shown.bs.tab', () => {
+ throw new Error('should not trigger shown event')
+ })
+
+ triggerDisabled.click()
+ setTimeout(() => {
+ expect().nothing()
+ done()
+ }, 30)
+ })
+
+ it('should not fire shown when tab has disabled class', done => {
+ fixtureEl.innerHTML = [
+ '<ul class="nav nav-tabs" role="tablist">',
+ ' <li class="nav-item" role="presentation"><a href="#home" class="nav-link active" role="tab" aria-selected="true">Home</a></li>',
+ ' <li class="nav-item" role="presentation"><a href="#profile" class="nav-link disabled" role="tab">Profile</a></li>',
+ '</ul>',
+ '<div class="tab-content">',
+ ' <div class="tab-pane active" id="home" role="tabpanel"></div>',
+ ' <div class="tab-pane" id="profile" role="tabpanel"></div>',
+ '</div>'
+ ].join('')
+
+ const triggerDisabled = fixtureEl.querySelector('a.disabled')
+
+ triggerDisabled.addEventListener('shown.bs.tab', () => {
+ throw new Error('should not trigger shown event')
+ })
+
+ triggerDisabled.click()
+ setTimeout(() => {
+ expect().nothing()
+ done()
+ }, 30)
+ })
})
})
diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js
index cc873d1fe..d298dc993 100644
--- a/js/tests/unit/toast.spec.js
+++ b/js/tests/unit/toast.spec.js
@@ -20,7 +20,24 @@ describe('Toast', () => {
})
})
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Toast.DATA_KEY).toEqual('bs.toast')
+ })
+ })
+
describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<div class="toast"></div>'
+
+ const toastEl = fixtureEl.querySelector('.toast')
+ const toastBySelector = new Toast('.toast')
+ const toastByElement = new Toast(toastEl)
+
+ expect(toastBySelector._element).toEqual(toastEl)
+ expect(toastByElement._element).toEqual(toastEl)
+ })
+
it('should allow to config in js', done => {
fixtureEl.innerHTML = [
'<div class="toast">',
@@ -274,13 +291,18 @@ describe('Toast', () => {
fixtureEl.innerHTML = '<div></div>'
const toastEl = fixtureEl.querySelector('div')
+ spyOn(toastEl, 'addEventListener').and.callThrough()
+ spyOn(toastEl, 'removeEventListener').and.callThrough()
+
const toast = new Toast(toastEl)
expect(Toast.getInstance(toastEl)).toBeDefined()
+ expect(toastEl.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
toast.dispose()
expect(Toast.getInstance(toastEl)).toBeNull()
+ expect(toastEl.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
})
it('should allow to destroy toast and hide it before that', done => {
@@ -368,11 +390,9 @@ describe('Toast', () => {
jQueryMock.fn.toast = Toast.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.toast.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
diff --git a/js/tests/unit/tooltip.spec.js b/js/tests/unit/tooltip.spec.js
index 9ea9096de..399f1f22a 100644
--- a/js/tests/unit/tooltip.spec.js
+++ b/js/tests/unit/tooltip.spec.js
@@ -63,6 +63,17 @@ describe('Tooltip', () => {
})
describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<a href="#" id="tooltipEl" rel="tooltip" title="Nice and short title">'
+
+ const tooltipEl = fixtureEl.querySelector('#tooltipEl')
+ const tooltipBySelector = new Tooltip('#tooltipEl')
+ const tooltipByElement = new Tooltip(tooltipEl)
+
+ expect(tooltipBySelector._element).toEqual(tooltipEl)
+ expect(tooltipByElement._element).toEqual(tooltipEl)
+ })
+
it('should not take care of disallowed data attributes', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-sanitize="false" title="Another tooltip">'
@@ -107,6 +118,41 @@ describe('Tooltip', () => {
tooltipInContainerEl.click()
})
+ it('should create offset modifier when offset is passed as a function', done => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Offset from function">'
+
+ const getOffset = jasmine.createSpy('getOffset').and.returnValue([10, 20])
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl, {
+ offset: getOffset,
+ popperConfig: {
+ onFirstUpdate: state => {
+ expect(getOffset).toHaveBeenCalledWith({
+ popper: state.rects.popper,
+ reference: state.rects.reference,
+ placement: state.placement
+ }, tooltipEl)
+ done()
+ }
+ }
+ })
+
+ const offset = tooltip._getOffset()
+
+ expect(typeof offset).toEqual('function')
+
+ tooltip.show()
+ })
+
+ it('should create offset modifier when offset option is passed in data attribute', () => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" data-bs-offset="10,20" title="Another tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+
+ expect(tooltip._getOffset()).toEqual([10, 20])
+ })
+
it('should allow to pass config to Popper with `popperConfig`', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip">'
@@ -121,6 +167,21 @@ describe('Tooltip', () => {
expect(popperConfig.placement).toEqual('left')
})
+
+ it('should allow to pass config to Popper with `popperConfig` as a function', () => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const getPopperConfig = jasmine.createSpy('getPopperConfig').and.returnValue({ placement: 'left' })
+ const tooltip = new Tooltip(tooltipEl, {
+ popperConfig: getPopperConfig
+ })
+
+ const popperConfig = tooltip._getPopperConfig('top')
+
+ expect(getPopperConfig).toHaveBeenCalled()
+ expect(popperConfig.placement).toEqual('left')
+ })
})
describe('enable', () => {
@@ -277,13 +338,45 @@ describe('Tooltip', () => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
const tooltipEl = fixtureEl.querySelector('a')
+ const addEventSpy = spyOn(tooltipEl, 'addEventListener').and.callThrough()
+ const removeEventSpy = spyOn(tooltipEl, 'removeEventListener').and.callThrough()
+
const tooltip = new Tooltip(tooltipEl)
expect(Tooltip.getInstance(tooltipEl)).toEqual(tooltip)
+ const expectedArgs = [
+ ['mouseover', jasmine.any(Function), jasmine.any(Boolean)],
+ ['mouseout', jasmine.any(Function), jasmine.any(Boolean)],
+ ['focusin', jasmine.any(Function), jasmine.any(Boolean)],
+ ['focusout', jasmine.any(Function), jasmine.any(Boolean)]
+ ]
+
+ expect(addEventSpy.calls.allArgs()).toEqual(expectedArgs)
+
tooltip.dispose()
expect(Tooltip.getInstance(tooltipEl)).toEqual(null)
+ expect(removeEventSpy.calls.allArgs()).toEqual(expectedArgs)
+ })
+
+ it('should destroy a tooltip after it is shown and hidden', done => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+
+ tooltipEl.addEventListener('shown.bs.tooltip', () => {
+ tooltip.hide()
+ })
+ tooltipEl.addEventListener('hidden.bs.tooltip', () => {
+ tooltip.dispose()
+ expect(tooltip.tip).toEqual(null)
+ expect(Tooltip.getInstance(tooltipEl)).toEqual(null)
+ done()
+ })
+
+ tooltip.show()
})
it('should destroy a tooltip and remove it from the dom', done => {
@@ -357,7 +450,7 @@ describe('Tooltip', () => {
tooltipEl.addEventListener('shown.bs.tooltip', () => {
expect(document.querySelector('.tooltip')).not.toBeNull()
- expect(EventHandler.on).toHaveBeenCalled()
+ expect(EventHandler.on).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
document.documentElement.ontouchstart = undefined
done()
})
@@ -483,24 +576,6 @@ describe('Tooltip', () => {
tooltip.show()
})
- it('should show a tooltip with offset as a function', done => {
- fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
-
- const spy = jasmine.createSpy('offset').and.returnValue({})
- const tooltipEl = fixtureEl.querySelector('a')
- const tooltip = new Tooltip(tooltipEl, {
- offset: spy
- })
-
- tooltipEl.addEventListener('shown.bs.tooltip', () => {
- expect(document.querySelector('.tooltip')).toBeDefined()
- expect(spy).toHaveBeenCalled()
- done()
- })
-
- tooltip.show()
- })
-
it('should show a tooltip without the animation', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
@@ -633,6 +708,100 @@ describe('Tooltip', () => {
tooltipEl.dispatchEvent(createEvent('mouseover'))
})
+ it('should not hide tooltip if leave event occurs and interaction remains inside trigger', done => {
+ fixtureEl.innerHTML = [
+ '<a href="#" rel="tooltip" title="Another tooltip">',
+ '<b>Trigger</b>',
+ 'the tooltip',
+ '</a>'
+ ]
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+ const triggerChild = tooltipEl.querySelector('b')
+
+ spyOn(tooltip, 'hide').and.callThrough()
+
+ tooltipEl.addEventListener('mouseover', () => {
+ const moveMouseToChildEvent = createEvent('mouseout')
+ Object.defineProperty(moveMouseToChildEvent, 'relatedTarget', {
+ value: triggerChild
+ })
+
+ tooltipEl.dispatchEvent(moveMouseToChildEvent)
+ })
+
+ tooltipEl.addEventListener('mouseout', () => {
+ expect(tooltip.hide).not.toHaveBeenCalled()
+ done()
+ })
+
+ tooltipEl.dispatchEvent(createEvent('mouseover'))
+ })
+
+ it('should properly maintain tooltip state if leave event occurs and enter event occurs during hide transition', done => {
+ // Style this tooltip to give it plenty of room for popper to do what it wants
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-placement="top" style="position:fixed;left:50%;top:50%;">Trigger</a>'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+
+ spyOn(window, 'getComputedStyle').and.returnValue({
+ transitionDuration: '0.15s',
+ transitionDelay: '0s'
+ })
+
+ setTimeout(() => {
+ expect(tooltip._popper).not.toBeNull()
+ expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top')
+ tooltipEl.dispatchEvent(createEvent('mouseout'))
+
+ setTimeout(() => {
+ expect(tooltip.getTipElement().classList.contains('show')).toEqual(false)
+ tooltipEl.dispatchEvent(createEvent('mouseover'))
+ }, 100)
+
+ setTimeout(() => {
+ expect(tooltip._popper).not.toBeNull()
+ expect(tooltip.getTipElement().getAttribute('data-popper-placement')).toBe('top')
+ done()
+ }, 200)
+ }, 0)
+
+ tooltipEl.dispatchEvent(createEvent('mouseover'))
+ })
+
+ it('should only trigger inserted event if a new tooltip element was created', done => {
+ fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
+
+ const tooltipEl = fixtureEl.querySelector('a')
+ const tooltip = new Tooltip(tooltipEl)
+
+ spyOn(window, 'getComputedStyle').and.returnValue({
+ transitionDuration: '0.15s',
+ transitionDelay: '0s'
+ })
+
+ const insertedFunc = jasmine.createSpy()
+ tooltipEl.addEventListener('inserted.bs.tooltip', insertedFunc)
+
+ setTimeout(() => {
+ expect(insertedFunc).toHaveBeenCalledTimes(1)
+ tooltip.hide()
+
+ setTimeout(() => {
+ tooltip.show()
+ }, 100)
+
+ setTimeout(() => {
+ expect(insertedFunc).toHaveBeenCalledTimes(1)
+ done()
+ }, 200)
+ }, 0)
+
+ tooltip.show()
+ })
+
it('should show a tooltip with custom class provided in data attributes', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip" data-bs-custom-class="custom-class">'
@@ -720,7 +889,7 @@ describe('Tooltip', () => {
tooltipEl.addEventListener('hidden.bs.tooltip', () => {
expect(document.querySelector('.tooltip')).toBeNull()
- expect(EventHandler.off).toHaveBeenCalled()
+ expect(EventHandler.off).toHaveBeenCalledWith(jasmine.any(Object), 'mouseover', noop)
document.documentElement.ontouchstart = undefined
done()
})
@@ -789,18 +958,18 @@ describe('Tooltip', () => {
})
describe('update', () => {
- it('should call popper schedule update', done => {
+ it('should call popper update', done => {
fixtureEl.innerHTML = '<a href="#" rel="tooltip" title="Another tooltip">'
const tooltipEl = fixtureEl.querySelector('a')
const tooltip = new Tooltip(tooltipEl)
tooltipEl.addEventListener('shown.bs.tooltip', () => {
- spyOn(tooltip._popper, 'scheduleUpdate')
+ spyOn(tooltip._popper, 'update')
tooltip.update()
- expect(tooltip._popper.scheduleUpdate).toHaveBeenCalled()
+ expect(tooltip._popper.update).toHaveBeenCalled()
done()
})
@@ -1206,11 +1375,9 @@ describe('Tooltip', () => {
jQueryMock.fn.tooltip = Tooltip.jQueryInterface
jQueryMock.elements = [div]
- try {
+ expect(() => {
jQueryMock.fn.tooltip.call(jQueryMock, action)
- } catch (error) {
- expect(error.message).toEqual(`No method named "${action}"`)
- }
+ }).toThrowError(TypeError, `No method named "${action}"`)
})
})
})
diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js
new file mode 100644
index 000000000..0a20a13bc
--- /dev/null
+++ b/js/tests/unit/util/backdrop.spec.js
@@ -0,0 +1,241 @@
+import Backdrop from '../../../src/util/backdrop'
+import { getTransitionDurationFromElement } from '../../../src/util/index'
+import { clearFixture, getFixture } from '../../helpers/fixture'
+
+const CLASS_BACKDROP = '.modal-backdrop'
+const CLASS_NAME_FADE = 'fade'
+const CLASS_NAME_SHOW = 'show'
+
+describe('Backdrop', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ const list = document.querySelectorAll(CLASS_BACKDROP)
+
+ list.forEach(el => {
+ document.body.removeChild(el)
+ })
+ })
+
+ describe('show', () => {
+ it('if it is "shown", should append the backdrop html once, on show, and contain "show" class', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: false
+ })
+ const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
+
+ expect(getElements().length).toEqual(0)
+
+ instance.show()
+ instance.show(() => {
+ expect(getElements().length).toEqual(1)
+ getElements().forEach(el => {
+ expect(el.classList.contains(CLASS_NAME_SHOW)).toEqual(true)
+ })
+ done()
+ })
+ })
+
+ it('if it is not "shown", should not append the backdrop html', done => {
+ const instance = new Backdrop({
+ isVisible: false,
+ isAnimated: true
+ })
+ const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
+
+ expect(getElements().length).toEqual(0)
+ instance.show(() => {
+ expect(getElements().length).toEqual(0)
+ done()
+ })
+ })
+
+ it('if it is "shown" and "animated", should append the backdrop html once, and contain "fade" class', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: true
+ })
+ const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
+
+ expect(getElements().length).toEqual(0)
+
+ instance.show(() => {
+ expect(getElements().length).toEqual(1)
+ getElements().forEach(el => {
+ expect(el.classList.contains(CLASS_NAME_FADE)).toEqual(true)
+ })
+ done()
+ })
+ })
+
+ it('Should be appended on "document.body" by default', done => {
+ const instance = new Backdrop({
+ isVisible: true
+ })
+ const getElement = () => document.querySelector(CLASS_BACKDROP)
+ instance.show(() => {
+ expect(getElement().parentElement).toEqual(document.body)
+ done()
+ })
+ })
+
+ it('Should appended on any element given by the proper config', done => {
+ fixtureEl.innerHTML = [
+ '<div id="wrapper">',
+ '</div>'
+ ].join('')
+
+ const wrapper = fixtureEl.querySelector('#wrapper')
+ const instance = new Backdrop({
+ isVisible: true,
+ rootElement: wrapper
+ })
+ const getElement = () => document.querySelector(CLASS_BACKDROP)
+ instance.show(() => {
+ expect(getElement().parentElement).toEqual(wrapper)
+ done()
+ })
+ })
+ })
+
+ describe('hide', () => {
+ it('should remove the backdrop html', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: true
+ })
+
+ const getElements = () => document.body.querySelectorAll(CLASS_BACKDROP)
+
+ expect(getElements().length).toEqual(0)
+ instance.show(() => {
+ expect(getElements().length).toEqual(1)
+ instance.hide(() => {
+ expect(getElements().length).toEqual(0)
+ done()
+ })
+ })
+ })
+
+ it('should remove "show" class', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: true
+ })
+ const elem = instance._getElement()
+
+ instance.show()
+ instance.hide(() => {
+ expect(elem.classList.contains(CLASS_NAME_SHOW)).toEqual(false)
+ done()
+ })
+ })
+
+ it('if it is not "shown", should not try to remove Node on remove method', done => {
+ const instance = new Backdrop({
+ isVisible: false,
+ isAnimated: true
+ })
+ const getElements = () => document.querySelectorAll(CLASS_BACKDROP)
+ const spy = spyOn(instance, 'dispose').and.callThrough()
+
+ expect(getElements().length).toEqual(0)
+ expect(instance._isAppended).toEqual(false)
+ instance.show(() => {
+ instance.hide(() => {
+ expect(getElements().length).toEqual(0)
+ expect(spy).not.toHaveBeenCalled()
+ expect(instance._isAppended).toEqual(false)
+ done()
+ })
+ })
+ })
+ })
+
+ describe('click callback', () => {
+ it('it should execute callback on click', done => {
+ const spy = jasmine.createSpy('spy')
+
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: false,
+ clickCallback: () => spy()
+ })
+ const endTest = () => {
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled()
+ done()
+ }, 10)
+ }
+
+ instance.show(() => {
+ const clickEvent = document.createEvent('MouseEvents')
+ clickEvent.initEvent('mousedown', true, true)
+ document.querySelector(CLASS_BACKDROP).dispatchEvent(clickEvent)
+ endTest()
+ })
+ })
+ })
+
+ describe('animation callbacks', () => {
+ it('if it is animated, should show and hide backdrop after counting transition duration', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: true
+ })
+ const spy2 = jasmine.createSpy('spy2')
+
+ const execDone = () => {
+ setTimeout(() => {
+ expect(spy2).toHaveBeenCalledTimes(2)
+ done()
+ }, 10)
+ }
+
+ instance.show(spy2)
+ instance.hide(() => {
+ spy2()
+ execDone()
+ })
+ expect(spy2).not.toHaveBeenCalled()
+ })
+
+ it('if it is not animated, should show and hide backdrop without delay', done => {
+ const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
+ const instance = new Backdrop({
+ isVisible: true,
+ isAnimated: false
+ })
+ const spy2 = jasmine.createSpy('spy2')
+
+ instance.show(spy2)
+ instance.hide(spy2)
+
+ setTimeout(() => {
+ expect(spy2).toHaveBeenCalled()
+ expect(spy).not.toHaveBeenCalled()
+ done()
+ }, 10)
+ })
+
+ it('if it is not "shown", should not call delay callbacks', done => {
+ const instance = new Backdrop({
+ isVisible: false,
+ isAnimated: true
+ })
+ const spy = jasmine.createSpy('spy', getTransitionDurationFromElement)
+
+ instance.show()
+ instance.hide(() => {
+ expect(spy).not.toHaveBeenCalled()
+ done()
+ })
+ })
+ })
+})
diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js
index ecad59b4d..11b6f7fa4 100644
--- a/js/tests/unit/util/index.spec.js
+++ b/js/tests/unit/util/index.spec.js
@@ -57,6 +57,28 @@ describe('Util', () => {
expect(Util.getSelectorFromElement(testEl)).toEqual('.target')
})
+ it('should return null if a selector from a href is a url without an anchor', () => {
+ fixtureEl.innerHTML = [
+ '<a id="test" data-bs-target="#" href="foo/bar.html"></a>',
+ '<div class="target"></div>'
+ ].join('')
+
+ const testEl = fixtureEl.querySelector('#test')
+
+ expect(Util.getSelectorFromElement(testEl)).toBeNull()
+ })
+
+ it('should return the anchor if a selector from a href is a url', () => {
+ fixtureEl.innerHTML = [
+ '<a id="test" data-bs-target="#" href="foo/bar.html#target"></a>',
+ '<div id="target"></div>'
+ ].join('')
+
+ const testEl = fixtureEl.querySelector('#test')
+
+ expect(Util.getSelectorFromElement(testEl)).toEqual('#target')
+ })
+
it('should return null if selector not found', () => {
fixtureEl.innerHTML = '<a id="test" href=".target"></a>'
@@ -212,7 +234,7 @@ describe('Util', () => {
expect(() => {
Util.typeCheckConfig(namePlugin, config, defaultType)
- }).toThrow(new Error('COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".'))
+ }).toThrowError(TypeError, 'COLLAPSE: Option "parent" provided type "number" but expected type "(string|element)".')
})
it('should return null stringified when null is passed', () => {
@@ -295,6 +317,114 @@ describe('Util', () => {
})
})
+ describe('isDisabled', () => {
+ it('should return true if the element is not defined', () => {
+ expect(Util.isDisabled(null)).toEqual(true)
+ expect(Util.isDisabled(undefined)).toEqual(true)
+ expect(Util.isDisabled()).toEqual(true)
+ })
+
+ it('should return true if the element provided is not a dom element', () => {
+ expect(Util.isDisabled({})).toEqual(true)
+ expect(Util.isDisabled('test')).toEqual(true)
+ })
+
+ it('should return true if the element has disabled attribute', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <div id="element" disabled="disabled"></div>',
+ ' <div id="element1" disabled="true"></div>',
+ ' <div id="element2" disabled></div>',
+ '</div>'
+ ].join('')
+
+ const div = fixtureEl.querySelector('#element')
+ const div1 = fixtureEl.querySelector('#element1')
+ const div2 = fixtureEl.querySelector('#element2')
+
+ expect(Util.isDisabled(div)).toEqual(true)
+ expect(Util.isDisabled(div1)).toEqual(true)
+ expect(Util.isDisabled(div2)).toEqual(true)
+ })
+
+ it('should return false if the element has disabled attribute with "false" value, or doesn\'t have attribute', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <div id="element" disabled="false"></div>',
+ ' <div id="element1" ></div>',
+ '</div>'
+ ].join('')
+
+ const div = fixtureEl.querySelector('#element')
+ const div1 = fixtureEl.querySelector('#element1')
+
+ expect(Util.isDisabled(div)).toEqual(false)
+ expect(Util.isDisabled(div1)).toEqual(false)
+ })
+
+ it('should return false if the element is not disabled ', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <button id="button"></button>',
+ ' <select id="select"></select>',
+ ' <select id="input"></select>',
+ '</div>'
+ ].join('')
+
+ const el = selector => fixtureEl.querySelector(selector)
+
+ expect(Util.isDisabled(el('#button'))).toEqual(false)
+ expect(Util.isDisabled(el('#select'))).toEqual(false)
+ expect(Util.isDisabled(el('#input'))).toEqual(false)
+ })
+ it('should return true if the element has disabled attribute', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <input id="input" disabled="disabled"/>',
+ ' <input id="input1" disabled="disabled"/>',
+ ' <button id="button" disabled="true"></button>',
+ ' <button id="button1" disabled="disabled"></button>',
+ ' <button id="button2" disabled></button>',
+ ' <select id="select" disabled></select>',
+ ' <select id="input" disabled></select>',
+ '</div>'
+ ].join('')
+
+ const el = selector => fixtureEl.querySelector(selector)
+
+ expect(Util.isDisabled(el('#input'))).toEqual(true)
+ expect(Util.isDisabled(el('#input1'))).toEqual(true)
+ expect(Util.isDisabled(el('#button'))).toEqual(true)
+ expect(Util.isDisabled(el('#button1'))).toEqual(true)
+ expect(Util.isDisabled(el('#button2'))).toEqual(true)
+ expect(Util.isDisabled(el('#input'))).toEqual(true)
+ })
+
+ it('should return true if the element has class "disabled"', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <div id="element" class="disabled"></div>',
+ '</div>'
+ ].join('')
+
+ const div = fixtureEl.querySelector('#element')
+
+ expect(Util.isDisabled(div)).toEqual(true)
+ })
+
+ it('should return true if the element has class "disabled" but disabled attribute is false', () => {
+ fixtureEl.innerHTML = [
+ '<div>',
+ ' <input id="input" class="disabled" disabled="false"/>',
+ '</div>'
+ ].join('')
+
+ const div = fixtureEl.querySelector('#input')
+
+ expect(Util.isDisabled(div)).toEqual(true)
+ })
+ })
+
describe('findShadowRoot', () => {
it('should return null if shadow dom is not available', () => {
// Only for newer browsers
@@ -347,8 +477,8 @@ describe('Util', () => {
})
describe('noop', () => {
- it('should return a function', () => {
- expect(typeof Util.noop()).toEqual('function')
+ it('should be a function', () => {
+ expect(typeof Util.noop).toEqual('function')
})
})
@@ -413,4 +543,37 @@ describe('Util', () => {
expect(spy).toHaveBeenCalled()
})
})
+
+ describe('defineJQueryPlugin', () => {
+ const fakejQuery = { fn: {} }
+
+ beforeEach(() => {
+ Object.defineProperty(window, 'jQuery', {
+ value: fakejQuery,
+ writable: true
+ })
+ })
+
+ afterEach(() => {
+ window.jQuery = undefined
+ })
+
+ it('should define a plugin on the jQuery instance', () => {
+ const pluginMock = function () {}
+ pluginMock.jQueryInterface = function () {}
+
+ Util.defineJQueryPlugin('test', pluginMock)
+ expect(fakejQuery.fn.test).toBe(pluginMock.jQueryInterface)
+ expect(fakejQuery.fn.test.Constructor).toBe(pluginMock)
+ expect(typeof fakejQuery.fn.test.noConflict).toEqual('function')
+ })
+ })
+
+ describe('execute', () => {
+ it('should execute if arg is function', () => {
+ const spy = jasmine.createSpy('spy')
+ Util.execute(spy)
+ expect(spy).toHaveBeenCalled()
+ })
+ })
})
diff --git a/js/tests/unit/util/sanitizer.spec.js b/js/tests/unit/util/sanitizer.spec.js
index 869b8c561..7379d221f 100644
--- a/js/tests/unit/util/sanitizer.spec.js
+++ b/js/tests/unit/util/sanitizer.spec.js
@@ -66,5 +66,15 @@ describe('Sanitizer', () => {
expect(result).toEqual(template)
expect(DOMParser.prototype.parseFromString).not.toHaveBeenCalled()
})
+
+ it('should allow multiple sanitation passes of the same template', () => {
+ const template = '<img src="test.jpg">'
+
+ const firstResult = sanitizeHtml(template, DefaultAllowlist, null)
+ const secondResult = sanitizeHtml(template, DefaultAllowlist, null)
+
+ expect(firstResult).toContain('src')
+ expect(secondResult).toContain('src')
+ })
})
})
diff --git a/js/tests/unit/util/scrollbar.spec.js b/js/tests/unit/util/scrollbar.spec.js
new file mode 100644
index 000000000..e09a37ce7
--- /dev/null
+++ b/js/tests/unit/util/scrollbar.spec.js
@@ -0,0 +1,261 @@
+import * as Scrollbar from '../../../src/util/scrollbar'
+import { clearBodyAndDocument, clearFixture, getFixture } from '../../helpers/fixture'
+import Manipulator from '../../../src/dom/manipulator'
+
+describe('ScrollBar', () => {
+ let fixtureEl
+ const parseInt = arg => Number.parseInt(arg, 10)
+ const getRightPadding = el => parseInt(window.getComputedStyle(el).paddingRight)
+ const getOverFlow = el => el.style.overflow
+ const getPaddingAttr = el => Manipulator.getDataAttribute(el, 'padding-right')
+ const getOverFlowAttr = el => Manipulator.getDataAttribute(el, 'overflow')
+ const windowCalculations = () => {
+ return {
+ htmlClient: document.documentElement.clientWidth,
+ htmlOffset: document.documentElement.offsetWidth,
+ docClient: document.body.clientWidth,
+ htmlBound: document.documentElement.getBoundingClientRect().width,
+ bodyBound: document.body.getBoundingClientRect().width,
+ window: window.innerWidth,
+ width: Math.abs(window.innerWidth - document.documentElement.clientWidth)
+ }
+ }
+
+ const isScrollBarHidden = () => { // IOS devices, Android devices and Browsers on Mac, hide scrollbar by default and appear it, only while scrolling. So the tests for scrollbar would fail
+ const calc = windowCalculations()
+ return calc.htmlClient === calc.htmlOffset && calc.htmlClient === calc.window
+ }
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ // custom fixture to avoid extreme style values
+ fixtureEl.removeAttribute('style')
+ })
+
+ afterAll(() => {
+ fixtureEl.remove()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ clearBodyAndDocument()
+ })
+
+ beforeEach(() => {
+ clearBodyAndDocument()
+ })
+
+ describe('isBodyOverflowing', () => {
+ it('should return true if body is overflowing', () => {
+ document.documentElement.style.overflowY = 'scroll'
+ document.body.style.overflowY = 'scroll'
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh; width: 100%"></div>'
+ ].join('')
+ const result = Scrollbar.isBodyOverflowing()
+
+ if (isScrollBarHidden()) {
+ expect(result).toEqual(false)
+ } else {
+ expect(result).toEqual(true)
+ }
+ })
+
+ it('should return false if body is overflowing', () => {
+ document.documentElement.style.overflowY = 'hidden'
+ document.body.style.overflowY = 'hidden'
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh; width: 100%"></div>'
+ ].join('')
+ const result = Scrollbar.isBodyOverflowing()
+
+ expect(result).toEqual(false)
+ })
+ })
+
+ describe('getWidth', () => {
+ it('should return an integer greater than zero, if body is overflowing', () => {
+ document.documentElement.style.overflowY = 'scroll'
+ document.body.style.overflowY = 'scroll'
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh; width: 100%"></div>'
+ ].join('')
+ const result = Scrollbar.getWidth()
+
+ if (isScrollBarHidden()) {
+ expect(result).toBe(0)
+ } else {
+ expect(result).toBeGreaterThan(1)
+ }
+ })
+
+ it('should return 0 if body is not overflowing', () => {
+ document.documentElement.style.overflowY = 'hidden'
+ document.body.style.overflowY = 'hidden'
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh; width: 100%"></div>'
+ ].join('')
+
+ const result = Scrollbar.getWidth()
+
+ expect(result).toEqual(0)
+ })
+ })
+
+ describe('hide - reset', () => {
+ it('should adjust the inline padding of fixed elements which are full-width', done => {
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh; width: 100%">' +
+ '<div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>',
+ '<div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>',
+ '</div>'
+ ].join('')
+ document.documentElement.style.overflowY = 'scroll'
+
+ const fixedEl = fixtureEl.querySelector('#fixed1')
+ const fixedEl2 = fixtureEl.querySelector('#fixed2')
+ const originalPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
+ const originalPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
+ const expectedPadding = originalPadding + Scrollbar.getWidth()
+ const expectedPadding2 = originalPadding2 + Scrollbar.getWidth()
+
+ Scrollbar.hide()
+
+ let currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
+ let currentPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
+ expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual('0px', 'original fixed element padding should be stored in data-bs-padding-right')
+ expect(fixedEl2.getAttribute('data-bs-padding-right')).toEqual('5px', 'original fixed element padding should be stored in data-bs-padding-right')
+ expect(currentPadding).toEqual(expectedPadding, 'fixed element padding should be adjusted while opening')
+ expect(currentPadding2).toEqual(expectedPadding2, 'fixed element padding should be adjusted while opening')
+
+ Scrollbar.reset()
+ currentPadding = Number.parseInt(window.getComputedStyle(fixedEl).paddingRight, 10)
+ currentPadding2 = Number.parseInt(window.getComputedStyle(fixedEl2).paddingRight, 10)
+ expect(fixedEl.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing')
+ expect(fixedEl2.getAttribute('data-bs-padding-right')).toEqual(null, 'data-bs-padding-right should be cleared after closing')
+ expect(currentPadding).toEqual(originalPadding, 'fixed element padding should be reset after closing')
+ expect(currentPadding2).toEqual(originalPadding2, 'fixed element padding should be reset after closing')
+ done()
+ })
+
+ it('should adjust the inline margin of sticky elements', done => {
+ fixtureEl.innerHTML = [
+ '<div style="height: 110vh">' +
+ '<div class="sticky-top" style="margin-right: 0px; width: 100vw; height: 10px"></div>',
+ '</div>'
+ ].join('')
+ document.documentElement.style.overflowY = 'scroll'
+
+ const stickyTopEl = fixtureEl.querySelector('.sticky-top')
+ const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ const expectedMargin = originalMargin - Scrollbar.getWidth()
+ Scrollbar.hide()
+
+ let currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual('0px', 'original sticky element margin should be stored in data-bs-margin-right')
+ expect(currentMargin).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening')
+
+ Scrollbar.reset()
+ currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+
+ expect(stickyTopEl.getAttribute('data-bs-margin-right')).toEqual(null, 'data-bs-margin-right should be cleared after closing')
+ expect(currentMargin).toEqual(originalMargin, 'sticky element margin should be reset after closing')
+ done()
+ })
+
+ it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => {
+ fixtureEl.innerHTML = [
+ '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>'
+ ].join('')
+
+ const stickyTopEl = fixtureEl.querySelector('.sticky-top')
+ const originalMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ const originalPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
+
+ Scrollbar.hide()
+
+ const currentMargin = Number.parseInt(window.getComputedStyle(stickyTopEl).marginRight, 10)
+ const currentPadding = Number.parseInt(window.getComputedStyle(stickyTopEl).paddingRight, 10)
+
+ expect(currentMargin).toEqual(originalMargin, 'sticky element\'s margin should not be adjusted while opening')
+ expect(currentPadding).toEqual(originalPadding, 'sticky element\'s padding should not be adjusted while opening')
+
+ Scrollbar.reset()
+ })
+
+ describe('Body Handling', () => {
+ it('should hide scrollbar and reset it to its initial value', () => {
+ const styleSheetPadding = '7px'
+ fixtureEl.innerHTML = [
+ '<style>',
+ ' body {',
+ ` padding-right: ${styleSheetPadding} }`,
+ ' }',
+ '</style>'
+ ].join('')
+
+ const el = document.body
+ const inlineStylePadding = '10px'
+ el.style.paddingRight = inlineStylePadding
+
+ const originalPadding = getRightPadding(el)
+ expect(originalPadding).toEqual(parseInt(inlineStylePadding)) // Respect only the inline style as it has prevails this of css
+ const originalOverFlow = 'auto'
+ el.style.overflow = originalOverFlow
+ const scrollBarWidth = Scrollbar.getWidth()
+
+ Scrollbar.hide()
+
+ const currentPadding = getRightPadding(el)
+
+ expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
+ expect(currentPadding).toEqual(scrollBarWidth + parseInt(inlineStylePadding))
+ expect(getPaddingAttr(el)).toEqual(inlineStylePadding)
+ expect(getOverFlow(el)).toEqual('hidden')
+ expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
+
+ Scrollbar.reset()
+
+ const currentPadding1 = getRightPadding(el)
+ expect(currentPadding1).toEqual(originalPadding)
+ expect(getPaddingAttr(el)).toEqual(null)
+ expect(getOverFlow(el)).toEqual(originalOverFlow)
+ expect(getOverFlowAttr(el)).toEqual(null)
+ })
+
+ it('should hide scrollbar and reset it to its initial value - respecting css rules', () => {
+ const styleSheetPadding = '7px'
+ fixtureEl.innerHTML = [
+ '<style>',
+ ' body {',
+ ` padding-right: ${styleSheetPadding} }`,
+ ' }',
+ '</style>'
+ ].join('')
+ const el = document.body
+ const originalPadding = getRightPadding(el)
+ const originalOverFlow = 'scroll'
+ el.style.overflow = originalOverFlow
+ const scrollBarWidth = Scrollbar.getWidth()
+
+ Scrollbar.hide()
+
+ const currentPadding = getRightPadding(el)
+
+ expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
+ expect(currentPadding).toEqual(scrollBarWidth + parseInt(styleSheetPadding))
+ expect(getPaddingAttr(el)).toBeNull() // We do not have to keep css padding
+ expect(getOverFlow(el)).toEqual('hidden')
+ expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
+
+ Scrollbar.reset()
+
+ const currentPadding1 = getRightPadding(el)
+ expect(currentPadding1).toEqual(originalPadding)
+ expect(getPaddingAttr(el)).toEqual(null)
+ expect(getOverFlow(el)).toEqual(originalOverFlow)
+ expect(getOverFlowAttr(el)).toEqual(null)
+ })
+ })
+ })
+})
diff --git a/js/tests/visual/alert.html b/js/tests/visual/alert.html
index 07f9b972d..c5e2efafa 100644
--- a/js/tests/visual/alert.html
+++ b/js/tests/visual/alert.html
@@ -46,6 +46,7 @@
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/alert.js"></script>
</body>
</html>
diff --git a/js/tests/visual/button.html b/js/tests/visual/button.html
index 75ad13209..0bd2f1097 100644
--- a/js/tests/visual/button.html
+++ b/js/tests/visual/button.html
@@ -47,6 +47,7 @@
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/button.js"></script>
</body>
</html>
diff --git a/js/tests/visual/carousel.html b/js/tests/visual/carousel.html
index 51f58e417..2a7f793f6 100644
--- a/js/tests/visual/carousel.html
+++ b/js/tests/visual/carousel.html
@@ -18,11 +18,11 @@
<p>The transition duration should be around 2s. Also, the carousel shouldn't slide when its window/tab is hidden. Check the console log.</p>
<div id="carousel-example-generic" class="carousel slide" data-bs-ride="carousel">
- <ol class="carousel-indicators">
- <li data-bs-target="#carousel-example-generic" data-bs-slide-to="0" class="active"></li>
- <li data-bs-target="#carousel-example-generic" data-bs-slide-to="1"></li>
- <li data-bs-target="#carousel-example-generic" data-bs-slide-to="2"></li>
- </ol>
+ <div class="carousel-indicators">
+ <button type="button" data-bs-target="#carousel-example-generic" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
+ <button type="button" data-bs-target="#carousel-example-generic" data-bs-slide-to="1" aria-label="Slide 2"></button>
+ <button type="button" data-bs-target="#carousel-example-generic" data-bs-slide-to="2" aria-label="Slide 3"></button>
+ </div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="https://i.imgur.com/iEZgY7Y.jpg" alt="First slide">
@@ -34,14 +34,14 @@
<img src="https://i.imgur.com/Nm7xoti.jpg" alt="Third slide">
</div>
</div>
- <a class="carousel-control-prev" href="#carousel-example-generic" role="button" data-bs-slide="prev">
+ <button class="carousel-control-prev" data-bs-target="#carousel-example-generic" type="button" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
- </a>
- <a class="carousel-control-next" href="#carousel-example-generic" role="button" data-bs-slide="next">
+ </button>
+ <button class="carousel-control-next" data-bs-target="#carousel-example-generic" type="button" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
- </a>
+ </button>
</div>
</div>
@@ -49,6 +49,7 @@
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/carousel.js"></script>
<script>
var t0
diff --git a/js/tests/visual/collapse.html b/js/tests/visual/collapse.html
index 3f38ec747..e4e534bcf 100644
--- a/js/tests/visual/collapse.html
+++ b/js/tests/visual/collapse.html
@@ -75,6 +75,7 @@
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/collapse.js"></script>
</body>
</html>
diff --git a/js/tests/visual/dropdown.html b/js/tests/visual/dropdown.html
index 930940a15..f1dd705f3 100644
--- a/js/tests/visual/dropdown.html
+++ b/js/tests/visual/dropdown.html
@@ -18,8 +18,8 @@
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav">
- <li class="nav-item active">
- <a class="nav-link" href="#" aria-current="page">Home</a>
+ <li class="nav-item">
+ <a class="nav-link active" href="#" aria-current="page">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
@@ -78,7 +78,9 @@
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</div>
+ </div>
+ <div class="col-sm-12 mt-4">
<div class="btn-group dropup">
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Dropup</button>
<ul class="dropdown-menu">
@@ -172,16 +174,6 @@
<div class="row">
<div class="col-sm-3 mt-4">
<div class="btn-group dropdown">
- <button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" data-bs-offset="10,20">Dropdown offset</button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#">Action</a></li>
- <li><a class="dropdown-item" href="#">Another action</a></li>
- <li><a class="dropdown-item" href="#">Something else here</a></li>
- </ul>
- </div>
- </div>
- <div class="col-sm-3 mt-4">
- <div class="btn-group dropdown">
<button type="button" class="btn btn-secondary">Dropdown reference</button>
<button type="button" class="btn btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" data-bs-reference="parent">
<span class="visually-hidden">Dropdown split</span>
@@ -206,14 +198,14 @@
</div>
</div>
</div>
-
</div>
- <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+ <script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/dropdown.js"></script>
<script src="../../dist/collapse.js"></script>
</body>
diff --git a/js/tests/visual/modal.html b/js/tests/visual/modal.html
index b39c4fd72..8ad9f63fb 100644
--- a/js/tests/visual/modal.html
+++ b/js/tests/visual/modal.html
@@ -18,8 +18,8 @@
<div class="collapse navbar-expand-md" id="navbarResponsive">
<a class="navbar-brand" href="#">This shouldn't jump!</a>
<ul class="navbar-nav">
- <li class="nav-item active">
- <a class="nav-link" href="#" aria-current="page">Home</a>
+ <li class="nav-item">
+ <a class="nav-link active" href="#" aria-current="page">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
@@ -204,6 +204,7 @@
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/modal.js"></script>
<script src="../../dist/collapse.js"></script>
<script src="../../dist/tooltip.js"></script>
diff --git a/js/tests/visual/popover.html b/js/tests/visual/popover.html
index c75825396..986ac571e 100644
--- a/js/tests/visual/popover.html
+++ b/js/tests/visual/popover.html
@@ -31,11 +31,12 @@
</button>
</div>
- <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+ <script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/tooltip.js"></script>
<script src="../../dist/popover.js"></script>
<script>
diff --git a/js/tests/visual/scrollspy.html b/js/tests/visual/scrollspy.html
index 7b07932eb..1e3215b6f 100644
--- a/js/tests/visual/scrollspy.html
+++ b/js/tests/visual/scrollspy.html
@@ -90,6 +90,7 @@
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/scrollspy.js"></script>
<script src="../../dist/dropdown.js"></script>
<script src="../../dist/collapse.js"></script>
diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html
index 0f7b734e7..16c19de63 100644
--- a/js/tests/visual/tab.html
+++ b/js/tests/visual/tab.html
@@ -19,16 +19,16 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
- <a class="nav-link active" data-bs-toggle="tab" href="#home" role="tab">Home</a>
+ <button type="button" class="nav-link active" data-bs-toggle="tab" data-bs-target="#home" role="tab" aria-selected="true">Home</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#profile" role="tab">Profile</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#profile" role="tab">Profile</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#fat" role="tab">@fat</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#fat" role="tab">@fat</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#mdo" role="tab">@mdo</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#mdo" role="tab">@mdo</button>
</li>
</ul>
@@ -55,16 +55,16 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
- <a class="nav-link active" data-bs-toggle="tab" href="#home2" role="tab">Home</a>
+ <button type="button" class="nav-link active" data-bs-toggle="tab" data-bs-target="#home2" role="tab" aria-selected="true">Home</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#profile2" role="tab">Profile</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#profile2" role="tab">Profile</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#fat2" role="tab">@fat</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#fat2" role="tab">@fat</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#mdo2" role="tab">@mdo</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#mdo2" role="tab">@mdo</button>
</li>
</ul>
@@ -91,16 +91,16 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#home3" role="tab">Home</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#home3" role="tab">Home</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#profile3" role="tab">Profile</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#profile3" role="tab">Profile</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#fat3" role="tab">@fat</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#fat3" role="tab">@fat</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#mdo3" role="tab">@mdo</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#mdo3" role="tab">@mdo</button>
</li>
</ul>
@@ -127,16 +127,16 @@
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#home4" role="tab">Home</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#home4" role="tab">Home</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#profile4" role="tab">Profile</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#profile4" role="tab">Profile</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#fat4" role="tab">@fat</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#fat4" role="tab">@fat</button>
</li>
<li class="nav-item" role="presentation">
- <a class="nav-link" data-bs-toggle="tab" href="#mdo4" role="tab">@mdo</a>
+ <button type="button" class="nav-link" data-bs-toggle="tab" data-bs-target="#mdo4" role="tab">@mdo</button>
</li>
</ul>
@@ -159,26 +159,23 @@
</div>
</div>
- <h4>Tabs with nav (with fade)</h4>
- <nav class="nav nav-pills">
- <a class="nav-link nav-item active" data-bs-toggle="tab" href="#home5">Home</a>
- <a class="nav-link nav-item" data-bs-toggle="tab" href="#profile5">Profile</a>
- <div class="nav-item dropdown">
- <a class="nav-link dropdown-toggle" href="#" id="dropdown5" data-bs-toggle="dropdown" aria-expanded="false">Dropdown</a>
- <ul class="dropdown-menu" aria-labelledby="dropdown5">
- <li><a class="dropdown-item" data-bs-toggle="tab" href="#fat5">@fat</a></li>
- <li><a class="dropdown-item" data-bs-toggle="tab" href="#mdo5">@mdo</a></li>
- </ul>
- </div>
- <a class="nav-link nav-item disabled" href="#">Disabled</a>
+ <h4>Tabs with nav and using links (with fade)</h4>
+ <nav>
+ <div class="nav nav-pills" id="nav-tab" role="tablist">
+ <a class="nav-link nav-item active" role="tab" data-bs-toggle="tab" href="#home5">Home</a>
+ <a class="nav-link nav-item" role="tab" data-bs-toggle="tab" href="#profile5">Profile</a>
+ <a class="nav-link nav-item" role="tab" data-bs-toggle="tab" href="#fat5">@fat</a>
+ <a class="nav-link nav-item" role="tab" data-bs-toggle="tab" href="#mdo5">@mdo</a>
+ <a class="nav-link nav-item disabled" role="tab" href="#">Disabled</a>
+ </div>
</nav>
- <div class="tab-content" role="tabpanel">
- <div role="tabpanel" class="tab-pane fade show active" id="home5">
+ <div class="tab-content">
+ <div class="tab-pane fade show active" id="home5" role="tabpanel">
<p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p>
<p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p>
</div>
- <div role="tabpanel" class="tab-pane fade" id="profile5">
+ <div class="tab-pane fade" id="profile5" role="tabpanel">
<p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p>
<p>Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.</p>
</div>
@@ -196,10 +193,10 @@
<div class="row">
<div class="col-4">
<div class="list-group" id="list-tab" role="tablist">
- <a class="list-group-item list-group-item-action active" id="list-home-list" data-bs-toggle="tab" href="#list-home" role="tab" aria-controls="list-home">Home</a>
- <a class="list-group-item list-group-item-action" id="list-profile-list" data-bs-toggle="tab" href="#list-profile" role="tab" aria-controls="list-profile">Profile</a>
- <a class="list-group-item list-group-item-action" id="list-messages-list" data-bs-toggle="tab" href="#list-messages" role="tab" aria-controls="list-messages">Messages</a>
- <a class="list-group-item list-group-item-action" id="list-settings-list" data-bs-toggle="tab" href="#list-settings" role="tab" aria-controls="list-settings">Settings</a>
+ <button type="button" class="list-group-item list-group-item-action active" id="list-home-list" data-bs-toggle="tab" data-bs-target="#list-home" role="tab" aria-controls="list-home" aria-selected="true">Home</button>
+ <button type="button" class="list-group-item list-group-item-action" id="list-profile-list" data-bs-toggle="tab" data-bs-target="#list-profile" role="tab" aria-controls="list-profile">Profile</button>
+ <button type="button" class="list-group-item list-group-item-action" id="list-messages-list" data-bs-toggle="tab" data-bs-target="#list-messages" role="tab" aria-controls="list-messages">Messages</button>
+ <button type="button" class="list-group-item list-group-item-action" id="list-settings-list" data-bs-toggle="tab" data-bs-target="#list-settings" role="tab" aria-controls="list-settings">Settings</button>
</div>
</div>
<div class="col-8">
@@ -226,6 +223,7 @@
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/data.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/tab.js"></script>
<script src="../../dist/dropdown.js"></script>
</body>
diff --git a/js/tests/visual/toast.html b/js/tests/visual/toast.html
index 4765026f3..3971a6075 100644
--- a/js/tests/visual/toast.html
+++ b/js/tests/visual/toast.html
@@ -53,6 +53,7 @@
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/toast.js"></script>
<script>
window.addEventListener('load', function () {
diff --git a/js/tests/visual/tooltip.html b/js/tests/visual/tooltip.html
index bade26a6c..370b0da77 100644
--- a/js/tests/visual/tooltip.html
+++ b/js/tests/visual/tooltip.html
@@ -68,11 +68,12 @@
<div id="customContainer"></div>
</div>
- <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script>
+ <script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script>
<script src="../../dist/dom/selector-engine.js"></script>
<script src="../../dist/dom/event-handler.js"></script>
<script src="../../dist/dom/manipulator.js"></script>
<script src="../../dist/dom/data.js"></script>
+ <script src="../../dist/base-component.js"></script>
<script src="../../dist/tooltip.js"></script>
<script>
if (typeof document.body.attachShadow === 'function') {