import { test, expect } from "bun:test";
import { writeFileSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { generate, parse } from "../src/index.ts";
import { compileWrnFile } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test";
// The compiled module below is written to an OS tmpdir with no node_modules
// of its own, so Node's bare-specifier resolution for "@wrnexus/core" would
// otherwise walk up to whatever (possibly stale, globally-installed) copy
// happens to sit outside the workspace. Symlink the workspace package in so
// it resolves to the real, currently-built `@wrnexus/core` — the same one
// every other package in this repo gets via its own
// `node_modules/@wrnexus/core` symlink.
const WORKSPACE_CORE = join(import.meta.dir, "../../core");
function linkWorkspaceCore(root: string): void {
const scopeDir = join(root, "node_modules", "@wrnexus");
const linkPath = join(scopeDir, "core");
if (existsSync(linkPath)) return;
mkdirSync(scopeDir, { recursive: true });
symlinkSync(WORKSPACE_CORE, linkPath, process.platform === "win32" ? "junction" : "dir");
}
test("explicit static rendering disables hydration metadata", () => {
const output = compileWrnFile(`page StaticPage {
render = "static"
state count = 0
view { {count} }
}`);
expect(output).toContain('export const __wrnexusRender = "static"');
expect(output).toContain('data-wrn-hydrate="none"');
expect(output).toContain('export const __wrnexusHydrate = "none"');
});
test("named data loads compile as parallel typed data entries", () => {
const output = compileWrnFile(`page Users {
load users { return ["Ada"] }
load server teams { return ["Core"] }
view {
Users
}
}`);
expect(output).toContain("await Promise.all");
expect(output).toContain('return { "users": __values[0], "teams": __values[1] }');
});
let seq = 0;
/** Compile a `.wrn` source and import the resulting module. */
async function compileAndImport(src: string): Promise> {
const dir = join(tmpdir(), "wrn-compiler-test");
mkdirSync(dir, { recursive: true });
linkWorkspaceCore(dir);
const file = join(dir, `m${seq++}.ts`);
writeFileSync(file, compileWrnFile(src));
return import(pathToFileURL(file).href);
}
type CompiledBehavior = {
functions: string;
lifecycle: {
mount?: string;
update?: string;
unmount?: string;
};
watches: Array<{
state: string;
body: string;
}>;
};
function extractCompiledBehavior(output: string): CompiledBehavior {
const marker = 'data-wrn-behavior="';
const start = output.indexOf(marker);
expect(start).toBeGreaterThanOrEqual(0);
const valueStart = start + marker.length;
const valueEnd = output.indexOf('"', valueStart);
expect(valueEnd).toBeGreaterThan(valueStart);
const encoded = output.slice(valueStart, valueEnd);
expect(encoded).toMatch(/^[A-Za-z0-9+/]+=*$/);
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")) as CompiledBehavior;
}
test("parses a page with a layout member", () => {
const ast = parse(`page Home {\n layout = "public"\n view { Hi }\n}`);
expect(ast.kind).toBe("page");
expect(ast.name).toBe("Home");
expect(ast.layout).toBe("public");
});
test("top-level imports are preserved and available to SSR view expressions", async () => {
const source = `import { posix } from "node:path";
page Home {
view { }
}`;
const ast = parse(source);
expect(ast.imports).toEqual(['import { posix } from "node:path";']);
const mod = await compileAndImport(source);
const render = mod.default as (ctx: unknown) => string;
expect(String(await render({}))).toContain('signInHref="/sso/sign-in"');
});
test("parses a component with props and state", () => {
const ast = parse(
`component C {\n props {\n n = 0\n }\n state count = n\n view { {count} }\n}`,
);
expect(ast.kind).toBe("component");
expect(ast.props.map((p) => p.name)).toEqual(["n"]);
expect(ast.states.map((s) => s.name)).toEqual(["count"]);
});
test("component event declarations compile to public root metadata", async () => {
const source = `component EventButton {
props {
label = "Save"
@event complete = function
}
view { {label} }
}`;
const ast = parse(source);
const output = generate(ast);
const mod = await compileAndImport(source);
const render = mod.render as (props: Record) => string;
expect(ast.events).toEqual([{ name: "complete" }]);
expect(output).toContain('data-wrn-events="complete"');
expect(render({})).toContain('data-wrn-events="complete"');
});
test("explicit nested component mounts serialize structured expression props", () => {
const output = compileWrnFile(`component Shell {
state brand = { "label": "WRNexus" }
state items = [{ "label": "Home", "href": "/" }]
view {
}
}`);
expect(output).toContain('data-component="Navbar"');
expect(output).toContain('brand="${__wrnProp(brand)}"');
expect(output).toContain('items="${__wrnProp(items)}"');
});
test("typed props, required props, state, custom types, and function parameters compile", () => {
const source = `component TypedPicker {
types {
interface DateOption { value: string; label: string }
type ChangeHandler = (value: string) => void
}
props {
value: string = ""
options: DateOption[] = []
required: boolean = false
onChange: ChangeHandler
}
state open: boolean = false
functions {
function choose(option: DateOption, index: number): void {
open = false
onChange(option.value)
}
}
view { {value} }
}`;
const ast = parse(source);
expect(ast.props.map(({ name, valueType, required }) => ({ name, valueType, required }))).toEqual(
[
{ name: "value", valueType: "string", required: false },
{ name: "options", valueType: "DateOption[]", required: false },
{ name: "required", valueType: "boolean", required: false },
{ name: "onChange", valueType: "ChangeHandler", required: true },
],
);
expect(ast.states[0]).toMatchObject({ name: "open", valueType: "boolean", expr: "false" });
const output = generate(ast);
expect(output).toContain("interface DateOption");
expect(output).toContain("export interface TypedPickerProps");
expect(output).toContain('"onChange": ChangeHandler;');
expect(output).toContain("let open: boolean = (false)");
expect(output).toContain("function choose(option: DateOption, index: number): void");
const behavior = extractCompiledBehavior(output);
expect(behavior.functions).toContain("function choose(option, index)");
expect(behavior.functions).not.toContain("option: DateOption");
});
test("typed declarations reject obvious initializer mismatches", () => {
expect(() =>
parse(`component Invalid {
props {
required: boolean = "yes"
}
view {
}
}`),
).toThrow("Prop 'required' is declared as boolean, but its initializer is string");
expect(() => parse(`page Invalid { state count: number = false\n view {
} }`)).toThrow(
"State 'count' is declared as number, but its initializer is boolean",
);
});
test("typed props enforce required values and runtime-compatible input", async () => {
const component = await compileAndImport(`component TypedInput {
props {
label: string
count: number = 0
enabled: boolean = false
}
view { {label}:{count}:{enabled} }
}`);
const render = component.render as (props?: Record) => string;
expect(() => render()).toThrow("TypedInput requires prop 'label' (string)");
const rendered = render({ label: "Total", count: "4", enabled: "true" });
expect(rendered).toContain('Total ');
expect(rendered).toContain('4 ');
expect(rendered).toContain('true ');
expect(() => render({ label: "Total", count: "many" })).toThrow("Expected a finite number prop");
expect(() => render({ label: "Total", enabled: "sometimes" })).toThrow("Expected a boolean prop");
});
test("HTML view: void elements, boolean attrs, comments, lone <", () => {
const ast = parse(
`component T {\n view {\n \n \n \n a < b
\n }\n}`,
);
const html = compileWrnFile(
`component T {\n view {\n \n \n \n a < b
\n }\n}`,
);
expect(html).toContain(" ");
expect(html).not.toContain("comment");
expect(html).toContain("a < b");
void ast;
});
test("dynamic HTML boolean attributes are omitted when false", async () => {
const mod = await compileAndImport(`component BooleanAttributes {
props {
disabled = false
checked = false
required = false
loading = false
}
view {
Save
}
}`);
const render = mod.render as (props?: Record) => string;
const enabled = render();
expect(enabled).not.toContain(" {
const mod = await compileAndImport(`component ForwardingButton {
props {
label = "Button"
variant = "default"
disabled = false
}
view {
{label}
}
}`);
const render = mod.render as (props?: Record) => string;
const html = render({
label: "Save",
variant: "primary",
formaction: "/submit-form",
formenctype: "application/x-www-form-urlencoded",
formmethod: "post",
popovertarget: "myPopover",
"aria-describedby": "save-help",
"data-testid": "save",
onclick: "alert(1)",
style: "display:none",
"data-wrn-bind-0": "unsafe",
});
expect(html).toContain('formaction="/submit-form"');
expect(html).toContain('formenctype="application/x-www-form-urlencoded"');
expect(html).toContain('formmethod="post"');
expect(html).toContain('popovertarget="myPopover"');
expect(html).toContain('aria-describedby="save-help"');
expect(html).toContain('data-testid="save"');
expect(html).not.toContain("variant=");
expect(html).not.toContain("onclick=");
expect(html).not.toContain("style=");
expect(html).not.toContain('data-wrn-bind-0="unsafe"');
});
test("an explicit attrs spread overrides automatic root forwarding", async () => {
const mod = await compileAndImport(`component WrappedControl {
props {
label = "Control"
class = ""
}
view {
{label}
}
}`);
const render = mod.render as (props?: Record) => string;
const html = render({ label: "Save", class: "wrapper", formaction: "/save" });
expect(html).toContain('');
expect(html).toContain('Save ');
});
test("prop-driven component renders SSR content and exposes reactive prop signals", async () => {
const mod = await compileAndImport(
`component Button {\n props {\n label = "Button"\n variant = "default"\n class = ""\n }\n view { {label} }\n}`,
);
const render = mod.render as (p: Record) => string;
const out = render({ label: "Save ", variant: "primary", class: "mt-2" });
expect(out).toContain('class="wrn-btn wrn-btn--primary mt-2"');
expect(out).toContain("Save <b>"); // html-escaped
expect(out).toContain("data-scope");
expect(out).toContain('data-text="label"');
});
test("stateful component keeps both prop and state text reactive", async () => {
const mod = await compileAndImport(
`component Counter {\n props {\n start = 0\n label = "Count"\n }\n state count = start\n view { {label}: {count} }\n}`,
);
const render = mod.render as (p: Record) => string;
const out = render({ start: "10", label: "Score" });
expect(out).toContain('data-scope="start: 10, label: "Score", count: 10"');
expect(out).toContain('data-on-click="count++"');
expect(out).toContain('Score : 10 ');
expect(out).toContain('data-scope="');
expect(out).toContain("start: 10");
expect(out).toContain("label: "Score"");
expect(out).toContain("count: 10");
});
test("prop type coercion follows the default value's type", async () => {
const mod = await compileAndImport(
`component X {\n props {\n n = 0\n s = "x"\n b = false\n }\n view { {n}{s}{b} }\n}`,
);
// needsScope=false → seeds nothing; verify via a stateful variant instead:
const mod2 = await compileAndImport(
`component Y {\n props {\n n = 0\n }\n state v = n\n view { {v} }\n}`,
);
const out = (mod2.render as (p: Record) => string)({ n: "42" });
expect(out).toContain("v: 42"); // "42" coerced to number 42 (not '42')
void mod;
});
test("platform events compile through the native runtime bridge", () => {
const out = compileWrnFile(`page Platform {
state count = 0
view {
Run
}
}`);
expect(out).toContain('data-on-wrnexus-browser-click="count++"');
expect(out).toContain('data-on-wrnexus-mobile-click="count = count + 2"');
});
test("{t:key} compiles to a data-t marker (both pages and components)", () => {
const page = compileWrnFile(`page P {\n view { {t:home.title} }\n}`);
expect(page).toContain(' ');
const comp = compileWrnFile(`component C {\n view { {t:x} }\n}`);
expect(comp).toContain(' ');
});
test("{t:} does NOT wrap a page in a data-scope (regression)", () => {
// Only state / events force a reactive scope, not i18n markers.
const page = compileWrnFile(`page P {\n view { {t:a} {t:b}
}\n}`);
expect(page).not.toContain("data-scope");
});
test("page state text bakes its initial value into a reactive data-text span", () => {
const page = compileWrnFile(
`page Reactive {\n state count = 3\n view { Count is {count}, doubled {count * 2}
}\n}`,
);
expect(page).toContain('3 '); // no-JS sees "3"
expect(page).toContain('6 '); // expression evaluated
expect(page).toContain("data-scope"); // state still forces a reactive scope
});
test("page state attributes bake their initial value and retain a reactive binding", () => {
const page = compileWrnFile(`page Password {
state show = false
view {
Toggle
}
}`);
expect(page).toContain('type="password"');
expect(page).toContain('aria-label="Show"');
expect(page.match(/data-wrn-bind-/g)).toHaveLength(2);
expect(page).toContain("show ? 'text' : 'password'");
});
test("request-dependent page state attributes resolve during server rendering", async () => {
const mod = await compileAndImport(`page Playground {
state label = ctx.url.searchParams.get("label") ?? "Default"
state loading = ctx.url.searchParams.get("loading") ?? "false"
view {
}
}`);
const render = mod.default as (ctx: { url: URL }) => Promise;
const html = await render({
url: new URL("https://example.test/playground?label=Visible&loading=false"),
});
expect(html).toContain('label="Visible"');
expect(html).toContain('loading="false"');
expect(html).not.toContain('label=""');
});
test("data-for: loop-variable mustaches stay literal (not baked server-side)", () => {
const comp = compileWrnFile(
`component TodoList {\n state todos = []\n view { }\n}`,
);
// The template keeps `{t.text}` for the client list renderer, and the
// loop variable is never baked (which would be a server-side ReferenceError).
expect(comp).toContain('data-for="t in todos"');
expect(comp).toContain("{t.text}");
expect(comp).not.toContain("__wrnHtml(t.text)");
});
test("named slots pass through to the component output", () => {
const out = compileWrnFile(
`component Card {\n view {
}\n}`,
);
expect(out).toContain('');
expect(out).toContain(" ");
});
test("capitalized self-closing tags compile to component mounts", () => {
const source = `
page ComponentPage {
view {
}
}
`;
const output = generate(parse(source));
expect(output).toContain('data-component="UserCard"');
expect(output).toContain('name="Ajay"');
expect(output).toContain('role="Admin"');
expect(output).not.toContain(" {
const source = `
page SlotPage {
view {
Hello
Component content
}
}
`;
const output = generate(parse(source));
expect(output).toContain('data-component="Card"');
expect(output).toContain("Hello ");
expect(output).toContain("Component content
");
});
test("component tags work inside reusable components", () => {
const source = `
component ParentCard {
props {
title = "Hello"
}
view {
}
}
`;
const output = generate(parse(source));
expect(output).toContain('data-component="ChildCard"');
expect(output).toContain('title="${__wrnProp(title)}"');
expect(output).toContain("function __wrnProp");
});
test("native array and object props serialize through component mounts", async () => {
const mod = await compileAndImport(`
component NativeProps {
state items = [{"label":"Home","href":"/"}]
view {
}
}
`);
const render = mod.render as (props?: Record) => string;
const html = render();
expect(html).toContain(
'items="[{"label":"Home","href":"/"}]"',
);
expect(html).toContain('options="{"dense":true}"');
expect(html).toContain(
'items="[{"label":"Cases","href":"/cases"}]"',
);
});
test("malformed structured prop strings fail with the prop name", async () => {
const mod = await compileAndImport(`component StructuredProps {
props { items = [] options = {} }
view {
}
}`);
const render = mod.render as (props?: Record) => string;
expect(() => render({ items: "not-json" })).toThrow("Expected an array prop 'items'");
expect(() => render({ options: "[]" })).toThrow("Expected an object prop 'options'");
});
test("parses a layout block", () => {
const ast = parse(`
layout AppLayout {
view {
{content}
}
}
`);
expect(ast.kind).toBe("layout");
expect(ast.name).toBe("AppLayout");
});
test("layouts compile as reusable render modules", () => {
const output = generate(
parse(`
layout AppLayout {
view {
{content}
}
}
`),
);
expect(output).toContain('export const __wrnexusLayout = "AppLayout"');
expect(output).toContain("${__wrnRaw(content)}");
expect(output).toContain("export function render");
});
test("parses component lifecycle hooks", () => {
const ast = parse(`
component BackToTop {
state visible = false
lifecycle {
mount {
visible = true
}
update {
console.log(visible)
}
unmount {
visible = false
}
}
view {
Top
}
}
`);
expect(ast.lifecycle.mount).toContain("visible = true");
expect(ast.lifecycle.update).toContain("console.log(visible)");
expect(ast.lifecycle.unmount).toContain("visible = false");
});
test("parses state watchers", () => {
const ast = parse(`
component Menu {
state open = false
watch open {
console.log("Open changed", open)
}
view {
{open}
}
}
`);
expect(ast.watches).toEqual([
{
state: "open",
body: expect.stringContaining('console.log("Open changed", open)'),
},
]);
});
test("rejects watching an undeclared state", () => {
expect(() =>
parse(`
component Broken {
watch missing {
console.log(missing)
}
view {
Broken
}
}
`),
).toThrow("Cannot watch undeclared state 'missing'");
});
test("rejects unknown lifecycle hooks", () => {
expect(() =>
parse(`
component Broken {
lifecycle {
created {
console.log("invalid")
}
}
view {
Broken
}
}
`),
).toThrow("Unknown lifecycle hook 'created'");
});
test("component functions compile into browser behavior metadata", () => {
const output = generate(
parse(`
component Counter {
state count = 0
functions {
function increment() {
count += 1
}
}
view {
{count}
}
}
`),
);
expect(output).toContain("data-wrn-behavior");
expect(output).toContain("function increment()");
expect(output).toContain("export const __wrnexusBehavior");
});
test("component lifecycle hooks compile into behavior metadata", () => {
const output = generate(
parse(`
component LifecycleTest {
state ready = false
lifecycle {
mount {
ready = true
}
update {
console.log(ready)
}
unmount {
ready = false
}
}
view {
{ready}
}
}
`),
);
const behavior = extractCompiledBehavior(output);
expect(behavior.lifecycle.mount).toContain("ready = true");
expect(behavior.lifecycle.update).toContain("console.log(ready)");
expect(behavior.lifecycle.unmount).toContain("ready = false");
});
test("component watchers compile into behavior metadata", () => {
const output = generate(
parse(`
component WatchTest {
state visible = false
watch visible {
console.log("changed", visible)
}
view {
{visible}
}
}
`),
);
const behavior = extractCompiledBehavior(output);
expect(behavior.watches).toHaveLength(1);
expect(behavior.watches[0]?.state).toBe("visible");
expect(behavior.watches[0]?.body).toContain('console.log("changed", visible)');
});
test("behavior-only components still receive a reactive scope", () => {
const output = generate(
parse(`
component BehaviorOnly {
functions {
function sayHello() {
console.log("hello")
}
}
lifecycle {
mount {
sayHello()
}
}
view {
Hello
}
}
`),
);
expect(output).toContain("data-scope");
expect(output).toContain("data-wrn-behavior");
});
test("component behavior metadata safely encodes multiline code", () => {
const output = generate(
parse(`
component BackToTop {
state visible = false
functions {
function updateVisibility() {
visible = window.scrollY > 500
}
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: "smooth"
})
}
}
lifecycle {
mount {
updateVisibility()
}
}
watch visible {
console.log(
value,
previous
)
}
view {
Top
}
}
`),
);
expect(output).toContain("data-wrn-behavior=");
expect(output).not.toContain('data-wrn-behavior="{');
expect(output).not.toContain('data-wrn-behavior="%7B');
});
test("component watchers compile into behavior metadata", () => {
const output = generate(
parse(`
component WatchTest {
state visible = false
watch visible {
console.log("changed", visible)
}
view {
{visible}
}
}
`),
);
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
expect(match).not.toBeNull();
const behavior = extractBehavior(output);
expect(behavior.watches).toEqual([
{
state: "visible",
body: expect.stringContaining('console.log("changed", visible)'),
},
]);
});
test("component lifecycle hooks compile into behavior metadata", () => {
const output = generate(
parse(`
component LifecycleTest {
state ready = false
lifecycle {
mount {
ready = true
}
update {
console.log(ready)
}
unmount {
ready = false
}
}
view {
{ready}
}
}
`),
);
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
expect(match).not.toBeNull();
const behavior = extractBehavior(output);
expect(behavior.lifecycle.mount).toContain("ready = true");
expect(behavior.lifecycle.update).toContain("console.log(ready)");
expect(behavior.lifecycle.unmount).toContain("ready = false");
});
function extractBehavior(output: string): {
functions: string;
lifecycle: {
mount?: string;
update?: string;
unmount?: string;
};
watches: Array<{
state: string;
body: string;
}>;
} {
const match = /data-wrn-behavior="([^"]+)"/.exec(output);
expect(match).not.toBeNull();
return JSON.parse(Buffer.from(match![1], "base64").toString("utf8"));
}
test("component interpolated class attributes remain reactive", () => {
const output = generate(
parse(`
component CookieToggle {
state personalizationCookies = false
view {
Cookie
}
}
`),
);
const match = output.match(/data-wrn-bind-class="([^"]+)"/);
expect(match).not.toBeNull();
const encodedBinding = match?.[1] ?? "";
const decodedBinding = encodedBinding
.replaceAll(""", '"')
.replaceAll("'", "'")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">");
const binding = JSON.parse(decodedBinding) as [string, string];
expect(binding[0]).toBe("class");
expect(binding[1]).toBe("base {personalizationCookies ? 'enabled' : 'disabled'}");
});
test("parses class directives containing Tailwind arbitrary values", () => {
const ast = parse(`
component PageHeader {
state centered = false
view {
Header
}
}
`);
expect(ast).toBeDefined();
});
test("compiles Tailwind arbitrary values in class directives", () => {
const output = generate(
parse(`
component PageHeader {
state centered = false
view {
Header
}
}
`),
);
expect(output).toContain("lg:grid-cols-[minmax(0,1fr)_auto]");
expect(output).toContain("dark:bg-white/10");
});
test("class directives accept single-quoted brace expressions", () => {
const output = generate(
parse(`
component MarketingSectionHeader {
props {
align = "start"
}
view {
}
}
`),
);
expect(output).toContain('${(align === "center") ? " mx-auto" : ""}');
expect(output).toContain('${(align === "center") ? " text-center" : ""}');
expect(output).not.toContain('({align === "center"})');
expect(output).toContain(""align === \\\\"center\\\\""");
});
test("components support each blocks", () => {
const output = generate(
parse(`
component PageHeader {
props {
highlights = []
}
view {
{#each highlights as highlight}
{highlight.title}
{/each}
}
}
`),
);
expect(output).toContain("highlights");
expect(output).toContain("highlight.title");
expect(output).not.toContain("supported in pages, not components");
});
test("components support if blocks", () => {
const output = generate(
parse(`
component Banner {
props {
visible = true
}
view {
{#if visible}
Visible
{:else}
Hidden
{/if}
}
}
`),
);
expect(output).toContain("visible");
expect(output).toContain("Visible");
expect(output).toContain("Hidden");
expect(output).toContain("__wrnexusEncodeControl");
});
test("bind directives compile to reactive values and assignment handlers", () => {
const output = generate(
parse(`page FormPage {
state name = ""
state active = false
view {
}
}`),
);
expect(output).not.toContain("bind:value");
expect(output).not.toContain("bind:checked");
expect(output).toContain("name = payload.value");
expect(output).toContain("active = payload.checked");
});
test("if and each blocks emit browser control metadata while preserving SSR", () => {
const output = generate(
parse(`component ClientBlocks {
state open = false
state items = ["a"]
view {
{#if open}Open
{:else}Closed
{/if}
{#each items as item}{item} {:empty}Empty {/each}
}
}`),
);
expect(output).toContain("data-wrn-if=");
expect(output).toContain("data-wrn-each=");
expect(output).toContain("Array.isArray(items)");
});
test("component array props support each blocks", () => {
const output = generate(
parse(`
component HighlightList {
props {
highlights = []
}
view {
{#each highlights as highlight}
{highlight.title}
{/each}
}
}
`),
);
expect(output).toContain("Array.isArray(def)");
expect(output).toContain("JSON.parse(v)");
expect(output).toContain("highlights");
});
test("page component props evaluate whole expressions", () => {
const output = generate(
parse(`
page Home {
view {
}
}
`),
);
expect(output).toContain("__wrnexusPropAttr(true)");
expect(output).toContain("__wrnexusPropAttr([");
});
test("page state passed to a component retains a parent-owned prop binding", () => {
const output = generate(
parse(`page Home {
state count = 1
view { }
}`),
);
expect(output).toContain("data-wrn-prop-bind-");
expect(output).toContain("{count}");
});
test("nested component props retain parent-owned reactive bindings", () => {
const output = generate(
parse(`component Parent {
props { value = 0 }
state count = value
view { }
}`),
);
expect(output).toContain("data-wrn-prop-bind-0");
expect(output).toContain("["value","{count}"]");
expect(output).toContain('lowerName.startsWith("data-wrn-prop-bind-")');
});
test("component functions are available during server rendering", () => {
const output = generate(
parse(`
component FAQAccordion {
props {
allowMultiple = false
items = []
}
state openIndexes = []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
function toggleItem(index) {
if (isOpen(index)) {
openIndexes = openIndexes.filter(
(itemIndex) =>
itemIndex !== index
)
} else if (allowMultiple) {
openIndexes = [
...openIndexes,
index
]
} else {
openIndexes = [index]
}
}
}
view {
{#each items as item, index}
{item.title}
{/each}
}
}
`),
);
expect(output).toContain("function isOpen(index)");
expect(output).toContain("function toggleItem(index)");
expect(output).toContain("let openIndexes =");
expect(output).toContain("isOpen(index)");
});
test("data-for conditional classes do not evaluate loop variables during SSR", () => {
const output = generate(
parse(`
component Accordion {
props {
items = []
}
state openIndexes = []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
}
view {
{item.title}
}
}
`),
);
expect(output).toContain('data-for="item, index in items"');
expect(output).toContain("data-wrn-class-0");
expect(output).not.toContain('${(isOpen(index)) ? " open" : ""}');
});
test("data-for parser supports optional stable key expressions", async () => {
const { parseForExpr } = await import("../src/codegen.ts");
expect(parseForExpr("item, index in items key item.id")).toEqual({
item: "item",
index: "index",
list: "items",
key: "item.id",
});
expect(parseForExpr("item in items")).toEqual({
item: "item",
index: undefined,
list: "items",
key: undefined,
});
});
test("server-rendered each locals work in reactive handlers", async () => {
const firstLocals = Buffer.from(
JSON.stringify({
item: {
question: "First",
},
index: 0,
}),
"utf8",
).toString("base64");
const secondLocals = Buffer.from(
JSON.stringify({
item: {
question: "Second",
},
index: 1,
}),
"utf8",
).toString("base64");
const dom = mountHtml(`
`);
const buttons = dom.querySelectorAll("button") as HTMLButtonElement[];
buttons[1]!.click();
await Promise.resolve();
const articles = dom.querySelectorAll("article");
expect(articles[0]!.querySelector("span")?.classList.contains("open")).toBe(false);
expect(articles[1]!.querySelector("span")?.classList.contains("open")).toBe(true);
}, 20_000);
test("WRN 0.3 metadata, computed values, loaders, and actions compile additively", () => {
const source = `page Dashboard {
runtime = "universal"
hydrate = "idle"
state count = 2
computed { doubled = count * 2 }
effect { console.log(doubled) }
security { auth = "required" }
load server { return { count: 2 } }
load client { return { refreshed: true } }
action save(input) { return input }
view { {doubled} }
}`;
const output = compileWrnFile(source);
expect(output).toContain('export const __wrnexusRuntime = "universal"');
expect(output).toContain('export const __wrnexusHydrate = "idle"');
expect(output).toContain("export async function __wrnexusLoad");
expect(output).toContain("export async function __wrnexusClientLoad");
expect(output).toContain("export async function save(input)");
expect(output).toContain("export const __wrnexusActions");
expect(output).toContain('data-wrn-hydrate="idle"');
});
test("page state is available inside server-rendered if blocks", () => {
const source = `
page Pricing {
state billingCycle = "monthly"
view {
{#if billingCycle === "annual"}
Annual
{:else}
Monthly
{/if}
}
}
`;
const ast = parse(source);
const code = generate(ast);
expect(code).toContain("const { billingCycle } = __state;");
expect(code).toContain('billingCycle === "annual"');
});
test("page state SSR condition renders without ReferenceError", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-page-state-"));
const generatedFile = join(root, "pricing.generated.ts");
try {
linkWorkspaceCore(root);
const source = `
page Pricing {
state billingCycle = "monthly"
view {
{#if billingCycle === "annual"}
Annual
{:else}
Monthly
{/if}
}
}
`;
const code = generate(parse(source));
expect(code).toContain("const { billingCycle } = __state;");
expect(code).toContain('billingCycle === "annual"');
writeFileSync(generatedFile, code, "utf8");
const moduleUrl = pathToFileURL(generatedFile).href + `?test=${Date.now()}-${Math.random()}`;
const generatedModule = await import(moduleUrl);
const html = await generatedModule.default({});
expect(html).toContain("Monthly
");
expect(html).not.toContain("Annual
");
} finally {
rmSync(root, {
recursive: true,
force: true,
});
}
});
test("page state is declared for server-rendered control blocks", () => {
const source = `
page Pricing {
state billingCycle = "monthly"
view {
{#if billingCycle === "annual"}
Annual
{:else}
Monthly
{/if}
}
}
`;
const code = generate(parse(source));
expect(code).toContain("const { billingCycle } = __state;");
expect(code).toContain('billingCycle === "annual"');
});
test("style blocks compile with stable page/component/layout metadata", () => {
const page = compileWrnFile(`page StyledPage {
style { .shared { color: red; } }
view { Page }
}`);
const component = compileWrnFile(`component StyledCard {
style { .shared { color: blue; } }
view { Card }
}`);
const layout = compileWrnFile(`layout StyledLayout {
style { .shared { color: green; } }
view {
}
}`);
expect(page).toContain('data-wrnexus-style-kind="page"');
expect(component).toContain('data-wrnexus-style-kind="component"');
expect(layout).toContain('data-wrnexus-style-kind="layout"');
expect(page).toContain("export const __wrnexusStyles");
expect(component).toContain("export const __wrnexusStyles");
expect(layout).toContain("export const __wrnexusStyles");
expect(page).toMatch(/data-wrnexus-style-id="wrn-page-[a-z0-9]+"/);
});
test("strict-mode reserved prop names compile through safe local references", () => {
const output = compileWrnFile(`component Visibility {
props { private: boolean = false }
view { {#if private}Private {/if} }
}`);
expect(output).toContain("const __p_private: boolean");
expect(output).toContain("(__p_private) ?");
expect(output).not.toContain("const private:");
});
// data-show holds an expression the client re-evaluates, so it must survive
// compilation verbatim. Interpolating `{open || visible}` to the literal
// "false" froze every overlay that used the braced form -- Drawer, Dropdown,
// Popover, ContextMenu and Tooltip could never open.
test("data-show keeps its expression instead of being interpolated away", () => {
const source = `component Panel {
props {
open: boolean = false
}
state visible = false
view {
body
}
}
`;
const code = compileWrnFile(source, "Panel.wrn");
expect(code).toContain(`data-show="open || visible"`);
expect(code).not.toContain(`data-show="false"`);
});
// Props and state are destructured into the same scope as the function body,
// so a local that shares one of their names used to emit a redeclaration and
// break the whole generated module with a parse error naming no source line.
test("a local variable may shadow a prop without breaking the module", () => {
const source = `component Sized {
props {
size: number = 10
}
state total = 0
functions {
client function recompute() {
var size = 4
var total = size * 2
return total
}
}
view {
{total}
}
}
`;
const code = compileWrnFile(source, "Sized.wrn");
// The alias must be dropped, not emitted alongside the local declaration.
expect(code).not.toContain("const { size } = context.props;");
expect(code).toContain("var size = 4");
});
// A boolean attribute whose expression names a loop variable cannot be
// resolved on the server -- __wrnBooleanAttr runs at render time, where the
// loop variable does not exist, and the generated module failed outright.
test("a boolean attribute bound to a loop variable binds on the client", () => {
const source = `component Picker {
state rows = []
functions {
shared function isOn(row) {
return row.on
}
}
view {
}
}
`;
const code = compileWrnFile(source, "Picker.wrn");
expect(code).not.toContain('__wrnBooleanAttr("checked"');
expect(code).toContain("data-wrn-bind-");
});
test("block comments inside props report the authoring restriction", () => {
expect(() =>
compileWrnFile(`component Example {
props {
/* use a line comment */
label: string = "Example"
}
view { {label} }
}`),
).toThrow("Block comments are not allowed inside props {}; use // line comments instead");
});
test("state page reports its collision with the page keyword", () => {
expect(() =>
compileWrnFile(`page Example {
state page = 1
view { {page} }
}`),
).toThrow("State name 'page' collides with the WRN 'page' keyword");
});