fix: isolate test globals, close tags at every caret, trim the runtime

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>
This commit is contained in:
2026-08-19 10:14:18 +05:30
co-authored by Claude Opus 5
parent ac248f2bb0
commit a20f143acb
16 changed files with 650 additions and 393 deletions
+28
View File
@@ -0,0 +1,28 @@
import { afterAll } from "bun:test";
/**
* Restore globals a suite replaces, once the suite is done.
*
* These suites install a happy-dom window over the real globals and delete
* them before each test so every test starts clean. bun test loads and runs
* one file at a time rather than importing them all up front, so anything left
* deleted is still missing when the next suite runs -- which is how `bun test`
* with no argument came to fail unrelated files with "fetch is not a
* function". Names absent at capture time are deleted again rather than being
* restored as undefined, so a global that never existed does not gain a key.
*/
export function restoreGlobalsAfterAll(names: readonly string[]): void {
const captured = new Map<string, unknown>(
names.map((name) => [name, (globalThis as Record<string, unknown>)[name]]),
);
afterAll(() => {
for (const [name, value] of captured) {
if (value === undefined) {
delete (globalThis as Record<string, unknown>)[name];
} else {
(globalThis as Record<string, unknown>)[name] = value;
}
}
});
}
+15 -10
View File
@@ -1,6 +1,7 @@
import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom";
import { NAV_RUNTIME } from "../src/nav-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
let win: any;
let fetchCalls: { url: string; opts: any }[];
@@ -36,18 +37,22 @@ function install(bodyHtml: string): void {
const flush = () => new Promise((r) => setTimeout(r, 0));
const REPLACED_GLOBALS = [
"window",
"document",
"history",
"location",
"DOMParser",
"CustomEvent",
"Event",
"fetch",
];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
const g = globalThis as any;
for (const k of [
"window",
"document",
"history",
"location",
"DOMParser",
"CustomEvent",
"Event",
"fetch",
]) {
for (const k of REPLACED_GLOBALS) {
delete g[k];
}
});
+28 -5
View File
@@ -3,6 +3,7 @@ import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { getComponentControllerRuntime, getReactiveRuntime } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
// Fresh DOM per test, with the runtime's globals bound.
function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window {
@@ -34,12 +35,14 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
return win as unknown as Window;
}
const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "MutationObserver"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
delete (globalThis as Record<string, unknown>).window;
delete (globalThis as Record<string, unknown>).document;
delete (globalThis as Record<string, unknown>).location;
delete (globalThis as Record<string, unknown>).fetch;
delete (globalThis as Record<string, unknown>).MutationObserver;
for (const name of REPLACED_GLOBALS) {
delete (globalThis as Record<string, unknown>)[name];
}
});
test("split runtime hydrates a controller only from the controller asset", () => {
@@ -1758,3 +1761,23 @@ test("an unbounded loop stops instead of hanging the page", () => {
expect(value).toBeGreaterThan(0);
expect(Number.isFinite(value)).toBe(true);
});
test("a class binding inside data-for follows state the row never mentions", () => {
// The row's own array is untouched, so nothing rebuilds the list. The
// binding has to be reactive in its own right to keep up.
const binding = JSON.stringify(["is-active", "selected === row.id"]);
const win = mount(
`<div data-scope="rows: [{&quot;id&quot;:1},{&quot;id&quot;:2}], selected: 1">` +
`<button data-on-click="selected = 2">pick</button>` +
`<ul><li data-for="row in rows" data-wrn-class-active='${binding}'></li></ul>` +
`</div>`,
);
const items = () => Array.from(win.document.querySelectorAll("li"));
expect(items()[0]?.classList.contains("is-active")).toBe(true);
expect(items()[1]?.classList.contains("is-active")).toBe(false);
win.document.querySelector("button")!.click();
expect(items()[0]?.classList.contains("is-active")).toBe(false);
expect(items()[1]?.classList.contains("is-active")).toBe(true);
});
+6 -1
View File
@@ -1,6 +1,7 @@
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[];
@@ -45,8 +46,12 @@ function boot(bodyHtml: string) {
return win as unknown as Window;
}
const REPLACED_GLOBALS = ["window", "document", "location", "WebSocket"];
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
for (const k of ["window", "document", "location", "WebSocket"]) {
for (const k of REPLACED_GLOBALS) {
delete (globalThis as Record<string, unknown>)[k];
}
});