feat: release design system engine, studio and component contracts

This commit is contained in:
Clintchiz committed 2026-10-19 13:09:03 +05:30
1 parent ff1065fb27
commit 3943b946d6
693 files changed
+175546 -3894

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.44",
"version": "0.8.74",
"type": "module",
"main": "src/index.ts",
"exports": {
+8 -3
View File
@@ -73,9 +73,13 @@ function stripControllerSections(source: string): string {
const CONTROLLER_LOADER = String.raw`
var componentControllerSelector = "[data-wrn-anchored],[data-wrn-dialog],[data-wrn-roving],[data-wrn-scrollspy],[data-wrn-splitter],[data-wrn-navbar],[data-wrn-preferences],[data-wrn-select],[data-wrn-pin-input]";
var componentControllerPromise = null;
var componentControllerUrl = (document.currentScript && document.currentScript.src)
? new URL("./controllers.js", document.currentScript.src).href
: "/__wrnexus/controllers.js";
var reactiveScript = document.currentScript || document.querySelector('script[src*="/__wrnexus/reactive.js"]');
var componentControllerUrl = "/__wrnexus/controllers.js";
if (reactiveScript && reactiveScript.src) {
var controllerUrl = new URL("./controllers.js", reactiveScript.src);
controllerUrl.search = new URL(reactiveScript.src).search;
componentControllerUrl = controllerUrl.href;
}
function needsComponentControllers(root) {
var host = root || document;
return !!((host.matches && host.matches(componentControllerSelector)) ||
@@ -166,6 +170,7 @@ ${sections}
setupRovingFocus();
setupScrollspy();
setupSplitters();
startDocumentWatch();
window.__wrnexusComponentControllers = { hydrate: hydrate, reposition: repositionAnchored };
hydrate(document);
})();
+7 -1
View File
@@ -285,6 +285,11 @@ export const NAV_RUNTIME = String.raw`
return loaded;
}
// Track the original document and scripts explicitly scheduled by navigation.
// Imported page script nodes are inert; seeing them in the swapped DOM does
// not mean their code has run.
var loadedNavigationScripts = loadedScriptPaths();
function runtimeIds(root) {
var ids = {};
if (!root || !root.querySelectorAll) return ids;
@@ -330,7 +335,7 @@ export const NAV_RUNTIME = String.raw`
* currently loaded.
*/
function ensureScripts(doc) {
var loaded = loadedScriptPaths();
var loaded = loadedNavigationScripts;
doc
.querySelectorAll("script[src]")
@@ -683,6 +688,7 @@ export const NAV_RUNTIME = String.raw`
return fetch(url, {
headers: {
"x-wrnexus-nav": "1",
"x-wrnexus-behaviors": "registry-v2",
...(isPrefetch ? { "x-wrnexus-prefetch": "1" } : {}),
accept: "text/html",
},
File diff suppressed because it is too large. Load diff
+50
View File
@@ -90,3 +90,53 @@ test("loop locals survive non-ASCII values", () => {
const decoded = JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
expect(decoded.row.label).toBe("Ünïcode — 日本語");
});
test("replaced loop rows release their native effects and stop observing state", () => {
const win = mount(
`<div id="owner" data-scope="items: [1], value: 0"><p class="row" data-for="item in items" data-wrn-bind-title='["title","{value}"]'>{item}</p></div>`,
);
const owner = win.document.querySelector("#owner") as any;
const removed: any[] = [];
for (let i = 1; i <= 20; i++) {
const row = win.document.querySelector(".row") as any;
removed.push(row);
owner.__wrnexusScopeApi.set("items", [i + 1]);
owner.__wrnexusScopeApi.set("value", i);
expect(win.document.querySelector(".row")!.getAttribute("title")).toBe(String(i));
expect(row.getAttribute("title")).toBe(String(i - 1));
expect(row.__wrnexusReactiveDisposers?.size ?? 0).toBe(0);
}
expect(removed.every((row) => !row.isConnected)).toBe(true);
});
test("removing nested rows disposes their loop markers and descendant effects", () => {
const win = mount(
`<div id="owner" data-scope="groups: [{items:[1,2]}], value: 0"><section class="group" data-for="group in groups"><p class="row" data-for="item in group.items" data-wrn-bind-title='["title","{value}"]'>{item}</p></section></div>`,
);
const owner = win.document.querySelector("#owner") as any;
const group = win.document.querySelector(".group") as any;
const rows = Array.from(group.querySelectorAll(".row")) as any[];
owner.__wrnexusScopeApi.set("groups", []);
owner.__wrnexusScopeApi.set("value", 4);
expect(rows.map((row) => row.getAttribute("title"))).toEqual(["0", "0"]);
const walker = win.document.createTreeWalker(group, (win as any).NodeFilter.SHOW_ALL);
let node: any = group;
do {
expect(node.__wrnexusReactiveDisposers?.size ?? 0).toBe(0);
} while ((node = walker.nextNode()) !== null);
});
test("moving a mounted scope keeps its listeners and state alive", async () => {
const win = mount(
`<div id="owner" data-scope="count: 0"><button data-on-click="count++">Increment</button><span data-text="count"></span></div>`,
);
const owner = win.document.querySelector("#owner") as any;
const button = owner.querySelector("button");
button.click();
expect(owner.querySelector("span").textContent).toBe("1");
win.document.body.appendChild(owner);
await new Promise((resolve) => setTimeout(resolve, 0));
button.click();
expect(owner.querySelector("span").textContent).toBe("2");
expect(owner.__wrnexusScopeApi.get("count")).toBe(2);
});
+27 -7
View File
@@ -58,6 +58,20 @@ beforeEach(() => {
}
});
test("loads a new page script even when its inert tag is copied inside app content", async () => {
install('<div id="app"><a href="/report" id="lnk">Report</a></div>', (window) => {
window.happyDOM.settings.disableJavaScriptFileLoading = true;
});
nextHtml =
'<html><body><div id="app"><button>Download</button><script type="module" src="/reports.js?v=1"></script></div></body></html>';
win.document.getElementById("lnk").click();
await flush();
expect(win.document.body.querySelectorAll('script[src="/reports.js?v=1"]')).toHaveLength(2);
win.__wrnexusNavigate("/report?environment=live");
await flush();
expect(win.document.body.querySelectorAll('script[src="/reports.js?v=1"]')).toHaveLength(2);
});
test("intercepts an internal link click and swaps #app in place", async () => {
install(`<div id="app"><h1>Home</h1><a href="/about" id="lnk">About</a></div>`);
nextHtml =
@@ -84,10 +98,12 @@ test("fragment history navigation preserves the page without fetching", async ()
test("navigation supplies TrustedHTML to the document parser", async () => {
let parses = 0;
install('<div id="app"><a href="/next" id="lnk">Next</a></div>', (window) => {
window.trustedTypes = { createPolicy: (_name: string, rules: any) => ({
createHTML: (value: string) => ({ trusted: true, value: rules.createHTML(value) }),
createScriptURL: rules.createScriptURL,
}) };
window.trustedTypes = {
createPolicy: (_name: string, rules: any) => ({
createHTML: (value: string) => ({ trusted: true, value: rules.createHTML(value) }),
createScriptURL: rules.createScriptURL,
}),
};
const Parser = window.DOMParser;
(globalThis as any).DOMParser = class {
parseFromString(value: any, type: string) {
@@ -105,13 +121,17 @@ test("navigation supplies TrustedHTML to the document parser", async () => {
});
test("navigation replaces document control registries before hydration", async () => {
install('<div id="app"><a href="/next" id="lnk">Next</a></div><script type="application/json" data-wrn-controls id="old">[]</script>');
install(
'<div id="app"><a href="/next" id="lnk">Next</a></div><script type="application/json" data-wrn-controls id="old">[]</script>',
);
const importNode = win.document.importNode.bind(win.document);
win.document.importNode = (node: any, deep: boolean) => {
if (node.nodeName === "SCRIPT") throw new Error("Script text must not be cloned under Trusted Types");
if (node.nodeName === "SCRIPT")
throw new Error("Script text must not be cloned under Trusted Types");
return importNode(node, deep);
};
nextHtml = '<html><body><div id="app">Next</div><script type="application/json" data-wrn-controls id="new">[[{"cond":null,"body":"Ready"}]]</script><script data-wrn-controls id="executable">window.bad = true</script></body></html>';
nextHtml =
'<html><body><div id="app">Next</div><script type="application/json" data-wrn-controls id="new">[[{"cond":null,"body":"Ready"}]]</script><script data-wrn-controls id="executable">window.bad = true</script></body></html>';
win.document.getElementById("lnk").click();
await flush();
expect(win.document.getElementById("old")).toBeNull();
+684 -17
View File
@@ -1,9 +1,87 @@
import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom";
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(
`<div data-scope="choice: 'field'"><select data-wrn-bind-0='["value","{choice}"]'><option value="">Choose</option><option value="field">Field</option><option value="form">Form</option></select><button data-on-click="choice = 'form'">Change</button></div>`,
);
const select = win.document.querySelector("select")!;
// Native option reconciliation can reset selection after the value binding.
select.innerHTML =
'<option value="">Choose</option><option value="field">Field</option><option value="form">Form</option>';
await new Promise((resolve) => setTimeout(resolve, 0));
expect(select.value).toBe("field");
select.innerHTML =
'<option value="">Choose</option><option value="field">Field</option><option value="form">Form</option>';
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: '<input name="answer" />' }]),
).toString("base64");
const win = mount(
`<div data-scope="count: 0"><button data-on-click="count++">Update</button><template data-wrn-if="${definition}"></template><input name="answer" /><template data-wrn-control-end></template></div>`,
);
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: '<p class="row">{index}:{item}</p>',
empty: '<p class="empty">Empty</p>',
});
const inner = encodeControlTemplate([
{
cond: "active",
body: `<template data-wrn-each="${rows}"></template><p>Stale SSR row</p><template data-wrn-control-end></template>`,
},
]);
const child = `<div data-scope="active: true, items: ['a','b']"><button data-on-click="items = []">Clear</button><template data-wrn-if="${inner}"></template><p>Stale SSR branch</p><template data-wrn-control-end></template></div>`;
const outer = encodeControlTemplate([
{ cond: "open", body: child },
{ cond: null, body: "<i>Closed</i>" },
]);
const win = mount(
renderControlTemplates() +
`<div data-scope="open: false"><button data-on-click="open = !open">Toggle</button><template data-wrn-if="${outer}"></template><i>Closed</i><template data-wrn-control-end></template></div>`,
);
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 {
@@ -26,6 +104,18 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
(globalThis as Record<string, unknown>).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
(globalThis as Record<string, unknown>).HTMLOptionElement = (
win as unknown as { HTMLOptionElement: unknown }
).HTMLOptionElement;
(globalThis as Record<string, unknown>).HTMLInputElement = (
win as unknown as { HTMLInputElement: unknown }
).HTMLInputElement;
(globalThis as Record<string, unknown>).HTMLTextAreaElement = (
win as unknown as { HTMLTextAreaElement: unknown }
).HTMLTextAreaElement;
(globalThis as Record<string, unknown>).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
@@ -35,10 +125,22 @@ function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Wind
return win as unknown as Window;
}
const REPLACED_GLOBALS = ["window", "document", "location", "fetch", "MutationObserver"];
const REPLACED_GLOBALS = [
"window",
"document",
"location",
"fetch",
"MutationObserver",
"HTMLOptionElement",
"HTMLInputElement",
"HTMLTextAreaElement",
"HTMLSelectElement",
];
test("optional properties and nullish fallbacks hydrate without crashing", () => {
const win = mount(`<div data-scope="records: null, row: { count: 0 }, items: ['ok']"><b data-text="records?.rows?.length ?? 0"></b><i data-text="row?.count ?? 5"></i><em data-text="items?.[0] ?? 'empty'"></em><u data-text="!records?.rows?.length"></u></div>`);
const win = mount(
`<div data-scope="records: null, row: { count: 0 }, items: ['ok']"><b data-text="records?.rows?.length ?? 0"></b><i data-text="row?.count ?? 5"></i><em data-text="items?.[0] ?? 'empty'"></em><u data-text="!records?.rows?.length"></u></div>`,
);
expect(win.document.querySelector("b")?.textContent).toBe("0");
expect(win.document.querySelector("i")?.textContent).toBe("0");
expect(win.document.querySelector("em")?.textContent).toBe("ok");
@@ -135,6 +237,24 @@ test("@event (data-on-click) mutates a signal and re-renders", () => {
expect(btn.textContent).toBe("2");
});
test("new conditional content preserves nested component field state", async () => {
const definition = Buffer.from(
JSON.stringify([
{
cond: "open",
body: '<div data-scope="label: &quot;Evidence&quot;, name: &quot;evidenceReference&quot;"><label data-text="label">Evidence</label><input name="{name}" data-wrn-bind-0="[&quot;name&quot;,&quot;{name}&quot;]"></div>',
},
]),
).toString("base64");
const win = mount(
`<div data-scope="open: false"><button data-on-click="open = true">Open</button><template data-wrn-if="${definition}"></template><template data-wrn-control-end></template></div>`,
);
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([
@@ -159,11 +279,19 @@ test("compiled if blocks switch branches after hydration", () => {
test("registry controls retain nested markup and work after the registry leaves the document", () => {
const definitions = [
[{ cond: "open", body: '<template data-wrn-if="@registry:1"></template><b>Nested</b><template data-wrn-control-end></template>' }, { cond: null, body: '<i>Closed</i>' }],
[{ cond: null, body: '<b>Nested</b>' }],
[
{
cond: "open",
body: '<template data-wrn-if="@registry:1"></template><b>Nested</b><template data-wrn-control-end></template>',
},
{ cond: null, body: "<i>Closed</i>" },
],
[{ cond: null, body: "<b>Nested</b>" }],
];
const win = mount(`<script type="application/json" data-wrn-controls id="registry">${JSON.stringify(definitions)}</script>` +
'<div data-scope="open: false"><button data-on-click="open = !open">Toggle</button><template data-wrn-if="@registry:0"></template><i>Closed</i><template data-wrn-control-end></template></div>');
const win = mount(
`<script type="application/json" data-wrn-controls id="registry">${JSON.stringify(definitions)}</script>` +
'<div data-scope="open: false"><button data-on-click="open = !open">Toggle</button><template data-wrn-if="@registry:0"></template><i>Closed</i><template data-wrn-control-end></template></div>',
);
win.document.getElementById("registry")!.remove();
win.document.querySelector("button")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("Nested");
@@ -294,6 +422,44 @@ test("nested scopes don't clobber each other (regression)", () => {
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<string, unknown>;
globals.__wrnSelectParentModule = new Promise<object>((resolve) => {
resolveModule = resolve;
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnSelectParentModule",
);
const win = mount(
`<div id="parent" data-scope="value: 'parent'" data-wrn-client-module="/select-parent-delayed.mjs"><div id="child" data-scope="value: 'personal'"><select data-wrn-select><option value="personal">Personal</option></select></div></div>`,
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(
`<div data-scope="n: 3"><strong data-text="n * 3">?</strong><button data-on-click="n = 5">x</button></div>`,
@@ -786,6 +952,16 @@ test("slot content is hydrated by the parent scope, not the component it lands i
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(
`<div data-scope="label: 'Audit hours', hours: 168"><b id="original">{hours}</b><div data-scope="label: 'wrapper', hours: 1"><div data-scope="label: 'modal', hours: 2"><wrn-slot data-wrn-slot><section><wrn-slot data-wrn-slot><span id="label">{label}</span><input data-wrn-bind-0='["value","{hours}"]' /><button data-on-click="hours++">Change</button></wrn-slot></section></wrn-slot></div></div></div>`,
);
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(
`<div data-scope="count: 0">` +
@@ -1914,15 +2090,506 @@ test("a @change binding still fires for a bubbling event inside a component", ()
expect(win.document.querySelector("#out")?.textContent).toBe("changed");
});
test("appending keyed notifications does not detach existing rows", () => {
const win = mount(`<div data-scope="rows: [{id: 1, name: 'first'}]"><ul><li data-for="row in rows key row.id">{row.name}</li></ul><button data-on-click="rows = [...rows, {id: 2, name: 'second'}]">add</button></div>`);
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();
const win = mount(
`<div data-scope="rows: [{id: 1, name: 'first'}]"><ul><li data-for="row in rows key row.id">{row.name}</li></ul><button data-on-click="rows = [...rows, {id: 2, name: 'second'}]">add</button></div>`,
);
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(
`<section data-scope="count: 2"><pre data-wrn-literal><code>${json}</code></pre><b>{count}</b></section>`,
);
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(
`<div data-scope="data: {}" data-wrn-scope="${encoded({ data: { files } })}"><div data-scope="data: {}" data-wrn-scope="${encoded({ data: { files } })}" data-wrn-prop-bind-0='["data","{data}"]'><div data-scope="title: &#39;Card&#39;"><div data-wrn-slot><div data-scope="files: []" data-wrn-scope="${encoded({ files })}" data-wrn-prop-bind-0='["files","{data.files}"]'><b id="files" data-text="JSON.stringify(files)">${JSON.stringify(files)}</b><span data-text="files[0]?.filename">report.pdf</span></div></div></div></div></div>`,
);
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 =
'<div data-scope="label: &quot;Input&quot;" data-wrn-prop-bind-0="[&quot;label&quot;,&quot;{messages.evidence}&quot;]"><label data-text="label">Input</label></div>';
const definition = Buffer.from(JSON.stringify([{ cond: "open", body }])).toString("base64");
const win = mount(
`<div data-scope='open: false, messages: {evidence: "Evidence"}'><button data-on-click="open = true">Open</button><button data-on-click="messages = {evidence: 'Updated'}">Change</button><template data-wrn-if="${definition}"></template><template data-wrn-control-end></template></div>`,
);
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<string, unknown>;
globals.__wrnDelayedTestModule = new Promise<object>((resolve) => {
resolveModule = resolve;
});
try {
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnDelayedTestModule",
);
expect(runtime).not.toBe(REACTIVE_RUNTIME);
const win = mount(
'<div id="parent" data-scope="value: 1"><div id="child" data-scope="value: 0" data-wrn-client-module="/delayed.mjs" data-wrn-prop-bind-0=\'["value","{value}"]\'><b data-text="value">0</b></div></div>',
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(
'<div id="parent" data-scope="answer: null, first: \'One\', second: \'Two\'"><div id="child" data-scope="value: null, label: \'\'" data-wrn-prop-bind-0=\'["value","{answer||{}}"]\' data-wrn-prop-bind-1=\'["label","{first} of {second}"]\'><b data-text="label"></b></div></div>',
);
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(
'<div id="parent" data-scope="items: [\'a\']"><div id="child" data-scope="options: []" data-wrn-prop-bind-0=\'["options","{items.map(item=>({value:item,label:item}))}"]\'></div></div>',
);
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 =
'<div data-scope="value: {}" data-wrn-prop-bind-0=\'["value","{answers[item.id]||{}}"]\'><b data-text="value.line1"></b></div>';
const definition = Buffer.from(
JSON.stringify({ list: "items", item: "item", body, empty: "" }),
).toString("base64");
const win = mount(
`<div id="parent" data-scope="items: [], answers: {}"><template data-wrn-each="${definition}"></template><template data-wrn-control-end></template></div>`,
);
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<string, unknown>;
globals.__wrnPendingRows = Promise.resolve({ bindClientScope: () => ({}) });
try {
const rows = Buffer.from(
JSON.stringify({ list: "fields", item: "field", body: "<b>{field.label}</b>", empty: "" }),
).toString("base64");
const child = `<div class="child" data-scope="fields: []" data-wrn-client-module="/pending.mjs" data-wrn-prop-bind-0='["fields","{fields}"]'><template data-wrn-each="${rows}"></template><template data-wrn-control-end></template></div>`;
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(
`<div id="parent" data-scope="open: false, fields: [{label:'Section heading'}]"><template data-wrn-if="${branches}"></template><template data-wrn-control-end></template></div>`,
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<string, unknown>;
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 =
'<div class="input" data-scope="value: \'\'" data-wrn-client-module="/input.mjs" data-wrn-prop-bind-0=\'["value","{currentName()}"]\'><input data-wrn-bind-0=\'["value","{value}"]\' /></div>';
const signature = `<div class="signature" data-scope="value: {}" data-wrn-client-module="/signature.mjs" data-wrn-prop-bind-0='["value","{answers[field.id] || {}}"]'>${input}</div>`;
const fields = Buffer.from(
JSON.stringify({ list: "fields", item: "field", body: signature, empty: "" }),
).toString("base64");
const branch = Buffer.from(
JSON.stringify([
{
cond: "open",
body: `<template data-wrn-each="${fields}"></template><template data-wrn-control-end></template>`,
},
]),
).toString("base64");
const win = mount(
`<div id="parent" data-scope="fields: [], answers: {}, open: false"><template data-wrn-if="${branch}"></template><template data-wrn-control-end></template></div>`,
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 =
'<div class="signature" data-scope="field: {}, value: {}" data-wrn-prop-bind-0=\'["field","{field}"]\' data-wrn-prop-bind-1=\'["value","{answers[field.id]||{}}"]\'><b data-text="value.name"></b></div>';
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: `<template data-wrn-each="${fields}"></template><template data-wrn-control-end></template>`,
},
]),
).toString("base64");
const pages = Buffer.from(
JSON.stringify({
list: "pages",
item: "page",
index: "index",
body: `<template data-wrn-if="${branch}"></template><template data-wrn-control-end></template>`,
empty: "",
}),
).toString("base64");
const win = mount(
`<div id="parent" data-scope="pages: [], answers: {}, pageIndex: 0"><template data-wrn-each="${pages}"></template><template data-wrn-control-end></template></div>`,
);
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<string, unknown>;
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(
'<div id="parent" data-scope="values: [\'a\']"><div id="child" data-scope="values: []" data-wrn-client-module="/props.mjs" data-wrn-prop-bind-0=\'["values","{values}"]\'><select multiple><option value="a" data-wrn-bind-0=\'["selected","{selectedA()}"]\'>A</option><option value="b" data-wrn-bind-0=\'["selected","{selectedB()}"]\'>B</option></select></div></div>',
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: '<div class="row"><span data-text="item.id"></span></div>',
empty: "",
});
const win = mount(
renderControlTemplates() +
`<div data-scope="items: []"><button data-on-click="items = [{ id: 'new-node' }]">Add</button><template data-wrn-each="${rows}"></template><template data-wrn-control-end></template></div>`,
);
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 =
'<div data-scope="count: 0" data-wrn-hydration="Control:test"><div data-wrn-events="confirm" data-wrn-out-confirm="saved = payload.value"><button data-on-click="output.confirm({value: 7})">Confirm</button></div></div>';
const definition = Buffer.from(JSON.stringify([{ cond: "open", body: child }])).toString(
"base64",
);
const win = mount(
`<div data-scope="open: false, saved: 0"><button id="toggle" data-on-click="open = !open">Toggle</button><span id="saved">{saved}</span><template data-wrn-if="${definition}"></template><template data-wrn-control-end></template></div>`,
);
win.document.querySelector<HappyButtonElement>("#toggle")!.click();
win.document.querySelectorAll("button")[1]!.click();
expect(win.document.querySelector("#saved")!.textContent).toBe("7");
win.document.querySelector<HappyButtonElement>("#toggle")!.click();
win.document.querySelector<HappyButtonElement>("#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(
`<div data-scope="saved: ''"><span id="saved" data-text="saved"></span><div data-scope="unused: 0" data-wrn-loop-locals="${outer}"><span data-wrn-events="click,confirm"><button data-wrn-events="click,confirm" data-wrn-loop-locals="${inner}" data-wrn-out-click="saved = row.id" data-wrn-out-confirm="saved = row.id">Save</button></span></div></div>`,
);
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<string, unknown>;
globals.__wrnAsyncControlModule = Promise.resolve({});
try {
const body = '<button data-wrn-events="click" data-wrn-out-click="saved = 7">Save</button>';
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(
`<div data-scope="saved: 0"><b data-text="saved"></b><div data-scope="active: true" data-wrn-client-module="/async-control.mjs"><template data-wrn-if="${definition}" data-wrn-control-client></template><template data-wrn-control-end></template></div></div>`,
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(
'<div data-scope="saved: 0"><b data-text="saved"></b><div data-scope="unused: 0"><div data-wrn-events="change" data-wrn-out-change="saved = payload.value"><button id="own" data-on-click="output.change({value: 7})">Own</button><div data-scope="unused: 0"><div data-wrn-events="change"><button id="nested" data-on-click="output.change({value: 99})">Nested</button></div></div></div></div></div>',
);
win.document.querySelector<HappyButtonElement>("#own")!.click();
expect(win.document.querySelector("b")?.textContent).toBe("7");
win.document.querySelector<HappyButtonElement>("#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<string, unknown>;
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: '<label class="entry-label">{field.label}</label>' },
]);
const innerRows = encode({
list: "fields",
item: "field",
body: `<template data-wrn-if="${innerIf}"></template><template data-wrn-control-end></template>`,
empty: "",
});
const inner = `<div class="entry-fields" data-scope="fields: []" data-wrn-client-module="/nested-fields.mjs" data-wrn-prop-bind-0='["fields","{field.fields}"]'><template data-wrn-each="${innerRows}"></template><template data-wrn-control-end></template></div>`;
const entries = encode({ list: "currentRows()", item: "row", body: inner, empty: "" });
const repeat = `<div class="repeat" data-scope="field: {}, rows: null" data-wrn-client-module="/nested-repeat.mjs" data-wrn-prop-bind-0='["field","{field}"]'><template data-wrn-each="${entries}"></template><template data-wrn-control-end></template><button class="add-entry" data-on-click="add()">Add</button></div>`;
const fields = encode({ list: "fields", item: "field", body: repeat, empty: "" });
const root = `<div data-scope="fields: []" data-wrn-client-module="/nested-fields.mjs" data-wrn-prop-bind-0='["fields","{fields}"]'><template data-wrn-each="${fields}"></template><template data-wrn-control-end></template></div>`;
const branch = encode([{ cond: "open", body: root }]);
const runtime = REACTIVE_RUNTIME.replace(
"promise = import(url)",
"promise = globalThis.__wrnNestedModules[url]",
);
const win = mount(
`<div id="parent" data-scope="open: false, fields: []"><template data-wrn-if="${branch}"></template><template data-wrn-control-end></template></div>`,
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<HappyButtonElement>(".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 =
'<div data-scope="count: 0" data-wrn-behavior="@behaviors:0"><button data-on-click="increment()">Increment</button><b data-text="count"></b></div>';
const win = mount(
'<script type="application/json" data-wrn-controls id="behaviors">' +
JSON.stringify([behavior]) +
"</script>" +
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 = `<div data-scope="open: false"><button id="opener" data-on-click="open=true">Open</button><div data-wrn-dialog data-open="false" data-wrn-bind-0="[&quot;data-open&quot;,&quot;{open ? 'true' : 'false'}&quot;]"><section role="dialog" aria-modal="true"><button id="closer" data-on-click="open=false">Close</button></section></div></div>`;
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 =
'<div data-scope="" data-wrn-scope="@metadata:0"><button class="choose" data-wrn-loop-locals="@metadata:1" data-on-click="label = entry.label">Choose</button><button class="add" data-on-click="count++">Add</button><span data-text="label"></span><b data-text="count"></b></div>';
const win = mount(
'<script type="application/json" data-wrn-controls id="metadata">' +
JSON.stringify(values) +
"</script>" +
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"]);
});