import { expect, test } from "@playwright/test"; import type { PlaitElement } from "@plait/core"; import { applyOutlineCommand, renameOutlineTopic, } from "@/lib/flow-board/mind-outline"; import { getTopicEntries } from "@/lib/flow-board/topic-details"; const topic = (id: string, children: PlaitElement[] = []): PlaitElement => ({ id, type: "mind_child", data: { topic: { type: "paragraph", children: [{ text: id, bold: true }] }, image: { url: "picture" }, }, xmind: { notes: "保留备注" }, children, }); const root = (...children: PlaitElement[]): PlaitElement => ({ ...topic("root", children), type: "mindmap", layout: "standard", rightNodeCount: children.length, points: [[0, 0]], }); const find = (children: PlaitElement[], id: string) => getTopicEntries(children).find((entry) => entry.element.id === id)!; test("outline nesting preserves IDs and metadata and rejects moving into descendants", () => { const source = [root(topic("a"), topic("b", [topic("c")]))]; const before = JSON.stringify(source); const nested = applyOutlineCommand(source, "b", "indent"); expect(find(nested.children, "b").path).toEqual([0, 0, 0]); expect(find(nested.children, "c").path).toEqual([0, 0, 0, 0]); expect(find(nested.children, "b").element.xmind).toEqual({ notes: "保留备注", }); expect(JSON.stringify(source)).toBe(before); expect(() => applyOutlineCommand(nested.children, "a", { targetId: "c", placement: "child", }) ).toThrow("不能把主题"); const outdented = applyOutlineCommand(nested.children, "b", "outdent"); expect(find(outdented.children, "b").path).toEqual([0, 1]); }); test("outline deletion adjusts summary range without assigning unrelated topics", () => { const source = [ root(topic("a"), topic("b"), topic("c"), { ...topic("summary"), start: 0, end: 1, }), ]; const removed = applyOutlineCommand(source, "a", "delete"); expect(find(removed.children, "summary").element).toMatchObject({ start: 0, end: 0, }); expect(() => applyOutlineCommand(source, "b", "down")).toThrow( "概要范围断开" ); expect(find(source, "summary").element).toMatchObject({ start: 0, end: 1 }); }); test("outline insertion expands folded parents and includes new topics inside a summary", () => { const source = [ root(topic("a"), topic("b"), { ...topic("summary"), start: 0, end: 1 }), ]; const inserted = applyOutlineCommand(source, "a", "sibling"); expect(find(inserted.children, "summary").element).toMatchObject({ start: 0, end: 2, }); expect(find(inserted.children, inserted.selectedId).path).toEqual([0, 1]); const folded = [{ ...root(topic("a")), isCollapsed: true }]; expect( applyOutlineCommand(folded, "root", "child").children[0].isCollapsed ).toBe(false); }); test("outline drag moves a subtree across branches and retains annotation references", () => { const relation = { id: "link", type: "arrow-line", mindAnnotation: { kind: "relationship", sourceId: "b", targetId: "c" }, }; const source = [root(topic("a", [topic("b")]), topic("c")), relation]; const moved = applyOutlineCommand(source, "b", { targetId: "c", placement: "child", }); expect(find(moved.children, "b").path).toEqual([0, 1, 0]); expect(moved.children[1]).toEqual(relation); }); test("outline creates the first root and renaming preserves image and text marks", () => { const created = applyOutlineCommand([], null, "child"); expect(created.children[0]).toMatchObject({ type: "mindmap", id: created.selectedId, }); const data = renameOutlineTopic(topic("a"), "新名称"); expect(data).toMatchObject({ image: { url: "picture" }, topic: { children: [{ text: "新名称", bold: true }] }, }); });