import { test, expect, beforeEach } from "bun:test"; import { Window } from "happy-dom"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; import { mountHtml } from "@wrnexus/test"; // Fresh DOM per test, with the runtime's globals bound. function mount(html: string): 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).NodeFilter = ( win as unknown as { NodeFilter: unknown } ).NodeFilter; (0, eval)(REACTIVE_RUNTIME); // 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; } beforeEach(() => { delete (globalThis as Record).window; delete (globalThis as Record).document; }); test("hydrates {expr} mustaches from data-scope", () => { const win = mount(`
{count}, {count * 2}
`); expect(win.document.querySelector("span")!.textContent).toBe("0, 0"); }); 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("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("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("data-show toggles visibility on a reactive expression (tabs pattern)", () => { const win = mount( `
A
B
`, ); const disp = (id: string) => (win.document.getElementById(id) as unknown as HTMLElement).style.display; expect(disp("a")).toBe(""); expect(disp("b")).toBe("none"); (win.document.querySelector("button") as unknown as HTMLElement).click(); expect(disp("a")).toBe("none"); expect(disp("b")).toBe(""); }); 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("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", ]); });