Add Event Emitter
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "0.1",
|
||||
"import": ["@generalprotocols/cspell-dictionary/cspell.json"],
|
||||
"words": ["nonfungible", "lockscript", "cashassembly", "checksigverify", "inputindex", "utxovalue"],
|
||||
"words": ["nonfungible", "lockscript", "cashassembly", "checksigverify", "inputindex", "utxovalue", "bivariance"],
|
||||
"ignorePaths": ["source/template/xo-template.schema.json"]
|
||||
}
|
||||
|
||||
Generated
+6
-6
@@ -7034,9 +7034,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -7715,9 +7715,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Error thrown when a waitFor timeout is reached
|
||||
*/
|
||||
export class WaitForTimeoutError extends Error {
|
||||
constructor(type: string) {
|
||||
super(`Timeout waiting for event "${type}"`);
|
||||
this.name = 'WaitForTimeoutError';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import type { DeeplyReadonly } from './types.ts';
|
||||
import type { deepFreeze } from './misc.ts';
|
||||
|
||||
import { WaitForTimeoutError } from './errors.ts';
|
||||
|
||||
export type EventMap = Record<string, unknown>;
|
||||
|
||||
type Listener<T> = (detail: T) => void;
|
||||
|
||||
/**
|
||||
* Internally permits listeners for individual event payloads to be stored
|
||||
* in a collection typed with the union of all event payloads.
|
||||
*/
|
||||
type StoredListener<T> = {
|
||||
bivarianceHack(detail: T): void;
|
||||
}['bivarianceHack'];
|
||||
|
||||
/**
|
||||
* A listener entry.
|
||||
* @template T - The event payload type.
|
||||
*/
|
||||
interface ListenerEntry<T> {
|
||||
listener: StoredListener<T>;
|
||||
wrappedListener: StoredListener<T>;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback returned by {@link on} and {@link once} for removing a listener.
|
||||
*/
|
||||
export type OffCallback = () => void;
|
||||
|
||||
/**
|
||||
* A simple event emitter implementation.
|
||||
* @template T - The event map type.
|
||||
*/
|
||||
export class EventEmitter<T extends EventMap> {
|
||||
/**
|
||||
* The listeners map.
|
||||
* @private
|
||||
*/
|
||||
#listeners: Map<keyof T, Set<ListenerEntry<T[keyof T]>>> = new Map();
|
||||
|
||||
/**
|
||||
* Add a listener for an event.
|
||||
* @param type - The event type.
|
||||
* @param listener - The listener function.
|
||||
* @param debounceMilliseconds - The debounce time in milliseconds.
|
||||
* @returns An off callback that can be called to stop listening for events.
|
||||
*/
|
||||
on<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds: number = 0): OffCallback {
|
||||
const { cancel, listener: cancellableListener } = this.cancellable(listener);
|
||||
|
||||
// Create a wrapped listener so that the debounce can be applied.
|
||||
const wrappedListener = debounceMilliseconds > 0 ? this.debounce(cancellableListener, debounceMilliseconds) : cancellableListener;
|
||||
|
||||
// If the listeners map does not have the event type, create a new set.
|
||||
if (!this.#listeners.has(type)) {
|
||||
this.#listeners.set(type, new Set());
|
||||
}
|
||||
|
||||
// Create a listener entry.
|
||||
const listenerEntry: ListenerEntry<T[K]> = {
|
||||
listener,
|
||||
wrappedListener,
|
||||
cancel,
|
||||
};
|
||||
|
||||
// Add the listener entry to the listeners map.
|
||||
this.#listeners.get(type)?.add(listenerEntry);
|
||||
|
||||
// Return an "off" callback that can be called to stop listening for events.
|
||||
return () => this.off(type, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a one-time listener for an event.
|
||||
* @param type - The event type.
|
||||
* @param listener - The listener function.
|
||||
* @param debounceMilliseconds - The debounce time in milliseconds.
|
||||
* @returns An off callback that can be called to stop listening for events.
|
||||
*/
|
||||
once<K extends keyof T>(type: K, listener: Listener<T[K]>, debounceMilliseconds: number = 0): OffCallback {
|
||||
const wrappedListener: Listener<T[K]> = (detail: T[K]) => {
|
||||
this.off(type, listener);
|
||||
listener(detail);
|
||||
};
|
||||
|
||||
// Create a cancellable listener.
|
||||
const { cancel, listener: cancellableListener } = this.cancellable(wrappedListener);
|
||||
|
||||
// Create a debounced listener.
|
||||
const debouncedListener = debounceMilliseconds > 0 ? this.debounce(cancellableListener, debounceMilliseconds) : cancellableListener;
|
||||
|
||||
// If the listeners map does not have the event type, create a new set.
|
||||
if (!this.#listeners.has(type)) {
|
||||
this.#listeners.set(type, new Set());
|
||||
}
|
||||
|
||||
// Create a listener entry.
|
||||
const listenerEntry: ListenerEntry<T[K]> = {
|
||||
listener,
|
||||
wrappedListener: debouncedListener,
|
||||
cancel,
|
||||
};
|
||||
|
||||
// Add the listener entry to the listeners map.
|
||||
this.#listeners.get(type)?.add(listenerEntry);
|
||||
|
||||
// Return an "off" callback that can be called to stop listening for events.
|
||||
return () => this.off(type, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener for an event.
|
||||
* @param type - The event type.
|
||||
* @param listener - The listener function.
|
||||
*/
|
||||
off<K extends keyof T>(type: K, listener?: Listener<T[K]>): void {
|
||||
// Get the listeners for the event type.
|
||||
const listeners = this.#listeners.get(type);
|
||||
if (!listeners) return;
|
||||
|
||||
// Find the listener entries (If a listener was provided, only 1 entry will be returned. Otherwise, all entries will be returned).
|
||||
const listenerEntries = Array.from(listeners).filter((entry) => !listener || entry.listener === listener || entry.wrappedListener === listener);
|
||||
|
||||
// Remove the listener entries from the listeners set.
|
||||
listenerEntries.forEach((entry) => {
|
||||
// Set the wrapped listener to a no-op function to prevent it from being called by debounced events after it's been removed.
|
||||
entry.cancel();
|
||||
|
||||
// Remove the listener entry from the listeners set.
|
||||
listeners.delete(entry);
|
||||
});
|
||||
|
||||
// If no listener was provided and no listeners are left for the event type, remove the listeners set from the listeners map.
|
||||
if (!listener || this.#listeners.get(type)?.size === 0) {
|
||||
this.#listeners.delete(type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event.
|
||||
*
|
||||
* @remarks The caller is responsible for ensuring the payload suits the intended mutability requirements.
|
||||
* By default, the payload will be mutable, so listeners may mutate the payload, effecting both
|
||||
* the original object and the other listeners.
|
||||
* To prevent this, the caller can use the {@link Object.freeze} or {@link deepFreeze} function to freeze the payload.
|
||||
* This will need to be defined in the EventMap using the built-in {@link Readonly} type or the provided {@link DeeplyReadonly} type.
|
||||
*
|
||||
* @param type - The event type.
|
||||
* @param payload - The event payload.
|
||||
* @returns True if there are listeners for the event, false otherwise.
|
||||
*/
|
||||
emit<K extends keyof T>(type: K, payload: T[K]): boolean {
|
||||
// Get the listeners for the event type.
|
||||
const listeners = this.#listeners.get(type);
|
||||
if (!listeners) return false;
|
||||
|
||||
// Emit the event to all listeners.
|
||||
listeners.forEach((entry) => {
|
||||
try {
|
||||
entry.wrappedListener(payload);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Return true if there are listeners for the event, false otherwise.
|
||||
return listeners.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all listeners.
|
||||
*/
|
||||
removeAllListeners(): void {
|
||||
for (const [ type, listeners ] of this.#listeners.entries()) {
|
||||
listeners.forEach((entry) => {
|
||||
this.off(type, entry.listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for an event to be emitted that matches the provided predicate function's criteria.
|
||||
* @param type - The event type.
|
||||
* @param predicate - Predicate function to filter for whether the event payload matches the criteria.
|
||||
* @param timeoutMs - The timeout in milliseconds.
|
||||
* @returns The event payload.
|
||||
*/
|
||||
async waitFor<K extends keyof T>(type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number): Promise<T[K]> {
|
||||
// Create a promise to wait for the event to be emitted.
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
// Create a cleanup function to remove the listener and clear the timeout if it is still pending.
|
||||
const cleanup = (listener: Listener<T[K]>): void => {
|
||||
// Remove the listener from the listeners map.
|
||||
this.off(type, listener);
|
||||
|
||||
// Clear the timeout if it is still pending.
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
// Create a listener function.
|
||||
const listener = (payload: T[K]): void => {
|
||||
try {
|
||||
// If the event payload does not match the predicate condition, return.
|
||||
if (!predicate(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup(listener);
|
||||
resolve(payload);
|
||||
} catch (error) {
|
||||
cleanup(listener);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up timeout if specified
|
||||
if (timeoutMs !== undefined) {
|
||||
timeoutId = setTimeout(() => {
|
||||
this.off(type, listener);
|
||||
reject(new WaitForTimeoutError(String(type)));
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
// Add the listener to the listeners map.
|
||||
this.on(type, listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce a function.
|
||||
*
|
||||
* @remarks If {@link off} is called on a listener while it is debounced, the timeout is not cleared with clearTimeout.
|
||||
* Instead, the function is no-oped.
|
||||
*
|
||||
* @param func - The function to debounce.
|
||||
* @param wait - The wait time in milliseconds.
|
||||
* @returns The debounced function.
|
||||
*/
|
||||
private debounce<K extends keyof T>(func: Listener<T[K]>, wait: number): Listener<T[K]> {
|
||||
// Create a timeout variable.
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
return (detail: T[K]) => {
|
||||
// If a debounce timer is already pending, clear it before scheduling the next one.
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
func(detail);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a function cancellable.
|
||||
* @param func - The function to make cancelable.
|
||||
* @returns The cancellable function with a cancel method.
|
||||
*/
|
||||
private cancellable<K extends keyof T>(func: Listener<T[K]>): { cancel: () => void; listener: Listener<T[K]> } {
|
||||
let cancelled = false;
|
||||
|
||||
return {
|
||||
cancel: (): boolean => (cancelled = true),
|
||||
listener: (detail: T[K]): void => {
|
||||
if (cancelled) return;
|
||||
func(detail);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './event-emitter.ts';
|
||||
export * from './extended-json.ts';
|
||||
export * from './script.ts';
|
||||
export * from './sse-session/index.ts';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { DeeplyReadonly } from './types';
|
||||
|
||||
/**
|
||||
* Recursively freezes an object by iterating over all properties and freezing them.
|
||||
* @param obj - The object to freeze.
|
||||
* @returns The frozen object.
|
||||
*/
|
||||
export const deepFreeze = <T>(value: T): DeeplyReadonly<T> => {
|
||||
if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
|
||||
for (const key of Reflect.ownKeys(value)) {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
||||
|
||||
if (descriptor && 'value' in descriptor) {
|
||||
deepFreeze(descriptor.value);
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* A deeply readonly type.
|
||||
* @template T - The type to make deeply readonly.
|
||||
* @returns The deeply readonly type.
|
||||
*/
|
||||
export type DeeplyReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends (...args: never[]) => unknown ? T[K] : DeeplyReadonly<T[K]>;
|
||||
};
|
||||
@@ -0,0 +1,819 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { EventEmitter } from '../source/event-emitter.ts';
|
||||
|
||||
import type { DeeplyReadonly } from '../source/types.ts';
|
||||
import { deepFreeze } from '../source/misc.ts';
|
||||
|
||||
/** Simple event map used across these tests. */
|
||||
type TestEvents = {
|
||||
message: string;
|
||||
count: number;
|
||||
readonly: Readonly<{ value: number }>;
|
||||
deeplyReadonly: DeeplyReadonly<{ nested: { value: number } }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter invokes listeners when an event is emitted.
|
||||
*/
|
||||
const testEventEmitterCallsListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
// Register the listener and emit an event.
|
||||
emitter.on('message', listener);
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
// Expect the listener to have been called with the emitted payload.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith('hello');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter invokes all listeners registered for the same event.
|
||||
*/
|
||||
const testEventEmitterCallsMultipleListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const firstListener = vi.fn();
|
||||
const secondListener = vi.fn();
|
||||
|
||||
// Register two listeners for the same event type.
|
||||
emitter.on('count', firstListener);
|
||||
emitter.on('count', secondListener);
|
||||
emitter.emit('count', 42);
|
||||
|
||||
// Expect both listeners to receive the same payload.
|
||||
expect(firstListener).toHaveBeenCalledOnce();
|
||||
expect(firstListener).toHaveBeenCalledWith(42);
|
||||
expect(secondListener).toHaveBeenCalledOnce();
|
||||
expect(secondListener).toHaveBeenCalledWith(42);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter only invokes listeners registered for the emitted event type.
|
||||
*/
|
||||
const testEventEmitterCallsOnlyMatchingListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const messageListener = vi.fn();
|
||||
const countListener = vi.fn();
|
||||
|
||||
// Register listeners on different event types.
|
||||
emitter.on('message', messageListener);
|
||||
emitter.on('count', countListener);
|
||||
|
||||
// Emit only the message event.
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
// Expect only the matching listener to have been called.
|
||||
expect(messageListener).toHaveBeenCalledOnce();
|
||||
expect(countListener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter.emit returns false when no listeners are registered.
|
||||
*/
|
||||
const testEventEmitterEmitReturnsFalseWithNoListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
const hasListeners = emitter.emit('message', 'hello');
|
||||
|
||||
// Expect emit to report that nobody was listening.
|
||||
expect(hasListeners).toBe(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter.emit returns true when listeners are registered.
|
||||
*/
|
||||
const testEventEmitterEmitReturnsTrueWithListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
emitter.on('message', vi.fn());
|
||||
|
||||
const hasListeners = emitter.emit('message', 'hello');
|
||||
|
||||
// Expect emit to report that at least one listener was invoked.
|
||||
expect(hasListeners).toBe(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that EventEmitter.emit continues after a listener throws an error.
|
||||
*/
|
||||
const testEventEmitterEmitContinuesAfterListenerThrows = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const secondListener = vi.fn();
|
||||
|
||||
emitter.on('message', (): void => {
|
||||
throw new Error('listener failure');
|
||||
});
|
||||
emitter.on('message', secondListener);
|
||||
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
expect(secondListener).toHaveBeenCalledOnce();
|
||||
expect(secondListener).toHaveBeenCalledWith('hello');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that it supports Object.freeze() frozen payloads.
|
||||
*/
|
||||
const testEventEmitterSupportsObjectFreezeFrozenPayloads = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
const payload = Object.freeze({ value: 42 });
|
||||
|
||||
emitter.on('readonly', listener, 100);
|
||||
emitter.emit('readonly', payload);
|
||||
|
||||
// @ts-expect-error - We are trying to mutate a frozen payload.
|
||||
expect(() => (payload.value = 43)).toThrow();
|
||||
|
||||
// Advance the timer to ensure the debounce timer goes off
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Expect the listener to have been called with the frozen payload.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith(payload);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that it supports deepFreeze() frozen payloads.
|
||||
*/
|
||||
const testEventEmitterSupportsDeepFreezeFrozenPayloads = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
const payload = deepFreeze({ nested: { value: 42 } });
|
||||
|
||||
emitter.on('deeplyReadonly', listener, 100);
|
||||
emitter.emit('deeplyReadonly', payload);
|
||||
|
||||
// @ts-expect-error - We are trying to mutate a frozen payload.
|
||||
expect(() => (payload.nested.value = 43)).toThrow();
|
||||
|
||||
// Advance the timer to ensure the debounce timer goes off
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Expect the listener to have been called with the frozen payload.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith(payload);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that the off callback returned by on() removes the listener.
|
||||
*/
|
||||
const testEventEmitterOffCallbackRemovesListener = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
// on() returns an off callback that removes the listener.
|
||||
const off = emitter.on('message', listener);
|
||||
emitter.emit('message', 'first');
|
||||
|
||||
// Unsubscribe before emitting again.
|
||||
off();
|
||||
emitter.emit('message', 'second');
|
||||
|
||||
// Expect the listener to have only received the first event.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith('first');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that off() removes a listener when given the same function reference.
|
||||
*/
|
||||
const testEventEmitterOffRemovesListenerByReference = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener);
|
||||
emitter.off('message', listener);
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
// Expect the listener to have been removed before the emit.
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that off() removes all listeners for an event type when no listener is provided.
|
||||
*/
|
||||
const testEventEmitterOffRemovesAllListenersForEventType = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener);
|
||||
emitter.off('message');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
expect(emitter.emit('message', 'hello')).toBe(false);
|
||||
expect(emitter.emit('count', 42)).toBe(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that off() does nothing when given an unknown listener reference.
|
||||
*/
|
||||
const testEventEmitterOffIgnoresUnknownListener = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener);
|
||||
|
||||
// Try to remove a different function reference.
|
||||
emitter.off('message', vi.fn());
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
// Expect the original listener to still receive the event.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that off() does nothing when called for an event type with no listeners.
|
||||
*/
|
||||
const testEventEmitterOffIgnoresUnregisteredEventType = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
// Call off without ever registering this listener.
|
||||
emitter.off('message', listener);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that once() listeners are invoked only one time.
|
||||
*/
|
||||
const testEventEmitterOnceListenerFiresOnce = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.once('message', listener);
|
||||
emitter.emit('message', 'first');
|
||||
emitter.emit('message', 'second');
|
||||
|
||||
// Expect the listener to auto-unsubscribe after the first emit.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith('first');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that once() can be added when regular listeners already exist for the event type.
|
||||
*/
|
||||
const testEventEmitterOnceWorksWithExistingListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const existingListener = vi.fn();
|
||||
const onceListener = vi.fn();
|
||||
|
||||
// Register a regular listener first so the event type already exists in the map.
|
||||
emitter.on('message', existingListener);
|
||||
emitter.once('message', onceListener);
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
expect(existingListener).toHaveBeenCalledOnce();
|
||||
expect(onceListener).toHaveBeenCalledOnce();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that the off callback returned by once() removes the listener before it fires.
|
||||
*/
|
||||
const testEventEmitterOnceOffCallbackRemovesListener = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
const off = emitter.once('message', listener);
|
||||
|
||||
// Unsubscribe before the event is ever emitted.
|
||||
off();
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that debounced listeners do not receive the debounced event if the listener is removed.
|
||||
*/
|
||||
const testEventEmitterOffCancelsPendingDebouncedCallback = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
const off = emitter.on('message', listener, 100);
|
||||
emitter.emit('message', 'first');
|
||||
|
||||
off();
|
||||
emitter.emit('message', 'second');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that removeAllListeners() clears every registered listener.
|
||||
*/
|
||||
const testEventEmitterRemoveAllListeners = (): void => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const messageListener = vi.fn();
|
||||
const countListener = vi.fn();
|
||||
|
||||
emitter.on('message', messageListener);
|
||||
emitter.on('count', countListener);
|
||||
emitter.removeAllListeners();
|
||||
|
||||
// Emit on both event types after clearing all listeners.
|
||||
emitter.emit('message', 'hello');
|
||||
emitter.emit('count', 1);
|
||||
|
||||
expect(messageListener).not.toHaveBeenCalled();
|
||||
expect(countListener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that removeAllListeners() cancels a pending debounced callback.
|
||||
*/
|
||||
const testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
// Arm a debounce timer, then clear every listener before it expires.
|
||||
emitter.on('message', listener, 100);
|
||||
emitter.emit('message', 'should not arrive');
|
||||
emitter.removeAllListeners();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Cleared listeners must not receive delayed debounced delivery.
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that timeouts are eventually cleared when removeAllListeners is called.
|
||||
*/
|
||||
const testEventEmitterTimeoutsAreClearedAfterTheyRunWhenRemoveAllListenersIsCalled = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener, 100);
|
||||
emitter.emit('message', 'pending');
|
||||
|
||||
// We expect there to be a timer since we emitted a debounced event
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
|
||||
// Remove all listeners
|
||||
emitter.removeAllListeners();
|
||||
|
||||
// Advance the timer to ensure the debounce timer is cleared.
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Make sure the listener was not called
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Make sure the timer was cleared
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that timeouts are eventually cleared when off is called.
|
||||
*/
|
||||
const testEventEmitterTimeoutsAreClearedWhenOffIsCalled = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
const off = emitter.on('message', listener, 100);
|
||||
emitter.emit('message', 'pending');
|
||||
|
||||
// We expect there to be a timer since we emitted a debounced event
|
||||
expect(vi.getTimerCount()).toBe(1);
|
||||
|
||||
// Cancel the listener
|
||||
off();
|
||||
|
||||
// Advance the timer to ensure the debounce timer is cleared.
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Make sure the listener was not called
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Make sure the timer was cleared
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() resolves when a matching event is emitted.
|
||||
*/
|
||||
const testEventEmitterWaitForResolvesOnMatch = async (): Promise<void> => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
// Wait until an event matches the predicate.
|
||||
const waitPromise = emitter.waitFor('count', (payload) => payload === 42);
|
||||
|
||||
// Emit a non-matching event first, then the matching one.
|
||||
emitter.emit('count', 41);
|
||||
emitter.emit('count', 42);
|
||||
|
||||
await expect(waitPromise).resolves.toBe(42);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() ignores non-matching events while other listeners still receive them.
|
||||
*/
|
||||
const testEventEmitterWaitForIgnoresNonMatchingEvents = async (): Promise<void> => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
const waitPromise = emitter.waitFor('message', (payload) => payload === 'done');
|
||||
|
||||
// A regular listener should still receive every emit while waitFor filters.
|
||||
emitter.on('message', listener);
|
||||
emitter.emit('message', 'pending');
|
||||
emitter.emit('message', 'done');
|
||||
|
||||
await expect(waitPromise).resolves.toBe('done');
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() rejects when the timeout expires.
|
||||
*/
|
||||
const testEventEmitterWaitForRejectsOnTimeout = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
const waitPromise = emitter.waitFor('message', () => true, 100);
|
||||
|
||||
// Attach the rejection handler before advancing timers so the rejection is handled.
|
||||
const assertion = expect(waitPromise).rejects.toThrow('Timeout waiting for event "message"');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
await assertion;
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() clears its timeout when it resolves before expiry.
|
||||
*/
|
||||
const testEventEmitterWaitForClearsTimeoutOnResolve = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
// Register waitFor with a timeout, then resolve it before the timer fires.
|
||||
const waitPromise = emitter.waitFor('message', (payload) => payload === 'done', 100);
|
||||
|
||||
emitter.emit('message', 'done');
|
||||
|
||||
await expect(waitPromise).resolves.toBe('done');
|
||||
|
||||
// If clearTimeout was not called, advancing past the timeout would reject the promise.
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() removes its listener after resolving.
|
||||
*/
|
||||
const testEventEmitterWaitForRemovesListenerAfterResolve = async (): Promise<void> => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
const waitPromise = emitter.waitFor('message', () => true);
|
||||
|
||||
emitter.emit('message', 'first');
|
||||
await expect(waitPromise).resolves.toBe('first');
|
||||
|
||||
// Register a second waitFor so we can verify the first listener was cleaned up.
|
||||
const secondWaitPromise = emitter.waitFor('message', (payload) => payload === 'second');
|
||||
|
||||
// Emit a payload that only the second waitFor should accept.
|
||||
emitter.emit('message', 'ignored');
|
||||
|
||||
// Track whether the second waitFor resolves too early.
|
||||
let resolvedEarly = false;
|
||||
/* eslint-disable-next-line */
|
||||
secondWaitPromise.then(() => {
|
||||
resolvedEarly = true;
|
||||
});
|
||||
|
||||
// Yield so any premature resolution would have a chance to run.
|
||||
await Promise.resolve();
|
||||
expect(resolvedEarly).toBe(false);
|
||||
|
||||
emitter.emit('message', 'second');
|
||||
await expect(secondWaitPromise).resolves.toBe('second');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that the first debounced emit does not call clearTimeout.
|
||||
*/
|
||||
const testEventEmitterDebouncedFirstEmitDoesNotClearTimeout = (): void => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener, 100);
|
||||
emitter.emit('message', 'first');
|
||||
|
||||
// The first emit starts the debounce timer; there is nothing to clear yet.
|
||||
expect(clearTimeoutSpy).not.toHaveBeenCalled();
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that debounced on() listeners receive only the last payload after the debounce window.
|
||||
*/
|
||||
const testEventEmitterDebouncedOnListener = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('message', listener, 100);
|
||||
|
||||
// Emit several events in quick succession.
|
||||
emitter.emit('message', 'first');
|
||||
emitter.emit('message', 'second');
|
||||
emitter.emit('message', 'third');
|
||||
|
||||
// Expect the listener to not have fired yet.
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Advance past the debounce window.
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Expect only the last payload to have been delivered.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith('third');
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that repeated debounced emits reset the debounce timer.
|
||||
*/
|
||||
const testEventEmitterDebouncedTimerResetsOnRepeatedEmits = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.on('count', listener, 100);
|
||||
emitter.emit('count', 1);
|
||||
|
||||
// Advance halfway through the debounce window and emit again.
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
emitter.emit('count', 2);
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
|
||||
// The timer was reset, so the listener should not have fired yet.
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// Advance the remaining time for the reset timer to expire.
|
||||
await vi.advanceTimersByTimeAsync(50);
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that a debounce time of zero behaves like a normal listener.
|
||||
*/
|
||||
const testEventEmitterZeroDebounceDoesNotDebounce = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
// A debounce time of zero should behave like a normal listener.
|
||||
emitter.on('message', listener, 0);
|
||||
emitter.emit('message', 'first');
|
||||
emitter.emit('message', 'second');
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that debounced once() listeners fire once with the last payload.
|
||||
*/
|
||||
const testEventEmitterDebouncedOnceListener = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
emitter.once('message', listener, 100);
|
||||
emitter.emit('message', 'first');
|
||||
emitter.emit('message', 'second');
|
||||
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Expect the debounced once listener to fire once with the last payload.
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(listener).toHaveBeenCalledWith('second');
|
||||
|
||||
// Emit again after the debounce window; the once listener should stay removed.
|
||||
emitter.emit('message', 'third');
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that the `waitFor` method rejects if the predicate function throws
|
||||
*/
|
||||
const testEventEmitterWaitForRejectsOnPredicateError = async (): Promise<void> => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const listener = vi.fn();
|
||||
|
||||
const waitPromise = emitter.waitFor('message', () => {
|
||||
throw new Error('predicate error');
|
||||
});
|
||||
|
||||
emitter.emit('message', 'hello');
|
||||
|
||||
await expect(waitPromise).rejects.toThrow('predicate error');
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() removes its listener after the predicate function throws an error.
|
||||
*/
|
||||
const testEventEmitterWaitForRemovesListenerAfterPredicateError = async (): Promise<void> => {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
|
||||
// Create a predicate function that throws an error.
|
||||
const predicate = vi.fn().mockImplementation(() => {
|
||||
throw new Error('predicate error');
|
||||
});
|
||||
|
||||
// Wait for the predicate function to throw an error.
|
||||
const waitPromise = emitter.waitFor('message', predicate);
|
||||
|
||||
// Emit an event, expecting the predicate function to throw an error.
|
||||
emitter.emit('message', 'hello');
|
||||
await expect(waitPromise).rejects.toThrow('predicate error');
|
||||
|
||||
// Expect the predicate function to have been called once.
|
||||
expect(predicate).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A later emit must not invoke the failed waitFor predicate again.
|
||||
emitter.emit('message', 'again');
|
||||
|
||||
// Expect the predicate function to still have been called once.
|
||||
expect(predicate).toHaveBeenCalledTimes(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests that waitFor() clears its timeout when the predicate function throws an error.
|
||||
*/
|
||||
const testEventEmitterWaitForClearsTimeoutAfterPredicateError = async (): Promise<void> => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
const emitter = new EventEmitter<TestEvents>();
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
|
||||
|
||||
const waitPromise = emitter.waitFor(
|
||||
'message',
|
||||
(): boolean => {
|
||||
throw new Error('predicate error');
|
||||
},
|
||||
100,
|
||||
);
|
||||
|
||||
emitter.emit('message', 'hello');
|
||||
await expect(waitPromise).rejects.toThrow('predicate error');
|
||||
|
||||
// The timeout scheduled for waitFor must be cleared on predicate failure.
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
|
||||
// Advancing past the original timeout must not produce a second rejection path.
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
}
|
||||
};
|
||||
|
||||
const runTests = async (): Promise<void> => {
|
||||
test('EventEmitter: calls listeners when an event is emitted', testEventEmitterCallsListeners);
|
||||
test('EventEmitter: calls multiple listeners for the same event', testEventEmitterCallsMultipleListeners);
|
||||
test('EventEmitter: only calls listeners for the emitted event type', testEventEmitterCallsOnlyMatchingListeners);
|
||||
test('EventEmitter: returns false when emitting with no listeners', testEventEmitterEmitReturnsFalseWithNoListeners);
|
||||
test('EventEmitter: returns true when emitting with listeners', testEventEmitterEmitReturnsTrueWithListeners);
|
||||
test('EventEmitter: continues after a listener throws an error', testEventEmitterEmitContinuesAfterListenerThrows);
|
||||
test('EventEmitter: supports Object.freeze() frozen payloads', testEventEmitterSupportsObjectFreezeFrozenPayloads);
|
||||
test('EventEmitter: supports deepFreeze() frozen payloads', testEventEmitterSupportsDeepFreezeFrozenPayloads);
|
||||
test('EventEmitter: stops calling a listener after its off callback is invoked', testEventEmitterOffCallbackRemovesListener);
|
||||
test('EventEmitter: removes a listener when off is called with the same reference', testEventEmitterOffRemovesListenerByReference);
|
||||
test(
|
||||
'EventEmitter: removes all listeners for an event type when off is called with no listener',
|
||||
testEventEmitterOffRemovesAllListenersForEventType,
|
||||
);
|
||||
test('EventEmitter: ignores off when the listener reference is unknown', testEventEmitterOffIgnoresUnknownListener);
|
||||
test('EventEmitter: ignores off for an event type with no listeners', testEventEmitterOffIgnoresUnregisteredEventType);
|
||||
test('EventEmitter: calls a once listener only one time', testEventEmitterOnceListenerFiresOnce);
|
||||
test('EventEmitter: registers once when listeners already exist', testEventEmitterOnceWorksWithExistingListeners);
|
||||
test('EventEmitter: stops a once listener after its off callback is invoked', testEventEmitterOnceOffCallbackRemovesListener);
|
||||
test(
|
||||
'EventEmitter: debounced listeners do not receive the debounced event if the listener is removed',
|
||||
testEventEmitterOffCancelsPendingDebouncedCallback,
|
||||
);
|
||||
test('EventEmitter: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners);
|
||||
test(
|
||||
'EventEmitter: cancels a pending debounced callback when removeAllListeners is called',
|
||||
testEventEmitterRemoveAllListenersCancelsPendingDebouncedCallback,
|
||||
);
|
||||
test(
|
||||
'EventEmitter: timeouts are cleared after they run when removeAllListeners is called',
|
||||
testEventEmitterTimeoutsAreClearedAfterTheyRunWhenRemoveAllListenersIsCalled,
|
||||
);
|
||||
test('EventEmitter: timeouts are cleared when off is called', testEventEmitterTimeoutsAreClearedWhenOffIsCalled);
|
||||
test('EventEmitter: resolves waitFor when a matching event is emitted', testEventEmitterWaitForResolvesOnMatch);
|
||||
test('EventEmitter: ignores non-matching events while waiting with waitFor', testEventEmitterWaitForIgnoresNonMatchingEvents);
|
||||
test('EventEmitter: rejects waitFor when the timeout is reached', testEventEmitterWaitForRejectsOnTimeout);
|
||||
test('EventEmitter: clears the timeout when waitFor resolves before expiry', testEventEmitterWaitForClearsTimeoutOnResolve);
|
||||
test('EventEmitter: removes the waitFor listener after it resolves', testEventEmitterWaitForRemovesListenerAfterResolve);
|
||||
test('EventEmitter: does not clear a timeout on the first debounced emit', testEventEmitterDebouncedFirstEmitDoesNotClearTimeout);
|
||||
test('EventEmitter: debounces on listeners', testEventEmitterDebouncedOnListener);
|
||||
test('EventEmitter: resets the debounce timer on repeated emits', testEventEmitterDebouncedTimerResetsOnRepeatedEmits);
|
||||
test('EventEmitter: does not debounce when debounceMilliseconds is zero', testEventEmitterZeroDebounceDoesNotDebounce);
|
||||
test('EventEmitter: debounces once listeners and invokes them only once', testEventEmitterDebouncedOnceListener);
|
||||
test('EventEmitter: rejects waitFor when the predicate function throws', testEventEmitterWaitForRejectsOnPredicateError);
|
||||
test(
|
||||
'EventEmitter: removes the waitFor listener after it rejects due to predicate error',
|
||||
testEventEmitterWaitForRemovesListenerAfterPredicateError,
|
||||
);
|
||||
test('EventEmitter: clears the timeout when waitFor rejects due to predicate error', testEventEmitterWaitForClearsTimeoutAfterPredicateError);
|
||||
};
|
||||
|
||||
await runTests();
|
||||
Reference in New Issue
Block a user