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[]) => 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); });