release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+75
View File
@@ -0,0 +1,75 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { ACTION_RUNTIME } from "../src/action-runtime.ts";
import { createActionClient } from "../src/actions.ts";
const originalGlobals = new Map(
["window", "document", "location", "CustomEvent", "FormData", "fetch"].map((key) => [
key,
(globalThis as Record<string, unknown>)[key],
]),
);
beforeEach(() => {
for (const key of ["window", "document", "location", "CustomEvent", "FormData", "fetch"]) {
delete (globalThis as Record<string, unknown>)[key];
}
});
afterEach(() => {
for (const [key, value] of originalGlobals) {
if (value === undefined) delete (globalThis as Record<string, unknown>)[key];
else (globalThis as Record<string, unknown>)[key] = value;
}
});
test("action runtime exposes pending, optimistic, success, and invalidation state", async () => {
const win = new Window({ url: "https://example.test/users" });
win.document.cookie = "wire-csrf=token";
win.document.body.innerHTML = `<form data-wrn-action="createUser"><input name="name" value="Ada"></form>`;
const phases: string[] = [];
["optimistic", "pending", "success"].forEach((phase) =>
win.document.addEventListener(`wrnexus:action:${phase}`, () => phases.push(phase)),
);
let invalidated: unknown;
win.addEventListener("wrnexus:cache:invalidate", (event) => {
invalidated = (event as unknown as CustomEvent).detail.tags;
});
const g = globalThis as Record<string, unknown>;
Object.assign(g, {
window: win,
document: win.document,
location: win.location,
CustomEvent: win.CustomEvent,
FormData: win.FormData,
fetch: async () => Response.json({ data: { id: 1 }, invalidated: ["users"] }),
});
(0, eval)(ACTION_RUNTIME);
win.document
.querySelector("form")!
.dispatchEvent(new win.Event("submit", { bubbles: true, cancelable: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
expect(phases).toEqual(["optimistic", "pending", "success"]);
expect(invalidated).toEqual(["users"]);
expect(win.document.querySelector("form")?.getAttribute("data-wrn-action-state")).toBe("success");
});
test("typed action client returns invalidations and structured validation errors", async () => {
const original = globalThis.fetch;
try {
globalThis.fetch = (async () =>
Response.json({ data: { id: 1 }, invalidated: ["users"] })) as unknown as typeof fetch;
const call = createActionClient<{ name: string }, { id: number }>("/users", "createUser");
expect(await call({ name: "Ada" }, { csrfToken: "token" })).toEqual({
data: { id: 1 },
invalidated: ["users"],
});
globalThis.fetch = (async () =>
Response.json({ errors: { name: "Required" } }, { status: 422 })) as unknown as typeof fetch;
await expect(call({ name: "" })).rejects.toMatchObject({
status: 422,
errors: { name: "Required" },
});
} finally {
globalThis.fetch = original;
}
});
+52
View File
@@ -18,6 +18,7 @@ function install(bodyHtml: string): void {
g.location = win.location;
g.DOMParser = win.DOMParser;
g.CustomEvent = win.CustomEvent;
g.Event = win.Event;
g.fetch = win.fetch = (url: string, opts: any) => {
fetchCalls.push({ url, opts });
return Promise.resolve({
@@ -44,6 +45,7 @@ beforeEach(() => {
"location",
"DOMParser",
"CustomEvent",
"Event",
"fetch",
]) {
delete g[k];
@@ -64,6 +66,22 @@ test("intercepts an internal link click and swaps #app in place", async () => {
expect(win.document.title).toBe("About");
});
test("prefetches focused routes once and reuses the document during navigation", async () => {
install(`<div id="app"><a href="/about" id="prefetch">About</a></div>`);
nextHtml =
`<!doctype html><html><head><title>About</title></head>` +
`<body><div id="app"><h1>Prefetched</h1></div></body></html>`;
const link = win.document.getElementById("prefetch");
link.dispatchEvent(new win.FocusEvent("focusin", { bubbles: true }));
await flush();
expect(fetchCalls).toHaveLength(1);
expect(fetchCalls[0]?.opts.headers["x-wrnexus-prefetch"]).toBe("1");
link.click();
await flush();
expect(fetchCalls).toHaveLength(1);
expect(win.document.querySelector("h1")?.textContent).toBe("Prefetched");
});
test("ignores cross-origin links (full navigation)", async () => {
install(`<div id="app"><a href="https://other.test/x" id="lnk">x</a></div>`);
win.document.getElementById("lnk").click();
@@ -151,3 +169,37 @@ test("synchronizes page styles during client navigation and preserves the curren
expect(style.textContent).toContain("color:blue");
expect(style.getAttribute("nonce")).toBe("current-nonce");
});
test("restores declared form state but never preserves sensitive fields", async () => {
install(
`<meta name="wrnexus-preserve" content="forms">` +
`<div id="app"><input name="query" value="draft"><input name="password" type="password" value="secret"></div>`,
);
nextHtml = `<html><head><meta name="wrnexus-preserve" content="forms"></head><body><div id="app">Next</div></body></html>`;
win.__wrnexusNavigate("/next");
await flush();
nextHtml =
`<html><head><meta name="wrnexus-preserve" content="forms"></head><body><div id="app">` +
`<input name="query" value=""><input name="password" type="password" value=""></div></body></html>`;
win.__wrnexusNavigate("/");
await flush();
expect(win.document.querySelector('[name="query"]').value).toBe("draft");
expect(win.document.querySelector('[name="password"]').value).toBe("");
});
test("KeepAlive preserves the same live DOM instance across routes", async () => {
install(
`<div id="app"><section data-wrn-keepalive="dashboard"><input value="live"></section></div>`,
);
const original = win.document.querySelector("[data-wrn-keepalive]");
original.runtimeState = { count: 7 };
nextHtml = `<html><body><div id="app"><section data-wrn-keepalive="dashboard"><input value="new"></section></div></body></html>`;
win.__wrnexusNavigate("/next");
await flush();
const restored = win.document.querySelector("[data-wrn-keepalive]");
expect(restored).toBe(original);
expect(restored.runtimeState).toEqual({ count: 7 });
expect(restored.querySelector("input").value).toBe("live");
});
+92 -11
View File
@@ -9,9 +9,13 @@ function mount(html: string): Window {
win.document.body.innerHTML = `<div id="app">${html}</div>`;
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(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.
@@ -23,6 +27,9 @@ function mount(html: string): Window {
beforeEach(() => {
delete (globalThis as Record<string, unknown>).window;
delete (globalThis as Record<string, unknown>).document;
delete (globalThis as Record<string, unknown>).location;
delete (globalThis as Record<string, unknown>).fetch;
delete (globalThis as Record<string, unknown>).MutationObserver;
});
test("hydrates {expr} mustaches from data-scope", () => {
@@ -30,6 +37,40 @@ test("hydrates {expr} mustaches from data-scope", () => {
expect(win.document.querySelector("span")!.textContent).toBe("0, 0");
});
test("mounts client-only templates before hydrating their scopes", () => {
const win = mount(
`<div data-wrn-client-root="client-page" aria-busy="true"></div>` +
`<template data-wrn-client-template="client-page"><main data-scope="count: 2"><b>{count}</b></main></template>`,
);
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<string, unknown>).fetch = () =>
Promise.resolve(Response.json({ data: { name: "Ada" } }));
const win = mount(
`<section data-wrn-async="users" data-wrn-async-retries="0" aria-busy="true">` +
`<div data-wrn-async-content>Loading</div>` +
`<template data-wrn-async-loading>Loading</template>` +
`<template data-wrn-async-success><strong>{users.name}</strong></template>` +
`<template data-wrn-async-error><b>{error.message}</b></template></section>`,
);
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(
`<div data-scope="count: 0"><button data-on-click="count++">{count}</button></div>`,
@@ -83,6 +124,26 @@ test("component functions support formatted multiline assignments and ternaries"
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(
`<div data-scope="count: 0, message: 'ready'" data-wrn-behavior="${behavior}">` +
`<button data-on-click="increment()">{message}</button></div>`,
);
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(
`<div data-scope="" data-wrn-events="complete">
@@ -220,7 +281,7 @@ test("expression evaluator supports flatMap callbacks", () => {
expect(win.document.querySelector("span")!.textContent).toBe("1,2,3");
});
test("data-show toggles visibility on a reactive expression (tabs pattern)", () => {
test("data-show toggles visibility without destroying interactive state", () => {
const win = mount(
`<div data-scope="tab: 0">
<button data-on-click="tab = 1">go</button>
@@ -228,17 +289,37 @@ test("data-show toggles visibility on a reactive expression (tabs pattern)", ()
<section id="b" data-show="tab === 1">B</section>
</div>`,
);
const disp = (id: string) =>
(win.document.getElementById(id) as unknown as HTMLElement).style.display;
expect(disp("a")).toBe("");
expect(disp("b")).toBe("none");
expect(win.document.getElementById("a")!.getAttribute("data-show")).toBe("true");
expect(win.document.getElementById("b")!.getAttribute("data-show")).toBe("false");
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(disp("a")).toBe("none");
expect(disp("b")).toBe("");
expect(win.document.getElementById("a")!.getAttribute("data-show")).toBe("false");
expect(win.document.getElementById("b")!.getAttribute("data-show")).toBe("true");
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(
`<div data-scope="active: 'Admin'">
<button data-on-click="active = active === 'Admin' ? 'Guest' : 'Admin'">switch</button>
<div data-wrn-dynamic-component="Admin" data-wrn-bind-0='["data-wrn-dynamic-component","{active}"]'>
<section data-component-case="Admin">Administrator secret</section>
<section data-component-case="Guest">Guest dashboard</section>
</div>
</div>`,
);
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", () => {