import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { $, serializeTree, deserializeTree, resolveBindings, isValidElement, type Element, } from "@inlay/core"; // --- Branding --- describe("branding", () => { it("only $-created objects are valid elements", () => { assert.equal(isValidElement($("com.example.Text", { value: "hi" })), true); assert.equal( isValidElement({ type: "com.example.Text", props: { value: "hi" } }), false ); }); it("primitives and nulls are never elements", () => { for (const v of [null, undefined, 0, "", false, 42, "hello"]) { assert.equal(isValidElement(v), false); } }); }); // --- Round-trip fidelity --- describe("round-trip", () => { it("serialize then deserialize is identity", () => { const original = $("com.example.Text", { value: "hello", count: 42 }); assert.deepEqual(deserializeTree(serializeTree(original)), original); }); it("survives JSON in between", () => { const original = $( "com.example.Stack", {}, $("com.example.Text", { value: "a" }), $("com.example.Text", { value: "b" }) ); const wire = serializeTree(original); assert.deepEqual( deserializeTree(JSON.parse(JSON.stringify(wire))), original ); }); it("preserves keys", () => { const original = $("com.example.Text", { key: "my-key", value: "hi" }); assert.deepEqual(deserializeTree(serializeTree(original)), original); }); it("handles deep nesting", () => { let tree = $("com.example.Text", { value: "leaf" }); for (let i = 0; i < 5; i++) { tree = $("com.example.Box", {}, tree); } assert.deepEqual(deserializeTree(serializeTree(tree)), tree); }); it("elements nested in plain objects survive", () => { const tree = $("com.example.Card", { header: { title: "Hello", icon: $("com.example.Icon", { name: "star" }), }, }); assert.deepEqual(deserializeTree(serializeTree(tree)), tree); }); it("mixed arrays of elements and primitives survive", () => { const tree = $("com.example.RichText", { children: ["Hello ", $("com.example.Bold", { children: "world" }), "!"], }); assert.deepEqual(deserializeTree(serializeTree(tree)), tree); }); }); // --- $ function --- describe("$ function", () => { it("separates key from props", () => { const node = $("com.example.Text", { key: "k1", value: "hi" }); assert.equal(node.key, "k1"); assert.equal((node.props as Record)?.key, undefined); assert.equal((node.props as Record)?.value, "hi"); }); it("auto-keys static children", () => { const node = $( "com.example.Stack", {}, $("com.example.Text", { value: "a" }), $("com.example.Text", { value: "b" }) ); const children = (node.props as Record).children as Array<{ key?: string; }>; assert.deepEqual( children.map((c) => c.key), ["0", "1"] ); }); it("rejects invalid NSIDs", () => { assert.throws(() => $("not-an-nsid", {})); assert.throws(() => $("", {})); }); }); // --- Binding resolution --- // Simple scope lookup for tests — optional chaining, returns undefined on miss function resolvePath(scope: Record, path: string[]): unknown { let current: unknown = scope; for (const seg of path) { if (current == null || typeof current !== "object") return undefined; current = (current as Record)[seg]; } return current; } // Test resolver: looks up path in scope, throws on miss function scopeResolver(scope: Record) { return (path: string[]) => { const value = resolvePath(scope, path); if (value === undefined) { throw new Error(`MISSING:${path.join(".")}`); } return value; }; } describe("resolveBindings", () => { // resolveBindings operates on props (plain objects), not on elements directly. // Elements are opaque — it won't recurse into their props. it("resolves binding in prop", () => { const props = { value: $("at.inlay.Binding", { path: ["reply", "text"] }), }; assert.deepEqual( resolveBindings(props, scopeResolver({ reply: { text: "Hello" } })), { value: "Hello" } ); }); it("resolves binding nested in plain object", () => { const props = { heading: { caption: $("at.inlay.Binding", { path: ["reply", "text"] }), }, }; assert.deepEqual( resolveBindings(props, scopeResolver({ reply: { text: "Hello" } })), { heading: { caption: "Hello" } } ); }); it("resolves binding in array", () => { const props = { items: [$("at.inlay.Binding", { path: ["user", "name"] }), "literal"], }; assert.deepEqual( resolveBindings(props, scopeResolver({ user: { name: "Dan" } })), { items: ["Dan", "literal"] } ); }); it("non-Binding elements are opaque — bindings inside stay unresolved", () => { const binding = $("at.inlay.Binding", { path: ["reply", "text"] }); const text = $("com.example.Text", { value: binding }); const props = { children: [text] }; const result = resolveBindings( props, scopeResolver({ reply: { text: "Hello" } }) ) as Record; // Text element passed through opaque — binding inside is untouched const children = result.children as Element[]; const textProps = children[0].props as Record; assert.ok( isValidElement(textProps.value), "binding inside Text is still an element" ); assert.equal((textProps.value as Element).type, "at.inlay.Binding"); }); it("resolver controls missing behavior", () => { const props = { value: $("at.inlay.Binding", { path: ["nonexistent"] }), }; assert.throws( () => resolveBindings(props, scopeResolver({})), /MISSING:nonexistent/ ); }); it("resolver can return sentinel for missing", () => { const MISSING = Symbol("missing"); const props = { value: $("at.inlay.Binding", { path: ["nope"] }), }; const result = resolveBindings(props, (path) => { const v = resolvePath({}, path); return v === undefined ? MISSING : v; }) as Record; assert.equal(result.value, MISSING); }); it("preserves null scope values", () => { const props = { value: $("at.inlay.Binding", { path: ["nothing"] }), }; assert.deepEqual( resolveBindings(props, (path) => resolvePath({ nothing: null }, path)), { value: null } ); }); it("preserves falsy scope values", () => { const scope = { zero: 0, no: false, empty: "" }; const props = { a: $("at.inlay.Binding", { path: ["zero"] }), b: $("at.inlay.Binding", { path: ["no"] }), c: $("at.inlay.Binding", { path: ["empty"] }), }; assert.deepEqual( resolveBindings(props, (path) => resolvePath(scope, path)), { a: 0, b: false, c: "" } ); }); it("no bindings — identity", () => { const props = { title: "hello", count: 42 }; const result = resolveBindings(props, () => { throw new Error("should not be called"); }); assert.deepEqual(result, props); }); it("throws on Binding missing path", () => { const props = { value: $("at.inlay.Binding", {}), }; assert.throws( () => resolveBindings(props, () => "x"), /Binding missing path/ ); }); });