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:
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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: [{"id":1},{"id":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);
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user