Skip to content

Commit b3d2fef

Browse files
Егор КоноваловЕгор Коновалов
authored andcommitted
imp(): tests improved, lint fix
1 parent 922534f commit b3d2fef

11 files changed

Lines changed: 416 additions & 17 deletions

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,5 +159,14 @@ describe('Batch', () => {
159159

160160
expect(batch.canAdd(op2)).toBe(false);
161161
});
162+
163+
it('should return false when payload is a multi-character string', () => {
164+
const op1 = new Operation(OperationType.Insert, templateIndex, { payload: 'a' }, userId);
165+
const op2 = new Operation(OperationType.Insert, createIndexByRange([1, 1]), { payload: 'bc' }, userId);
166+
167+
const batch = new BatchedOperation(op1);
168+
169+
expect(batch.canAdd(op2)).toBe(false);
170+
});
162171
});
163172
});

packages/collaboration-manager/src/BatchedOperation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
5656
batch.add(Operation.from(op));
5757
});
5858

59-
return batch as BatchedOperation<T>;
59+
return batch;
6060
} else {
6161
const batch = new BatchedOperation<T>(Operation.from(opBatchOrJSON));
6262

@@ -90,7 +90,7 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
9090
this.operations.toReversed().slice(1)
9191
.map(op => newBatchedOperation.add(op.inverse()));
9292

93-
return newBatchedOperation as BatchedOperation<InvertedOperationType<T>>;
93+
return newBatchedOperation;
9494
}
9595

9696
/**
@@ -130,6 +130,9 @@ export class BatchedOperation<T extends OperationType = OperationType> extends O
130130
return true;
131131
}
132132

133+
/**
134+
* @todo - implement other index types
135+
*/
133136
if (!op.index.isTextIndex || !lastOp.index.isTextIndex) {
134137
return false;
135138
}

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

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { createDataKey, IndexBuilder } from '@editorjs/model';
33
import { EditorJSModel } from '@editorjs/model';
44
import type { CoreConfig } from '@editorjs/sdk';
55
import { beforeAll, jest } from '@jest/globals';
6+
import { BatchedOperation } from './BatchedOperation.js';
67
import { CollaborationManager } from './CollaborationManager.js';
78
import { Operation, OperationType } from './Operation.js';
9+
import { UndoRedoManager } from './UndoRedoManager.js';
810

911
const userId = 'user';
1012
const documentId = 'document';
@@ -228,6 +230,82 @@ describe('CollaborationManager', () => {
228230
});
229231
});
230232

233+
it('should not change the model when applying Neutral operation', () => {
234+
const model = new EditorJSModel(userId, { identifier: documentId });
235+
236+
model.initializeDocument({
237+
blocks: [ {
238+
name: 'paragraph',
239+
data: {
240+
text: {
241+
value: 'hello',
242+
$t: 't',
243+
},
244+
},
245+
} ],
246+
});
247+
const collaborationManager = new CollaborationManager(config as Required<CoreConfig>, model);
248+
const index = new IndexBuilder().addBlockIndex(0)
249+
.addDataKey(createDataKey('text'))
250+
.addTextRange([0, 5])
251+
.build();
252+
const operation = new Operation(OperationType.Neutral, index, {
253+
payload: [],
254+
}, userId);
255+
256+
const before = model.serialized;
257+
258+
collaborationManager.applyOperation(operation);
259+
260+
expect(model.serialized).toStrictEqual(before);
261+
});
262+
263+
it('should apply every operation when applying BatchedOperation', () => {
264+
const model = new EditorJSModel(userId, { identifier: documentId });
265+
266+
model.initializeDocument({
267+
blocks: [ {
268+
name: 'paragraph',
269+
data: {
270+
text: {
271+
value: '',
272+
$t: 't',
273+
},
274+
},
275+
} ],
276+
});
277+
const collaborationManager = new CollaborationManager(config as Required<CoreConfig>, model);
278+
const op1 = new Operation(OperationType.Insert, new IndexBuilder().addBlockIndex(0)
279+
.addDataKey(createDataKey('text'))
280+
.addTextRange([0, 0])
281+
.build(), { payload: 'a' }, userId);
282+
const op2 = new Operation(OperationType.Insert, new IndexBuilder().addBlockIndex(0)
283+
.addDataKey(createDataKey('text'))
284+
.addTextRange([1, 1])
285+
.build(), { payload: 'b' }, userId);
286+
const batch = new BatchedOperation(op1);
287+
288+
batch.add(op2);
289+
290+
collaborationManager.applyOperation(batch);
291+
292+
expect(model.serialized).toStrictEqual({
293+
identifier: documentId,
294+
blocks: [ {
295+
name: 'paragraph',
296+
tunes: {},
297+
data: {
298+
text: {
299+
$t: 't',
300+
value: 'ab',
301+
fragments: [],
302+
},
303+
},
304+
} ],
305+
properties: {},
306+
});
307+
});
308+
231309
it('should unformat text on apply Modify Operation', () => {
232310
const model = new EditorJSModel(userId, { identifier: documentId });
233311

@@ -946,6 +1024,38 @@ describe('CollaborationManager', () => {
9461024
});
9471025
});
9481026

1027+
describe('debounce', () => {
1028+
it('should move the open batch to the undo stack after the debounce delay', () => {
1029+
const putSpy = jest.spyOn(UndoRedoManager.prototype, 'put');
1030+
1031+
const model = new EditorJSModel(userId, { identifier: documentId });
1032+
1033+
model.initializeDocument({
1034+
blocks: [ {
1035+
name: 'paragraph',
1036+
data: {
1037+
text: {
1038+
value: '',
1039+
$t: 't',
1040+
},
1041+
},
1042+
} ],
1043+
});
1044+
void new CollaborationManager(config as Required<CoreConfig>, model);
1045+
1046+
model.insertText(userId, 0, createDataKey('text'), 'a', 0);
1047+
1048+
expect(putSpy).not.toHaveBeenCalled();
1049+
1050+
jest.advanceTimersByTime(500);
1051+
1052+
expect(putSpy).toHaveBeenCalledTimes(1);
1053+
expect(putSpy.mock.calls[0][0]).toBeInstanceOf(BatchedOperation);
1054+
1055+
putSpy.mockRestore();
1056+
});
1057+
});
1058+
9491059
describe('remote operations', () => {
9501060
it('should transform current batch when remote operation arrives', () => {
9511061
const model = new EditorJSModel(userId, { identifier: documentId });
@@ -1006,6 +1116,47 @@ describe('CollaborationManager', () => {
10061116
});
10071117
});
10081118

1119+
it('should transform undo stack when remote user edits arrive through model events', () => {
1120+
const model = new EditorJSModel(userId, { identifier: documentId });
1121+
1122+
model.initializeDocument({
1123+
blocks: [ {
1124+
name: 'paragraph',
1125+
data: {
1126+
text: {
1127+
value: '',
1128+
$t: 't',
1129+
},
1130+
},
1131+
} ],
1132+
});
1133+
1134+
const collaborationManager = new CollaborationManager(config as Required<CoreConfig>, model);
1135+
1136+
model.insertText(userId, 0, createDataKey('text'), 'world', 0);
1137+
jest.advanceTimersByTime(500);
1138+
1139+
model.insertText('remote-user', 0, createDataKey('text'), 'hello', 0);
1140+
1141+
collaborationManager.undo();
1142+
1143+
expect(model.serialized).toStrictEqual({
1144+
identifier: documentId,
1145+
blocks: [ {
1146+
name: 'paragraph',
1147+
tunes: {},
1148+
data: {
1149+
text: {
1150+
$t: 't',
1151+
value: 'hello',
1152+
fragments: [],
1153+
},
1154+
},
1155+
} ],
1156+
properties: {},
1157+
});
1158+
});
1159+
10091160
it('should clear current batch if not transformable with remote operation', () => {
10101161
const model = new EditorJSModel(userId, { identifier: documentId });
10111162

packages/collaboration-manager/src/CollaborationManager.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ export class CollaborationManager {
3131
#undoRedoManager: UndoRedoManager;
3232

3333
/**
34-
* Flag to control whether events should be handled to avoid putting operations to the stack on undo/redo. Used for preventing operations infinity loop on undo/redo
34+
* Flag to control whether events should be handled to avoid putting operations to the stack on undo/redo.
35+
* Used for preventing operations infinity loop on undo/redo
3536
*/
3637
#shouldHandleEvents = true;
3738

@@ -98,7 +99,7 @@ export class CollaborationManager {
9899
* Undo last operation in the local stack
99100
*/
100101
public undo(): void {
101-
this.#moveBatchToUndo();
102+
this.#putBatchToUndo();
102103

103104
const operation = this.#undoRedoManager.undo();
104105

@@ -119,7 +120,7 @@ export class CollaborationManager {
119120
* Redo last undone operation in the local stack
120121
*/
121122
public redo(): void {
122-
this.#moveBatchToUndo();
123+
this.#putBatchToUndo();
123124

124125
const operation = this.#undoRedoManager.redo();
125126

@@ -265,7 +266,7 @@ export class CollaborationManager {
265266
* If current operation could not be added to the batch, then terminate current batch and create a new one with current operation
266267
*/
267268
if (!this.#currentBatch.canAdd(operation)) {
268-
this.#moveBatchToUndo();
269+
this.#putBatchToUndo();
269270

270271
this.#currentBatch = new BatchedOperation(operation);
271272
this.#debounce();
@@ -280,7 +281,7 @@ export class CollaborationManager {
280281
/**
281282
* Puts current batch to the undo stack and clears the batch
282283
*/
283-
#moveBatchToUndo(): void {
284+
#putBatchToUndo(): void {
284285
if (this.#currentBatch !== null) {
285286
this.#undoRedoManager.put(this.#currentBatch);
286287

@@ -289,13 +290,13 @@ export class CollaborationManager {
289290
}
290291

291292
/**
292-
* Debouneces timer of #moveBatchToUndo method
293+
* Debouneces timer of #putBatchToUndo method
293294
*/
294295
#debounce(): void {
295296
clearTimeout(this.#debounceTimer);
296297

297298
this.#debounceTimer = setTimeout(() => {
298-
this.#moveBatchToUndo();
299+
this.#putBatchToUndo();
299300
}, DEBOUCE_TIMEOUT);
300301
}
301302
}

0 commit comments

Comments
 (0)