UNPKG

scroll-padlock

Version:

Locks elements scroll handling scrollbar gaps and iOS Safari with CSS variables

379 lines (282 loc) 11.6 kB
import 'jsdom-global/register.js'; import { expect } from 'chai'; import Padlock from '../../src/padlock.js'; import { DOM_DATA_ATTRIBUTE_NAME, SCROLL, OUTER_WIDTH, OUTER_HEIGHT, RESIZE, TOP, LEFT } from '../../src/constants.js'; describe('padlock', () => { it('should be able to create an instance and throw if incorrect arguments are passed', () => { // This instance has an invalid element argument expect(() => void new Padlock(null)).to.throw(TypeError); // This instance has an invalid CSS class argument expect(() => void new Padlock(document.createElement('div'), '')).to.throw(TypeError); expect(() => void new Padlock(document.createElement('div'), null)).to.throw(TypeError); // These instances are created on the same element, an error has to be thrown expect(() => { const div = document.createElement('div'); const instance = new Padlock(div); // Catches the error in order to clean former instance before rebinding it to make the test work properly try { void new Padlock(div); }catch(e){ instance.destroy(); throw e; } }).to.throw(Error); // These instances are created on the same element, but no error is thrown because the first one is destroyed before the second expect(() => { const div = document.createElement('div'); let instance = new Padlock(div); instance.destroy(); instance = new Padlock(div); instance.destroy(); }).to.not.throw(Error); expect(() => { // If no element is given as an argument, then the default one is page scroller (<html> element) let instance = new Padlock(); expect(document.documentElement.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)); instance.destroy(); // If <body> is given as an argument, then it's a global scroller (still <html> element) instance = new Padlock(document.body); expect(document.documentElement.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)); instance.destroy(); }).to.not.throw(Error); }); it('should update instance "layout" object only on resize and "scroll" object only on scroll', async () => { const div = document.createElement('div'); let dimensions = { width: 100, height: 200 }; let scrollPosition = { top: 1000, left: 2000 }; div.getBoundingClientRect = () => dimensions; div.scrollTop = scrollPosition.top; div.scrollLeft = scrollPosition.left; const instance = new Padlock(div); expect(instance.layout).to.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); dimensions = { width: 300, height: 400 }; scrollPosition = { top: 2000, left: 3000 }; div.getBoundingClientRect = () => dimensions; div.scrollTop = scrollPosition.top; div.scrollLeft = scrollPosition.left; expect(instance.layout).to.not.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.not.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); window.dispatchEvent(new CustomEvent(RESIZE)); await new Promise(window.setTimeout); expect(instance.layout).to.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.not.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); dimensions = { width: 400, height: 500 }; div.dispatchEvent(new CustomEvent(SCROLL)); await new Promise(window.setTimeout); expect(instance.layout).not.to.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); instance.destroy(); }); it('should update instance "scroll" object and "layout" object on "update" method call', () => { const div = document.createElement('div'); let dimensions = { width: 100, height: 200 }; let scrollPosition = { top: 1000, left: 2000 }; div.getBoundingClientRect = () => dimensions; div.scrollTop = scrollPosition.top; div.scrollLeft = scrollPosition.left; const instance = new Padlock(div); expect(instance.layout).to.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); dimensions = { width: 300, height: 400 }; scrollPosition = { top: 2000, left: 3000 }; div.getBoundingClientRect = () => dimensions; div.scrollTop = scrollPosition.top; div.scrollLeft = scrollPosition.left; expect(instance.layout).to.not.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.not.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); instance.update(); expect(instance.layout).to.include({ [OUTER_WIDTH]: dimensions.width, [OUTER_HEIGHT]: dimensions.height }); expect(instance.scroll).to.include({ [TOP]: scrollPosition.top, [LEFT]: scrollPosition.left }); instance.destroy(); }); it('should scroll the element through "scroll" setter when unlocked state, save it for later when locked, restore saved scroll on unlock', async () => { const div = document.createElement('div'); const lockedStateCSSClassName = 'loooocked'; let scrollToCalls = 0; div.scrollTo = arg => { scrollToCalls++; if( typeof arg === 'object' ){ div.scrollTop = arg?.top; } if( typeof arg === 'number' ){ div.scrollTop = arg; } }; const instance = new Padlock(div, lockedStateCSSClassName); // Tests programmatic scroll change through instance let { scrollTop } = div; instance.scroll = { top: 100, left: 200 }; expect(scrollTop).not.to.equals(div.scrollTop); expect(scrollToCalls).to.equals(1); scrollTop = instance.scroll.top; // Tests programmatic scroll change attempt through instance on locked state (given scroll object is saved for later) ({ scrollTop } = div); div.classList.add(lockedStateCSSClassName); await new Promise(window.setTimeout); instance.scroll = { top: 200, left: 300 }; expect(scrollTop).to.equals(div.scrollTop); expect(scrollToCalls).to.equals(1); // Tests automatic scroll restore on previously given scroll object (through scroll change attempt) div.classList.remove(lockedStateCSSClassName); await new Promise(window.setTimeout); expect(scrollTop).not.to.equals(div.scrollTop); expect(scrollToCalls).to.equals(2); }); it('should avoid further computations or DOM changes after "destroy" method call', () => { const div = document.createElement('div'); const getStylesheetsCount = () => document.head.querySelectorAll('style').length; const stylesheetsCountBeforeInit = getStylesheetsCount(); const { MutationObserver: windowMutationObserverBackup, addEventListener: windowAddEventListenerBackup, removeEventListener: windowRemoveEventListenerBackup } = window; let observed = 0; let unobserved = 0; let disconnect = 0; let resizeListened = 0; let scrollListened = 0; let scrollRemoved = 0; let resizeRemoved = 0; window.MutationObserver = class { observe = () => observed++; unobserve = () => unobserved++; disconnect = () => disconnect++; }; window.addEventListener = div.addEventListener = type => { switch(type){ case SCROLL: scrollListened++; break; case RESIZE: resizeListened++; break; } }; window.removeEventListener = div.removeEventListener = type => { switch(type){ case SCROLL: scrollRemoved++; break; case RESIZE: resizeRemoved++; break; } }; expect(observed).to.equals(0); expect(unobserved).to.equals(0); expect(disconnect).to.equals(0); expect(resizeListened).to.equals(0); expect(scrollListened).to.equals(0); expect(resizeRemoved).to.equals(0); expect(scrollRemoved).to.equals(0); expect(getStylesheetsCount()).to.equals(stylesheetsCountBeforeInit); const instance = new Padlock(div); expect(getStylesheetsCount()).to.not.equals(stylesheetsCountBeforeInit); expect(observed).to.equals(1); expect(unobserved).to.equals(0); expect(disconnect).to.equals(0); expect(resizeListened).to.equals(1); expect(scrollListened).to.equals(1); expect(resizeRemoved).to.equals(0); expect(scrollRemoved).to.equals(0); expect(div.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)).to.be.true; instance.destroy(); expect(getStylesheetsCount()).to.equals(stylesheetsCountBeforeInit); expect(div.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)).to.be.false; div.dispatchEvent(new CustomEvent(SCROLL)); expect(div.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)).to.be.false; div.dispatchEvent(new CustomEvent(RESIZE)); expect(div.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)).to.be.false; instance.update(); expect(div.matches(`[${DOM_DATA_ATTRIBUTE_NAME}]`)).to.be.false; expect(observed).to.equals(1); expect(unobserved).to.equals(0); expect(disconnect).to.equals(1); expect(resizeListened).to.equals(1); expect(scrollListened).to.equals(1); expect(resizeRemoved).to.equals(1); expect(scrollRemoved).to.equals(1); window.addEventListener = windowAddEventListenerBackup; window.removeEventListener = windowRemoveEventListenerBackup; window.MutationObserver = windowMutationObserverBackup; }); });