From 246474daac02e9623c8f028e845ec78e4dc62ab8 Mon Sep 17 00:00:00 2001 From: Tomas Engebretsen Date: Thu, 16 Jan 2025 13:47:24 +0100 Subject: [PATCH] test: Create tool for mocking event listeners (#14415) Co-authored-by: andreastanderen <71079896+standeren@users.noreply.github.com> --- .../test/EventListeners.test.ts | 210 ++++++++++++++++++ .../process-editor/test/EventListeners.ts | 78 +++++++ 2 files changed, 288 insertions(+) create mode 100644 frontend/packages/process-editor/test/EventListeners.test.ts create mode 100644 frontend/packages/process-editor/test/EventListeners.ts diff --git a/frontend/packages/process-editor/test/EventListeners.test.ts b/frontend/packages/process-editor/test/EventListeners.test.ts new file mode 100644 index 00000000000..95bc72fe6ba --- /dev/null +++ b/frontend/packages/process-editor/test/EventListeners.test.ts @@ -0,0 +1,210 @@ +import { EventListeners } from './EventListeners'; + +describe('EventListeners', () => { + describe('add', () => { + it('Adds a listener to the given event', () => { + const eventListeners = new EventListeners<{ event: () => void }>(); + const fun = jest.fn(); + const eventName = 'event'; + + eventListeners.add(eventName, fun); + eventListeners.triggerEvent(eventName); + + expect(fun).toHaveBeenCalledTimes(1); + }); + + it('Supports adding multiple listeners to the same event', () => { + const eventListeners = new EventListeners<{ event: () => void }>(); + const fun1 = jest.fn(); + const fun2 = jest.fn(); + const eventName = 'event'; + + eventListeners.add(eventName, fun1); + eventListeners.add(eventName, fun2); + eventListeners.triggerEvent(eventName); + + expect(fun1).toHaveBeenCalledTimes(1); + expect(fun2).toHaveBeenCalledTimes(1); + }); + + it('Supports adding listeners to multiple events', () => { + const eventListeners = new EventListeners void>>(); + const event1Fun = jest.fn(); + const event2Fun = jest.fn(); + const event1Name = 'event1'; + const event2Name = 'event2'; + + eventListeners.add(event1Name, event1Fun); + eventListeners.add(event2Name, event2Fun); + eventListeners.triggerEvent(event1Name); + eventListeners.triggerEvent(event2Name); + + expect(event1Fun).toHaveBeenCalledTimes(1); + expect(event2Fun).toHaveBeenCalledTimes(1); + }); + + it('Supports adding the same function to multiple events', () => { + const eventListeners = new EventListeners void>>(); + const fun = jest.fn(); + const event1Name = 'event1'; + const event2Name = 'event2'; + + eventListeners.add(event1Name, fun); + eventListeners.add(event2Name, fun); + eventListeners.triggerEvent(event1Name); + eventListeners.triggerEvent(event2Name); + + expect(fun).toHaveBeenCalledTimes(2); + }); + }); + + describe('remove', () => { + it('Removes the given function from the given event listener', () => { + const eventListeners = new EventListeners<{ event: () => void }>(); + const fun = jest.fn(); + const eventName = 'event'; + eventListeners.add(eventName, fun); + + eventListeners.remove(eventName, fun); + eventListeners.triggerEvent(eventName); + + expect(fun).not.toHaveBeenCalled(); + }); + + it('Does not remove other functions than the given one', () => { + const eventListeners = new EventListeners< + Record<'event.of.interest' | 'another.event', () => void> + >(); + const funToRemove = jest.fn(); + const funOnSameEvent = jest.fn(); + const funOnAnotherEvent = jest.fn(); + const eventOfInterestName = 'event.of.interest'; + const anotherEventName = 'another.event'; + eventListeners.add(eventOfInterestName, funToRemove); + eventListeners.add(eventOfInterestName, funOnSameEvent); + eventListeners.add(anotherEventName, funOnAnotherEvent); + + eventListeners.remove(eventOfInterestName, funToRemove); + eventListeners.triggerEvent(eventOfInterestName); + eventListeners.triggerEvent(anotherEventName); + + expect(funOnSameEvent).toHaveBeenCalled(); + expect(funOnAnotherEvent).toHaveBeenCalled(); + expect(funToRemove).not.toHaveBeenCalled(); + }); + + it('Does only remove the function on the given event listener when the same function also exists on another listener', () => { + const eventListeners = new EventListeners< + Record<'event.of.interest' | 'another.event', () => void> + >(); + const funToRemoveFromSingleEvent = jest.fn(); + const eventOfInterestName = 'event.of.interest'; + const anotherEventName = 'another.event'; + eventListeners.add(eventOfInterestName, funToRemoveFromSingleEvent); + eventListeners.add(anotherEventName, funToRemoveFromSingleEvent); + + eventListeners.remove(eventOfInterestName, funToRemoveFromSingleEvent); + eventListeners.triggerEvent(eventOfInterestName); + eventListeners.triggerEvent(anotherEventName); + + expect(funToRemoveFromSingleEvent).toHaveBeenCalledTimes(1); + }); + + it('Throws the expected error when attempting to remove a function that is not added', () => { + const eventListeners = new EventListeners<{ event: () => void }>(); + const fun = jest.fn(); + const eventName = 'event'; + + expect(() => eventListeners.remove(eventName, fun)).toThrow( + `The provided callback function does not exist on the ${eventName} listener.`, + ); + }); + }); + + describe('triggerEvent', () => { + it('Calls all the functions added to the given event listener with correct parameters', () => { + const eventListeners = new EventListeners<{ event: (p: string) => void }>(); + const fun1 = jest.fn(); + const fun2 = jest.fn(); + const eventName = 'event'; + eventListeners.add(eventName, fun1); + eventListeners.add(eventName, fun2); + const param = 'test'; + + eventListeners.triggerEvent(eventName, param); + + expect(fun1).toHaveBeenCalledTimes(1); + expect(fun1).toHaveBeenCalledWith(param); + expect(fun2).toHaveBeenCalledTimes(1); + expect(fun2).toHaveBeenCalledWith(param); + }); + + it('Supports functions with multiple parameters', () => { + const eventListeners = new EventListeners<{ + event: (p1: string, p2: number, p3: boolean) => void; + }>(); + const fun = jest.fn(); + const eventName = 'event'; + eventListeners.add(eventName, fun); + const param1 = 'test'; + const param2 = 1; + const param3 = true; + + eventListeners.triggerEvent(eventName, param1, param2, param3); + + expect(fun).toHaveBeenCalledTimes(1); + expect(fun).toHaveBeenCalledWith(param1, param2, param3); + }); + + it('Supports functions with no parameters', () => { + const eventListeners = new EventListeners<{ event: () => void }>(); + const fun = jest.fn(); + const eventName = 'event'; + eventListeners.add(eventName, fun); + + eventListeners.triggerEvent(eventName); + + expect(fun).toHaveBeenCalledTimes(1); + expect(fun).toHaveBeenCalledWith(); + }); + + it('Does not call functions on other listeners than the given one', () => { + const eventListeners = new EventListeners< + Record<'event.of.interest' | 'another.event', () => void> + >(); + const funOfInterest = jest.fn(); + const funOnAnotherEvent = jest.fn(); + const eventOfInterestName = 'event.of.interest'; + const anotherEventName = 'another.event'; + eventListeners.add(eventOfInterestName, funOfInterest); + eventListeners.add(anotherEventName, funOnAnotherEvent); + + eventListeners.triggerEvent(eventOfInterestName); + + expect(funOnAnotherEvent).not.toHaveBeenCalled(); + expect(funOfInterest).toHaveBeenCalled(); + }); + }); + + describe('clear', () => { + it('Removes all listeners', () => { + const eventListeners = new EventListeners void>>(); + const event1Fun1 = jest.fn(); + const event1Fun2 = jest.fn(); + const event2Fun = jest.fn(); + const event1Name = 'event1'; + const event2Name = 'event2'; + eventListeners.add(event1Name, event1Fun1); + eventListeners.add(event1Name, event1Fun2); + eventListeners.add(event2Name, event2Fun); + + eventListeners.clear(); + eventListeners.triggerEvent(event1Name); + eventListeners.triggerEvent(event2Name); + + expect(event1Fun1).not.toHaveBeenCalled(); + expect(event1Fun2).not.toHaveBeenCalled(); + expect(event2Fun).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/packages/process-editor/test/EventListeners.ts b/frontend/packages/process-editor/test/EventListeners.ts new file mode 100644 index 00000000000..6df2269e375 --- /dev/null +++ b/frontend/packages/process-editor/test/EventListeners.ts @@ -0,0 +1,78 @@ +import { ArrayUtils } from '@studio/pure-functions'; + +type ListenerMap void>> = Map< + keyof EventMap, + Array +>; + +export class EventListeners void>> { + private readonly list: ListenerMap; + + constructor() { + this.list = new Map(); + } + + triggerEvent( + eventName: Key, + ...params: Parameters + ): void { + if (this.has(eventName)) { + const functions = this.get(eventName); + functions.forEach((fun) => fun(...params)); + } + } + + add(eventName: Key, callback: EventMap[Key]): void { + if (this.has(eventName)) this.addListenerToCurrentList(eventName, callback); + else this.createNewListenerList(eventName, [callback]); + } + + private addListenerToCurrentList( + eventName: Key, + callback: EventMap[Key], + ): void { + const currentListeners = this.get(eventName); + this.set(eventName, [...currentListeners, callback]); + } + + private createNewListenerList( + eventName: Key, + callbacks: EventMap[Key][], + ): void { + this.set(eventName, callbacks); + } + + remove(eventName: Key, callback: EventMap[Key]): void { + if (!this.functionExists(eventName, callback)) + throw new Error( + `The provided callback function does not exist on the ${String(eventName)} listener.`, + ); + + const currentList = this.get(eventName); + const newList = ArrayUtils.removeItemByValue(currentList, callback); + this.set(eventName, newList); + } + + private functionExists( + eventName: Key, + callback: EventMap[Key], + ): boolean { + return this.has(eventName) && this.get(eventName).includes(callback); + } + + private has(eventName: keyof EventMap): boolean { + return this.list.has(eventName); + } + + private get(eventName: Key): EventMap[Key][] | undefined { + return this.list.get(eventName) as EventMap[Key][] | undefined; + } + + private set(eventName: Key, callbacks: EventMap[Key][]): void { + this.list.set(eventName, callbacks); + } + + clear(): void { + this.list.clear(); + } +}