import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("./api", () => ({ getActiveAccount: () => 7 })); // A WebSocket double the test drives: each instance is recorded, and its events are fired by hand. class FakeSocket { static all: FakeSocket[] = []; url: string; onopen: (() => void) | null = null; onmessage: ((ev: { data: string }) => void) | null = null; onclose: ((ev: { code: number; wasClean: boolean }) => void) | null = null; onerror: (() => void) | null = null; closed = false; constructor(url: string) { this.url = url; FakeSocket.all.push(this); } close() { this.closed = true; // the real close event arrives later, on its own task — fired by the test } } beforeEach(() => { vi.useFakeTimers(); vi.resetModules(); FakeSocket.all = []; vi.stubGlobal("WebSocket", FakeSocket); vi.stubGlobal("location", { protocol: "https:", host: "app.example" }); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); }); const frame = (sock: FakeSocket) => sock.onmessage?.({ data: JSON.stringify({ type: "message", message: { id: 1 } }) }); describe("messagesSocket", () => { it("a socket closed on unsubscribe cannot take over from the one a re-subscribe opened", async () => { const { onMessage } = await import("./messagesSocket"); const first = vi.fn(); onMessage(first)(); // subscribe, then leave (a remount / StrictMode's double effect) const got = vi.fn(); onMessage(got); const [old, current] = FakeSocket.all; expect(old!.closed).toBe(true); old!.onclose?.({ code: 1000, wasClean: true }); // the OLD socket's close lands late vi.advanceTimersByTime(60_000); expect(FakeSocket.all).toHaveLength(2); // no third socket frame(current!); expect(got).toHaveBeenCalledOnce(); }); it("reconnects when the current socket drops, and logs it without the account query", async () => { // Imported after resetModules, like the socket module: the ring it writes to is this instance. const { onMessage } = await import("./messagesSocket"); const { events } = await import("./log"); onMessage(() => {}); const [sock] = FakeSocket.all; expect(sock!.url).toBe("wss://app.example/api/messages/ws?account=7"); sock!.onopen?.(); sock!.onclose?.({ code: 1006, wasClean: false }); vi.advanceTimersByTime(1000); expect(FakeSocket.all).toHaveLength(2); const logged = events().map((e) => [e.event, e.level, e.attrs?.path]); expect(logged).toEqual([ ["ws.open", "INFO", "/api/messages/ws"], ["ws.close", "WARNING", "/api/messages/ws"], ]); expect(JSON.stringify(events())).not.toContain("account"); }); });