import { expect, test } from "bun:test"; import { diagnose, formatDiagnostic, parse } from "../src/index.ts"; test("parses explicit rendering modes and the never hydration alias", () => { const ast = parse(`page Marketing { render = "static" hydrate = "never" view {

Fast

} }`); expect(ast.renderMode).toBe("static"); expect(ast.hydrate).toBe("none"); expect(() => parse('page Invalid { render = "sometimes" view {

No

} }')).toThrow( "Unknown render mode", ); }); test("parses a declarative cache policy", () => { const ast = parse(`page Dashboard { cache { strategy = "stale-while-revalidate" ttl = "5m" tags = ["users", "dashboard"] vary = ["tenant", "language"] } view {

Dashboard

} }`); expect(ast.cache).toEqual({ strategy: "stale-while-revalidate", ttl: "5m", tags: '["users", "dashboard"]', vary: '["tenant", "language"]', }); }); test("parses native array and object literals in state and component props", () => { const ast = parse(` component Navigation { state items = [{"label":"Home","href":"/"}] state options = {"dense":true,"nested":{"label":"A {brace}"}} view { } } `); expect(ast.states[0]?.expr).toBe('[{"label":"Home","href":"/"}]'); expect(ast.states[1]?.expr).toBe('{"dense":true,"nested":{"label":"A {brace}"}}'); const elements = ast.view.filter((node) => node.type === "element"); const navbar = elements[0]; const sidebar = elements[1]; expect(navbar?.type).toBe("element"); expect(sidebar?.type).toBe("element"); if (navbar?.type !== "element" || sidebar?.type !== "element") return; expect(navbar.attrs.find((attr) => attr.name === "items")?.value).toBe("{items}"); expect(sidebar.attrs.find((attr) => attr.name === "items")?.value).toBe( '{[{"label":"Cases","href":"/cases"}]}', ); expect(sidebar.attrs.find((attr) => attr.name === "options")?.value).toBe('{{"dense":true}}'); }); test("parses formatted multiline arrays and objects in state", () => { const ast = parse(`component Navigation { state items = [ { "label": "Home", "href": "/" }, { "label": "Services", "children": [ { "label": "Reports", "href": "/reports" } ] } ] state options = { "dense": true, "columns": 4 } view { } }`); expect(ast.states[0]?.expr).toContain('"Services"'); expect(ast.states[0]?.expr).toContain('"Reports"'); expect(ast.states[1]?.expr).toContain('"columns": 4'); }); test("parses WRN 0.3 execution, data, security, and reactivity blocks", () => { const ast = parse(`page Dashboard { runtime = "universal" hydrate = "visible" state count = 1 computed { doubled = count * 2 } effect { console.log(doubled) } security { auth = "required" csrf = "true" } load server { return { count: 1 } } load client { return { refreshed: true } } action save(input) { return input } view { } }`); expect(ast.runtime).toBe("universal"); expect(ast.hydrate).toBe("visible"); expect(ast.computed).toEqual([{ name: "doubled", expr: "count * 2" }]); expect(ast.effects).toHaveLength(1); expect(ast.security).toEqual({ auth: "required", csrf: "true" }); expect(ast.loads.map((load) => load.mode)).toEqual(["server", "client"]); expect(ast.actions).toEqual([expect.objectContaining({ name: "save", args: ["input"] })]); }); test("diagnoses server-only interactive roots and accessibility issues", () => { const diagnostics = diagnose( `component AvatarButton { runtime = "server" state open = false view { } }`, { file: "AvatarButton.wrn", accessibility: true }, ); expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain( "WRN-RUNTIME-SERVER-INTERACTIVE", ); expect(diagnostics.map((diagnostic) => diagnostic.code)).toContain("WRN-A11Y-001"); }); test("formats parser diagnostics with stable codes and source locations", () => { const source = `page Broken { runtime = "quantum"\n view {
} }`; const [diagnostic] = diagnose(source, { file: "Broken.wrn" }); expect(diagnostic?.code).toBe("WRN-RUNTIME-TARGET"); expect(formatDiagnostic(source, diagnostic!)).toContain("Broken.wrn"); }); test("parses keyed each blocks without changing legacy loop syntax", () => { const keyed = parse(`component Rows { props { rows = [] } view { {#each rows as row, index key row.id}

{index}: {row.name}

{/each} } }`); const loop = keyed.view.find((node) => node.type === "each"); expect(loop).toEqual( expect.objectContaining({ type: "each", list: "rows", item: "row", index: "index", key: "row.id", }), ); const legacy = parse(`component Rows { props { rows = [] } view { {#each rows as row}

{row.name}

{/each} } }`); expect(legacy.view.find((node) => node.type === "each")).toEqual( expect.objectContaining({ type: "each", key: undefined }), ); }); test("parses public component event declarations inside props", () => { const ast = parse(`component SearchBox { props { value = "" @event search = function @event clear = function } view { } }`); expect(ast.props.map((prop) => prop.name)).toEqual(["value"]); expect(ast.events).toEqual([{ name: "search" }, { name: "clear" }]); }); test("parses edge and worker execution targets", () => { for (const runtime of ["edge", "worker", "service-worker"] as const) { expect(parse(`page Runtime { runtime = "${runtime}" view {

Hi

} }`).runtime).toBe( runtime, ); } }); test("maps literal unions to their runtime primitive types", async () => { const { runtimeTypeOf } = await import("../src/types.ts"); expect(runtimeTypeOf('"_blank" | "_self"')).toBe("string"); expect(runtimeTypeOf("1 | 2 | 3")).toBe("number"); expect(runtimeTypeOf("true | false")).toBe("boolean"); }); // An apostrophe in prose is not a string. The brace scanner used to treat one // as an opening quote and swallow every brace until the next apostrophe, so a // comment like "the Input's slot" broke the whole component with a baffling // "Unbalanced braces" error pointing at the block's first line. test("comments may contain apostrophes without unbalancing a block", () => { const source = `component Demo { style { /* The panel's own color -- do not inherit it. */ .demo { color: red; } // A trailing note about the card's border. .demo-b { color: blue; } } view {

Docs at https://example.com/a//b are not comments.

} } `; const ast = parse(source); expect(ast.name).toBe("Demo"); expect(ast.styles.join(" ")).toContain(".demo-b"); // The URL in view text must survive: `//` is only a comment at line start. expect(JSON.stringify(ast.view)).toContain("https://example.com/a//b"); }); // A regex literal is not a string and not a pair of braces. The brace scanner // knew about quotes and comments but had no case for regexes, so a quote inside // one opened a phantom string that swallowed every brace until the next quote, // and a lone `{` or `}` inside one miscounted depth. Both failed the whole // component -- with a green build in the reported case, because the damage // landed in generated output rather than at parse time. test("regex literals do not unbalance a block", () => { const mk = (body: string) => `page P { load server { ${body} return { x }; } view {
{x}
} } `; // A quote inside a regex used to open a string that ran to the next quote. expect(parse(mk(` const x = /it's/.test("its");`)).name).toBe("P"); expect(parse(mk(` const x = /"/.test("q");`)).name).toBe("P"); // A brace inside a regex used to be counted as block depth. expect(parse(mk(` const x = /\{/.test("{");`)).name).toBe("P"); expect(parse(mk(` const x = /}/.test("}");`)).name).toBe("P"); // A brace quantifier is balanced, but must not be counted either. expect(parse(mk(` const x = /^a{2,3}$/.test("aa");`)).name).toBe("P"); // A `/` inside a character class does not close the regex. expect(parse(mk(` const x = /[/'"{]/.test("/");`)).name).toBe("P"); // The case that already worked must keep working. expect(parse(mk(` const x = "a-b".replace(/-/g, " ");`)).name).toBe("P"); }); // Division must not be mistaken for a regex, or the scanner would swallow code // from the `/` to the next one and lose any braces in between. test("division is not treated as a regex literal", () => { const source = `page P { load server { const half = 10 / 2; const ratio = (a + b) / 2; const each = items[0] / total; if (half > 1) { return { half }; } return { half: 0 }; } view {
{half}
} } `; expect(parse(source).name).toBe("P"); });