Files
WRNexusJS/packages/syntax/test/syntax.test.ts
T
Clintchiz cc98bccd6c
Quality / quality (ubuntu-latest) (push) Failing after 12m30s
Quality / quality (windows-latest) (push) Canceled after 0s
@
feat(ui): add DataTable and Toaster, drop the legacy Table, fix overlay dialogs

DataTable replaces the 20-line Table scaffold entirely: columns, sorting,
filtering, pagination, selection, bulk actions, comparison layout, sticky
first column, custom HTML cells, and a remote source driven by a `request`
output rather than a function prop (props travel as HTML attributes, so a
function arrives as its own source text).

Toaster replaces the hand-rolled status div: tone icons, actions, hover
pause/resume and a progress bar.

Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing
ever moved focus into the panel, so the @keydown handler on their root
never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap
and a body scroll lock now live in the reactive runtime, shared by both.

ContextMenu placed pointer menus by subtracting a guessed 340x420 from the
viewport, which pushed every menu that was not that size away from the
pointer; it now positions at the pointer and lets the anchored clamp pull
it back once it can be measured.

The reactive runtime size budget moves 150k -> 175k to cover anchored
overlays, dialog behaviour, the toaster and the DataTable client half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@
2026-08-07 14:57:17 +05:30

238 lines
7.1 KiB
TypeScript

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 { <h1>Fast</h1> }
}`);
expect(ast.renderMode).toBe("static");
expect(ast.hydrate).toBe("none");
expect(() => parse('page Invalid { render = "sometimes" view { <p>No</p> } }')).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 { <h1>Dashboard</h1> }
}`);
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 {
<Navbar items={items} />
<Sidebar items={[{"label":"Cases","href":"/cases"}]} options={{"dense":true}} />
}
}
`);
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 { <nav></nav> }
}`);
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 { <button @click="count++">{doubled}</button> }
}`);
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 {
<button @click="open = true"><img src="/avatar.png"></button>
}
}`,
{ 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 { <main></main> } }`;
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}
<p>{index}: {row.name}</p>
{/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}<p>{row.name}</p>{/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 { <input value="{value}" /> }
}`);
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 { <p>Hi</p> } }`).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 {
<p>Docs at https://example.com/a//b are not comments.</p>
}
}
`;
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");
});