From 8f2c3192d8e397fc1bcc83662fcb96f98d3f3f2e Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Sat, 4 Apr 2026 07:53:36 +0200 Subject: [PATCH] fix(canvas): restore compare previews for render inputs --- .../canvas/__tests__/canvas-helpers.test.ts | 96 +++++++++++++++++++ components/canvas/canvas-helpers.ts | 35 ++++++- vitest.config.ts | 1 + 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 components/canvas/__tests__/canvas-helpers.test.ts diff --git a/components/canvas/__tests__/canvas-helpers.test.ts b/components/canvas/__tests__/canvas-helpers.test.ts new file mode 100644 index 0000000..bdb735b --- /dev/null +++ b/components/canvas/__tests__/canvas-helpers.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import type { Edge as RFEdge, Node as RFNode } from "@xyflow/react"; + +import { withResolvedCompareData } from "../canvas-helpers"; + +function createNode(overrides: Partial & Pick): RFNode { + return { + position: { x: 0, y: 0 }, + data: {}, + ...overrides, + } as RFNode; +} + +function createEdge( + overrides: Partial & Pick, +): RFEdge { + return { + ...overrides, + } as RFEdge; +} + +describe("withResolvedCompareData", () => { + it("does not backfill compare render input from the upstream source image when no render output exists", () => { + const imageNode = createNode({ + id: "image-1", + type: "image", + data: { url: "https://cdn.example.com/source.png" }, + }); + const renderNode = createNode({ + id: "render-1", + type: "render", + data: {}, + }); + const compareNode = createNode({ + id: "compare-1", + type: "compare", + data: {}, + }); + + const nextNodes = withResolvedCompareData( + [imageNode, renderNode, compareNode], + [ + createEdge({ id: "edge-image-render", source: "image-1", target: "render-1" }), + createEdge({ + id: "edge-render-compare", + source: "render-1", + target: "compare-1", + targetHandle: "left", + }), + ], + ); + + const nextCompare = nextNodes.find((node) => node.id === "compare-1"); + expect(nextCompare).toBeDefined(); + expect((nextCompare?.data as { leftUrl?: string }).leftUrl).toBeUndefined(); + }); + + it("uses uploaded render output URLs for compare inputs when available", () => { + const imageNode = createNode({ + id: "image-1", + type: "image", + data: { url: "https://cdn.example.com/source.png" }, + }); + const renderNode = createNode({ + id: "render-1", + type: "render", + data: { + lastUploadUrl: "https://cdn.example.com/render-output.png", + }, + }); + const compareNode = createNode({ + id: "compare-1", + type: "compare", + data: {}, + }); + + const nextNodes = withResolvedCompareData( + [imageNode, renderNode, compareNode], + [ + createEdge({ id: "edge-image-render", source: "image-1", target: "render-1" }), + createEdge({ + id: "edge-render-compare", + source: "render-1", + target: "compare-1", + targetHandle: "left", + }), + ], + ); + + const nextCompare = nextNodes.find((node) => node.id === "compare-1"); + expect(nextCompare).toBeDefined(); + expect((nextCompare?.data as { leftUrl?: string }).leftUrl).toBe( + "https://cdn.example.com/render-output.png", + ); + }); +}); diff --git a/components/canvas/canvas-helpers.ts b/components/canvas/canvas-helpers.ts index a3fe45d..b19b883 100644 --- a/components/canvas/canvas-helpers.ts +++ b/components/canvas/canvas-helpers.ts @@ -116,6 +116,36 @@ export function withResolvedCompareData(nodes: RFNode[], edges: RFEdge[]): RFNod return undefined; }; + const resolveRenderOutputUrl = (node: RFNode): string | undefined => { + const nodeData = node.data as { + url?: string; + lastUploadUrl?: string; + storageId?: string; + lastUploadStorageId?: string; + }; + + if (typeof nodeData.lastUploadUrl === "string" && nodeData.lastUploadUrl.length > 0) { + return nodeData.lastUploadUrl; + } + + if (typeof nodeData.url === "string" && nodeData.url.length > 0) { + return nodeData.url; + } + + const storageId = + typeof nodeData.storageId === "string" + ? nodeData.storageId + : typeof nodeData.lastUploadStorageId === "string" + ? nodeData.lastUploadStorageId + : undefined; + + if (storageId) { + return resolveStorageFallbackUrl(storageId); + } + + return undefined; + }; + const resolvePipelineImageUrl = (sourceNode: RFNode): string | undefined => { const direct = resolveImageFromNode(sourceNode); if (direct) { @@ -163,7 +193,10 @@ export function withResolvedCompareData(nodes: RFNode[], edges: RFEdge[]): RFNod ? sourceDataRecord.lastUploadStorageId : undefined; const hasSourceUrl = typeof srcData.url === "string" && srcData.url.length > 0; - let resolvedUrl = resolvePipelineImageUrl(source); + let resolvedUrl = + source.type === "render" + ? resolveRenderOutputUrl(source) + : resolvePipelineImageUrl(source); if ( resolvedUrl === undefined && !hasSourceUrl && diff --git a/vitest.config.ts b/vitest.config.ts index a672d96..654be67 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ environment: "node", include: [ "tests/**/*.test.ts", + "components/canvas/__tests__/canvas-helpers.test.ts", "components/canvas/__tests__/canvas-flow-reconciliation-helpers.test.ts", "components/canvas/__tests__/use-canvas-flow-reconciliation.test.ts", "components/canvas/__tests__/use-canvas-drop.test.tsx",