import { describe, it, expect, vi, beforeEach } from "vitest"; import { executeDerivedImage, isDerivedImageExecutionNode } from "../derivedImageExecutor"; import type { NodeExecutionContext } from "../types"; import type { WorkflowNode } from "@/types"; import { createCroppedImageBlob, loadCropImage } from "@/components/media/imageCropUtils"; import { uploadBlobToGatewayMedia } from "@/utils/gatewayMediaUpload"; vi.mock("@/components/media/imageCropUtils", () => ({ createCroppedImageBlob: vi.fn(), loadCropImage: vi.fn(), })); vi.mock("@/utils/gatewayMediaUpload", () => ({ uploadBlobToGatewayMedia: vi.fn(), })); function makeNode(data: Record): WorkflowNode { return { id: "derived-1", type: "smartImage", position: { x: 0, y: 0 }, data, } as WorkflowNode; } function makeCtx(node: WorkflowNode, images: string[] = ["source.png"]): NodeExecutionContext { return { node, getConnectedInputs: vi.fn().mockReturnValue({ images, videos: [], audio: [], model3d: null, text: null, textItems: [], dynamicInputs: {}, easeCurve: null, }), updateNodeData: vi.fn(), updateMediaNodeData: vi.fn(), getFreshNode: vi.fn().mockReturnValue(node), getEdges: vi.fn().mockReturnValue([]), getNodes: vi.fn().mockReturnValue([]), providerSettings: {} as any, addIncurredCost: vi.fn(), appendOutputGalleryImage: vi.fn(), get: vi.fn(), }; } describe("derivedImageExecutor", () => { beforeEach(() => { vi.clearAllMocks(); }); it("recognizes crop and splitGrid derived image nodes", () => { expect(isDerivedImageExecutionNode({ derivedImage: { operation: "crop" } })).toBe(true); expect(isDerivedImageExecutionNode({ derivedImage: { operation: "splitGrid" } })).toBe(true); expect(isDerivedImageExecutionNode({ derivedImage: { operation: "multiAngle" } })).toBe(false); }); it("executes crop tasks and writes the uploaded image back to the node", async () => { const blob = new Blob(["crop"], { type: "image/png" }); vi.mocked(createCroppedImageBlob).mockResolvedValue(blob); vi.mocked(uploadBlobToGatewayMedia).mockResolvedValue({ url: "https://media.example/crop.png", previewUrl: "https://media.example/crop-thumb.png", filename: "crop.png", contentType: "image/png", }); const node = makeNode({ filename: "crop.png", derivedImage: { sourceNodeId: "source-1", operation: "crop", task: { cropRect: { x: 1, y: 2, width: 100, height: 80 }, outputWidth: 100, outputHeight: 80, aspectRatio: "original", }, }, }); const ctx = makeCtx(node); await executeDerivedImage(ctx); expect(createCroppedImageBlob).toHaveBeenCalledWith("source.png", expect.objectContaining({ outputWidth: 100 })); expect(uploadBlobToGatewayMedia).toHaveBeenCalledWith(blob, "crop.png", "image/png", "image"); expect(ctx.updateMediaNodeData).toHaveBeenCalledWith("derived-1", expect.objectContaining({ image: "https://media.example/crop.png", previewImage: "https://media.example/crop-thumb.png", dimensions: { width: 100, height: 80 }, status: "complete", error: null, }), { width: 100, height: 80 }); }); it("sets an error when the source image is missing", async () => { const node = makeNode({ derivedImage: { sourceNodeId: "source-1", operation: "crop", task: { cropRect: { x: 0, y: 0, width: 10, height: 10 }, outputWidth: 10, outputHeight: 10, aspectRatio: "1:1", }, }, }); const ctx = makeCtx(node, []); await expect(executeDerivedImage(ctx)).rejects.toThrow("Missing source image."); expect(ctx.updateNodeData).toHaveBeenCalledWith("derived-1", { status: "error", error: "Missing source image.", }); }); it("uses grid task coordinates to find the selected cell", async () => { const blob = new Blob(["grid"], { type: "image/png" }); const drawImage = vi.fn(); vi.mocked(uploadBlobToGatewayMedia).mockResolvedValue({ url: "https://media.example/grid.png", filename: "grid.png", contentType: "image/png", }); vi.mocked(loadCropImage).mockResolvedValue({ naturalWidth: 40, naturalHeight: 40, width: 40, height: 40, } as HTMLImageElement); vi.spyOn(document, "createElement").mockReturnValue({ width: 0, height: 0, getContext: () => ({ drawImage }), toBlob: (callback: (blob: Blob | null) => void) => callback(blob), } as unknown as HTMLCanvasElement); const node = makeNode({ filename: "split-2-1.png", derivedImage: { sourceNodeId: "source-1", operation: "splitGrid", task: { rows: 2, cols: 2, row: 2, col: 1, cellKey: "2-1" }, }, }); const ctx = makeCtx(node); await executeDerivedImage(ctx); expect(loadCropImage).toHaveBeenCalledWith("source.png"); expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 20, 20, 20, 0, 0, 20, 20); expect(ctx.updateMediaNodeData).toHaveBeenCalledWith("derived-1", expect.objectContaining({ image: "https://media.example/grid.png", dimensions: { width: 20, height: 20 }, status: "complete", }), { width: 20, height: 20 }); }); });