Browse Source

test(quick-010): add unit tests for binary stripping and equality utilities

- 21 tests covering:
  - stripBinaryData: Image, video, audio, 3D, history, thumbnail stripping
  - Preservation of non-binary fields and ref fields
  - No mutation of original nodes
  - Empty array handling
  - undoStateEquality: Referential equality checks for all state fields
  - partializeForUndo: Output shape verification and binary stripping
  - BINARY_DATA_KEYS: Complete key set validation
- All tests passing
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
f520fb0378
  1. 454
      src/store/__tests__/undoUtils.test.ts

454
src/store/__tests__/undoUtils.test.ts

@ -0,0 +1,454 @@
import { describe, it, expect } from "vitest";
import { stripBinaryData, undoStateEquality, partializeForUndo, BINARY_DATA_KEYS } from "../undoUtils";
import type { WorkflowNode, WorkflowEdge, NodeGroup } from "@/types";
import type { EdgeStyle } from "../workflowStore";
describe("undoUtils", () => {
describe("stripBinaryData", () => {
it("strips image fields from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "imageInput",
position: { x: 0, y: 0 },
data: {
image: "data:image/png;base64,ABC123",
outputImage: "data:image/png;base64,DEF456",
sourceImage: "data:image/png;base64,GHI789",
imageRef: "img_001",
prompt: "test prompt",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("image");
expect(stripped[0].data).not.toHaveProperty("outputImage");
expect(stripped[0].data).not.toHaveProperty("sourceImage");
expect(stripped[0].data).toHaveProperty("imageRef", "img_001");
expect(stripped[0].data).toHaveProperty("prompt", "test prompt");
});
it("strips video fields from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "generateVideo",
position: { x: 0, y: 0 },
data: {
outputVideo: "data:video/mp4;base64,VIDEO123",
video: "data:video/mp4;base64,VIDEO456",
outputVideoRef: "vid_001",
model: "test-model",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("outputVideo");
expect(stripped[0].data).not.toHaveProperty("video");
expect(stripped[0].data).toHaveProperty("outputVideoRef", "vid_001");
expect(stripped[0].data).toHaveProperty("model", "test-model");
});
it("strips audio fields from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "audioInput",
position: { x: 0, y: 0 },
data: {
audioFile: "data:audio/mp3;base64,AUDIO123",
outputAudio: "data:audio/mp3;base64,AUDIO456",
audio: "data:audio/mp3;base64,AUDIO789",
fileName: "test.mp3",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("audioFile");
expect(stripped[0].data).not.toHaveProperty("outputAudio");
expect(stripped[0].data).not.toHaveProperty("audio");
expect(stripped[0].data).toHaveProperty("fileName", "test.mp3");
});
it("strips 3D model fields from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "glbViewer",
position: { x: 0, y: 0 },
data: {
glbUrl: "data:model/gltf-binary;base64,GLB123",
output3dUrl: "data:model/gltf-binary;base64,GLB456",
modelName: "test.glb",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("glbUrl");
expect(stripped[0].data).not.toHaveProperty("output3dUrl");
expect(stripped[0].data).toHaveProperty("modelName", "test.glb");
});
it("strips history arrays from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "nanoBanana",
position: { x: 0, y: 0 },
data: {
imageHistory: [
{ timestamp: "2024-01-01", image: "data:image/png;base64,IMG1" },
{ timestamp: "2024-01-02", image: "data:image/png;base64,IMG2" },
],
videoHistory: [
{ timestamp: "2024-01-01", video: "data:video/mp4;base64,VID1" },
],
audioHistory: [
{ timestamp: "2024-01-01", audio: "data:audio/mp3;base64,AUD1" },
],
prompt: "test prompt",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("imageHistory");
expect(stripped[0].data).not.toHaveProperty("videoHistory");
expect(stripped[0].data).not.toHaveProperty("audioHistory");
expect(stripped[0].data).toHaveProperty("prompt", "test prompt");
});
it("strips thumbnail fields from node data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "videoStitch",
position: { x: 0, y: 0 },
data: {
thumbnail: "data:image/png;base64,THUMB123",
thumbnails: ["data:image/png;base64,THUMB1", "data:image/png;base64,THUMB2"],
clips: [],
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("thumbnail");
expect(stripped[0].data).not.toHaveProperty("thumbnails");
expect(stripped[0].data).toHaveProperty("clips");
});
it("preserves non-binary fields", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "nanoBanana",
position: { x: 100, y: 200 },
data: {
image: "data:image/png;base64,ABC123",
prompt: "test prompt",
model: "gemini-2.5-flash-image",
aspectRatio: "1:1",
selectedModel: { provider: "gemini", modelId: "test" },
seed: 12345,
steps: 20,
guidanceScale: 7.5,
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("image");
expect(stripped[0].data).toHaveProperty("prompt", "test prompt");
expect(stripped[0].data).toHaveProperty("model", "gemini-2.5-flash-image");
expect(stripped[0].data).toHaveProperty("aspectRatio", "1:1");
expect(stripped[0].data).toHaveProperty("selectedModel");
expect(stripped[0].data).toHaveProperty("seed", 12345);
expect(stripped[0].data).toHaveProperty("steps", 20);
expect(stripped[0].data).toHaveProperty("guidanceScale", 7.5);
});
it("preserves ref fields", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "imageInput",
position: { x: 0, y: 0 },
data: {
image: "data:image/png;base64,ABC123",
imageRef: "img_001",
outputImageRef: "img_002",
sourceImageRef: "img_003",
inputImageRefs: ["img_004", "img_005"],
outputVideoRef: "vid_001",
outputAudioRef: "aud_001",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).not.toHaveProperty("image");
expect(stripped[0].data).toHaveProperty("imageRef", "img_001");
expect(stripped[0].data).toHaveProperty("outputImageRef", "img_002");
expect(stripped[0].data).toHaveProperty("sourceImageRef", "img_003");
expect(stripped[0].data).toHaveProperty("inputImageRefs");
expect(stripped[0].data).toHaveProperty("outputVideoRef", "vid_001");
expect(stripped[0].data).toHaveProperty("outputAudioRef", "aud_001");
});
it("does not mutate original nodes", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "imageInput",
position: { x: 0, y: 0 },
data: {
image: "data:image/png;base64,ABC123",
prompt: "test",
},
},
];
const originalImage = nodes[0].data.image;
stripBinaryData(nodes);
expect(nodes[0].data.image).toBe(originalImage);
expect(nodes[0].data).toHaveProperty("image", "data:image/png;base64,ABC123");
});
it("handles empty nodes array", () => {
const nodes: WorkflowNode[] = [];
const stripped = stripBinaryData(nodes);
expect(stripped).toEqual([]);
});
it("handles nodes with no binary data", () => {
const nodes: WorkflowNode[] = [
{
id: "node-1",
type: "prompt",
position: { x: 0, y: 0 },
data: {
prompt: "test prompt",
variableName: "myPrompt",
},
},
];
const stripped = stripBinaryData(nodes);
expect(stripped[0].data).toEqual({
prompt: "test prompt",
variableName: "myPrompt",
});
});
});
describe("undoStateEquality", () => {
const createMockState = () => ({
nodes: [
{
id: "node-1",
type: "prompt" as const,
position: { x: 0, y: 0 },
data: { prompt: "test" },
},
],
edges: [
{
id: "edge-1",
source: "node-1",
target: "node-2",
data: {},
},
] as WorkflowEdge[],
edgeStyle: "curved" as EdgeStyle,
groups: {} as Record<string, NodeGroup>,
});
it("returns true when same object refs (no change)", () => {
const state = createMockState();
const result = undoStateEquality(state, state);
expect(result).toBe(true);
});
it("returns false when edges array differs", () => {
const state1 = createMockState();
const state2 = createMockState();
state2.edges = [...state2.edges]; // New array ref
const result = undoStateEquality(state1, state2);
expect(result).toBe(false);
});
it("returns false when edgeStyle differs", () => {
const state1 = createMockState();
const state2 = createMockState();
state2.edgeStyle = "angular";
const result = undoStateEquality(state1, state2);
expect(result).toBe(false);
});
it("returns false when groups object differs", () => {
const state1 = createMockState();
const state2 = createMockState();
state2.groups = { ...state2.groups }; // New object ref
const result = undoStateEquality(state1, state2);
expect(result).toBe(false);
});
it("returns false when node count differs", () => {
const state1 = createMockState();
const state2 = createMockState();
state2.nodes = [
...state2.nodes,
{
id: "node-2",
type: "prompt" as const,
position: { x: 100, y: 100 },
data: { prompt: "test2" },
},
];
const result = undoStateEquality(state1, state2);
expect(result).toBe(false);
});
it("returns false when any node ref differs (position change)", () => {
const state1 = createMockState();
const state2 = createMockState();
state2.nodes = [
{
...state2.nodes[0],
position: { x: 100, y: 100 },
},
];
const result = undoStateEquality(state1, state2);
expect(result).toBe(false);
});
it("returns true when all node refs are identical even with different wrapper array", () => {
const state1 = createMockState();
const state2 = {
...state1,
nodes: [...state1.nodes], // New array ref but same node refs
};
const result = undoStateEquality(state1, state2);
expect(result).toBe(true);
});
});
describe("partializeForUndo", () => {
it("returns object with only nodes, edges, edgeStyle, groups", () => {
const mockState = {
nodes: [],
edges: [],
edgeStyle: "curved" as EdgeStyle,
groups: {},
isRunning: false,
clipboard: null,
openModalCount: 0,
workflowId: "test-id",
hasUnsavedChanges: true,
};
const result = partializeForUndo(mockState);
expect(Object.keys(result)).toEqual(["nodes", "edges", "edgeStyle", "groups"]);
expect(result).not.toHaveProperty("isRunning");
expect(result).not.toHaveProperty("clipboard");
expect(result).not.toHaveProperty("openModalCount");
expect(result).not.toHaveProperty("workflowId");
expect(result).not.toHaveProperty("hasUnsavedChanges");
});
it("returned nodes have binary data stripped", () => {
const mockState = {
nodes: [
{
id: "node-1",
type: "imageInput" as const,
position: { x: 0, y: 0 },
data: {
image: "data:image/png;base64,ABC123",
imageRef: "img_001",
prompt: "test",
},
},
],
edges: [],
edgeStyle: "curved" as EdgeStyle,
groups: {},
isRunning: false,
};
const result = partializeForUndo(mockState);
expect(result.nodes[0].data).not.toHaveProperty("image");
expect(result.nodes[0].data).toHaveProperty("imageRef", "img_001");
expect(result.nodes[0].data).toHaveProperty("prompt", "test");
});
});
describe("BINARY_DATA_KEYS", () => {
it("contains all expected binary field names", () => {
const expectedKeys = [
// Image fields
"image",
"outputImage",
"sourceImage",
"inputImages",
"images",
"imageA",
"imageB",
"capturedImage",
// Video fields
"outputVideo",
"video",
// Audio fields
"audioFile",
"outputAudio",
"audio",
// 3D fields
"glbUrl",
"output3dUrl",
// History arrays
"imageHistory",
"videoHistory",
"audioHistory",
"globalImageHistory",
// Thumbnail fields
"thumbnail",
"thumbnails",
];
expectedKeys.forEach((key) => {
expect(BINARY_DATA_KEYS.has(key)).toBe(true);
});
});
});
});
Loading…
Cancel
Save