import assert from "node:assert/strict"; import test from "node:test"; import { apiClient } from "@/lib/api"; test("multiple realtime subscribers share one WebSocket connection", async () => { const originalWindow = globalThis.window; const originalLocalStorage = globalThis.localStorage; const originalWebSocket = globalThis.WebSocket; const originalRequest = apiClient.request; class FakeWebSocket { static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3; static instances = []; constructor(url) { this.url = url; this.readyState = FakeWebSocket.CONNECTING; this.sent = []; FakeWebSocket.instances.push(this); } open() { this.readyState = FakeWebSocket.OPEN; this.onopen?.(); } send(payload) { this.sent.push(JSON.parse(payload)); } receive(payload) { this.onmessage?.({ data: JSON.stringify(payload) }); } close() { this.readyState = FakeWebSocket.CLOSED; this.onclose?.({ code: 1000, reason: "", wasClean: true }); } } globalThis.window = { addEventListener() {} }; globalThis.localStorage = { getItem(key) { return key === "access_token" ? "test-token" : null; }, }; globalThis.WebSocket = FakeWebSocket; apiClient.request = async (endpoint) => { assert.equal(endpoint, "/api/auth/ws-ticket/"); return { ticket: "single-use-ticket", expires_in: 30 }; }; try { const { default: manager } = await import("./websocketManager.ts"); const unsubscribeMessages = manager.subscribe(() => {}); const unsubscribeStatus = manager.subscribeStatus(() => {}); await new Promise((resolve) => setTimeout(resolve, 0)); assert.equal(FakeWebSocket.instances.length, 1); assert.match(FakeWebSocket.instances[0].url, /ticket=single-use-ticket/); assert.doesNotMatch(FakeWebSocket.instances[0].url, /token=/); const socket = FakeWebSocket.instances[0]; socket.open(); manager.setActiveChat(114); assert.equal(FakeWebSocket.instances.length, 1); assert.deepEqual(socket.sent.at(-1), { action: "open_chat", chat_id: 114, load_history: false, }); socket.receive({ type: "ping", timestamp: "2026-08-17T12:00:00Z" }); assert.deepEqual(socket.sent.at(-1), { action: "pong", timestamp: "2026-08-17T12:00:00Z", }); unsubscribeMessages(); unsubscribeStatus(); } finally { globalThis.window = originalWindow; globalThis.localStorage = originalLocalStorage; globalThis.WebSocket = originalWebSocket; apiClient.request = originalRequest; } }); test("connection recovers immediately when browser comes back online", async () => { const originalWindow = globalThis.window; const originalDocument = globalThis.document; const originalLocalStorage = globalThis.localStorage; const originalWebSocket = globalThis.WebSocket; const originalRequest = apiClient.request; const originalSetTimeout = globalThis.setTimeout; const originalClearTimeout = globalThis.clearTimeout; const originalSetInterval = globalThis.setInterval; const originalClearInterval = globalThis.clearInterval; const listeners = new Map(); const scheduledTimeouts = []; const statuses = []; class FakeWebSocket { static OPEN = 1; static instances = []; constructor(url) { this.url = url; this.readyState = 0; FakeWebSocket.instances.push(this); } open() { this.readyState = FakeWebSocket.OPEN; this.onopen?.(); } send() {} fail() { this.readyState = 3; this.onclose?.({ code: 1006, reason: "network", wasClean: false }); } close() { this.readyState = 3; } } globalThis.window = { addEventListener(type, handler) { listeners.set(type, handler); }, }; globalThis.document = { visibilityState: "visible", addEventListener(type, handler) { listeners.set(type, handler); }, }; globalThis.localStorage = { getItem(key) { return key === "access_token" ? "access" : null; }, }; globalThis.WebSocket = FakeWebSocket; globalThis.setTimeout = (handler, delay) => { const timer = { handler, delay, cleared: false }; scheduledTimeouts.push(timer); return timer; }; globalThis.clearTimeout = (timer) => { timer.cleared = true; }; globalThis.setInterval = () => ({ interval: true }); globalThis.clearInterval = () => {}; apiClient.request = async () => ({ ticket: "recovery-ticket", expires_in: 30, }); try { const { WebSocketManager } = await import("./websocketManager.ts"); const manager = new WebSocketManager(); const unsubscribe = manager.subscribe(() => {}); const unsubscribeStatus = manager.subscribeStatus((status) => statuses.push(status), ); await new Promise((resolve) => originalSetTimeout(resolve, 0)); assert.equal(FakeWebSocket.instances.length, 1); FakeWebSocket.instances[0].open(); FakeWebSocket.instances[0].fail(); assert.equal(statuses.at(-1).lastCloseCode, 1006); assert.equal(statuses.at(-1).lastCloseReason, "network"); assert.equal(scheduledTimeouts.length, 1); assert.ok(scheduledTimeouts[0].delay >= 1600); assert.ok(scheduledTimeouts[0].delay <= 2400); listeners.get("online")?.(); await new Promise((resolve) => originalSetTimeout(resolve, 0)); assert.equal(scheduledTimeouts[0].cleared, true); assert.equal(FakeWebSocket.instances.length, 2); unsubscribe(); unsubscribeStatus(); } finally { globalThis.window = originalWindow; globalThis.document = originalDocument; globalThis.localStorage = originalLocalStorage; globalThis.WebSocket = originalWebSocket; globalThis.setTimeout = originalSetTimeout; globalThis.clearTimeout = originalClearTimeout; globalThis.setInterval = originalSetInterval; globalThis.clearInterval = originalClearInterval; apiClient.request = originalRequest; } });