aboutsummaryrefslogtreecommitdiff
path: root/js/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'js/tests/unit')
-rw-r--r--js/tests/unit/alert.spec.js21
-rw-r--r--js/tests/unit/base-component.spec.js14
-rw-r--r--js/tests/unit/carousel.spec.js71
-rw-r--r--js/tests/unit/collapse.spec.js2
-rw-r--r--js/tests/unit/dom/manipulator.spec.js54
-rw-r--r--js/tests/unit/dom/selector-engine.spec.js82
-rw-r--r--js/tests/unit/dropdown.spec.js26
-rw-r--r--js/tests/unit/modal.spec.js83
-rw-r--r--js/tests/unit/offcanvas.spec.js32
-rw-r--r--js/tests/unit/scrollspy.spec.js15
-rw-r--r--js/tests/unit/toast.spec.js4
-rw-r--r--js/tests/unit/util/backdrop.spec.js84
-rw-r--r--js/tests/unit/util/component-functions.spec.js108
-rw-r--r--js/tests/unit/util/focustrap.spec.js210
-rw-r--r--js/tests/unit/util/index.spec.js18
15 files changed, 694 insertions, 130 deletions
diff --git a/js/tests/unit/alert.spec.js b/js/tests/unit/alert.spec.js
index 53dc0700c..72cd23d89 100644
--- a/js/tests/unit/alert.spec.js
+++ b/js/tests/unit/alert.spec.js
@@ -2,7 +2,7 @@ import Alert from '../../src/alert'
import { getTransitionDurationFromElement } from '../../src/util/index'
/** Test helpers */
-import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture'
+import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture'
describe('Alert', () => {
let fixtureEl
@@ -102,25 +102,20 @@ describe('Alert', () => {
it('should not remove alert if close event is prevented', done => {
fixtureEl.innerHTML = '<div class="alert"></div>'
- const alertEl = document.querySelector('.alert')
+ const getAlert = () => document.querySelector('.alert')
+ const alertEl = getAlert()
const alert = new Alert(alertEl)
- const endTest = () => {
+ alertEl.addEventListener('close.bs.alert', event => {
+ event.preventDefault()
setTimeout(() => {
- expect(alert._removeElement).not.toHaveBeenCalled()
+ expect(getAlert()).not.toBeNull()
done()
}, 10)
- }
-
- spyOn(alert, '_removeElement')
-
- alertEl.addEventListener('close.bs.alert', event => {
- event.preventDefault()
- endTest()
})
alertEl.addEventListener('closed.bs.alert', () => {
- endTest()
+ throw new Error('should not fire closed event')
})
alert.close()
@@ -167,9 +162,9 @@ describe('Alert', () => {
jQueryMock.fn.alert = Alert.jQueryInterface
jQueryMock.elements = [alertEl]
+ expect(Alert.getInstance(alertEl)).toBeNull()
jQueryMock.fn.alert.call(jQueryMock, 'close')
- expect(Alert.getInstance(alertEl)).not.toBeNull()
expect(fixtureEl.querySelector('.alert')).toBeNull()
})
diff --git a/js/tests/unit/base-component.spec.js b/js/tests/unit/base-component.spec.js
index 1000f2841..b8ec83f12 100644
--- a/js/tests/unit/base-component.spec.js
+++ b/js/tests/unit/base-component.spec.js
@@ -104,6 +104,20 @@ describe('Base Component', () => {
expect(DummyClass.getInstance(element)).toBeInstanceOf(DummyClass)
})
+ it('should accept element, either passed as a CSS selector, jQuery element, or DOM element', () => {
+ createInstance()
+
+ expect(DummyClass.getInstance('#foo')).toEqual(instance)
+ expect(DummyClass.getInstance(element)).toEqual(instance)
+
+ const fakejQueryObject = {
+ 0: element,
+ jquery: 'foo'
+ }
+
+ expect(DummyClass.getInstance(fakejQueryObject)).toEqual(instance)
+ })
+
it('should return null when there is no instance', () => {
fixtureEl.innerHTML = '<div></div>'
diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js
index 74f82ce1f..97482537a 100644
--- a/js/tests/unit/carousel.spec.js
+++ b/js/tests/unit/carousel.spec.js
@@ -203,6 +203,26 @@ describe('Carousel', () => {
expect(spySlide).not.toHaveBeenCalled()
})
+ it('should not slide if arrow key is pressed and carousel is sliding', () => {
+ fixtureEl.innerHTML = '<div></div>'
+
+ const carouselEl = fixtureEl.querySelector('div')
+ const carousel = new Carousel(carouselEl, {})
+
+ spyOn(carousel, '_triggerSlideEvent')
+
+ carousel._isSliding = true;
+
+ ['ArrowLeft', 'ArrowRight'].forEach(key => {
+ const keydown = createEvent('keydown')
+ keydown.key = key
+
+ carouselEl.dispatchEvent(keydown)
+ })
+
+ expect(carousel._triggerSlideEvent).not.toHaveBeenCalled()
+ })
+
it('should wrap around from end to start when wrap option is true', done => {
fixtureEl.innerHTML = [
'<div id="myCarousel" class="carousel slide">',
@@ -487,6 +507,49 @@ describe('Carousel', () => {
})
})
+ it('should not slide when swiping and carousel is sliding', done => {
+ Simulator.setType('touch')
+ clearPointerEvents()
+ document.documentElement.ontouchstart = () => {}
+
+ fixtureEl.innerHTML = [
+ '<div class="carousel" data-bs-interval="false">',
+ ' <div class="carousel-inner">',
+ ' <div id="item" class="carousel-item active">',
+ ' <img alt="">',
+ ' </div>',
+ ' <div class="carousel-item">',
+ ' <img alt="">',
+ ' </div>',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const carouselEl = fixtureEl.querySelector('.carousel')
+ const carousel = new Carousel(carouselEl)
+ carousel._isSliding = true
+
+ spyOn(carousel, '_triggerSlideEvent')
+
+ Simulator.gestures.swipe(carouselEl, {
+ deltaX: 300,
+ deltaY: 0
+ })
+
+ Simulator.gestures.swipe(carouselEl, {
+ pos: [300, 10],
+ deltaX: -300,
+ deltaY: 0
+ })
+
+ setTimeout(() => {
+ expect(carousel._triggerSlideEvent).not.toHaveBeenCalled()
+ delete document.documentElement.ontouchstart
+ restorePointerEvents()
+ done()
+ }, 300)
+ })
+
it('should not allow pinch with touch events', done => {
Simulator.setType('touch')
clearPointerEvents()
@@ -552,12 +615,12 @@ describe('Carousel', () => {
const carouselEl = fixtureEl.querySelector('div')
const carousel = new Carousel(carouselEl, {})
- spyOn(carousel, '_slide')
+ spyOn(carousel, '_triggerSlideEvent')
carousel._isSliding = true
carousel.next()
- expect(carousel._slide).not.toHaveBeenCalled()
+ expect(carousel._triggerSlideEvent).not.toHaveBeenCalled()
})
it('should not fire slid when slide is prevented', done => {
@@ -763,12 +826,12 @@ describe('Carousel', () => {
const carouselEl = fixtureEl.querySelector('div')
const carousel = new Carousel(carouselEl, {})
- spyOn(carousel, '_slide')
+ spyOn(carousel, '_triggerSlideEvent')
carousel._isSliding = true
carousel.prev()
- expect(carousel._slide).not.toHaveBeenCalled()
+ expect(carousel._triggerSlideEvent).not.toHaveBeenCalled()
})
})
diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js
index 11d8d52bf..9bce3f0bb 100644
--- a/js/tests/unit/collapse.spec.js
+++ b/js/tests/unit/collapse.spec.js
@@ -225,7 +225,7 @@ describe('Collapse', () => {
})
it('should show a collapsed element on width', done => {
- fixtureEl.innerHTML = '<div class="collapse width" style="width: 0px;"></div>'
+ fixtureEl.innerHTML = '<div class="collapse collapse-horizontal" style="width: 0px;"></div>'
const collapseEl = fixtureEl.querySelector('div')
const collapse = new Collapse(collapseEl, {
diff --git a/js/tests/unit/dom/manipulator.spec.js b/js/tests/unit/dom/manipulator.spec.js
index 3d91e6f74..4f8e40067 100644
--- a/js/tests/unit/dom/manipulator.spec.js
+++ b/js/tests/unit/dom/manipulator.spec.js
@@ -119,6 +119,60 @@ describe('Manipulator', () => {
expect(offset.top).toEqual(jasmine.any(Number))
expect(offset.left).toEqual(jasmine.any(Number))
})
+
+ it('should return offset relative to attached element\'s offset', () => {
+ const top = 500
+ const left = 1000
+
+ fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>`
+
+ const div = fixtureEl.querySelector('div')
+ const offset = Manipulator.offset(div)
+ const fixtureOffset = Manipulator.offset(fixtureEl)
+
+ expect(offset).toEqual({
+ top: fixtureOffset.top + top,
+ left: fixtureOffset.left + left
+ })
+ })
+
+ it('should not change offset when viewport is scrolled', done => {
+ const top = 500
+ const left = 1000
+ const scrollY = 200
+ const scrollX = 400
+
+ fixtureEl.innerHTML = `<div style="position:absolute;top:${top}px;left:${left}px"></div>`
+
+ const div = fixtureEl.querySelector('div')
+ const offset = Manipulator.offset(div)
+
+ // append an element that forces scrollbars on the window so we can scroll
+ const { defaultView: win, body } = fixtureEl.ownerDocument
+ const forceScrollBars = document.createElement('div')
+ forceScrollBars.style.cssText = 'position:absolute;top:5000px;left:5000px;width:1px;height:1px'
+ body.appendChild(forceScrollBars)
+
+ const scrollHandler = () => {
+ expect(window.pageYOffset).toBe(scrollY)
+ expect(window.pageXOffset).toBe(scrollX)
+
+ const newOffset = Manipulator.offset(div)
+
+ expect(newOffset).toEqual({
+ top: offset.top,
+ left: offset.left
+ })
+
+ win.removeEventListener('scroll', scrollHandler)
+ body.removeChild(forceScrollBars)
+ win.scrollTo(0, 0)
+ done()
+ }
+
+ win.addEventListener('scroll', scrollHandler)
+ win.scrollTo(scrollX, scrollY)
+ })
})
describe('position', () => {
diff --git a/js/tests/unit/dom/selector-engine.spec.js b/js/tests/unit/dom/selector-engine.spec.js
index d108a2efb..08c3ae818 100644
--- a/js/tests/unit/dom/selector-engine.spec.js
+++ b/js/tests/unit/dom/selector-engine.spec.js
@@ -156,5 +156,87 @@ describe('SelectorEngine', () => {
expect(SelectorEngine.next(divTest, '.btn')).toEqual([btn])
})
})
+
+ describe('focusableChildren', () => {
+ it('should return only elements with specific tag names', () => {
+ fixtureEl.innerHTML = [
+ '<div>lorem</div>',
+ '<span>lorem</span>',
+ '<a>lorem</a>',
+ '<button>lorem</button>',
+ '<input />',
+ '<textarea></textarea>',
+ '<select></select>',
+ '<details>lorem</details>'
+ ].join('')
+
+ const expectedElements = [
+ fixtureEl.querySelector('a'),
+ fixtureEl.querySelector('button'),
+ fixtureEl.querySelector('input'),
+ fixtureEl.querySelector('textarea'),
+ fixtureEl.querySelector('select'),
+ fixtureEl.querySelector('details')
+ ]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return any element with non negative tab index', () => {
+ fixtureEl.innerHTML = [
+ '<div tabindex>lorem</div>',
+ '<div tabindex="0">lorem</div>',
+ '<div tabindex="10">lorem</div>'
+ ].join('')
+
+ const expectedElements = [
+ fixtureEl.querySelector('[tabindex]'),
+ fixtureEl.querySelector('[tabindex="0"]'),
+ fixtureEl.querySelector('[tabindex="10"]')
+ ]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return not return elements with negative tab index', () => {
+ fixtureEl.innerHTML = [
+ '<button tabindex="-1">lorem</button>'
+ ].join('')
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should return contenteditable elements', () => {
+ fixtureEl.innerHTML = [
+ '<div contenteditable="true">lorem</div>'
+ ].join('')
+
+ const expectedElements = [fixtureEl.querySelector('[contenteditable="true"]')]
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should not return disabled elements', () => {
+ fixtureEl.innerHTML = [
+ '<button disabled="true">lorem</button>'
+ ].join('')
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+
+ it('should not return invisible elements', () => {
+ fixtureEl.innerHTML = [
+ '<button style="display:none;">lorem</button>'
+ ].join('')
+
+ const expectedElements = []
+
+ expect(SelectorEngine.focusableChildren(fixtureEl)).toEqual(expectedElements)
+ })
+ })
})
diff --git a/js/tests/unit/dropdown.spec.js b/js/tests/unit/dropdown.spec.js
index 9703eaab1..2b6d8cd78 100644
--- a/js/tests/unit/dropdown.spec.js
+++ b/js/tests/unit/dropdown.spec.js
@@ -59,26 +59,6 @@ describe('Dropdown', () => {
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('')
-
- 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 a function', done => {
fixtureEl.innerHTML = [
'<div class="dropdown">',
@@ -943,21 +923,19 @@ 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).not.toBeNull()
expect(dropdown._element).not.toBeNull()
- expect(btnDropdown.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
+ spyOn(EventHandler, 'off')
dropdown.dispose()
expect(dropdown._menu).toBeNull()
expect(dropdown._element).toBeNull()
- expect(btnDropdown.removeEventListener).toHaveBeenCalledWith('click', jasmine.any(Function), jasmine.any(Boolean))
+ expect(EventHandler.off).toHaveBeenCalledWith(btnDropdown, Dropdown.EVENT_KEY)
})
it('should dispose dropdown with Popper', () => {
diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js
index e6ef555e7..212f98ca8 100644
--- a/js/tests/unit/modal.spec.js
+++ b/js/tests/unit/modal.spec.js
@@ -249,7 +249,7 @@ describe('Modal', () => {
modal.show()
})
- it('should close modal when a click occurred on data-bs-dismiss="modal"', done => {
+ it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', done => {
fixtureEl.innerHTML = [
'<div class="modal fade">',
' <div class="modal-dialog">',
@@ -278,6 +278,33 @@ describe('Modal', () => {
modal.show()
})
+ it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', done => {
+ fixtureEl.innerHTML = [
+ '<button type="button" data-bs-dismiss="modal" data-bs-target="#modal1"></button>',
+ '<div id="modal1" class="modal fade">',
+ ' <div class="modal-dialog">',
+ ' </div>',
+ '</div>'
+ ].join('')
+
+ const modalEl = fixtureEl.querySelector('.modal')
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
+ const modal = new Modal(modalEl)
+
+ spyOn(modal, 'hide').and.callThrough()
+
+ modalEl.addEventListener('shown.bs.modal', () => {
+ btnClose.click()
+ })
+
+ modalEl.addEventListener('hidden.bs.modal', () => {
+ expect(modal.hide).toHaveBeenCalled()
+ done()
+ })
+
+ modal.show()
+ })
+
it('should set .modal\'s scroll top to 0', done => {
fixtureEl.innerHTML = [
'<div class="modal fade">',
@@ -318,7 +345,7 @@ describe('Modal', () => {
modal.show()
})
- it('should not enforce focus if focus equal to false', done => {
+ it('should not trap focus if focus equal to false', done => {
fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
@@ -326,10 +353,10 @@ describe('Modal', () => {
focus: false
})
- spyOn(modal, '_enforceFocus')
+ spyOn(modal._focustrap, 'activate').and.callThrough()
modalEl.addEventListener('shown.bs.modal', () => {
- expect(modal._enforceFocus).not.toHaveBeenCalled()
+ expect(modal._focustrap.activate).not.toHaveBeenCalled()
done()
})
@@ -561,33 +588,17 @@ describe('Modal', () => {
modal.show()
})
- it('should enforce focus', done => {
+ it('should trap focus', done => {
fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl)
- spyOn(modal, '_enforceFocus').and.callThrough()
-
- const focusInListener = () => {
- expect(modal._element.focus).toHaveBeenCalled()
- document.removeEventListener('focusin', focusInListener)
- done()
- }
+ spyOn(modal._focustrap, 'activate').and.callThrough()
modalEl.addEventListener('shown.bs.modal', () => {
- expect(modal._enforceFocus).toHaveBeenCalled()
-
- spyOn(modal._element, 'focus')
-
- document.addEventListener('focusin', focusInListener)
-
- const focusInEvent = createEvent('focusin', { bubbles: true })
- Object.defineProperty(focusInEvent, 'target', {
- value: fixtureEl
- })
-
- document.dispatchEvent(focusInEvent)
+ expect(modal._focustrap.activate).toHaveBeenCalled()
+ done()
})
modal.show()
@@ -694,6 +705,25 @@ describe('Modal', () => {
modal.show()
})
+
+ it('should release focus trap', done => {
+ fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
+
+ const modalEl = fixtureEl.querySelector('.modal')
+ const modal = new Modal(modalEl)
+ spyOn(modal._focustrap, 'deactivate').and.callThrough()
+
+ modalEl.addEventListener('shown.bs.modal', () => {
+ modal.hide()
+ })
+
+ modalEl.addEventListener('hidden.bs.modal', () => {
+ expect(modal._focustrap.deactivate).toHaveBeenCalled()
+ done()
+ })
+
+ modal.show()
+ })
})
describe('dispose', () => {
@@ -702,6 +732,8 @@ describe('Modal', () => {
const modalEl = fixtureEl.querySelector('.modal')
const modal = new Modal(modalEl)
+ const focustrap = modal._focustrap
+ spyOn(focustrap, 'deactivate').and.callThrough()
expect(Modal.getInstance(modalEl)).toEqual(modal)
@@ -710,7 +742,8 @@ describe('Modal', () => {
modal.dispose()
expect(Modal.getInstance(modalEl)).toBeNull()
- expect(EventHandler.off).toHaveBeenCalledTimes(4)
+ expect(EventHandler.off).toHaveBeenCalledTimes(3)
+ expect(focustrap.deactivate).toHaveBeenCalled()
})
})
diff --git a/js/tests/unit/offcanvas.spec.js b/js/tests/unit/offcanvas.spec.js
index a13875b51..ecbb710a5 100644
--- a/js/tests/unit/offcanvas.spec.js
+++ b/js/tests/unit/offcanvas.spec.js
@@ -219,7 +219,7 @@ describe('Offcanvas', () => {
offCanvas.show()
})
- it('should not enforce focus if focus scroll is allowed', done => {
+ it('should not trap focus if scroll is allowed', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
@@ -227,10 +227,10 @@ describe('Offcanvas', () => {
scroll: true
})
- spyOn(offCanvas, '_enforceFocusOnElement')
+ spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
- expect(offCanvas._enforceFocusOnElement).not.toHaveBeenCalled()
+ expect(offCanvas._focustrap.activate).not.toHaveBeenCalled()
done()
})
@@ -345,16 +345,16 @@ describe('Offcanvas', () => {
expect(Offcanvas.prototype.show).toHaveBeenCalled()
})
- it('should enforce focus', done => {
+ it('should trap focus', done => {
fixtureEl.innerHTML = '<div class="offcanvas"></div>'
const offCanvasEl = fixtureEl.querySelector('.offcanvas')
const offCanvas = new Offcanvas(offCanvasEl)
- spyOn(offCanvas, '_enforceFocusOnElement')
+ spyOn(offCanvas._focustrap, 'activate').and.callThrough()
offCanvasEl.addEventListener('shown.bs.offcanvas', () => {
- expect(offCanvas._enforceFocusOnElement).toHaveBeenCalled()
+ expect(offCanvas._focustrap.activate).toHaveBeenCalled()
done()
})
@@ -421,6 +421,22 @@ describe('Offcanvas', () => {
offCanvas.hide()
})
+
+ it('should release focus trap', done => {
+ fixtureEl.innerHTML = '<div class="offcanvas"></div>'
+
+ const offCanvasEl = fixtureEl.querySelector('div')
+ const offCanvas = new Offcanvas(offCanvasEl)
+ spyOn(offCanvas._focustrap, 'deactivate').and.callThrough()
+ offCanvas.show()
+
+ offCanvasEl.addEventListener('hidden.bs.offcanvas', () => {
+ expect(offCanvas._focustrap.deactivate).toHaveBeenCalled()
+ done()
+ })
+
+ offCanvas.hide()
+ })
})
describe('dispose', () => {
@@ -431,6 +447,8 @@ describe('Offcanvas', () => {
const offCanvas = new Offcanvas(offCanvasEl)
const backdrop = offCanvas._backdrop
spyOn(backdrop, 'dispose').and.callThrough()
+ const focustrap = offCanvas._focustrap
+ spyOn(focustrap, 'deactivate').and.callThrough()
expect(Offcanvas.getInstance(offCanvasEl)).toEqual(offCanvas)
@@ -440,6 +458,8 @@ describe('Offcanvas', () => {
expect(backdrop.dispose).toHaveBeenCalled()
expect(offCanvas._backdrop).toBeNull()
+ expect(focustrap.deactivate).toHaveBeenCalled()
+ expect(offCanvas._focustrap).toBeNull()
expect(Offcanvas.getInstance(offCanvasEl)).toEqual(null)
})
})
diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js
index 8724b8369..ad44d5b3c 100644
--- a/js/tests/unit/scrollspy.spec.js
+++ b/js/tests/unit/scrollspy.spec.js
@@ -65,21 +65,6 @@ describe('ScrollSpy', () => {
expect(sSpyByElement._element).toEqual(sSpyEl)
})
- it('should generate an id when there is not one', () => {
- fixtureEl.innerHTML = [
- '<nav></nav>',
- '<div class="content"></div>'
- ].join('')
-
- const navEl = fixtureEl.querySelector('nav')
- const scrollSpy = new ScrollSpy(fixtureEl.querySelector('.content'), {
- target: navEl
- })
-
- expect(scrollSpy).toBeDefined()
- expect(navEl.getAttribute('id')).not.toEqual(null)
- })
-
it('should not process element without target', () => {
fixtureEl.innerHTML = [
'<nav id="navigation" class="navbar">',
diff --git a/js/tests/unit/toast.spec.js b/js/tests/unit/toast.spec.js
index 59d0247b2..c491650b1 100644
--- a/js/tests/unit/toast.spec.js
+++ b/js/tests/unit/toast.spec.js
@@ -467,18 +467,14 @@ 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)).not.toBeNull()
- 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 => {
diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js
index 3150ba14d..59b789071 100644
--- a/js/tests/unit/util/backdrop.spec.js
+++ b/js/tests/unit/util/backdrop.spec.js
@@ -230,46 +230,62 @@ describe('Backdrop', () => {
})
})
})
-
- describe('rootElement initialization', () => {
- 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()
+ describe('Config', () => {
+ describe('rootElement initialization', () => {
+ 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 find the rootElement if passed as a string', done => {
- const instance = new Backdrop({
- isVisible: true,
- rootElement: 'body'
- })
- const getElement = () => document.querySelector(CLASS_BACKDROP)
- instance.show(() => {
- expect(getElement().parentElement).toEqual(document.body)
- done()
+ it('Should find the rootElement if passed as a string', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ rootElement: 'body'
+ })
+ 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('')
+ 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 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()
+ })
})
- const getElement = () => document.querySelector(CLASS_BACKDROP)
- instance.show(() => {
- expect(getElement().parentElement).toEqual(wrapper)
- done()
+ })
+
+ describe('ClassName', () => {
+ it('Should be able to have different classNames than default', done => {
+ const instance = new Backdrop({
+ isVisible: true,
+ className: 'foo'
+ })
+ const getElement = () => document.querySelector('.foo')
+ instance.show(() => {
+ expect(getElement()).toEqual(instance._getElement())
+ instance.dispose()
+ done()
+ })
})
})
})
diff --git a/js/tests/unit/util/component-functions.spec.js b/js/tests/unit/util/component-functions.spec.js
new file mode 100644
index 000000000..edaedd32e
--- /dev/null
+++ b/js/tests/unit/util/component-functions.spec.js
@@ -0,0 +1,108 @@
+/* Test helpers */
+
+import { clearFixture, createEvent, getFixture } from '../../helpers/fixture'
+import { enableDismissTrigger } from '../../../src/util/component-functions'
+import BaseComponent from '../../../src/base-component'
+
+class DummyClass2 extends BaseComponent {
+ static get NAME() {
+ return 'test'
+ }
+
+ hide() {
+ return true
+ }
+
+ testMethod() {
+ return true
+ }
+}
+
+describe('Plugin functions', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('data-bs-dismiss functionality', () => {
+ it('should get Plugin and execute the given method, when a click occurred on data-bs-dismiss="PluginName"', () => {
+ fixtureEl.innerHTML = [
+ '<div id="foo" class="test">',
+ ' <button type="button" data-bs-dismiss="test" data-bs-target="#foo"></button>',
+ '</div>'
+ ].join('')
+
+ spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+ spyOn(DummyClass2.prototype, 'testMethod')
+ const componentWrapper = fixtureEl.querySelector('#foo')
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+ const event = createEvent('click')
+
+ enableDismissTrigger(DummyClass2, 'testMethod')
+ btnClose.dispatchEvent(event)
+
+ expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper)
+ expect(DummyClass2.prototype.testMethod).toHaveBeenCalled()
+ })
+
+ it('if data-bs-dismiss="PluginName" hasn\'t got "data-bs-target", "getOrCreateInstance" has to be initialized by closest "plugin.Name" class', () => {
+ fixtureEl.innerHTML = [
+ '<div id="foo" class="test">',
+ ' <button type="button" data-bs-dismiss="test"></button>',
+ '</div>'
+ ].join('')
+
+ spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+ spyOn(DummyClass2.prototype, 'hide')
+ const componentWrapper = fixtureEl.querySelector('#foo')
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+ const event = createEvent('click')
+
+ enableDismissTrigger(DummyClass2)
+ btnClose.dispatchEvent(event)
+
+ expect(DummyClass2.getOrCreateInstance).toHaveBeenCalledWith(componentWrapper)
+ expect(DummyClass2.prototype.hide).toHaveBeenCalled()
+ })
+
+ it('if data-bs-dismiss="PluginName" is disabled, must not trigger function', () => {
+ fixtureEl.innerHTML = [
+ '<div id="foo" class="test">',
+ ' <button type="button" disabled data-bs-dismiss="test"></button>',
+ '</div>'
+ ].join('')
+
+ spyOn(DummyClass2, 'getOrCreateInstance').and.callThrough()
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+ const event = createEvent('click')
+
+ enableDismissTrigger(DummyClass2)
+ btnClose.dispatchEvent(event)
+
+ expect(DummyClass2.getOrCreateInstance).not.toHaveBeenCalled()
+ })
+
+ it('should prevent default when the trigger is <a> or <area>', () => {
+ fixtureEl.innerHTML = [
+ '<div id="foo" class="test">',
+ ' <a type="button" data-bs-dismiss="test"></a>',
+ '</div>'
+ ].join('')
+
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="test"]')
+ const event = createEvent('click')
+
+ enableDismissTrigger(DummyClass2)
+ spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+ btnClose.dispatchEvent(event)
+
+ expect(Event.prototype.preventDefault).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/js/tests/unit/util/focustrap.spec.js b/js/tests/unit/util/focustrap.spec.js
new file mode 100644
index 000000000..2457239c4
--- /dev/null
+++ b/js/tests/unit/util/focustrap.spec.js
@@ -0,0 +1,210 @@
+import FocusTrap from '../../../src/util/focustrap'
+import EventHandler from '../../../src/dom/event-handler'
+import SelectorEngine from '../../../src/dom/selector-engine'
+import { clearFixture, getFixture, createEvent } from '../../helpers/fixture'
+
+describe('FocusTrap', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ })
+
+ describe('activate', () => {
+ it('should autofocus itself by default', () => {
+ fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
+
+ const trapElement = fixtureEl.querySelector('div')
+
+ spyOn(trapElement, 'focus')
+
+ const focustrap = new FocusTrap({ trapElement })
+ focustrap.activate()
+
+ expect(trapElement.focus).toHaveBeenCalled()
+ })
+
+ it('if configured not to autofocus, should not autofocus itself', () => {
+ fixtureEl.innerHTML = '<div id="focustrap" tabindex="-1"></div>'
+
+ const trapElement = fixtureEl.querySelector('div')
+
+ spyOn(trapElement, 'focus')
+
+ const focustrap = new FocusTrap({ trapElement, autofocus: false })
+ focustrap.activate()
+
+ expect(trapElement.focus).not.toHaveBeenCalled()
+ })
+
+ it('should force focus inside focus trap if it can', done => {
+ fixtureEl.innerHTML = [
+ '<a href="#" id="outside">outside</a>',
+ '<div id="focustrap" tabindex="-1">',
+ ' <a href="#" id="inside">inside</a>',
+ '</div>'
+ ].join('')
+
+ const trapElement = fixtureEl.querySelector('div')
+ const focustrap = new FocusTrap({ trapElement })
+ focustrap.activate()
+
+ const inside = document.getElementById('inside')
+
+ const focusInListener = () => {
+ expect(inside.focus).toHaveBeenCalled()
+ document.removeEventListener('focusin', focusInListener)
+ done()
+ }
+
+ spyOn(inside, 'focus')
+ spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [inside])
+
+ document.addEventListener('focusin', focusInListener)
+
+ const focusInEvent = createEvent('focusin', { bubbles: true })
+ Object.defineProperty(focusInEvent, 'target', {
+ value: document.getElementById('outside')
+ })
+
+ document.dispatchEvent(focusInEvent)
+ })
+
+ it('should wrap focus around foward on tab', done => {
+ fixtureEl.innerHTML = [
+ '<a href="#" id="outside">outside</a>',
+ '<div id="focustrap" tabindex="-1">',
+ ' <a href="#" id="first">first</a>',
+ ' <a href="#" id="inside">inside</a>',
+ ' <a href="#" id="last">last</a>',
+ '</div>'
+ ].join('')
+
+ const trapElement = fixtureEl.querySelector('div')
+ const focustrap = new FocusTrap({ trapElement })
+ focustrap.activate()
+
+ const first = document.getElementById('first')
+ const inside = document.getElementById('inside')
+ const last = document.getElementById('last')
+ const outside = document.getElementById('outside')
+
+ spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
+ spyOn(first, 'focus').and.callThrough()
+
+ const focusInListener = () => {
+ expect(first.focus).toHaveBeenCalled()
+ first.removeEventListener('focusin', focusInListener)
+ done()
+ }
+
+ first.addEventListener('focusin', focusInListener)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'Tab'
+
+ document.dispatchEvent(keydown)
+ outside.focus()
+ })
+
+ it('should wrap focus around backwards on shift-tab', done => {
+ fixtureEl.innerHTML = [
+ '<a href="#" id="outside">outside</a>',
+ '<div id="focustrap" tabindex="-1">',
+ ' <a href="#" id="first">first</a>',
+ ' <a href="#" id="inside">inside</a>',
+ ' <a href="#" id="last">last</a>',
+ '</div>'
+ ].join('')
+
+ const trapElement = fixtureEl.querySelector('div')
+ const focustrap = new FocusTrap({ trapElement })
+ focustrap.activate()
+
+ const first = document.getElementById('first')
+ const inside = document.getElementById('inside')
+ const last = document.getElementById('last')
+ const outside = document.getElementById('outside')
+
+ spyOn(SelectorEngine, 'focusableChildren').and.callFake(() => [first, inside, last])
+ spyOn(last, 'focus').and.callThrough()
+
+ const focusInListener = () => {
+ expect(last.focus).toHaveBeenCalled()
+ last.removeEventListener('focusin', focusInListener)
+ done()
+ }
+
+ last.addEventListener('focusin', focusInListener)
+
+ const keydown = createEvent('keydown')
+ keydown.key = 'Tab'
+ keydown.shiftKey = true
+
+ document.dispatchEvent(keydown)
+ outside.focus()
+ })
+
+ it('should force focus on itself if there is no focusable content', done => {
+ fixtureEl.innerHTML = [
+ '<a href="#" id="outside">outside</a>',
+ '<div id="focustrap" tabindex="-1"></div>'
+ ].join('')
+
+ const trapElement = fixtureEl.querySelector('div')
+ const focustrap = new FocusTrap({ trapElement })
+ focustrap.activate()
+
+ const focusInListener = () => {
+ expect(focustrap._config.trapElement.focus).toHaveBeenCalled()
+ document.removeEventListener('focusin', focusInListener)
+ done()
+ }
+
+ spyOn(focustrap._config.trapElement, 'focus')
+
+ document.addEventListener('focusin', focusInListener)
+
+ const focusInEvent = createEvent('focusin', { bubbles: true })
+ Object.defineProperty(focusInEvent, 'target', {
+ value: document.getElementById('outside')
+ })
+
+ document.dispatchEvent(focusInEvent)
+ })
+ })
+
+ describe('deactivate', () => {
+ it('should flag itself as no longer active', () => {
+ const focustrap = new FocusTrap({ trapElement: fixtureEl })
+ focustrap.activate()
+ expect(focustrap._isActive).toBe(true)
+
+ focustrap.deactivate()
+ expect(focustrap._isActive).toBe(false)
+ })
+
+ it('should remove all event listeners', () => {
+ const focustrap = new FocusTrap({ trapElement: fixtureEl })
+ focustrap.activate()
+
+ spyOn(EventHandler, 'off')
+ focustrap.deactivate()
+
+ expect(EventHandler.off).toHaveBeenCalled()
+ })
+
+ it('doesn\'t try removing event listeners unless it needs to (in case it hasn\'t been activated)', () => {
+ const focustrap = new FocusTrap({ trapElement: fixtureEl })
+
+ spyOn(EventHandler, 'off')
+ focustrap.deactivate()
+
+ expect(EventHandler.off).not.toHaveBeenCalled()
+ })
+ })
+})
diff --git a/js/tests/unit/util/index.spec.js b/js/tests/unit/util/index.spec.js
index 04ad6bf43..38e94dc6b 100644
--- a/js/tests/unit/util/index.spec.js
+++ b/js/tests/unit/util/index.spec.js
@@ -543,8 +543,9 @@ describe('Util', () => {
fixtureEl.innerHTML = '<div></div>'
const div = fixtureEl.querySelector('div')
-
- expect(Util.reflow(div)).toEqual(0)
+ const spy = spyOnProperty(div, 'offsetHeight')
+ Util.reflow(div)
+ expect(spy).toHaveBeenCalled()
})
})
@@ -582,15 +583,24 @@ describe('Util', () => {
})
describe('onDOMContentLoaded', () => {
- it('should execute callback when DOMContentLoaded is fired', () => {
+ it('should execute callbacks when DOMContentLoaded is fired and should not add more than one listener', () => {
const spy = jasmine.createSpy()
+ const spy2 = jasmine.createSpy()
+
+ spyOn(document, 'addEventListener').and.callThrough()
spyOnProperty(document, 'readyState').and.returnValue('loading')
+
Util.onDOMContentLoaded(spy)
- window.document.dispatchEvent(new Event('DOMContentLoaded', {
+ Util.onDOMContentLoaded(spy2)
+
+ document.dispatchEvent(new Event('DOMContentLoaded', {
bubbles: true,
cancelable: true
}))
+
expect(spy).toHaveBeenCalled()
+ expect(spy2).toHaveBeenCalled()
+ expect(document.addEventListener).toHaveBeenCalledTimes(1)
})
it('should execute callback if readyState is not "loading"', () => {