import { describe, expect, it } from "bun:test"; import { createHeadlessEditor } from "@lexical/headless"; import { $getRoot, $isElementNode } from "lexical"; import { exportPlan, importPlan } from "../convert"; import { registry } from "../registry"; import { $isDecisionNode } from "./decision"; import type { LexicalEditor } from "lexical"; const REGISTRY = registry(); const ID = "01K0N4TR8K7JGM4R1J7PW4R8YJ"; function editor(): LexicalEditor { return createHeadlessEditor({ nodes: REGISTRY.nodes, onError(err) { throw err; }, }); } function through(source: string): string { let instance = editor(); importPlan(instance, source, { registry: REGISTRY }); return exportPlan(instance, { registry: REGISTRY }); } const ACCEPTED = `\n` + `\n` + `\n` + `\n`; describe("decision", () => { it("round-trips an accepted thread", () => { let out = through(ACCEPTED); expect(through(out)).toBe(out); expect(out).toContain(`id="${ID}"`); expect(out).toContain('by="krzysztof"'); expect(out).toContain('at="2026-07-28T10:14:00Z"'); expect(out).toContain('quote="The renderer caches tiles for 60 seconds."'); }); it("keeps the thread in the order it was said", () => { let instance = editor(); importPlan(instance, ACCEPTED, { registry: REGISTRY }); instance.getEditorState().read(() => { let node = $getRoot().getFirstChild(); if (!$isDecisionNode(node)) throw new Error("expected decision"); expect(node.getDecision().notes.map(note => note.by)).toEqual(["krzysztof", "ana"]); }); }); it("gives a note no identity of its own", () => { let note = through(ACCEPTED).split("\n").find(line => line.includes(" { let instance = editor(); importPlan(instance, ACCEPTED, { registry: REGISTRY }); instance.getEditorState().read(() => { let node = $getRoot().getFirstChild(); expect($isDecisionNode(node)).toBe(true); if (!$isDecisionNode(node)) return; // A decorator, not an element: an accepted thread is frozen, so // there is no editable subtree inside it. expect($isElementNode(node)).toBe(false); expect(node.getDecision().id).toBe(ID); }); }); it("carries a note through unchanged when it spans lines and quotes things", () => { // The text is an attribute rather than children precisely so this // survives: a flow component's children parse as blocks, and prose // written between the tags would come back wrapped in a paragraph. let text = 'line one\nline two\n\nand "quoted" & '; let source = `\n` + `\n` + `\n`; let instance = editor(); importPlan(instance, source, { registry: REGISTRY }); instance.getEditorState().read(() => { let node = $getRoot().getFirstChild(); if (!$isDecisionNode(node)) throw new Error("expected decision"); expect(node.getDecision().notes[0]!.text).toBe(text); }); expect(through(through(source))).toBe(through(source)); }); });