Skip to content

Commit bb68221

Browse files
committed
test(collaboration-manager): add tests for transformations
1 parent 59e8c4f commit bb68221

11 files changed

+450
-437
lines changed

packages/collaboration-manager/src/BatchedOperation.spec.ts

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { createDataKey, IndexBuilder } from '@editorjs/model';
2-
import { OperationsBatch } from './BatchedOperation.js';
3-
import { Operation, OperationType, SerializedOperation } from './Operation.js';
1+
import { createDataKey, Index, IndexBuilder } from '@editorjs/model';
2+
import { BatchedOperation } from './BatchedOperation.js';
3+
import type { SerializedOperation } from './Operation.js';
4+
import { Operation, OperationType } from './Operation.js';
45

56
const templateIndex = new IndexBuilder()
67
.addBlockIndex(0)
@@ -22,7 +23,7 @@ describe('Batch', () => {
2223
userId
2324
);
2425

25-
const batch = new OperationsBatch(op1);
26+
const batch = new BatchedOperation(op1);
2627

2728
batch.add(op2);
2829

@@ -42,7 +43,7 @@ describe('Batch', () => {
4243
userId
4344
);
4445

45-
const batch = new OperationsBatch(op1);
46+
const batch = new BatchedOperation(op1);
4647

4748
batch.add(op2);
4849

@@ -56,23 +57,30 @@ describe('Batch', () => {
5657
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
5758
const op2 = new Operation(
5859
OperationType.Insert,
59-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
60+
new IndexBuilder().from(templateIndex)
61+
.addTextRange([1, 1])
62+
.build(),
6063
{ payload: 'b' },
6164
userId
6265
);
63-
const originalBatch = new OperationsBatch(op1);
66+
const originalBatch = new BatchedOperation(op1);
67+
6468
originalBatch.add(op2);
6569

66-
const newBatch = OperationsBatch.from(originalBatch);
70+
71+
const newBatch = BatchedOperation.from(originalBatch);
72+
console.log('original operations', originalBatch.operations);
73+
74+
console.log('new operations', newBatch.operations)
6775

68-
expect(newBatch.operations).toEqual(originalBatch.operations);
76+
expect(newBatch.operations).toStrictEqual(originalBatch.operations);
6977
expect(newBatch).not.toBe(originalBatch); // Should be a new instance
7078
});
7179

7280
it('should create a new batch from serialized operation', () => {
7381
const serializedOp: SerializedOperation<OperationType> = new Operation(OperationType.Delete, templateIndex, { payload: 'a' }, userId).serialize();
7482

75-
const batch = OperationsBatch.from(serializedOp);
83+
const batch = BatchedOperation.from(serializedOp);
7684

7785
expect(batch.operations[0].type).toBe(serializedOp.type);
7886
expect(batch.operations[0].data).toEqual(serializedOp.data);
@@ -84,11 +92,14 @@ describe('Batch', () => {
8492
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
8593
const op2 = new Operation(
8694
OperationType.Insert,
87-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
95+
new IndexBuilder().from(templateIndex)
96+
.addTextRange([1, 1])
97+
.build(),
8898
{ payload: 'b' },
8999
userId
90100
);
91-
const batch = new OperationsBatch(op1);
101+
const batch = new BatchedOperation(op1);
102+
92103
batch.add(op2);
93104

94105
const inversedBatch = batch.inverse();
@@ -103,16 +114,21 @@ describe('Batch', () => {
103114
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
104115
const op2 = new Operation(
105116
OperationType.Insert,
106-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
117+
new IndexBuilder().from(templateIndex)
118+
.addTextRange([1, 1])
119+
.build(),
107120
{ payload: 'b' },
108121
userId
109122
);
110-
const batch = new OperationsBatch(op1);
123+
const batch = new BatchedOperation(op1);
124+
111125
batch.add(op2);
112126

113127
const againstOp = new Operation(
114128
OperationType.Insert,
115-
new IndexBuilder().from(templateIndex).addTextRange([0, 0]).build(),
129+
new IndexBuilder().from(templateIndex)
130+
.addTextRange([0, 0])
131+
.build(),
116132
{ payload: 'x' },
117133
'other-user'
118134
);
@@ -126,16 +142,23 @@ describe('Batch', () => {
126142
expect(transformedBatch!.operations[1].index.textRange![0]).toBe(2);
127143
});
128144

129-
it('should return null if no operations can be transformed', () => {
145+
it('should return batch with Neutral operations if no operations can be transformed', () => {
130146
const op = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
131-
const batch = new OperationsBatch(op);
132-
147+
const batch = new BatchedOperation(op);
148+
149+
const deleteIndex = new IndexBuilder()
150+
.from(templateIndex)
151+
.addTextRange([0, 2])
152+
.build();
153+
133154
// An operation that would make transformation impossible
134-
const againstOp = new Operation(OperationType.Delete, templateIndex, { payload: 'a' }, 'other-user');
155+
const againstOp = new Operation(OperationType.Delete, deleteIndex, { payload: 'a' }, 'other-user');
135156

136157
const transformedBatch = batch.transform(againstOp);
137158

138-
expect(transformedBatch).toBeNull();
159+
const neutralOp = new Operation(OperationType.Neutral, templateIndex, { payload: 'a' }, userId)
160+
161+
expect(transformedBatch.operations[0]).toEqual(neutralOp);
139162
});
140163
});
141164

@@ -144,11 +167,13 @@ describe('Batch', () => {
144167
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
145168
const op2 = new Operation(
146169
OperationType.Insert,
147-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
170+
new IndexBuilder().from(templateIndex)
171+
.addTextRange([1, 1])
172+
.build(),
148173
{ payload: 'b' },
149174
userId
150175
);
151-
const batch = new OperationsBatch(op1);
176+
const batch = new BatchedOperation(op1);
152177

153178
expect(batch.canAdd(op2)).toBe(true);
154179
});
@@ -157,11 +182,13 @@ describe('Batch', () => {
157182
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
158183
const op2 = new Operation(
159184
OperationType.Insert,
160-
new IndexBuilder().from(templateIndex).addTextRange([2, 2]).build(),
185+
new IndexBuilder().from(templateIndex)
186+
.addTextRange([2, 2])
187+
.build(),
161188
{ payload: 'b' },
162189
userId
163190
);
164-
const batch = new OperationsBatch(op1);
191+
const batch = new BatchedOperation(op1);
165192

166193
expect(batch.canAdd(op2)).toBe(false);
167194
});
@@ -170,11 +197,13 @@ describe('Batch', () => {
170197
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
171198
const op2 = new Operation(
172199
OperationType.Delete,
173-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
200+
new IndexBuilder().from(templateIndex)
201+
.addTextRange([1, 1])
202+
.build(),
174203
{ payload: 'b' },
175204
userId
176205
);
177-
const batch = new OperationsBatch(op1);
206+
const batch = new BatchedOperation(op1);
178207

179208
expect(batch.canAdd(op2)).toBe(false);
180209
});
@@ -183,11 +212,13 @@ describe('Batch', () => {
183212
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
184213
const op2 = new Operation(
185214
OperationType.Modify,
186-
new IndexBuilder().from(templateIndex).addTextRange([1, 1]).build(),
215+
new IndexBuilder().from(templateIndex)
216+
.addTextRange([1, 1])
217+
.build(),
187218
{ payload: { tool: 'bold' } },
188219
userId
189220
);
190-
const batch = new OperationsBatch(op1);
221+
const batch = new BatchedOperation(op1);
191222

192223
expect(batch.canAdd(op2)).toBe(false);
193224
});

packages/collaboration-manager/src/BatchedOperation.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import { InvertedOperationType, Operation, OperationType, type SerializedOperation } from './Operation.js';
1+
import type { InvertedOperationType } from './Operation.js';
2+
import { Operation, OperationType, type SerializedOperation } from './Operation.js';
23

34
/**
45
* Class to batch Text operations (maybe others in the future) for Undo/Redo purposes
5-
*
6-
* Operations are batched on timeout basis or if batch is terminated from the outside
76
*/
87
export class BatchedOperation<T extends OperationType = OperationType> extends Operation<T> {
98
/**
109
* Array of operations to batch
1110
*/
12-
operations: (Operation<T> | Operation<OperationType.Neutral>)[] = [];
11+
public operations: (Operation<T> | Operation<OperationType.Neutral>)[] = [];
1312

1413
/**
1514
* Batch constructor function
@@ -26,42 +25,42 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
2625

2726
/**
2827
* Create a new operation batch from an array of operations
29-
*
28+
*
3029
* @param opBatch - operation batch to clone
31-
*/
30+
*/
3231
public static from<T extends OperationType>(opBatch: BatchedOperation<T>): BatchedOperation<T>;
3332

3433
/**
3534
* Create a new operation batch from a serialized operation
36-
*
35+
*
3736
* @param json - serialized operation
3837
*/
3938
public static from<T extends OperationType>(json: SerializedOperation<T>): BatchedOperation<T>;
4039

4140
/**
4241
* Create a new operation batch from an operation batch or a serialized operation
43-
*
42+
*
4443
* @param opBatchOrJSON - operation batch or serialized operation
45-
*/
44+
*/
4645
public static from<T extends OperationType>(opBatchOrJSON: BatchedOperation<T> | SerializedOperation<T>): BatchedOperation<T> {
4746
if (opBatchOrJSON instanceof BatchedOperation) {
4847
/**
4948
* Every batch should have at least one operation
5049
*/
51-
const batch = new BatchedOperation(Operation.from(opBatchOrJSON.operations.shift()!));
50+
const batch = new BatchedOperation(Operation.from(opBatchOrJSON.operations[0]));
5251

53-
opBatchOrJSON.operations.forEach((op) => {
52+
opBatchOrJSON.operations.slice(1).forEach((op) => {
5453
/**
5554
* Deep clone operation to the new batch
5655
*/
5756
batch.add(Operation.from(op));
5857
});
59-
58+
6059
return batch as BatchedOperation<T>;
6160
} else {
6261
const batch = new BatchedOperation<T>(Operation.from(opBatchOrJSON));
6362

64-
return batch;
63+
return batch;
6564
}
6665
}
6766

@@ -78,15 +77,17 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
7877
/**
7978
* Method that inverses all of the operations in the batch
8079
*
81-
* @returns new batch with inversed operations
80+
* @returns {BatchedOperation<InvertedOperationType<OperationType>>} new batch with inversed operations
8281
*/
8382
public inverse(): BatchedOperation<InvertedOperationType<T>> {
83+
const lastOp = this.operations[this.operations.length - 1];
84+
8485
/**
8586
* Every batch should have at least one operation
8687
*/
87-
const newBatchedOperation = new BatchedOperation<InvertedOperationType<T> | OperationType.Neutral>(this.operations.pop()!.inverse())
88+
const newBatchedOperation = new BatchedOperation<InvertedOperationType<T>>(lastOp.inverse());
8889

89-
this.operations.toReversed().map(op => newBatchedOperation.add(op.inverse()));
90+
this.operations.toReversed().slice(1).map(op => newBatchedOperation.add(op.inverse()));
9091

9192
return newBatchedOperation as BatchedOperation<InvertedOperationType<T>>;
9293
}
@@ -95,14 +96,14 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
9596
* Method that transforms all of the operations in the batch against another operation
9697
*
9798
* @param againstOp - operation to transform against
98-
* @returns new batch with transformed operations
99+
* @returns {BatchedOperation} new batch with transformed operations
99100
*/
100101
public transform<K extends OperationType>(againstOp: Operation<K>): BatchedOperation<T | OperationType.Neutral> {
101-
const transformedOp = this.operations.shift()!.transform(againstOp);
102+
const transformedOp = this.operations[0].transform(againstOp);
102103

103104
const newBatchedOperation = new BatchedOperation(transformedOp);
104105

105-
this.operations.map(op => newBatchedOperation.add(op.transform(againstOp)));
106+
this.operations.slice(1).map(op => newBatchedOperation.add(op.transform(againstOp)));
106107

107108
return newBatchedOperation;
108109
}
@@ -114,7 +115,14 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
114115
*
115116
* @param op - operation to check
116117
*/
117-
canAdd(op: Operation): boolean {
118+
public canAdd(op: Operation): boolean {
119+
/**
120+
* Can't add to batch insertion or deletion of several characters
121+
*/
122+
if (typeof op.data.payload === 'string' && op.data.payload?.length > 1) {
123+
return false;
124+
}
125+
118126
const lastOp = this.operations[this.operations.length - 1];
119127

120128
if (lastOp === undefined) {

0 commit comments

Comments
 (0)