import { describe, it, expect } from "vitest"; import { h, createRoot, createComponent, Fragment, jsx, jsxs, jsxDEV } from "../src/core/h.js"; import { isUElement, isURoot, isUPrimitive, UJSX } from "../src/core/schema.js"; import type { UNode, UElement } from "../src/core/schema.js"; import { Value } from "@alkdev/typebox/value"; import { Context } from "../src/core/context.js"; import { TransformRegistry, childCtx, ctx as transformCtx } from "../src/transform/registry.js"; import type { Direction } from "../src/core/context.js"; import { ValuePointer, selectNode, setNode } from "../src/core/pointer.js"; import { signal, computed, ReactiveRoot, reactiveComponent, reactiveElement } from "../src/core/reactive.js"; import { createPubSubEmitter } from "../src/core/events.js"; import { createRoot as createHostRoot } from "../src/host/config.js"; import type { HostConfig } from "../src/host/config.js"; describe("h()", () => { it("creates UElement", () => { const el = h("div", { class: "test" }, "hello"); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.type).toBe("div"); expect(el.props.class).toBe("test"); expect(el.children).toEqual(["hello"]); } }); it("with null props creates empty props object", () => { const el = h("p", null, "text"); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.props).toEqual({}); expect(el.children).toEqual(["text"]); } }); it("with root type creates URoot", () => { const root = h("root", { id: "test" }, "child1", "child2"); expect(isURoot(root)).toBe(true); if (isURoot(root)) { expect(root.type).toBe("root"); expect(root.props.id).toBe("test"); expect(root.children).toEqual(["child1", "child2"]); } }); }); describe("createRoot", () => { it("creates URoot with id", () => { const root = createRoot("my-root", "a", "b"); expect(isURoot(root)).toBe(true); if (isURoot(root)) { expect(root.type).toBe("root"); expect(root.props.id).toBe("my-root"); } }); }); describe("Fragment", () => { it("flattens children and removes null/false", () => { const result = Fragment({ children: ["a", null, "b", false, "c"] as UNode[] }); expect(result).toEqual(["a", "b", "c"]); }); }); describe("jsx runtime aliases", () => { it("jsx/jsxs/jsxDEV are aliases for h", () => { expect(jsx).toBe(h); expect(jsxs).toBe(h); expect(jsxDEV).toBe(h); }); }); describe("createComponent", () => { it("adds displayName and targets", () => { const MyComp = createComponent("MyComp", (props) => h("div", null, props.text), ["markdown"]); expect(MyComp.displayName).toBe("MyComp"); expect(MyComp.targets).toEqual(["markdown"]); }); }); describe("type guards", () => { it("isUElement discriminates", () => { expect(isUElement(h("div", null))).toBe(true); expect(isUElement("text")).toBe(false); expect(isUElement(null)).toBe(false); expect(isUElement(createRoot("r"))).toBe(false); }); it("isURoot discriminates", () => { expect(isURoot(createRoot("r"))).toBe(true); expect(isURoot(h("div", null))).toBe(false); }); it("isUPrimitive discriminates", () => { expect(isUPrimitive("text")).toBe(true); expect(isUPrimitive(42)).toBe(true); expect(isUPrimitive(true)).toBe(true); expect(isUPrimitive(null)).toBe(true); expect(isUPrimitive(h("div", null))).toBe(false); }); }); describe("UElement key field", () => { it("h('div', { key: 'a' }) produces UElement with key: 'a' and no key in props", () => { const el = h("div", { key: "a" }); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBe("a"); expect(el.props.key).toBeUndefined(); } }); it("h('div', { key: 'b', class: 'x' }) — key promoted, class remains in props", () => { const el = h("div", { key: "b", class: "x" }); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBe("b"); expect(el.props.key).toBeUndefined(); expect(el.props.class).toBe("x"); } }); it("h('div', null) — key is undefined, no key in props", () => { const el = h("div", null); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBeUndefined(); expect(el.props).toEqual({}); } }); it("h() extracts key from props and promotes to element level", () => { const el = h("div", { key: "item-1", class: "test" }, "hello"); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBe("item-1"); expect(el.props.key).toBeUndefined(); expect(el.props.class).toBe("test"); } }); it("h() without key does not add key field", () => { const el = h("div", { class: "test" }, "hello"); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBeUndefined(); } }); it("h() with null props has no key", () => { const el = h("div", null, "hello"); expect(isUElement(el)).toBe(true); if (isUElement(el)) { expect(el.key).toBeUndefined(); } }); it("UElement with key validates against TypeBox schema", () => { const UElementSchema = UJSX.Import("UElement"); const el = h("li", { key: "a" }, "item"); expect(Value.Check(UElementSchema, el)).toBe(true); }); it("UElement without key validates against TypeBox schema (backward compat)", () => { const UElementSchema = UJSX.Import("UElement"); const el = h("li", { class: "item" }, "item"); expect(Value.Check(UElementSchema, el)).toBe(true); }); it("URoot does not have a key field", () => { const root = createRoot("r"); expect("key" in root).toBe(false); }); it("h() with root type does not promote key to URoot", () => { const root = h("root", { key: "x", id: "test" }, "child"); expect(isURoot(root)).toBe(true); if (isURoot(root)) { expect("key" in root).toBe(false); expect(root.props.key).toBe("x"); } }); }); describe("Context", () => { it("stores and updates values", () => { const ctx = new Context({ density: "full", target: "markdown" }); expect(ctx.get().density).toBe("full"); expect(ctx.get().target).toBe("markdown"); ctx.set({ density: "compact" }); expect(ctx.get().density).toBe("compact"); expect(ctx.get().target).toBe("markdown"); }); it("fork creates independent copy", () => { const parent = new Context({ density: "full", target: "markdown" }); const child = parent.fork({ density: "minimal" }); expect(child.get().density).toBe("minimal"); expect(parent.get().density).toBe("full"); }); }); describe("reactive pipeline", () => { it("signal + computed", () => { const name = signal("world"); const greeting = computed(() => `hello ${name.value}`); expect(greeting.value).toBe("hello world"); name.value = "ujsx"; expect(greeting.value).toBe("hello ujsx"); }); it("ReactiveRoot update and subscribe", () => { const root = new ReactiveRoot(h("div", null, "initial")); const received: UNode[] = []; const unsub = root.subscribe((n) => received.push(n)); root.update(() => h("div", null, "updated")); expect(received.length).toBe(2); unsub(); }); it("ReactiveRoot render emits events", () => { const events: { type: string; id: string; payload: unknown }[] = []; const root = new ReactiveRoot(h("div", null, "hello")); const stop = root.render((e) => events.push(e)); expect(events.length).toBe(1); expect(events[0].type).toBe("root.render"); stop(); }); it("reactiveComponent wraps component in computed signal", () => { const MyComp = createComponent("MyComp", (props) => h("div", null, props.text as string)); const propsSignal = signal({ text: "hello" }); const reactive = reactiveComponent(MyComp, propsSignal); expect(reactive.type).toBe("MyComp"); const node = reactive.signal.value; expect(isUElement(node)).toBe(true); if (isUElement(node)) { expect(node.children[0]).toBe("hello"); } propsSignal.value = { text: "world" }; const updated = reactive.signal.value; expect(isUElement(updated)).toBe(true); if (isUElement(updated)) { expect(updated.children[0]).toBe("world"); } }); }); describe("ValuePointer", () => { it("stores and updates reactive values", () => { const ptr = new ValuePointer("initial"); expect(ptr.value).toBe("initial"); ptr.value = "updated"; expect(ptr.value).toBe("updated"); }); }); describe("tree traversal", () => { it("selectNode traverses tree by child index", () => { const tree = h("div", null, h("p", null, "child1"), h("span", null, "child2")); const child0 = selectNode(tree, ["0"]); expect(isUElement(child0)).toBe(true); if (isUElement(child0)) { expect(child0.type).toBe("p"); } const child1 = selectNode(tree, ["1"]); expect(isUElement(child1)).toBe(true); if (isUElement(child1)) { expect(child1.type).toBe("span"); } expect(selectNode(tree, ["9"])).toBeUndefined(); }); it("setNode updates tree immutably", () => { const original = h("div", null, "old") as UElement; const updated = setNode(original, ["0"], "new") as UElement; expect(original.children[0]).toBe("old"); expect(updated.children[0]).toBe("new"); }); }); describe("TransformRegistry", () => { it("applies bidirectional rules", () => { const registry = new TransformRegistry, unknown>(); registry.register({ name: "div-ujsx-to-mdast", direction: "ujsx→mdast" as Direction, match: (n) => isUElement(n) && n.type === "div", transform: (n, ctx, next) => ({ type: "paragraph", children: (Array.isArray((n as UElement).children) ? (n as UElement).children : []).map((c: UNode, i: number) => next(c, childCtx(n, ctx, i))), }), priority: 1, }); registry.register({ name: "text-ujsx-to-mdast", direction: "ujsx→mdast" as Direction, match: (n) => isUPrimitive(n) && typeof n === "string", transform: (n) => ({ type: "text", value: n }), priority: 10, }); registry.register({ name: "paragraph-mdast-to-ujsx", direction: "mdast→ujsx" as Direction, match: (n) => (n as Record).type === "paragraph", transform: (n, ctx, next) => { const mdNode = n as { children: unknown[] }; return h("div", null, ...mdNode.children.map((c, i) => next(c as UNode, childCtx(n, ctx, i)))); }, priority: 1, }); registry.register({ name: "text-mdast-to-ujsx", direction: "mdast→ujsx" as Direction, match: (n) => (n as Record).type === "text", transform: (n) => (n as unknown as { value: string }).value, priority: 10, }); const ujsxNode = h("div", null, "hello") as UNode; const mdastCtx = transformCtx("ujsx→mdast" as Direction); const mdastResult = registry.transform(ujsxNode, mdastCtx); expect((mdastResult as Record).type).toBe("paragraph"); const mdastNode = { type: "paragraph", children: [{ type: "text", value: "hello" }] } as unknown as UNode; const ujsxCtx = transformCtx("mdast→ujsx" as Direction); const ujsxResult = registry.transform(mdastNode, ujsxCtx); expect(isUElement(ujsxResult)).toBe(true); if (isUElement(ujsxResult)) { expect(ujsxResult.type).toBe("div"); } }); }); describe("HostConfig", () => { it("createRoot and render", () => { const instances: { tag: string; props: Record }[] = []; const texts: string[] = []; const events: { type: string; payload: unknown }[] = []; const testHost: HostConfig> = { name: "test", createRootContext: () => ({}), createInstance: (tag, props) => { instances.push({ tag, props }); return tag; }, createTextInstance: (text) => { texts.push(text); return text; }, appendChild: () => {}, emit: (type, _id, payload) => events.push({ type, payload }), }; const root = createHostRoot(testHost, {}); root.render(h("div", { class: "outer" }, "hello", h("span", null, "world"))); expect(instances.length).toBe(2); expect(instances[0]!.tag).toBe("div"); expect(instances[1]!.tag).toBe("span"); expect(texts.sort()).toEqual(["hello", "world"]); expect(events.some((e) => e.type === "root.render")).toBe(true); }); }); describe("pubsub emitter", () => { it("bridges to event envelope", () => { const published: { type: string; id: string; payload: unknown }[] = []; const mockPubSub = { publish: (type: TType, id: string, payload: unknown) => { published.push({ type, id, payload }); }, subscribe: async function* () {}, }; const emit = createPubSubEmitter(mockPubSub); emit("root.render", "test-1", { childCount: 3 }); emit("root.render", "test-2", { childCount: 1 }); expect(published.length).toBe(2); expect(published[0]!.type).toBe("root.render"); expect(published[0]!.payload).toEqual({ childCount: 3 }); }); });