import type { Wire } from "../../shared/api.ts"; import type { ServerToClientMessage } from "../../shared/ws.ts"; import { act } from "@testing-library/react"; /** * Stands in for the browser WebSocket. Every socket the app opens is kept in * `FakeWebSocket.instances`; a test opens it, feeds it server messages and * reads back what the client sent. */ export class FakeWebSocket extends EventTarget { static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3; static instances: FakeWebSocket[] = []; static reset() { FakeWebSocket.instances = []; } /** The newest socket the app opened. */ static get last(): FakeWebSocket { const ws = FakeWebSocket.instances.at(-1); if (!ws) throw new Error("no WebSocket was opened"); return ws; } readyState = FakeWebSocket.CONNECTING; sent: string[] = []; onopen: ((e: Event) => void) | null = null; onmessage: ((e: MessageEvent) => void) | null = null; onclose: ((e: CloseEvent) => void) | null = null; onerror: ((e: Event) => void) | null = null; url: string; constructor(url: string) { super(); this.url = url; FakeWebSocket.instances.push(this); } send(data: string) { if (this.readyState !== FakeWebSocket.OPEN) { throw new Error("send on a socket that isn't open"); } this.sent.push(data); } close() { if (this.readyState === FakeWebSocket.CLOSED) return; this.readyState = FakeWebSocket.CLOSED; this.onclose?.(new Event("close") as CloseEvent); } /** What the client sent, parsed. */ get messages(): Array> { return this.sent.map((s) => JSON.parse(s)); } // ── Server side ─────────────────────────────────────────────────────────── open() { act(() => { this.readyState = FakeWebSocket.OPEN; this.onopen?.(new Event("open")); }); } receive(msg: Wire | Record) { act(() => { this.onmessage?.( new MessageEvent("message", { data: JSON.stringify(msg) }), ); }); } receiveRaw(data: string) { act(() => { this.onmessage?.(new MessageEvent("message", { data })); }); } /** The server drops the connection. */ drop() { act(() => this.close()); } /** Opens the socket and sends a welcome, as the server does on connect. */ welcome( overrides: Partial< Extract, { type: "welcome" }> > = {}, ) { this.open(); this.receive({ type: "welcome", users: [], myVotes: [], myCommentLikes: [], unreadNotificationCount: 0, unreadChatCount: 0, ...overrides, }); } }