import { afterEach, describe, expect, it } from "bun:test"; import { ReconnectingWebSocket, ReconnectingWebSocketState, UserActivityDetector, } from "../ReconnectingWebsocket"; const OriginalWebSocket = globalThis.WebSocket; const originalVisibilityState = Object.getOwnPropertyDescriptor( document, "visibilityState" ); class MockWebSocket { static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3; static instances: MockWebSocket[] = []; readyState = MockWebSocket.CONNECTING; onopen: ((event: Event) => void) | null = null; onmessage: ((event: MessageEvent) => void) | null = null; onclose: ((event: CloseEvent) => void) | null = null; onerror: ((event: Event) => void) | null = null; constructor() { MockWebSocket.instances.push(this); } close() { if (this.readyState === MockWebSocket.CLOSED) return; this.readyState = MockWebSocket.CLOSED; this.onclose?.(new CloseEvent("close")); } send() {} } function setVisibilityState(state: DocumentVisibilityState) { Object.defineProperty(document, "visibilityState", { configurable: true, value: state, }); } const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); afterEach(() => { globalThis.WebSocket = OriginalWebSocket; MockWebSocket.instances = []; if (originalVisibilityState) { Object.defineProperty(document, "visibilityState", originalVisibilityState); } else { Reflect.deleteProperty(document, "visibilityState"); } }); describe("ReconnectingWebSocket browser activity recovery", () => { it("restarts connection attempts when an exhausted tab becomes visible", async () => { globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; setVisibilityState("hidden"); const states: ReconnectingWebSocketState[] = []; const socket = new ReconnectingWebSocket({ activityDetector: new UserActivityDetector(), reconnectInterval: 0, maxReconnectAttempts: 1, timeoutInterval: 10_000, }); socket.addListener((state) => states.push(state)); socket.connect("wss://example.com"); MockWebSocket.instances[0].close(); await tick(); MockWebSocket.instances[1].close(); expect(socket.state).toBe("MAX_ATTEMPTS_EXCEEDED"); expect(MockWebSocket.instances).toHaveLength(2); document.dispatchEvent(new Event("visibilitychange")); await tick(); expect(MockWebSocket.instances).toHaveLength(2); setVisibilityState("visible"); document.dispatchEvent(new Event("visibilitychange")); await tick(); expect(MockWebSocket.instances).toHaveLength(3); expect(states.slice(-2)).toEqual(["RECONNECTING", "CONNECTING"]); socket.shutdown(); }); });