test(web): 画布测试 - 修复源码断言与编辑器插槽的跨文件状态依赖
This commit is contained in:
1 parent
da0531f6eb
commit
c114865781
3 files changed
+32
-19
No files matched your search
@@ -10,6 +10,8 @@ const source: CanvasNodeData = {
|
|||||||
metadata: { content: "original-image", storageKey: "original-key", status: "success" },
|
metadata: { content: "original-image", storageKey: "original-key", status: "success" },
|
||||||
};
|
};
|
||||||
const read = (path: string) => readFileSync(resolve(import.meta.dir, "../src", path), "utf8");
|
const read = (path: string) => readFileSync(resolve(import.meta.dir, "../src", path), "utf8");
|
||||||
|
// 断言源码时忽略换行与缩进:长条件被拆行属于排版变化,不应让契约测试失效。
|
||||||
|
const flat = (text: string) => text.replace(/\s+/g, " ");
|
||||||
|
|
||||||
describe("上传图片是输入素材", () => {
|
describe("上传图片是输入素材", () => {
|
||||||
test("上传与导入图片没有生成框,空节点与生成结果仍可配置", () => {
|
test("上传与导入图片没有生成框,空节点与生成结果仍可配置", () => {
|
||||||
@@ -40,7 +42,7 @@ describe("上传图片是输入素材", () => {
|
|||||||
expect(canGenerateImageInPlace(source)).toBe(false);
|
expect(canGenerateImageInPlace(source)).toBe(false);
|
||||||
});
|
});
|
||||||
test("页面和工具入口共享素材判定,人物质感接入原图连线", () => {
|
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)");
|
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 mediaTools = read("pages/canvas/use-canvas-media-tools.ts");
|
||||||
const portrait = mediaTools.slice(mediaTools.indexOf("const openPortraitTextureEditor"), mediaTools.indexOf("const cropImageNode"));
|
const portrait = mediaTools.slice(mediaTools.indexOf("const openPortraitTextureEditor"), mediaTools.indexOf("const cropImageNode"));
|
||||||
|
|||||||
@@ -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 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 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", () => {
|
describe("canvas node drag overlays", () => {
|
||||||
test("hides floating editors and selection controls for the whole drag preview", () => {
|
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("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).not.toContain("angleNode?.metadata?.content && !isCanvasNodeMoving");
|
||||||
expect(projectSource).toContain("emotionNode?.metadata?.content && !isCanvasNodeMoving");
|
expect(projectSource).toContain("emotionNode?.metadata?.content && !isCanvasNodeMoving");
|
||||||
expect(projectSource).toContain("selectedNodeBounds && !selectionBox && !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("onNodeDragEnd: handleNodeDragEnd");
|
||||||
expect(projectSource).toContain("setDialogNodeId(node.id);");
|
expect(projectSource).toContain("setDialogNodeId(node.id);");
|
||||||
expect(selectionControllerSource).toContain("if (clickedNodeId) onNodeDragEnd?.(clickedNodeId);");
|
expect(selectionControllerSource).toContain("if (clickedNodeId) onNodeDragEnd?.(clickedNodeId);");
|
||||||
|
|||||||
@@ -8,14 +8,19 @@ import {
|
|||||||
|
|
||||||
const render = () => null;
|
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", () => {
|
describe("editor slot registry", () => {
|
||||||
test("registers a slot and returns it", () => {
|
test("registers a slot and returns it", () => {
|
||||||
const unregister = registerEditorSlot({ pluginId: "editor-a", slot: "timeline-panel", render });
|
const unregister = registerEditorSlot({ pluginId: "editor-a", slot: "timeline-panel", render });
|
||||||
const slots = getEditorSlots("timeline-panel");
|
expect(ownSlots("timeline-panel")).toEqual(["editor-a"]);
|
||||||
expect(slots).toHaveLength(1);
|
|
||||||
expect(slots[0].pluginId).toBe("editor-a");
|
|
||||||
unregister();
|
unregister();
|
||||||
expect(getEditorSlots("timeline-panel")).toHaveLength(0);
|
expect(ownSlots("timeline-panel")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sorts by priority desc, then registration order asc", () => {
|
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: "b", slot: "preview-renderer", render }),
|
||||||
registerEditorSlot({ pluginId: "c", slot: "preview-renderer", priority: 5, render }),
|
registerEditorSlot({ pluginId: "c", slot: "preview-renderer", priority: 5, render }),
|
||||||
];
|
];
|
||||||
const slots = getEditorSlots("preview-renderer");
|
expect(ownSlots("preview-renderer")).toEqual(["c", "a", "b"]);
|
||||||
expect(slots.map((s) => s.pluginId)).toEqual(["c", "a", "b"]);
|
|
||||||
unregs.forEach((u) => u());
|
unregs.forEach((u) => u());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-registering same plugin+slot overrides (idempotent, HMR-safe)", () => {
|
test("re-registering same plugin+slot overrides (idempotent, HMR-safe)", () => {
|
||||||
const unreg1 = registerEditorSlot({ pluginId: "x", slot: "inspector", render });
|
const unreg1 = registerEditorSlot({ pluginId: "x", slot: "inspector", render });
|
||||||
const unreg2 = registerEditorSlot({ pluginId: "x", slot: "inspector", render });
|
const unreg2 = registerEditorSlot({ pluginId: "x", slot: "inspector", render });
|
||||||
const slots = getEditorSlots("inspector");
|
expect(ownSlots("inspector")).toEqual(["x"]);
|
||||||
expect(slots).toHaveLength(1);
|
|
||||||
expect(slots[0].pluginId).toBe("x");
|
|
||||||
// 旧卸载函数不应误删新注册项
|
// 旧卸载函数不应误删新注册项
|
||||||
unreg1();
|
unreg1();
|
||||||
expect(getEditorSlots("inspector")).toHaveLength(1);
|
expect(ownSlots("inspector")).toEqual(["x"]);
|
||||||
unreg2();
|
unreg2();
|
||||||
expect(getEditorSlots("inspector")).toHaveLength(0);
|
expect(ownSlots("inspector")).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("different plugins coexist in the same slot", () => {
|
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: "p1", slot: "subtitle-tool", render }),
|
||||||
registerEditorSlot({ pluginId: "p2", 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");
|
unregisterEditorSlot("p1", "subtitle-tool");
|
||||||
expect(getEditorSlots("subtitle-tool").map((s) => s.pluginId)).toEqual(["p2"]);
|
expect(ownSlots("subtitle-tool")).toEqual(["p2"]);
|
||||||
unregs.forEach((u) => u());
|
unregs.forEach((u) => u());
|
||||||
});
|
});
|
||||||
|
|
||||||
test("unknown slot returns empty", () => {
|
test("slots this file never registered into stay untouched", () => {
|
||||||
expect(getEditorSlots("export-renderer")).toHaveLength(0);
|
// 真实插件会占用 export-renderer,因此断言「本文件没有向未注册的插槽留下贡献」。
|
||||||
|
expect(ownSlots("export-renderer")).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user