Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
4.1 kB · 134 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";import { connectLivestreamWebsocket } from "./connect";import { makeLivestreamStore } from "./store";
type Listener = (event?: any) => void;
class FakeWebSocket { static instances: FakeWebSocket[] = []; url: string; onopen: Listener | null = null; onmessage: Listener | null = null; onclose: Listener | null = null; onerror: Listener | null = null; closed = false;
constructor(url: string) { this.url = url; FakeWebSocket.instances.push(this); } close() { this.closed = true; }}
let realWebSocket: typeof WebSocket;
beforeEach(() => { realWebSocket = globalThis.WebSocket; FakeWebSocket.instances = []; (globalThis as any).WebSocket = FakeWebSocket; vi.useFakeTimers();});
afterEach(() => { (globalThis as any).WebSocket = realWebSocket; vi.useRealTimers();});
describe("connectLivestreamWebsocket", () => { it("opens a socket at the given url", () => { const store = makeLivestreamStore(); connectLivestreamWebsocket(store, "wss://example.com/socket"); expect(FakeWebSocket.instances).toHaveLength(1); expect(FakeWebSocket.instances[0].url).toBe("wss://example.com/socket"); });
it("feeds messages through the reducer and marks the socket connected", async () => { const store = makeLivestreamStore(); connectLivestreamWebsocket(store, "wss://example.com/socket"); const ws = FakeWebSocket.instances[0];
ws.onmessage!({ data: JSON.stringify({ $type: "place.stream.livestream#viewerCount", count: 7, }), });
// flush happens on the next macrotask (setTimeout 0) await vi.advanceTimersByTimeAsync(1); expect(store.getState().viewers).toBe(7); expect(store.getState().websocketConnected).toBe(true); });
it("does not flush a partial batch until the batch window elapses", async () => { const store = makeLivestreamStore(); connectLivestreamWebsocket(store, "wss://example.com/socket", { batchWindowMs: 50, }); const ws = FakeWebSocket.instances[0];
ws.onmessage!({ data: JSON.stringify({ $type: "place.stream.livestream#viewerCount", count: 1, }), }); expect(store.getState().viewers).toBeNull();
await vi.advanceTimersByTimeAsync(50); expect(store.getState().viewers).toBe(1); });
it("schedules a reconnect on close after the reconnect delay", async () => { const store = makeLivestreamStore(); connectLivestreamWebsocket(store, "wss://example.com/socket"); const ws = FakeWebSocket.instances[0];
ws.onclose!(); expect(store.getState().websocketConnected).toBe(false); expect(FakeWebSocket.instances).toHaveLength(1);
await vi.advanceTimersByTimeAsync(3000); expect(FakeWebSocket.instances).toHaveLength(2); });
it("does not reconnect once disconnected", async () => { const store = makeLivestreamStore(); const { disconnect } = connectLivestreamWebsocket( store, "wss://example.com/socket", ); const ws = FakeWebSocket.instances[0];
disconnect(); ws.onclose!(); await vi.advanceTimersByTimeAsync(3000); expect(FakeWebSocket.instances).toHaveLength(1); });
it("calls onDisconnect once per drop even when error triggers close", async () => { const onDisconnect = vi.fn(); const store = makeLivestreamStore(); connectLivestreamWebsocket(store, "wss://example.com/socket", { reconnectDelayMs: 1000, onDisconnect, }); const ws1 = FakeWebSocket.instances[0];
// onerror closes the socket, which fires onclose async; the reconnect // guard must prevent a second onDisconnect / second timer. ws1.onerror!(); ws1.onclose!(); expect(onDisconnect).toHaveBeenCalledTimes(1);
// reconnect opens a fresh socket await vi.advanceTimersByTimeAsync(1000); expect(FakeWebSocket.instances).toHaveLength(2);
// a second drop on the new socket fires onDisconnect again FakeWebSocket.instances[1].onclose!(); expect(onDisconnect).toHaveBeenCalledTimes(2); });});