import { describe, test, expect, vi, beforeEach } from 'vitest'; import { EventEmitter } from '../../src/utils/event-emitter.js'; type TestEvents = { test: { data: string }; count: { value: number }; multiPayload: { first: string; second: number }; }; describe('EventEmitter', () => { let emitter: EventEmitter; beforeEach(() => { emitter = new EventEmitter(); }); test('should register and trigger an event listener', () => { const mockCallback = vi.fn(); emitter.on('test', mockCallback); emitter.emit('test', { data: 'hello' }); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith({ data: 'hello' }); }); test('should register multiple listeners for the same event', () => { const mockCallback1 = vi.fn(); const mockCallback2 = vi.fn(); emitter.on('test', mockCallback1); emitter.on('test', mockCallback2); emitter.emit('test', { data: 'hello' }); expect(mockCallback1).toHaveBeenCalledTimes(1); expect(mockCallback2).toHaveBeenCalledTimes(1); }); test('should remove a specific listener', () => { const mockCallback1 = vi.fn(); const mockCallback2 = vi.fn(); emitter.on('test', mockCallback1); emitter.on('test', mockCallback2); emitter.off('test', mockCallback1); emitter.emit('test', { data: 'hello' }); expect(mockCallback1).not.toHaveBeenCalled(); expect(mockCallback2).toHaveBeenCalledTimes(1); }); test('should remove all listeners when removeAllListeners is called', () => { const mockCallback1 = vi.fn(); const mockCallback2 = vi.fn(); emitter.on('test', mockCallback1); emitter.on('count', mockCallback2); emitter.removeAllListeners(); emitter.emit('test', { data: 'hello' }); emitter.emit('count', { value: 42 }); expect(mockCallback1).not.toHaveBeenCalled(); expect(mockCallback2).not.toHaveBeenCalled(); }); test('should call listener only once when using once', () => { const mockCallback = vi.fn(); emitter.once('test', mockCallback); emitter.emit('test', { data: 'first' }); emitter.emit('test', { data: 'second' }); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith({ data: 'first' }); }); test('should handle multiple event types correctly', () => { const testCallback = vi.fn(); const countCallback = vi.fn(); emitter.on('test', testCallback); emitter.on('count', countCallback); emitter.emit('test', { data: 'test data' }); emitter.emit('count', { value: 123 }); expect(testCallback).toHaveBeenCalledWith({ data: 'test data' }); expect(countCallback).toHaveBeenCalledWith({ value: 123 }); }); test('should debounce events correctly', async () => { vi.useFakeTimers(); const mockCallback = vi.fn(); emitter.on('test', mockCallback, 100); emitter.emit('test', { data: 'first' }); emitter.emit('test', { data: 'second' }); emitter.emit('test', { data: 'third' }); expect(mockCallback).not.toHaveBeenCalled(); await vi.runAllTimersAsync(); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith({ data: 'third' }); vi.useRealTimers(); }); test('should handle complex payloads', () => { const mockCallback = vi.fn(); emitter.on('multiPayload', mockCallback); const payload = { first: 'hello', second: 42 }; emitter.emit('multiPayload', payload); expect(mockCallback).toHaveBeenCalledWith(payload); }); test('should return true when event has listeners and false when it does not', () => { const mockCallback = vi.fn(); expect(emitter.emit('test', { data: 'hello' })).toBe(false); emitter.on('test', mockCallback); expect(emitter.emit('test', { data: 'hello' })).toBe(true); }); test('should handle debounced once events correctly', async () => { vi.useFakeTimers(); const mockCallback = vi.fn(); emitter.once('test', mockCallback, 100); emitter.emit('test', { data: 'first' }); emitter.emit('test', { data: 'second' }); emitter.emit('test', { data: 'third' }); expect(mockCallback).not.toHaveBeenCalled(); await vi.runAllTimersAsync(); expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith({ data: 'third' }); // Emit again after debounce emitter.emit('test', { data: 'fourth' }); await vi.runAllTimersAsync(); expect(mockCallback).toHaveBeenCalledTimes(1); vi.useRealTimers(); }); test('waitFor should resolve when predicate is satisfied', async () => { const promise = emitter.waitFor( 'test', (payload) => payload.data === 'success', ); emitter.emit('test', { data: 'not yet' }); emitter.emit('test', { data: 'success' }); const result = await promise; expect(result).toEqual({ data: 'success' }); }); test('waitFor should reject on timeout', async () => { vi.useFakeTimers(); const promise = emitter.waitFor( 'test', (payload) => payload.data === 'success', 1000, ); // Need to handle the rejection or wait for it const promiseResult = promise.catch((error) => { expect(error.message).toBe('Timeout waiting for event "test"'); }); emitter.emit('test', { data: 'not yet' }); await vi.runAllTimersAsync(); await promiseResult; vi.useRealTimers(); }); test('waitFor should clean up listeners after resolution', async () => { const promise = emitter.waitFor( 'test', (payload) => payload.data === 'success', ); emitter.emit('test', { data: 'success' }); await promise; // The internal listener should be removed expect(emitter.emit('test', { data: 'another' })).toBe(false); }); test('waitFor should handle multiple concurrent waits', async () => { const promise1 = emitter.waitFor( 'test', (payload) => payload.data === 'first', ); const promise2 = emitter.waitFor( 'test', (payload) => payload.data === 'second', ); emitter.emit('test', { data: 'second' }); emitter.emit('test', { data: 'first' }); const [result1, result2] = await Promise.all([promise1, promise2]); expect(result1).toEqual({ data: 'first' }); expect(result2).toEqual({ data: 'second' }); }); test('waitFor should clean up timeout on successful resolution', async () => { vi.useFakeTimers(); const promise = emitter.waitFor( 'test', (payload) => payload.data === 'success', 1000, ); // Emit success before timeout emitter.emit('test', { data: 'success' }); // Fast-forward time - should not cause rejection await vi.runAllTimersAsync(); const result = await promise; expect(result).toEqual({ data: 'success' }); vi.useRealTimers(); }); });