diff options
| author | XhmikosR <[email protected]> | 2021-09-07 21:49:51 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2021-09-07 21:49:51 +0300 |
| commit | 65413de3131294cbf7bc8bff94914cc8062149c6 (patch) | |
| tree | d606f927ef9d5e505245b0fce29366979d219f83 /js/tests | |
| parent | 72e79d0e3997c3a850263880240f26684b9784f7 (diff) | |
| parent | 0d81d3cbc14dfcdca8a868e3f25189a4f1ab273c (diff) | |
| download | bootstrap-docs-offcanvas-navbar.tar.xz bootstrap-docs-offcanvas-navbar.zip | |
Merge branch 'main' into docs-offcanvas-navbardocs-offcanvas-navbar
Diffstat (limited to 'js/tests')
25 files changed, 699 insertions, 96 deletions
diff --git a/js/tests/browsers.js b/js/tests/browsers.js index b8e47a364..665b26a2e 100644 --- a/js/tests/browsers.js +++ b/js/tests/browsers.js @@ -28,13 +28,27 @@ const browsers = { os: 'Windows', os_version: '10', browser: 'Chrome', - browser_version: 'latest' + browser_version: '60' }, firefoxWin10: { base: 'BrowserStack', os: 'Windows', os_version: '10', browser: 'Firefox', + browser_version: '60' + }, + chromeWin10Latest: { + base: 'BrowserStack', + os: 'Windows', + os_version: '10', + browser: 'Chrome', + browser_version: 'latest' + }, + firefoxWin10Latest: { + base: 'BrowserStack', + os: 'Windows', + os_version: '10', + browser: 'Firefox', browser_version: 'latest' }, iphone7: { diff --git a/js/tests/helpers/fixture.js b/js/tests/helpers/fixture.js index 3d6f395e8..2ce1081fc 100644 --- a/js/tests/helpers/fixture.js +++ b/js/tests/helpers/fixture.js @@ -11,7 +11,7 @@ export const getFixture = () => { fixtureEl.style.left = '-10000px' fixtureEl.style.width = '10000px' fixtureEl.style.height = '10000px' - document.body.appendChild(fixtureEl) + document.body.append(fixtureEl) } return fixtureEl diff --git a/js/tests/karma.conf.js b/js/tests/karma.conf.js index 67b60f15e..1d4b0d3e4 100644 --- a/js/tests/karma.conf.js +++ b/js/tests/karma.conf.js @@ -108,7 +108,7 @@ if (BROWSERSTACK) { conf.browserStack = { username: ENV.BROWSER_STACK_USERNAME, accessKey: ENV.BROWSER_STACK_ACCESS_KEY, - build: `bootstrap-${new Date().toISOString()}`, + build: `bootstrap-${ENV.GITHUB_SHA ? ENV.GITHUB_SHA.slice(0, 7) + '-' : ''}${new Date().toISOString()}`, project: 'Bootstrap', retryLimit: 2 } diff --git a/js/tests/unit/.eslintrc.json b/js/tests/unit/.eslintrc.json index adde3105d..6362a1acf 100644 --- a/js/tests/unit/.eslintrc.json +++ b/js/tests/unit/.eslintrc.json @@ -4,5 +4,10 @@ ], "env": { "jasmine": true + }, + "rules": { + "unicorn/consistent-function-scoping": "off", + "unicorn/no-useless-undefined": "off", + "unicorn/prefer-add-event-listener": "off" } } diff --git a/js/tests/unit/carousel.spec.js b/js/tests/unit/carousel.spec.js index 97482537a..a933f1eda 100644 --- a/js/tests/unit/carousel.spec.js +++ b/js/tests/unit/carousel.spec.js @@ -14,7 +14,7 @@ describe('Carousel', () => { const stylesCarousel = document.createElement('style') stylesCarousel.type = 'text/css' - stylesCarousel.appendChild(document.createTextNode(cssStyleCarousel)) + stylesCarousel.append(document.createTextNode(cssStyleCarousel)) const clearPointerEvents = () => { window.PointerEvent = null @@ -345,7 +345,7 @@ describe('Carousel', () => { } document.documentElement.ontouchstart = () => {} - document.head.appendChild(stylesCarousel) + document.head.append(stylesCarousel) Simulator.setType('pointer') fixtureEl.innerHTML = [ @@ -371,7 +371,7 @@ describe('Carousel', () => { expect(item.classList.contains('active')).toEqual(true) expect(carousel._slide).toHaveBeenCalledWith('right') expect(event.direction).toEqual('right') - document.head.removeChild(stylesCarousel) + stylesCarousel.remove() delete document.documentElement.ontouchstart done() }) @@ -390,7 +390,7 @@ describe('Carousel', () => { } document.documentElement.ontouchstart = () => {} - document.head.appendChild(stylesCarousel) + document.head.append(stylesCarousel) Simulator.setType('pointer') fixtureEl.innerHTML = [ @@ -416,7 +416,7 @@ describe('Carousel', () => { expect(item.classList.contains('active')).toEqual(false) expect(carousel._slide).toHaveBeenCalledWith('left') expect(event.direction).toEqual('left') - document.head.removeChild(stylesCarousel) + stylesCarousel.remove() delete document.documentElement.ontouchstart done() }) diff --git a/js/tests/unit/collapse.spec.js b/js/tests/unit/collapse.spec.js index 9bce3f0bb..ece88eff5 100644 --- a/js/tests/unit/collapse.spec.js +++ b/js/tests/unit/collapse.spec.js @@ -65,8 +65,7 @@ describe('Collapse', () => { parent: fakejQueryObject }) - expect(collapse._config.parent).toEqual(fakejQueryObject) - expect(collapse._getParent()).toEqual(myCollapseEl) + expect(collapse._config.parent).toEqual(myCollapseEl) }) it('should allow non jquery object in parent config', () => { @@ -104,8 +103,7 @@ describe('Collapse', () => { parent: 'div.my-collapse' }) - expect(collapse._config.parent).toEqual('div.my-collapse') - expect(collapse._getParent()).toEqual(myCollapseEl) + expect(collapse._config.parent).toEqual(myCollapseEl) }) }) @@ -269,6 +267,56 @@ describe('Collapse', () => { collapse.show() }) + it('should not change tab tabpanels descendants on accordion', done => { + fixtureEl.innerHTML = [ + '<div class="accordion" id="accordionExample">', + ' <div class="accordion-item">', + ' <h2 class="accordion-header" id="headingOne">', + ' <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">', + ' Accordion Item #1', + ' </button>', + ' </h2>', + ' <div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionExample">', + ' <div class="accordion-body">', + ' <nav>', + ' <div class="nav nav-tabs" id="nav-tab" role="tablist">', + ' <button class="nav-link active" id="nav-home-tab" data-bs-toggle="tab" data-bs-target="#nav-home" type="button" role="tab" aria-controls="nav-home" aria-selected="true">Home</button>', + ' <button class="nav-link" id="nav-profile-tab" data-bs-toggle="tab" data-bs-target="#nav-profile" type="button" role="tab" aria-controls="nav-profile" aria-selected="false">Profile</button>', + ' </div>', + ' </nav>', + ' <div class="tab-content" id="nav-tabContent">', + ' <div class="tab-pane fade show active" id="nav-home" role="tabpanel" aria-labelledby="nav-home-tab">Home</div>', + ' <div class="tab-pane fade" id="nav-profile" role="tabpanel" aria-labelledby="nav-profile-tab">Profile</div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>', + ' </div>' + ].join('') + + // const btn = fixtureEl.querySelector('[data-bs-target="#collapseOne"]') + const el = fixtureEl.querySelector('#collapseOne') + const activeTabPane = fixtureEl.querySelector('#nav-home') + const collapse = new Collapse(el) + let times = 1 + + el.addEventListener('hidden.bs.collapse', () => { + setTimeout(() => collapse.show(), 10) + }) + + el.addEventListener('shown.bs.collapse', () => { + expect(activeTabPane.classList.contains('show')).toEqual(true) + times++ + if (times === 2) { + done() + } + + collapse.hide() + }) + + collapse.show() + }) + it('should not fire shown when show is prevented', done => { fixtureEl.innerHTML = '<div class="collapse"></div>' diff --git a/js/tests/unit/dom/event-handler.spec.js b/js/tests/unit/dom/event-handler.spec.js index 4dc4ffc35..45f2d6e55 100644 --- a/js/tests/unit/dom/event-handler.spec.js +++ b/js/tests/unit/dom/event-handler.spec.js @@ -368,10 +368,10 @@ describe('EventHandler', () => { it('should remove the correct delegated event listener', () => { const element = document.createElement('div') const subelement = document.createElement('span') - element.appendChild(subelement) + element.append(subelement) const anchor = document.createElement('a') - element.appendChild(anchor) + element.append(anchor) let i = 0 const handler = () => { @@ -381,7 +381,7 @@ describe('EventHandler', () => { EventHandler.on(element, 'click', 'a', handler) EventHandler.on(element, 'click', 'span', handler) - fixtureEl.appendChild(element) + fixtureEl.append(element) EventHandler.trigger(anchor, 'click') EventHandler.trigger(subelement, 'click') diff --git a/js/tests/unit/dom/manipulator.spec.js b/js/tests/unit/dom/manipulator.spec.js index 3d91e6f74..13d0c3d17 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.append(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) + forceScrollBars.remove() + 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/jquery.spec.js b/js/tests/unit/jquery.spec.js index 289612df5..7513341a4 100644 --- a/js/tests/unit/jquery.spec.js +++ b/js/tests/unit/jquery.spec.js @@ -5,6 +5,7 @@ import Carousel from '../../src/carousel' import Collapse from '../../src/collapse' import Dropdown from '../../src/dropdown' import Modal from '../../src/modal' +import Offcanvas from '../../src/offcanvas' import Popover from '../../src/popover' import ScrollSpy from '../../src/scrollspy' import Tab from '../../src/tab' @@ -32,6 +33,7 @@ describe('jQuery', () => { expect(Collapse.jQueryInterface).toEqual(jQuery.fn.collapse) expect(Dropdown.jQueryInterface).toEqual(jQuery.fn.dropdown) expect(Modal.jQueryInterface).toEqual(jQuery.fn.modal) + expect(Offcanvas.jQueryInterface).toEqual(jQuery.fn.offcanvas) expect(Popover.jQueryInterface).toEqual(jQuery.fn.popover) expect(ScrollSpy.jQueryInterface).toEqual(jQuery.fn.scrollspy) expect(Tab.jQueryInterface).toEqual(jQuery.fn.tab) diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js index e6ef555e7..a65ed4afa 100644 --- a/js/tests/unit/modal.spec.js +++ b/js/tests/unit/modal.spec.js @@ -19,7 +19,7 @@ describe('Modal', () => { document.querySelectorAll('.modal-backdrop') .forEach(backdrop => { - document.body.removeChild(backdrop) + backdrop.remove() }) }) @@ -143,7 +143,7 @@ describe('Modal', () => { modalEl.addEventListener('shown.bs.modal', () => { const dynamicModal = document.getElementById(id) expect(dynamicModal).not.toBeNull() - dynamicModal.parentNode.removeChild(dynamicModal) + dynamicModal.remove() done() }) @@ -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() }) }) @@ -945,6 +978,29 @@ describe('Modal', () => { trigger.click() }) + + it('should call hide first, if another modal is open', done => { + fixtureEl.innerHTML = [ + '<button data-bs-toggle="modal" data-bs-target="#modal2"></button>', + '<div id="modal1" class="modal fade"><div class="modal-dialog"></div></div>', + '<div id="modal2" class="modal"><div class="modal-dialog"></div></div>' + ].join('') + + const trigger2 = fixtureEl.querySelector('button') + const modalEl1 = document.querySelector('#modal1') + const modalEl2 = document.querySelector('#modal2') + const modal1 = new Modal(modalEl1) + + modalEl1.addEventListener('shown.bs.modal', () => { + trigger2.click() + }) + modalEl1.addEventListener('hidden.bs.modal', () => { + expect(Modal.getInstance(modalEl2)).not.toBeNull() + expect(modalEl2.classList.contains('show')).toBeTrue() + done() + }) + modal1.show() + }) }) describe('jQueryInterface', () => { 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/popover.spec.js b/js/tests/unit/popover.spec.js index f5a163050..c54fc49ee 100644 --- a/js/tests/unit/popover.spec.js +++ b/js/tests/unit/popover.spec.js @@ -1,7 +1,7 @@ import Popover from '../../src/popover' /** Test helpers */ -import { getFixture, clearFixture, jQueryMock } from '../helpers/fixture' +import { clearFixture, getFixture, jQueryMock } from '../helpers/fixture' describe('Popover', () => { let fixtureEl @@ -16,7 +16,7 @@ describe('Popover', () => { const popoverList = document.querySelectorAll('.popover') popoverList.forEach(popoverEl => { - document.body.removeChild(popoverEl) + popoverEl.remove() }) }) @@ -157,6 +157,37 @@ describe('Popover', () => { popover.show() }) + it('should call setContent once', done => { + fixtureEl.innerHTML = '<a href="#">BS twitter</a>' + + const popoverEl = fixtureEl.querySelector('a') + const popover = new Popover(popoverEl, { + content: 'Popover content' + }) + + const spy = spyOn(popover, 'setContent').and.callThrough() + let times = 1 + + popoverEl.addEventListener('hidden.bs.popover', () => { + popover.show() + }) + + popoverEl.addEventListener('shown.bs.popover', () => { + const popoverDisplayed = document.querySelector('.popover') + + expect(popoverDisplayed).not.toBeNull() + expect(popoverDisplayed.querySelector('.popover-body').textContent).toEqual('Popover content') + expect(spy).toHaveBeenCalledTimes(1) + if (times > 1) { + done() + } + + times++ + popover.hide() + }) + popover.show() + }) + it('should show a popover with provided custom class', done => { fixtureEl.innerHTML = '<a href="#" title="Popover" data-bs-content="https://twitter.com/getbootstrap" data-bs-custom-class="custom-class">BS twitter</a>' diff --git a/js/tests/unit/tab.spec.js b/js/tests/unit/tab.spec.js index 621012c12..4bd9c7a73 100644 --- a/js/tests/unit/tab.spec.js +++ b/js/tests/unit/tab.spec.js @@ -333,8 +333,8 @@ describe('Tab', () => { const tabId = linkEl.getAttribute('href') const tabIdEl = fixtureEl.querySelector(tabId) - liEl.parentNode.removeChild(liEl) - tabIdEl.parentNode.removeChild(tabIdEl) + liEl.remove() + tabIdEl.remove() secondNavTab.show() }) 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/tooltip.spec.js b/js/tests/unit/tooltip.spec.js index 8f8524c89..22a7edd01 100644 --- a/js/tests/unit/tooltip.spec.js +++ b/js/tests/unit/tooltip.spec.js @@ -16,7 +16,7 @@ describe('Tooltip', () => { clearFixture() document.querySelectorAll('.tooltip').forEach(tooltipEl => { - document.body.removeChild(tooltipEl) + tooltipEl.remove() }) }) @@ -490,7 +490,7 @@ describe('Tooltip', () => { tooltipEl.removeEventListener('shown.bs.tooltip', firstCallback) let tooltipShown = document.querySelector('.tooltip') - tooltipShown.parentNode.removeChild(tooltipShown) + tooltipShown.remove() tooltipEl.addEventListener('shown.bs.tooltip', () => { tooltipShown = document.querySelector('.tooltip') @@ -1045,10 +1045,10 @@ describe('Tooltip', () => { const tooltipEl = fixtureEl.querySelector('a') const tooltip = new Tooltip(tooltipEl) - tooltip.setContent() - const tip = tooltip.getTipElement() + tooltip.setContent(tip) + expect(tip.classList.contains('show')).toEqual(false) expect(tip.classList.contains('fade')).toEqual(false) expect(tip.querySelector('.tooltip-inner').textContent).toEqual('Another tooltip') @@ -1129,7 +1129,7 @@ describe('Tooltip', () => { html: true }) - tooltip.getTipElement().appendChild(childContent) + tooltip.getTipElement().append(childContent) tooltip.setElementContent(tooltip.getTipElement(), childContent) expect().nothing() diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js index 59b789071..b885b60b5 100644 --- a/js/tests/unit/util/backdrop.spec.js +++ b/js/tests/unit/util/backdrop.spec.js @@ -18,7 +18,7 @@ describe('Backdrop', () => { const list = document.querySelectorAll(CLASS_BACKDROP) list.forEach(el => { - document.body.removeChild(el) + el.remove() }) }) @@ -141,7 +141,7 @@ describe('Backdrop', () => { const getElements = () => document.querySelectorAll(CLASS_BACKDROP) instance.show(() => { - wrapper.parentNode.removeChild(wrapper) + wrapper.remove() instance.hide(() => { expect(getElements().length).toEqual(0) 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..99bc95fca --- /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 forward 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 9b5d7b70e..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() }) }) diff --git a/js/tests/visual/carousel.html b/js/tests/visual/carousel.html index 2a7f793f6..561b6ce5a 100644 --- a/js/tests/visual/carousel.html +++ b/js/tests/visual/carousel.html @@ -60,7 +60,7 @@ // Test to show that transition-duration can be changed with css carousel.addEventListener('slid.bs.carousel', function (event) { t1 = performance.now() - console.log('transition-duration took ' + (t1 - t0) + 'ms, slid at ', event.timeStamp) + console.log('transition-duration took ' + (t1 - t0) + 'ms, slid at ' + event.timeStamp) }) carousel.addEventListener('slide.bs.carousel', function () { t0 = performance.now() diff --git a/js/tests/visual/modal.html b/js/tests/visual/modal.html index 8ad9f63fb..ac0a931af 100644 --- a/js/tests/visual/modal.html +++ b/js/tests/visual/modal.html @@ -199,7 +199,7 @@ </button> </div> - <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script> + <script src="../../../node_modules/@popperjs/core/dist/umd/popper.min.js"></script> <script src="../../dist/dom/event-handler.js"></script> <script src="../../dist/dom/selector-engine.js"></script> <script src="../../dist/dom/data.js"></script> diff --git a/js/tests/visual/tab.html b/js/tests/visual/tab.html index 16c19de63..0ce8be03c 100644 --- a/js/tests/visual/tab.html +++ b/js/tests/visual/tab.html @@ -218,13 +218,11 @@ </div> </div> - <script src="../../../node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="../../dist/dom/event-handler.js"></script> <script src="../../dist/dom/selector-engine.js"></script> <script src="../../dist/dom/data.js"></script> <script src="../../dist/dom/manipulator.js"></script> <script src="../../dist/base-component.js"></script> <script src="../../dist/tab.js"></script> - <script src="../../dist/dropdown.js"></script> </body> </html> diff --git a/js/tests/visual/tooltip.html b/js/tests/visual/tooltip.html index 370b0da77..ab6040920 100644 --- a/js/tests/visual/tooltip.html +++ b/js/tests/visual/tooltip.html @@ -45,9 +45,6 @@ </div> <div class="row"> <p> - <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="left" title="Tooltip with XSS" data-bs-container="<img src=1 onerror=alert(123)>"> - Tooltip with XSS - </button> <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="left" title="Tooltip with container (selector)" data-bs-container="#customContainer"> Tooltip with container (selector) </button> @@ -57,6 +54,9 @@ <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-html="true" title="<em>Tooltip</em> <u>with</u> <b>HTML</b>"> Tooltip with HTML </button> + <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="left" title="Tooltip with XSS" data-bs-container="<img src=1 onerror=alert(123)>"> + Tooltip with XSS + </button> </p> </div> <div class="row"> |
