/** * Rendering with the reconciler and the applicator in one process, which is * how the CLI turns a piece's UI into HTML. * * The tree under test is shaped the way the renderer's schema delivers one: * props and children arrive as cells rather than as plain values, and the * children are cells one level down again. A renderer that cannot follow those * cells produces a bare tag with nothing inside it. */ import { assertEquals } from "@std/assert"; import { Identity } from "@commonfabric/identity"; import { Runtime } from "@commonfabric/runner"; import { StorageManager } from "@commonfabric/runner/storage/cache.deno"; import { renderInProcess } from "../src/in-process.ts"; import { MockDoc } from "../src/mock-doc.ts"; const signer = await Identity.fromPassphrase("test in-process render"); const space = signer.did(); function makeRuntime() { return new Runtime({ apiUrl: new URL(import.meta.url), storageManager: StorageManager.emulate({ as: signer }), }); } function makeContainer() { const mock = new MockDoc( '
', ); return { container: mock.document.getElementById("root")!, options: { document: mock.document, setProp: mock.renderOptions.setProp, }, }; } Deno.test("in-process render - piece UI to HTML", async (t) => { const runtime = makeRuntime(); try { await t.step("renders nested elements, props and text", async () => { const tx = runtime.edit(); const vdom = runtime.getCell(space, "greeting", undefined, tx); vdom.set({ type: "vnode", name: "div", props: { id: "hello" }, children: [ { type: "vnode", name: "p", props: { class: "line" }, children: ["Hello world!"], }, { type: "vnode", name: "span", props: {}, children: ["and again"], }, ], }); await tx.commit().settled; const { container, options } = makeContainer(); const render = renderInProcess(container, vdom, options); await runtime.idle(); render.flush(); assertEquals( container.innerHTML, '

Hello world!

' + "and again
", ); render.cancel(); }); await t.step("follows a cell in child position", async () => { const tx = runtime.edit(); const label = runtime.getCell(space, "label", undefined, tx); label.set("first"); const vdom = runtime.getCell(space, "labeled", undefined, tx); vdom.set({ type: "vnode", name: "div", props: {}, children: [label], }); await tx.commit().settled; const { container, options } = makeContainer(); const render = renderInProcess(container, vdom, options); await runtime.idle(); render.flush(); assertEquals(container.innerHTML, "
first
"); const update = runtime.edit(); label.withTx(update).set("second"); await update.commit().settled; await runtime.idle(); render.flush(); assertEquals(container.innerHTML, "
second
"); render.cancel(); }); await t.step("converts a style object to a style attribute", async () => { const tx = runtime.edit(); const vdom = runtime.getCell(space, "styled", undefined, tx); vdom.set({ type: "vnode", name: "div", props: { style: { backgroundColor: "red", marginTop: 10 } }, children: [], }); await tx.commit().settled; const { container, options } = makeContainer(); const render = renderInProcess(container, vdom, options); await runtime.idle(); render.flush(); assertEquals( container.innerHTML, '
', ); render.cancel(); }); await t.step("cancelling clears the container", async () => { const tx = runtime.edit(); const vdom = runtime.getCell(space, "transient", undefined, tx); vdom.set({ type: "vnode", name: "div", props: {}, children: ["gone soon"], }); await tx.commit().settled; const { container, options } = makeContainer(); const render = renderInProcess(container, vdom, options); await runtime.idle(); render.flush(); assertEquals(container.innerHTML, "
gone soon
"); render.cancel(); assertEquals(container.innerHTML, ""); }); await t.step("announces each applied batch", async () => { const tx = runtime.edit(); const text = runtime.getCell( space, "watched-text", undefined, tx, ); text.set("one"); const vdom = runtime.getCell(space, "watched", undefined, tx); vdom.set({ type: "vnode", name: "div", props: {}, children: [text], }); await tx.commit().settled; const { container, options } = makeContainer(); const applied: string[] = []; const render = renderInProcess(container, vdom, { ...options, onApplied: () => applied.push(container.innerHTML), }); await runtime.idle(); render.flush(); assertEquals(applied.length > 0, true); assertEquals(applied[applied.length - 1], "
one
"); const update = runtime.edit(); text.withTx(update).set("two"); await update.commit().settled; await runtime.idle(); render.flush(); assertEquals(applied[applied.length - 1], "
two
"); render.cancel(); }); } finally { await runtime.dispose(); } }); Deno.test("in-process render - reports a bound prop without a runtime client", async (t) => { const runtime = makeRuntime(); try { await t.step("a $value binding names its cell", async () => { const tx = runtime.edit(); const value = runtime.getCell( space, "bound-value", undefined, tx, ); value.set("typed"); const vdom = runtime.getCell(space, "bound", undefined, tx); vdom.set({ type: "vnode", name: "cf-input", props: { $value: value }, children: [], }); await tx.commit().settled; const { container, options } = makeContainer(); const render = renderInProcess(container, vdom, options); await runtime.idle(); render.flush(); // No client-side runtime is present, so the applicator hands the element // the reference rather than a live handle; the mock document renders any // object-valued property as "[binding]". assertEquals( container.innerHTML, '', ); render.cancel(); }); } finally { await runtime.dispose(); } });