Files
WRNexusJS/packages/csr/test/realtime.test.ts
T
ClintchizandClaude Opus 5 06433e4ac4
Quality / quality (ubuntu-latest) (push) Failing after 10m1s
Quality / quality (windows-latest) (push) Canceled after 0s
chore(release): publish db 0.8.21, cli 0.8.65 and seven in-flight packages
Carries the rollback refusal in db and the matching CLI message, together
with the auth, captcha, csr, dev-server, dev-toolbar, ui and validation
changes that were already in the working tree, since publishing stages
from it and those files would otherwise ship without being recorded here.

  @wrnexus/auth        0.8.28 -> 0.8.29
  @wrnexus/captcha     0.8.15 -> 0.8.16
  @wrnexus/cli         0.8.64 -> 0.8.65
  @wrnexus/csr         0.8.37 -> 0.8.38
  @wrnexus/db          0.8.20 -> 0.8.21
  @wrnexus/dev-server  0.8.60 -> 0.8.61
  @wrnexus/dev-toolbar 0.8.17 -> 0.8.18
  @wrnexus/ui          0.8.75 -> 0.8.76
  @wrnexus/validation  0.8.12 -> 0.8.13

Also declares Event among the browser globals captcha's client asset
already lists, which was the last lint failure, and reformats four files.
typecheck, lint, 1998 tests and format:check all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:17:09 +05:30

167 lines
5.8 KiB
TypeScript

import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom";
import { REALTIME_RUNTIME } from "../src/realtime-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
/* A fake WebSocket that records instances + sent frames and lets tests drive events. */
let sockets: FakeWS[];
class FakeWS {
url: string;
readyState = 0;
sent: string[] = [];
onopen: (() => void) | null = null;
onclose: (() => void) | null = null;
onerror: (() => void) | null = null;
onmessage: ((e: { data: string }) => void) | null = null;
constructor(url: string) {
this.url = url;
sockets.push(this);
}
send(data: string) {
this.sent.push(data);
}
close() {
this.readyState = 3;
}
fireOpen() {
this.readyState = 1;
this.onopen?.();
}
fireMessage(obj: unknown) {
this.onmessage?.({ data: JSON.stringify(obj) });
}
}
function boot(bodyHtml: string) {
sockets = [];
const win = new Window({ url: "http://localhost/" }) as unknown as Window &
Record<string, unknown>;
win.document.body.innerHTML = bodyHtml;
const g = globalThis as Record<string, unknown>;
g.window = win;
g.document = win.document;
g.location = win.location;
g.WebSocket = FakeWS;
(0, eval)(REALTIME_RUNTIME);
return win as unknown as Window;
}
const REPLACED_GLOBALS = ["window", "document", "location", "WebSocket"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
for (const k of REPLACED_GLOBALS) {
delete (globalThis as Record<string, unknown>)[k];
}
});
const CHAT = `
<div data-room="chat">
<span data-room-status data-room-status-class="badge" class="badge">connecting…</span>
<div data-room-log></div>
<template data-room-item="message"><div class="msg"><strong>%user%</strong>: %text%</div></template>
<template data-room-item="system"><div class="sys">%text%</div></template>
<form data-room-send>
<input name="user" value="Ada">
<input name="text" value="hello" data-room-reset>
<button type="submit">Send</button>
</form>
</div>`;
test("[data-room] connects to the right URL and reflects status", () => {
const win = boot(CHAT);
expect(sockets.length).toBe(1);
expect(sockets[0]!.url).toBe("ws://localhost/realtime/chat");
sockets[0]!.fireOpen();
const status = win.document.querySelector("[data-room-status]")!;
expect(status.textContent).toBe("connected");
expect(status.className).toContain("is-connected");
});
test("incoming messages render via the typed template, HTML-escaped", () => {
const win = boot(CHAT);
sockets[0]!.fireOpen();
sockets[0]!.fireMessage({ type: "message", user: "<b>Ada</b>", text: "hi & bye" });
sockets[0]!.fireMessage({ type: "system", text: "joined" });
const log = win.document.querySelector("[data-room-log]")!;
expect(log.querySelector(".msg strong")!.textContent).toBe("<b>Ada</b>"); // escaped, not parsed
expect(log.querySelector(".msg")!.textContent).toBe("<b>Ada</b>: hi & bye");
expect(log.querySelector(".sys")!.textContent).toBe("joined");
});
test("submitting [data-room-send] sends JSON and clears reset fields", () => {
const win = boot(CHAT);
sockets[0]!.fireOpen();
const form = win.document.querySelector("form[data-room-send]")! as unknown as HTMLFormElement;
form.dispatchEvent(
new (win as unknown as { Event: typeof Event }).Event("submit", {
bubbles: true,
cancelable: true,
}),
);
expect(sockets[0]!.sent.length).toBe(1);
expect(JSON.parse(sockets[0]!.sent[0]!)).toEqual({ user: "Ada", text: "hello" });
// text field had data-room-reset → cleared; user field kept.
const inputs = win.document.querySelectorAll("input");
expect((inputs[0] as unknown as HTMLInputElement).value).toBe("Ada");
expect((inputs[1] as unknown as HTMLInputElement).value).toBe("");
});
test("data-room-refresh softly refreshes and emits a browser event", async () => {
const win = boot(`<div data-room="updates" data-room-refresh></div>`) as unknown as Window & {
__wrnexusRefresh: () => void;
};
let events = 0;
let refreshes = 0;
win.addEventListener("wrnexus:realtime", () => events++);
win.__wrnexusRefresh = () => refreshes++;
sockets[0]!.fireMessage({ type: "invalidate", resource: "messages" });
await Bun.sleep(300);
expect(events).toBe(1);
expect(refreshes).toBe(1);
});
test("programmatic wrn.room() sends and receives", () => {
const win = boot(`<div></div>`) as unknown as Window & {
wrn: {
room: (n: string) => {
on: (t: string, cb: (m: unknown) => void) => unknown;
send: (o: unknown) => void;
};
};
};
const got: unknown[] = [];
const room = win.wrn.room("lobby");
room.on("ping", (m: unknown) => got.push(m));
sockets[0]!.fireOpen();
room.send({ type: "hello" });
expect(JSON.parse(sockets[0]!.sent[0]!)).toEqual({ type: "hello" });
sockets[0]!.fireMessage({ type: "ping", n: 1 });
expect(got).toEqual([{ type: "ping", n: 1 }]);
});
test("same room with different users opens identity-specific sockets", () => {
boot(`
<div data-room="teamspace" data-room-user="asha"></div>
<div data-room="teamspace" data-room-user="rohan"></div>
`);
expect(sockets.map((socket) => socket.url).sort()).toEqual([
"ws://localhost/realtime/teamspace?user=asha",
"ws://localhost/realtime/teamspace?user=rohan",
]);
});
test("declarative room reconnects when its user identity changes", () => {
const win = boot(
`<div id="room" data-room="teamspace" data-room-user="asha"></div>`,
) as unknown as Window & {
wrn: { bindRooms: (root?: unknown) => void };
};
expect(sockets[0]?.url).toBe("ws://localhost/realtime/teamspace?user=asha");
win.document.getElementById("room")!.setAttribute("data-room-user", "neha");
win.wrn.bindRooms(win.document);
expect(sockets[0]?.readyState).toBe(3);
expect(sockets[1]?.url).toBe("ws://localhost/realtime/teamspace?user=neha");
});