aboutsummaryrefslogtreecommitdiff
path: root/js/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'js/tests/unit')
-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
21 files changed, 2969 insertions, 523 deletions
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)
+ })
+ })
+ })
+})