import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Emitter } from './emitter'; describe('Emitter', () => { let emitter: Emitter; beforeEach(() => { emitter = new Emitter(); }); describe('on()', () => { it('should subscribe to events', () => { const handler = vi.fn(); emitter.on('test', handler); emitter.emit('test', 'data'); expect(handler).toHaveBeenCalledWith('data'); expect(handler).toHaveBeenCalledTimes(1); }); it('should support multiple arguments', () => { const handler = vi.fn(); emitter.on('test', handler); emitter.emit('test', 'arg1', 'arg2', 'arg3'); expect(handler).toHaveBeenCalledWith('arg1', 'arg2', 'arg3'); }); it('should support multiple listeners on same event', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); const handler3 = vi.fn(); emitter.on('test', handler1); emitter.on('test', handler2); emitter.on('test', handler3); emitter.emit('test', 'data'); expect(handler1).toHaveBeenCalledWith('data'); expect(handler2).toHaveBeenCalledWith('data'); expect(handler3).toHaveBeenCalledWith('data'); }); it('should throw TypeError if handler is not a function', () => { expect(() => { emitter.on('test', 'not a function' as any); }).toThrow(TypeError); expect(() => { emitter.on('test', 'not a function' as any); }).toThrow('handler must be a function'); }); it('should return an unsubscribe function', () => { const handler = vi.fn(); const unsubscribe = emitter.on('test', handler); expect(typeof unsubscribe).toBe('function'); emitter.emit('test', 'data1'); expect(handler).toHaveBeenCalledTimes(1); unsubscribe(); emitter.emit('test', 'data2'); expect(handler).toHaveBeenCalledTimes(1); // Still 1, not called again }); }); describe('off()', () => { it('should unsubscribe a specific handler', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); emitter.on('test', handler1); emitter.on('test', handler2); emitter.off('test', handler1); emitter.emit('test', 'data'); expect(handler1).not.toHaveBeenCalled(); expect(handler2).toHaveBeenCalledWith('data'); }); it('should only remove matching pattern and handler', () => { const handler = vi.fn(); const otherHandler = vi.fn(); emitter.on('test', handler); emitter.on('other', otherHandler); emitter.off('test', handler); emitter.emit('test', 'data'); emitter.emit('other', 'data'); expect(handler).not.toHaveBeenCalled(); expect(otherHandler).toHaveBeenCalledWith('data'); }); it('should handle unsubscribing non-existent handler gracefully', () => { const handler = vi.fn(); expect(() => { emitter.off('test', handler); }).not.toThrow(); }); }); describe('emit()', () => { it('should emit to exact match subscribers', () => { const handler = vi.fn(); emitter.on('widget.show', handler); emitter.emit('widget.show', 'data'); expect(handler).toHaveBeenCalledWith('data'); }); it('should not emit to non-matching subscribers', () => { const handler = vi.fn(); emitter.on('widget.show', handler); emitter.emit('widget.hide', 'data'); expect(handler).not.toHaveBeenCalled(); }); it('should catch and log errors in handlers', () => { const errorHandler = vi.fn(() => { throw new Error('Handler error'); }); const goodHandler = vi.fn(); const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); emitter.on('test', errorHandler); emitter.on('test', goodHandler); emitter.emit('test', 'data'); expect(errorHandler).toHaveBeenCalled(); expect(goodHandler).toHaveBeenCalled(); expect(consoleError).toHaveBeenCalledWith( 'Error in event handler for "test":', expect.any(Error) ); consoleError.mockRestore(); }); }); describe('wildcard patterns', () => { describe('suffix wildcard (event.*)', () => { it('should match suffix wildcard patterns', () => { const handler = vi.fn(); emitter.on('widget.*', handler); emitter.emit('widget.show', 'data1'); emitter.emit('widget.hide', 'data2'); emitter.emit('widget.resize', 'data3'); expect(handler).toHaveBeenCalledTimes(3); expect(handler).toHaveBeenCalledWith('data1'); expect(handler).toHaveBeenCalledWith('data2'); expect(handler).toHaveBeenCalledWith('data3'); }); it('should not match non-matching events', () => { const handler = vi.fn(); emitter.on('widget.*', handler); emitter.emit('modal.show', 'data'); emitter.emit('widgets.show', 'data'); // Different namespace emitter.emit('widget', 'data'); // No suffix expect(handler).not.toHaveBeenCalled(); }); }); describe('prefix wildcard (*.event)', () => { it('should match prefix wildcard patterns', () => { const handler = vi.fn(); emitter.on('*.show', handler); emitter.emit('widget.show', 'data1'); emitter.emit('modal.show', 'data2'); emitter.emit('tooltip.show', 'data3'); expect(handler).toHaveBeenCalledTimes(3); expect(handler).toHaveBeenCalledWith('data1'); expect(handler).toHaveBeenCalledWith('data2'); expect(handler).toHaveBeenCalledWith('data3'); }); it('should not match non-matching events', () => { const handler = vi.fn(); emitter.on('*.show', handler); emitter.emit('widget.hide', 'data'); emitter.emit('show', 'data'); // No prefix emitter.emit('widget.show.modal', 'data'); // Extra suffix expect(handler).not.toHaveBeenCalled(); }); }); describe('match all (*)', () => { it('should match all events', () => { const handler = vi.fn(); emitter.on('*', handler); emitter.emit('widget.show', 'data1'); emitter.emit('modal.hide', 'data2'); emitter.emit('user.login', 'data3'); emitter.emit('anything', 'data4'); expect(handler).toHaveBeenCalledTimes(4); }); }); describe('nested wildcards', () => { it('should support multiple wildcard levels', () => { const handler = vi.fn(); emitter.on('widget.*.show', handler); emitter.emit('widget.modal.show', 'data1'); emitter.emit('widget.tooltip.show', 'data2'); expect(handler).toHaveBeenCalledTimes(2); }); it('should match deeply nested events', () => { const handler = vi.fn(); emitter.on('app.*', handler); emitter.emit('app.user.login', 'data1'); emitter.emit('app.widget.modal.show', 'data2'); emitter.emit('app.a.b.c.d.e', 'data3'); expect(handler).toHaveBeenCalledTimes(3); }); }); describe('mixed exact and wildcard', () => { it('should support both exact and wildcard subscribers', () => { const exactHandler = vi.fn(); const wildcardHandler = vi.fn(); emitter.on('widget.show', exactHandler); emitter.on('widget.*', wildcardHandler); emitter.emit('widget.show', 'data'); expect(exactHandler).toHaveBeenCalledWith('data'); expect(wildcardHandler).toHaveBeenCalledWith('data'); }); it('should not cross-contaminate patterns', () => { const widgetHandler = vi.fn(); const modalHandler = vi.fn(); emitter.on('widget.*', widgetHandler); emitter.on('modal.*', modalHandler); emitter.emit('widget.show', 'data1'); emitter.emit('modal.hide', 'data2'); expect(widgetHandler).toHaveBeenCalledTimes(1); expect(widgetHandler).toHaveBeenCalledWith('data1'); expect(modalHandler).toHaveBeenCalledTimes(1); expect(modalHandler).toHaveBeenCalledWith('data2'); }); }); describe('regex special characters', () => { it('should handle events with dots correctly', () => { const handler = vi.fn(); emitter.on('a.b.c', handler); emitter.emit('a.b.c', 'data'); expect(handler).toHaveBeenCalledWith('data'); emitter.emit('axbxc', 'data'); expect(handler).toHaveBeenCalledTimes(1); // Should not match }); it('should escape regex special characters', () => { const handler = vi.fn(); // Test that parentheses, brackets, etc. are treated literally emitter.on('test(1)', handler); emitter.emit('test(1)', 'data'); expect(handler).toHaveBeenCalledWith('data'); }); }); }); describe('removeAllListeners()', () => { it('should remove all listeners', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); const handler3 = vi.fn(); emitter.on('event1', handler1); emitter.on('event2', handler2); emitter.on('*', handler3); emitter.removeAllListeners(); emitter.emit('event1', 'data'); emitter.emit('event2', 'data'); emitter.emit('anything', 'data'); expect(handler1).not.toHaveBeenCalled(); expect(handler2).not.toHaveBeenCalled(); expect(handler3).not.toHaveBeenCalled(); }); it('should allow adding new listeners after removal', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); emitter.on('test', handler1); emitter.removeAllListeners(); emitter.on('test', handler2); emitter.emit('test', 'data'); expect(handler1).not.toHaveBeenCalled(); expect(handler2).toHaveBeenCalledWith('data'); }); }); describe('memory management', () => { it('should not leak memory after unsubscribe', () => { const handler = vi.fn(); const unsubscribe = emitter.on('test', handler); unsubscribe(); emitter.emit('test', 'data'); expect(handler).not.toHaveBeenCalled(); // Handler should be removed from internal array expect((emitter as any).subscriptions.length).toBe(0); }); it('should handle multiple subscribe/unsubscribe cycles', () => { const handler = vi.fn(); for (let i = 0; i < 10; i++) { const unsub = emitter.on('test', handler); unsub(); } emitter.emit('test', 'data'); expect(handler).not.toHaveBeenCalled(); expect((emitter as any).subscriptions.length).toBe(0); }); it('should properly clean up with many listeners', () => { const handlers = Array.from({ length: 100 }, () => vi.fn()); const unsubscribers = handlers.map((handler) => emitter.on('test', handler)); // Unsubscribe every other one unsubscribers.forEach((unsub, i) => { if (i % 2 === 0) unsub(); }); emitter.emit('test', 'data'); handlers.forEach((handler, i) => { if (i % 2 === 0) { expect(handler).not.toHaveBeenCalled(); } else { expect(handler).toHaveBeenCalledWith('data'); } }); }); }); describe('edge cases', () => { it('should handle empty event names', () => { const handler = vi.fn(); emitter.on('', handler); emitter.emit('', 'data'); expect(handler).toHaveBeenCalledWith('data'); }); it('should handle events with no subscribers', () => { expect(() => { emitter.emit('nonexistent', 'data'); }).not.toThrow(); }); it('should handle emitting with no arguments', () => { const handler = vi.fn(); emitter.on('test', handler); emitter.emit('test'); expect(handler).toHaveBeenCalledWith(); expect(handler).toHaveBeenCalledTimes(1); }); it('should preserve handler context', () => { const context = { value: 42 }; const handler = vi.fn(function (this: any) { return this.value; }); emitter.on('test', handler.bind(context)); emitter.emit('test'); expect(handler).toHaveBeenCalled(); }); }); });