diff options
Diffstat (limited to 'js/tests/unit/util')
| -rw-r--r-- | js/tests/unit/util/backdrop.spec.js | 292 | ||||
| -rw-r--r-- | js/tests/unit/util/component-functions.spec.js | 108 | ||||
| -rw-r--r-- | js/tests/unit/util/focustrap.spec.js | 210 | ||||
| -rw-r--r-- | js/tests/unit/util/index.spec.js | 434 | ||||
| -rw-r--r-- | js/tests/unit/util/sanitizer.spec.js | 10 | ||||
| -rw-r--r-- | js/tests/unit/util/scrollbar.spec.js | 353 |
6 files changed, 1367 insertions, 40 deletions
diff --git a/js/tests/unit/util/backdrop.spec.js b/js/tests/unit/util/backdrop.spec.js new file mode 100644 index 000000000..b885b60b5 --- /dev/null +++ b/js/tests/unit/util/backdrop.spec.js @@ -0,0 +1,292 @@ +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 => { + el.remove() + }) + }) + + 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() + }) + }) + }) + + 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() + }) + }) + }) + + it('should not error if the backdrop no longer has a parent', done => { + fixtureEl.innerHTML = '<div id="wrapper"></div>' + + const wrapper = fixtureEl.querySelector('#wrapper') + const instance = new Backdrop({ + isVisible: true, + isAnimated: true, + rootElement: wrapper + }) + + const getElements = () => document.querySelectorAll(CLASS_BACKDROP) + + instance.show(() => { + wrapper.remove() + instance.hide(() => { + expect(getElements().length).toEqual(0) + 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() + }) + }) + }) + 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 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('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 935e021dd..38e94dc6b 100644 --- a/js/tests/unit/util/index.spec.js +++ b/js/tests/unit/util/index.spec.js @@ -1,7 +1,7 @@ import * as Util from '../../../src/util/index' /** Test helpers */ -import { getFixture, clearFixture } from '../../helpers/fixture' +import { clearFixture, getFixture } from '../../helpers/fixture' describe('Util', () => { let fixtureEl @@ -157,12 +157,13 @@ describe('Util', () => { describe('triggerTransitionEnd', () => { it('should trigger transitionend event', done => { - fixtureEl.innerHTML = '<div style="transition: all 300ms ease-out;"></div>' + fixtureEl.innerHTML = '<div></div>' const el = fixtureEl.querySelector('div') + const spy = spyOn(el, 'dispatchEvent').and.callThrough() el.addEventListener('transitionend', () => { - expect().nothing() + expect(spy).toHaveBeenCalled() done() }) @@ -171,51 +172,58 @@ describe('Util', () => { }) describe('isElement', () => { - it('should detect if the parameter is an element or not', () => { - fixtureEl.innerHTML = '<div></div>' + it('should detect if the parameter is an element or not and return Boolean', () => { + fixtureEl.innerHTML = + [ + '<div id="foo" class="test"></div>', + '<div id="bar" class="test"></div>' + ].join('') - const el = document.querySelector('div') + const el = fixtureEl.querySelector('#foo') - expect(Util.isElement(el)).toEqual(el.nodeType) - expect(Util.isElement({})).toEqual(undefined) + expect(Util.isElement(el)).toEqual(true) + expect(Util.isElement({})).toEqual(false) + expect(Util.isElement(fixtureEl.querySelectorAll('.test'))).toEqual(false) }) it('should detect jQuery element', () => { fixtureEl.innerHTML = '<div></div>' - const el = document.querySelector('div') + const el = fixtureEl.querySelector('div') const fakejQuery = { - 0: el + 0: el, + jquery: 'foo' } - expect(Util.isElement(fakejQuery)).toEqual(el.nodeType) + expect(Util.isElement(fakejQuery)).toEqual(true) }) }) - describe('emulateTransitionEnd', () => { - it('should emulate transition end', () => { - fixtureEl.innerHTML = '<div></div>' - - const el = document.querySelector('div') - const spy = spyOn(window, 'setTimeout') - - Util.emulateTransitionEnd(el, 10) - expect(spy).toHaveBeenCalled() - }) - - it('should not emulate transition end if already triggered', done => { - fixtureEl.innerHTML = '<div></div>' + describe('getElement', () => { + it('should try to parse element', () => { + fixtureEl.innerHTML = + [ + '<div id="foo" class="test"></div>', + '<div id="bar" class="test"></div>' + ].join('') const el = fixtureEl.querySelector('div') - const spy = spyOn(el, 'removeEventListener') - Util.emulateTransitionEnd(el, 10) - Util.triggerTransitionEnd(el) + expect(Util.getElement(el)).toEqual(el) + expect(Util.getElement('#foo')).toEqual(el) + expect(Util.getElement('#fail')).toBeNull() + expect(Util.getElement({})).toBeNull() + expect(Util.getElement([])).toBeNull() + expect(Util.getElement()).toBeNull() + expect(Util.getElement(null)).toBeNull() + expect(Util.getElement(fixtureEl.querySelectorAll('.test'))).toBeNull() + + const fakejQueryObject = { + 0: el, + jquery: 'foo' + } - setTimeout(() => { - expect(spy).toHaveBeenCalled() - done() - }, 20) + expect(Util.getElement(fakejQueryObject)).toEqual(el) }) }) @@ -292,10 +300,14 @@ describe('Util', () => { expect(Util.isVisible(div)).toEqual(false) }) - it('should return false if the parent element is not visible', () => { + it('should return false if an ancestor element is display none', () => { fixtureEl.innerHTML = [ '<div style="display: none;">', - ' <div class="content"></div>', + ' <div>', + ' <div>', + ' <div class="content"></div>', + ' </div>', + ' </div>', '</div>' ].join('') @@ -304,6 +316,38 @@ describe('Util', () => { expect(Util.isVisible(div)).toEqual(false) }) + it('should return false if an ancestor element is visibility hidden', () => { + fixtureEl.innerHTML = [ + '<div style="visibility: hidden;">', + ' <div>', + ' <div>', + ' <div class="content"></div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const div = fixtureEl.querySelector('.content') + + expect(Util.isVisible(div)).toEqual(false) + }) + + it('should return true if an ancestor element is visibility hidden, but reverted', () => { + fixtureEl.innerHTML = [ + '<div style="visibility: hidden;">', + ' <div style="visibility: visible;">', + ' <div>', + ' <div class="content"></div>', + ' </div>', + ' </div>', + '</div>' + ].join('') + + const div = fixtureEl.querySelector('.content') + + expect(Util.isVisible(div)).toEqual(true) + }) + it('should return true if the element is visible', () => { fixtureEl.innerHTML = [ '<div>', @@ -315,6 +359,126 @@ describe('Util', () => { expect(Util.isVisible(div)).toEqual(true) }) + + it('should return false if the element is hidden, but not via display or visibility', () => { + fixtureEl.innerHTML = [ + '<details>', + ' <div id="element"></div>', + '</details>' + ].join('') + + const div = fixtureEl.querySelector('#element') + + expect(Util.isVisible(div)).toEqual(false) + }) + }) + + 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', () => { @@ -369,8 +533,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') }) }) @@ -379,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() }) }) @@ -418,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"', () => { @@ -452,12 +626,192 @@ describe('Util', () => { it('should define a plugin on the jQuery instance', () => { const pluginMock = function () {} + pluginMock.NAME = 'test' pluginMock.jQueryInterface = function () {} - Util.defineJQueryPlugin('test', pluginMock) + Util.defineJQueryPlugin(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() + }) + }) + + describe('executeAfterTransition', () => { + it('should immediately execute a function when waitForTransition parameter is false', () => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') + const eventListenerSpy = spyOn(el, 'addEventListener') + + Util.executeAfterTransition(callbackSpy, el, false) + + expect(callbackSpy).toHaveBeenCalled() + expect(eventListenerSpy).not.toHaveBeenCalled() + }) + + it('should execute a function when a transitionend event is dispatched', () => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, el) + + el.dispatchEvent(new TransitionEvent('transitionend')) + + expect(callbackSpy).toHaveBeenCalled() + }) + + it('should execute a function after a computed CSS transition duration and there was no transitionend event dispatched', done => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, el) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + done() + }, 70) + }) + + it('should not execute a function a second time after a computed CSS transition duration and if a transitionend event has already been dispatched', done => { + const el = document.createElement('div') + const callbackSpy = jasmine.createSpy('callback spy') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, el) + + setTimeout(() => { + el.dispatchEvent(new TransitionEvent('transitionend')) + }, 50) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalledTimes(1) + done() + }, 70) + }) + + it('should not trigger a transitionend event if another transitionend event had already happened', done => { + const el = document.createElement('div') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(() => {}, el) + + // simulate a event dispatched by the browser + el.dispatchEvent(new TransitionEvent('transitionend')) + + const dispatchSpy = spyOn(el, 'dispatchEvent').and.callThrough() + + setTimeout(() => { + // setTimeout should not have triggered another transitionend event. + expect(dispatchSpy).not.toHaveBeenCalled() + done() + }, 70) + }) + + it('should ignore transitionend events from nested elements', done => { + fixtureEl.innerHTML = [ + '<div class="outer">', + ' <div class="nested"></div>', + '</div>' + ].join('') + + const outer = fixtureEl.querySelector('.outer') + const nested = fixtureEl.querySelector('.nested') + const callbackSpy = jasmine.createSpy('callback spy') + + spyOn(window, 'getComputedStyle').and.returnValue({ + transitionDuration: '0.05s', + transitionDelay: '0s' + }) + + Util.executeAfterTransition(callbackSpy, outer) + + nested.dispatchEvent(new TransitionEvent('transitionend', { + bubbles: true + })) + + setTimeout(() => { + expect(callbackSpy).not.toHaveBeenCalled() + }, 20) + + setTimeout(() => { + expect(callbackSpy).toHaveBeenCalled() + done() + }, 70) + }) + }) + + describe('getNextActiveElement', () => { + it('should return first element if active not exists or not given and shouldGetNext is either true, or false with cycling being disabled', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, '', true, true)).toEqual('a') + expect(Util.getNextActiveElement(array, 'g', true, true)).toEqual('a') + expect(Util.getNextActiveElement(array, '', true, false)).toEqual('a') + expect(Util.getNextActiveElement(array, 'g', true, false)).toEqual('a') + expect(Util.getNextActiveElement(array, '', false, false)).toEqual('a') + expect(Util.getNextActiveElement(array, 'g', false, false)).toEqual('a') + }) + + it('should return last element if active not exists or not given and shouldGetNext is false but cycling is enabled', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, '', false, true)).toEqual('d') + expect(Util.getNextActiveElement(array, 'g', false, true)).toEqual('d') + }) + + it('should return next element or same if is last', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, 'a', true, true)).toEqual('b') + expect(Util.getNextActiveElement(array, 'b', true, true)).toEqual('c') + expect(Util.getNextActiveElement(array, 'd', true, false)).toEqual('d') + }) + + it('should return next element or first, if is last and "isCycleAllowed = true"', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, 'c', true, true)).toEqual('d') + expect(Util.getNextActiveElement(array, 'd', true, true)).toEqual('a') + }) + + it('should return previous element or same if is first', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, 'b', false, true)).toEqual('a') + expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c') + expect(Util.getNextActiveElement(array, 'a', false, false)).toEqual('a') + }) + + it('should return next element or first, if is last and "isCycleAllowed = true"', () => { + const array = ['a', 'b', 'c', 'd'] + + expect(Util.getNextActiveElement(array, 'd', false, true)).toEqual('c') + expect(Util.getNextActiveElement(array, 'a', false, true)).toEqual('d') + }) + }) }) 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..280adb8e5 --- /dev/null +++ b/js/tests/unit/util/scrollbar.spec.js @@ -0,0 +1,353 @@ +import { clearBodyAndDocument, clearFixture, getFixture } from '../../helpers/fixture' +import Manipulator from '../../../src/dom/manipulator' +import ScrollBarHelper from '../../../src/util/scrollbar' + +describe('ScrollBar', () => { + let fixtureEl + const doc = document.documentElement + const parseInt = arg => Number.parseInt(arg, 10) + const getPaddingX = el => parseInt(window.getComputedStyle(el).paddingRight) + const getMarginX = el => parseInt(window.getComputedStyle(el).marginRight) + const getOverFlow = el => el.style.overflow + const getPaddingAttr = el => Manipulator.getDataAttribute(el, 'padding-right') + const getMarginAttr = el => Manipulator.getDataAttribute(el, 'margin-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 = new ScrollBarHelper().isOverflowing() + + if (isScrollBarHidden()) { + expect(result).toEqual(false) + } else { + expect(result).toEqual(true) + } + }) + + it('should return false if body is not overflowing', () => { + doc.style.overflowY = 'hidden' + document.body.style.overflowY = 'hidden' + fixtureEl.innerHTML = [ + '<div style="height: 110vh; width: 100%"></div>' + ].join('') + const scrollBar = new ScrollBarHelper() + const result = scrollBar.isOverflowing() + + expect(result).toEqual(false) + }) + }) + + describe('getWidth', () => { + it('should return an integer greater than zero, if body is overflowing', () => { + doc.style.overflowY = 'scroll' + document.body.style.overflowY = 'scroll' + fixtureEl.innerHTML = [ + '<div style="height: 110vh; width: 100%"></div>' + ].join('') + const result = new ScrollBarHelper().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 = new ScrollBarHelper().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('') + doc.style.overflowY = 'scroll' + + const fixedEl = fixtureEl.querySelector('#fixed1') + const fixedEl2 = fixtureEl.querySelector('#fixed2') + const originalPadding = getPaddingX(fixedEl) + const originalPadding2 = getPaddingX(fixedEl2) + const scrollBar = new ScrollBarHelper() + const expectedPadding = originalPadding + scrollBar.getWidth() + const expectedPadding2 = originalPadding2 + scrollBar.getWidth() + + scrollBar.hide() + + let currentPadding = getPaddingX(fixedEl) + let currentPadding2 = getPaddingX(fixedEl2) + expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`, 'original fixed element padding should be stored in data-bs-padding-right') + expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}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') + expect(currentPadding2).toEqual(expectedPadding2, 'fixed element padding should be adjusted while opening') + + scrollBar.reset() + currentPadding = getPaddingX(fixedEl) + currentPadding2 = getPaddingX(fixedEl2) + expect(getPaddingAttr(fixedEl)).toEqual(null, 'data-bs-padding-right should be cleared after closing') + expect(getPaddingAttr(fixedEl2)).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 and padding of sticky elements', done => { + fixtureEl.innerHTML = [ + '<div style="height: 110vh">' + + '<div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>', + '</div>' + ].join('') + doc.style.overflowY = 'scroll' + + const stickyTopEl = fixtureEl.querySelector('.sticky-top') + const originalMargin = getMarginX(stickyTopEl) + const originalPadding = getPaddingX(stickyTopEl) + const scrollBar = new ScrollBarHelper() + const expectedMargin = originalMargin - scrollBar.getWidth() + const expectedPadding = originalPadding + scrollBar.getWidth() + scrollBar.hide() + + expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`, 'original sticky element margin should be stored in data-bs-margin-right') + expect(getMarginX(stickyTopEl)).toEqual(expectedMargin, 'sticky element margin should be adjusted while opening') + expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`, 'original sticky element margin should be stored in data-bs-margin-right') + expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding, 'sticky element margin should be adjusted while opening') + + scrollBar.reset() + expect(getMarginAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing') + expect(getMarginX(stickyTopEl)).toEqual(originalMargin, 'sticky element margin should be reset after closing') + expect(getPaddingAttr(stickyTopEl)).toEqual(null, 'data-bs-margin-right should be cleared after closing') + expect(getPaddingX(stickyTopEl)).toEqual(originalPadding, '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 = getMarginX(stickyTopEl) + const originalPadding = getPaddingX(stickyTopEl) + + const scrollBar = new ScrollBarHelper() + scrollBar.hide() + + const currentMargin = getMarginX(stickyTopEl) + const currentPadding = getPaddingX(stickyTopEl) + + 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() + }) + + it('should not put data-attribute if element doesn\'t have the proper style property, should just remove style property if element didn\'t had one', () => { + fixtureEl.innerHTML = [ + '<div style="height: 110vh; width: 100%">' + + '<div class="sticky-top" id="sticky" style="width: 100vw"></div>', + '</div>' + ].join('') + + document.body.style.overflowY = 'scroll' + const scrollBar = new ScrollBarHelper() + + const hasPaddingAttr = el => el.hasAttribute('data-bs-padding-right') + const hasMarginAttr = el => el.hasAttribute('data-bs-margin-right') + const stickyEl = fixtureEl.querySelector('#sticky') + const originalPadding = getPaddingX(stickyEl) + const originalMargin = getMarginX(stickyEl) + const scrollBarWidth = scrollBar.getWidth() + + scrollBar.hide() + + expect(getPaddingX(stickyEl)).toEqual(scrollBarWidth + originalPadding) + const expectedMargin = scrollBarWidth + originalMargin + expect(getMarginX(stickyEl)).toEqual(expectedMargin === 0 ? expectedMargin : -expectedMargin) + expect(hasMarginAttr(stickyEl)).toBeFalse() // We do not have to keep css margin + expect(hasPaddingAttr(stickyEl)).toBeFalse() // We do not have to keep css padding + + scrollBar.reset() + + expect(getPaddingX(stickyEl)).toEqual(originalPadding) + expect(getPaddingX(stickyEl)).toEqual(originalPadding) + }) + + describe('Body Handling', () => { + it('should ignore other inline styles when trying to restore body defaults ', () => { + document.body.style.color = 'red' + + const scrollBar = new ScrollBarHelper() + const scrollBarWidth = scrollBar.getWidth() + scrollBar.hide() + + expect(getPaddingX(document.body)).toEqual(scrollBarWidth, 'body does not have inline padding set') + expect(document.body.style.color).toEqual('red', 'body still has other inline styles set') + + scrollBar.reset() + }) + + 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 = getPaddingX(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 scrollBar = new ScrollBarHelper() + const scrollBarWidth = scrollBar.getWidth() + + scrollBar.hide() + + const currentPadding = getPaddingX(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 = getPaddingX(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 = getPaddingX(el) + const originalOverFlow = 'scroll' + el.style.overflow = originalOverFlow + const scrollBar = new ScrollBarHelper() + const scrollBarWidth = scrollBar.getWidth() + + scrollBar.hide() + + const currentPadding = getPaddingX(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 = getPaddingX(el) + expect(currentPadding1).toEqual(originalPadding) + expect(getPaddingAttr(el)).toEqual(null) + expect(getOverFlow(el)).toEqual(originalOverFlow) + expect(getOverFlowAttr(el)).toEqual(null) + }) + + it('should not adjust the inline body padding when it does not overflow', () => { + const originalPadding = getPaddingX(document.body) + const scrollBar = new ScrollBarHelper() + + // Hide scrollbars to prevent the body overflowing + doc.style.overflowY = 'hidden' + doc.style.paddingRight = '0px' + + scrollBar.hide() + const currentPadding = getPaddingX(document.body) + + expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted') + scrollBar.reset() + }) + + it('should not adjust the inline body padding when it does not overflow, even on a scaled display', () => { + const originalPadding = getPaddingX(document.body) + const scrollBar = new ScrollBarHelper() + // Remove body margins as would be done by Bootstrap css + document.body.style.margin = '0' + + // Hide scrollbars to prevent the body overflowing + doc.style.overflowY = 'hidden' + + // Simulate a discrepancy between exact, i.e. floating point body width, and rounded body width + // as it can occur when zooming or scaling the display to something else than 100% + doc.style.paddingRight = '.48px' + scrollBar.hide() + + const currentPadding = getPaddingX(document.body) + + expect(currentPadding).toEqual(originalPadding, 'body padding should not be adjusted') + + scrollBar.reset() + }) + }) + }) +}) |
