From a87e93e3f12338d458439537cd0bfe509ced813d Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 4 Aug 2025 23:37:18 +0000 Subject: [PATCH 01/12] wip: commit progress on tests --- site/src/testHelpers/websockets.test.ts | 135 +++++++++++++++++++++++ site/src/testHelpers/websockets.ts | 138 ++++++++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 site/src/testHelpers/websockets.test.ts create mode 100644 site/src/testHelpers/websockets.ts diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts new file mode 100644 index 0000000000000..a9ca5751680fc --- /dev/null +++ b/site/src/testHelpers/websockets.test.ts @@ -0,0 +1,135 @@ +import { createMockWebSocket } from "./websockets"; + +describe(createMockWebSocket.name, () => { + it("Throws if URL does not have ws:// or wss:// protocols", () => { + const urls: readonly string[] = [ + "http://www.dog.ceo/roll-over", + "https://www.dog.ceo/roll-over" + ]; + for (const url of urls) { + expect(() => { + void createMockWebSocket(url); + }).toThrow("URL must start with ws:// or wss://"); + } + }) + + it("Sends events from publisher to socket", () => { + const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/shake"); + + const onOpen = jest.fn(); + const onError = jest.fn(); + const onMessage = jest.fn(); + const onClose = jest.fn(); + + socket.addEventListener("open", onOpen); + socket.addEventListener("error", onError); + socket.addEventListener("message", onMessage); + socket.addEventListener("close", onClose); + + const openEvent = new Event("open"); + const errorEvent = new Event("error"); + const messageEvent = new MessageEvent("message") + const closeEvent = new CloseEvent("close"); + + publisher.publishOpen(openEvent); + publisher.publishError(errorEvent); + publisher.publishMessage(messageEvent); + publisher.publishClose(closeEvent); + + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onOpen).toHaveBeenCalledWith(openEvent); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(errorEvent); + + expect(onMessage).toHaveBeenCalledTimes(1); + expect(onMessage).toHaveBeenCalledWith(messageEvent); + + expect(onClose).toHaveBeenCalledTimes(1); + expect(onClose).toHaveBeenCalledWith(closeEvent); + }); + + it("Sends JSON data to the socket for message events", () => { + const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/wag"); + const onMessage = jest.fn(); + + // Could type this as a special JSON type, but unknown is good enough, + // since any invalid values will throw in the test case + const jsonData: readonly unknown[] =[ + "blah", + 42, + true, + false, + null, + {}, + [], + [{ value: "blah" }, { value: "guh" }, { value: "huh" }], + { + name: "Hershel Layton", + age: 40, + profession: "Puzzle Solver", + sadBackstory: true, + greatVideoGames: true, + } + ]; + for (const jd of jsonData) { + socket.addEventListener("message", onMessage); + publisher.publishMessage(new MessageEvent("message", { + "data": JSON.stringify(jd) + })); + + expect(onMessage).toHaveBeenCalledTimes(1); + expect(onMessage).toHaveBeenCalledWith(new MessageEvent("message", { + data: JSON.stringify(jd) + })); + + socket.removeEventListener("message", onMessage); + onMessage.mockClear(); + } + }) + + it("Only registers each socket event handler once", () => { + const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/borf"); + + const onOpen = jest.fn(); + const onError = jest.fn(); + const onMessage = jest.fn(); + const onClose = jest.fn(); + + // Do it once + socket.addEventListener("open", onOpen); + socket.addEventListener("error", onError); + socket.addEventListener("message", onMessage); + socket.addEventListener("close", onClose); + + // Do it again with the exact same functions + socket.addEventListener("open", onOpen); + socket.addEventListener("error", onError); + socket.addEventListener("message", onMessage); + socket.addEventListener("close", onClose); + + publisher.publishOpen(new Event("open")); + publisher.publishError(new Event("error")); + publisher.publishMessage(new MessageEvent("message")); + publisher.publishClose(new CloseEvent("close")); + + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledTimes(1); + expect(onMessage).toHaveBeenCalledTimes(1); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it("Renders socket inert after being closed", () => { + const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/woof"); + expect(publisher.isConnectionOpen).toBe(true); + + const onMessage = jest.fn(); + socket.addEventListener("message", onMessage); + + socket.close(); + expect(publisher.isConnectionOpen).toBe(false); + + publisher.publishMessage(new MessageEvent("message")); + expect(onMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts new file mode 100644 index 0000000000000..418326f20a6fd --- /dev/null +++ b/site/src/testHelpers/websockets.ts @@ -0,0 +1,138 @@ +import type { WebSocketEventType } from "utils/OneWayWebSocket"; + +export type MockWebSocketPublisher = Readonly<{ + publishMessage: (event: MessageEvent) => void; + publishError: (event: Event) => void; + publishClose: (event: CloseEvent) => void; + publishOpen: (event: Event) => void; + readonly isConnectionOpen: boolean; +}>; + +export function createMockWebSocket( + url: string, + protocols?: string | string[], +): readonly [WebSocket, MockWebSocketPublisher] { + type EventMap = { + message: MessageEvent; + error: Event; + close: CloseEvent; + open: Event; + }; + type CallbackStore = { + [K in keyof EventMap]: ((event: EventMap[K]) => void)[]; + }; + + let activeProtocol: string; + if (Array.isArray(protocols)) { + activeProtocol = protocols[0] ?? ""; + } else if (typeof protocols === "string") { + activeProtocol = protocols; + } else { + activeProtocol = ""; + } + + let isOpen = true; + const store: CallbackStore = { + message: [], + error: [], + close: [], + open: [], + }; + + const mockSocket: WebSocket = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + + url, + protocol: activeProtocol, + readyState: 1, + binaryType: "blob", + bufferedAmount: 0, + extensions: "", + onclose: null, + onerror: null, + onmessage: null, + onopen: null, + send: jest.fn(), + dispatchEvent: jest.fn(), + + addEventListener: ( + eventType: E, + callback: (event: WebSocketEventMap[E]) => void, + ) => { + if (!isOpen) { + return; + } + + const subscribers = store[eventType]; + if (!subscribers.includes(callback)) { + subscribers.push(callback); + } + }, + + removeEventListener: ( + eventType: E, + callback: (event: WebSocketEventMap[E]) => void, + ) => { + if (!isOpen) { + return; + } + + const subscribers = store[eventType]; + if (subscribers.includes(callback)) { + const updated = store[eventType].filter((c) => c !== callback); + store[eventType] = updated as CallbackStore[E]; + } + }, + + close: () => { + isOpen = false; + }, + }; + + const publisher: MockWebSocketPublisher = { + get isConnectionOpen() { + return isOpen; + }, + + publishOpen: (event) => { + if (!isOpen) { + return; + } + for (const sub of store.open) { + sub(event); + } + }, + + publishError: (event) => { + if (!isOpen) { + return; + } + for (const sub of store.error) { + sub(event); + } + }, + + publishMessage: (event) => { + if (!isOpen) { + return; + } + for (const sub of store.message) { + sub(event); + } + }, + + publishClose: (event) => { + if (!isOpen) { + return; + } + for (const sub of store.close) { + sub(event); + } + }, + }; + + return [mockSocket, publisher] as const; +} From 1c48afcf1f98f8323d1f2a4df22ad387b917fcc5 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 00:24:34 +0000 Subject: [PATCH 02/12] chore: get another test working --- site/src/testHelpers/websockets.test.ts | 33 +++++++++++++++++++++++++ site/src/testHelpers/websockets.ts | 11 ++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts index a9ca5751680fc..d937091e4d600 100644 --- a/site/src/testHelpers/websockets.test.ts +++ b/site/src/testHelpers/websockets.test.ts @@ -119,6 +119,35 @@ describe(createMockWebSocket.name, () => { expect(onClose).toHaveBeenCalledTimes(1); }); + it("Lets a socket unsubscribe to event types", () => { + const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/zoomies"); + + const onOpen = jest.fn(); + const onError = jest.fn(); + const onMessage = jest.fn(); + const onClose = jest.fn(); + + socket.addEventListener("open", onOpen); + socket.addEventListener("error", onError); + socket.addEventListener("message", onMessage); + socket.addEventListener("close", onClose); + + socket.removeEventListener("open", onOpen); + socket.removeEventListener("error", onError); + socket.removeEventListener("message", onMessage); + socket.removeEventListener("close", onClose); + + publisher.publishOpen(new Event("open")); + publisher.publishError(new Event("error")); + publisher.publishMessage(new MessageEvent("message")); + publisher.publishClose(new CloseEvent("close")); + + expect(onOpen).not.toHaveBeenCalled(); + expect(onError).not.toHaveBeenCalled(); + expect(onMessage).not.toHaveBeenCalled(); + expect(onClose).not.toHaveBeenCalled(); + }); + it("Renders socket inert after being closed", () => { const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/woof"); expect(publisher.isConnectionOpen).toBe(true); @@ -132,4 +161,8 @@ describe(createMockWebSocket.name, () => { publisher.publishMessage(new MessageEvent("message")); expect(onMessage).not.toHaveBeenCalled(); }); + + it("Lets a socket send data to the mock server", () => { + expect.hasAssertions(); + }); }); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index 418326f20a6fd..d0288014a1c61 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -1,17 +1,18 @@ import type { WebSocketEventType } from "utils/OneWayWebSocket"; -export type MockWebSocketPublisher = Readonly<{ +export type MockWebSocketServer = Readonly<{ publishMessage: (event: MessageEvent) => void; publishError: (event: Event) => void; publishClose: (event: CloseEvent) => void; publishOpen: (event: Event) => void; + readonly isConnectionOpen: boolean; }>; export function createMockWebSocket( url: string, protocols?: string | string[], -): readonly [WebSocket, MockWebSocketPublisher] { +): readonly [WebSocket, MockWebSocketServer] { type EventMap = { message: MessageEvent; error: Event; @@ -22,6 +23,10 @@ export function createMockWebSocket( [K in keyof EventMap]: ((event: EventMap[K]) => void)[]; }; + if (!url.startsWith("ws://") && !url.startsWith("wss://")) { + throw new Error("URL must start with ws:// or wss://"); + } + let activeProtocol: string; if (Array.isArray(protocols)) { activeProtocol = protocols[0] ?? ""; @@ -92,7 +97,7 @@ export function createMockWebSocket( }, }; - const publisher: MockWebSocketPublisher = { + const publisher: MockWebSocketServer = { get isConnectionOpen() { return isOpen; }, From 5eff4dfe314e658465530ede9c64c4960924f213 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 00:50:06 +0000 Subject: [PATCH 03/12] chore: get all initial tests passing --- site/src/testHelpers/websockets.test.ts | 67 ++++++++++++++++--------- site/src/testHelpers/websockets.ts | 17 ++++++- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts index d937091e4d600..2b5cae97fee67 100644 --- a/site/src/testHelpers/websockets.test.ts +++ b/site/src/testHelpers/websockets.test.ts @@ -1,3 +1,4 @@ +import { subscribe } from "node:diagnostics_channel"; import { createMockWebSocket } from "./websockets"; describe(createMockWebSocket.name, () => { @@ -13,8 +14,8 @@ describe(createMockWebSocket.name, () => { } }) - it("Sends events from publisher to socket", () => { - const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/shake"); + it("Sends events from server to socket", () => { + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/shake"); const onOpen = jest.fn(); const onError = jest.fn(); @@ -31,10 +32,10 @@ describe(createMockWebSocket.name, () => { const messageEvent = new MessageEvent("message") const closeEvent = new CloseEvent("close"); - publisher.publishOpen(openEvent); - publisher.publishError(errorEvent); - publisher.publishMessage(messageEvent); - publisher.publishClose(closeEvent); + server.publishOpen(openEvent); + server.publishError(errorEvent); + server.publishMessage(messageEvent); + server.publishClose(closeEvent); expect(onOpen).toHaveBeenCalledTimes(1); expect(onOpen).toHaveBeenCalledWith(openEvent); @@ -50,7 +51,7 @@ describe(createMockWebSocket.name, () => { }); it("Sends JSON data to the socket for message events", () => { - const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/wag"); + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/wag"); const onMessage = jest.fn(); // Could type this as a special JSON type, but unknown is good enough, @@ -74,7 +75,7 @@ describe(createMockWebSocket.name, () => { ]; for (const jd of jsonData) { socket.addEventListener("message", onMessage); - publisher.publishMessage(new MessageEvent("message", { + server.publishMessage(new MessageEvent("message", { "data": JSON.stringify(jd) })); @@ -89,7 +90,7 @@ describe(createMockWebSocket.name, () => { }) it("Only registers each socket event handler once", () => { - const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/borf"); + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/borf"); const onOpen = jest.fn(); const onError = jest.fn(); @@ -108,10 +109,10 @@ describe(createMockWebSocket.name, () => { socket.addEventListener("message", onMessage); socket.addEventListener("close", onClose); - publisher.publishOpen(new Event("open")); - publisher.publishError(new Event("error")); - publisher.publishMessage(new MessageEvent("message")); - publisher.publishClose(new CloseEvent("close")); + server.publishOpen(new Event("open")); + server.publishError(new Event("error")); + server.publishMessage(new MessageEvent("message")); + server.publishClose(new CloseEvent("close")); expect(onOpen).toHaveBeenCalledTimes(1); expect(onError).toHaveBeenCalledTimes(1); @@ -120,7 +121,7 @@ describe(createMockWebSocket.name, () => { }); it("Lets a socket unsubscribe to event types", () => { - const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/zoomies"); + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/zoomies"); const onOpen = jest.fn(); const onError = jest.fn(); @@ -137,10 +138,10 @@ describe(createMockWebSocket.name, () => { socket.removeEventListener("message", onMessage); socket.removeEventListener("close", onClose); - publisher.publishOpen(new Event("open")); - publisher.publishError(new Event("error")); - publisher.publishMessage(new MessageEvent("message")); - publisher.publishClose(new CloseEvent("close")); + server.publishOpen(new Event("open")); + server.publishError(new Event("error")); + server.publishMessage(new MessageEvent("message")); + server.publishClose(new CloseEvent("close")); expect(onOpen).not.toHaveBeenCalled(); expect(onError).not.toHaveBeenCalled(); @@ -149,20 +150,38 @@ describe(createMockWebSocket.name, () => { }); it("Renders socket inert after being closed", () => { - const [socket, publisher] = createMockWebSocket("wss://www.dog.ceo/woof"); - expect(publisher.isConnectionOpen).toBe(true); + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/woof"); + expect(server.isConnectionOpen).toBe(true); const onMessage = jest.fn(); socket.addEventListener("message", onMessage); socket.close(); - expect(publisher.isConnectionOpen).toBe(false); + expect(server.isConnectionOpen).toBe(false); - publisher.publishMessage(new MessageEvent("message")); + server.publishMessage(new MessageEvent("message")); expect(onMessage).not.toHaveBeenCalled(); }); - it("Lets a socket send data to the mock server", () => { - expect.hasAssertions(); + it("Tracks arguments sent by the mock socket", () => { + const [socket, server] = createMockWebSocket("wss://www.dog.ceo/wan-wan"); + const data = JSON.stringify({ + famousDogs: [ + "snoopy", + "clifford", + "lassie", + "beethoven", + "courage the cowardly dog", + ], + }); + + socket.send(data); + expect(server.socketSendArguments).toHaveLength(1); + expect(server.socketSendArguments).toEqual([data]); + + socket.close(); + socket.send(data); + expect(server.socketSendArguments).toHaveLength(1); + expect(server.socketSendArguments).toEqual([data]); }); }); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index d0288014a1c61..adde2606a7b2a 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -1,5 +1,7 @@ import type { WebSocketEventType } from "utils/OneWayWebSocket"; +type SocketSendData = string | ArrayBufferLike | Blob | ArrayBufferView; + export type MockWebSocketServer = Readonly<{ publishMessage: (event: MessageEvent) => void; publishError: (event: Event) => void; @@ -7,6 +9,7 @@ export type MockWebSocketServer = Readonly<{ publishOpen: (event: Event) => void; readonly isConnectionOpen: boolean; + readonly socketSendArguments: readonly SocketSendData[]; }>; export function createMockWebSocket( @@ -44,6 +47,8 @@ export function createMockWebSocket( open: [], }; + let sendData: SocketSendData[] = []; + const mockSocket: WebSocket = { CONNECTING: 0, OPEN: 1, @@ -60,9 +65,15 @@ export function createMockWebSocket( onerror: null, onmessage: null, onopen: null, - send: jest.fn(), dispatchEvent: jest.fn(), + send: (data) => { + if (!isOpen) { + return; + } + sendData.push(data); + }, + addEventListener: ( eventType: E, callback: (event: WebSocketEventMap[E]) => void, @@ -102,6 +113,10 @@ export function createMockWebSocket( return isOpen; }, + get socketSendArguments() { + return [...sendData]; + }, + publishOpen: (event) => { if (!isOpen) { return; From c2d07fbe2ffcf4d9352440861dfd95831f4b10c7 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 01:20:39 +0000 Subject: [PATCH 04/12] refactor: centralize socket implementation --- site/src/utils/OneWayWebSocket.test.ts | 153 ++----------------------- 1 file changed, 8 insertions(+), 145 deletions(-) diff --git a/site/src/utils/OneWayWebSocket.test.ts b/site/src/utils/OneWayWebSocket.test.ts index c6b00b593111f..bdb8f9202ea6c 100644 --- a/site/src/utils/OneWayWebSocket.test.ts +++ b/site/src/utils/OneWayWebSocket.test.ts @@ -7,145 +7,8 @@ * WebSocket class doesn't have any bugs, and can safely be mocked out. */ -import { - type OneWayMessageEvent, - OneWayWebSocket, - type WebSocketEventType, -} from "./OneWayWebSocket"; - -type MockPublisher = Readonly<{ - publishMessage: (event: MessageEvent) => void; - publishError: (event: ErrorEvent) => void; - publishClose: (event: CloseEvent) => void; - publishOpen: (event: Event) => void; -}>; - -function createMockWebSocket( - url: string, - protocols?: string | string[], -): readonly [WebSocket, MockPublisher] { - type EventMap = { - message: MessageEvent; - error: ErrorEvent; - close: CloseEvent; - open: Event; - }; - type CallbackStore = { - [K in keyof EventMap]: ((event: EventMap[K]) => void)[]; - }; - - let activeProtocol: string; - if (Array.isArray(protocols)) { - activeProtocol = protocols[0] ?? ""; - } else if (typeof protocols === "string") { - activeProtocol = protocols; - } else { - activeProtocol = ""; - } - - let closed = false; - const store: CallbackStore = { - message: [], - error: [], - close: [], - open: [], - }; - - const mockSocket: WebSocket = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3, - - url, - protocol: activeProtocol, - readyState: 1, - binaryType: "blob", - bufferedAmount: 0, - extensions: "", - onclose: null, - onerror: null, - onmessage: null, - onopen: null, - send: jest.fn(), - dispatchEvent: jest.fn(), - - addEventListener: ( - eventType: E, - callback: WebSocketEventMap[E], - ) => { - if (closed) { - return; - } - - const subscribers = store[eventType]; - const cb = callback as unknown as CallbackStore[E][0]; - if (!subscribers.includes(cb)) { - subscribers.push(cb); - } - }, - - removeEventListener: ( - eventType: E, - callback: WebSocketEventMap[E], - ) => { - if (closed) { - return; - } - - const subscribers = store[eventType]; - const cb = callback as unknown as CallbackStore[E][0]; - if (subscribers.includes(cb)) { - const updated = store[eventType].filter((c) => c !== cb); - store[eventType] = updated as unknown as CallbackStore[E]; - } - }, - - close: () => { - closed = true; - }, - }; - - const publisher: MockPublisher = { - publishOpen: (event) => { - if (closed) { - return; - } - for (const sub of store.open) { - sub(event); - } - }, - - publishError: (event) => { - if (closed) { - return; - } - for (const sub of store.error) { - sub(event); - } - }, - - publishMessage: (event) => { - if (closed) { - return; - } - for (const sub of store.message) { - sub(event); - } - }, - - publishClose: (event) => { - if (closed) { - return; - } - for (const sub of store.close) { - sub(event); - } - }, - }; - - return [mockSocket, publisher] as const; -} +import { createMockWebSocket, MockWebSocketServer } from "testHelpers/websockets"; +import { type OneWayMessageEvent, OneWayWebSocket } from "./OneWayWebSocket"; describe(OneWayWebSocket.name, () => { const dummyRoute = "/api/v2/blah"; @@ -167,7 +30,7 @@ describe(OneWayWebSocket.name, () => { }); it("Lets a consumer add an event listener of each type", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { @@ -207,7 +70,7 @@ describe(OneWayWebSocket.name, () => { }); it("Lets a consumer remove an event listener of each type", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { @@ -252,7 +115,7 @@ describe(OneWayWebSocket.name, () => { }); it("Only calls each callback once if callback is added multiple times", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { @@ -294,7 +157,7 @@ describe(OneWayWebSocket.name, () => { }); it("Lets consumers register multiple callbacks for each event type", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { @@ -375,7 +238,7 @@ describe(OneWayWebSocket.name, () => { }); it("Gives consumers pre-parsed versions of message events", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { @@ -405,7 +268,7 @@ describe(OneWayWebSocket.name, () => { }); it("Exposes parsing error if message payload could not be parsed as JSON", () => { - let publisher!: MockPublisher; + let publisher!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { From 21c9eb8c6791472c468bc85801d660b027c8941e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 01:22:44 +0000 Subject: [PATCH 05/12] chore: format --- site/src/testHelpers/websockets.test.ts | 29 ++++++++++++++----------- site/src/testHelpers/websockets.ts | 2 +- site/src/utils/OneWayWebSocket.test.ts | 5 ++++- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts index 2b5cae97fee67..f0288f090bd0a 100644 --- a/site/src/testHelpers/websockets.test.ts +++ b/site/src/testHelpers/websockets.test.ts @@ -1,18 +1,17 @@ -import { subscribe } from "node:diagnostics_channel"; import { createMockWebSocket } from "./websockets"; describe(createMockWebSocket.name, () => { it("Throws if URL does not have ws:// or wss:// protocols", () => { const urls: readonly string[] = [ "http://www.dog.ceo/roll-over", - "https://www.dog.ceo/roll-over" + "https://www.dog.ceo/roll-over", ]; for (const url of urls) { expect(() => { void createMockWebSocket(url); }).toThrow("URL must start with ws:// or wss://"); } - }) + }); it("Sends events from server to socket", () => { const [socket, server] = createMockWebSocket("wss://www.dog.ceo/shake"); @@ -29,7 +28,7 @@ describe(createMockWebSocket.name, () => { const openEvent = new Event("open"); const errorEvent = new Event("error"); - const messageEvent = new MessageEvent("message") + const messageEvent = new MessageEvent("message"); const closeEvent = new CloseEvent("close"); server.publishOpen(openEvent); @@ -56,7 +55,7 @@ describe(createMockWebSocket.name, () => { // Could type this as a special JSON type, but unknown is good enough, // since any invalid values will throw in the test case - const jsonData: readonly unknown[] =[ + const jsonData: readonly unknown[] = [ "blah", 42, true, @@ -71,23 +70,27 @@ describe(createMockWebSocket.name, () => { profession: "Puzzle Solver", sadBackstory: true, greatVideoGames: true, - } + }, ]; for (const jd of jsonData) { socket.addEventListener("message", onMessage); - server.publishMessage(new MessageEvent("message", { - "data": JSON.stringify(jd) - })); + server.publishMessage( + new MessageEvent("message", { + data: JSON.stringify(jd), + }), + ); expect(onMessage).toHaveBeenCalledTimes(1); - expect(onMessage).toHaveBeenCalledWith(new MessageEvent("message", { - data: JSON.stringify(jd) - })); + expect(onMessage).toHaveBeenCalledWith( + new MessageEvent("message", { + data: JSON.stringify(jd), + }), + ); socket.removeEventListener("message", onMessage); onMessage.mockClear(); } - }) + }); it("Only registers each socket event handler once", () => { const [socket, server] = createMockWebSocket("wss://www.dog.ceo/borf"); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index adde2606a7b2a..c3dcd5fdd9766 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -47,7 +47,7 @@ export function createMockWebSocket( open: [], }; - let sendData: SocketSendData[] = []; + const sendData: SocketSendData[] = []; const mockSocket: WebSocket = { CONNECTING: 0, diff --git a/site/src/utils/OneWayWebSocket.test.ts b/site/src/utils/OneWayWebSocket.test.ts index bdb8f9202ea6c..3b674cc194fac 100644 --- a/site/src/utils/OneWayWebSocket.test.ts +++ b/site/src/utils/OneWayWebSocket.test.ts @@ -7,7 +7,10 @@ * WebSocket class doesn't have any bugs, and can safely be mocked out. */ -import { createMockWebSocket, MockWebSocketServer } from "testHelpers/websockets"; +import { + type MockWebSocketServer, + createMockWebSocket, +} from "testHelpers/websockets"; import { type OneWayMessageEvent, OneWayWebSocket } from "./OneWayWebSocket"; describe(OneWayWebSocket.name, () => { From 1834c5908d9d82ba6800cce7f5009b6e8230a153 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 01:27:38 +0000 Subject: [PATCH 06/12] refactor: rename variable --- site/src/utils/OneWayWebSocket.test.ts | 72 +++++++++++++------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/site/src/utils/OneWayWebSocket.test.ts b/site/src/utils/OneWayWebSocket.test.ts index 3b674cc194fac..a0492dab9b439 100644 --- a/site/src/utils/OneWayWebSocket.test.ts +++ b/site/src/utils/OneWayWebSocket.test.ts @@ -33,12 +33,12 @@ describe(OneWayWebSocket.name, () => { }); it("Lets a consumer add an event listener of each type", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -53,14 +53,14 @@ describe(OneWayWebSocket.name, () => { oneWay.addEventListener("error", onError); oneWay.addEventListener("message", onMessage); - publisher.publishOpen(new Event("open")); - publisher.publishClose(new CloseEvent("close")); - publisher.publishError( + mockServer.publishOpen(new Event("open")); + mockServer.publishClose(new CloseEvent("close")); + mockServer.publishError( new ErrorEvent("error", { error: new Error("Whoops - connection broke"), }), ); - publisher.publishMessage( + mockServer.publishMessage( new MessageEvent("message", { data: "null", }), @@ -73,12 +73,12 @@ describe(OneWayWebSocket.name, () => { }); it("Lets a consumer remove an event listener of each type", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -98,14 +98,14 @@ describe(OneWayWebSocket.name, () => { oneWay.removeEventListener("error", onError); oneWay.removeEventListener("message", onMessage); - publisher.publishOpen(new Event("open")); - publisher.publishClose(new CloseEvent("close")); - publisher.publishError( + mockServer.publishOpen(new Event("open")); + mockServer.publishClose(new CloseEvent("close")); + mockServer.publishError( new ErrorEvent("error", { error: new Error("Whoops - connection broke"), }), ); - publisher.publishMessage( + mockServer.publishMessage( new MessageEvent("message", { data: "null", }), @@ -118,12 +118,12 @@ describe(OneWayWebSocket.name, () => { }); it("Only calls each callback once if callback is added multiple times", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -140,14 +140,14 @@ describe(OneWayWebSocket.name, () => { oneWay.addEventListener("message", onMessage); } - publisher.publishOpen(new Event("open")); - publisher.publishClose(new CloseEvent("close")); - publisher.publishError( + mockServer.publishOpen(new Event("open")); + mockServer.publishClose(new CloseEvent("close")); + mockServer.publishError( new ErrorEvent("error", { error: new Error("Whoops - connection broke"), }), ); - publisher.publishMessage( + mockServer.publishMessage( new MessageEvent("message", { data: "null", }), @@ -160,12 +160,12 @@ describe(OneWayWebSocket.name, () => { }); it("Lets consumers register multiple callbacks for each event type", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -188,14 +188,14 @@ describe(OneWayWebSocket.name, () => { oneWay.addEventListener("error", onError2); oneWay.addEventListener("message", onMessage2); - publisher.publishOpen(new Event("open")); - publisher.publishClose(new CloseEvent("close")); - publisher.publishError( + mockServer.publishOpen(new Event("open")); + mockServer.publishClose(new CloseEvent("close")); + mockServer.publishError( new ErrorEvent("error", { error: new Error("Whoops - connection broke"), }), ); - publisher.publishMessage( + mockServer.publishMessage( new MessageEvent("message", { data: "null", }), @@ -241,12 +241,12 @@ describe(OneWayWebSocket.name, () => { }); it("Gives consumers pre-parsed versions of message events", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -262,7 +262,7 @@ describe(OneWayWebSocket.name, () => { data: JSON.stringify(payload), }); - publisher.publishMessage(event); + mockServer.publishMessage(event); expect(onMessage).toHaveBeenCalledWith({ sourceEvent: event, parsedMessage: payload, @@ -271,12 +271,12 @@ describe(OneWayWebSocket.name, () => { }); it("Exposes parsing error if message payload could not be parsed as JSON", () => { - let publisher!: MockWebSocketServer; + let mockServer!: MockWebSocketServer; const oneWay = new OneWayWebSocket({ apiRoute: dummyRoute, websocketInit: (url, protocols) => { - const [socket, pub] = createMockWebSocket(url, protocols); - publisher = pub; + const [socket, server] = createMockWebSocket(url, protocols); + mockServer = server; return socket; }, }); @@ -288,7 +288,7 @@ describe(OneWayWebSocket.name, () => { const event = new MessageEvent("message", { data: payload, }); - publisher.publishMessage(event); + mockServer.publishMessage(event); const arg: OneWayMessageEvent = onMessage.mock.lastCall[0]; expect(arg.sourceEvent).toEqual(event); From 6ee9afc035ae4a0cfbda4091f64dab84fd4ecb61 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Tue, 5 Aug 2025 01:46:00 +0000 Subject: [PATCH 07/12] refactor: rename property --- site/src/testHelpers/websockets.test.ts | 8 ++++---- site/src/testHelpers/websockets.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts index f0288f090bd0a..eceefbc00b638 100644 --- a/site/src/testHelpers/websockets.test.ts +++ b/site/src/testHelpers/websockets.test.ts @@ -179,12 +179,12 @@ describe(createMockWebSocket.name, () => { }); socket.send(data); - expect(server.socketSendArguments).toHaveLength(1); - expect(server.socketSendArguments).toEqual([data]); + expect(server.clientSentData).toHaveLength(1); + expect(server.clientSentData).toEqual([data]); socket.close(); socket.send(data); - expect(server.socketSendArguments).toHaveLength(1); - expect(server.socketSendArguments).toEqual([data]); + expect(server.clientSentData).toHaveLength(1); + expect(server.clientSentData).toEqual([data]); }); }); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index c3dcd5fdd9766..5792fa945b52b 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -9,7 +9,7 @@ export type MockWebSocketServer = Readonly<{ publishOpen: (event: Event) => void; readonly isConnectionOpen: boolean; - readonly socketSendArguments: readonly SocketSendData[]; + readonly clientSentData: readonly SocketSendData[]; }>; export function createMockWebSocket( @@ -47,7 +47,7 @@ export function createMockWebSocket( open: [], }; - const sendData: SocketSendData[] = []; + const sentData: SocketSendData[] = []; const mockSocket: WebSocket = { CONNECTING: 0, @@ -71,7 +71,7 @@ export function createMockWebSocket( if (!isOpen) { return; } - sendData.push(data); + sentData.push(data); }, addEventListener: ( @@ -113,8 +113,8 @@ export function createMockWebSocket( return isOpen; }, - get socketSendArguments() { - return [...sendData]; + get clientSentData() { + return [...sentData]; }, publishOpen: (event) => { From cb42aa7857306cc776187eb750cc27b86c79c3fe Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 7 Aug 2025 20:06:13 +0000 Subject: [PATCH 08/12] fix: use sets --- site/src/testHelpers/websockets.ts | 53 +++++++++++------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index 5792fa945b52b..cb2bb9f662510 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -12,39 +12,31 @@ export type MockWebSocketServer = Readonly<{ readonly clientSentData: readonly SocketSendData[]; }>; +type EventMap = { + message: MessageEvent; + error: Event; + close: CloseEvent; + open: Event; +}; + +type CallbackStore = { + [K in keyof EventMap]: Set<(event: EventMap[K]) => void>; +}; + export function createMockWebSocket( url: string, - protocols?: string | string[], + protocol?: string, ): readonly [WebSocket, MockWebSocketServer] { - type EventMap = { - message: MessageEvent; - error: Event; - close: CloseEvent; - open: Event; - }; - type CallbackStore = { - [K in keyof EventMap]: ((event: EventMap[K]) => void)[]; - }; - if (!url.startsWith("ws://") && !url.startsWith("wss://")) { throw new Error("URL must start with ws:// or wss://"); } - let activeProtocol: string; - if (Array.isArray(protocols)) { - activeProtocol = protocols[0] ?? ""; - } else if (typeof protocols === "string") { - activeProtocol = protocols; - } else { - activeProtocol = ""; - } - let isOpen = true; const store: CallbackStore = { - message: [], - error: [], - close: [], - open: [], + message: new Set(), + error: new Set(), + close: new Set(), + open: new Set(), }; const sentData: SocketSendData[] = []; @@ -56,7 +48,7 @@ export function createMockWebSocket( CLOSED: 3, url, - protocol: activeProtocol, + protocol: protocol ?? "", readyState: 1, binaryType: "blob", bufferedAmount: 0, @@ -81,11 +73,8 @@ export function createMockWebSocket( if (!isOpen) { return; } - const subscribers = store[eventType]; - if (!subscribers.includes(callback)) { - subscribers.push(callback); - } + subscribers.add(callback); }, removeEventListener: ( @@ -95,12 +84,8 @@ export function createMockWebSocket( if (!isOpen) { return; } - const subscribers = store[eventType]; - if (subscribers.includes(callback)) { - const updated = store[eventType].filter((c) => c !== callback); - store[eventType] = updated as CallbackStore[E]; - } + subscribers.delete(callback); }, close: () => { From b63ff10ffa4b6677b89f875b3861d08d0d5a40ed Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 7 Aug 2025 20:17:04 +0000 Subject: [PATCH 09/12] refactor: clean up types --- site/src/testHelpers/websockets.test.ts | 8 ++----- site/src/testHelpers/websockets.ts | 31 +++++++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/site/src/testHelpers/websockets.test.ts b/site/src/testHelpers/websockets.test.ts index eceefbc00b638..edd4191cffebe 100644 --- a/site/src/testHelpers/websockets.test.ts +++ b/site/src/testHelpers/websockets.test.ts @@ -75,16 +75,12 @@ describe(createMockWebSocket.name, () => { for (const jd of jsonData) { socket.addEventListener("message", onMessage); server.publishMessage( - new MessageEvent("message", { - data: JSON.stringify(jd), - }), + new MessageEvent("message", { data: JSON.stringify(jd) }), ); expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledWith( - new MessageEvent("message", { - data: JSON.stringify(jd), - }), + new MessageEvent("message", { data: JSON.stringify(jd) }), ); socket.removeEventListener("message", onMessage); diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index cb2bb9f662510..3f473ead0711f 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -1,6 +1,6 @@ import type { WebSocketEventType } from "utils/OneWayWebSocket"; -type SocketSendData = string | ArrayBufferLike | Blob | ArrayBufferView; +type SocketSendData = Parameters[0]; export type MockWebSocketServer = Readonly<{ publishMessage: (event: MessageEvent) => void; @@ -12,21 +12,28 @@ export type MockWebSocketServer = Readonly<{ readonly clientSentData: readonly SocketSendData[]; }>; -type EventMap = { - message: MessageEvent; - error: Event; - close: CloseEvent; - open: Event; +type CallbackStore = { + [K in keyof WebSocketEventMap]: Set<(event: WebSocketEventMap[K]) => void>; }; -type CallbackStore = { - [K in keyof EventMap]: Set<(event: EventMap[K]) => void>; +export type MockWebSocket = Omit & { + /** + * A version of the WebSocket `send` method that has been pre-wrapped inside + * a Jest mock. Most of the time, you should not be using the Jest mock + * features, and should instead be using the `clientSentData` property from + * the `MockWebSocketServer` type. + * + * The Jest mock functionality only exists to help make sure that the + * client-side socket method got called. It does nothing to make sure that + * the mock server actually received anything. + */ + send: jest.Mock; }; export function createMockWebSocket( url: string, protocol?: string, -): readonly [WebSocket, MockWebSocketServer] { +): readonly [MockWebSocket, MockWebSocketServer] { if (!url.startsWith("ws://") && !url.startsWith("wss://")) { throw new Error("URL must start with ws:// or wss://"); } @@ -41,7 +48,7 @@ export function createMockWebSocket( const sentData: SocketSendData[] = []; - const mockSocket: WebSocket = { + const mockSocket: MockWebSocket = { CONNECTING: 0, OPEN: 1, CLOSING: 2, @@ -59,12 +66,12 @@ export function createMockWebSocket( onopen: null, dispatchEvent: jest.fn(), - send: (data) => { + send: jest.fn((data) => { if (!isOpen) { return; } sentData.push(data); - }, + }), addEventListener: ( eventType: E, From dab74d71cc6a536f2386056c4bf9d95f3a02d71c Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 7 Aug 2025 20:24:12 +0000 Subject: [PATCH 10/12] docs: beef up comment --- site/src/testHelpers/websockets.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index 3f473ead0711f..6b975e3144318 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -19,13 +19,21 @@ type CallbackStore = { export type MockWebSocket = Omit & { /** * A version of the WebSocket `send` method that has been pre-wrapped inside - * a Jest mock. Most of the time, you should not be using the Jest mock - * features, and should instead be using the `clientSentData` property from - * the `MockWebSocketServer` type. + * a Jest mock. * - * The Jest mock functionality only exists to help make sure that the - * client-side socket method got called. It does nothing to make sure that - * the mock server actually received anything. + * The Jest mock functionality should be used at a minimum. Basically: + * 1. If you want to check that the mock socket sent something to the mock + * server: call the `send` method as a function, and then check the + * `clientSentData` on `MockWebSocketServer` to see what data got + * received. + * 2. If you need to make sure that the client-side `send` method got called + * at all: you can use the Jest mock functionality, but you should + * probably also be checking `clientSentData` still and making additional + * assertions with it. + * + * Generally, tests should center around whether socket-to-server + * communication was successful, not whether the client-side method was + * called. */ send: jest.Mock; }; From c17ee61a3433488754c71af06e3d15f8de2d56ba Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 7 Aug 2025 20:34:35 +0000 Subject: [PATCH 11/12] fix: update protocol back --- site/src/testHelpers/websockets.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index 6b975e3144318..010f8bf8c7664 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -40,12 +40,16 @@ export type MockWebSocket = Omit & { export function createMockWebSocket( url: string, - protocol?: string, + protocol?: string | string[] | undefined, ): readonly [MockWebSocket, MockWebSocketServer] { if (!url.startsWith("ws://") && !url.startsWith("wss://")) { throw new Error("URL must start with ws:// or wss://"); } + const activeProtocol = Array.isArray(protocol) + ? protocol.join(" ") + : (protocol ?? ""); + let isOpen = true; const store: CallbackStore = { message: new Set(), @@ -63,7 +67,7 @@ export function createMockWebSocket( CLOSED: 3, url, - protocol: protocol ?? "", + protocol: activeProtocol, readyState: 1, binaryType: "blob", bufferedAmount: 0, From 5f9032d09a2951c5a159a36d44c7a0e8ecbe9db2 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 7 Aug 2025 20:46:13 +0000 Subject: [PATCH 12/12] fix: knip --- site/src/testHelpers/websockets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/testHelpers/websockets.ts b/site/src/testHelpers/websockets.ts index 010f8bf8c7664..57584cd55e887 100644 --- a/site/src/testHelpers/websockets.ts +++ b/site/src/testHelpers/websockets.ts @@ -16,7 +16,7 @@ type CallbackStore = { [K in keyof WebSocketEventMap]: Set<(event: WebSocketEventMap[K]) => void>; }; -export type MockWebSocket = Omit & { +type MockWebSocket = Omit & { /** * A version of the WebSocket `send` method that has been pre-wrapped inside * a Jest mock. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy