@@ -18,6 +18,8 @@ import { NODE_TYPE_HIDDEN_PROP } from './consts.js';
1818import { TextAddedEvent , TuneModifiedEvent , ValueModifiedEvent } from '../../EventBus/events/index.js' ;
1919import { EventType } from '../../EventBus/types/EventType.js' ;
2020import { createBlockTuneName } from '../BlockTune/index.js' ;
21+ import { get } from '../../utils/keypath.js' ;
22+ import { AlreadyExistingKeyError } from './errors/AlreadyExistingKeyError.js' ;
2123
2224// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed to spy on conditional-typed getter with @jest/globals strict types
2325const ValueNodeProto = ValueNode . prototype as unknown as {
@@ -508,29 +510,151 @@ describe('BlockNode', () => {
508510 } ) ) ;
509511 } ) ;
510512
511- it ( 'should not change the node if key already exists' , ( ) => {
513+ it ( 'should throw an error if key already exists' , ( ) => {
512514 const key = createDataKey ( 'url' ) ;
513515 const value = 'https://editorjs.io' ;
514516 const blockNode = createBlockNodeWithData ( { [ key ] : value } ) ;
515517
516518 const currentNode = blockNode . data [ key ] ;
517519
518- blockNode . createDataNode ( key , 'another value' ) ;
520+ expect ( ( ) => {
521+ blockNode . createDataNode ( key , 'another value' ) ;
522+ } ) . toThrowError ( AlreadyExistingKeyError ) ;
523+ } ) ;
524+
525+ it ( 'should create value node at a nested path within an object' , ( ) => {
526+ const blockNode = createBlockNodeWithData ( { } ) ;
527+ const key = createDataKey ( 'meta.url' ) ;
528+ const value = 'https://editorjs.io' ;
519529
520- expect ( blockNode . data [ key ] ) . toStrictEqual ( currentNode ) ;
530+ blockNode . createDataNode ( key , value ) ;
531+
532+ expect ( get ( blockNode . data , 'meta.url' ) ) . toBeInstanceOf ( ValueNode ) ;
521533 } ) ;
522534
523- it ( 'should not emit DataNodeAddedEvent if key already exists' , ( ) => {
524- const key = createDataKey ( 'url' ) ;
535+ it ( 'should create text node at a nested path within an object' , ( ) => {
536+ const blockNode = createBlockNodeWithData ( { } ) ;
537+ const key = createDataKey ( 'meta.title' ) ;
538+ const value = { [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
539+ value : 'hello' ,
540+ fragments : [ ] } ;
541+
542+ blockNode . createDataNode ( key , value ) ;
543+
544+ expect ( get ( blockNode . data , 'meta.title' ) ) . toBeInstanceOf ( TextNode ) ;
545+ } ) ;
546+
547+ it ( 'should create value node at an array index path' , ( ) => {
548+ const blockNode = createBlockNodeWithData ( { } ) ;
549+ const key = createDataKey ( 'items.0' ) ;
550+ const value = 'first item' ;
551+
552+ blockNode . createDataNode ( key , value ) ;
553+
554+ expect ( get ( blockNode . data , 'items.0' ) ) . toBeInstanceOf ( ValueNode ) ;
555+ } ) ;
556+
557+ it ( 'should create value node in a nested object inside an array' , ( ) => {
558+ const blockNode = createBlockNodeWithData ( { } ) ;
559+ const key = createDataKey ( 'items.0.content' ) ;
560+ const value = 'content text' ;
561+
562+ blockNode . createDataNode ( key , value ) ;
563+
564+ expect ( get ( blockNode . data , 'items.0.content' ) ) . toBeInstanceOf ( ValueNode ) ;
565+ } ) ;
566+
567+ it ( 'should create text node in a nested object inside an array' , ( ) => {
568+ const blockNode = createBlockNodeWithData ( { } ) ;
569+ const key = createDataKey ( 'items.0.content' ) ;
570+ const value = {
571+ value : 'text' ,
572+ fragments : [ ] ,
573+ $t : 't' ,
574+ } ;
575+
576+ blockNode . createDataNode ( key , value ) ;
577+
578+ expect ( get ( blockNode . data , 'items.0.content' ) ) . toBeInstanceOf ( TextNode ) ;
579+ } ) ;
580+
581+ it ( 'should throw an error if a nested key already exists' , ( ) => {
582+ const blockNode = createBlockNodeWithData ( { meta : { url : 'editorjs.io' } } ) ;
583+ const key = createDataKey ( 'meta.url' ) ;
584+ const existingNode = get ( blockNode . data , 'meta.url' ) ;
585+
586+ expect ( ( ) => blockNode . createDataNode ( key , 'another value' ) )
587+ . toThrowError ( AlreadyExistingKeyError ) ;
588+
589+ expect ( get ( blockNode . data , 'meta.url' ) ) . toStrictEqual ( existingNode ) ;
590+ } ) ;
591+
592+ it ( 'should emit DataNodeAddedEvent with nested dataKey' , async ( ) => {
593+ const blockNode = createBlockNodeWithData ( { } ) ;
594+ const key = createDataKey ( 'meta.url' ) ;
525595 const value = 'https://editorjs.io' ;
526- const blockNode = createBlockNodeWithData ( { [ key ] : value } ) ;
527596 const listener = jest . fn ( ) ;
528597
529598 blockNode . addEventListener ( EventType . Changed , listener ) ;
530599
531600 blockNode . createDataNode ( key , value ) ;
532601
533- expect ( listener ) . not . toHaveBeenCalled ( ) ;
602+ await Promise . resolve ( ) ;
603+
604+ expect ( listener ) . toBeCalledWith ( expect . objectContaining ( {
605+ detail : expect . objectContaining ( {
606+ action : EventAction . Added ,
607+ index : expect . objectContaining ( { dataKey : key } ) ,
608+ } ) ,
609+ } ) ) ;
610+ } ) ;
611+
612+ it ( 'should splice a new node into an existing array at the given index' , ( ) => {
613+ const blockNode = createBlockNodeWithData ( {
614+ items : [
615+ { [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
616+ value : 'first' ,
617+ fragments : [ ] } ,
618+ { [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
619+ value : 'third' ,
620+ fragments : [ ] } ,
621+ ] ,
622+ } ) ;
623+
624+ blockNode . createDataNode ( createDataKey ( 'items.1' ) , {
625+ [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
626+ value : 'second' ,
627+ fragments : [ ] ,
628+ } ) ;
629+
630+ const items = ( blockNode . data as Record < string , unknown [ ] > ) [ 'items' ] ;
631+ const expectedLength = 3 ;
632+
633+ expect ( items ) . toHaveLength ( expectedLength ) ;
634+ expect ( items [ 1 ] ) . toBeInstanceOf ( TextNode ) ;
635+ } ) ;
636+
637+ it ( 'should shift existing nodes right when splicing into an array' , ( ) => {
638+ const blockNode = createBlockNodeWithData ( {
639+ items : [
640+ { [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
641+ value : 'second' ,
642+ fragments : [ ] } ,
643+ ] ,
644+ } ) ;
645+
646+ const originalNode = ( blockNode . data as Record < string , unknown [ ] > ) [ 'items' ] [ 0 ] ;
647+
648+ blockNode . createDataNode ( createDataKey ( 'items.0' ) , {
649+ [ NODE_TYPE_HIDDEN_PROP ] : BlockChildType . Text ,
650+ value : 'first' ,
651+ fragments : [ ] ,
652+ } ) ;
653+
654+ const items = ( blockNode . data as Record < string , unknown [ ] > ) [ 'items' ] ;
655+
656+ expect ( items ) . toHaveLength ( 2 ) ;
657+ expect ( items [ 1 ] ) . toStrictEqual ( originalNode ) ;
534658 } ) ;
535659 } ) ;
536660
@@ -544,6 +668,20 @@ describe('BlockNode', () => {
544668 expect ( result ) . toBeUndefined ( ) ;
545669 } ) ;
546670
671+ it ( 'should return undefined if the nested key does not exist' , ( ) => {
672+ const blockNode = createBlockNodeWithData ( { } ) ;
673+ const result = blockNode . getDataNode ( createDataKey ( 'meta.nonexistent' ) ) ;
674+
675+ expect ( result ) . toBeUndefined ( ) ;
676+ } ) ;
677+
678+ it ( 'should return undefined if the array index does not exist' , ( ) => {
679+ const blockNode = createBlockNodeWithData ( { } ) ;
680+ const result = blockNode . getDataNode ( createDataKey ( 'meta.0' ) ) ;
681+
682+ expect ( result ) . toBeUndefined ( ) ;
683+ } ) ;
684+
547685 it ( 'should return serialized ValueNode for a value key' , ( ) => {
548686 const key = createDataKey ( 'url' ) ;
549687 const value = 'https://editorjs.io' ;
@@ -630,6 +768,62 @@ describe('BlockNode', () => {
630768
631769 expect ( listener ) . not . toHaveBeenCalled ( ) ;
632770 } ) ;
771+
772+ it ( 'should remove data at a nested object path' , ( ) => {
773+ const blockNode = createBlockNodeWithData ( { meta : { url : 'editorjs.io' } } ) ;
774+
775+ blockNode . removeDataNode ( createDataKey ( 'meta.url' ) ) ;
776+
777+ expect ( get ( blockNode . data , 'meta.url' ) ) . toBeUndefined ( ) ;
778+ } ) ;
779+
780+ it ( 'should not remove sibling properties when removing a nested key' , ( ) => {
781+ const blockNode = createBlockNodeWithData ( { meta : { url : 'editorjs.io' ,
782+ title : 'Editor.js' } } ) ;
783+
784+ blockNode . removeDataNode ( createDataKey ( 'meta.url' ) ) ;
785+
786+ expect ( get ( blockNode . data , 'meta.title' ) ) . toBeDefined ( ) ;
787+ } ) ;
788+
789+ it ( 'should remove a node at an array index path' , ( ) => {
790+ const blockNode = createBlockNodeWithData ( { items : [ 'first' , 'second' ] } ) ;
791+
792+ blockNode . removeDataNode ( createDataKey ( 'items.0' ) ) ;
793+
794+ // After splice, 'second' shifts to index 0
795+ expect ( ( blockNode . data as Record < string , unknown [ ] > ) [ 'items' ] ) . toHaveLength ( 1 ) ;
796+ } ) ;
797+
798+ it ( 'should emit DataNodeRemovedEvent with a nested dataKey' , ( ) => {
799+ const blockNode = createBlockNodeWithData ( { meta : { url : 'editorjs.io' } } ) ;
800+ const key = createDataKey ( 'meta.url' ) ;
801+ const listener = jest . fn ( ) ;
802+
803+ jest . spyOn ( ValueNodeProto , 'serialized' , 'get' ) . mockReturnValueOnce ( 'editorjs.io' ) ;
804+
805+ blockNode . addEventListener ( EventType . Changed , listener ) ;
806+
807+ blockNode . removeDataNode ( key ) ;
808+
809+ expect ( listener ) . toBeCalledWith ( expect . objectContaining ( {
810+ detail : expect . objectContaining ( {
811+ action : EventAction . Removed ,
812+ index : expect . objectContaining ( { dataKey : key } ) ,
813+ } ) ,
814+ } ) ) ;
815+ } ) ;
816+
817+ it ( 'should not emit DataNodeRemovedEvent if nested key doesnt exist' , ( ) => {
818+ const blockNode = createBlockNodeWithData ( { meta : { } } ) ;
819+ const listener = jest . fn ( ) ;
820+
821+ blockNode . addEventListener ( EventType . Changed , listener ) ;
822+
823+ blockNode . removeDataNode ( createDataKey ( 'meta.nonexistent' ) ) ;
824+
825+ expect ( listener ) . not . toHaveBeenCalled ( ) ;
826+ } ) ;
633827 } ) ;
634828
635829 describe ( '.updateTuneData()' , ( ) => {
@@ -766,6 +960,30 @@ describe('BlockNode', () => {
766960 expect ( blockNode . data [ dataKey ] ) . toBeInstanceOf ( ValueNode ) ;
767961 } ) ;
768962
963+ it ( 'should create new ValueNode at a nested path if the node does not exist' , ( ) => {
964+ const blockNode = new BlockNode ( {
965+ name : createBlockToolName ( 'paragraph' ) ,
966+ data : { } ,
967+ parent : { } as EditorDocument ,
968+ } ) ;
969+
970+ blockNode . updateValue ( createDataKey ( 'meta.url' ) , 'https://editorjs.io' ) ;
971+
972+ expect ( get ( blockNode . data , 'meta.url' ) ) . toBeInstanceOf ( ValueNode ) ;
973+ } ) ;
974+
975+ it ( 'should create new ValueNode inside an array if the node does not exist' , ( ) => {
976+ const blockNode = new BlockNode ( {
977+ name : createBlockToolName ( 'paragraph' ) ,
978+ data : { } ,
979+ parent : { } as EditorDocument ,
980+ } ) ;
981+
982+ blockNode . updateValue ( createDataKey ( 'items.0' ) , 'first item' ) ;
983+
984+ expect ( get ( blockNode . data , 'items.0' ) ) . toBeInstanceOf ( ValueNode ) ;
985+ } ) ;
986+
769987 it ( 'should throw an error if the ValueNode with the passed dataKey is not a ValueNode' , ( ) => {
770988 const dataKey = createDataKey ( 'data-key-1a2b' ) ;
771989 const value = 'Some value' ;
@@ -913,6 +1131,22 @@ describe('BlockNode', () => {
9131131 expect ( node . data [ key ] ) . toBeInstanceOf ( TextNode ) ;
9141132 } ) ;
9151133
1134+ it ( 'should create new TextNode at a nested path if the node does not exist' , ( ) => {
1135+ const node = createBlockNodeWithData ( { } ) ;
1136+
1137+ node . insertText ( createDataKey ( 'meta.title' ) , text ) ;
1138+
1139+ expect ( get ( node . data , 'meta.title' ) ) . toBeInstanceOf ( TextNode ) ;
1140+ } ) ;
1141+
1142+ it ( 'should create new TextNode inside an array if the node does not exist' , ( ) => {
1143+ const node = createBlockNodeWithData ( { } ) ;
1144+
1145+ node . insertText ( createDataKey ( 'items.0' ) , text ) ;
1146+
1147+ expect ( get ( node . data , 'items.0' ) ) . toBeInstanceOf ( TextNode ) ;
1148+ } ) ;
1149+
9161150 it ( 'should throw an error if node is not a TextNode' , ( ) => {
9171151 const node = new BlockNode ( {
9181152 name : createBlockToolName ( 'header' ) ,
@@ -1503,6 +1737,8 @@ describe('BlockNode', () => {
15031737 tuneKey : key ,
15041738 tuneName : tuneName ,
15051739 } ) ) ;
1740+ expect ( event )
1741+ . toHaveProperty ( 'detail.userId' , 'user' ) ;
15061742 } ) ;
15071743
15081744 it ( 'should not emit Changed event if ValueNode dispatched event that is not a BaseDocumentEvent' , ( ) => {
0 commit comments