83fae6f1c2
* [#448] added reconnection logic to websocket * [#448] added test Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
284 lines
7.8 KiB
TypeScript
284 lines
7.8 KiB
TypeScript
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<NodeJS.Timeout | null>;
|
|
let isAuthenticatedRef: MutableRefObject<boolean>;
|
|
let reconnectAttemptsRef: MutableRefObject<number>;
|
|
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();
|
|
});
|
|
});
|