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"); }); // `skipTrivia` skipped `// line comments` but not `/* block comments */`, so a // block comment between page or component members failed with a bare // "Unexpected character '/'". Block comments inside a braced body already // worked, which made the failure look arbitrary: the same comment moved a few // lines parsed or did not depending on whether it sat inside a block. test("block comments are allowed between members", () => { const page = `page P { /* Explains the state below. */ state x = 1 /* Explains the view below. */ view {
{x}
} } `; expect(parse(page).name).toBe("P"); const component = `component C { /* A note about this component. */ state y = 2 view {

{y}

} } `; expect(parse(component).name).toBe("C"); // A block comment before the opening brace, and a multi-line one. const spaced = `/* Leading note. */ page Q { /* * A multi-line note about the view. */ view {
ok
} } `; expect(parse(spaced).name).toBe("Q"); }); // Block comments inside props {} stay a deliberate, explained error -- the // props parser cannot represent them, and silently skipping one would drop a // declaration the author believed was there. test("a block comment inside props is still refused with its own message", () => { const source = `component C { props { /* not allowed here */ name: string } view {

{name}

} } `; expect(() => parse(source)).toThrow("Block comments are not allowed inside props"); });