import { test, expect, beforeEach } from "bun:test";
import { Window, type HTMLButtonElement as HappyButtonElement } 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";
import {
runWithRequestContext,
encodeControlTemplate,
renderControlTemplates,
} from "@wrnexus/core";
test("controlled select survives option reconciliation and uses the latest value", async () => {
const win = mount(
`
Choose Field Form Change
`,
);
const select = win.document.querySelector("select")!;
// Native option reconciliation can reset selection after the value binding.
select.innerHTML =
'Choose Field Form ';
await new Promise((resolve) => setTimeout(resolve, 0));
expect(select.value).toBe("field");
select.innerHTML =
'Choose Field Form ';
await new Promise((resolve) => setTimeout(resolve, 10));
expect(select.value).toBe("field");
win.document.querySelector("button")!.click();
select.value = "";
await new Promise((resolve) => setTimeout(resolve, 0));
expect(select.value).toBe("form");
});
test("an unchanged conditional branch preserves input identity and focus", () => {
const definition = Buffer.from(
JSON.stringify([{ cond: "count >= 0", body: ' ' }]),
).toString("base64");
const win = mount(
`Update
`,
);
const input = win.document.querySelector("input")!;
input.value = "Typing";
input.focus();
win.document.querySelector("button")!.click();
expect(win.document.querySelector("input") === input).toBe(true);
expect(win.document.activeElement === input).toBe(true);
expect(input.value).toBe("Typing");
});
test("compacted nested component controls materialize immediately and keep loop reactivity", () => {
runWithRequestContext({} as never, () => {
const rows = encodeControlTemplate({
list: "items",
item: "item",
index: "index",
body: '{index}:{item}
',
empty: 'Empty
',
});
const inner = encodeControlTemplate([
{
cond: "active",
body: `Stale SSR row
`,
},
]);
const child = ``;
const outer = encodeControlTemplate([
{ cond: "open", body: child },
{ cond: null, body: "Closed " },
]);
const win = mount(
renderControlTemplates() +
`Toggle Closed
`,
);
win.document.querySelector("button")!.click();
expect([...win.document.querySelectorAll(".row")].map((node) => node.textContent)).toEqual([
"0:a",
"1:b",
]);
expect(win.document.querySelector("#app")!.textContent).not.toContain("Stale SSR");
win.document.querySelectorAll("button")[1]!.click();
expect(win.document.querySelector(".row")).toBeNull();
expect(win.document.querySelector(".empty")?.textContent).toBe("Empty");
win.document.querySelector("button")!.click();
expect(win.document.querySelector("i")?.textContent).toBe("Closed");
});
});
// Fresh DOM per test, with the runtime's globals bound.
function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window {
const win = new Window() as unknown as Window & Record;
win.document.body.innerHTML = `${html}
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
/*
* Bind the window CustomEvent too. Without it the runtime constructs events
* from the host global, and a listener registered through happy-dom never
* matches them, so anything dispatched looks silently lost.
*/
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
(globalThis as Record).HTMLOptionElement = (
win as unknown as { HTMLOptionElement: unknown }
).HTMLOptionElement;
(globalThis as Record).HTMLInputElement = (
win as unknown as { HTMLInputElement: unknown }
).HTMLInputElement;
(globalThis as Record).HTMLTextAreaElement = (
win as unknown as { HTMLTextAreaElement: unknown }
).HTMLTextAreaElement;
(globalThis as Record).HTMLSelectElement = (
win as unknown as { HTMLSelectElement: unknown }
).HTMLSelectElement;
(0, eval)(runtime);
if (controllers) (0, eval)(controllers);
// Hydrate deterministically (auto-init waits on DOMContentLoaded, which the
// test window may not fire). setupScope is idempotent, so this is safe.
const w = win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void };
w.__wrnexusHydrateScopes?.(win.document);
return win as unknown as Window;
}
const REPLACED_GLOBALS = [
"window",
"document",
"location",
"fetch",
"MutationObserver",
"HTMLOptionElement",
"HTMLInputElement",
"HTMLTextAreaElement",
"HTMLSelectElement",
];
test("optional properties and nullish fallbacks hydrate without crashing", () => {
const win = mount(
`
`,
);
expect(win.document.querySelector("b")?.textContent).toBe("0");
expect(win.document.querySelector("i")?.textContent).toBe("0");
expect(win.document.querySelector("em")?.textContent).toBe("ok");
expect(win.document.querySelector("u")?.textContent).toBe("true");
});
restoreGlobalsAfterAll(REPLACED_GLOBALS);
beforeEach(() => {
for (const name of REPLACED_GLOBALS) {
delete (globalThis as Record)[name];
}
});
test("split runtime hydrates a controller only from the controller asset", () => {
const core = getReactiveRuntime(true);
const controllers = getComponentControllerRuntime(true);
expect(core).not.toContain("function setupRovingFocus");
expect(controllers).toContain("function setupRovingFocus");
const win = mount(
`One Two
`,
core,
controllers,
);
const buttons = win.document.querySelectorAll("button");
(buttons[0] as unknown as HTMLButtonElement).focus();
buttons[0]!.dispatchEvent(new win.KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }));
expect(win.document.activeElement).toBe(buttons[1]);
});
test("production hydration uses Trusted Types for HTML and controller URLs", () => {
const runtime = getReactiveRuntime();
expect(runtime).toContain('createPolicy("wrnexus"');
expect(runtime).toContain('template.innerHTML = trustedHTML(markup || "")');
expect(runtime).toContain("script.src = trustedScriptURL(componentControllerUrl)");
});
test("a page without controller markers does not request the controller asset", () => {
const win = mount(
`{count} `,
getReactiveRuntime(true),
);
expect(win.document.querySelector('script[src$="controllers.js"]')).toBeNull();
});
test("hydrates {expr} mustaches from data-scope", () => {
const win = mount(`{count}, {count * 2}
`);
expect(win.document.querySelector("span")!.textContent).toBe("0, 0");
});
test("mounts client-only templates before hydrating their scopes", () => {
const win = mount(
`
` +
`{count} `,
);
const runtime = win as unknown as {
__wrnexusMountClientRoots?: (root: unknown) => void;
__wrnexusHydrateScopes?: (root: unknown) => void;
};
runtime.__wrnexusMountClientRoots?.(win.document);
runtime.__wrnexusHydrateScopes?.(win.document);
expect(win.document.querySelector("template")).toBeNull();
expect(win.document.querySelector("main b")?.textContent).toBe("2");
});
test("orchestrates named client loads and renders the success template", async () => {
(globalThis as Record).fetch = () =>
Promise.resolve(Response.json({ data: { name: "Ada" } }));
const win = mount(
`` +
`Loading
` +
`Loading ` +
`{users.name} ` +
`{error.message} `,
);
const runtime = win as unknown as { __wrnexusHydrateAsyncBoundaries?: (root: unknown) => void };
runtime.__wrnexusHydrateAsyncBoundaries?.(win.document);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("section")?.getAttribute("data-wrn-async-state")).toBe(
"success",
);
expect(win.document.querySelector("strong")?.textContent).toBe("Ada");
});
test("@event (data-on-click) mutates a signal and re-renders", () => {
const win = mount(
`{count}
`,
);
const btn = win.document.querySelector("button")!;
expect(btn.textContent).toBe("0");
btn.click();
btn.click();
expect(btn.textContent).toBe("2");
});
test("new conditional content preserves nested component field state", async () => {
const definition = Buffer.from(
JSON.stringify([
{
cond: "open",
body: 'Evidence
',
},
]),
).toString("base64");
const win = mount(
`Open
`,
);
win.document.querySelector("button")!.click();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("label")?.textContent).toBe("Evidence");
expect(win.document.querySelector("input")?.name).toBe("evidenceReference");
});
test("compiled if blocks switch branches after hydration", () => {
const definition = Buffer.from(
JSON.stringify([
{ cond: "open", body: 'Open {count}
' },
{ cond: null, body: 'Closed
' },
]),
).toString("base64");
const win = mount(
`` +
`
toggle ` +
`
increment ` +
`
Closed
` +
`
`,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector(".closed")).toBeNull();
expect(win.document.querySelector(".open")?.textContent).toBe("Open 2");
win.document.querySelectorAll("button")[1]!.click();
expect(win.document.querySelector(".open")?.textContent).toBe("Open 3");
});
test("registry controls retain nested markup and work after the registry leaves the document", () => {
const definitions = [
[
{
cond: "open",
body: 'Nested ',
},
{ cond: null, body: "Closed " },
],
[{ cond: null, body: "Nested " }],
];
const win = mount(
`` +
'Toggle Closed
',
);
win.document.getElementById("registry")!.remove();
win.document.querySelector("button")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("Nested");
expect(win.document.querySelector("i")).toBeNull();
win.document.querySelector("button")!.click();
expect(win.document.querySelector("i")?.textContent).toBe("Closed");
});
test("compiled each blocks rerender rows and their empty branch", () => {
const definition = Buffer.from(
JSON.stringify({
list: "items",
item: "item",
index: "index",
body: '{index}:{item}
',
empty: 'Empty
',
}),
).toString("base64");
const win = mount(
`` +
`
more ` +
`
clear ` +
`
0:a
` +
`
`,
);
win.document.querySelectorAll("button")[0]!.click();
expect(Array.from(win.document.querySelectorAll(".row")).map((node) => node.textContent)).toEqual(
["0:b", "1:c"],
);
win.document.querySelectorAll("button")[1]!.click();
expect(win.document.querySelector(".row")).toBeNull();
expect(win.document.querySelector(".empty")?.textContent).toBe("Empty");
});
test("component functions support formatted multiline assignments and ternaries", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `
function increment(event, nextValue) {
nextValue =
Number(count) +
(event.shiftKey ? 10 : 1)
count = nextValue
}
function normalize(useMinimum) {
count = useMinimum
? 0
: Number(count)
}
`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = mount(
`
{count}
normalize
`,
);
const increment = win.document.getElementById("increment") as unknown as HTMLElement;
increment.click();
expect(increment.textContent).toBe("1");
const MouseEventConstructor = (win as unknown as { MouseEvent: typeof MouseEvent }).MouseEvent;
increment.dispatchEvent(
new MouseEventConstructor("click", { shiftKey: true }) as unknown as Event,
);
expect(increment.textContent).toBe("11");
(win.document.getElementById("normalize") as unknown as HTMLElement).click();
expect(increment.textContent).toBe("0");
});
test("component functions evaluate template literals without unsafe eval", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `function increment() {
count++
message = \`Count is now \${count}.\`
}`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = mount(
`` +
`{message}
`,
);
const button = win.document.querySelector("button")!;
button.click();
expect(button.textContent).toBe("Count is now 1.");
});
test("declared component events emit through the generic $emit function", () => {
const win = mount(
`
Complete
`,
);
const root = win.document.querySelector("[data-wrn-events]")!;
let detail: Record | undefined;
root.addEventListener("complete", (event) => {
detail = (event as unknown as CustomEvent).detail;
});
win.document.querySelector("button")!.click();
expect(detail).toEqual({ value: "4829" });
});
test("nested scopes don't clobber each other (regression)", () => {
// An empty outer scope must not touch inner scopes' values.
const win = mount(
``,
);
const [a, b] = Array.from(win.document.querySelectorAll("button"));
expect(a!.textContent).toBe("A0");
expect(b!.textContent).toBe("B10");
a!.click();
expect(a!.textContent).toBe("A1");
expect(b!.textContent).toBe("B10"); // unchanged
});
test("a delayed parent does not replace a nested select's scope API", async () => {
let resolveModule: (module: object) => void = () => {};
const globals = globalThis as Record;
globals.__wrnSelectParentModule = new Promise((resolve) => {
resolveModule = resolve;
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnSelectParentModule",
);
const win = mount(
``,
runtime,
);
const child = win.document.querySelector("#child") as unknown as {
__wrnexusScopeApi: { get(name: string): unknown };
};
const select = win.document.querySelector("select") as unknown as {
__wrnexusScopeApi: { get(name: string): unknown };
};
expect(select.__wrnexusScopeApi.get("value")).toBe("personal");
resolveModule({});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(
(
win.document.querySelector("#parent") as unknown as {
__wrnexusScopeApi: { get(name: string): unknown };
}
).__wrnexusScopeApi.get("value"),
).toBe("parent");
expect(select.__wrnexusScopeApi).toBe(child.__wrnexusScopeApi);
expect(select.__wrnexusScopeApi.get("value")).toBe("personal");
} finally {
delete globals.__wrnSelectParentModule;
}
});
test("data-text binds an element's textContent to an expression", () => {
const win = mount(
`? x
`,
);
expect(win.document.querySelector("strong")!.textContent).toBe("9");
win.document.querySelector("button")!.click();
expect(win.document.querySelector("strong")!.textContent).toBe("15");
});
test("string scope values bind via data-text", () => {
const win = mount(`?
`);
expect(win.document.querySelector("strong")!.textContent).toBe("hi");
});
test("data-for renders a list of objects and reacts to array changes", () => {
const win = mount(
``,
);
const items = () => Array.from(win.document.querySelectorAll("li"), (li) => li.textContent);
expect(items()).toEqual(["a", "b"]);
(win.document.getElementById("add") as unknown as HTMLElement).click();
expect(items()).toEqual(["a", "b", "c"]);
(win.document.getElementById("clear") as unknown as HTMLElement).click();
expect(items()).toEqual([]);
});
test("data-for exposes item + index, mustaches and member access", () => {
const win = mount(
``,
);
expect(Array.from(win.document.querySelectorAll("li"), (li) => li.textContent)).toEqual([
"0:x",
"1:y",
]);
});
test("keyed data-for preserves DOM identity when items reorder", () => {
const win = mount(
``,
);
const before = Array.from(win.document.querySelectorAll("li"));
expect(before.map((node) => node.textContent)).toEqual(["a", "b"]);
(win.document.querySelector("button") as unknown as HTMLElement).click();
const after = Array.from(win.document.querySelectorAll("li"));
expect(after.map((node) => node.textContent)).toEqual(["b", "a"]);
expect(after[0]).toBe(before[1]);
expect(after[1]).toBe(before[0]);
});
test("expression evaluator: member access, ternary, comparison, calls", () => {
const win = mount(
`
`,
);
expect(win.document.getElementById("a")!.textContent).toBe("Ada");
expect(win.document.getElementById("b")!.textContent).toBe("senior");
expect(win.document.getElementById("c")!.textContent).toBe("3");
});
test("expression evaluator exposes safe primitive conversion globals", () => {
const win = mount(
`
`,
);
expect(win.document.querySelector("span")!.textContent).toBe("12");
});
test("logical expressions consume their right-hand side when the result short-circuits", () => {
const win = mount(
`
`,
);
expect(win.document.getElementById("and")!.textContent).toBe("false");
expect(win.document.getElementById("or")!.textContent).toBe("true");
});
test("expression evaluator supports flatMap callbacks", () => {
const win = mount(
`
`,
);
expect(win.document.querySelector("span")!.textContent).toBe("1,2,3");
});
test("data-show toggles visibility without destroying interactive state", () => {
const win = mount(
`
go
`,
);
const a = win.document.getElementById("a") as unknown as HTMLElement;
const b = win.document.getElementById("b") as unknown as HTMLElement;
expect(a.style.display).toBe("");
expect(b.style.display).toBe("none");
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(a.style.display).toBe("none");
expect(b.style.display).toBe("");
});
test("dynamic components remove inactive cases from the live DOM", async () => {
const win = mount(
``,
);
win.document.dispatchEvent(new win.Event("DOMContentLoaded"));
expect(win.document.querySelector('[data-component-case="Admin"]')?.textContent).toContain(
"Administrator",
);
expect(win.document.querySelector('[data-component-case="Guest"]')).toBeNull();
(win.document.querySelector("button") as unknown as HTMLElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector('[data-component-case="Admin"]')).toBeNull();
expect(win.document.querySelector('[data-component-case="Guest"]')?.textContent).toContain(
"Guest",
);
});
test("reactive data attributes preserve explicit boolean strings", () => {
const win = mount(
`
`,
);
expect(win.document.getElementById("carousel-layout")!.getAttribute("data-show")).toBe("true");
});
test("reactive attribute bindings update input and accessibility attributes", () => {
const win = mount(
`
Toggle
`,
);
const input = win.document.getElementById("password") as unknown as HTMLInputElement;
const button = win.document.querySelector("button") as unknown as HTMLElement;
expect(input.type).toBe("password");
expect(button.getAttribute("aria-label")).toBe("Show password");
button.click();
expect(input.type).toBe("text");
expect(button.getAttribute("aria-label")).toBe("Hide password");
});
test("parent state updates a mounted child's reactive prop", async () => {
const win = mount(
`` +
`
increment ` +
`
` +
`1 ` +
`
`,
);
await Promise.resolve();
expect(win.document.querySelector("#child-value")?.textContent).toBe("1");
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#child-value")?.textContent).toBe("2");
});
test("a prop marker forwarded onto a child control still updates the child scope", async () => {
const win = mount(
`` +
`
enable ` +
`
` +
`child ` +
`
`,
);
await Promise.resolve();
const buttons = win.document.querySelectorAll("button");
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(true);
(buttons[0] as unknown as HTMLElement).click();
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(false);
});
test("independent signals in one scope update correctly (dependency tracking)", () => {
const win = mount(
`
A
B
`,
);
const ta = () => win.document.getElementById("ta")!.textContent;
const tb = () => win.document.getElementById("tb")!.textContent;
const click = (id: string) => (win.document.getElementById(id) as unknown as HTMLElement).click();
expect([ta(), tb()]).toEqual(["0", "100"]);
click("ba");
click("ba");
expect([ta(), tb()]).toEqual(["2", "100"]); // b untouched by a's changes
click("bb");
expect([ta(), tb()]).toEqual(["2", "101"]);
});
test("data-for preserves arrays of objects from encoded component scope", () => {
const items = [
{
question: "One",
answer: "First",
},
{
question: "Two",
answer: "Second",
},
{
question: "Three",
answer: "Third",
},
{
question: "Four",
answer: "Fourth",
},
];
const encoded = Buffer.from(
JSON.stringify({
items,
}),
"utf8",
).toString("base64");
const dom = mountHtml(`
{index + 1}
{item.question}
`);
expect(dom.querySelectorAll("article")).toHaveLength(4);
expect(dom.querySelectorAll("strong").map((element) => element.textContent)).toEqual([
"One",
"Two",
"Three",
"Four",
]);
});
test("$emit inside component functions does not depend on a global browser event", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `
function showOverlay(sourceEvent) {
visible = true
$emit("open", { source: sourceEvent.type })
}
`,
watches: [],
lifecycle: {},
}),
).toString("base64");
delete (globalThis as Record).event;
const win = mount(
`
Open overlay
{visible}
`,
);
const root = win.document.querySelector("[data-wrn-events]")!;
let detail: Record | undefined;
root.addEventListener("open", (event) => {
detail = (event as unknown as CustomEvent).detail;
});
win.document.querySelector("button")!.click();
expect(win.document.querySelector("span")!.textContent).toBe("true");
expect(detail).toEqual({ source: "click" });
});
test("template literals work inside browser API call arguments", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `function save() {
window.localStorage.setItem(\`prefs:\${count}\`, JSON.stringify(count))
message = \`Saved \${count}\`
}`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = mount(
`` +
`{message}
`,
);
win.document.querySelector("button")!.click();
expect(win.localStorage.getItem("prefs:3")).toBe("3");
expect(win.document.querySelector("button")!.textContent).toBe("Saved 3");
});
test("hydration consumes private scope, behavior, and binding metadata", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: "function toggle() { active = !active }",
watches: [],
lifecycle: {},
}),
).toString("base64");
const scope = Buffer.from(JSON.stringify({ active: false })).toString("base64");
const win = mount(
`` +
`Toggle ` +
`
`,
);
const root = win.document.getElementById("scope")!;
const button = win.document.getElementById("toggle")!;
expect(root.hasAttribute("data-scope")).toBe(false);
expect(root.hasAttribute("data-wrn-scope")).toBe(false);
expect(root.hasAttribute("data-wrn-behavior")).toBe(false);
expect(button.hasAttribute("data-wrn-bind-0")).toBe(false);
expect(button.getAttribute("aria-expanded")).toBe("false");
(button as unknown as { click(): void }).click();
expect(button.getAttribute("aria-expanded")).toBe("true");
});
test("hydration removes server-resolved document binding metadata", () => {
const win = mount(`Document content `);
const root = win.document.documentElement;
root.setAttribute("lang", "en");
root.setAttribute("data-theme", "dark");
root.setAttribute("data-wrn-bind-0", '["lang","{language}"]');
root.setAttribute("data-wrn-bind-1", '["data-theme","{theme}"]');
const runtime = win as unknown as {
__wrnexusHydrateScopes?: (root: unknown) => void;
};
runtime.__wrnexusHydrateScopes?.(win.document);
expect(root.getAttribute("lang")).toBe("en");
expect(root.getAttribute("data-theme")).toBe("dark");
expect(root.hasAttribute("data-wrn-bind-0")).toBe(false);
expect(root.hasAttribute("data-wrn-bind-1")).toBe(false);
});
test("compiled client functions are not overwritten by fallback behavior parsing", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: `function save() { message = \`fallback\` }`,
watches: [],
lifecycle: {},
}),
).toString("base64");
const win = new Window() as unknown as Window & Record;
win.document.body.innerHTML =
`` +
`{message}
`;
const scope = win.document.getElementById("scope") as unknown as HTMLElement & {
__wrnexusClientModule?: unknown;
};
scope.__wrnexusClientModule = {
bindClientScope(context: { state: Record }) {
return {
save() {
context.state.message = "native";
},
};
},
};
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(0, eval)(REACTIVE_RUNTIME);
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
win.document,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("button")!.textContent).toBe("native");
});
test("$route updates after client navigation", () => {
const win = new Window({ url: "http://localhost/" }) as unknown as Window &
Record;
win.document.body.innerHTML =
` ` +
`{$route.pathname} {$route.params.id}
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(0, eval)(REACTIVE_RUNTIME);
(win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }).__wrnexusHydrateScopes?.(
win.document,
);
expect(win.document.querySelector("strong")!.textContent).toBe("/");
expect(win.document.querySelector("em")!.textContent).toBe("42");
expect(win.document.querySelector("[data-wrn-route-params]")).toBeNull();
win.history.pushState({}, "", "/settings?tab=chat");
win.dispatchEvent(
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
"wrnexus:navigated",
) as unknown as Parameters[0],
);
expect(win.document.querySelector("strong")!.textContent).toBe("/settings");
});
test("cache invalidation refetches matching client Async boundaries", async () => {
let requests = 0;
(globalThis as Record).fetch = () => {
requests++;
return Promise.resolve(Response.json({ data: { value: requests } }));
};
const win = mount(
`` +
`
` +
`Loading ` +
`{uploads.value} ` +
`{error.message} ` +
` `,
);
const runtime = win as unknown as { __wrnexusHydrateAsyncBoundaries?: (root: unknown) => void };
runtime.__wrnexusHydrateAsyncBoundaries?.(win.document);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("b")?.textContent).toBe("1");
win.dispatchEvent(
new (win as unknown as { CustomEvent: typeof CustomEvent }).CustomEvent(
"wrnexus:cache:invalidate",
{ detail: { tags: ["uploads"] } },
) as unknown as Parameters[0],
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(requests).toBe(2);
expect(win.document.querySelector("b")?.textContent).toBe("2");
});
// --- parent-scope ownership across a component boundary --------------------
// Slot content and `@output` handlers on a component tag are authored in the
// PARENT (page) source, but SSR splices them inside the child component's
// [data-scope] root. Without an owner-chain rule the child scope claims them,
// so the page's own functions and state are invisible to markup the page
// itself wrote.
test("slot content is hydrated by the parent scope, not the component it lands in", () => {
const win = mount(
`` +
`
` +
`
` +
`{pageValue} shown ` +
`
`,
);
expect(win.document.querySelector("b")?.textContent).toBe("page");
expect(win.document.querySelector("i")?.getAttribute("data-show")).toBe("true");
});
test("forwarded slots keep the original author through wrapper and modal scopes", () => {
const win = mount(
``,
);
expect(win.document.querySelector("#label")?.textContent).toBe("Audit hours");
expect(win.document.querySelector("input")?.value).toBe("168");
win.document.querySelector("button")!.click();
expect(win.document.querySelector("#original")?.textContent).toBe("169");
});
test("slot event handlers write to the parent scope", () => {
const win = mount(
``,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("1");
});
test("component output handlers run in the parent scope", () => {
const win = mount(
``,
);
const target = win.document.querySelector("[data-wrn-events]") as unknown as {
__wrnexusOutputHandlers?: Record unknown>>;
};
const handlers = target.__wrnexusOutputHandlers?.confirm;
expect(handlers && handlers.size).toBe(1);
handlers!.forEach((handler) => handler({}));
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
});
test("a camelCase output reaches a parent binding despite attribute lowercasing", () => {
/*
* A parent writes @sizeChange; HTML lowercases attribute names, so the
* handler registers under "sizechange" while the component emits
* "sizeChange". The lookup used to miss and fall through to a DOM dispatch,
* so the binding was never invoked and nothing reported an error. Every
* camelCase output in the library was undeliverable -- LayoutSplitter's
* sizeChange, DataTable's pageChange and rowClick, Map's markerClick and
* twelve more. Verified in a browser before and after the fix.
*/
const win = mount(
``,
);
const target = win.document.querySelector("[data-wrn-events]") as unknown as {
__wrnexusOutputHandlers?: Record unknown>>;
};
// The registry is keyed as the DOM gave it: lowercased, not as authored.
expect(Object.keys(target.__wrnexusOutputHandlers ?? {})).toContain("sizechange");
expect(target.__wrnexusOutputHandlers?.sizeChange).toBeUndefined();
// Emitting through the real output proxy, under the camelCase name the
// component actually writes, must still reach the parent.
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
});
test("development runtime allows an output with no parent binding", () => {
const win = new Window() as unknown as Window & Record;
win.document.body.innerHTML = `go
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
(win.document.querySelector("button") as unknown as HTMLElement).click();
(win.document.querySelector("button") as unknown as HTMLElement).click();
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(0);
});
test("an unbound native-named output does not re-enter its DOM handler", () => {
const win = mount(
`` +
`go ` +
`
`,
);
const root = win.document.querySelector("[data-wrn-events]") as unknown as HTMLElement;
let outputs = 0;
root.addEventListener("click", (event) => {
if ((event as Event & { __wrnexusComponentOutput?: boolean }).__wrnexusComponentOutput) {
outputs += 1;
}
});
expect(() =>
(win.document.querySelector("button") as unknown as HTMLElement).click(),
).not.toThrow();
expect(outputs).toBe(1);
});
test("component server calls send the CSRF cookie in the RPC header", async () => {
const win = mount(
`` +
`go ` +
`
`,
);
Object.defineProperty(win.document, "cookie", {
configurable: true,
value: "wrn-csrf=rpc%20token",
});
let request: RequestInit | undefined;
const globals = globalThis as Record;
const originalFetch = globals.fetch;
globals.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
request = init;
return Response.json({ ok: true });
};
try {
(win.document.querySelector("button") as unknown as HTMLElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
globals.fetch = originalFetch;
}
expect(new Headers(request?.headers).get("x-csrf-token")).toBe("rpc token");
});
test("component server calls use the nearest hydration component identity", async () => {
const win = mount(
``,
);
let payload: { component?: string } | undefined;
const globals = globalThis as Record;
const originalFetch = globals.fetch;
globals.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
payload = JSON.parse(String(init?.body));
return Response.json({ ok: true });
};
try {
(win.document.querySelector("button") as unknown as HTMLElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
globals.fetch = originalFetch;
}
expect(payload?.component).toBe("CButton");
});
test("a parent scope does not inherit a child component RPC identity", async () => {
const win = mount(
``,
);
let payload: { component?: string } | undefined;
const globals = globalThis as Record;
const originalFetch = globals.fetch;
globals.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
payload = JSON.parse(String(init?.body));
return Response.json({ ok: true });
};
try {
(win.document.querySelector("#parent-action") as unknown as HTMLElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
globals.fetch = originalFetch;
}
expect(payload?.component).toBe("Home");
});
test("component server calls prefer the rendered CSRF token over stale cookies", async () => {
const win = mount(
` ` +
`` +
`go ` +
`
`,
);
Object.defineProperty(win.document, "cookie", {
configurable: true,
value: "wrn-csrf=stale-token",
});
let request: RequestInit | undefined;
const globals = globalThis as Record;
const originalFetch = globals.fetch;
globals.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
request = init;
return Response.json({ ok: true });
};
try {
(win.document.querySelector("button") as unknown as HTMLElement).click();
await new Promise((resolve) => setTimeout(resolve, 0));
} finally {
globals.fetch = originalFetch;
}
expect(new Headers(request?.headers).get("x-csrf-token")).toBe("current-token");
});
test("development runtime warns when a component binding names a missing function", () => {
const win = new Window() as unknown as Window & Record;
win.document.body.innerHTML =
``;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
const originalError = console.error;
console.warn = (...args: unknown[]) => warnings.push(args);
console.error = () => {};
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
const target = win.document.querySelector("[data-wrn-events]") as unknown as {
__wrnexusOutputHandlers?: Record unknown>>;
};
target.__wrnexusOutputHandlers?.save?.forEach((handler) => handler({ id: 1 }));
target.__wrnexusOutputHandlers?.save?.forEach((handler) => handler({ id: 2 }));
} finally {
console.warn = originalWarn;
console.error = originalError;
}
expect(warnings).toHaveLength(1);
expect(String(warnings[0]?.[0])).toContain("WRN-DEV-BINDING-MISSING");
expect(String(warnings[0]?.[0])).toContain("missingSave");
});
test("development runtime warns for referenced theme tokens absent from rendered CSS", () => {
const win = new Window() as unknown as Window & Record;
win.document.head.innerHTML =
``;
win.document.body.innerHTML = `
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(1);
expect(String(warnings[0]?.[0])).toContain("WRN-DEV-THEME-TOKEN-MISSING");
expect(String(warnings[0]?.[0])).toContain("--wrn-missing-test");
});
test("development runtime accepts component-local and inline wrn variables", () => {
const win = new Window() as unknown as Window & Record;
win.document.head.innerHTML =
``;
win.document.body.innerHTML = `
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(0);
});
test("development runtime ignores fallback and inactive component variables", () => {
const win = new Window() as unknown as Window & Record;
win.document.head.innerHTML =
``;
win.document.body.innerHTML = `
`;
(globalThis as Record).window = win;
(globalThis as Record).document = win.document;
(globalThis as Record).location = win.location;
(globalThis as Record).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(0);
});
test("production runtime strips development diagnostics", () => {
const production = getReactiveRuntime();
expect(production).not.toContain("WRN-DEV-");
expect(production).not.toContain("warnOnce");
expect(REACTIVE_RUNTIME).toContain("__WRNEXUS_DEV_START__");
});
// --- browser globals + regex literals in client expressions ----------------
// Client functions and inline handlers are interpreted by the runtime's own
// eval-free expression engine (so a strict CSP needs no unsafe-eval). Anything
// the engine cannot resolve silently becomes undefined, so an author calling
// an ordinary browser API gets a confusing "not a function" instead of the
// behavior they wrote.
test("client expressions can call browser globals like alert", () => {
const win = mount(
`go
`,
) as unknown as Window & {
alert?: (message: string) => void;
};
const seen: string[] = [];
win.alert = (message: string) => seen.push(String(message));
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(seen).toEqual(["hi"]);
});
test("client expressions resolve JS builtins", () => {
const win = mount(
`` +
`go ` +
`{out} {keys} ` +
`
`,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#n")?.textContent).toBe("42");
expect(win.document.querySelector("#k")?.textContent).toBe("a-b");
});
test("client expressions support regex literals", () => {
const win = mount(
`` +
// String.raw so the backslashes reach the runtime: in a plain template
// literal `\s` collapses to `s` and the test would pass against a
// pattern the author never wrote.
String.raw`go ` +
`{ok}
`,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("yes");
});
test("a regex literal with a division sign nearby still parses as division", () => {
const win = mount(
`go {out}
`,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("2.5");
});
test("a string literal that spells an operator stays a value", () => {
const win = mount(
`` +
`go ` +
`{joined} {first}
`,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#j")?.textContent).toBe("x-y");
expect(win.document.querySelector("#f")?.textContent).toBe("a");
});
// --- toast() ---------------------------------------------------------------
// The runtime raises notifications through a window event and never imports
// the Toaster component, so an app can host toasts however it likes.
test("toast() dispatches a wrnexus:toast event when a toaster is mounted", () => {
const win = mount(
`
` +
`go
`,
) as unknown as Window & { addEventListener: Window["addEventListener"] };
const seen: Record[] = [];
win.addEventListener("wrnexus:toast", ((event: { detail: Record }) => {
seen.push(event.detail);
}) as unknown as Parameters[1]);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(seen).toEqual([{ message: "hi Ada", title: "Done", tone: "success" }]);
});
test("toast() falls back to the console when no toaster is mounted", () => {
const win = mount(
`go
`,
) as unknown as Window & {
addEventListener: Window["addEventListener"];
};
const dispatched: unknown[] = [];
win.addEventListener("wrnexus:toast", ((event: unknown) =>
dispatched.push(event)) as unknown as Parameters[1]);
const original = console.info;
const logged: string[] = [];
console.info = (...args: unknown[]) => void logged.push(args.map(String).join(" "));
try {
(win.document.querySelector("button") as unknown as HTMLElement).click();
} finally {
console.info = original;
}
expect(dispatched).toEqual([]);
expect(logged.join("\n")).toContain("orphan");
});
test("toast is reachable as a real global for compiled client modules", () => {
const win = mount(`
`) as unknown as Window & {
toast?: unknown;
wrnToast?: unknown;
};
expect(typeof win.wrnToast).toBe("function");
expect(typeof win.toast).toBe("function");
});
test("data-show is evaluated inside a data-for row", () => {
const win = mount(
`` +
`
` +
`{row.label} ` +
`` +
` `,
);
const extras = [...win.document.querySelectorAll(".extra")] as unknown as HTMLElement[];
expect(extras.length).toBe(2);
// First row has no extra: hidden. Second row does: visible.
expect(extras[0]!.getAttribute("data-show")).toBe("false");
expect(extras[0]!.style.display).toBe("none");
expect(extras[1]!.getAttribute("data-show")).toBe("true");
expect(extras[1]!.style.display).not.toBe("none");
});
test("data-show in a data-for row can read component scope, not just the item", () => {
const win = mount(
``,
);
const node = win.document.querySelector("b") as unknown as HTMLElement;
expect(node.getAttribute("data-show")).toBe("false");
expect(node.style.display).toBe("none");
});
// --- nested data-for -------------------------------------------------------
// A table is rows containing cells, so a loop inside a loop has to work. The
// inner template used to be left unexpanded (and its mustaches baked against
// the outer item, where the inner name is undefined), so nested rows rendered
// blank.
test("a data-for nested inside a data-for renders its rows", () => {
const win = mount(
`` +
`
` +
`` +
`{cell.v} ` +
` ` +
`
`,
);
const cells = [...win.document.querySelectorAll(".cell")].map((n) => n.textContent);
expect(cells).toEqual(["a1", "b1", "a2", "b2"]);
});
test("a nested loop can read the enclosing item and component scope", () => {
const win = mount(
`` +
`
` +
`{prefix}-{group.name}-{item} ` +
` `,
);
const leaves = [...win.document.querySelectorAll(".leaf")].map((n) => n.textContent);
expect(leaves).toEqual(["x-g1-p", "x-g1-q"]);
});
// A binding inside a loop row must react to state the row's list expression
// never touched. Effects registered during the initial render sweep used to be
// skipped entirely (forEach fixes its range up front), so they never ran, never
// subscribed, and stayed frozen at their first value for the life of the page.
test("a data-show inside a loop reacts to state the list expression never reads", () => {
const win = mount(
`` +
`
` +
`` +
`
` +
`
go ` +
`
`,
);
const hidden = [...win.document.querySelectorAll(".extra")] as unknown as HTMLElement[];
expect(hidden.length).toBe(2);
expect(hidden.every((n) => n.style.display === "none")).toBe(true);
(win.document.querySelector("button") as unknown as HTMLElement).click();
const shown = [...win.document.querySelectorAll(".extra")] as unknown as HTMLElement[];
expect(shown.every((n) => n.style.display !== "none")).toBe(true);
});
test("a checked binding inside a loop toggles with state", () => {
const win = mount(
``,
);
const boxes = () => [...win.document.querySelectorAll(".box")] as unknown as HTMLInputElement[];
expect(boxes().map((b) => b.checked)).toEqual([false, true]);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(boxes().map((b) => b.checked)).toEqual([true, false]);
});
// Rows rebuilt AFTER hydration register their effects outside the initial
// sweep, so nothing ran them: data-show kept its raw expression and the
// element defaulted to visible. Re-sorting a table therefore drew a second
// copy of every header label.
test("data-show still resolves in rows rebuilt after the first render", () => {
const win = mount(
`` +
`
` +
`yes ` +
`
` +
`
swap ` +
`
`,
);
const shown = () =>
([...win.document.querySelectorAll(".yes")] as unknown as HTMLElement[]).map(
(node) => node.style.display !== "none",
);
expect(shown()).toEqual([true, false]);
// New item objects mean brand new clones, hydrated outside the first sweep.
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(shown()).toEqual([false, true]);
});
// data-html is the one directive that does not escape, so it is a separate
// opt-in rather than a flag on data-text.
test("data-html renders markup while data-text escapes it", () => {
const win = mount(
`` +
` ` +
` ` +
`
`,
);
expect(win.document.querySelector("#raw")?.innerHTML).toBe("bold ");
expect(win.document.querySelector("#raw b")?.textContent).toBe("bold");
// data-text must still show the markup as literal characters.
expect(win.document.querySelector("#safe b")).toBeNull();
expect(win.document.querySelector("#safe")?.textContent).toBe("bold ");
});
test("data-html applies inside a data-for row", () => {
const win = mount(
``,
);
const cells = [...win.document.querySelectorAll(".cell")].map((n) => n.innerHTML);
expect(cells).toEqual(["one ", "two "]);
});
// Statement bodies given to the expression engine -- lifecycle hooks, inline
// handlers -- are ordinary authored code and contain comments. The leading
// slash used to fall through to the regex-literal branch and kill the body
// with "Unclosed regular expression".
test("comments are ignored inside interpreted statements", () => {
const win = mount(
`` +
`go ` +
`{total}
`,
);
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(win.document.querySelector("#out")?.textContent).toBe("2");
});
/*
* happy-dom builds its own KeyboardEvent, which is structurally distinct from
* the DOM lib Event that dispatchEvent is typed against. Same cast the window
* dispatches above use, kept in one place.
*/
function keydown(win: Window, key: string): Event {
const ctor = (win as unknown as { KeyboardEvent: new (type: string, init: unknown) => unknown })
.KeyboardEvent;
return new ctor("keydown", { key, bubbles: true }) as unknown as Event;
}
test("roving focus moves with arrow keys and wraps", () => {
const win = mount(
`
A
B
C
`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
const c = doc.querySelector("#c") as unknown as HTMLElement;
expect(a.getAttribute("tabindex")).toBe("0");
expect(doc.querySelector("#b")!.getAttribute("tabindex")).toBe("-1");
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("b");
expect(doc.querySelector("#b")!.getAttribute("tabindex")).toBe("0");
expect(a.getAttribute("tabindex")).toBe("-1");
(doc.querySelector("#b") as unknown as HTMLElement).dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(keydown(win, "ArrowLeft"));
expect(doc.activeElement!.id).toBe("c");
c.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("a");
});
test("roving focus honours Home and End and skips disabled items", () => {
const win = mount(
`
A
B
C
`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(doc.activeElement!.id).toBe("c");
(doc.querySelector("#c") as unknown as HTMLElement).dispatchEvent(keydown(win, "Home"));
expect(doc.activeElement!.id).toBe("a");
a.dispatchEvent(keydown(win, "End"));
expect(doc.activeElement!.id).toBe("c");
});
test("horizontal roving ignores vertical arrows so the page still scrolls", () => {
const win = mount(
`
A
B
`,
);
const a = win.document.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowDown"));
expect(win.document.activeElement!.id).toBe("a");
});
test("roving tabindex starts on the selected item, not the first", () => {
const win = mount(
`
A
B
`,
);
expect(win.document.querySelector("#b")!.getAttribute("tabindex")).toBe("0");
expect(win.document.querySelector("#a")!.getAttribute("tabindex")).toBe("-1");
});
test("nested roving groups do not capture the outer group items", () => {
const win = mount(
``,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("b");
});
test("opening a modal dialog traps Tab inside it", () => {
const win = mount(
``,
);
const doc = win.document;
const last = doc.querySelector("#last") as unknown as HTMLElement;
last.focus();
last.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("first");
});
test("a hidden dialog does not trap Tab", () => {
const win = mount(
``,
);
const doc = win.document;
const outside = doc.querySelector("#outside") as unknown as HTMLElement;
outside.focus();
outside.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("outside");
});
test("an empty or false roving attribute opts the group out entirely", () => {
const win = mount(
`
A
B
`,
);
const doc = win.document;
const a = doc.querySelector("#a") as unknown as HTMLElement;
expect(a.getAttribute("tabindex")).toBeNull();
a.focus();
a.dispatchEvent(keydown(win, "ArrowRight"));
expect(doc.activeElement!.id).toBe("a");
});
test("json inside a textarea is left alone, not read as mustaches", () => {
const win = mount(
`
{count}
`,
);
const doc = win.document;
// The surrounding scope still interpolates normally...
expect(doc.querySelector("#out")!.textContent).toBe("7");
// ...but the textarea holds data, and an expression engine would have
// evaluated {"id":"a-1","count":3} away and left it empty.
expect(doc.querySelector("#editor")!.textContent).toBe('{"id":"a-1","count":3}');
});
test("a closed drawer does not hold the body scroll lock", () => {
const win = mount(
``,
);
const doc = win.document;
/*
* A drawer animates open, so its panel cannot be hidden with data-show --
* display:none is not transitionable. It publishes data-open instead. Reading
* only data-show made every closed drawer look open, which locked the body
* and left the page unscrollable.
*/
expect(doc.body.style.overflow).not.toBe("hidden");
const outside = doc.querySelector("#inside") as unknown as HTMLElement;
outside.focus();
outside.dispatchEvent(keydown(win, "Tab"));
// No trap either: focus is free to leave a closed dialog.
expect(doc.activeElement!.id).toBe("inside");
});
test("an open drawer locks the body scroll and traps Tab", () => {
const win = mount(
`
`,
);
const doc = win.document;
expect(doc.body.style.overflow).toBe("hidden");
const last = doc.querySelector("#last") as unknown as HTMLElement;
last.focus();
last.dispatchEvent(keydown(win, "Tab"));
expect(doc.activeElement!.id).toBe("first");
});
test("splitter keyboard steps move the divider and clamp at the bounds", () => {
const win = mount(
``,
);
const doc = win.document;
const root = doc.querySelector("[data-wrn-splitter]") as unknown as HTMLElement;
const handle = doc.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement;
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("60");
expect(root.style.getPropertyValue("--wrn-split").trim()).toBe("60%");
handle.dispatchEvent(keydown(win, "ArrowLeft"));
expect(handle.getAttribute("aria-valuenow")).toBe("50");
// Home and End go to the bounds, not to 0 and 100: a pane dragged to
// nothing cannot be recovered with a pointer.
handle.dispatchEvent(keydown(win, "Home"));
expect(handle.getAttribute("aria-valuenow")).toBe("20");
handle.dispatchEvent(keydown(win, "End"));
expect(handle.getAttribute("aria-valuenow")).toBe("80");
// Already at the maximum; another step must not exceed it.
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("80");
});
test("a vertical splitter responds to up and down instead", () => {
const win = mount(
``,
);
const handle = win.document.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement;
handle.dispatchEvent(keydown(win, "ArrowDown"));
expect(handle.getAttribute("aria-valuenow")).toBe("55");
// The other axis is left alone so the page can still scroll sideways.
handle.dispatchEvent(keydown(win, "ArrowRight"));
expect(handle.getAttribute("aria-valuenow")).toBe("55");
});
test("splitter announces its new size for the component to re-emit", () => {
const win = mount(
``,
);
const doc = win.document;
const root = doc.querySelector("[data-wrn-splitter]") as unknown as HTMLElement;
const seen: number[] = [];
root.addEventListener("wrnexus:splitter:resize", (event) =>
seen.push((event as CustomEvent).detail.size),
);
(doc.querySelector("[data-wrn-splitter-handle]") as unknown as HTMLElement).dispatchEvent(
keydown(win, "ArrowRight"),
);
expect(seen).toEqual([60]);
});
test("control blocks created by a client rerender render their own content", () => {
// A nested block that arrives with the server HTML is hydrated: its first
// reactive pass must NOT redraw, or it would throw away server DOM. A nested
// block created later by an outer rerender has no server DOM, so skipping its
// first pass leaves it permanently empty — its dependencies never change
// again to trigger a second one.
const inner = Buffer.from(
JSON.stringify([
{ cond: "g.rows.length > 0", body: 'HAS
' },
{ cond: null, body: 'NONE
' },
]),
).toString("base64");
const outer = Buffer.from(
JSON.stringify({
list: "groups",
item: "g",
body:
``,
empty: "",
}),
).toString("base64");
const win = mount(
`` +
`
swap ` +
`
` +
`
` +
`
` +
`
`,
);
win.document.querySelector("button")!.click();
// The outer each rerendered: the new group's heading is present.
expect(win.document.querySelector(".group span")?.textContent).toBe("g2");
// The nested if inside that new row must have rendered its else branch.
expect(win.document.querySelector(".no-rows")?.textContent).toBe("NONE");
expect(win.document.querySelector(".has-rows")).toBeNull();
});
test("a for loop with a declaration initialiser runs in a handler", () => {
const win = mount(
`` +
`go ` +
`0 ` +
`
`,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("span")?.textContent).toBe("6");
});
test("a while loop runs in a handler", () => {
const win = mount(
`` +
`go ` +
`1 ` +
`
`,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("span")?.textContent).toBe("16");
});
test("a declaration stays local instead of becoming reactive state", () => {
// An unknown name reaching writeScope becomes a signal and triggers a render
// sweep. A var inside a function called during a render would then loop
// forever, so declarations must bind locally.
const win = mount(
`` +
`go ` +
`0 ` +
` ` +
`
`,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector(".out")?.textContent).toBe("6");
expect(win.document.querySelector(".leak")?.textContent).toBe("");
});
test("a control block removed from the DOM does not abort later renders", () => {
// Its effect stays in the renderers list. Running it against a detached node
// throws, which would abort the sweep and leave every later effect stale.
const definition = Buffer.from(
JSON.stringify([{ cond: "n < 100", body: ' ' }]),
).toString("base64");
const win = mount(
`` +
` ` +
`go ` +
`0 ` +
`
`,
);
const block = win.document.querySelector("[data-wrn-if]")!;
block.parentNode!.removeChild(block);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("span")?.textContent).toBe("1");
});
test("a declaration statement assigns into scope", () => {
const win = mount(
`` +
`go ` +
`0 ` +
`
`,
);
win.document.querySelector("button")!.click();
expect(win.document.querySelector("span")?.textContent).toBe("6");
});
test("an unbounded loop stops instead of hanging the page", () => {
// Handler source is author-controlled and runs in the browser. Without a cap
// a mistaken condition freezes the tab with no way back.
const win = mount(
`` +
`go ` +
`0 ` +
`
`,
);
win.document.querySelector("button")!.click();
const value = Number(win.document.querySelector("span")?.textContent);
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(
``,
);
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);
});
test("a @focus binding on a component tag fires for a focusable element inside it", () => {
/*
* focus and blur do NOT bubble. The runtime bound every output-named DOM
* fallback listener in the bubble phase, so a parent writing @focus on a
* component tag never heard the component's own input or button take focus:
* the event fired on the descendant and stopped there. Nothing errored --
* the binding simply did nothing, which is why the library still declares
* focus/blur outputs on components that could never deliver them
* (button, TextLink, WysiwygEditor).
*
* The ui ratchet for "outputs nothing emits" excludes natively-named
* outputs on the stated grounds that a native event reaches the root
* anyway. That reasoning holds for click and change, which bubble, and
* fails for focus and blur, which do not.
*/
const win = mount(
``,
);
const inner = win.document.querySelector("#inner") as unknown as {
dispatchEvent: (event: unknown) => boolean;
};
// A real focus event: fires on the descendant, does not bubble.
const FocusEventCtor = (win as unknown as { Event: typeof Event }).Event;
inner.dispatchEvent(new FocusEventCtor("focus", { bubbles: false }));
expect(win.document.querySelector("#out")?.textContent).toBe("focused");
});
test("a @change binding still fires for a bubbling event inside a component", () => {
// The companion case: change DOES bubble, and must keep working unchanged.
const win = mount(
``,
);
const inner = win.document.querySelector("#inner") as unknown as {
dispatchEvent: (event: unknown) => boolean;
};
const EventCtor = (win as unknown as { Event: typeof Event }).Event;
inner.dispatchEvent(new EventCtor("change", { bubbles: true }));
expect(win.document.querySelector("#out")?.textContent).toBe("changed");
});
test("appending keyed notifications does not detach existing rows", () => {
const win = mount(
``,
);
const list = win.document.querySelector("ul")!;
const first = list.querySelector("li");
const observer = new win.MutationObserver(() => {});
observer.observe(list, { childList: true });
(win.document.querySelector("button") as unknown as HTMLElement).click();
expect(list.querySelectorAll("li").length).toBe(2);
expect(observer.takeRecords().flatMap((record) => Array.from(record.removedNodes))).not.toContain(
first,
);
observer.disconnect();
});
test("literal server-rendered JSON survives hydration inside component scopes", () => {
const json = '{"ok":true,"data":{"message":"Test event","sample":"{count}"}}';
const win = mount(
``,
);
expect(win.document.querySelector("pre")?.textContent).toBe(json);
expect(win.document.querySelector("b")?.textContent).toBe("2");
});
test("nested structured prop bindings preserve attachment arrays across slot ownership", async () => {
const files = [{ nodeId: "file", filename: "report.pdf" }];
const encoded = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64");
const win = mount(
`${JSON.stringify(files)} report.pdf
`,
);
await Promise.resolve();
expect(win.document.querySelector("#files")?.textContent).toBe(JSON.stringify(files));
expect(win.document.querySelector("span")?.textContent).toBe("report.pdf");
});
test("dynamic child props receive parent translations and subsequent changes", async () => {
const body =
'Input
';
const definition = Buffer.from(JSON.stringify([{ cond: "open", body }])).toString("base64");
const win = mount(
`Open Change
`,
);
win.document.querySelector("button")!.click();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("label")?.textContent).toBe("Evidence");
win.document.querySelectorAll("button")[1]!.click();
await new Promise((resolve) => setTimeout(resolve, 0));
expect(win.document.querySelector("label")?.textContent).toBe("Updated");
});
test("child props wait for an asynchronous module and retain the latest parent values", async () => {
let resolveModule: (module: object) => void = () => {};
const globals = globalThis as Record;
globals.__wrnDelayedTestModule = new Promise((resolve) => {
resolveModule = resolve;
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnDelayedTestModule",
);
expect(runtime).not.toBe(REACTIVE_RUNTIME);
const win = mount(
'',
runtime,
);
const parent = win.document.querySelector("#parent") as any,
child = win.document.querySelector("#child") as any;
parent.__wrnexusScopeApi.set("value", 7);
expect(child.__wrnexusScopeApi).toBeUndefined();
resolveModule({});
await new Promise((resolve) => setTimeout(resolve, 20));
expect(child.__wrnexusScopeApi.get("value")).toBe(7);
expect(child.querySelector("b").textContent).toBe("7");
parent.__wrnexusScopeApi.set("value", 9);
expect(child.querySelector("b").textContent).toBe("9");
} finally {
delete globals.__wrnDelayedTestModule;
}
});
test("child prop bindings preserve inline objects and mixed text", () => {
const win = mount(
'',
);
const parent = win.document.querySelector("#parent") as any,
child = win.document.querySelector("#child") as any;
expect(child.__wrnexusScopeApi.get("value")).toEqual({});
expect(child.querySelector("b").textContent).toBe("One of Two");
parent.__wrnexusScopeApi.set("answer", { line1: "10 Test Street", country: "India" });
expect(child.__wrnexusScopeApi.get("value")).toEqual({
line1: "10 Test Street",
country: "India",
});
parent.__wrnexusScopeApi.set("first", "Updated");
expect(child.querySelector("b").textContent).toBe("Updated of Two");
});
test("child props retain arrays produced by inline object callbacks", () => {
const win = mount(
'({value:item,label:item}))}"]\'>
',
);
const parent = win.document.querySelector("#parent") as any,
child = win.document.querySelector("#child") as any;
expect(child.__wrnexusScopeApi.get("options")).toEqual([{ value: "a", label: "a" }]);
parent.__wrnexusScopeApi.set("items", ["b", "c"]);
expect(child.__wrnexusScopeApi.get("options")).toEqual([
{ value: "b", label: "b" },
{ value: "c", label: "c" },
]);
});
test("loop hydration preserves structured child prop expressions", () => {
const body =
'
';
const definition = Buffer.from(
JSON.stringify({ list: "items", item: "item", body, empty: "" }),
).toString("base64");
const win = mount(
`
`,
);
const parent = win.document.querySelector("#parent") as any;
parent.__wrnexusScopeApi.set("answers", { address: { line1: "10 Test Street" } });
parent.__wrnexusScopeApi.set("items", [{ id: "address" }]);
expect(win.document.querySelector("b")?.textContent).toBe("10 Test Street");
parent.__wrnexusScopeApi.set("answers", { address: { line1: "Updated" } });
expect(win.document.querySelector("b")?.textContent).toBe("Updated");
});
test("new async component controls render pending props on their first hydration", async () => {
const globals = globalThis as Record;
globals.__wrnPendingRows = Promise.resolve({ bindClientScope: () => ({}) });
try {
const rows = Buffer.from(
JSON.stringify({ list: "fields", item: "field", body: "{field.label} ", empty: "" }),
).toString("base64");
const child = `
`;
const branches = Buffer.from(JSON.stringify([{ cond: "open", body: child }])).toString(
"base64",
);
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnPendingRows",
);
const win = mount(
`
`,
runtime,
);
(win.document.querySelector("#parent") as any).__wrnexusScopeApi.set("open", true);
await new Promise((resolve) => setTimeout(resolve, 30));
expect(win.document.querySelector(".child b")?.textContent).toBe("Section heading");
} finally {
delete globals.__wrnPendingRows;
}
});
test("async nested controls restore structured field props after navigation", async () => {
const globals = globalThis as Record;
globals.__wrnRestoreModule = Promise.resolve({
bindClientScope: (ctx: any) => ({
currentName: () => ctx.props.value?.name || "",
currentConsent: () => !!ctx.props.value?.consent,
}),
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnRestoreModule",
);
const input =
'
';
const signature = `${input}
`;
const fields = Buffer.from(
JSON.stringify({ list: "fields", item: "field", body: signature, empty: "" }),
).toString("base64");
const branch = Buffer.from(
JSON.stringify([
{
cond: "open",
body: ` `,
},
]),
).toString("base64");
const win = mount(
`
`,
runtime,
);
const parent = win.document.querySelector("#parent") as any;
parent.__wrnexusScopeApi.set("answers", {
signature: { name: "Browser Signer", consent: true },
});
parent.__wrnexusScopeApi.set("fields", [{ id: "signature" }]);
parent.__wrnexusScopeApi.set("open", true);
await new Promise((resolve) => setTimeout(resolve, 30));
expect(win.document.querySelector("input")?.value).toBe("Browser Signer");
parent.__wrnexusScopeApi.set("open", false);
await new Promise((resolve) => setTimeout(resolve, 10));
parent.__wrnexusScopeApi.set("open", true);
await new Promise((resolve) => setTimeout(resolve, 30));
expect(win.document.querySelector("input")?.value).toBe("Browser Signer");
} finally {
delete globals.__wrnRestoreModule;
}
});
test("nested page conditions restore structured field props after navigation", () => {
const signature =
'
';
const fields = Buffer.from(
JSON.stringify({ list: "page.fields", item: "field", body: signature, empty: "" }),
).toString("base64");
const branch = Buffer.from(
JSON.stringify([
{
cond: "index === pageIndex",
body: ` `,
},
]),
).toString("base64");
const pages = Buffer.from(
JSON.stringify({
list: "pages",
item: "page",
index: "index",
body: ` `,
empty: "",
}),
).toString("base64");
const win = mount(
`
`,
);
const parent = win.document.querySelector("#parent") as any;
parent.__wrnexusScopeApi.set("answers", { signature: { name: "Browser Signer", consent: true } });
parent.__wrnexusScopeApi.set("pages", [
{ id: "first", fields: [{ id: "signature" }] },
{ id: "last", fields: [] },
]);
expect(win.document.querySelector(".signature b")?.textContent).toBe("Browser Signer");
parent.__wrnexusScopeApi.set("pageIndex", 1);
expect(win.document.querySelector(".signature")).toBeNull();
parent.__wrnexusScopeApi.set("pageIndex", 0);
expect(win.document.querySelector(".signature b")?.textContent).toBe("Browser Signer");
});
test("compiled helpers react to prop changes, including controlled multiple selections", async () => {
const globals = globalThis as Record;
globals.__wrnCompiledPropsModule = Promise.resolve({
bindClientScope: (ctx: any) => ({
selectedA: () => ctx.props.values.includes("a"),
selectedB: () => ctx.props.values.includes("b"),
}),
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnCompiledPropsModule",
);
const win = mount(
'',
runtime,
);
await new Promise((resolve) => setTimeout(resolve, 20));
const parent = win.document.querySelector("#parent") as any;
const values = () =>
Array.from(win.document.querySelector("select")!.selectedOptions).map(
(option) => option.value,
);
expect(values()).toEqual(["a"]);
parent.__wrnexusScopeApi.set("values", ["b"]);
expect(values()).toEqual(["b"]);
parent.__wrnexusScopeApi.set("values", ["a", "b"]);
expect(values()).toEqual(["a", "b"]);
} finally {
delete globals.__wrnCompiledPropsModule;
}
});
test("client control rows expose loop locals for child component bindings", () => {
runWithRequestContext({} as never, () => {
const rows = encodeControlTemplate({
list: "items",
item: "item",
body: '
',
empty: "",
});
const win = mount(
renderControlTemplates() +
`Add
`,
);
win.document.querySelector("button")!.click();
const row = win.document.querySelector(".row")!;
expect(row.textContent).toBe("new-node");
expect(JSON.parse(win.atob(row.getAttribute("data-wrn-loop-locals")!)).item.id).toBe(
"new-node",
);
});
});
test("components inserted by a conditional bind outputs in their parent scope", () => {
const child =
'';
const definition = Buffer.from(JSON.stringify([{ cond: "open", body: child }])).toString(
"base64",
);
const win = mount(
`Toggle {saved}
`,
);
win.document.querySelector("#toggle")!.click();
win.document.querySelectorAll("button")[1]!.click();
expect(win.document.querySelector("#saved")!.textContent).toBe("7");
win.document.querySelector("#toggle")!.click();
win.document.querySelector("#toggle")!.click();
const target = win.document.querySelector("[data-wrn-events]") as any;
expect(target.__wrnexusOutputHandlers.confirm.size).toBe(1);
});
test("forwarded component events use their binding node's nested loop locals", () => {
const outer = Buffer.from(JSON.stringify({ group: "outer" })).toString("base64");
const inner = Buffer.from(JSON.stringify({ group: "outer", row: { id: "nested-row" } })).toString(
"base64",
);
const win = mount(
``,
);
const button = win.document.querySelector("button") as any;
button.click();
expect(win.document.querySelector("#saved")?.textContent).toBe("nested-row");
const parent = win.document.querySelector("[data-text]")?.parentElement as any;
parent.__wrnexusScopeApi.set("saved", "");
for (const handler of button.__wrnexusOutputHandlers.confirm) handler({});
expect(win.document.querySelector("#saved")?.textContent).toBe("nested-row");
});
test("forwarded events created during async child hydration bind in the parent", async () => {
const globals = globalThis as Record;
globals.__wrnAsyncControlModule = Promise.resolve({});
try {
const body = 'Save ';
const definition = Buffer.from(JSON.stringify([{ cond: "active", body }])).toString("base64");
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnAsyncControlModule",
);
const win = mount(
``,
runtime,
);
await new Promise((resolve) => setTimeout(resolve, 20));
win.document.querySelector("button")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("7");
} finally {
delete globals.__wrnAsyncControlModule;
}
});
test("an unhandled nested output cannot overwrite a same-named parent output", () => {
const win = mount(
'',
);
win.document.querySelector("#own")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("7");
win.document.querySelector("#nested")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("7");
});
test("async repeated field components render nested conditional rows after dynamic insertion", async () => {
const globals = globalThis as Record;
globals.__wrnNestedModules = {
"/nested-fields.mjs": Promise.resolve({
bindClientScope: () => ({ visible: (field: any) => !!field }),
}),
"/nested-repeat.mjs": Promise.resolve({
bindClientScope: (ctx: any) => ({
currentRows: () => ctx.state.rows || [],
add: () => {
ctx.state.rows = [{ id: 0, values: {} }];
},
}),
}),
};
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64");
try {
const innerIf = encode([
{ cond: "visible(field)", body: '{field.label} ' },
]);
const innerRows = encode({
list: "fields",
item: "field",
body: ` `,
empty: "",
});
const inner = `
`;
const entries = encode({ list: "currentRows()", item: "row", body: inner, empty: "" });
const repeat = `Add
`;
const fields = encode({ list: "fields", item: "field", body: repeat, empty: "" });
const root = `
`;
const branch = encode([{ cond: "open", body: root }]);
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnNestedModules[url]",
);
const win = mount(
`
`,
runtime,
);
const parent = win.document.querySelector("#parent") as any;
parent.__wrnexusScopeApi.set("fields", [
{
id: "group",
fields: [
{ id: "age", label: "Group age" },
{ id: "name", label: "Group member" },
],
},
]);
parent.__wrnexusScopeApi.set("open", true);
await new Promise((resolve) => setTimeout(resolve, 50));
win.document.querySelector(".add-entry")!.click();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(
Array.from(win.document.querySelectorAll(".entry-label")).map((label) => label.textContent),
).toEqual(["Group age", "Group member"]);
} finally {
delete globals.__wrnNestedModules;
}
});
test("request-registry behavior references hydrate isolated component state", () => {
const behavior = {
functions: "function increment(){count++}",
outputs: [],
computed: [],
effects: [],
lifecycle: {},
watches: [],
};
const markup =
'Increment
';
const win = mount(
'" +
markup +
markup,
);
const buttons = win.document.querySelectorAll("button");
buttons[0]!.click();
expect([...win.document.querySelectorAll("b")].map((node) => node.textContent)).toEqual([
"1",
"0",
]);
buttons[1]!.click();
buttons[1]!.click();
expect([...win.document.querySelectorAll("b")].map((node) => node.textContent)).toEqual([
"1",
"2",
]);
});
test("split controllers observe drawers opened after initial hydration", async () => {
const win = new Window({ settings: { enableJavaScriptEvaluation: true } });
win.document.body.innerHTML = ``;
win.eval(getReactiveRuntime(true));
win.eval(getComponentControllerRuntime(true));
(win as unknown as { __wrnexusHydrateScopes: (node: unknown) => void }).__wrnexusHydrateScopes(
win.document,
);
(win.document.querySelector("#opener") as HappyButtonElement).focus();
(win.document.querySelector("#opener") as HappyButtonElement).click();
for (let attempt = 0; attempt < 30 && win.document.activeElement?.id !== "closer"; attempt++)
await new Promise((resolve) => setTimeout(resolve, 10));
expect(win.document.activeElement?.id).toBe("closer");
expect(win.document.body.style.overflow).toBe("hidden");
(win.document.querySelector("#closer") as HappyButtonElement).click();
for (let attempt = 0; attempt < 30 && win.document.activeElement?.id !== "opener"; attempt++)
await new Promise((resolve) => setTimeout(resolve, 10));
expect(win.document.querySelector("[data-wrn-dialog]")?.getAttribute("data-open")).toBe("false");
expect(win.document.activeElement?.id).toBe("opener");
expect(win.document.body.style.overflow).toBe("");
expect(getReactiveRuntime()).toContain(
"controllerUrl.search = new URL(reactiveScript.src).search",
);
});
test("shared scope and loop metadata preserves per-instance state and row values", () => {
const values = [{ label: "start", count: 0 }, { entry: { label: "Selected row" } }];
const node =
'Choose Add
';
const win = mount(
'" +
node +
node,
);
(win.document.querySelector(".choose") as HappyButtonElement).click();
(win.document.querySelector(".add") as HappyButtonElement).click();
expect([...win.document.querySelectorAll("span")].map((el) => el.textContent)).toEqual([
"Selected row",
"start",
]);
expect([...win.document.querySelectorAll("b")].map((el) => el.textContent)).toEqual(["1", "0"]);
});