type Listener = | ((event: Event | MessageEvent | CloseEvent) => void) | { handleEvent: (event: Event | MessageEvent | CloseEvent) => void }; type EventType = "open" | "close" | "message" | "error"; export class MockWebSocket { static CONNECTING = 0; static OPEN = 1; static CLOSING = 2; static CLOSED = 3; static instances: MockWebSocket[] = []; public readyState = MockWebSocket.CONNECTING; public bufferedAmount = 0; public binaryType: BinaryType = "blob"; public extensions = ""; public protocol = ""; public onopen: ((event: Event) => void) | null = null; public onclose: ((event: CloseEvent) => void) | null = null; public onmessage: ((event: MessageEvent) => void) | null = null; public onerror: ((event: Event) => void) | null = null; public sentMessages: Array = []; private readonly listeners: Record = { open: [], close: [], message: [], error: [], }; constructor( public readonly url: string, public readonly protocols?: string | string[] ) { MockWebSocket.instances.push(this); } static reset() { MockWebSocket.instances = []; } static latest(): MockWebSocket { const latest = MockWebSocket.instances[MockWebSocket.instances.length - 1]; if (!latest) { throw new Error("No MockWebSocket instances have been created."); } return latest; } addEventListener(type: EventType, listener: Listener) { this.listeners[type].push(listener); } removeEventListener(type: EventType, listener: Listener) { this.listeners[type] = this.listeners[type].filter( (currentListener) => currentListener !== listener ); } send(data: string | ArrayBuffer | Blob | ArrayBufferView) { if (this.readyState !== MockWebSocket.OPEN) { throw new Error("INVALID_STATE_ERR"); } this.sentMessages.push(data); } close(code = 1000, reason = "") { if (this.readyState === MockWebSocket.CLOSED) { return; } this.readyState = MockWebSocket.CLOSED; this.emitClose(code, reason, true); } open() { this.readyState = MockWebSocket.OPEN; this.dispatch("open", new Event("open")); } emitMessage(data: string) { this.dispatch("message", new MessageEvent("message", { data })); } emitError(message = "error") { const errorEvent = new Event("error"); Object.defineProperty(errorEvent, "message", { value: message, configurable: true, }); this.dispatch("error", errorEvent); } fail(code = 1006, reason = "connection lost") { this.readyState = MockWebSocket.CLOSED; this.emitClose(code, reason, false); } private emitClose(code: number, reason: string, wasClean: boolean) { this.dispatch( "close", new CloseEvent("close", { code, reason, wasClean, }) ); } private dispatch( type: EventType, event: Event | MessageEvent | CloseEvent ) { const handlerName = `on${type}` as const; const levelZeroHandler = this[handlerName]; if (typeof levelZeroHandler === "function") { levelZeroHandler(event as never); } this.listeners[type].forEach((listener) => { if (typeof listener === "function") { listener(event); return; } listener.handleEvent(event); }); } }