ai-chatbot/tests/routes/canvas-grouping.test.ts

364 lines
10 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
attachCanvasNodesToGroup,
collectCanvasGroupDescendantIds,
detachCanvasGroups,
duplicateCanvasNodeSelection,
duplicateCanvasNodeTree,
expandCanvasGroupSelection,
getCanvasGroupBounds,
isCanvasGroupNode,
isCanvasStoryboardGroupNode,
mergeCanvasStoryboardGroups,
} from "@/lib/canvas/grouping";
const childA = {
draftValues: { prompt: "A" },
height: 525,
id: "image-a",
width: 350,
x: 621,
y: 267.5,
};
const childB = {
draftValues: { prompt: "B" },
height: 200,
id: "text-b",
width: 350,
x: 1105.75,
y: 272,
};
test.describe("video canvas grouping", () => {
test("uses the observed group title and content padding geometry", () => {
expect(getCanvasGroupBounds([childA, childB])).toEqual({
height: 601,
width: 874.75,
x: 601,
y: 211.5,
});
});
test("attaches children and detaches them without losing their draft values", () => {
const group = {
draftValues: { action: "CANVAS_GROUP", nodeType: "group" },
height: 601,
id: "group-1",
width: 874.75,
x: 601,
y: 211.5,
};
const attached = attachCanvasNodesToGroup(
[group, childA, childB],
group.id,
[childA.id, childB.id],
);
expect(isCanvasGroupNode(attached[0])).toBe(true);
expect(attached[0].draftValues).toMatchObject({
childNodeIds: ["image-a", "text-b"],
});
expect(attached[1].draftValues).toEqual({ parentGroupId: "group-1", prompt: "A" });
const detached = detachCanvasGroups(attached, [group.id]);
expect(detached.detachedChildIds).toEqual(["image-a", "text-b"]);
expect(detached.nodes.map((node) => node.id)).toEqual(["image-a", "text-b"]);
expect(detached.nodes[0].draftValues).toEqual({ prompt: "A" });
});
test("expands nested groups for drag and destructive operations", () => {
const nodes = [
{
draftValues: {
action: "CANVAS_GROUP",
childNodeIds: ["nested", "image-a"],
nodeType: "group",
},
height: 100,
id: "outer",
width: 100,
x: 0,
y: 0,
},
{
draftValues: {
action: "CANVAS_GROUP",
childNodeIds: ["text-b"],
nodeType: "group",
},
height: 80,
id: "nested",
width: 80,
x: 10,
y: 10,
},
childA,
childB,
];
expect(collectCanvasGroupDescendantIds(nodes, ["outer"])).toEqual([
"nested",
"image-a",
"text-b",
]);
expect(expandCanvasGroupSelection(nodes, ["outer"])).toEqual([
"outer",
"nested",
"image-a",
"text-b",
]);
});
test("merges storyboard groups into one ordinary group without changing their children", () => {
const storyboardA = {
draftValues: {
action: "CANVAS_STORYBOARD_GROUP",
childNodeIds: ["shot-a", "shot-b"],
nodeType: "group",
},
height: 120,
id: "storyboard-a",
title: "分镜图 · A",
width: 240,
x: -20,
y: -56,
};
const storyboardB = {
draftValues: {
action: "CANVAS_GROUP",
childNodeIds: ["shot-c", "shot-d"],
groupType: "storyboard",
nodeType: "group",
},
height: 120,
id: "storyboard-b",
title: "分镜图 · B",
width: 240,
x: -20,
y: 94,
};
const shotNodes = [
{ id: "shot-a", x: 0, y: 0 },
{ id: "shot-b", x: 120, y: 0 },
{ id: "shot-c", x: 0, y: 150 },
{ id: "shot-d", x: 120, y: 150 },
].map((node) => ({
...node,
draftValues: {
parentGroupId: node.id < "shot-c" ? storyboardA.id : storyboardB.id,
prompt: node.id,
},
height: 100,
title: node.id,
width: 100,
}));
const unrelatedNode = {
draftValues: { prompt: "outside" },
height: 80,
id: "outside",
title: "outside",
width: 80,
x: 400,
y: 0,
};
const result = mergeCanvasStoryboardGroups({
createGroup: ({ bounds, childNodeIds }) => ({
...bounds,
draftValues: {
action: "CANVAS_GROUP",
childNodeIds,
nodeType: "group",
},
id: "merged-group",
title: `分组 ${childNodeIds.length} 个节点`,
}),
edges: [
{ id: "group-edge", source: storyboardA.id, target: unrelatedNode.id },
{ id: "child-edge", source: "shot-a", target: "shot-b" },
{ id: "cross-edge", source: "shot-b", target: "shot-c" },
],
nodes: [storyboardA, storyboardB, ...shotNodes, unrelatedNode],
selectedNodeIds: [storyboardB.id, storyboardA.id],
});
expect(isCanvasStoryboardGroupNode(storyboardA)).toBe(true);
expect(isCanvasStoryboardGroupNode(storyboardB)).toBe(true);
expect(result?.sourceGroupIds).toEqual([storyboardB.id, storyboardA.id]);
expect(result?.childNodeIds).toEqual([
"shot-c",
"shot-d",
"shot-a",
"shot-b",
]);
expect(result?.group).toMatchObject({
height: 326,
id: "merged-group",
title: "分组 4 个节点",
width: 260,
x: -20,
y: -56,
});
expect(result?.nodes.map((node) => node.id)).toEqual([
"shot-a",
"shot-b",
"shot-c",
"shot-d",
"outside",
"merged-group",
]);
expect(result?.nodes.slice(0, 4).map((node) => node.draftValues)).toEqual([
{ parentGroupId: "merged-group", prompt: "shot-a" },
{ parentGroupId: "merged-group", prompt: "shot-b" },
{ parentGroupId: "merged-group", prompt: "shot-c" },
{ parentGroupId: "merged-group", prompt: "shot-d" },
]);
expect(result?.nodes.at(-1)?.draftValues).toMatchObject({
childNodeIds: ["shot-c", "shot-d", "shot-a", "shot-b"],
});
expect(result?.edges).toEqual([
{ id: "child-edge", source: "shot-a", target: "shot-b" },
{ id: "cross-edge", source: "shot-b", target: "shot-c" },
]);
});
test("does not merge ordinary groups", () => {
const ordinaryGroup = {
draftValues: {
action: "CANVAS_GROUP",
childNodeIds: [childA.id, childB.id],
nodeType: "group",
},
height: 601,
id: "ordinary-group",
title: "整组执行",
width: 874.75,
x: 601,
y: 211.5,
};
expect(
mergeCanvasStoryboardGroups({
createGroup: () => ordinaryGroup,
edges: [],
nodes: [ordinaryGroup, childA, childB],
selectedNodeIds: [ordinaryGroup.id, childA.id, childB.id],
}),
).toBeNull();
});
test("duplicates a whole group tree without retaining original relationships", () => {
let nodeIndex = 0;
let edgeIndex = 0;
const group = {
draftValues: {
action: "CANVAS_GROUP",
childNodeIds: [childA.id, childB.id],
nodeType: "group",
},
height: 601,
id: "group-1",
title: "整组执行",
width: 874.75,
x: 601,
y: 211.5,
};
const attachedChildren = attachCanvasNodesToGroup(
[{ ...childA, title: "参考图" }, { ...childB, title: "提示词" }],
group.id,
[childA.id, childB.id],
);
const result = duplicateCanvasNodeTree({
createEdgeId: () => `edge-copy-${++edgeIndex}`,
createNodeId: () => `node-copy-${++nodeIndex}`,
edges: [{ id: "edge-1", source: childA.id, target: childB.id }],
nodes: [group, ...attachedChildren],
offsetY: 100,
rootNodeId: group.id,
});
expect(result?.rootNodeId).toBe("node-copy-1");
expect(result?.duplicateNodes.map((node) => node.id)).toEqual([
"node-copy-1",
"node-copy-2",
"node-copy-3",
]);
expect(result?.duplicateNodes[0]).toMatchObject({
draftValues: { childNodeIds: ["node-copy-2", "node-copy-3"] },
title: "整组执行 - 副本",
y: 311.5,
});
expect(result?.duplicateNodes[1].draftValues).toMatchObject({
parentGroupId: "node-copy-1",
prompt: "A",
});
expect(result?.duplicateEdges).toEqual([
{
id: "edge-copy-1",
source: "node-copy-2",
target: "node-copy-3",
},
]);
});
test("detaches a copied child from its original group", () => {
const attachedChild = attachCanvasNodesToGroup(
[{ ...childA, title: "参考图" }],
"group-1",
[childA.id],
)[0];
const result = duplicateCanvasNodeTree({
createEdgeId: () => "unused-edge",
createNodeId: () => "image-copy",
edges: [],
nodes: [attachedChild],
rootNodeId: attachedChild.id,
});
expect(result?.duplicateNodes[0].draftValues).toEqual({ prompt: "A" });
});
test("duplicates a selected set without its internal edges for Option drag", () => {
let nodeIndex = 0;
const result = duplicateCanvasNodeSelection({
createEdgeId: () => "unused-edge",
createNodeId: () => `copy-${++nodeIndex}`,
edges: [{ id: "edge-1", source: childA.id, target: childB.id }],
includeInternalEdges: false,
nodes: [
{ ...childA, title: "参考图" },
{ ...childB, title: "提示词 - 副本" },
],
selectedNodeIds: [childA.id, childB.id],
});
expect(result?.duplicateNodes).toEqual([
expect.objectContaining({ id: "copy-1", title: "参考图 - 副本" }),
expect.objectContaining({ id: "copy-2", title: "提示词 - 副本" }),
]);
expect(result?.duplicateEdges).toEqual([]);
expect(result?.rootNodeIds).toEqual(["copy-1", "copy-2"]);
});
test("duplicates selected-set internal edges for Command+Option drag", () => {
let nodeIndex = 0;
let edgeIndex = 0;
const result = duplicateCanvasNodeSelection({
createEdgeId: () => `edge-copy-${++edgeIndex}`,
createNodeId: () => `copy-${++nodeIndex}`,
edges: [
{ id: "internal", source: childA.id, target: childB.id },
{ id: "external", source: childB.id, target: "outside" },
],
includeInternalEdges: true,
nodes: [
{ ...childA, title: "参考图" },
{ ...childB, title: "提示词" },
],
selectedNodeIds: [childA.id, childB.id],
});
expect(result?.duplicateEdges).toEqual([
{
id: "edge-copy-1",
source: "copy-1",
target: "copy-2",
},
]);
});
});