import { describe, it, expect, vi } from "vitest"; import type { ReactElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { renderGenerativeUI } from "../renderGenerativeUI"; import { createActionRegistry } from "../actionRegistry"; import { layoutVocabulary } from "./layout"; import type { FormControlElementLike } from "./collectFormValues"; const render = (node: unknown) => renderToStaticMarkup(<>{renderGenerativeUI(node, layoutVocabulary)}); const el = ( partial: Partial, ): FormControlElementLike => ({ name: "", type: "text", value: "", disabled: false, hasAttribute: () => false, ...partial, }); const getFooterButtons = (out: ReactElement) => { const footer = (out.props as { children: unknown[] }) .children[2] as ReactElement; const [confirmBtn, cancelBtn] = ( footer.props as { children: [ReactElement | null, ReactElement | null] } ).children; return { confirmBtn, cancelBtn }; }; describe("layoutVocabulary", () => { it("Card renders a section with optional title and padding/background hooks", () => { expect( render({ $type: "Card", title: "T", padding: 2, background: "var(--muted)", }), ).toBe( '
T
', ); }); it("Card flags itself as a surface only when a background or a footer makes it one", () => { expect(render({ $type: "Card", title: "Plain" })).not.toContain( "data-aui-surface", ); expect(render({ $type: "Card", background: "var(--muted)" })).toContain( "data-aui-surface", ); expect(render({ $type: "Card", confirm: { label: "Yes" } })).toContain( "data-aui-surface", ); expect(render({ $type: "Card", cancel: { label: "No" } })).toContain( "data-aui-surface", ); }); it("Card applies background as an inline style so arbitrary CSS values (colors, gradients) render", () => { const html = render({ $type: "Card", background: "linear-gradient(90deg, #3b82f6, #1d4ed8)", }); expect(html).toContain( 'style="background:linear-gradient(90deg, #3b82f6, #1d4ed8);color:white"', ); }); it("Card sets its text color to white whenever a background is provided", () => { const html = render({ $type: "Card", background: "#1d4ed8" }); expect(html).toContain("color:white"); }); it("Card omits the style attribute when background is not provided", () => { expect(render({ $type: "Card", title: "T" })).not.toContain("style="); }); it("Card without a title omits the header", () => { expect(render({ $type: "Card" })).toBe( '
', ); }); it("Col renders a div with gap/align hooks and nested children", () => { expect( render({ $type: "Col", gap: 2, align: "center", children: { $type: "Text", value: "x" } as never, }), ).toContain('data-aui="col"'); }); it("Row renders a div with gap/align/justify hooks", () => { const html = render({ $type: "Row", gap: 4, align: "start", justify: "between", }); expect(html).toBe( '
', ); }); it("Spacer renders an empty div", () => { expect(render({ $type: "Spacer" })).toBe('
'); }); it("Badge renders a span with the value and variant hook", () => { expect(render({ $type: "Badge", value: "new", variant: "success" })).toBe( 'new', ); }); it("ignores malformed card and badge text properties", () => { expect( render({ $type: "Card", title: { unexpected: true }, confirm: { label: { unexpected: true } }, cancel: { label: { unexpected: true } }, }), ).toBe( '
', ); expect(render({ $type: "Badge", value: { unexpected: true } })).toBe( '', ); }); }); describe("layoutVocabulary Box", () => { it("renders a bare div with no style attribute when no props are given", () => { expect(render({ $type: "Box" })).toBe('
'); }); it("applies width/height as inline pixel lengths when given numbers", () => { const html = render({ $type: "Box", width: 200, height: 8 }); expect(html).toContain('style="width:200px;height:8px"'); }); it("applies width/height verbatim when given strings", () => { const html = render({ $type: "Box", width: "100%", height: "1.5rem" }); expect(html).toContain('style="width:100%;height:1.5rem"'); }); it("applies a numeric radius as an inline pixel border-radius", () => { const html = render({ $type: "Box", radius: 6 }); expect(html).toContain('style="border-radius:6px"'); expect(html).not.toContain("data-aui-radius"); }); it('applies radius "full" as a 9999px inline border-radius and a data-aui-radius hook', () => { const html = render({ $type: "Box", radius: "full" }); expect(html).toContain('data-aui-radius="full"'); expect(html).toContain('style="border-radius:9999px"'); }); it("applies background as an inline style", () => { const html = render({ $type: "Box", background: "#3b82f6" }); expect(html).toContain('style="background:#3b82f6"'); }); it("renders a progress bar as a track Box containing a partial-width fill Box", () => { const html = render({ $type: "Box", width: "100%", height: 8, radius: "full", background: "var(--muted)", children: { $type: "Box", width: "60%", height: 8, radius: "full", background: "var(--primary)", }, }); expect(html).toBe( '
' + '
' + "
", ); }); it("renders nested children alongside style props", () => { const html = render({ $type: "Box", background: "red", children: "content", }); expect(html).toBe( '
content
', ); }); }); describe("layoutVocabulary Card asForm/confirm/cancel", () => { it("Card with asForm renders a form element with the data-aui-asform hook", () => { const html = render({ $type: "Card", asForm: true, title: "T" }); expect(html).toContain("
{ const html = render({ $type: "Card" }); expect(html).toContain(" { expect(render({ $type: "Card" })).not.toContain("card-footer"); }); it("Card renders a footer when only confirm is present", () => { expect(render({ $type: "Card", confirm: { label: "OK" } })).toContain( 'data-aui="card-footer"', ); }); it("Card renders a footer when only cancel is present", () => { expect(render({ $type: "Card", cancel: { label: "Cancel" } })).toContain( 'data-aui="card-footer"', ); }); it("Card confirm renders a submit button when asForm is set", () => { const html = render({ $type: "Card", asForm: true, confirm: { label: "Save" }, }); expect(html).toContain('type="submit" data-aui="card-confirm"'); expect(html).toContain("Save"); }); it("Card confirm renders a click button when asForm is not set", () => { const html = render({ $type: "Card", confirm: { label: "Save" } }); expect(html).toContain('type="button" data-aui="card-confirm"'); }); it("Card cancel always renders a plain button regardless of asForm", () => { const html = render({ $type: "Card", asForm: true, cancel: { label: "Dismiss" }, }); expect(html).toContain('type="button" data-aui="card-cancel"'); }); }); describe("layoutVocabulary Card asForm/confirm/cancel dispatch", () => { it("Card asForm submit collects named values and dispatches confirm.$action with $input", () => { const handler = vi.fn(); const registry = createActionRegistry({ save: handler }); const out = layoutVocabulary.Card.render({ asForm: true, confirm: { label: "Save", $action: { type: "save" } }, $status: "done", $dispatch: registry.dispatch, }) as ReactElement; const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit; const event = { preventDefault: vi.fn(), currentTarget: { elements: [el({ name: "email", type: "email", value: "a@x.com" })], }, }; onSubmit(event); expect(event.preventDefault).toHaveBeenCalled(); expect(handler).toHaveBeenCalledWith({ payload: { type: "save", $input: { email: "a@x.com" } }, }); }); it("Card confirm click (non-asForm) fires confirm.$action without $input", () => { const handler = vi.fn(); const registry = createActionRegistry({ save: handler }); const out = layoutVocabulary.Card.render({ confirm: { label: "Save", $action: { type: "save" } }, $status: "done", $dispatch: registry.dispatch, }) as ReactElement; const { confirmBtn } = getFooterButtons(out); const onClick = (confirmBtn!.props as { onClick: () => void }).onClick; onClick(); expect(handler).toHaveBeenCalledWith({ payload: { type: "save" } }); }); it("Card confirm button has no onClick when asForm is set (the form submit handles it)", () => { const out = layoutVocabulary.Card.render({ asForm: true, confirm: { label: "Save", $action: { type: "save" } }, $status: "done", }) as ReactElement; const { confirmBtn } = getFooterButtons(out); expect( (confirmBtn!.props as { onClick?: unknown }).onClick, ).toBeUndefined(); }); it("Card cancel click fires cancel.$action without $input regardless of asForm", () => { const handler = vi.fn(); const registry = createActionRegistry({ dismiss: handler }); const out = layoutVocabulary.Card.render({ asForm: true, cancel: { label: "Dismiss", $action: { type: "dismiss" } }, $status: "done", $dispatch: registry.dispatch, }) as ReactElement; const { cancelBtn } = getFooterButtons(out); const onClick = (cancelBtn!.props as { onClick: () => void }).onClick; onClick(); expect(handler).toHaveBeenCalledWith({ payload: { type: "dismiss" } }); }); });