import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; 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"; describe("in-process-attributes", () => { it("reflects successive property values on the same rendered element", async () => { const signer = await Identity.fromPassphrase("in-process attributes"); const runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: StorageManager.emulate({ as: signer }), }); const mock = new MockDoc('
'); const container = mock.document.getElementById("root")!; let render: ReturnType | undefined; try { const tx = runtime.edit(); const vdom = runtime.getCell( signer.did(), "attributes", undefined, tx, ); vdom.set({ type: "vnode", name: "span", props: { "aria-label": "green vote", style: "color: green" }, children: ["V"], }); await tx.commit().settled; render = renderInProcess(container, vdom, { document: mock.document, setProp: mock.renderOptions.setProp, }); await runtime.idle(); render.flush(); const element = container.firstChild; expect(element).toBeDefined(); expect(element).not.toBeNull(); expect(container.innerHTML).toBe( 'V', ); for (const color of ["yellow", "green"]) { const update = runtime.edit(); vdom.withTx(update).key("props").set({ "aria-label": `${color} vote`, style: `color: ${color}`, }); await update.commit().settled; await runtime.idle(); render.flush(); expect(container.firstChild).toBe(element); expect(container.innerHTML).toBe( `V`, ); } } finally { render?.cancel(); await runtime.dispose(); } }); it("renders the same `style` attribute whichever order a style object lists its keys in", async () => { // The reconciler turns a style object into the attribute's text. A // runtime holds the object's keys in the order its writer listed them, // and holds the same object loaded from the store in UTF-8 order. const signer = await Identity.fromPassphrase("in-process style order"); const runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: StorageManager.emulate({ as: signer }), }); const mock = new MockDoc('
'); const container = mock.document.getElementById("root")!; let render: ReturnType | undefined; try { const tx = runtime.edit(); const vdom = runtime.getCell( signer.did(), "style-order", undefined, tx, ); vdom.set({ type: "vnode", name: "div", props: {}, children: [ { type: "vnode", name: "span", props: { style: { display: "none", gap: "0.25rem", alignItems: "center" }, }, children: [], }, { type: "vnode", name: "span", props: { style: { alignItems: "center", display: "none", gap: "0.25rem" }, }, children: [], }, ], }); await tx.commit().settled; render = renderInProcess(container, vdom, { document: mock.document, setProp: mock.renderOptions.setProp, }); await runtime.idle(); render.flush(); const span = ''; expect(container.innerHTML).toBe(`
${span}${span}
`); } finally { render?.cancel(); await runtime.dispose(); } }); it("updates and removes ARIA attributes through the default property setter", async () => { const signer = await Identity.fromPassphrase("in-process ARIA attributes"); const runtime = new Runtime({ apiUrl: new URL(import.meta.url), storageManager: StorageManager.emulate({ as: signer }), }); const mock = new MockDoc('
'); const container = mock.document.getElementById("root")!; const elementDescriptor = Object.getOwnPropertyDescriptor( globalThis, "Element", ); // A supplied document may create nodes outside the ambient DOM realm. // Its elements must still receive attributes through the default setter. Object.defineProperty(globalThis, "Element", { configurable: true, value: class AmbientElement {}, }); let render: ReturnType | undefined; try { const tx = runtime.edit(); const vdom = runtime.getCell( signer.did(), "aria", undefined, tx, ); vdom.set({ type: "vnode", name: "button", props: { "aria-label": "Edit name", "aria-expanded": false }, children: ["Edit"], }); await tx.commit().settled; render = renderInProcess(container, vdom, { document: mock.document }); await runtime.idle(); render.flush(); const element = container.firstChild as Element; expect(element.getAttribute("aria-label")).toBe("Edit name"); expect(element.getAttribute("aria-expanded")).toBe("false"); expect(Object.hasOwn(element, "aria-label")).toBe(false); const updates = [ { "aria-label": "Save name", "aria-expanded": true }, { "aria-label": null, "aria-expanded": false }, { "aria-label": "Edit again", "aria-expanded": undefined }, {}, ]; for (const props of updates) { const update = runtime.edit(); vdom.withTx(update).key("props").set(props); await update.commit().settled; await runtime.idle(); render.flush(); expect(container.firstChild).toBe(element); for (const key of ["aria-label", "aria-expanded"] as const) { const value = props[key]; if (value == null) { expect(element.hasAttribute(key)).toBe(false); } else { expect(element.getAttribute(key)).toBe(String(value)); } } } } finally { if (elementDescriptor) { Object.defineProperty(globalThis, "Element", elementDescriptor); } else { Reflect.deleteProperty(globalThis, "Element"); } try { render?.cancel(); } finally { await runtime.dispose(); } } }); });