diff --git a/packages/overlayscrollbars/src/initialization.ts b/packages/overlayscrollbars/src/initialization.ts index 8cab6d7..b69f2b8 100644 --- a/packages/overlayscrollbars/src/initialization.ts +++ b/packages/overlayscrollbars/src/initialization.ts @@ -1,14 +1,19 @@ import { isFunction, isHTMLElement, isNull, isUndefined } from 'support'; import { getEnvironment } from 'environment'; -import { DeepPartial } from 'typings'; -import { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; +import type { DeepPartial } from 'typings'; type StaticInitialization = HTMLElement | false | null; type DynamicInitialization = HTMLElement | boolean | null; - -type FallbackInitializtationElement< - InitElm extends StaticInitializationElement | DynamicInitializationElement -> = Extract any> extends (...args: infer P) => any +type FallbackStaticInitializtationElement = Extract< + StaticInitializationElement, + (...args: Args) => any +> extends (...args: infer P) => any + ? (...args: P) => HTMLElement + : never; +type FallbackDynamicInitializtationElement = Extract< + DynamicInitializationElement, + (...args: Args) => any +> extends (...args: infer P) => any ? (...args: P) => HTMLElement : never; @@ -59,30 +64,30 @@ export type InitializationTarget = InitializationTargetElement | InitializationT const resolveInitialization = (value: any, args: any): T => isFunction(value) ? value.apply(0, args) : value; -export const staticInitializationElement = >( - args: Parameters any>>, - fallbackStaticInitializationElement: FallbackInitializtationElement, - defaultStaticInitializationElementStrategy: T, - staticInitializationElementValue?: T +export const staticInitializationElement = ( + args: Args, + fallbackStaticInitializationElement: FallbackStaticInitializtationElement, + defaultStaticInitializationElement: StaticInitializationElement, + staticInitializationElementValue?: StaticInitializationElement ): HTMLElement => { const staticInitialization = isUndefined(staticInitializationElementValue) - ? defaultStaticInitializationElementStrategy + ? defaultStaticInitializationElement : staticInitializationElementValue; const resolvedInitialization = resolveInitialization( staticInitialization, args ); - return resolvedInitialization || fallbackStaticInitializationElement(); + return resolvedInitialization || fallbackStaticInitializationElement.apply(0, args); }; -export const dynamicInitializationElement = >( - args: Parameters any>>, - fallbackDynamicInitializationElement: FallbackInitializtationElement, - defaultDynamicInitializationElementStrategy: T, - dynamicInitializationElementValue?: T +export const dynamicInitializationElement = ( + args: Args, + fallbackDynamicInitializationElement: FallbackDynamicInitializtationElement, + defaultDynamicInitializationElement: DynamicInitializationElement, + dynamicInitializationElementValue?: DynamicInitializationElement ): HTMLElement | false => { const dynamicInitialization = isUndefined(dynamicInitializationElementValue) - ? defaultDynamicInitializationElementStrategy + ? defaultDynamicInitializationElement : dynamicInitializationElementValue; const resolvedInitialization = resolveInitialization( dynamicInitialization, @@ -92,20 +97,19 @@ export const dynamicInitializationElement = | false | null | undefined, - structureSetupElements: StructureSetupElementsObj + isBody: boolean, + defaultCancelInitialization: Initialization['cancel'], + cancelInitializationValue?: DeepPartial | false | null | undefined ): boolean => { const { nativeScrollbarsOverlaid, body } = cancelInitializationValue || {}; - const { _isBody } = structureSetupElements; - const { _getDefaultInitialization, _nativeScrollbarsOverlaid, _nativeScrollbarsHiding } = - getEnvironment(); + const { _nativeScrollbarsOverlaid, _nativeScrollbarsHiding } = getEnvironment(); const { nativeScrollbarsOverlaid: defaultNativeScrollbarsOverlaid, body: defaultbody } = - _getDefaultInitialization().cancel; + defaultCancelInitialization; const resolvedNativeScrollbarsOverlaid = nativeScrollbarsOverlaid ?? defaultNativeScrollbarsOverlaid; @@ -115,7 +119,7 @@ export const cancelInitialization = ( (_nativeScrollbarsOverlaid.x || _nativeScrollbarsOverlaid.y) && resolvedNativeScrollbarsOverlaid; const finalDocumentScrollingElement = - _isBody && + isBody && (isNull(resolvedDocumentScrollingElement) ? !_nativeScrollbarsHiding : resolvedDocumentScrollingElement); diff --git a/packages/overlayscrollbars/src/overlayscrollbars.ts b/packages/overlayscrollbars/src/overlayscrollbars.ts index 09a78c3..21d5717 100644 --- a/packages/overlayscrollbars/src/overlayscrollbars.ts +++ b/packages/overlayscrollbars/src/overlayscrollbars.ts @@ -115,7 +115,11 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( options?: DeepPartial, eventListeners?: InitialEventListeners ) => { - const { _getDefaultOptions, _addListener: addEnvListener } = getEnvironment(); + const { + _getDefaultOptions, + _getDefaultInitialization, + _addListener: addEnvListener, + } = getEnvironment(); const plugins = getPlugins(); const targetIsElement = isHTMLElement(target); const instanceTarget = targetIsElement ? target : target.target; @@ -271,7 +275,13 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = ( } }); - if (cancelInitialization(!targetIsElement && target.cancel, structureState._elements)) { + if ( + cancelInitialization( + structureState._elements._isBody, + _getDefaultInitialization().cancel, + !targetIsElement && target.cancel + ) + ) { destroy(true); return instance; } diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts index b0ffcfb..4b52802 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.elements.ts @@ -28,14 +28,14 @@ import { getScrollbarHandleOffsetRatio, } from 'setups/scrollbarsSetup/scrollbarsSetup.calculations'; import type { - Initialization, InitializationTarget, + InitializationTargetElement, InitializationTargetObject, } from 'initialization'; import type { StructureSetupElementsObj } from 'setups/structureSetup/structureSetup.elements'; import type { ScrollbarsSetupEvents } from 'setups/scrollbarsSetup/scrollbarsSetup.events'; import type { StyleObject } from 'typings'; -import { StructureSetupState } from 'setups'; +import type { StructureSetupState } from 'setups'; export interface ScrollbarStructure { _scrollbar: HTMLElement; @@ -84,7 +84,7 @@ export const createScrollbarsSetupElements = ( const { scrollbars: scrollbarsInit } = (_targetIsElm ? {} : target) as InitializationTargetObject; const { slot: initScrollbarsSlot } = scrollbarsInit || {}; const evaluatedScrollbarSlot = generalDynamicInitializationElement< - Initialization['scrollbars']['slot'] + [InitializationTargetElement, HTMLElement, HTMLElement] >([_target, _host, _viewport], () => _host, defaultInitScrollbarsSlot, initScrollbarsSlot); const scrollbarStructureAddRemoveClass = ( scrollbarStructures: ScrollbarStructure[], diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts index 65086ac..9e7bdd3 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.events.ts @@ -7,7 +7,7 @@ import { runEachAndClear, stopPropagation, XY, - selfCancelTimeout, + selfClearTimeout, parent, closest, rAF, @@ -223,7 +223,7 @@ export const createScrollbarsSetupEvents = isHorizontal ) => { const { _scrollbar } = scrollbarStructure; - const [wheelTimeout, clearScrollTimeout] = selfCancelTimeout(333); + const [wheelTimeout, clearScrollTimeout] = selfClearTimeout(333); const scrollByFn = !!scrollOffsetElm.scrollBy; let wheelScrollBy = true; diff --git a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts index 67444d6..bfad2c1 100644 --- a/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts +++ b/packages/overlayscrollbars/src/setups/scrollbarsSetup/scrollbarsSetup.ts @@ -1,4 +1,4 @@ -import { on, runEachAndClear, parent, scrollLeft, scrollTop, selfCancelTimeout } from 'support'; +import { on, runEachAndClear, parent, scrollLeft, scrollTop, selfClearTimeout } from 'support'; import { createState, createOptionCheck } from 'setups/setups'; import { createScrollbarsSetupEvents } from 'setups/scrollbarsSetup/scrollbarsSetup.events'; import { @@ -51,11 +51,11 @@ export const createScrollbarsSetup = ( const state = createState({}); const [getState] = state; - const [requestMouseMoveAnimationFrame, cancelMouseMoveAnimationFrame] = selfCancelTimeout(); - const [requestScrollAnimationFrame, cancelScrollAnimationFrame] = selfCancelTimeout(); - const [scrollTimeout, clearScrollTimeout] = selfCancelTimeout(100); - const [auotHideMoveTimeout, clearAutoHideTimeout] = selfCancelTimeout(100); - const [auotHideTimeout, clearAutoTimeout] = selfCancelTimeout(() => globalAutoHideDelay); + const [requestMouseMoveAnimationFrame, cancelMouseMoveAnimationFrame] = selfClearTimeout(); + const [requestScrollAnimationFrame, cancelScrollAnimationFrame] = selfClearTimeout(); + const [scrollTimeout, clearScrollTimeout] = selfClearTimeout(100); + const [auotHideMoveTimeout, clearAutoHideTimeout] = selfClearTimeout(100); + const [auotHideTimeout, clearAutoTimeout] = selfClearTimeout(() => globalAutoHideDelay); const [elements, appendElements, destroyElements] = createScrollbarsSetupElements( target, structureSetupState._elements, diff --git a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts index e6d092d..ccddff8 100644 --- a/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts +++ b/packages/overlayscrollbars/src/setups/structureSetup/structureSetup.elements.ts @@ -39,7 +39,6 @@ import { dynamicInitializationElement as generalDynamicInitializationElement, } from 'initialization'; import type { - Initialization, InitializationTarget, InitializationTargetElement, InitializationTargetObject, @@ -117,10 +116,10 @@ export const createStructureSetupElements = ( const isBody = targetElement === ownerDocument.body; const wnd = ownerDocument.defaultView as Window; const staticInitializationElement = generalStaticInitializationElement< - Initialization['elements']['viewport'] + [InitializationTargetElement] >.bind(0, [targetElement]); const dynamicInitializationElement = generalDynamicInitializationElement< - Initialization['elements']['content'] + [InitializationTargetElement] >.bind(0, [targetElement]); const viewportElement = staticInitializationElement( createNewDiv, diff --git a/packages/overlayscrollbars/src/support/utils/function.ts b/packages/overlayscrollbars/src/support/utils/function.ts index e81ba9d..1c83f79 100644 --- a/packages/overlayscrollbars/src/support/utils/function.ts +++ b/packages/overlayscrollbars/src/support/utils/function.ts @@ -30,7 +30,12 @@ export interface Debounced any> { export const noop = () => {}; // eslint-disable-line -export const selfCancelTimeout = (timeout?: number | (() => number)) => { +/** + * Creates a timeout and cleartimeout tuple. The timeout function always clears the previously created timeout before it runs. + * @param timeout The timeout in ms. If no timeout (or 0) is passed requestAnimationFrame is used instead of setTimeout. + * @returns A tuple with the timeout function as the first value and the clearTimeout function as the second value. + */ +export const selfClearTimeout = (timeout?: number | (() => number)) => { let id: number; const setTFn = timeout ? setT : rAF!; const clearTFn = timeout ? clearT : cAF!; diff --git a/packages/overlayscrollbars/tests/jest-jsdom/environment.test.ts b/packages/overlayscrollbars/tests/jest-jsdom/environment.test.ts new file mode 100644 index 0000000..c7e1d9f --- /dev/null +++ b/packages/overlayscrollbars/tests/jest-jsdom/environment.test.ts @@ -0,0 +1,186 @@ +import { DeepPartial } from 'typings'; +import { defaultOptions, Options } from 'options'; +import { Initialization } from 'initialization'; +import { getEnvironment } from 'environment'; +import { scrollbarsHidingPlugin, scrollbarsHidingPluginName } from 'plugins'; + +const defaultInitialization = { + elements: { + host: null, + padding: true, + viewport: expect.any(Function), + content: false, + }, + scrollbars: { + slot: true, + }, + cancel: { + nativeScrollbarsOverlaid: false, + body: null, + }, +}; + +let getEnv = getEnvironment; + +describe('environment', () => { + beforeEach(async () => { + jest.resetModules(); + jest.doMock('support', () => { + const originalModule = jest.requireActual('support'); + let i = 0; + return { + ...originalModule, + offsetSize: jest.fn().mockImplementation(() => { + i += 1; + return { w: 100 + i, h: 100 + i }; + }), + clientSize: jest.fn().mockImplementation(() => ({ w: 90, h: 90 })), + }; + }); + jest.doMock('plugins', () => { + const originalModule = jest.requireActual('plugins'); + return { + ...originalModule, + getPlugins: jest.fn(() => originalModule.getPlugins()), + }; + }); + + ({ getEnvironment: getEnv } = await import('environment')); + }); + + test('singleton behavior', () => { + const env = getEnv(); + expect(env).toBe(getEnv()); + }); + + describe('statics', () => { + test('defaultOptions', () => { + const { _staticDefaultOptions, _getDefaultOptions } = getEnv(); + expect(_staticDefaultOptions).not.toBe(defaultOptions); + expect(_staticDefaultOptions).toEqual(defaultOptions); + expect(_staticDefaultOptions).not.toBe(_getDefaultOptions()); + expect(_staticDefaultOptions).toEqual(_getDefaultOptions()); + }); + + test('defaultInitialization', () => { + const { _staticDefaultInitialization, _getDefaultInitialization } = getEnv(); + expect(_staticDefaultInitialization).not.toBe(defaultInitialization); + expect(_staticDefaultInitialization).toEqual(defaultInitialization); + expect(_staticDefaultInitialization).not.toBe(_getDefaultInitialization()); + expect(_staticDefaultInitialization).toEqual(_getDefaultInitialization()); + }); + }); + + describe('defaultOptions', () => { + test('get', () => { + const { _getDefaultOptions } = getEnv(); + expect(_getDefaultOptions()).not.toBe(defaultOptions); + expect(_getDefaultOptions()).toEqual(defaultOptions); + }); + + test('set', () => { + const newDefaultOptions: DeepPartial = { + paddingAbsolute: true, + overflow: { + x: 'hidden', + }, + }; + const { _getDefaultOptions, _setDefaultOptions } = getEnv(); + expect(_getDefaultOptions()).not.toBe(defaultOptions); + expect(_getDefaultOptions()).toEqual(defaultOptions); + + _setDefaultOptions(newDefaultOptions); + + expect(_getDefaultOptions()).toEqual({ + ...defaultOptions, + ...newDefaultOptions, + overflow: { + ...defaultOptions.overflow, + ...newDefaultOptions.overflow, + }, + }); + }); + }); + + describe('defaultInitialization', () => { + test('get', () => { + const { _getDefaultInitialization } = getEnv(); + expect(_getDefaultInitialization()).not.toBe(defaultInitialization); + expect(_getDefaultInitialization()).toEqual(defaultInitialization); + }); + + test('set', () => { + const newDefaultInitialization: DeepPartial = { + elements: { + viewport: false, + padding: false, + }, + cancel: { + body: true, + nativeScrollbarsOverlaid: false, + }, + }; + const { _getDefaultInitialization, _setDefaultInitialization } = getEnv(); + expect(_getDefaultInitialization()).not.toBe(defaultInitialization); + expect(_getDefaultInitialization()).toEqual(defaultInitialization); + + _setDefaultInitialization(newDefaultInitialization); + + expect(_getDefaultInitialization()).toEqual({ + ...defaultInitialization, + ...newDefaultInitialization, + elements: { + ...defaultInitialization.elements, + ...newDefaultInitialization.elements, + }, + cancel: { + ...defaultInitialization.cancel, + ...newDefaultInitialization.cancel, + }, + }); + }); + }); + + describe('addListener', () => { + test('with scrollbarsHidingPlugin registered before environment was created', async () => { + const { getPlugins } = await import('plugins'); + (getPlugins as jest.Mock).mockImplementation(() => ({ + [scrollbarsHidingPluginName]: scrollbarsHidingPlugin[scrollbarsHidingPluginName], + })); + + const { _addListener } = getEnv(); + const listener = jest.fn(); + + _addListener(listener); + window.dispatchEvent(new Event('resize')); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + test('with scrollbarsHidingPlugin registered after environment was created', async () => { + const { _addListener } = getEnv(); + const listener = jest.fn(); + + _addListener(listener); + + const { getPlugins } = await import('plugins'); + (getPlugins as jest.Mock).mockImplementation(() => ({ + [scrollbarsHidingPluginName]: scrollbarsHidingPlugin[scrollbarsHidingPluginName], + })); + + window.dispatchEvent(new Event('resize')); + + expect(listener).toHaveBeenCalledTimes(1); + }); + + test('without scrollbarsHidingPlugin', () => { + const { _addListener } = getEnv(); + const listener = jest.fn(); + + _addListener(listener); + window.dispatchEvent(new Event('resize')); + + expect(listener).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/packages/overlayscrollbars/tests/jest-jsdom/initialization.test.ts b/packages/overlayscrollbars/tests/jest-jsdom/initialization.test.ts new file mode 100644 index 0000000..5012808 --- /dev/null +++ b/packages/overlayscrollbars/tests/jest-jsdom/initialization.test.ts @@ -0,0 +1,587 @@ +import { + staticInitializationElement, + dynamicInitializationElement, + cancelInitialization, + Initialization, +} from 'initialization'; +import { getEnvironment } from 'environment'; + +jest.mock('environment', () => ({ + getEnvironment: jest.fn(() => jest.requireActual('environment').getEnvironment()), +})); + +const createDiv = () => document.createElement('div'); + +describe('initialization', () => { + describe('staticInitializationElement', () => { + test('defined', () => { + const args: [a: boolean, b: string] = [true, '']; + const fallbackElm = createDiv(); + const defaultElm = createDiv(); + const elm = createDiv(); + const fallbackElmFn = jest.fn(() => fallbackElm); + const elmFn = jest.fn(() => elm); + const nullFn = jest.fn(() => null); + const falseFn = jest.fn(() => false); + + const values = { + elm: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + elm + ), + (result: HTMLElement) => expect(result).toBe(elm), + ], + null: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + null + ), + (result: HTMLElement) => expect(result).toBe(fallbackElm), + ], + false: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + false + ), + (result: HTMLElement) => expect(result).toBe(fallbackElm), + ], + undefined: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + undefined + ), + (result: HTMLElement) => expect(result).toBe(defaultElm), + ], + }; + + const fns = { + elm: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + elmFn + ), + (result: HTMLElement) => { + expect(result).toBe(elm); + expect(elmFn).toHaveBeenCalledTimes(1); + expect(elmFn).toHaveBeenLastCalledWith(...args); + }, + ], + null: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + nullFn + ), + (result: HTMLElement) => { + expect(result).toBe(fallbackElm); + expect(nullFn).toHaveBeenCalledTimes(1); + expect(nullFn).toHaveBeenLastCalledWith(...args); + }, + ], + false: [ + staticInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + falseFn + ), + (result: HTMLElement) => { + expect(result).toBe(fallbackElm); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + }; + + Object.keys(values).forEach((key) => { + const [result, assertion] = values[key]; + assertion(result); + }); + + Object.keys(fns).forEach((key) => { + const [result, assertion] = fns[key]; + assertion(result); + }); + }); + + test('default', () => { + const args: [a: boolean, b: string] = [true, '']; + const fallbackElm = createDiv(); + const elm = createDiv(); + const fallbackElmFn = jest.fn(() => fallbackElm); + const elmFn = jest.fn(() => elm); + const nullFn = jest.fn(() => null); + const falseFn = jest.fn(() => false); + + const values = { + elm: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, elm), + (result: HTMLElement) => expect(result).toBe(elm), + ], + null: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, null), + (result: HTMLElement) => expect(result).toBe(fallbackElm), + ], + false: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, false), + (result: HTMLElement) => expect(result).toBe(fallbackElm), + ], + }; + + const fns = { + elm: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, elmFn), + (result: HTMLElement) => { + expect(result).toBe(elm); + expect(elmFn).toHaveBeenCalledTimes(1); + expect(elmFn).toHaveBeenLastCalledWith(...args); + }, + ], + null: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, nullFn), + (result: HTMLElement) => { + expect(result).toBe(fallbackElm); + expect(nullFn).toHaveBeenCalledTimes(1); + expect(nullFn).toHaveBeenLastCalledWith(...args); + }, + ], + false: [ + staticInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, falseFn), + (result: HTMLElement) => { + expect(result).toBe(fallbackElm); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + }; + + Object.keys(values).forEach((key) => { + const [result, assertion] = values[key]; + assertion(result); + }); + + Object.keys(fns).forEach((key) => { + const [result, assertion] = fns[key]; + assertion(result); + }); + }); + }); + + describe('dynamicInitializationElement', () => { + test('defined', () => { + const args: [a: boolean, b: string] = [true, '']; + const fallbackElm = createDiv(); + const defaultElm = createDiv(); + const elm = createDiv(); + const fallbackElmFn = jest.fn(() => fallbackElm); + const elmFn = jest.fn(() => elm); + const snullFn = jest.fn(() => null); + const falseFn = jest.fn(() => false); + const trueFn = jest.fn(() => true); + + const values = { + elm: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + elm + ), + (result: HTMLElement | false) => expect(result).toBe(elm), + ], + null: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + null + ), + (result: HTMLElement | false) => expect(result).toBe(false), + ], + false: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + false + ), + (result: HTMLElement | false) => expect(result).toBe(false), + ], + true: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + true + ), + (result: HTMLElement | false) => expect(result).toBe(fallbackElm), + ], + undefined: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + undefined + ), + (result: HTMLElement | false) => expect(result).toBe(defaultElm), + ], + }; + + const fns = { + elm: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + elmFn + ), + (result: HTMLElement | false) => { + expect(result).toBe(elm); + expect(elmFn).toHaveBeenCalledTimes(1); + expect(elmFn).toHaveBeenLastCalledWith(...args); + }, + ], + null: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + snullFn + ), + (result: HTMLElement | false) => { + expect(result).toBe(false); + expect(snullFn).toHaveBeenCalledTimes(1); + expect(snullFn).toHaveBeenLastCalledWith(...args); + }, + ], + false: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + falseFn + ), + (result: HTMLElement | false) => { + expect(result).toBe(false); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + true: [ + dynamicInitializationElement<[a: boolean, b: string]>( + args, + fallbackElmFn, + defaultElm, + trueFn + ), + (result: HTMLElement | false) => { + expect(result).toBe(fallbackElm); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + }; + + Object.keys(values).forEach((key) => { + const [result, assertion] = values[key]; + assertion(result); + }); + + Object.keys(fns).forEach((key) => { + const [result, assertion] = fns[key]; + assertion(result); + }); + }); + + test('default', () => { + const args: [a: boolean, b: string] = [true, '']; + const fallbackElm = createDiv(); + const elm = createDiv(); + const fallbackElmFn = jest.fn(() => fallbackElm); + const elmFn = jest.fn(() => elm); + const nullFn = jest.fn(() => null); + const falseFn = jest.fn(() => false); + const trueFn = jest.fn(() => true); + + const values = { + elm: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, elm), + (result: HTMLElement | false) => expect(result).toBe(elm), + ], + null: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, null), + (result: HTMLElement | false) => expect(result).toBe(false), + ], + false: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, false), + (result: HTMLElement | false) => expect(result).toBe(false), + ], + true: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, true), + (result: HTMLElement | false) => expect(result).toBe(fallbackElm), + ], + }; + + const fns = { + elm: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, elmFn), + (result: HTMLElement | false) => { + expect(result).toBe(elm); + expect(elmFn).toHaveBeenCalledTimes(1); + expect(elmFn).toHaveBeenLastCalledWith(...args); + }, + ], + null: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, nullFn), + (result: HTMLElement | false) => { + expect(result).toBe(false); + expect(nullFn).toHaveBeenCalledTimes(1); + expect(nullFn).toHaveBeenLastCalledWith(...args); + }, + ], + false: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, falseFn), + (result: HTMLElement | false) => { + expect(result).toBe(false); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + true: [ + dynamicInitializationElement<[a: boolean, b: string]>(args, fallbackElmFn, trueFn), + (result: HTMLElement | false) => { + expect(result).toBe(fallbackElm); + expect(falseFn).toHaveBeenCalledTimes(1); + expect(falseFn).toHaveBeenLastCalledWith(...args); + }, + ], + }; + + Object.keys(values).forEach((key) => { + const [result, assertion] = values[key]; + assertion(result); + }); + + Object.keys(fns).forEach((key) => { + const [result, assertion] = fns[key]; + assertion(result); + }); + }); + }); + + describe('cancelInitialization', () => { + describe('nativeScrollbarsOverlaid', () => { + test('defined', () => { + ( + [ + { + nativeScrollbarsOverlaid: false, + body: false, + }, + { + nativeScrollbarsOverlaid: true, + body: false, + }, + ] as Initialization['cancel'][] + ).forEach((defaultCancelInitialization) => { + [ + { nativeScrollbarsOverlaid: false }, + { nativeScrollbarsOverlaid: true }, + { nativeScrollbarsOverlaid: undefined }, + ].forEach((initializationValue) => { + [ + { + _nativeScrollbarsOverlaid: { + x: false, + y: false, + }, + }, + { + _nativeScrollbarsOverlaid: { + x: false, + y: true, + }, + }, + { + _nativeScrollbarsOverlaid: { + x: true, + y: true, + }, + }, + ].forEach((env) => { + (getEnvironment as jest.Mock).mockImplementation(() => ({ + ...jest.requireActual('environment').getEnvironment(), + ...env, + })); + const hasOverlaidScrollbars = + env._nativeScrollbarsOverlaid.x || env._nativeScrollbarsOverlaid.y; + const expected = + hasOverlaidScrollbars && + (initializationValue.nativeScrollbarsOverlaid ?? + defaultCancelInitialization.nativeScrollbarsOverlaid); + + expect( + cancelInitialization(false, defaultCancelInitialization, initializationValue) + ).toEqual(expected); + }); + }); + }); + }); + + test('default', () => { + ( + [ + { + nativeScrollbarsOverlaid: false, + body: false, + }, + { + nativeScrollbarsOverlaid: true, + body: false, + }, + ] as Initialization['cancel'][] + ).forEach((defaultCancelInitialization) => { + [ + { + _nativeScrollbarsOverlaid: { + x: false, + y: false, + }, + }, + { + _nativeScrollbarsOverlaid: { + x: false, + y: true, + }, + }, + { + _nativeScrollbarsOverlaid: { + x: true, + y: true, + }, + }, + ].forEach((env) => { + (getEnvironment as jest.Mock).mockImplementation(() => ({ + ...jest.requireActual('environment').getEnvironment(), + ...env, + })); + const hasOverlaidScrollbars = + env._nativeScrollbarsOverlaid.x || env._nativeScrollbarsOverlaid.y; + const expected = + hasOverlaidScrollbars && defaultCancelInitialization.nativeScrollbarsOverlaid; + + expect(cancelInitialization(false, defaultCancelInitialization)).toEqual(expected); + expect(cancelInitialization(false, defaultCancelInitialization, undefined)).toEqual( + expected + ); + expect(cancelInitialization(false, defaultCancelInitialization, null)).toEqual( + expected + ); + expect(cancelInitialization(false, defaultCancelInitialization, false)).toEqual( + expected + ); + }); + }); + }); + }); + + describe('body', () => { + test('defined', () => { + ( + [ + { + nativeScrollbarsOverlaid: false, + body: false, + }, + { + nativeScrollbarsOverlaid: false, + body: true, + }, + { + nativeScrollbarsOverlaid: false, + body: null, + }, + ] as Initialization['cancel'][] + ).forEach((defaultCancelInitialization) => { + [{ body: false }, { body: true }, { body: null }, { body: undefined }].forEach( + (initializationValue) => { + [{ _nativeScrollbarsHiding: false }, { _nativeScrollbarsHiding: true }].forEach( + (env) => { + [false, true].forEach((isBody) => { + (getEnvironment as jest.Mock).mockImplementation(() => ({ + ...jest.requireActual('environment').getEnvironment(), + ...env, + })); + const defaultBody = defaultCancelInitialization.body; + const bodyValue = initializationValue.body; + const finalBody = bodyValue === undefined ? defaultBody : bodyValue; + const expected = + isBody && (finalBody === null ? !env._nativeScrollbarsHiding : finalBody); + + expect( + cancelInitialization(isBody, defaultCancelInitialization, initializationValue) + ).toEqual(expected); + }); + } + ); + } + ); + }); + }); + + test('default', () => { + ( + [ + { + nativeScrollbarsOverlaid: false, + body: false, + }, + { + nativeScrollbarsOverlaid: false, + body: true, + }, + { + nativeScrollbarsOverlaid: false, + body: null, + }, + ] as Initialization['cancel'][] + ).forEach((defaultCancelInitialization) => { + [{ _nativeScrollbarsHiding: false }, { _nativeScrollbarsHiding: true }].forEach((env) => { + [false, true].forEach((isBody) => { + (getEnvironment as jest.Mock).mockImplementation(() => ({ + ...jest.requireActual('environment').getEnvironment(), + ...env, + })); + const defaultBody = defaultCancelInitialization.body; + const expected = + isBody && (defaultBody === null ? !env._nativeScrollbarsHiding : defaultBody); + + expect(cancelInitialization(isBody, defaultCancelInitialization)).toEqual(expected); + expect(cancelInitialization(isBody, defaultCancelInitialization, undefined)).toEqual( + expected + ); + expect(cancelInitialization(isBody, defaultCancelInitialization, null)).toEqual( + expected + ); + expect(cancelInitialization(isBody, defaultCancelInitialization, false)).toEqual( + expected + ); + }); + }); + }); + }); + }); + }); +}); diff --git a/packages/overlayscrollbars/tests/jest-jsdom/support/utils/function.test.ts b/packages/overlayscrollbars/tests/jest-jsdom/support/utils/function.test.ts index 3d6a043..6d873e6 100644 --- a/packages/overlayscrollbars/tests/jest-jsdom/support/utils/function.test.ts +++ b/packages/overlayscrollbars/tests/jest-jsdom/support/utils/function.test.ts @@ -1,5 +1,5 @@ -import { noop, debounce } from 'support/utils/function'; -import { rAF, setT } from 'support/compatibility/apis'; +import { noop, debounce, selfClearTimeout } from 'support/utils/function'; +import { rAF, cAF, setT, clearT } from 'support/compatibility/apis'; jest.useFakeTimers(); @@ -318,4 +318,105 @@ describe('function', () => { }); }); }); + + describe('selfClearTimeout', () => { + test('without timeout', () => { + let i = 0; + const [timeout, clear] = selfClearTimeout(); + + expect(rAF).not.toHaveBeenCalled(); + expect(cAF).not.toHaveBeenCalled(); + expect(setT).not.toHaveBeenCalled(); + expect(clearT).not.toHaveBeenCalled(); + + timeout(() => { + i += 1; + }); + clear(); + + expect(rAF).toHaveBeenCalledTimes(1); + expect(cAF).toHaveBeenCalledTimes(2); + expect(setT).not.toHaveBeenCalled(); + expect(clearT).not.toHaveBeenCalled(); + + expect(i).toBe(0); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + jest.runAllTimers(); + expect(i).toBe(1); + }); + + test('with timeout', () => { + let i = 0; + const [timeout, clear] = selfClearTimeout(100); + + expect(rAF).not.toHaveBeenCalled(); + expect(cAF).not.toHaveBeenCalled(); + expect(setT).not.toHaveBeenCalled(); + expect(clearT).not.toHaveBeenCalled(); + + timeout(() => { + i += 1; + }); + clear(); + + expect(rAF).not.toHaveBeenCalled(); + expect(cAF).not.toHaveBeenCalled(); + expect(setT).toHaveBeenCalledTimes(1); + expect(clearT).toHaveBeenCalledTimes(2); + + expect(i).toBe(0); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + jest.runAllTimers(); + expect(i).toBe(1); + }); + + test('with timeout function', () => { + let i = 0; + const [timeout, clear] = selfClearTimeout(() => 100); + + expect(rAF).not.toHaveBeenCalled(); + expect(cAF).not.toHaveBeenCalled(); + expect(setT).not.toHaveBeenCalled(); + expect(clearT).not.toHaveBeenCalled(); + + timeout(() => { + i += 1; + }); + clear(); + + expect(rAF).not.toHaveBeenCalled(); + expect(cAF).not.toHaveBeenCalled(); + expect(setT).toHaveBeenCalledTimes(1); + expect(clearT).toHaveBeenCalledTimes(2); + + expect(i).toBe(0); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + timeout(() => { + i += 1; + }); + jest.runAllTimers(); + expect(i).toBe(1); + }); + }); });