aboutsummaryrefslogtreecommitdiff
path: root/test/js/web/websocket/websocket-client.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'test/js/web/websocket/websocket-client.test.ts')
-rw-r--r--test/js/web/websocket/websocket-client.test.ts280
1 files changed, 280 insertions, 0 deletions
diff --git a/test/js/web/websocket/websocket-client.test.ts b/test/js/web/websocket/websocket-client.test.ts
new file mode 100644
index 000000000..470fa71c0
--- /dev/null
+++ b/test/js/web/websocket/websocket-client.test.ts
@@ -0,0 +1,280 @@
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import type { Subprocess } from "bun";
+import { spawn } from "bun";
+import { bunEnv, bunExe, nodeExe } from "harness";
+
+const strings = [
+ {
+ label: "string (ascii)",
+ message: "ascii",
+ bytes: [0x61, 0x73, 0x63, 0x69, 0x69],
+ },
+ {
+ label: "string (latin1)",
+ message: "latin1-©",
+ bytes: [0x6c, 0x61, 0x74, 0x69, 0x6e, 0x31, 0x2d, 0xc2, 0xa9],
+ },
+ {
+ label: "string (utf-8)",
+ message: "utf8-😶",
+ bytes: [0x75, 0x74, 0x66, 0x38, 0x2d, 0xf0, 0x9f, 0x98, 0xb6],
+ },
+];
+
+const buffers = [
+ {
+ label: "Uint8Array (utf-8)",
+ message: new TextEncoder().encode("utf8-🙂"),
+ bytes: [0x75, 0x74, 0x66, 0x38, 0x2d, 0xf0, 0x9f, 0x99, 0x82],
+ },
+ {
+ label: "ArrayBuffer (utf-8)",
+ message: new TextEncoder().encode("utf8-🙃").buffer,
+ bytes: [0x75, 0x74, 0x66, 0x38, 0x2d, 0xf0, 0x9f, 0x99, 0x83],
+ },
+ {
+ label: "Buffer (utf-8)",
+ message: Buffer.from("utf8-🤩"),
+ bytes: [0x75, 0x74, 0x66, 0x38, 0x2d, 0xf0, 0x9f, 0xa4, 0xa9],
+ },
+];
+
+const messages = [...strings, ...buffers];
+
+const binaryTypes = [
+ {
+ label: "nodebuffer",
+ type: Buffer,
+ },
+ {
+ label: "arraybuffer",
+ type: ArrayBuffer,
+ },
+] as const;
+
+let servers: Subprocess[] = [];
+let clients: WebSocket[] = [];
+
+function cleanUp() {
+ for (const client of clients) {
+ client.terminate();
+ }
+ for (const server of servers) {
+ server.kill();
+ }
+}
+
+beforeEach(cleanUp);
+afterEach(cleanUp);
+
+describe("WebSocket", () => {
+ test("url", (ws, done) => {
+ expect(ws.url).toStartWith("ws://");
+ done();
+ });
+ test("readyState", (ws, done) => {
+ expect(ws.readyState).toBe(WebSocket.CONNECTING);
+ ws.addEventListener("open", () => {
+ expect(ws.readyState).toBe(WebSocket.OPEN);
+ ws.close();
+ });
+ ws.addEventListener("close", () => {
+ expect(ws.readyState).toBe(WebSocket.CLOSED);
+ done();
+ });
+ });
+ describe("binaryType", () => {
+ test("(default)", (ws, done) => {
+ expect(ws.binaryType).toBe("nodebuffer");
+ done();
+ });
+ test("(invalid)", (ws, done) => {
+ try {
+ // @ts-expect-error
+ ws.binaryType = "invalid";
+ done(new Error("Expected an error"));
+ } catch {
+ done();
+ }
+ });
+ for (const { label, type } of binaryTypes) {
+ test(label, (ws, done) => {
+ ws.binaryType = label;
+ ws.addEventListener("open", () => {
+ expect(ws.binaryType).toBe(label);
+ ws.send(new Uint8Array(1));
+ });
+ ws.addEventListener("message", ({ data }) => {
+ expect(data).toBeInstanceOf(type);
+ ws.ping();
+ });
+ ws.addEventListener("ping", ({ data }) => {
+ expect(data).toBeInstanceOf(type);
+ ws.pong();
+ });
+ ws.addEventListener("pong", ({ data }) => {
+ expect(data).toBeInstanceOf(type);
+ done();
+ });
+ });
+ }
+ });
+ describe("send()", () => {
+ for (const { label, message, bytes } of messages) {
+ test(label, (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.send(message);
+ });
+ ws.addEventListener("message", ({ data }) => {
+ if (typeof data === "string") {
+ expect(data).toBe(message);
+ } else {
+ expect(data).toEqual(Buffer.from(bytes));
+ }
+ done();
+ });
+ });
+ }
+ });
+ describe("ping()", () => {
+ test("(no argument)", (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.ping();
+ });
+ ws.addEventListener("ping", ({ data }) => {
+ expect(data).toBeInstanceOf(Buffer);
+ done();
+ });
+ });
+ for (const { label, message, bytes } of messages) {
+ test(label, (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.ping(message);
+ });
+ ws.addEventListener("ping", ({ data }) => {
+ expect(data).toEqual(Buffer.from(bytes));
+ done();
+ });
+ });
+ }
+ });
+ describe("pong()", () => {
+ test("(no argument)", (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.pong();
+ });
+ ws.addEventListener("pong", ({ data }) => {
+ expect(data).toBeInstanceOf(Buffer);
+ done();
+ });
+ });
+ for (const { label, message, bytes } of messages) {
+ test(label, (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.pong(message);
+ });
+ ws.addEventListener("pong", ({ data }) => {
+ expect(data).toEqual(Buffer.from(bytes));
+ done();
+ });
+ });
+ }
+ });
+ describe("close()", () => {
+ test("(no arguments)", (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.close();
+ });
+ ws.addEventListener("close", ({ code, reason, wasClean }) => {
+ expect(code).toBe(1000);
+ expect(reason).toBeString();
+ expect(wasClean).toBeTrue();
+ done();
+ });
+ });
+ test("(no reason)", (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.close(1001);
+ });
+ ws.addEventListener("close", ({ code, reason, wasClean }) => {
+ expect(code).toBe(1001);
+ expect(reason).toBeString();
+ expect(wasClean).toBeTrue();
+ done();
+ });
+ });
+ // FIXME: Encoding issue
+ // Expected: "latin1-©"
+ // Received: "latin1-©"
+ /*
+ for (const { label, message } of strings) {
+ test(label, (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.close(1002, message);
+ });
+ ws.addEventListener("close", ({ code, reason, wasClean }) => {
+ expect(code).toBe(1002);
+ expect(reason).toBe(message);
+ expect(wasClean).toBeTrue();
+ done();
+ });
+ });
+ }
+ */
+ });
+ test("terminate()", (ws, done) => {
+ ws.addEventListener("open", () => {
+ ws.terminate();
+ });
+ ws.addEventListener("close", ({ code, reason, wasClean }) => {
+ expect(code).toBe(1006);
+ expect(reason).toBeString();
+ expect(wasClean).toBeFalse();
+ done();
+ });
+ });
+});
+
+function test(label: string, fn: (ws: WebSocket, done: (err?: unknown) => void) => void, timeout?: number) {
+ it(
+ label,
+ testDone => {
+ let isDone = false;
+ const done = (err?: unknown) => {
+ if (!isDone) {
+ isDone = true;
+ testDone(err);
+ }
+ };
+ listen()
+ .then(url => {
+ const ws = new WebSocket(url);
+ clients.push(ws);
+ fn(ws, done);
+ })
+ .catch(done);
+ },
+ { timeout: timeout ?? 1000 },
+ );
+}
+
+async function listen(): Promise<URL> {
+ const { pathname } = new URL("./websocket-server-echo.mjs", import.meta.url);
+ const server = spawn({
+ cmd: [nodeExe() ?? bunExe(), pathname],
+ cwd: import.meta.dir,
+ env: bunEnv,
+ stderr: "ignore",
+ stdout: "pipe",
+ });
+ servers.push(server);
+ for await (const chunk of server.stdout) {
+ const text = new TextDecoder().decode(chunk);
+ try {
+ return new URL(text);
+ } catch {
+ throw new Error(`Invalid URL: '${text}'`);
+ }
+ }
+ throw new Error("No URL found?");
+}