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
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 });
|
|
});
|
|
});
|
|
|