import { getSnapshot , unprotect , recordPatches , types , IType , IJsonPatch , Instance , cast , IAnyModelType , IMSTMap , escapeJsonPath , getPath , resolvePath , splitJsonPath , joinJsonPath } from "../../src" ; function testPatches < C , S , T > ( type , snapshot , fn , expectedPatches ) { const instance = type . create ( snapshot ) ; const baseSnapshot = getSnapshot ( instance ) ; const recorder = recordPatches ( instance ) ; unprotect ( instance ) ; fn ( instance ) ; recorder . stop ( ) ; expect ( recorder . patches ) . toEqual ( expectedPatches ) ; const clone = type . create ( snapshot ) ; recorder . replay ( clone ) ; expect ( getSnapshot ( clone ) ) . toEqual ( getSnapshot ( instance ) ) ; recorder . undo ( ) ; expect ( getSnapshot ( instance ) ) . toEqual ( baseSnapshot ) ; } const Node = types . model ( "Node" , { id : types . identifierNumber , text : "Hi" , children : types . optional ( types . array ( types . late ( ( ) : IAnyModelType => Node ) ) , [ ] ) } ) ; test ( "it should apply simple patch" , ( ) => { testPatches ( Node , { id : 1 } , ( n ) => { n . text = "test" ; } , [ { op : "replace" , path : "/text" , value : "test" } ] ) ; } ) ; test ( "it should apply deep patches to arrays" , ( ) => { testPatches ( Node , { id : 1 , children : [ { id : 2 } ] } , ( n ) => { const children = ( n . children as unknown ) as Instance < typeof Node > [ ] ; children [ 0 ] . text = "test" ; children [ 0 ] = cast ( { id : 2 , text : "world" } ) ; children [ 0 ] = cast ( { id : 4 , text : "coffee" } ) ; children [ 1 ] = cast ( { id : 3 , text : "world" } ) ; children . splice ( 0 , 1 ) ; } , [ { op : "replace" , path : "/children/0/text" , value : "test" } , { op : "replace" , path : "/children/0/text" , value : "world" } , { op : "replace" , path : "/children/0" , value : { id : 4 , text : "coffee" , children : [ ] } } , { op : "add" , path : "/children/1" , value : { id : 3 , text : "world" , children : [ ] } } , { op : "remove" , path : "/children/0" } ] ) ; } ) ; test ( "it should apply deep patches to arrays with object instances" , ( ) => { testPatches ( Node , { id : 1 , children : [ { id : 2 } ] } , ( n ) => { const children = ( n . children as unknown ) as Instance < typeof Node > [ ] ; children [ 0 ] . text = "test" ; children [ 0 ] = Node . create ( { id : 2 , text : "world" } ) ; children [ 0 ] = Node . create ( { id : 4 , text : "coffee" } ) ; } , [ { op : "replace" , path : "/children/0/text" , value : "test" } , { op : "replace" , path : "/children/0" , value : { id : 2 , text : "world" , children : [ ] } } , { op : "replace" , path : "/children/0" , value : { id : 4 , text : "coffee" , children : [ ] } } ] ) ; } ) ; test ( "it should apply non flat patches" , ( ) => { testPatches ( Node , { id : 1 } , ( n ) => { const children = ( n . children as unknown ) as Instance < typeof Node > [ ] ; children . push ( cast ( { id : 2 , children : [ { id : 4 } , { id : 5 , text : "Tea" } ] } ) ) ; } , [ { op : "add" , path : "/children/0" , value : { id : 2 , text : "Hi" , children : [ { id : 4 , text : "Hi" , children : [ ] } , { id : 5 , text : "Tea" , children : [ ] } ] } } ] ) ; } ) ; test ( "it should apply non flat patches with object instances" , ( ) => { testPatches ( Node , { id : 1 } , ( n ) => { const children = ( n . children as unknown ) as Instance < typeof Node > [ ] ; children . push ( Node . create ( { id : 2 , children : [ { id : 5 , text : "Tea" } ] } ) ) ; } , [ { op : "add" , path : "/children/0" , value : { id : 2 , text : "Hi" , children : [ { id : 5 , text : "Tea" , children : [ ] } ] } } ] ) ; } ) ; test ( "it should apply deep patches to maps" , ( ) => { const NodeMap = types . model ( "NodeMap" , { id : types . identifierNumber , text : "Hi" , children : types . optional ( types . map ( types . late ( ( ) : IAnyModelType => NodeMap ) ) , { } ) } ) ; testPatches ( NodeMap , { id : 1 , children : { 2 : { id : 2 } } } , ( n ) => { const children = n . children as IMSTMap < typeof NodeMap > ; children . get ( "2" ) ! . text = "test" ; children . put ( { id : 2 , text : "world" } ) ; children . set ( "4" , NodeMap . create ( { id : 4 , text : "coffee" , children : { 23 : { id : 23 } } } ) ) ; children . put ( { id : 3 , text : "world" , children : { 7 : { id : 7 } } } ) ; children . delete ( "2" ) ; } , [ { op : "replace" , path : "/children/2/text" , value : "test" } , { op : "replace" , path : "/children/2/text" , value : "world" } , { op : "add" , path : "/children/4" , value : { children : { 23 : { children : { } , id : 23 , text : "Hi" } } , id : 4 , text : "coffee" } } , { op : "add" , path : "/children/3" , value : { children : { 7 : { children : { } , id : 7 , text : "Hi" } } , id : 3 , text : "world" } } , { op : "remove" , path : "/children/2" } ] ) ; } ) ; test ( "it should apply deep patches to objects" , ( ) => { const NodeObject = types . model ( "NodeObject" , { id : types . identifierNumber , text : "Hi" , child : types . maybe ( types . late ( ( ) : IAnyModelType => NodeObject ) ) } ) ; testPatches ( NodeObject , { id : 1 , child : { id : 2 } } , ( n ) => { n . child ! . text = "test" ; n . child = cast ( { id : 2 , text : "world" } ) ; n . child = NodeObject . create ( { id : 2 , text : "coffee" , child : { id : 23 } } ) ; n . child = cast ( { id : 3 , text : "world" , child : { id : 7 } } ) ; n . child = undefined ; } , [ { op : "replace" , path : "/child/text" , value : "test" } , { op : "replace" , path : "/child/text" , value : "world" } , { op : "replace" , path : "/child" , value : { child : { child : undefined , id : 23 , text : "Hi" } , id : 2 , text : "coffee" } } , { op : "replace" , path : "/child" , value : { child : { child : undefined , id : 7 , text : "Hi" } , id : 3 , text : "world" } } , { op : "replace" , path : "/child" , value : undefined } ] ) ; } ) ; test ( "it should correctly split/join json patches" , ( ) => { function isValid ( str , array , altStr ? ) { expect ( splitJsonPath ( str ) ) . toEqual ( array ) ; expect ( joinJsonPath ( array ) ) . toBe ( altStr !== undefined ? altStr : str ) ; } isValid ( "" , [ ] ) ; isValid ( "/" , [ "" ] ) ; isValid ( "//" , [ "" , "" ] ) ; isValid ( "/a" , [ "a" ] ) ; isValid ( "/a/" , [ "a" , "" ] ) ; isValid ( "/a//" , [ "a" , "" , "" ] ) ; isValid ( "." , [ "." ] ) ; isValid ( ".." , [ ".." ] ) ; isValid ( "./a" , [ "." , "a" ] ) ; isValid ( "../a" , [ ".." , "a" ] ) ; isValid ( "/.a" , [ ".a" ] ) ; isValid ( "/..a" , [ "..a" ] ) ; isValid ( "/." , [ "." ] , "." ) ; isValid ( "/.." , [ ".." ] , ".." ) ; isValid ( "/./a" , [ "." , "a" ] , "./a" ) ; isValid ( "/../a" , [ ".." , "a" ] , "../a" ) ; function isInvalid ( str ) { expect ( ( ) => { splitJsonPath ( str ) ; } ) . toThrow ( "a json path must be either rooted, empty or relative" ) ; } isInvalid ( "a" ) ; isInvalid ( "a/" ) ; isInvalid ( "a//" ) ; isInvalid ( ".a" ) ; isInvalid ( ".a/" ) ; isInvalid ( "..a" ) ; isInvalid ( "..a/" ) ; } ) ; test ( "it should correctly escape/unescape json patches" , ( ) => { expect ( escapeJsonPath ( "http://example.com" ) ) . toBe ( "http:~1~1example.com" ) ; const AppStore = types . model ( { items : types . map ( types . frozen < any > ( ) ) } ) ; testPatches ( AppStore , { items : { } } , ( store ) => { store . items . set ( "with/slash~tilde" , 1 ) ; } , [ { op : "add" , path : "/items/with~1slash~0tilde" , value : 1 } ] ) ; } ) ; test ( "weird keys are handled correctly" , ( ) => { const Store = types . model ( { map : types . map ( types . model ( { model : types . model ( { value : types . string } ) } ) ) } ) ; const store = Store . create ( { map : { "" : { model : { value : "val1" } } , "/" : { model : { value : "val2" } } , "~" : { model : { value : "val3" } } } } ) ; { const target = store . map . get ( "" ) ! . model ; const path = getPath ( target ) ; expect ( path ) . toBe ( "/map//model" ) ; expect ( resolvePath ( store , path ) ) . toBe ( target ) ; } { const target = store . map . get ( "/" ) ! . model ; const path = getPath ( target ) ; expect ( path ) . toBe ( "/map/~1/model" ) ; expect ( resolvePath ( store , path ) ) . toBe ( target ) ; } { const target = store . map . get ( "~" ) ! . model ; const path = getPath ( target ) ; expect ( path ) . toBe ( "/map/~0/model" ) ; expect ( resolvePath ( store , path ) ) . toBe ( target ) ; } } ) ; test ( "relativePath with a different base than the root works correctly" , ( ) => { const Store = types . model ( { map : types . map ( types . model ( { model : types . model ( { value : types . string } ) } ) ) } ) ; const store = Store . create ( { map : { "1" : { model : { value : "val1" } } , "2" : { model : { value : "val2" } } } } ) ; { const target = store . map . get ( "1" ) ! . model ; expect ( resolvePath ( store . map , "./1/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "../map/1/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "/./1/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "/../map/1/model" ) ) . toBe ( target ) ; } { const target = store . map . get ( "2" ) ! . model ; expect ( resolvePath ( store . map , "./2/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "../map/2/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "/./2/model" ) ) . toBe ( target ) ; expect ( resolvePath ( store . map , "/../map/2/model" ) ) . toBe ( target ) ; } } ) ;