test(web): 画布测试 - 修复源码断言与编辑器插槽的跨文件状态依赖

This commit is contained in:
zhuchi
2026-09-16 20:54:14 +08:00
parent 9868f5e8b8
commit 585e8d1c49
3 changed files with 32 additions and 19 deletions

View File

@@ -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"));

View File

@@ -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("<CanvasNodePanelOverlay", overlayStart));
expect(overlayCondition).toContain("dialogNode.type !== CanvasNodeType.Drawing");
expect(overlayCondition).toContain("!selectionBox");
expect(overlayCondition).toContain("!isCanvasNodeMoving");
expect(projectSource).not.toContain("angleNode?.metadata?.content && !isCanvasNodeMoving");
expect(projectSource).toContain("emotionNode?.metadata?.content && !isCanvasNodeMoving");
expect(projectSource).toContain("selectedNodeBounds && !selectionBox && !isCanvasNodeMoving");
expect(projectSource).toContain("node={isCanvasNodeMoving || nodeImageSettingsOpen || emotionNodeId ? null : toolbarNode}");
expect(projectSource).toContain("node={isCanvasNodeMoving || nodeImageSettingsOpen || emotionNodeId || angleNodeId ? null : toolbarNode}");
expect(projectSource).toContain("onNodeDragEnd: handleNodeDragEnd");
expect(projectSource).toContain("setDialogNodeId(node.id);");
expect(selectionControllerSource).toContain("if (clickedNodeId) onNodeDragEnd?.(clickedNodeId);");

View File

@@ -8,14 +8,19 @@ import {
const render = () => null;
// 真实插件 editor-shell 会在被导入时占满全部 EditorSlotKind,而它是否已被导入取决于测试执行顺序。
// 这里只断言本文件自己注册的 pluginId,使结果不再受其它测试文件的副作用影响。
const ownSlots = (slot: Parameters<typeof getEditorSlots>[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([]);
});
});
});