Files
multica/apps/mobile/data/realtime/ws-client.test.ts
2026-08-03 10:19:45 +08:00

94 lines
2.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { WSClient } from "./ws-client";
class MockWebSocket {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSING = 2;
static readonly CLOSED = 3;
static instances: MockWebSocket[] = [];
readyState = MockWebSocket.CONNECTING;
onopen: (() => void) | null = null;
onmessage: ((event: { data: string }) => void) | null = null;
onerror: (() => void) | null = null;
onclose: (() => void) | null = null;
readonly sent: string[] = [];
constructor(readonly url: string) {
MockWebSocket.instances.push(this);
}
open() {
this.readyState = MockWebSocket.OPEN;
this.onopen?.();
}
receive(frame: unknown) {
this.onmessage?.({ data: JSON.stringify(frame) });
}
send(frame: string) {
this.sent.push(frame);
}
close() {
this.readyState = MockWebSocket.CLOSED;
this.onclose?.();
}
}
function connectAuthenticatedClient() {
const client = new WSClient({
url: "wss://example.test/ws",
token: "token",
workspaceSlug: "workspace",
});
client.connect();
const socket = MockWebSocket.instances[0];
socket.open();
socket.receive({ type: "auth_ack" });
return { client, socket };
}
describe("WSClient application heartbeat", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.stubGlobal("WebSocket", MockWebSocket);
MockWebSocket.instances = [];
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("reconnects a stale OPEN socket through the jittered backoff path", () => {
vi.spyOn(Math, "random").mockReturnValue(0);
const { client, socket } = connectAuthenticatedClient();
expect(socket.sent.map((frame) => JSON.parse(frame))).toEqual([
{ type: "auth", payload: { token: "token" } },
{ type: "ping" },
]);
// No pong arrives even though the JS-visible readyState remains OPEN.
vi.advanceTimersByTime(10_000);
vi.advanceTimersByTime(1);
expect(MockWebSocket.instances).toHaveLength(2);
client.disconnect();
});
it("keeps a healthy socket connected when its pong arrives", () => {
const { client, socket } = connectAuthenticatedClient();
socket.receive({ type: "pong" });
vi.advanceTimersByTime(10_000);
expect(MockWebSocket.instances).toHaveLength(1);
client.disconnect();
});
});