diff --git a/web/test/canvas-image-source.test.ts b/web/test/canvas-image-source.test.ts index f1a90b1f2..5edfd187b 100644 --- a/web/test/canvas-image-source.test.ts +++ b/web/test/canvas-image-source.test.ts @@ -10,6 +10,8 @@ const source: CanvasNodeData = { metadata: { content: "original-image", storageKey: "original-key", status: "success" }, }; const read = (path: string) => readFileSync(resolve(import.meta.dir, "../src", path), "utf8"); +// 断言源码时忽略换行与缩进:长条件被拆行属于排版变化,不应让契约测试失效。 +const flat = (text: string) => text.replace(/\s+/g, " "); describe("上传图片是输入素材", () => { test("上传与导入图片没有生成框,空节点与生成结果仍可配置", () => { @@ -40,7 +42,7 @@ describe("上传图片是输入素材", () => { expect(canGenerateImageInPlace(source)).toBe(false); }); test("页面和工具入口共享素材判定,人物质感接入原图连线", () => { - expect(read("pages/canvas/project.tsx")).toContain("dialogNode && !isCanvasImageSourceNode(dialogNode)"); + expect(flat(read("pages/canvas/project.tsx"))).toContain("dialogNode && !isCanvasImageSourceNode(dialogNode)"); expect(read("lib/canvas/tool-registry/definitions/node-hover-tools.tsx")).toContain("!isCanvasImageSourceNode(ctx.node)"); const mediaTools = read("pages/canvas/use-canvas-media-tools.ts"); const portrait = mediaTools.slice(mediaTools.indexOf("const openPortraitTextureEditor"), mediaTools.indexOf("const cropImageNode")); diff --git a/web/test/canvas-node-drag-overlays.test.ts b/web/test/canvas-node-drag-overlays.test.ts index 3695913da..13ef826ee 100644 --- a/web/test/canvas-node-drag-overlays.test.ts +++ b/web/test/canvas-node-drag-overlays.test.ts @@ -5,15 +5,23 @@ import { describe, expect, test } from "bun:test"; const projectSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8"); const selectionControllerSource = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/use-canvas-selection-controller.ts"), "utf8"); +const flat = (text: string) => text.replace(/\s+/g, " "); describe("canvas node drag overlays", () => { test("hides floating editors and selection controls for the whole drag preview", () => { expect(projectSource).toContain("const isCanvasNodeMoving = isNodeDragging || Boolean(dragPreview?.nodeIds.size);"); - expect(projectSource).toContain("dialogNode.type !== CanvasNodeType.Drawing && !selectionBox && !isCanvasNodeMoving"); + // 面板浮层的守卫必须同时成立:类型排除、框选、拖拽预览。断言整段条件而不是某一行的 + // 字面量,避免上游拆行或新增类型排除(Panorama 等)后把排版变化报成契约失效。 + const flatSource = flat(projectSource); + const overlayStart = flatSource.indexOf("{dialogNode &&"); + const overlayCondition = flatSource.slice(overlayStart, flatSource.indexOf(" null; +// 真实插件 editor-shell 会在被导入时占满全部 EditorSlotKind,而它是否已被导入取决于测试执行顺序。 +// 这里只断言本文件自己注册的 pluginId,使结果不再受其它测试文件的副作用影响。 +const ownSlots = (slot: Parameters[0]) => + getEditorSlots(slot) + .filter((item) => ["editor-a", "a", "b", "c", "x", "p1", "p2"].includes(item.pluginId)) + .map((item) => item.pluginId); + describe("editor slot registry", () => { test("registers a slot and returns it", () => { const unregister = registerEditorSlot({ pluginId: "editor-a", slot: "timeline-panel", render }); - const slots = getEditorSlots("timeline-panel"); - expect(slots).toHaveLength(1); - expect(slots[0].pluginId).toBe("editor-a"); + expect(ownSlots("timeline-panel")).toEqual(["editor-a"]); unregister(); - expect(getEditorSlots("timeline-panel")).toHaveLength(0); + expect(ownSlots("timeline-panel")).toEqual([]); }); test("sorts by priority desc, then registration order asc", () => { @@ -24,22 +29,19 @@ describe("editor slot registry", () => { registerEditorSlot({ pluginId: "b", slot: "preview-renderer", render }), registerEditorSlot({ pluginId: "c", slot: "preview-renderer", priority: 5, render }), ]; - const slots = getEditorSlots("preview-renderer"); - expect(slots.map((s) => s.pluginId)).toEqual(["c", "a", "b"]); + expect(ownSlots("preview-renderer")).toEqual(["c", "a", "b"]); unregs.forEach((u) => u()); }); test("re-registering same plugin+slot overrides (idempotent, HMR-safe)", () => { const unreg1 = registerEditorSlot({ pluginId: "x", slot: "inspector", render }); const unreg2 = registerEditorSlot({ pluginId: "x", slot: "inspector", render }); - const slots = getEditorSlots("inspector"); - expect(slots).toHaveLength(1); - expect(slots[0].pluginId).toBe("x"); + expect(ownSlots("inspector")).toEqual(["x"]); // 旧卸载函数不应误删新注册项 unreg1(); - expect(getEditorSlots("inspector")).toHaveLength(1); + expect(ownSlots("inspector")).toEqual(["x"]); unreg2(); - expect(getEditorSlots("inspector")).toHaveLength(0); + expect(ownSlots("inspector")).toEqual([]); }); test("different plugins coexist in the same slot", () => { @@ -47,13 +49,14 @@ describe("editor slot registry", () => { registerEditorSlot({ pluginId: "p1", slot: "subtitle-tool", render }), registerEditorSlot({ pluginId: "p2", slot: "subtitle-tool", render }), ]; - expect(getEditorSlots("subtitle-tool")).toHaveLength(2); + expect(ownSlots("subtitle-tool")).toEqual(["p1", "p2"]); unregisterEditorSlot("p1", "subtitle-tool"); - expect(getEditorSlots("subtitle-tool").map((s) => s.pluginId)).toEqual(["p2"]); + expect(ownSlots("subtitle-tool")).toEqual(["p2"]); unregs.forEach((u) => u()); }); - test("unknown slot returns empty", () => { - expect(getEditorSlots("export-renderer")).toHaveLength(0); + test("slots this file never registered into stay untouched", () => { + // 真实插件会占用 export-renderer,因此断言「本文件没有向未注册的插槽留下贡献」。 + expect(ownSlots("export-renderer")).toEqual([]); }); -}); +}); \ No newline at end of file