-
Notifications
You must be signed in to change notification settings - Fork 960
fix(site): add tests for createMockWebSocket
#19172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
a87e93e
1c48afc
5eff4df
c2d07fb
21c9eb8
1834c59
6ee9afc
c95aa4b
cb42aa7
b63ff10
dab74d7
c17ee61
9b155b1
5f9032d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string>("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<string>("message", { | ||
"data": JSON.stringify(jd) | ||
})); | ||
|
||
expect(onMessage).toHaveBeenCalledTimes(1); | ||
expect(onMessage).toHaveBeenCalledWith(new MessageEvent<string>("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<string>("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<string>("message")); | ||
expect(onMessage).not.toHaveBeenCalled(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import type { WebSocketEventType } from "utils/OneWayWebSocket"; | ||
|
||
export type MockWebSocketPublisher = Readonly<{ | ||
publishMessage: (event: MessageEvent<string>) => 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<string>; | ||
error: Event; | ||
close: CloseEvent; | ||
open: Event; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just use the built-in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kind of forgot the reason, but I think it was originally in service for the Now that this is split off, though, I'll see if I can remove these and still have the types line up |
||
type CallbackStore = { | ||
[K in keyof EventMap]: ((event: EventMap[K]) => void)[]; | ||
}; | ||
Parkreiner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let activeProtocol: string; | ||
if (Array.isArray(protocols)) { | ||
activeProtocol = protocols[0] ?? ""; | ||
} else if (typeof protocols === "string") { | ||
activeProtocol = protocols; | ||
} else { | ||
activeProtocol = ""; | ||
} | ||
Parkreiner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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: <E extends WebSocketEventType>( | ||
eventType: E, | ||
callback: (event: WebSocketEventMap[E]) => void, | ||
) => { | ||
if (!isOpen) { | ||
return; | ||
} | ||
|
||
const subscribers = store[eventType]; | ||
if (!subscribers.includes(callback)) { | ||
subscribers.push(callback); | ||
} | ||
Parkreiner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
|
||
removeEventListener: <E extends WebSocketEventType>( | ||
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]; | ||
} | ||
Parkreiner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
|
||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this test makes me feel like I'm looking at a freshly ironed shirt. super nice!