import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { render, VNode } from "../src/index.ts"; import { MockDoc } from "../src/mock-doc.ts"; import { type Cell, createBuilder, type IExtendedStorageTransaction, Runtime, } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; import * as assert from "./assert.ts"; import { Identity } from "@commontools/identity"; import { h } from "@commontools/html"; const signer = await Identity.fromPassphrase("test operator"); const space = signer.did(); describe("recipes with HTML", () => { let mock: MockDoc; let storageManager: ReturnType; let runtime: Runtime; let tx: IExtendedStorageTransaction; let lift: ReturnType["commontools"]["lift"]; let derive: ReturnType["commontools"]["derive"]; let recipe: ReturnType["commontools"]["recipe"]; let str: ReturnType["commontools"]["str"]; let UI: ReturnType["commontools"]["UI"]; beforeEach(() => { mock = new MockDoc( `
`, ); storageManager = StorageManager.emulate({ as: signer }); // Create runtime with the shared storage provider // We need to bypass the URL-based configuration for this test runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager, }); tx = runtime.edit(); const { commontools } = createBuilder(); ({ lift, derive, recipe, str, UI } = commontools); }); afterEach(async () => { await tx.commit(); await runtime?.dispose(); await storageManager?.close(); }); it("should render a simple UI", async () => { const simpleRecipe = recipe<{ value: number }>( "Simple UI Recipe", ({ value }) => { const doubled = lift((x: number) => x * 2)(value); return { [UI]: h("div", null, doubled) }; }, ); const result = runtime.run( tx, simpleRecipe, { value: 5 }, runtime.getCell(space, "simple-ui-result", undefined, tx), ); tx.commit(); const resultValue = await result.pull(); assert.matchObject(resultValue, { [UI]: { type: "vnode", name: "div", props: {}, children: [10], }, }); }); it("works with mapping over a list", async () => { const { document, renderOptions } = mock; type Item = { title: string; done: boolean }; const todoList = recipe<{ title: string; items: Item[]; }>("todo list", ({ title, items }) => { return { [UI]: h( "div", null, h("h1", null, title), h( "ul", null, items.map((item, i) => h("li", { key: derive(i, (i) => i.toString()) }, item.title) ) as VNode[], ), ), }; }); const result = runtime.run( tx, todoList, { title: "test", items: [ { title: "item 1", done: false }, { title: "item 2", done: true }, ], }, runtime.getCell(space, "todo-list-result", undefined, tx), ) as Cell<{ [UI]: VNode }>; tx.commit(); await result.pull(); const root = document.getElementById("root")!; const cell = result.key(UI); render(root, cell.get(), renderOptions); assert.equal( root.innerHTML, '

test

  • item 1
  • item 2
', ); }); it("works with paths on nested recipes", async () => { const { document, renderOptions } = mock; const todoList = recipe<{ title: { name: string }; items: { title: string; done: boolean }[]; }>("todo list", ({ title }) => { const { [UI]: summaryUI } = recipe< { title: { name: string } }, { [UI]: VNode } >( "summary", ({ title }) => { return { [UI]: h("div", null, title.name) }; }, )({ title }); return { [UI]: h("div", null, summaryUI as VNode) }; }); const result = runtime.run( tx, todoList, { title: { name: "test" }, items: [ { title: "item 1", done: false }, { title: "item 2", done: true }, ], }, runtime.getCell(space, "nested-todo-result", undefined, tx), ) as Cell<{ [UI]: VNode }>; tx.commit(); await result.pull(); const root = document.getElementById("root")!; const cell = result.key(UI); render(root, cell, renderOptions); assert.equal(root.innerHTML, "
test
"); }); it("works with str", async () => { const { document, renderOptions } = mock; const strRecipe = recipe<{ name: string }>("str recipe", ({ name }) => { return { [UI]: h("div", null, str`Hello, ${name}!`) }; }); const result = runtime.run( tx, strRecipe, { name: "world" }, runtime.getCell(space, "str-recipe-result", undefined, tx), ) as Cell<{ [UI]: VNode }>; tx.commit(); await result.pull(); const root = document.getElementById("root")!; const cell = result.key(UI); render(root, cell.get(), renderOptions); assert.equal(root.innerHTML, "
Hello, world!
"); }); it("works with nested maps of non-objects", async () => { const { document, renderOptions } = mock; const entries = lift((row: object) => Object.entries(row)); const data = [ { test: 123, ok: false }, { test: 345, another: "xxx" }, { test: 456, ok: true }, ]; const nestedMapRecipe = recipe[]>( "nested map recipe", (data) => ({ [UI]: h( "div", null, data.map((row: Record) => h( "ul", null, entries(row).map((input) => h("li", null, [input[0], ": ", str`${input[1]}`]) ) as VNode[], ) ) as VNode[], ), }), ); const result = runtime.run( tx, nestedMapRecipe, data, runtime.getCell(space, "nested-map-result", undefined, tx), ) as Cell<{ [UI]: VNode }>; tx.commit(); await result.pull(); const root = document.getElementById("root")!; const cell = result.key(UI); render(root, cell.get(), renderOptions); assert.equal( root.innerHTML, "
  • test: 123
  • ok: false
  • test: 345
  • another: xxx
  • test: 456
  • ok: true
", ); }); it("detects cyclic cell references using .equals() not object identity", async () => { const { document, renderOptions } = mock; // Get a cell twice - this creates different Cell wrapper objects // but they reference the same underlying cell data const cellId = "cyclic-cell-test"; const cell1 = runtime.getCell<{ ui: VNode }>(space, cellId, undefined, tx); const cell2 = runtime.getCell<{ ui: VNode }>(space, cellId, undefined, tx); // Verify they are different objects but equal cells assert.equal(cell1 === cell2, false, "Should be different wrapper objects"); assert.equal(cell1.equals(cell2), true, "Should be equal cells"); // Also verify that key projections are equal assert.equal( cell1.key("ui").equals(cell2.key("ui")), true, "Key projections should be equal", ); // Create a cyclic structure: cell1.ui contains cell2 (which is the same cell) // This simulates a cell whose UI references itself cell1.set({ ui: { type: "vnode", name: "div", props: {}, children: [cell2.key("ui")], // Using cell2 wrapper, but it's the same cell }, }); tx.commit(); await cell1.pull(); const root = document.getElementById("root")!; render(root, cell1.key("ui"), renderOptions); // Should detect the cycle and render placeholder, not infinite loop // MockDoc doesn't properly reflect textContent/title in innerHTML, // but the span placeholder should be present assert.equal( root.innerHTML, "
", "Should render div with cycle placeholder span", ); }); });