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(); }); });