/** * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars import deltaTransform from '../../../../src/model/delta/transform'; const transformDeltaSets = deltaTransform.transformDeltaSets; import Document from '../../../../src/model/document'; import Element from '../../../../src/model/element'; import Text from '../../../../src/model/text'; import Position from '../../../../src/model/position'; import Delta from '../../../../src/model/delta/delta'; import InsertDelta from '../../../../src/model/delta/insertdelta'; import RemoveDelta from '../../../../src/model/delta/removedelta'; import MoveDelta from '../../../../src/model/delta/movedelta'; import SplitDelta from '../../../../src/model/delta/splitdelta'; import NoOperation from '../../../../src/model/operation/nooperation'; import MoveOperation from '../../../../src/model/operation/moveoperation'; import RemoveOperation from '../../../../src/model/operation/removeoperation'; import ReinsertOperation from '../../../../src/model/operation/reinsertoperation'; import InsertOperation from '../../../../src/model/operation/insertoperation'; import { expectDelta, getInsertDelta, getSplitDelta, getRemoveDelta, getMoveDelta, getMergeDelta } from '../../../../tests/model/delta/transform/_utils/utils'; describe( 'transform', () => { let doc, root, baseVersion; beforeEach( () => { doc = new Document(); root = doc.createRoot(); root.appendChildren( new Element( 'p', null, new Text( 'foobar' ) ) ); baseVersion = doc.version; } ); describe( 'transformDeltaSets', () => { it( 'should use deltaTransform.transform', () => { sinon.spy( deltaTransform, 'transform' ); const insertDelta = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion ); const removeDelta = getRemoveDelta( new Position( root, [ 0, 0 ] ), 2, baseVersion ); transformDeltaSets( [ insertDelta ], [ removeDelta ] ); expect( deltaTransform.transform.called ).to.be.true; deltaTransform.transform.restore(); } ); it( 'should transform two arrays of deltas', () => { const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion ); const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 ); const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion ); const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 ); const { deltasA, deltasB } = transformDeltaSets( [ splitDelta, insertDeltaX ], [ removeDelta, insertDeltaY ] ); expect( deltasA.length ).to.equal( 3 ); expect( deltasB.length ).to.equal( 3 ); expectDelta( deltasA[ 0 ], { type: SplitDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 1 ] ), baseVersion: 2 }, { type: MoveOperation, sourcePosition: new Position( root, [ 0, 2 ] ), howMany: 5, targetPosition: new Position( root, [ 1, 0 ] ), baseVersion: 3 } ] } ); expectDelta( deltasA[ 1 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 0, 2 ] ), baseVersion: 4 } ] } ); expectDelta( deltasA[ 2 ], { type: Delta, operations: [ { type: NoOperation, baseVersion: 5 } ] } ); expectDelta( deltasB[ 0 ], { type: RemoveDelta, operations: [ { type: RemoveOperation, sourcePosition: new Position( root, [ 1, 0 ] ), howMany: 1, baseVersion: 3 } ] } ); expectDelta( deltasB[ 1 ], { type: RemoveDelta, operations: [ { type: RemoveOperation, sourcePosition: new Position( root, [ 0, 2 ] ), howMany: 1, baseVersion: 4 } ] } ); expectDelta( deltasB[ 2 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 1, 2 ] ), baseVersion: 5 } ] } ); } ); it( 'should transform two arrays of deltas - reverse', () => { const splitDelta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, baseVersion ); const insertDeltaX = getInsertDelta( new Position( root, [ 0, 3 ] ), new Text( 'xxx' ), baseVersion + 2 ); const removeDelta = getRemoveDelta( new Position( root, [ 0, 2 ] ), 2, baseVersion ); const insertDeltaY = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion + 1 ); const { deltasA, deltasB } = transformDeltaSets( [ removeDelta, insertDeltaY ], [ splitDelta, insertDeltaX ] ); expect( deltasA.length ).to.equal( 3 ); expect( deltasB.length ).to.equal( 3 ); expectDelta( deltasA[ 0 ], { type: RemoveDelta, operations: [ { type: RemoveOperation, sourcePosition: new Position( root, [ 1, 0 ] ), howMany: 1, baseVersion: 3 } ] } ); expectDelta( deltasA[ 1 ], { type: RemoveDelta, operations: [ { type: RemoveOperation, sourcePosition: new Position( root, [ 0, 2 ] ), howMany: 1, baseVersion: 4 } ] } ); expectDelta( deltasA[ 2 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 1, 2 ] ), baseVersion: 5 } ] } ); expectDelta( deltasB[ 0 ], { type: SplitDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 1 ] ), baseVersion: 2 }, { type: MoveOperation, sourcePosition: new Position( root, [ 0, 2 ] ), howMany: 5, targetPosition: new Position( root, [ 1, 0 ] ), baseVersion: 3 } ] } ); expectDelta( deltasB[ 1 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 0, 2 ] ), baseVersion: 4 } ] } ); expectDelta( deltasB[ 2 ], { type: Delta, operations: [ { type: NoOperation, baseVersion: 5 } ] } ); } ); it( 'should not modify original deltas or arrays', () => { const insertDeltaA = getInsertDelta( new Position( root, [ 0, 0 ] ), new Text( 'x' ), baseVersion ); const insertDeltaB = getInsertDelta( new Position( root, [ 1, 0 ] ), new Text( 'y' ), baseVersion ); const originalDeltasA = [ insertDeltaA ]; const originalDeltasB = [ insertDeltaB ]; const { deltasA, deltasB } = transformDeltaSets( originalDeltasA, originalDeltasB ); expect( deltasA ).to.not.equal( originalDeltasA ); expect( deltasB ).to.not.equal( originalDeltasB ); expect( deltasA[ 0 ] ).to.not.equal( originalDeltasA[ 0 ] ); expect( deltasB[ 0 ] ).to.not.equal( originalDeltasB[ 0 ] ); } ); describe( 'context', () => { it( 'first set is important', () => { const insertDeltaA = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'xxx' ), baseVersion ); const insertDeltaB = getInsertDelta( new Position( root, [ 0, 4 ] ), new Text( 'yyy' ), baseVersion ); const { deltasA, deltasB } = transformDeltaSets( [ insertDeltaA ], [ insertDeltaB ] ); expectDelta( deltasA[ 0 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 0, 4 ] ), baseVersion: 1 } ] } ); expectDelta( deltasB[ 0 ], { type: InsertDelta, operations: [ { type: InsertOperation, position: new Position( root, [ 0, 7 ] ), baseVersion: 1 } ] } ); } ); it( 'remove delta is more important if additional context is not used', () => { const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion ); const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion ); const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ] ); expectDelta( deltasA[ 0 ], { type: Delta, operations: [ { type: NoOperation, baseVersion: 1 } ] } ); expectDelta( deltasB[ 0 ], { type: RemoveDelta, operations: [ { type: RemoveOperation, sourcePosition: new Position( root, [ 1, 0 ] ), howMany: 3, targetPosition: new Position( doc.graveyard, [ 0 ] ), baseVersion: 1 } ] } ); } ); it( 'remove delta may be less important if additional context is used and the delta was undone', () => { const moveDelta = getMoveDelta( new Position( root, [ 0, 4 ] ), 3, new Position( root, [ 1, 0 ] ), baseVersion ); const removeDelta = getRemoveDelta( new Position( root, [ 0, 4 ] ), 3, baseVersion ); // "Fake" delta undoing. doc.history.setDeltaAsUndone( removeDelta, new Delta() ); const { deltasA, deltasB } = transformDeltaSets( [ moveDelta ], [ removeDelta ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: MoveOperation, sourcePosition: new Position( doc.graveyard, [ 0 ] ), howMany: 3, targetPosition: new Position( root, [ 1, 0 ] ), baseVersion: 1 } ] } ); expectDelta( deltasB[ 0 ], { type: Delta, operations: [ { type: NoOperation, baseVersion: 1 } ] } ); } ); it( 'should keep correct order of nodes if additional context is used (insert before)', () => { // Assume document state: 'a^bcd'. User presses 'delete' key twice. const removeDelta1 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion ); // 'acd'. const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'. const reinsertDelta2 = removeDelta2.getReversed(); // 'acd'. doc.history.addDelta( removeDelta1 ); doc.history.addDelta( removeDelta2 ); doc.history.addDelta( reinsertDelta2 ); doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 ); // This should insert 'b' at offset 1. // However this delta will be transformed by `reinsertDelta2` which also inserts a node at offset 1. // This leads to a conflict and history needs to be check how deltas relate to each other. // Compare this scenario to the scenario below. const reinsertDelta1 = removeDelta1.getReversed(); const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: ReinsertOperation, targetPosition: new Position( root, [ 1 ] ), howMany: 1, baseVersion: 3 } ] } ); } ); it( 'should keep correct order of nodes if additional context is used (insert after)', () => { // Assume document state: 'abc^d'. User presses 'backspace' key twice. const removeDelta1 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion ); // 'abd'. const removeDelta2 = getRemoveDelta( new Position( root, [ 1 ] ), 1, baseVersion + 1 ); // 'ad'. const reinsertDelta2 = removeDelta2.getReversed(); // 'abd'. doc.history.addDelta( removeDelta1 ); doc.history.addDelta( removeDelta2 ); doc.history.addDelta( reinsertDelta2 ); doc.history.setDeltaAsUndone( removeDelta2, reinsertDelta2 ); // This should insert 'c' at offset 2. // However this delta will be first transformed by `removeDelta2`. After that transformation // the delta will try to insert 'c' at offset 1. Then, it will be transformed by `reinsertDelta2`, // which also tries to insert node at offset 1. // This leads to a conflict and history needs to be check how deltas relate to each other. // Compare this scenario to the scenario below. // In both cases transformed delta was inserting something at the same position. However, in // both cases the expected outcome is different. const reinsertDelta1 = removeDelta1.getReversed(); const { deltasA } = transformDeltaSets( [ reinsertDelta1 ], [ removeDelta2, reinsertDelta2 ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: ReinsertOperation, targetPosition: new Position( root, [ 2 ] ), howMany: 1, baseVersion: 3 } ] } ); } ); it( 'should not throw if delta transformed by undoing delta was not transformed by undone delta', () => { // This scenario may happen when deltas are undone in different order than usual LIFO order. const removeDelta1 = getRemoveDelta( new Position( root, [ 0 ] ), 1, baseVersion ); const removeDelta2 = getRemoveDelta( new Position( root, [ 2 ] ), 1, baseVersion + 1 ); const reinsertDelta1 = removeDelta1.getReversed(); reinsertDelta1.baseVersion = 2; doc.history.addDelta( removeDelta1 ); doc.history.addDelta( removeDelta2 ); doc.history.addDelta( reinsertDelta1 ); doc.history.setDeltaAsUndone( removeDelta1, reinsertDelta1 ); const reinsertDelta2 = removeDelta2.getReversed(); const { deltasA } = transformDeltaSets( [ reinsertDelta2 ], [ reinsertDelta1 ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: MoveOperation, sourcePosition: new Position( root, [ 0 ] ), targetPosition: new Position( root, [ 3 ] ), howMany: 1, baseVersion: 3 } ] } ); } ); it( 'should use stickiness if additional context is not used', () => { const moveDeltaA = getMoveDelta( new Position( root, [ 0, 0 ] ), 1, new Position( root, [ 1, 0 ] ), baseVersion ); const moveDeltaB = getMoveDelta( new Position( root, [ 1, 0 ] ), 3, new Position( root, [ 2, 2 ] ), baseVersion ); moveDeltaB.operations[ 0 ].isSticky = true; const { deltasA } = transformDeltaSets( [ moveDeltaA ], [ moveDeltaB ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: MoveOperation, targetPosition: new Position( root, [ 2, 2 ] ), howMany: 1, baseVersion: 1 } ] } ); } ); it( 'should discard stickiness if additional context is used and delta with sticky move operation has been undone', () => { // Assume following data model: abcd // Then we remove 'b': acd const removeDelta = getRemoveDelta( new Position( root, [ 0, 1 ] ), 1, baseVersion ); // Then we merge paragraphs: acd // Keep in mind that merging has sticky move operation ('cd' is sticky-moved to the first paragraph). const mergeDelta = getMergeDelta( new Position( root, [ 1 ] ), 2, 1, baseVersion + 1 ); // Then we reverse merging: acd // Keep in mind that now we have two deltas with sticky moves in a history. const splitDelta = mergeDelta.getReversed(); doc.history.addDelta( removeDelta ); doc.history.addDelta( mergeDelta ); doc.history.addDelta( splitDelta ); doc.history.setDeltaAsUndone( mergeDelta, splitDelta ); // This delta should insert 'b' at position [ 0, 1 ]. // However, without context data, after transforming `reinsertDelta` by `splitDelta`, // 'b' would "go away" together with 'cd'. const reinsertDelta = removeDelta.getReversed(); const { deltasA } = transformDeltaSets( [ reinsertDelta ], [ mergeDelta, splitDelta ], doc ); expectDelta( deltasA[ 0 ], { type: MoveDelta, operations: [ { type: ReinsertOperation, targetPosition: new Position( root, [ 0, 1 ] ), howMany: 1, baseVersion: 5 } ] } ); } ); } ); } ); } );