import { expect, test, vi } from 'vitest'; import { EventEmitter } from '../../src/utils/event-emitter.js'; /** Simple event map used across these tests. */ type TestEvents = { message: string; count: number; }; /** * Tests that EventEmitter invokes listeners when an event is emitted. */ const testEventEmitterCallsListeners = (): void => { const emitter = new EventEmitter(); 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(); 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(); 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(); 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(); 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 the off callback returned by on() removes the listener. */ const testEventEmitterOffCallbackRemovesListener = (): void => { const emitter = new EventEmitter(); 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(); 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() does nothing when given an unknown listener reference. */ const testEventEmitterOffIgnoresUnknownListener = (): void => { const emitter = new EventEmitter(); 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(); 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(); 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(); 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(); 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 removeAllListeners() clears every registered listener. */ const testEventEmitterRemoveAllListeners = (): void => { const emitter = new EventEmitter(); 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 waitFor() resolves when a matching event is emitted. */ const testEventEmitterWaitForResolvesOnMatch = async (): Promise => { const emitter = new EventEmitter(); // 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 => { const emitter = new EventEmitter(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); // 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 => { const emitter = new EventEmitter(); 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(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); 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 => { vi.useFakeTimers(); try { const emitter = new EventEmitter(); 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(); } }; const runTests = async (): Promise => { 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: 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: 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: removes all listeners when removeAllListeners is called', testEventEmitterRemoveAllListeners); 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); }; await runTests();