All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
2.8 KiB
TypeScript
107 lines
2.8 KiB
TypeScript
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<Record<string, unknown>> {
|
|
return this.sent.map((s) => JSON.parse(s));
|
|
}
|
|
|
|
// ── Server side ───────────────────────────────────────────────────────────
|
|
|
|
open() {
|
|
act(() => {
|
|
this.readyState = FakeWebSocket.OPEN;
|
|
this.onopen?.(new Event("open"));
|
|
});
|
|
}
|
|
|
|
receive(msg: Wire<ServerToClientMessage> | Record<string, unknown>) {
|
|
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<Wire<ServerToClientMessage>, { type: "welcome" }>
|
|
> = {},
|
|
) {
|
|
this.open();
|
|
this.receive({
|
|
type: "welcome",
|
|
users: [],
|
|
myVotes: [],
|
|
myCommentLikes: [],
|
|
unreadNotificationCount: 0,
|
|
unreadChatCount: 0,
|
|
...overrides,
|
|
});
|
|
}
|
|
}
|