import { ConnectionManager } from '../../managers/ConnectionManager'; import { ConnectionState } from '../../types'; // Mock WebSocket class MockWebSocket { public onopen: ((event: Event) => void) | null = null; public onclose: ((event: CloseEvent) => void) | null = null; public onerror: ((event: Event) => void) | null = null; public onmessage: ((event: MessageEvent) => void) | null = null; public readyState: number = WebSocket.CONNECTING; public url: string; constructor(url: string) { this.url = url; // Simulate async connection setTimeout(() => { this.readyState = WebSocket.OPEN; if (this.onopen) { this.onopen(new Event('open')); } }, 0); } send(data: string | ArrayBuffer): void { if (this.readyState !== WebSocket.OPEN) { throw new Error('WebSocket is not connected'); } } close(code?: number, reason?: string): void { this.readyState = WebSocket.CLOSED; if (this.onclose) { this.onclose(new CloseEvent('close', { code: code || 1000, reason: reason || '' })); } } simulateError(): void { if (this.onerror) { this.onerror(new Event('error')); } } simulateMessage(data: string): void { if (this.onmessage) { this.onmessage(new MessageEvent('message', { data })); } } } // Replace global WebSocket with mock (global as any).WebSocket = MockWebSocket; describe('ConnectionManager', () => { let connectionManager: ConnectionManager; let mockEventHandlers: any; beforeEach(() => { mockEventHandlers = { connected: jest.fn(), disconnected: jest.fn(), reconnecting: jest.fn(), error: jest.fn(), message: jest.fn(), stateChange: jest.fn() }; connectionManager = new ConnectionManager({ streamUrl: 'wss://test.example.com', autoReconnect: true, reconnectDelay: 100, maxReconnectAttempts: 3, debug: false }); // Register mock event handlers Object.keys(mockEventHandlers).forEach(event => { connectionManager.on(event as any, mockEventHandlers[event]); }); jest.useFakeTimers(); }); afterEach(() => { connectionManager.disconnect(); jest.useRealTimers(); }); describe('connect', () => { it('should connect successfully', async () => { const connectPromise = connectionManager.connect(); jest.runAllTimers(); await connectPromise; expect(connectionManager.isConnected()).toBe(true); expect(connectionManager.getState()).toBe(ConnectionState.CONNECTED); expect(mockEventHandlers.connected).toHaveBeenCalledTimes(1); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith(ConnectionState.CONNECTING); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith(ConnectionState.CONNECTED); }); it('should not connect if already connecting', async () => { const promise1 = connectionManager.connect(); const promise2 = connectionManager.connect(); jest.runAllTimers(); await promise1; await promise2; expect(mockEventHandlers.connected).toHaveBeenCalledTimes(1); }); it('should handle connection error', async () => { // Mock WebSocket that fails const OriginalWebSocket = (global as any).WebSocket; (global as any).WebSocket = class { constructor() { setTimeout(() => { if (this.onerror) { this.onerror(new Event('error')); } }, 0); } onerror: any = null; }; const connectPromise = connectionManager.connect(); jest.runAllTimers(); await expect(connectPromise).rejects.toThrow('WebSocket connection failed'); // Restore original WebSocket (global as any).WebSocket = OriginalWebSocket; }); }); describe('disconnect', () => { it('should disconnect cleanly', async () => { await connectionManager.connect(); jest.runAllTimers(); connectionManager.disconnect(); expect(connectionManager.getState()).toBe(ConnectionState.DISCONNECTED); expect(connectionManager.isConnected()).toBe(false); }); }); describe('send', () => { it('should send message when connected', async () => { await connectionManager.connect(); jest.runAllTimers(); expect(() => { connectionManager.send('test message'); }).not.toThrow(); }); it('should throw error when not connected', () => { expect(() => { connectionManager.send('test message'); }).toThrow('WebSocket is not connected'); }); }); describe('reconnection', () => { it('should attempt reconnection on unexpected disconnect', async () => { await connectionManager.connect(); jest.runAllTimers(); // Simulate unexpected disconnect const ws = connectionManager['ws'] as any; ws.close(1006, 'Connection lost'); expect(mockEventHandlers.disconnected).toHaveBeenCalledWith(1006, 'Connection lost'); expect(mockEventHandlers.reconnecting).toHaveBeenCalledWith(1); }); it('should stop reconnecting after max attempts', async () => { await connectionManager.connect(); jest.runAllTimers(); // Mock WebSocket to always fail after initial connection (global as any).WebSocket = class { constructor() { setTimeout(() => { if (this.onerror) { this.onerror(new Event('error')); } }, 0); } onerror: any = null; }; // Simulate unexpected disconnect const ws = connectionManager['ws'] as any; ws.close(1006, 'Connection lost'); // Run timers to trigger all reconnection attempts for (let i = 0; i < 5; i++) { jest.runAllTimers(); } expect(mockEventHandlers.reconnecting).toHaveBeenCalledTimes(3); // maxReconnectAttempts expect(mockEventHandlers.error).toHaveBeenCalledWith( expect.objectContaining({ message: 'Max reconnection attempts reached' }) ); expect(connectionManager.getState()).toBe(ConnectionState.FAILED); }); it('should not reconnect on normal closure', async () => { await connectionManager.connect(); jest.runAllTimers(); connectionManager.disconnect(); // Normal closure (code 1000) jest.runAllTimers(); expect(mockEventHandlers.reconnecting).not.toHaveBeenCalled(); }); }); describe('message handling', () => { it('should emit message events', async () => { await connectionManager.connect(); jest.runAllTimers(); const ws = connectionManager['ws'] as any; ws.simulateMessage('test message'); expect(mockEventHandlers.message).toHaveBeenCalledWith('test message'); }); }); describe('state management', () => { it('should emit state change events', async () => { expect(connectionManager.getState()).toBe(ConnectionState.DISCONNECTED); const connectPromise = connectionManager.connect(); expect(mockEventHandlers.stateChange).toHaveBeenCalledWith(ConnectionState.CONNECTING); jest.runAllTimers(); await connectPromise; expect(mockEventHandlers.stateChange).toHaveBeenCalledWith(ConnectionState.CONNECTED); }); it('should not emit duplicate state changes', async () => { await connectionManager.connect(); jest.runAllTimers(); mockEventHandlers.stateChange.mockClear(); // Try to connect again - should not change state await connectionManager.connect(); expect(mockEventHandlers.stateChange).not.toHaveBeenCalled(); }); }); });