import { describe, expect, test } from "bun:test"; import { applyCommand, createRegistry, decodeClipboard, defineBlock, defineDocumentKind, duplicateNodes, encodeClipboard, json, object, record, resolveDocumentVariables, safeUrl, string, validateDocument, type BlockDocument, } from "../src/index.ts"; let serial = 0; const id = () => `id-${++serial}`; const styles = record(json) as never; const text = defineBlock({ manifest: { type: "content.text", version: 1, name: { key: "text", fallback: "Text" }, icon: "text", category: "content", capabilities: ["content"], props: object({ text: string({ max: 20 }) }) as never, styles, slots: {}, }, create: ({ id }) => ({ id: id(), type: "content.text", version: 1, props: { text: "" }, styles: {}, slots: {}, }), renderers: { html: ({ node }) => String(node.props.text) }, textRenderer: ({ node }) => String(node.props.text), }); const root = defineBlock({ manifest: { type: "layout.root", version: 1, name: { key: "root", fallback: "Root" }, icon: "root", category: "layout", props: object({}) as never, styles, slots: { content: { accepts: { capabilities: ["content"] }, maxChildren: 5 } }, }, create: ({ id }) => ({ id: id(), type: "layout.root", version: 1, props: {}, styles: {}, slots: { content: [] }, }), renderers: { html: ({ slots }) => slots.content }, textRenderer: ({ slots }) => slots.content, }); const kind = defineDocumentKind({ kind: "test", version: 1, metadata: object({ title: string() }) as never, rootTypes: ["layout.root"], blocks: [root, text], variables: { recipientName: "string" }, breakpoints: {}, capabilities: [], targets: ["html"], limits: { maxBytes: 10_000, maxNodes: 20, maxDepth: 5, maxChildrenPerSlot: 5, maxStringLength: 100, maxOutputBytes: 1_000, }, urlSchemes: ["https"], }); const registry = createRegistry(kind.blocks).freeze(); const doc = (): BlockDocument => ({ schemaVersion: 1, kind: "test", meta: { title: "x" }, theme: { tokens: {} }, root: { id: "root", type: "layout.root", version: 1, props: {}, styles: {}, slots: { content: [] }, }, }); describe("dynamic blocks engine", () => { test("rejects duplicate registrations and freezes registries", () => { expect(() => createRegistry([text, text])).toThrow(/Duplicate block/); const frozen = createRegistry([text]).freeze(); expect(() => (frozen as never as { register(value: unknown): void }).register(text)).toThrow(); }); test("validates named slots and applies invertible commands", () => { const node = text.create({ id }); node.props.text = "Hello"; const inserted = applyCommand( doc(), { type: "insert", location: { parentId: "root", slot: "content", index: 0 }, nodes: [node] }, kind, registry, ); expect(inserted.ok).toBe(true); if (!inserted.ok) return; expect(inserted.value.document.root.slots.content[0]?.props.text).toBe("Hello"); const undone = applyCommand(inserted.value.document, inserted.value.inverse, kind, registry); expect(undone.ok && undone.value.document.root.slots.content).toEqual([]); }); test("rejects cycles and invalid slots", () => { const bad = doc(); bad.root.slots.missing = []; expect(validateDocument(bad, kind, registry).ok).toBe(false); }); test("clipboard is deterministic and remaps ids", () => { const node = text.create({ id }); node.props.text = "Hello"; const encoded = encodeClipboard("test", [node]); expect(encoded).toBe(encodeClipboard("test", [node])); const decoded = decodeClipboard(encoded, id); expect(decoded.ok && decoded.value.roots[0]?.id).not.toBe(node.id); expect(duplicateNodes([node], id)[0]?.props).toEqual(node.props); }); test("moves forward within one slot without an off-by-one error", () => { const document = doc(); document.root.slots.content = ["a", "b", "c"].map((nodeId) => ({ ...text.create({ id: () => nodeId }), props: { text: nodeId }, })); const moved = applyCommand( document, { type: "move", intent: { nodeIds: ["a"], from: [{ parentId: "root", slot: "content", index: 0 }], to: { parentId: "root", slot: "content", index: 3 }, }, }, kind, registry, ); expect(moved.ok && moved.value.document.root.slots.content.map((node) => node.id)).toEqual([ "b", "c", "a", ]); }); test("duplicates blocks with deterministic injected ids", () => { const document = doc(); document.root.slots.content = [{ ...text.create({ id: () => "a" }), props: { text: "A" } }]; const duplicated = applyCommand( document, { type: "duplicate", nodeIds: ["a"], ids: ["a-copy"], }, kind, registry, ); expect( duplicated.ok && duplicated.value.document.root.slots.content.map((node) => node.id), ).toEqual(["a", "a-copy"]); }); test("validates and resolves typed personalization tokens", () => { const document = doc(); document.root.slots.content = [ { ...text.create({ id: () => "personalized" }), props: { text: "Hi {{recipientName}}" } }, ]; expect(validateDocument(document, kind, registry).ok).toBe(true); const resolved = resolveDocumentVariables(document, kind.variables, { recipientName: "Ada" }); expect(resolved.ok && resolved.value.root.slots.content[0]?.props.text).toBe("Hi Ada"); document.root.slots.content[0]!.props.text = "{{notAllowed}}"; expect(validateDocument(document, kind, registry).ok).toBe(false); }); test("safe URLs allow bounded variable templates without allowing unsafe schemes", () => { const schema = safeUrl({ schemes: ["https"] }); expect(schema.parse("https://example.com/u/{{recipientName}}").ok).toBe(true); expect(schema.parse("{{unsubscribeUrl}}").ok).toBe(true); expect(schema.parse("javascript:alert(1)").ok).toBe(false); }); });