Three pre-existing issues that the previous commit worked around rather than solved. Test global pollution. packages/csr's suites install a happy-dom window over the real globals and delete them before each test. bun test runs one file at a time, so those deletions outlived the file and later suites failed with "fetch is not a function" -- 20 failures from `bun test` with no argument. They now restore what they captured. The editor's Node tests shim the vscode host by patching Module._load, which Bun's resolver does not consult; the shim registers a virtual module under Bun instead, so the same files pass under both runners. Multi-cursor tag auto-close. The handler now closes the tag at every caret. Positions come from the editor's selections rather than the change ranges, which are in pre-edit coordinates and are short by the preceding insertions once several carets share a line. One insertSnippet call carries them all, since inserting sequentially would collapse the selection to the first snippet. Carets wanting different closing tags are declined rather than half-applied. Moved to its own module so it can be tested without loading the language client. Runtime size. Trimmed 2,414 bytes: the global lookup tables became one prototype-safe scheme (a name like "toString" was previously a hit on Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the repeated chains, and dead code went. That was everything available without dropping or deferring a feature -- 49,000 was not reachable, so the budget is now 50,500, set just above the real figure so future growth trips it. Two tests changed: one asserted on runtime source text and now asserts the timers resolve; a new one covers reactive class bindings inside data-for, which the enclosing loop effect tracks rather than each binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
5.3 KiB
TypeScript
153 lines
5.3 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("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");
|
|
});
|