90 lines
6.3 KiB
TypeScript
90 lines
6.3 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { requireResolvedReferenceImages, connectionError, connectionTypeError, videoInputLimitError, connectedInputNodes, connectedImageUrls, connectedText, assertConnectedInputsReady, withConnectedText, type ConnectionNode } from "@/lib/video-canvas/connections";
|
|
import { canvasImageRequest } from "@/lib/video-canvas/generation-contract";
|
|
import { validateVideoReferences } from "@/lib/video-canvas/reference-input";
|
|
|
|
const image = (id: string, url = `https://media.example/${id}.png`): ConnectionNode => ({ id, title: id, inputType: "image", draftValues: { mediaUrl: url } });
|
|
const target: ConnectionNode = { id: "target", inputType: "video", draftValues: { params: { referenceImages: ["https://media.example/stale.png"] } } };
|
|
const edge = (source: string, target = "target") => ({ id: `${source}-${target}`, source, target });
|
|
|
|
test("rewiring changes serialized image request and disconnect never revives cached references", () => {
|
|
const a = image("a"), b = image("b"), nodes = [a, b, target];
|
|
const request = (edges: ReturnType<typeof edge>[]) => canvasImageRequest({ ratio: "16:9", resolution: "1K", count: "1" }, connectedImageUrls(connectedInputNodes(nodes, edges, target.id).map(n => [n.draftValues!.mediaUrl])));
|
|
expect(JSON.parse(JSON.stringify(request([edge("a")]))).referenceImage).toBe(a.draftValues!.mediaUrl);
|
|
b.draftValues!.mediaUrl = "https://media.example/new-b.png";
|
|
expect(request([edge("b")]).referenceImage).toContain("new-b");
|
|
expect(request([]).referenceImage).toBeUndefined();
|
|
expect(JSON.stringify(request([]))).not.toContain("stale");
|
|
});
|
|
|
|
test("separate first and last frame slots preserve order even with identical URLs", async () => {
|
|
const a = image("a"), b = image("b", a.draftValues!.mediaUrl);
|
|
const inputs = connectedInputNodes([a, b, target], [edge("b"), edge("a")], "target");
|
|
expect(inputs.map(n => n.id)).toEqual(["b", "a"]);
|
|
const images = connectedImageUrls(inputs.map(n => [n.draftValues!.mediaUrl]));
|
|
expect(images).toHaveLength(2);
|
|
expect((await validateVideoReferences(images, async () => "reachable")).images).toEqual(images);
|
|
});
|
|
|
|
test("cycles, missing endpoints, group frames and duplicate connections are rejected", () => {
|
|
const nodes = [image("a"), image("b"), image("c"), { id: "g", kind: "group" }];
|
|
const edges = [edge("a", "b"), edge("b", "c")];
|
|
expect(connectionError(nodes, edges, "c", "a")).toContain("循环");
|
|
expect(connectionError(nodes, edges, "a", "a")).toContain("同一个");
|
|
expect(connectionError(nodes, edges, "a", "b")).toContain("已经连接");
|
|
expect(connectionError(nodes, edges, "a", "gone")).toContain("不存在");
|
|
expect(connectionError(nodes, edges, "g", "a")).toContain("分组");
|
|
expect(connectionError(nodes, edges, "a", "c")).toBeNull();
|
|
expect(() => connectedInputNodes(nodes, [...edges, edge("c", "a")], "a")).toThrow("循环");
|
|
});
|
|
|
|
test("upstream failure or pending preview blocks downstream execution", () => {
|
|
for (const status of ["pending", "running", "failed", "cancelled"]) {
|
|
const n = image("a"); n.draftValues!.generationStatus = status;
|
|
expect(() => assertConnectedInputsReady([n])).toThrow("尚未成功");
|
|
}
|
|
expect(() => assertConnectedInputsReady([{ ...image("a"), draftValues: { sourceUrl: "https://media.example/preview.png", poster: "preview.png" } }])).toThrow("尚无可用素材");
|
|
expect(() => assertConnectedInputsReady([image("a")])).not.toThrow();
|
|
expect(() => assertConnectedInputsReady([{ ...image("a"), title: "拖入图片", draftValues: { uploadStatus: "uploading" } }])).toThrow("仍在上传");
|
|
expect(() => assertConnectedInputsReady([{ ...image("a"), title: "拖入图片", draftValues: { uploadStatus: "failed" } }])).toThrow("上传失败");
|
|
expect(() => connectedInputNodes([target], [edge("missing")], "target")).toThrow("已删除");
|
|
});
|
|
|
|
test("connected text enters prompts, refreshes on edit and disappears on disconnect", () => {
|
|
const text: ConnectionNode = { id: "text", inputType: "text", draftValues: { content: ["白色柜门"] } };
|
|
const first = withConnectedText("自然光", [text]);
|
|
expect(first).toContain("白色柜门");
|
|
text.draftValues!.content = ["红色柜门"];
|
|
const next = withConnectedText(first, [text]);
|
|
expect(next).toContain("红色柜门"); expect(next).not.toContain("白色柜门");
|
|
expect(withConnectedText(next, [])).toBe("自然光");
|
|
expect(connectedText({ id: "script", kind: "script", draftValues: { rows: [{ shot: "01", sceneDescription: "开门" }] } })).toContain("开门");
|
|
});
|
|
|
|
test("unsupported input types cannot produce decorative connections", () => {
|
|
expect(connectionTypeError({ id: "v", inputType: "video" }, target)).toContain("不能消费");
|
|
expect(connectionTypeError(image("a"), { id: "edit", inputType: "video", draftValues: { nodeType: "smart-edit" } })).toContain("不能消费");
|
|
expect(connectionTypeError({ id: "v", inputType: "video" }, { id: "edit", inputType: "video", draftValues: { nodeType: "smart-edit" } })).toBeNull();
|
|
expect(connectionTypeError({ id: "t", inputType: "text" }, { id: "tts", inputType: "audio" })).toBeNull();
|
|
expect(connectionTypeError({ id: "a", inputType: "audio" }, { ...target, draftValues: { targetPreset: "audio-to-video" } })).toBeNull();
|
|
const frameAnalysis = { id: "frame", inputType: "text", draftValues: { nodeType: "frame-analysis" } };
|
|
expect(connectionTypeError({ id: "v", inputType: "video" }, frameAnalysis)).toBeNull();
|
|
expect(connectionTypeError(image("a"), frameAnalysis)).toContain("不能消费");
|
|
});
|
|
|
|
|
|
test("video modes reject extra connected images rather than silently dropping edges", () => {
|
|
expect(videoInputLimitError("first-frame", 2)).toContain("1 张");
|
|
expect(videoInputLimitError("first-last-frame", 3)).toContain("2 张");
|
|
expect(videoInputLimitError("reference-images", 9)).toContain("8 张");
|
|
expect(videoInputLimitError("text-to-video", 1)).toContain("切换");
|
|
expect(videoInputLimitError("first-last-frame", 2)).toBeNull();
|
|
});
|
|
|
|
|
|
test("a failed selected-grid crop aborts instead of substituting the composite or a partial set", () => {
|
|
expect(() => requireResolvedReferenceImages(["https://media.example/cell.png", ""], 2)).toThrow("准备失败");
|
|
expect(() => requireResolvedReferenceImages([], 2)).toThrow("准备失败");
|
|
expect(requireResolvedReferenceImages(["https://media.example/1.png", "https://media.example/2.png"], 2)).toHaveLength(2);
|
|
});
|