Files
ttstd-infinite-canvas/web/test/canvas-node-toolbar.test.ts
ddcat666 c0a9250cb3 feat(previs): 图片生成预演台 - 从图片节点交给 Agent 复现为可编辑 3D 预演
图片工具栏新增「生成预演台」,自动选用支持看图的文本模型并提交复现指令。对话用节点卡片展示「此图」,普通图片演员只写名称时不再要求角色卡绑定。
2026-10-09 17:17:28 +08:00

98 lines
5.6 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { buildImageToolbarTools } from "@/components/canvas/canvas-image-toolbar-tools";
import { resolveNodeToolbarPlacement, resolveToolbarTools, type ToolContext, type ToolbarHandlers } from "@/lib/canvas/tool-registry";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
function createNodeContext(node: CanvasNodeData): ToolContext {
return {
selectedCount: 0,
selectedNodeTypes: new Set(),
selectedVideoCount: 0,
canvasTool: "move",
workspaceMode: "professional",
isProjectLinked: false,
canUndo: false,
canRedo: false,
node,
nodeMetadata: node.metadata,
extractingVideoFrames: false,
extractingAudio: false,
trimmingVideo: false,
mergingVideos: false,
addPanelOpen: false,
appearancePanelOpen: false,
settingsPanelOpen: false,
handlers: {} as ToolbarHandlers,
};
}
describe("canvas node toolbar model", () => {
test("preserves empty, simple and busy video states", () => {
const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: {} });
const emptyTools = resolveToolbarTools("node-hover", ctx, null);
expect(emptyTools.some((tool) => tool.id === "extractAudio")).toBe(false);
expect(resolveNodeToolbarPlacement(emptyTools.find((tool) => tool.id === "uploadVideo")!, ctx).group).toBe("primary");
ctx.nodeMetadata = { content: "video.mp4" };
ctx.extractingAudio = true;
const audio = resolveToolbarTools("node-hover", ctx, null).find((tool) => tool.id === "extractAudio")!;
expect(audio.disabled?.(ctx)).toBe(true);
ctx.workspaceMode = "simple";
expect(resolveToolbarTools("node-hover", ctx, null).some((tool) => tool.id === "extractAudio")).toBe(false);
});
test("image menu actions remain lazy and keep the original node", () => {
const node: CanvasNodeData = { id: "image", type: CanvasNodeType.Image, title: "图片", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "image.png", freeResize: true } };
const received: CanvasNodeData[] = [];
const tools = buildImageToolbarTools(node, { onCrop: (target: CanvasNodeData) => received.push(target) } as never);
expect(received).toEqual([]);
tools.find((tool) => tool.id === "crop")!.onClick();
expect(received).toEqual([node]);
expect(tools.find((tool) => tool.id === "resize")?.active).toBe(false);
});
test("keeps video primary actions compact and moves secondary processing into a menu", () => {
const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "video.mp4" } });
const tools = resolveToolbarTools("node-hover", ctx, null);
const groups = new Map(tools.map((tool) => [tool.id, resolveNodeToolbarPlacement(tool, ctx).group]));
expect([...groups].filter(([, group]) => group === "primary").map(([id]) => id)).toEqual(["trimRegenerate", "subtitles"]);
expect([...groups].filter(([, group]) => group === "process").map(([id]) => id)).toEqual(["extractFrames", "extractAudio"]);
expect(groups.get("download")).toBe("utility");
expect(groups.get("timeline")).toBe("workspace");
expect(groups.get("uploadVideo")).toBe("more");
expect(groups.get("delete")).toBe("more");
expect(groups.get("node-lock")).toBe("more");
});
test("uses the redesigned video terminology from the registry", () => {
const ctx = createNodeContext({ id: "video", type: CanvasNodeType.Video, title: "视频", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "video.mp4" } });
const tools = new Map(resolveToolbarTools("node-hover", ctx, null).map((tool) => [tool.id, tool]));
const displayLabel = (id: string) => {
const value = tools.get(id)?.displayLabel;
return typeof value === "function" ? value(ctx) : value;
};
expect(displayLabel("subtitles")).toBe("字幕");
expect(displayLabel("timeline")).toBe("进入剪辑");
expect(displayLabel("extractFrames")).toBe("提取画面");
expect(displayLabel("extractAudio")).toBe("提取音频");
});
test("image-only tools carry presentation metadata instead of relying on component ID lists", () => {
const node: CanvasNodeData = { id: "image", type: CanvasNodeType.Image, title: "图片", position: { x: 0, y: 0 }, width: 320, height: 180, metadata: { content: "image.png" } };
const tools = buildImageToolbarTools(node, {} as never);
expect(tools.find((tool) => tool.id === "angle")?.group).toBe("viewpoint");
expect(tools.find((tool) => tool.id === "maskEdit")?.group).toBe("primary");
expect(tools.some((tool) => tool.id === "superResolve")).toBe(false);
expect(tools.find((tool) => tool.id === "emotion")?.group).toBe("portrait");
expect(tools.find((tool) => tool.id === "upscale")?.description).toContain("不是 AI 超分");
expect(tools.find((tool) => tool.id === "crop")?.section).toBe("构图与尺寸");
expect(tools.find((tool) => tool.id === "resize")?.active).toBe(true);
expect(tools.find((tool) => tool.id === "copyPrompt")?.group).toBe("more");
expect(tools.find((tool) => tool.id === "previs")?.group).toBe("primary");
expect(tools.find((tool) => tool.id === "previs")?.description).toContain("Agent");
});
});