418 lines
10 KiB
TypeScript
418 lines
10 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildGraphSnapshot,
|
|
resolveRenderPreviewInputFromGraph,
|
|
} from "@/lib/canvas-render-preview";
|
|
import {
|
|
computeMixerCompareOverlayImageStyle,
|
|
computeMixerFrameRectInSurface,
|
|
computeVisibleMixerContentRect,
|
|
computeMixerCropImageStyle,
|
|
isMixerCropImageReady,
|
|
} from "@/lib/mixer-crop-layout";
|
|
|
|
describe("resolveRenderPreviewInputFromGraph", () => {
|
|
it("resolves mixer input as renderable mixer composition", () => {
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
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: "overlay",
|
|
opacity: 76,
|
|
overlayX: 0.2,
|
|
overlayY: 0.1,
|
|
overlayWidth: 0.55,
|
|
overlayHeight: 0.44,
|
|
cropLeft: 0.08,
|
|
cropTop: 0.15,
|
|
cropRight: 0.22,
|
|
cropBottom: 0.1,
|
|
},
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[
|
|
{ source: "base-image", target: "mixer-1", targetHandle: "base" },
|
|
{ source: "overlay-image", target: "mixer-1", targetHandle: "overlay" },
|
|
{ source: "mixer-1", target: "render-1" },
|
|
],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({
|
|
nodeId: "render-1",
|
|
graph,
|
|
});
|
|
|
|
expect(preview).toEqual({
|
|
sourceUrl: null,
|
|
sourceComposition: {
|
|
kind: "mixer",
|
|
baseUrl: "https://cdn.example.com/base.png",
|
|
overlayUrl: "https://cdn.example.com/overlay.png",
|
|
blendMode: "overlay",
|
|
opacity: 76,
|
|
overlayX: 0.2,
|
|
overlayY: 0.1,
|
|
overlayWidth: 0.55,
|
|
overlayHeight: 0.44,
|
|
cropLeft: 0.08,
|
|
cropTop: 0.15,
|
|
cropRight: 0.22,
|
|
cropBottom: 0.1,
|
|
},
|
|
steps: [],
|
|
});
|
|
});
|
|
|
|
it("normalizes mixer composition values for render input", () => {
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
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: "unknown",
|
|
opacity: 180,
|
|
overlayX: -3,
|
|
overlayY: "1.4",
|
|
overlayWidth: 2,
|
|
overlayHeight: 0,
|
|
cropLeft: "0.95",
|
|
cropTop: -2,
|
|
cropRight: "4",
|
|
cropBottom: "0",
|
|
},
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[
|
|
{ source: "base-image", target: "mixer-1", targetHandle: "base" },
|
|
{ source: "overlay-image", target: "mixer-1", targetHandle: "overlay" },
|
|
{ source: "mixer-1", target: "render-1" },
|
|
],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({
|
|
nodeId: "render-1",
|
|
graph,
|
|
});
|
|
|
|
expect(preview.sourceComposition).toEqual({
|
|
kind: "mixer",
|
|
baseUrl: "https://cdn.example.com/base.png",
|
|
overlayUrl: "https://cdn.example.com/overlay.png",
|
|
blendMode: "normal",
|
|
opacity: 100,
|
|
overlayX: 0,
|
|
overlayY: 0.9,
|
|
overlayWidth: 1,
|
|
overlayHeight: 0.1,
|
|
cropLeft: 0.9,
|
|
cropTop: 0,
|
|
cropRight: 0,
|
|
cropBottom: 0,
|
|
});
|
|
});
|
|
|
|
it("includes crop in collected pipeline steps", () => {
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
id: "image-1",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/source.png" },
|
|
},
|
|
{
|
|
id: "crop-1",
|
|
type: "crop",
|
|
data: { cropRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.3 } },
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[
|
|
{ source: "image-1", target: "crop-1" },
|
|
{ source: "crop-1", target: "render-1" },
|
|
],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({
|
|
nodeId: "render-1",
|
|
graph,
|
|
});
|
|
|
|
expect(preview.steps).toEqual([
|
|
{
|
|
nodeId: "crop-1",
|
|
type: "crop",
|
|
params: { cropRect: { x: 0.1, y: 0.2, width: 0.4, height: 0.3 } },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("derives proxied pexels video source URL from mp4Url", () => {
|
|
const mp4Url = "https://player.pexels.com/videos/example.mp4";
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
id: "video-1",
|
|
type: "video",
|
|
data: { mp4Url },
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[{ source: "video-1", target: "render-1" }],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({ nodeId: "render-1", graph });
|
|
|
|
expect(preview.sourceUrl).toBe(`/api/pexels-video?u=${encodeURIComponent(mp4Url)}`);
|
|
});
|
|
|
|
it("uses ai-video data.url as source URL when available", () => {
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
id: "ai-video-1",
|
|
type: "ai-video",
|
|
data: { url: "https://cdn.example.com/generated-video.mp4" },
|
|
},
|
|
{
|
|
id: "render-1",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[{ source: "ai-video-1", target: "render-1" }],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({ nodeId: "render-1", graph });
|
|
|
|
expect(preview.sourceUrl).toBe("https://cdn.example.com/generated-video.mp4");
|
|
expect(preview.sourceComposition).toBeUndefined();
|
|
});
|
|
|
|
it("prefers live render preview URLs over stale baked render URLs inside downstream mixer compositions", () => {
|
|
const graph = buildGraphSnapshot(
|
|
[
|
|
{
|
|
id: "base-image",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/base.png" },
|
|
},
|
|
{
|
|
id: "overlay-upstream",
|
|
type: "image",
|
|
data: { url: "https://cdn.example.com/upstream.png" },
|
|
},
|
|
{
|
|
id: "render-overlay",
|
|
type: "render",
|
|
data: {
|
|
lastUploadUrl: "https://cdn.example.com/stale-render-output.png",
|
|
},
|
|
},
|
|
{
|
|
id: "mixer-1",
|
|
type: "mixer",
|
|
data: {},
|
|
},
|
|
{
|
|
id: "render-2",
|
|
type: "render",
|
|
data: {},
|
|
},
|
|
],
|
|
[
|
|
{ source: "overlay-upstream", target: "render-overlay" },
|
|
{ source: "base-image", target: "mixer-1", targetHandle: "base" },
|
|
{ source: "render-overlay", target: "mixer-1", targetHandle: "overlay" },
|
|
{ source: "mixer-1", target: "render-2" },
|
|
],
|
|
);
|
|
|
|
const preview = resolveRenderPreviewInputFromGraph({ nodeId: "render-2", graph });
|
|
|
|
expect(preview).toEqual({
|
|
sourceUrl: null,
|
|
sourceComposition: {
|
|
kind: "mixer",
|
|
baseUrl: "https://cdn.example.com/base.png",
|
|
overlayUrl: "https://cdn.example.com/upstream.png",
|
|
blendMode: "normal",
|
|
opacity: 100,
|
|
overlayX: 0,
|
|
overlayY: 0,
|
|
overlayWidth: 1,
|
|
overlayHeight: 1,
|
|
cropLeft: 0,
|
|
cropTop: 0,
|
|
cropRight: 0,
|
|
cropBottom: 0,
|
|
},
|
|
steps: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("mixer crop layout parity", () => {
|
|
it("contains a wide cropped source inside a square overlay frame", () => {
|
|
expect(
|
|
computeVisibleMixerContentRect({
|
|
frameAspectRatio: 1,
|
|
sourceWidth: 200,
|
|
sourceHeight: 100,
|
|
cropLeft: 0,
|
|
cropTop: 0.25,
|
|
cropRight: 0,
|
|
cropBottom: 0.25,
|
|
}),
|
|
).toEqual({
|
|
x: 0,
|
|
y: 0.375,
|
|
width: 1,
|
|
height: 0.25,
|
|
});
|
|
});
|
|
|
|
it("returns compare image styles that letterbox instead of stretching", () => {
|
|
expect(
|
|
computeMixerCropImageStyle({
|
|
frameAspectRatio: 1,
|
|
sourceWidth: 200,
|
|
sourceHeight: 100,
|
|
cropLeft: 0,
|
|
cropTop: 0,
|
|
cropRight: 0,
|
|
cropBottom: 0,
|
|
}),
|
|
).toEqual({
|
|
left: "0%",
|
|
top: "25%",
|
|
width: "100%",
|
|
height: "50%",
|
|
});
|
|
});
|
|
|
|
it("uses the actual base-aware frame pixel ratio for compare crop math", () => {
|
|
expect(
|
|
computeMixerCompareOverlayImageStyle({
|
|
surfaceWidth: 500,
|
|
surfaceHeight: 380,
|
|
baseWidth: 200,
|
|
baseHeight: 100,
|
|
overlayX: 0.1,
|
|
overlayY: 0.2,
|
|
overlayWidth: 0.4,
|
|
overlayHeight: 0.4,
|
|
sourceWidth: 200,
|
|
sourceHeight: 100,
|
|
cropLeft: 0.1,
|
|
cropTop: 0,
|
|
cropRight: 0.1,
|
|
cropBottom: 0,
|
|
}),
|
|
).toEqual({
|
|
left: "0%",
|
|
top: "0%",
|
|
width: "100%",
|
|
height: "100%",
|
|
});
|
|
});
|
|
|
|
it("does not mark compare crop overlay ready before natural size is known", () => {
|
|
expect(
|
|
isMixerCropImageReady({
|
|
currentOverlayUrl: "https://cdn.example.com/overlay-a.png",
|
|
loadedOverlayUrl: null,
|
|
sourceWidth: 0,
|
|
sourceHeight: 0,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("invalidates compare crop overlay readiness on source swap until the new image loads", () => {
|
|
expect(
|
|
isMixerCropImageReady({
|
|
currentOverlayUrl: "https://cdn.example.com/overlay-b.png",
|
|
loadedOverlayUrl: "https://cdn.example.com/overlay-a.png",
|
|
sourceWidth: 200,
|
|
sourceHeight: 100,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("positions mixer overlay frame relative to the displayed base-image rect", () => {
|
|
expect(
|
|
computeMixerFrameRectInSurface({
|
|
surfaceWidth: 1,
|
|
surfaceHeight: 1,
|
|
baseWidth: 200,
|
|
baseHeight: 100,
|
|
overlayX: 0.1,
|
|
overlayY: 0.2,
|
|
overlayWidth: 0.4,
|
|
overlayHeight: 0.4,
|
|
}),
|
|
).toEqual({
|
|
x: 0.1,
|
|
y: 0.35,
|
|
width: 0.4,
|
|
height: 0.2,
|
|
});
|
|
});
|
|
|
|
it("returns null frame placement until base image natural size is known", () => {
|
|
expect(
|
|
computeMixerFrameRectInSurface({
|
|
surfaceWidth: 1,
|
|
surfaceHeight: 1,
|
|
baseWidth: 0,
|
|
baseHeight: 0,
|
|
overlayX: 0.1,
|
|
overlayY: 0.2,
|
|
overlayWidth: 0.4,
|
|
overlayHeight: 0.4,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|