import { getRetryDelay } from '@/utils/getRetryDelay' import { MAX_RECONNECT_ATTEMPTS, RECONNECT_CONFIG, useWebSocketReconnect } from '@/websocket/connection/lifecycle/useWebSocketReconnect' import { act, renderHook } from '@testing-library/react' import { MutableRefObject } from 'react' // Mock the retry delay utility jest.mock('@/utils/getRetryDelay') const mockGetRetryDelay = getRetryDelay as jest.MockedFunction< typeof getRetryDelay > describe('useWebSocketReconnect', () => { let reconnectTimeoutRef: MutableRefObject let isAuthenticatedRef: MutableRefObject let reconnectAttemptsRef: MutableRefObject let setShouldConnect: jest.Mock beforeEach(() => { jest.useFakeTimers() reconnectTimeoutRef = { current: null } isAuthenticatedRef = { current: true } reconnectAttemptsRef = { current: 0 } setShouldConnect = jest.fn() mockGetRetryDelay.mockReturnValue(1000) // Default 1 second delay }) afterEach(() => { jest.runOnlyPendingTimers() jest.useRealTimers() jest.clearAllMocks() }) describe('scheduleReconnect', () => { it('should schedule a reconnection with correct delay', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, // isAuthenticated reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.scheduleReconnect() }) expect(mockGetRetryDelay).toHaveBeenCalledWith(0, RECONNECT_CONFIG) expect(reconnectTimeoutRef.current).not.toBeNull() expect(setShouldConnect).not.toHaveBeenCalled() act(() => { jest.advanceTimersByTime(1000) }) expect(setShouldConnect).toHaveBeenCalledWith(expect.any(Function)) expect(reconnectAttemptsRef.current).toBe(1) }) it('should not schedule reconnection if not authenticated', () => { isAuthenticatedRef = { current: false } const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, // Not authenticated reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.scheduleReconnect() }) expect(reconnectTimeoutRef.current).toBeNull() expect(mockGetRetryDelay).not.toHaveBeenCalled() }) it('should stop after MAX_RECONNECT_ATTEMPTS', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation() reconnectAttemptsRef.current = MAX_RECONNECT_ATTEMPTS const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.scheduleReconnect() }) expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining( `Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS})` ) ) expect(reconnectTimeoutRef.current).toBeNull() consoleErrorSpy.mockRestore() }) it('should increment attempt counter on each reconnection', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) expect(reconnectAttemptsRef.current).toBe(0) act(() => { result.current.scheduleReconnect() jest.advanceTimersByTime(1000) }) expect(reconnectAttemptsRef.current).toBe(1) act(() => { result.current.scheduleReconnect() jest.advanceTimersByTime(1000) }) expect(reconnectAttemptsRef.current).toBe(2) }) it('should toggle setShouldConnect correctly', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.scheduleReconnect() jest.advanceTimersByTime(1000) }) expect(setShouldConnect).toHaveBeenCalledTimes(1) // Test the toggle function const toggleFn = setShouldConnect.mock.calls[0][0] expect(toggleFn(false)).toBe(true) expect(toggleFn(true)).toBe(false) }) }) describe('clearReconnectTimeout', () => { it('should clear pending timeout', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.scheduleReconnect() }) expect(reconnectTimeoutRef.current).not.toBeNull() act(() => { result.current.clearReconnectTimeout() }) expect(reconnectTimeoutRef.current).toBeNull() // Timeout should not fire act(() => { jest.advanceTimersByTime(1000) }) expect(setShouldConnect).not.toHaveBeenCalled() }) it('should handle multiple clears gracefully', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) act(() => { result.current.clearReconnectTimeout() result.current.clearReconnectTimeout() result.current.clearReconnectTimeout() }) expect(reconnectTimeoutRef.current).toBeNull() }) it('should clear timeout before scheduling new one', () => { const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) // Schedule first reconnection act(() => { result.current.scheduleReconnect() }) const firstTimeout = reconnectTimeoutRef.current expect(firstTimeout).not.toBeNull() // Schedule second reconnection (should clear first) act(() => { result.current.scheduleReconnect() }) const secondTimeout = reconnectTimeoutRef.current expect(secondTimeout).not.toBeNull() expect(secondTimeout).not.toBe(firstTimeout) // Only second timeout should fire act(() => { jest.advanceTimersByTime(1000) }) expect(setShouldConnect).toHaveBeenCalledTimes(1) }) }) it('should stop reconnecting after MAX_RECONNECT_ATTEMPTS (10)', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation() const { result } = renderHook(() => useWebSocketReconnect( reconnectTimeoutRef, isAuthenticatedRef, reconnectAttemptsRef, setShouldConnect ) ) // Simulate 10 reconnection attempts for (let i = 0; i < MAX_RECONNECT_ATTEMPTS; i++) { act(() => { result.current.scheduleReconnect() jest.advanceTimersByTime( mockGetRetryDelay.mock.results[i]?.value || 1000 ) }) } expect(reconnectAttemptsRef.current).toBe(MAX_RECONNECT_ATTEMPTS) expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS) // Try to schedule one more reconnection - should fail act(() => { result.current.scheduleReconnect() }) expect(consoleErrorSpy).toHaveBeenCalledWith( `Max WebSocket reconnection attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.` ) expect(reconnectTimeoutRef.current).toBeNull() expect(setShouldConnect).toHaveBeenCalledTimes(MAX_RECONNECT_ATTEMPTS) // Should not increment consoleErrorSpy.mockRestore() }) })