release: WRNexusJS 0.3.5
This commit is contained in:
@@ -80,6 +80,24 @@ test("parses a component with props and state", () => {
|
||||
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 { <button>{label}</button> }
|
||||
}`;
|
||||
const ast = parse(source);
|
||||
const output = generate(ast);
|
||||
const mod = await compileAndImport(source);
|
||||
const render = mod.render as (props: Record<string, unknown>) => string;
|
||||
|
||||
expect(ast.events).toEqual([{ name: "complete" }]);
|
||||
expect(output).toContain('data-wrn-events="complete"');
|
||||
expect(render({})).toContain('data-wrn-events="complete"');
|
||||
});
|
||||
|
||||
test("typed props, required props, state, custom types, and function parameters compile", () => {
|
||||
const source = `component TypedPicker {
|
||||
types {
|
||||
@@ -169,6 +187,89 @@ test("HTML view: void elements, boolean attrs, comments, lone <", () => {
|
||||
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 {
|
||||
<button disabled="{disabled || loading}">Save</button>
|
||||
<input checked="{checked}" required="{required}" />
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => string;
|
||||
|
||||
const enabled = render();
|
||||
expect(enabled).not.toContain(" disabled");
|
||||
expect(enabled).not.toContain(" checked");
|
||||
expect(enabled).not.toContain(" required");
|
||||
|
||||
const disabled = render({ disabled: "true", checked: "true", required: "true" });
|
||||
expect(disabled).toContain("<button disabled>");
|
||||
expect(disabled).toContain("<input checked required>");
|
||||
});
|
||||
|
||||
test("components safely forward undeclared HTML attributes to their root", async () => {
|
||||
const mod = await compileAndImport(`component ForwardingButton {
|
||||
props {
|
||||
label = "Button"
|
||||
variant = "default"
|
||||
disabled = false
|
||||
}
|
||||
view {
|
||||
<button disabled="{disabled}">{label}</button>
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => 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");
|
||||
});
|
||||
|
||||
test("an explicit attrs spread overrides automatic root forwarding", async () => {
|
||||
const mod = await compileAndImport(`component WrappedControl {
|
||||
props {
|
||||
label = "Control"
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<span class="{class}">
|
||||
<button {...attrs}>{label}</button>
|
||||
</span>
|
||||
}
|
||||
}`);
|
||||
const render = mod.render as (props?: Record<string, unknown>) => string;
|
||||
const html = render({ label: "Save", class: "wrapper", formaction: "/save" });
|
||||
|
||||
expect(html).toContain('<span class="wrapper">');
|
||||
expect(html).not.toContain('<span formaction="/save"');
|
||||
expect(html).toContain('<button formaction="/save">Save</button>');
|
||||
});
|
||||
|
||||
test("stateless component bakes props into server HTML (zero JS)", async () => {
|
||||
const mod = await compileAndImport(
|
||||
`component Button {\n props {\n label = "Button"\n variant = "default"\n class = ""\n }\n view { <button class="wire-btn wire-btn--{variant} {class}">{label}</button> }\n}`,
|
||||
@@ -260,6 +361,24 @@ test("page state attributes bake their initial value and retain a reactive bindi
|
||||
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 {
|
||||
<div data-component="Button" label="{label}" loading="{loading}"></div>
|
||||
}
|
||||
}`);
|
||||
const render = mod.default as (ctx: { url: URL }) => Promise<string>;
|
||||
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 = compileWireFile(
|
||||
`component TodoList {\n state todos = []\n view { <ul><li data-for="t in todos">{t.text}</li></ul> }\n}`,
|
||||
|
||||
Reference in New Issue
Block a user