import { test, expect } from "bun:test";
import { writeFileSync, mkdirSync, mkdtempSync, rmSync } 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";
test("explicit static rendering disables hydration metadata", () => {
const output = compileWrnFile(`page StaticPage {
render = "static"
state count = 0
view { }
}`);
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 });
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 { }
}`;
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 { }
}`;
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 {
}
}`);
const render = mod.render as (props?: Record) => string;
const enabled = render();
expect(enabled).not.toContain("