Przeglądaj źródła

Fixed: `SplitDelta` special transformation cases threw when second operation become `NoOperation`.

Szymon Cofalik 8 lat temu
rodzic
commit
75c3d09bde

+ 56 - 8
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -50,6 +50,11 @@ addTransformationCase( AttributeDelta, WeakInsertDelta, ( a, b, context ) => {
 
 
 // Add special case for AttributeDelta x SplitDelta transformation.
 // Add special case for AttributeDelta x SplitDelta transformation.
 addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 	const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
 
 
 	const deltas = defaultTransform( a, b, context );
 	const deltas = defaultTransform( a, b, context );
@@ -173,6 +178,11 @@ addTransformationCase( MergeDelta, MoveDelta, ( a, b, context ) => {
 
 
 // Add special case for SplitDelta x SplitDelta transformation.
 // Add special case for SplitDelta x SplitDelta transformation.
 addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position || !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	const pathA = a.position.getParentPath();
 	const pathA = a.position.getParentPath();
 	const pathB = b.position.getParentPath();
 	const pathB = b.position.getParentPath();
 
 
@@ -202,6 +212,11 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 
 
 // Add special case for SplitDelta x UnwrapDelta transformation.
 // Add special case for SplitDelta x UnwrapDelta transformation.
 addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
 	// If incoming split delta tries to split a node that just got unwrapped, there is actually nothing to split,
 	// so we discard that delta.
 	// so we discard that delta.
 	if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
 	if ( a.position.root == b.position.root && compareArrays( b.position.path, a.position.getParentPath() ) === 'same' ) {
@@ -213,6 +228,11 @@ addTransformationCase( SplitDelta, UnwrapDelta, ( a, b, context ) => {
 
 
 // Add special case for SplitDelta x WrapDelta transformation.
 // Add special case for SplitDelta x WrapDelta transformation.
 addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
 	// If split is applied at the position between wrapped nodes, we cancel the split as it's results may be unexpected and
 	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
 	// very weird. Even if we do some "magic" we don't know what really are users' expectations.
 
 
@@ -264,7 +284,12 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 } );
 } );
 
 
 // Add special case for SplitDelta x WrapDelta transformation.
 // Add special case for SplitDelta x WrapDelta transformation.
-addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
+addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !a.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	a = a.clone();
 	a = a.clone();
 
 
 	const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
 	const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
@@ -290,6 +315,11 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b ) => {
 
 
 // Add special case for UnwrapDelta x SplitDelta transformation.
 // Add special case for UnwrapDelta x SplitDelta transformation.
 addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( UnwrapDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
 	// If incoming unwrap delta tries to unwrap node that got split we should unwrap the original node and the split copy.
 	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
 	// This can be achieved either by reverting split and applying unwrap to singular node, or creating additional unwrap delta.
 	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
 	if ( a.position.root == b.position.root && compareArrays( a.position.path, b.position.getParentPath() ) === 'same' ) {
@@ -316,9 +346,13 @@ addTransformationCase( WeakInsertDelta, AttributeDelta, ( a, b ) => {
 
 
 // Add special case for WrapDelta x SplitDelta transformation.
 // Add special case for WrapDelta x SplitDelta transformation.
 addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
+	// Do not apply special transformation case if `SplitDelta` has `NoOperation` as the second operation.
+	if ( !b.position ) {
+		return defaultTransform( a, b, context );
+	}
+
 	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
 	// If incoming wrap delta tries to wrap range that contains split position, we have to cancel the split and apply
 	// the wrap. Since split was already applied, we have to revert it.
 	// the wrap. Since split was already applied, we have to revert it.
-
 	const sameRoot = a.range.start.root == b.position.root;
 	const sameRoot = a.range.start.root == b.position.root;
 	const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
 	const operateInSameParent = sameRoot && compareArrays( a.range.start.getParentPath(), b.position.getParentPath() ) === 'same';
 	const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
 	const splitInsideWrapRange = a.range.start.offset < b.position.offset && a.range.end.offset >= b.position.offset;
@@ -349,15 +383,22 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 // Add special case for RenameDelta x SplitDelta transformation.
 // Add special case for RenameDelta x SplitDelta transformation.
 addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 	const undoMode = context.aWasUndone || context.bWasUndone;
 	const undoMode = context.aWasUndone || context.bWasUndone;
-	const posBeforeSplitParent = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
+
+	// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
+	// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
+	let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
+
+	if ( insertPosition ) {
+		insertPosition = insertPosition.getShiftedBy( -1 );
+	}
 
 
 	const deltas = defaultTransform( a, b, context );
 	const deltas = defaultTransform( a, b, context );
 
 
-	if ( !undoMode && a.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
+	if ( insertPosition && !undoMode && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
 		// If a node that has been split has it's name changed, we should also change name of
 		// If a node that has been split has it's name changed, we should also change name of
 		// the node created during splitting.
 		// the node created during splitting.
 		const additionalRenameDelta = a.clone();
 		const additionalRenameDelta = a.clone();
-		additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
+		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
 
 
 		deltas.push( additionalRenameDelta );
 		deltas.push( additionalRenameDelta );
 	}
 	}
@@ -368,12 +409,19 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
 // Add special case for SplitDelta x RenameDelta transformation.
 // Add special case for SplitDelta x RenameDelta transformation.
 addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
 addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
 	const undoMode = context.aWasUndone || context.bWasUndone;
 	const undoMode = context.aWasUndone || context.bWasUndone;
-	const posBeforeSplitParent = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
+
+	// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
+	// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
+	let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
+
+	if ( insertPosition ) {
+		insertPosition = insertPosition.getShiftedBy( -1 );
+	}
 
 
 	// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
 	// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
-	if ( !undoMode && b.operations[ 0 ].position.isEqual( posBeforeSplitParent ) ) {
+	if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
 		const additionalRenameDelta = b.clone();
 		const additionalRenameDelta = b.clone();
-		additionalRenameDelta.operations[ 0 ].position = posBeforeSplitParent.getShiftedBy( 1 );
+		additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
 
 
 		// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
 		// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
 		// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.
 		// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.

+ 27 - 0
packages/ckeditor5-engine/tests/model/delta/transform/attributedelta.js

@@ -256,6 +256,33 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
+				const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );
+				const splitDelta = getSplitDelta( new Position( root, [ 0, 2 ] ), new Element( 'paragraph' ), 3, 0 );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( attrDelta, splitDelta, context );
+
+				baseVersion = splitDelta.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: AttributeDelta,
+					operations: [
+						{
+							type: AttributeOperation,
+							range: new Range( new Position( root, [ 2 ] ), new Position( root, [ 3, 3 ] ) ),
+							key: 'foo',
+							oldValue: null,
+							newValue: 'bar',
+							baseVersion
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'AttributeDelta', () => {
 		describe( 'AttributeDelta', () => {

+ 33 - 0
packages/ckeditor5-engine/tests/model/delta/transform/renamedelta.js

@@ -11,8 +11,10 @@ import Element from '../../../../src/model/element';
 import Position from '../../../../src/model/position';
 import Position from '../../../../src/model/position';
 
 
 import RenameDelta from '../../../../src/model/delta/renamedelta';
 import RenameDelta from '../../../../src/model/delta/renamedelta';
+import SplitDelta from '../../../../src/model/delta/splitdelta';
 import Delta from '../../../../src/model/delta/delta';
 import Delta from '../../../../src/model/delta/delta';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
 import RenameOperation from '../../../../src/model/operation/renameoperation';
+import MoveOperation from '../../../../src/model/operation/moveoperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import {
 import {
@@ -106,6 +108,37 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
+				const noOpSplitDelta = new SplitDelta();
+				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
+				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
+
+				const renameDelta = new RenameDelta();
+				renameDelta.addOperation( new RenameOperation(
+					new Position( root, [ 1 ] ),
+					'p',
+					'li',
+					baseVersion
+				) );
+
+				const transformed = transform( renameDelta, noOpSplitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: RenameDelta,
+					operations: [
+						{
+							type: RenameOperation,
+							position: new Position( root, [ 1 ] ),
+							oldName: 'p',
+							newName: 'li',
+							baseVersion: 2
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RenameDelta', () => {
 		describe( 'RenameDelta', () => {

+ 175 - 0
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -322,6 +322,61 @@ describe( 'transform', () => {
 				// DIV and P elements are correctly split.
 				// DIV and P elements are correctly split.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVPabcPPfoobarxyzPDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVPabcPPfoobarxyzPDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if transformed split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
+
+				const transformed = transform( splitDelta, splitDeltaB, context );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
+
+			it( 'should use default algorithm and not throw if split delta to transform by has NoOperation', () => {
+				const splitDeltaB = getSplitDelta( new Position( root, [ 3, 3, 3, 1 ] ), new Element( 'p' ), 11, baseVersion );
+				splitDeltaB.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( splitDelta, splitDeltaB, context );
+
+				baseVersion = splitDeltaB.operations.length;
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
+							howMany: 9,
+							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'UnwrapDelta', () => {
 		describe( 'UnwrapDelta', () => {
@@ -389,6 +444,33 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are correctly applied.
 				// UnwrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXaXXXXXXabcdXPabcPPfoobarxyzPDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXaXXXXXXabcdXPabcPPfoobarxyzPDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const unwrapDelta = getUnwrapDelta( new Position( root, [ 3, 3 ] ), 4, baseVersion );
+
+				const transformed = transform( splitDelta, unwrapDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = unwrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 7 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'WrapDelta', () => {
 		describe( 'WrapDelta', () => {
@@ -500,6 +582,35 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				// WrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
+				const wrapElement = new Element( 'E' );
+				const wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
+
+				const transformed = transform( splitDelta, wrapDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 3 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'AttributeDelta', () => {
 		describe( 'AttributeDelta', () => {
@@ -619,6 +730,35 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const attributeDelta = new AttributeDelta();
+				attributeDelta.addOperation( new AttributeOperation(
+					Range.createFromParentsAndOffsets( root, 0, root, 4 ), 'key', 'oldValue', 'newValue', baseVersion
+				) );
+
+				const transformed = transform( splitDelta, attributeDelta, context );
+
+				baseVersion = attributeDelta.operations.length;
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion
+						},
+						{
+							type: NoOperation,
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RenameDelta', () => {
 		describe( 'RenameDelta', () => {
@@ -737,6 +877,41 @@ describe( 'transform', () => {
 					]
 					]
 				} );
 				} );
 			} );
 			} );
+
+			it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
+				const noOpSplitDelta = new SplitDelta();
+				noOpSplitDelta.addOperation( new NoOperation( 0 ) );
+				noOpSplitDelta.addOperation( new MoveOperation( new Position( root, [ 1, 2 ] ), 3, new Position( root, [ 2, 0 ] ), 1 ) );
+
+				const renameDelta = new RenameDelta();
+				renameDelta.addOperation( new RenameOperation(
+					new Position( root, [ 1 ] ),
+					'p',
+					'li',
+					baseVersion
+				) );
+
+				const transformed = transform( noOpSplitDelta, renameDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: NoOperation,
+							baseVersion: 1
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 1, 2 ] ),
+							howMany: 3,
+							targetPosition: new Position( root, [ 2, 0 ] ),
+							baseVersion: 2
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 
 
 		describe( 'RemoveDelta', () => {
 		describe( 'RemoveDelta', () => {

+ 34 - 0
packages/ckeditor5-engine/tests/model/delta/transform/unwrapdelta.js

@@ -13,6 +13,8 @@ import Position from '../../../../src/model/position';
 import Range from '../../../../src/model/range';
 import Range from '../../../../src/model/range';
 
 
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import MoveOperation from '../../../../src/model/operation/moveoperation';
+import RemoveOperation from '../../../../src/model/operation/removeoperation';
+import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
 import UnwrapDelta from '../../../../src/model/delta/unwrapdelta';
@@ -146,6 +148,38 @@ describe( 'transform', () => {
 				// UnwrapDelta and SplitDelta are applied.
 				// UnwrapDelta and SplitDelta are applied.
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVabcfoobarxyzDIV' );
 				expect( nodesAndText ).to.equal( 'DIVXXXXXabcdXDIVDIVabcfoobarxyzDIV' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
+				const splitDelta = getSplitDelta( splitPosition, new Element( 'div' ), 1, baseVersion );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( unwrapDelta, splitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = splitDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: UnwrapDelta,
+					operations: [
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 4, 0 ] ),
+							howMany: 12,
+							targetPosition: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion
+						},
+						{
+							type: RemoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 16 ] ),
+							howMany: 1,
+							targetPosition: new Position( gy, [ 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );

+ 31 - 0
packages/ckeditor5-engine/tests/model/delta/transform/wrapdelta.js

@@ -14,6 +14,7 @@ import Range from '../../../../src/model/range';
 
 
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import MoveOperation from '../../../../src/model/operation/moveoperation';
 import InsertOperation from '../../../../src/model/operation/insertoperation';
 import InsertOperation from '../../../../src/model/operation/insertoperation';
+import NoOperation from '../../../../src/model/operation/nooperation';
 
 
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import MergeDelta from '../../../../src/model/delta/mergedelta';
 import WrapDelta from '../../../../src/model/delta/wrapdelta';
 import WrapDelta from '../../../../src/model/delta/wrapdelta';
@@ -190,6 +191,36 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				// WrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
 			} );
 			} );
+
+			it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
+				const splitPosition = new Position( root, [ 3, 3, 2, 1 ] );
+				const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
+				splitDelta.operations[ 1 ] = new NoOperation( 1 );
+
+				const transformed = transform( wrapDelta, splitDelta, context );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: WrapDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4, 5 ] ),
+							baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 4, 1 ] ),
+							howMany: 4,
+							targetPosition: new Position( root, [ 3, 3, 4, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+			} );
 		} );
 		} );
 	} );
 	} );
 } );
 } );