@ -1,5 +1,5 @@
import { describe , it , expect , beforeEach } from "vitest" ;
import { UndoManager , UndoSnapshot } from "../undoHistory" ;
import { UndoManager , UndoSnapshot , clonePreservingStrings } from "../undoHistory" ;
function makeSnapshot ( label : string ) : UndoSnapshot {
return {
@ -146,3 +146,125 @@ describe("UndoManager", () => {
expect ( manager . canRedo ) . toBe ( false ) ;
} ) ;
} ) ;
describe ( "clonePreservingStrings" , ( ) = > {
it ( "preserves string references (identity, not just equality)" , ( ) = > {
const bigString = "data:image/png;base64," + "A" . repeat ( 1000 ) ;
const input = { image : bigString , label : "test" } ;
const cloned = clonePreservingStrings ( input ) ;
// Same string reference — not a copy
expect ( cloned . image ) . toBe ( input . image ) ;
expect ( cloned . label ) . toBe ( input . label ) ;
} ) ;
it ( "creates new object containers" , ( ) = > {
const input = { a : 1 , nested : { b : 2 } } ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( input ) ;
expect ( cloned ) . not . toBe ( input ) ;
expect ( cloned . nested ) . not . toBe ( input . nested ) ;
} ) ;
it ( "creates new array containers" , ( ) = > {
const input = [ 1 , [ 2 , 3 ] ] ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( input ) ;
expect ( cloned ) . not . toBe ( input ) ;
expect ( cloned [ 1 ] ) . not . toBe ( input [ 1 ] ) ;
} ) ;
it ( "skips undefined values in objects" , ( ) = > {
const input = { a : 1 , b : undefined , c : "hello" } ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( { a : 1 , c : "hello" } ) ;
expect ( "b" in cloned ) . toBe ( false ) ;
} ) ;
it ( "converts undefined to null in arrays" , ( ) = > {
const input = [ 1 , undefined , 3 ] ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( [ 1 , null , 3 ] ) ;
} ) ;
it ( "skips function values in objects" , ( ) = > {
const input = { a : 1 , fn : ( ) = > 42 , c : "hello" } ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( { a : 1 , c : "hello" } ) ;
expect ( "fn" in cloned ) . toBe ( false ) ;
} ) ;
it ( "converts functions to null in arrays" , ( ) = > {
const input = [ 1 , ( ) = > 42 , 3 ] ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( [ 1 , null , 3 ] ) ;
} ) ;
it ( "handles nested structures with large base64-like strings" , ( ) = > {
const blob1 = "data:image/png;base64," + "B" . repeat ( 5000 ) ;
const blob2 = "data:audio/wav;base64," + "C" . repeat ( 3000 ) ;
const input = {
nodes : [
{ id : "1" , data : { image : blob1 , prompt : "a cat" } } ,
{ id : "2" , data : { audio : blob2 } } ,
] ,
edges : [ { source : "1" , target : "2" } ] ,
} ;
const cloned = clonePreservingStrings ( input ) ;
// Structure is equivalent
expect ( cloned ) . toEqual ( input ) ;
// Containers are independent
expect ( cloned . nodes ) . not . toBe ( input . nodes ) ;
expect ( cloned . nodes [ 0 ] ) . not . toBe ( input . nodes [ 0 ] ) ;
expect ( cloned . nodes [ 0 ] . data ) . not . toBe ( input . nodes [ 0 ] . data ) ;
// Strings share references
expect ( cloned . nodes [ 0 ] . data . image ) . toBe ( blob1 ) ;
expect ( cloned . nodes [ 0 ] . data . prompt ) . toBe ( input . nodes [ 0 ] . data . prompt ) ;
expect ( cloned . nodes [ 1 ] . data . audio ) . toBe ( blob2 ) ;
} ) ;
it ( "produces output equivalent to JSON.parse(JSON.stringify()) for plain data" , ( ) = > {
const input = {
nodes : [
{ id : "n1" , type : "imageInput" , position : { x : 100 , y : 200 } , data : { image : "base64data" } } ,
{ id : "n2" , type : "prompt" , position : { x : 300 , y : 200 } , data : { prompt : "hello" } } ,
] ,
edges : [ { id : "e1" , source : "n1" , target : "n2" } ] ,
groups : { g1 : { name : "Group" , color : "blue" , nodeIds : [ "n1" , "n2" ] } } ,
edgeStyle : "curved" ,
} ;
const jsonClone = JSON . parse ( JSON . stringify ( input ) ) ;
const fastClone = clonePreservingStrings ( input ) ;
expect ( fastClone ) . toEqual ( jsonClone ) ;
} ) ;
it ( "handles null values correctly" , ( ) = > {
const input = { a : null , b : [ null , 1 ] } ;
const cloned = clonePreservingStrings ( input ) ;
expect ( cloned ) . toEqual ( { a : null , b : [ null , 1 ] } ) ;
} ) ;
it ( "handles primitives at the top level" , ( ) = > {
expect ( clonePreservingStrings ( "hello" ) ) . toBe ( "hello" ) ;
expect ( clonePreservingStrings ( 42 ) ) . toBe ( 42 ) ;
expect ( clonePreservingStrings ( true ) ) . toBe ( true ) ;
expect ( clonePreservingStrings ( null ) ) . toBe ( null ) ;
} ) ;
it ( "handles empty objects and arrays" , ( ) = > {
expect ( clonePreservingStrings ( { } ) ) . toEqual ( { } ) ;
expect ( clonePreservingStrings ( [ ] ) ) . toEqual ( [ ] ) ;
} ) ;
} ) ;