improve event listener code and tests

This commit is contained in:
Rene
2022-07-11 17:07:34 +02:00
parent 60dd573cfc
commit 295f94309f
4 changed files with 203 additions and 38 deletions
@@ -31,9 +31,9 @@ import { Options, defaultOptions } from 'options';
import { PartialOptions } from 'typings'; import { PartialOptions } from 'typings';
import { InitializationStrategy } from 'initialization'; import { InitializationStrategy } from 'initialization';
export interface EnvironmentListenersNameArgsMap { type EnvironmentEventMap = {
_: undefined; _: [];
} };
export interface InternalEnvironment { export interface InternalEnvironment {
readonly _nativeScrollbarSize: XY; readonly _nativeScrollbarSize: XY;
@@ -44,7 +44,7 @@ export interface InternalEnvironment {
readonly _cssCustomProperties: boolean; readonly _cssCustomProperties: boolean;
readonly _defaultInitializationStrategy: InitializationStrategy; readonly _defaultInitializationStrategy: InitializationStrategy;
readonly _defaultDefaultOptions: Options; readonly _defaultDefaultOptions: Options;
_addListener(listener: EventListener<EnvironmentListenersNameArgsMap, '_'>): () => void; _addListener(listener: EventListener<EnvironmentEventMap, '_'>): () => void;
_getInitializationStrategy(): InitializationStrategy; _getInitializationStrategy(): InitializationStrategy;
_setInitializationStrategy(newInitializationStrategy: Partial<InitializationStrategy>): void; _setInitializationStrategy(newInitializationStrategy: Partial<InitializationStrategy>): void;
_getDefaultOptions(): Options; _getDefaultOptions(): Options;
@@ -152,7 +152,7 @@ const createEnvironment = (): InternalEnvironment => {
const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`); const envDOM = createDOM(`<div class="${classNameEnvironment}"><div></div></div>`);
const envElm = envDOM[0] as HTMLElement; const envElm = envDOM[0] as HTMLElement;
const envChildElm = envElm.firstChild as HTMLElement; const envChildElm = envElm.firstChild as HTMLElement;
const [addEvent, , triggerEvent] = createEventListenerHub<EnvironmentListenersNameArgsMap>(); const [addEvent, , triggerEvent] = createEventListenerHub<EnvironmentEventMap>();
const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache({ const [updateNativeScrollbarSizeCache, getNativeScrollbarSizeCache] = createCache({
_initialValue: getNativeScrollbarSize(body, envElm, envChildElm), _initialValue: getNativeScrollbarSize(body, envElm, envChildElm),
_equal: equalXY, _equal: equalXY,
@@ -99,12 +99,12 @@ export interface OnUpdatedEventListenerArgs {
force: boolean; force: boolean;
} }
export interface EventListenerMap { export type EventListenerMap = {
initialized: undefined; initialized: [];
initializationWithdrawn: undefined; initializationWithdrawn: [];
updated: OnUpdatedEventListenerArgs; updated: [OnUpdatedEventListenerArgs];
destroyed: undefined; destroyed: [];
} };
export type InitialEventListeners = GeneralInitialEventListeners<EventListenerMap>; export type InitialEventListeners = GeneralInitialEventListeners<EventListenerMap>;
@@ -128,8 +128,8 @@ export interface OverlayScrollbars {
on<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>): () => void; on<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>): () => void;
on<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>[]): () => void; on<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>[]): () => void;
off<Name extends keyof EventListenerMap>(name: Name, listener?: EventListener<Name>): void; off<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>): void;
off<Name extends keyof EventListenerMap>(name: Name, listener?: EventListener<Name>[]): void; off<Name extends keyof EventListenerMap>(name: Name, listener: EventListener<Name>[]): void;
} }
/** /**
@@ -205,19 +205,21 @@ export const OverlayScrollbars: OverlayScrollbarsStatic = (
_hostMutation, _hostMutation,
} = updateHints; } = updateHints;
triggerEvent('updated', { triggerEvent('updated', [
updateHints: { {
sizeChanged: _sizeChanged, updateHints: {
directionChanged: _directionChanged, sizeChanged: _sizeChanged,
heightIntrinsicChanged: _heightIntrinsicChanged, directionChanged: _directionChanged,
overflowAmountChanged: _overflowAmountChanged, heightIntrinsicChanged: _heightIntrinsicChanged,
overflowStyleChanged: _overflowStyleChanged, overflowAmountChanged: _overflowAmountChanged,
contentMutation: _contentMutation, overflowStyleChanged: _overflowStyleChanged,
hostMutation: _hostMutation, contentMutation: _contentMutation,
hostMutation: _hostMutation,
},
changedOptions,
force,
}, },
changedOptions, ]);
force,
});
}); });
const instance: OverlayScrollbars = { const instance: OverlayScrollbars = {
@@ -1,28 +1,28 @@
import { isArray } from 'support/utils/types'; import { isArray } from 'support/utils/types';
import { keys } from 'support/utils/object'; import { keys } from 'support/utils/object';
import { each, from } from 'support/utils/array'; import { each, from, isEmptyArray } from 'support/utils/array';
export type EventListener< export type EventListener<
EventMap extends Record<string, any>, EventMap extends Record<string, any[]>,
Name extends keyof EventMap = keyof EventMap Name extends keyof EventMap = keyof EventMap
> = (...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]]) => void; > = (...args: EventMap[Name]) => void;
export type InitialEventListeners<EventMap extends Record<string, any>> = { export type InitialEventListeners<EventMap extends Record<string, any[]>> = {
[K in keyof EventMap]?: EventListener<EventMap> | EventListener<EventMap>[]; [K in keyof EventMap]?: EventListener<EventMap> | EventListener<EventMap>[];
}; };
const manageListener = <EventMap extends Record<string, any>>( const manageListener = <EventMap extends Record<string, any[]>>(
callback: (listener?: EventListener<EventMap>) => void, callback: (listener?: EventListener<EventMap>) => void,
listener?: EventListener<EventMap> | EventListener<EventMap>[] listener?: EventListener<EventMap> | EventListener<EventMap>[]
) => { ) => {
each(isArray(listener) ? listener : [listener], callback); each(isArray(listener) ? listener : [listener], callback);
}; };
export const createEventListenerHub = <EventMap extends Record<string, any>>( export const createEventListenerHub = <EventMap extends Record<string, any[]>>(
initialEventListeners?: InitialEventListeners<EventMap> initialEventListeners?: InitialEventListeners<EventMap>
) => { ) => {
type EventListener<Name extends keyof EventMap = keyof EventMap> = ( type EventListener<Name extends keyof EventMap = keyof EventMap> = (
...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]] ...args: EventMap[Name]
) => void; ) => void;
const events = new Map<keyof EventMap, Set<EventListener>>(); const events = new Map<keyof EventMap, Set<EventListener>>();
@@ -76,15 +76,12 @@ export const createEventListenerHub = <EventMap extends Record<string, any>>(
return removeEvent.bind(0, name as any, listener as any); return removeEvent.bind(0, name as any, listener as any);
} }
function triggerEvent<Name extends keyof EventMap>( function triggerEvent<Name extends keyof EventMap>(name: Name, args?: EventMap[Name]): void {
name: Name,
...args: EventMap[Name] extends undefined ? [] : [args: EventMap[Name]]
): void {
const eventSet = events.get(name); const eventSet = events.get(name);
each(from(eventSet), (event) => { each(from(eventSet), (event) => {
if (args) { if (args && !isEmptyArray(args)) {
(event as (args: EventMap[keyof EventMap]) => void)(args as any); (event as (...args: EventMap[keyof EventMap]) => void).apply(0, args as any);
} else { } else {
(event as () => void)(); (event as () => void)();
} }
@@ -0,0 +1,166 @@
import { createEventListenerHub } from 'support/eventListeners';
type EventMap = {
onBoolean: [boolean, string];
onUndefined: [];
};
describe('eventListeners', () => {
describe('createEventListenerHub', () => {
test('initialization', () => {
const onBooleanA = jest.fn();
const onBooleanB = jest.fn();
const onUndefined = jest.fn();
const [, , triggerEvent] = createEventListenerHub<EventMap>({
onBoolean: [onBooleanA, onBooleanB],
onUndefined,
});
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(1);
expect(onBooleanA).toHaveBeenLastCalledWith(true, 'hi');
expect(onBooleanB).toHaveBeenCalledTimes(1);
expect(onBooleanB).toHaveBeenLastCalledWith(true, 'hi');
expect(onUndefined).toHaveBeenCalledTimes(1);
expect(onUndefined).toHaveBeenLastCalledWith();
});
test('addEvent', () => {
const onBooleanA = jest.fn();
const onBooleanB = jest.fn();
const onUndefinedA = jest.fn();
const onUndefinedB = jest.fn();
const [addEvent, , triggerEvent] = createEventListenerHub<EventMap>();
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
addEvent('onBoolean', onBooleanA);
addEvent('onUndefined', [onUndefinedA, onUndefinedB]);
triggerEvent('onBoolean', [true, 'hi']);
expect(onBooleanA).toHaveBeenCalledTimes(1);
expect(onBooleanA).toHaveBeenLastCalledWith(true, 'hi');
expect(onUndefinedA).not.toHaveBeenCalled();
expect(onUndefinedB).not.toHaveBeenCalled();
triggerEvent('onUndefined');
expect(onUndefinedA).toHaveBeenCalledTimes(1);
expect(onUndefinedA).toHaveBeenLastCalledWith();
expect(onUndefinedB).toHaveBeenCalledTimes(1);
expect(onUndefinedB).toHaveBeenLastCalledWith();
addEvent('onBoolean', onBooleanB);
triggerEvent('onBoolean', [false, 'hi2']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(2);
expect(onBooleanA).toHaveBeenLastCalledWith(false, 'hi2');
expect(onBooleanB).toHaveBeenCalledTimes(1);
expect(onBooleanB).toHaveBeenLastCalledWith(false, 'hi2');
expect(onUndefinedA).toHaveBeenCalledTimes(2);
expect(onUndefinedA).toHaveBeenLastCalledWith();
expect(onUndefinedB).toHaveBeenCalledTimes(2);
expect(onUndefinedB).toHaveBeenLastCalledWith();
const something = jest.fn();
// @ts-ignore
addEvent('something', something);
// @ts-ignore
triggerEvent('something');
expect(something).toHaveBeenCalledTimes(1);
});
test('removeEvent', () => {
const onBooleanA = jest.fn();
const onBooleanB = jest.fn();
const onUndefinedA = jest.fn();
const onUndefinedB = jest.fn();
const [addEvent, removeEvent, triggerEvent] = createEventListenerHub<EventMap>({
onBoolean: [onBooleanA, onBooleanB],
});
const removeUndefined = addEvent('onUndefined', [onUndefinedA, onUndefinedB]);
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(1);
expect(onBooleanB).toHaveBeenCalledTimes(1);
expect(onUndefinedA).toHaveBeenCalledTimes(1);
expect(onUndefinedB).toHaveBeenCalledTimes(1);
removeUndefined();
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(2);
expect(onBooleanB).toHaveBeenCalledTimes(2);
expect(onUndefinedA).toHaveBeenCalledTimes(1);
expect(onUndefinedB).toHaveBeenCalledTimes(1);
removeEvent('onBoolean', onBooleanA);
triggerEvent('onBoolean', [true, 'hi']);
expect(onBooleanA).toHaveBeenCalledTimes(2);
expect(onBooleanB).toHaveBeenCalledTimes(3);
removeEvent('onBoolean', onBooleanB);
triggerEvent('onBoolean', [true, 'hi']);
expect(onBooleanA).toHaveBeenCalledTimes(2);
expect(onBooleanB).toHaveBeenCalledTimes(3);
addEvent('onBoolean', [onBooleanA, onBooleanB]);
addEvent('onUndefined', [onUndefinedA, onUndefinedB]);
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(3);
expect(onBooleanB).toHaveBeenCalledTimes(4);
expect(onUndefinedA).toHaveBeenCalledTimes(2);
expect(onUndefinedB).toHaveBeenCalledTimes(2);
removeEvent('onBoolean');
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(3);
expect(onBooleanB).toHaveBeenCalledTimes(4);
expect(onUndefinedA).toHaveBeenCalledTimes(3);
expect(onUndefinedB).toHaveBeenCalledTimes(3);
addEvent('onBoolean', [onBooleanA, onBooleanB]);
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(4);
expect(onBooleanB).toHaveBeenCalledTimes(5);
expect(onUndefinedA).toHaveBeenCalledTimes(4);
expect(onUndefinedB).toHaveBeenCalledTimes(4);
removeEvent();
triggerEvent('onBoolean', [true, 'hi']);
triggerEvent('onUndefined');
expect(onBooleanA).toHaveBeenCalledTimes(4);
expect(onBooleanB).toHaveBeenCalledTimes(5);
expect(onUndefinedA).toHaveBeenCalledTimes(4);
expect(onUndefinedB).toHaveBeenCalledTimes(4);
// @ts-ignore
const something = jest.fn();
// @ts-ignore
addEvent('something', something);
// @ts-ignore
removeEvent('something');
// @ts-ignore
triggerEvent('something');
expect(something).not.toHaveBeenCalled();
// @ts-ignore
removeEvent('not previously added');
});
});
});