feat(framework): make component props reactive
This commit is contained in:
@@ -200,9 +200,10 @@ test("typed props enforce required values and runtime-compatible input", async (
|
||||
}`);
|
||||
const render = component.render as (props?: Record<string, unknown>) => string;
|
||||
expect(() => render()).toThrow("TypedInput requires prop 'label' (string)");
|
||||
expect(render({ label: "Total", count: "4", enabled: "true" })).toContain(
|
||||
"<span>Total:4:true</span>",
|
||||
);
|
||||
const rendered = render({ label: "Total", count: "4", enabled: "true" });
|
||||
expect(rendered).toContain('<span data-text="label">Total</span>');
|
||||
expect(rendered).toContain('<span data-text="count">4</span>');
|
||||
expect(rendered).toContain('<span data-text="enabled">true</span>');
|
||||
expect(() => render({ label: "Total", count: "many" })).toThrow("Expected a finite number prop");
|
||||
expect(() => render({ label: "Total", enabled: "sometimes" })).toThrow("Expected a boolean prop");
|
||||
});
|
||||
@@ -238,13 +239,14 @@ test("dynamic HTML boolean attributes are omitted when false", async () => {
|
||||
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");
|
||||
expect(enabled).not.toContain("<button disabled");
|
||||
expect(enabled).not.toContain("<input checked");
|
||||
expect(enabled).not.toContain("<input required");
|
||||
|
||||
const disabled = render({ disabled: "true", checked: "true", required: "true" });
|
||||
expect(disabled).toContain("<button disabled>");
|
||||
expect(disabled).toContain("<input checked required>");
|
||||
expect(disabled).toContain("<button disabled");
|
||||
expect(disabled).toContain("<input checked");
|
||||
expect(disabled).toContain(" required");
|
||||
});
|
||||
|
||||
test("components safely forward undeclared HTML attributes to their root", async () => {
|
||||
@@ -282,7 +284,7 @@ test("components safely forward undeclared HTML attributes to their root", async
|
||||
expect(html).not.toContain("variant=");
|
||||
expect(html).not.toContain("onclick=");
|
||||
expect(html).not.toContain("style=");
|
||||
expect(html).not.toContain("data-wrn-bind");
|
||||
expect(html).not.toContain('data-wrn-bind-0="unsafe"');
|
||||
});
|
||||
|
||||
test("an explicit attrs spread overrides automatic root forwarding", async () => {
|
||||
@@ -300,12 +302,13 @@ test("an explicit attrs spread overrides automatic root forwarding", async () =>
|
||||
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).toContain('<span class="wrapper"');
|
||||
expect(html).not.toContain('<span formaction="/save"');
|
||||
expect(html).toContain('<button formaction="/save">Save</button>');
|
||||
expect(html).toContain('<button formaction="/save">');
|
||||
expect(html).toContain('<span data-text="label">Save</span>');
|
||||
});
|
||||
|
||||
test("stateless component bakes props into server HTML (zero JS)", async () => {
|
||||
test("prop-driven component renders SSR content and exposes reactive prop signals", 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}`,
|
||||
);
|
||||
@@ -313,10 +316,11 @@ test("stateless component bakes props into server HTML (zero JS)", async () => {
|
||||
const out = render({ label: "Save <b>", variant: "primary", class: "mt-2" });
|
||||
expect(out).toContain('class="wire-btn wire-btn--primary mt-2"');
|
||||
expect(out).toContain("Save <b>"); // html-escaped
|
||||
expect(out).not.toContain("data-scope"); // no reactivity → no scope
|
||||
expect(out).toContain("data-scope");
|
||||
expect(out).toContain('data-text="label"');
|
||||
});
|
||||
|
||||
test("stateful component: state text baked into a reactive data-text span, prop text baked", async () => {
|
||||
test("stateful component keeps both prop and state text reactive", async () => {
|
||||
const mod = await compileAndImport(
|
||||
`component Counter {\n props {\n start = 0\n label = "Count"\n }\n state count = start\n view { <button @click="count++">{label}: {count}</button> }\n}`,
|
||||
);
|
||||
@@ -324,8 +328,9 @@ test("stateful component: state text baked into a reactive data-text span, prop
|
||||
const out = render({ start: "10", label: "Score" });
|
||||
expect(out).toContain('data-scope="start: 10, label: "Score", count: 10"');
|
||||
expect(out).toContain('data-on-click="count++"');
|
||||
// label baked as static text; count baked as its initial value AND kept live.
|
||||
expect(out).toContain('Score: <span data-text="count">10</span>');
|
||||
expect(out).toContain(
|
||||
'<span data-text="label">Score</span>: <span data-text="count">10</span>',
|
||||
);
|
||||
|
||||
expect(out).toContain('data-scope="');
|
||||
|
||||
@@ -1083,6 +1088,19 @@ page Home {
|
||||
|
||||
expect(output).toContain("__wrnexusPropAttr([");
|
||||
});
|
||||
test("nested component props retain parent-owned reactive bindings", () => {
|
||||
const output = generate(
|
||||
parse(`component Parent {
|
||||
props { value = 0 }
|
||||
state count = value
|
||||
view { <Child value={count} /> }
|
||||
}`),
|
||||
);
|
||||
|
||||
expect(output).toContain("data-wrn-prop-bind-0");
|
||||
expect(output).toContain("["value","{count}"]");
|
||||
expect(output).toContain('lowerName.startsWith("data-wrn-prop-bind-")');
|
||||
});
|
||||
test("component functions are available during server rendering", () => {
|
||||
const output = generate(
|
||||
parse(`
|
||||
|
||||
Reference in New Issue
Block a user