You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

161 lines
5.3 KiB

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<string, unknown>): 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 });
});
});