267 lines
6.8 KiB
TypeScript
267 lines
6.8 KiB
TypeScript
import React from "react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
|
|
import { CanvasGraphProvider } from "@/components/canvas/canvas-graph-context";
|
|
|
|
type StoreState = {
|
|
nodes: Array<{ id: string; type?: string; data?: unknown }>;
|
|
edges: Array<{
|
|
id: string;
|
|
source: string;
|
|
target: string;
|
|
className?: string;
|
|
targetHandle?: string;
|
|
}>;
|
|
};
|
|
|
|
const storeState: StoreState = {
|
|
nodes: [],
|
|
edges: [],
|
|
};
|
|
|
|
const compareSurfaceSpy = vi.fn();
|
|
|
|
vi.mock("@xyflow/react", () => ({
|
|
Handle: () => null,
|
|
Position: { Left: "left", Right: "right" },
|
|
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
|
|
}));
|
|
|
|
vi.mock("../nodes/base-node-wrapper", () => ({
|
|
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
|
}));
|
|
|
|
vi.mock("../nodes/compare-surface", () => ({
|
|
default: (props: unknown) => {
|
|
compareSurfaceSpy(props);
|
|
return null;
|
|
},
|
|
}));
|
|
|
|
import CompareNode from "../nodes/compare-node";
|
|
|
|
function renderCompareNode(props: Record<string, unknown>) {
|
|
return renderToStaticMarkup(
|
|
<CanvasGraphProvider
|
|
nodes={storeState.nodes as Array<{ id: string; type: string; data?: unknown }>}
|
|
edges={storeState.edges}
|
|
>
|
|
<CompareNode {...(props as React.ComponentProps<typeof CompareNode>)} />
|
|
</CanvasGraphProvider>,
|
|
);
|
|
}
|
|
|
|
describe("CompareNode render preview inputs", () => {
|
|
beforeEach(() => {
|
|
storeState.nodes = [];
|
|
storeState.edges = [];
|
|
compareSurfaceSpy.mockReset();
|
|
});
|
|
|
|
it("passes previewInput to CompareSurface for a connected render node without final output", () => {
|
|
storeState.nodes = [
|
|
{
|
|
id: "image-1",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/source.png" },
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
];
|
|
storeState.edges = [
|
|
{ id: "edge-image-render", source: "image-1", target: "render-1" },
|
|
{
|
|
id: "edge-render-compare",
|
|
source: "render-1",
|
|
target: "compare-1",
|
|
targetHandle: "left",
|
|
},
|
|
];
|
|
|
|
renderCompareNode({
|
|
id: "compare-1",
|
|
data: { leftUrl: "https://cdn.example.com/render-output.png" },
|
|
selected: false,
|
|
dragging: false,
|
|
zIndex: 0,
|
|
isConnectable: true,
|
|
type: "compare",
|
|
xPos: 0,
|
|
yPos: 0,
|
|
width: 500,
|
|
height: 380,
|
|
sourcePosition: undefined,
|
|
targetPosition: undefined,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
});
|
|
|
|
expect(compareSurfaceSpy).toHaveBeenCalled();
|
|
const previewCall = compareSurfaceSpy.mock.calls.find(
|
|
([props]) =>
|
|
Boolean(
|
|
(props as { previewInput?: { sourceUrl: string; steps: unknown[] } }).previewInput,
|
|
),
|
|
);
|
|
expect(previewCall).toBeDefined();
|
|
expect(
|
|
(previewCall?.[0] as { previewInput?: { sourceUrl: string; steps: unknown[] } })
|
|
.previewInput,
|
|
).toEqual({
|
|
sourceUrl: "https://cdn.example.com/source.png",
|
|
steps: [],
|
|
});
|
|
});
|
|
|
|
it("defaults render-backed compare inputs to preview mode even when a final render output exists", () => {
|
|
storeState.nodes = [
|
|
{
|
|
id: "image-1",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/source.png" },
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {
|
|
lastUploadUrl: "https://cdn.example.com/render-output.png",
|
|
},
|
|
},
|
|
];
|
|
storeState.edges = [
|
|
{ id: "edge-image-render", source: "image-1", target: "render-1" },
|
|
{
|
|
id: "edge-render-compare",
|
|
source: "render-1",
|
|
target: "compare-1",
|
|
targetHandle: "left",
|
|
},
|
|
];
|
|
|
|
renderCompareNode({
|
|
id: "compare-1",
|
|
data: { leftUrl: "https://cdn.example.com/render-output.png" },
|
|
selected: false,
|
|
dragging: false,
|
|
zIndex: 0,
|
|
isConnectable: true,
|
|
type: "compare",
|
|
xPos: 0,
|
|
yPos: 0,
|
|
width: 500,
|
|
height: 380,
|
|
sourcePosition: undefined,
|
|
targetPosition: undefined,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
});
|
|
|
|
expect(compareSurfaceSpy).toHaveBeenCalledTimes(1);
|
|
expect(compareSurfaceSpy.mock.calls[0]?.[0]).toMatchObject({
|
|
finalUrl: "https://cdn.example.com/render-output.png",
|
|
preferPreview: true,
|
|
});
|
|
});
|
|
|
|
it("prefers mixer composite preview over persisted compare finalUrl when mixer is connected", () => {
|
|
storeState.nodes = [
|
|
{
|
|
id: "base-image",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/base.png" },
|
|
},
|
|
{
|
|
id: "overlay-image",
|
|
type: "asset",
|
|
data: { url: "https://cdn.example.com/overlay.png" },
|
|
},
|
|
{
|
|
id: "mixer-1",
|
|
type: "mixer",
|
|
data: {
|
|
blendMode: "multiply",
|
|
opacity: 62,
|
|
offsetX: 12,
|
|
offsetY: -4,
|
|
},
|
|
},
|
|
{
|
|
id: "right-image",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/right.png" },
|
|
},
|
|
];
|
|
storeState.edges = [
|
|
{
|
|
id: "edge-base-mixer",
|
|
source: "base-image",
|
|
target: "mixer-1",
|
|
targetHandle: "base",
|
|
},
|
|
{
|
|
id: "edge-overlay-mixer",
|
|
source: "overlay-image",
|
|
target: "mixer-1",
|
|
targetHandle: "overlay",
|
|
},
|
|
{
|
|
id: "edge-mixer-compare",
|
|
source: "mixer-1",
|
|
target: "compare-1",
|
|
targetHandle: "left",
|
|
},
|
|
{
|
|
id: "edge-image-compare",
|
|
source: "right-image",
|
|
target: "compare-1",
|
|
targetHandle: "right",
|
|
},
|
|
];
|
|
|
|
renderCompareNode({
|
|
id: "compare-1",
|
|
data: {
|
|
leftUrl: "https://cdn.example.com/base.png",
|
|
rightUrl: "https://cdn.example.com/right.png",
|
|
},
|
|
selected: false,
|
|
dragging: false,
|
|
zIndex: 0,
|
|
isConnectable: true,
|
|
type: "compare",
|
|
xPos: 0,
|
|
yPos: 0,
|
|
width: 500,
|
|
height: 380,
|
|
sourcePosition: undefined,
|
|
targetPosition: undefined,
|
|
positionAbsoluteX: 0,
|
|
positionAbsoluteY: 0,
|
|
});
|
|
|
|
expect(compareSurfaceSpy).toHaveBeenCalledTimes(2);
|
|
const mixerCall = compareSurfaceSpy.mock.calls.find(
|
|
([props]) =>
|
|
Boolean((props as { mixerPreviewState?: { status?: string } }).mixerPreviewState),
|
|
);
|
|
expect(mixerCall?.[0]).toMatchObject({
|
|
finalUrl: undefined,
|
|
mixerPreviewState: {
|
|
status: "ready",
|
|
baseUrl: "https://cdn.example.com/base.png",
|
|
overlayUrl: "https://cdn.example.com/overlay.png",
|
|
blendMode: "multiply",
|
|
opacity: 62,
|
|
overlayX: 0,
|
|
overlayY: 0,
|
|
overlayWidth: 1,
|
|
overlayHeight: 1,
|
|
},
|
|
});
|
|
});
|
|
});
|