import { test, expect, beforeEach } from "bun:test"; import { Window } from "happy-dom"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; import { getComponentControllerRuntime, getReactiveRuntime } from "../src/index.ts"; import { mountHtml } from "@wrnexus/test"; import { restoreGlobalsAfterAll } from "./global-restore.ts"; // Fresh DOM per test, with the runtime's globals bound. function mount(html: string, runtime = REACTIVE_RUNTIME, controllers = ""): Window { 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; (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"]; 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( `
`, 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( `
` + ``, ); 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
` + `` + `` + `
`, ); 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( `
`, ); const btn = win.document.querySelector("button")!; expect(btn.textContent).toBe("0"); btn.click(); btn.click(); expect(btn.textContent).toBe("2"); }); 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( `
` + `` + `` + `

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("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( `
` + `` + `` + `

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( `
`, ); 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( `
` + `
`, ); 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( `
`, ); 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("data-text binds an element's textContent to an expression", () => { const win = mount( `
?
`, ); 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( `
  • {i}:{r.name}
`, ); 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( `
  • {row.name}
`, ); 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( `
A
B
`, ); 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( `
Administrator secret
Guest dashboard
`, ); 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( `
`, ); 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( `
` + `` + `
` + `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( `
` + `` + `
` + `` + `
`, ); 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( `
`, ); 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( `
{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( `
` + `
`, ); 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( `
` + `` + `
`, ); 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 = `
` + `
`; 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( `
` + `
` + `` + `` + `` + `
`, ); 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("slot event handlers write to the parent scope", () => { const win = mount( `
` + `{count}` + `
` + `
` + `` + `
`, ); (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( `
` + `{saved}` + `
` + `
` + `
`, ); 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( `
` + `{saved}` + `
` + `
` + `` + `
`, ); 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 = `
`; (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( `
` + `` + `
`, ); 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( `
` + `` + `
`, ); 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( `` + `
` + `` + `
`, ); 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( `
`, ) 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( `
` + `` + `{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`` + `{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( `
{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( `
` + `` + `{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( `
` + `
`, ) 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( `
`, ) 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}` + `{row.extra}` + `
`, ); 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( `
` + `
  • ` + `always?` + `
`, ); 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( `
` + `
` + `detail` + `
` + `` + `
`, ); 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` + `
` + `` + `
`, ); 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( `
` + `` + `{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( `
`, ); 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( `
`, ); 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( `
`, ); 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( `
`, ); 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( `
`, ); 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( `
left
right
`, ); 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( `
top
bottom
`, ); 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( `
left
right
`, ); 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: `
{g.name}` + `` + `
`, empty: "", }), ).toString("base64"); const win = mount( `
` + `` + `` + `
g1` + `

HAS

` + `
` + `` + `
`, ); 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( `
` + `` + `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( `
` + `` + `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( `
` + `` + `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( `
` + `` + `` + `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( `
` + `` + `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( `
` + `` + `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( `
` + `{seen}` + `
` + `
` + `` + `
`, ); 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( `
` + `{seen}` + `
` + `
` + `` + `
`, ); 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"); });