diff options
Diffstat (limited to 'test/js/web/websocket/websocket-client.test.ts')
-rw-r--r-- | test/js/web/websocket/websocket-client.test.ts | 280 |
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?"); +} |