11/* eslint-disable @typescript-eslint/no-magic-numbers */
2- import type { BlockNodeSerialized , DataKey } from '@editorjs/model' ;
2+ import type { BlockNodeSerialized , DataKey , DocumentIndex } from '@editorjs/model' ;
33import { IndexBuilder } from '@editorjs/model' ;
44import { describe } from '@jest/globals' ;
55import { type InsertOrDeleteOperationData , type ModifyOperationData , Operation , OperationType } from './Operation.js' ;
@@ -10,7 +10,8 @@ const createOperation = (
1010 // eslint-disable-next-line @typescript-eslint/no-explicit-any
1111 value : string | [ BlockNodeSerialized ] | Record < any , any > ,
1212 // eslint-disable-next-line @typescript-eslint/no-explicit-any
13- prevValue ?: Record < any , any >
13+ prevValue ?: Record < any , any > ,
14+ endIndex ?: number
1415) : Operation => {
1516 const index = new IndexBuilder ( )
1617 . addBlockIndex ( 0 ) ;
@@ -40,6 +41,234 @@ const createOperation = (
4041
4142
4243describe ( 'Operation' , ( ) => {
44+ describe ( '.transform()' , ( ) => {
45+ it ( 'should not change operation if document ids are different' , ( ) => {
46+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
47+ const localOp = createOperation ( OperationType . Insert , 0 , 'def' ) ;
48+
49+ localOp . index . documentId = 'document2' as DocumentIndex ;
50+ const transformedOp = receivedOp . transform ( localOp ) ;
51+
52+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
53+ } ) ;
54+
55+ it ( 'should not change operation if data keys are different' , ( ) => {
56+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
57+ const localOp = createOperation ( OperationType . Insert , 0 , 'def' ) ;
58+
59+ localOp . index . dataKey = 'dataKey2' as DataKey ;
60+
61+ const transformedOp = receivedOp . transform ( localOp ) ;
62+
63+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
64+ } ) ;
65+
66+ it ( 'should throw Unsupppoted index type error if op is not Block or Text operation' , ( ) => {
67+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
68+ const localOp = createOperation ( OperationType . Insert , 0 , 'def' ) ;
69+
70+ localOp . index . textRange = undefined ;
71+
72+ try {
73+ receivedOp . transform ( localOp ) ;
74+ } catch ( e ) {
75+ expect ( e ) . toBeInstanceOf ( Error ) ;
76+ expect ( ( e as Error ) . message ) . toContain ( 'Unsupported index type' ) ;
77+ }
78+ } ) ;
79+
80+ it ( 'should throw an error if unsupported operation type is provided' , ( ) => {
81+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'def' ) ;
82+ // @ts -expect-error — for test purposes
83+ const localOp = createOperation ( 'unsupported' , 0 , 'def' ) ;
84+
85+ expect ( ( ) => receivedOp . transform ( localOp ) ) . toThrow ( 'Unsupported operation type' ) ;
86+ } ) ;
87+
88+ it ( 'should not transform relative to the Modify operation (as Modify operation doesn\'t change index)' , ( ) => {
89+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
90+ const localOp = createOperation ( OperationType . Modify , 0 , 'def' ) ;
91+ const transformedOp = receivedOp . transform ( localOp ) ;
92+
93+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
94+ } ) ;
95+
96+ describe ( 'Transformation relative to Insert operation' , ( ) => {
97+ it ( 'should not change a received operation if it is before a local one' , ( ) => {
98+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
99+ const localOp = createOperation ( OperationType . Insert , 3 , 'def' ) ;
100+ const transformedOp = receivedOp . transform ( localOp ) ;
101+
102+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
103+ } ) ;
104+
105+ it ( 'should transform an index for a received operation if it is after a local one' , ( ) => {
106+ const receivedOp = createOperation ( OperationType . Delete , 3 , 'def' ) ;
107+ const localOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
108+ const transformedOp = receivedOp . transform ( localOp ) ;
109+
110+ expect ( transformedOp . index . textRange ) . toEqual ( [ 6 , 6 ] ) ;
111+ } ) ;
112+
113+ it ( 'should transform a received operation if it is at the same position as a local one' , ( ) => {
114+ const receivedOp = createOperation ( OperationType . Modify , 0 , 'abc' ) ;
115+ const localOp = createOperation ( OperationType . Insert , 0 , 'def' ) ;
116+ const transformedOp = receivedOp . transform ( localOp ) ;
117+
118+ expect ( transformedOp . index . textRange ) . toEqual ( [ 3 , 3 ] ) ;
119+ } ) ;
120+
121+ it ( 'should not change the text index if local op is a Block operation' , ( ) => {
122+ const receivedOp = createOperation ( OperationType . Modify , 0 , 'abc' ) ;
123+ const localOp = createOperation ( OperationType . Insert , 0 , [ {
124+ name : 'paragraph' ,
125+ data : { text : 'hello' } ,
126+ } ] ) ;
127+ const transformedOp = receivedOp . transform ( localOp ) ;
128+
129+ expect ( transformedOp . index . textRange ) . toEqual ( [ 0 , 0 ] ) ;
130+ } ) ;
131+
132+ it ( 'should not change the operation if local op is a Block operation after a received one' , ( ) => {
133+ const receivedOp = createOperation ( OperationType . Insert , 0 , [ {
134+ name : 'paragraph' ,
135+ data : { text : 'abc' } ,
136+ } ] ) ;
137+ const localOp = createOperation ( OperationType . Insert , 1 , [ {
138+ name : 'paragraph' ,
139+ data : { text : 'hello' } ,
140+ } ] ) ;
141+
142+ const transformedOp = receivedOp . transform ( localOp ) ;
143+
144+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
145+ } ) ;
146+
147+ it ( 'should adjust the block index if local op is a Block operation before a received one' , ( ) => {
148+ const receivedOp = createOperation ( OperationType . Insert , 1 , [ {
149+ name : 'paragraph' ,
150+ data : { text : 'abc' } ,
151+ } ] ) ;
152+ const localOp = createOperation ( OperationType . Insert , 0 , [ {
153+ name : 'paragraph' ,
154+ data : { text : 'hello' } ,
155+ } ] ) ;
156+
157+ const transformedOp = receivedOp . transform ( localOp ) ;
158+
159+ expect ( transformedOp . index . blockIndex ) . toEqual ( 2 ) ;
160+ } ) ;
161+
162+ it ( 'should adjust the block index if local op is a Block operation at the same index as a received one' , ( ) => {
163+ const receivedOp = createOperation ( OperationType . Insert , 0 , [ {
164+ name : 'paragraph' ,
165+ data : { text : 'abc' } ,
166+ } ] ) ;
167+ const localOp = createOperation ( OperationType . Insert , 0 , [ {
168+ name : 'paragraph' ,
169+ data : { text : 'hello' } ,
170+ } ] ) ;
171+
172+ const transformedOp = receivedOp . transform ( localOp ) ;
173+
174+ expect ( transformedOp . index . blockIndex ) . toEqual ( 1 ) ;
175+ } ) ;
176+ } ) ;
177+
178+ describe ( 'Transformation relative to Delete operation' , ( ) => {
179+ it ( 'should not change a received operation if it is before a local one' , ( ) => {
180+ const receivedOp = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
181+ const localOp = createOperation ( OperationType . Delete , 3 , 'def' ) ;
182+ const transformedOp = receivedOp . transform ( localOp ) ;
183+
184+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
185+ } ) ;
186+
187+ it ( 'should transform an index for a received operation if it is after a local one' , ( ) => {
188+ const receivedOp = createOperation ( OperationType . Delete , 3 , 'def' ) ;
189+ const localOp = createOperation ( OperationType . Delete , 0 , 'abc' ) ;
190+ const transformedOp = receivedOp . transform ( localOp ) ;
191+
192+ expect ( transformedOp . index . textRange ) . toEqual ( [ 0 , 0 ] ) ;
193+ } ) ;
194+
195+ it ( 'should transform a received operation if it is at the same position as a local one' , ( ) => {
196+ const receivedOp = createOperation ( OperationType . Modify , 3 , 'abc' ) ;
197+ const localOp = createOperation ( OperationType . Delete , 0 , 'def' , undefined , 3 ) ;
198+ const transformedOp = receivedOp . transform ( localOp ) ;
199+
200+ expect ( transformedOp . index . textRange ) . toEqual ( [ 0 , 0 ] ) ;
201+ } ) ;
202+
203+ it ( 'should not change the text index if local op is a Block operation' , ( ) => {
204+ const receivedOp = createOperation ( OperationType . Modify , 1 , 'abc' ) ;
205+ const localOp = createOperation ( OperationType . Delete , 0 , [ {
206+ name : 'paragraph' ,
207+ data : { text : 'hello' } ,
208+ } ] ) ;
209+ const transformedOp = receivedOp . transform ( localOp ) ;
210+
211+ expect ( transformedOp . index . textRange ) . toEqual ( [ 1 , 1 ] ) ;
212+ } ) ;
213+
214+ it ( 'should not change the text index if local op is a Block operation' , ( ) => {
215+ const receivedOp = createOperation ( OperationType . Modify , 0 , 'abc' ) ;
216+ const localOp = createOperation ( OperationType . Insert , 0 , [ {
217+ name : 'paragraph' ,
218+ data : { text : 'hello' } ,
219+ } ] ) ;
220+ const transformedOp = receivedOp . transform ( localOp ) ;
221+
222+ expect ( transformedOp . index . textRange ) . toEqual ( [ 0 , 0 ] ) ;
223+ } ) ;
224+
225+ it ( 'should not change the operation if local op is a Block operation after a received one' , ( ) => {
226+ const receivedOp = createOperation ( OperationType . Insert , 0 , [ {
227+ name : 'paragraph' ,
228+ data : { text : 'abc' } ,
229+ } ] ) ;
230+ const localOp = createOperation ( OperationType . Delete , 1 , [ {
231+ name : 'paragraph' ,
232+ data : { text : 'hello' } ,
233+ } ] ) ;
234+
235+ const transformedOp = receivedOp . transform ( localOp ) ;
236+
237+ expect ( transformedOp ) . toEqual ( receivedOp ) ;
238+ } ) ;
239+
240+ it ( 'should adjust the block index if local op is a Block operation before a received one' , ( ) => {
241+ const receivedOp = createOperation ( OperationType . Insert , 1 , [ {
242+ name : 'paragraph' ,
243+ data : { text : 'abc' } ,
244+ } ] ) ;
245+ const localOp = createOperation ( OperationType . Delete , 0 , [ {
246+ name : 'paragraph' ,
247+ data : { text : 'hello' } ,
248+ } ] ) ;
249+
250+ const transformedOp = receivedOp . transform ( localOp ) ;
251+
252+ expect ( transformedOp . index . blockIndex ) . toEqual ( 0 ) ;
253+ } ) ;
254+
255+ it ( 'should return Neutral operation if local op is a Block operation at the same index as a received one' , ( ) => {
256+ const receivedOp = createOperation ( OperationType . Insert , 1 , [ {
257+ name : 'paragraph' ,
258+ data : { text : 'abc' } ,
259+ } ] ) ;
260+ const localOp = createOperation ( OperationType . Delete , 1 , [ {
261+ name : 'paragraph' ,
262+ data : { text : 'hello' } ,
263+ } ] ) ;
264+
265+ const transformedOp = receivedOp . transform ( localOp ) ;
266+
267+ expect ( transformedOp . type ) . toBe ( OperationType . Neutral ) ;
268+ } ) ;
269+ } ) ;
270+ } ) ;
271+
43272 describe ( '.inverse()' , ( ) => {
44273 it ( 'should change the type of Insert operation to Delete operation' , ( ) => {
45274 const op = createOperation ( OperationType . Insert , 0 , 'abc' ) ;
0 commit comments