Przeglądaj źródła

#396: Serialization/deserialization of deltas.

Maciej Gołaszewski 9 lat temu
rodzic
commit
38ac333526
23 zmienionych plików z 526 dodań i 3 usunięć
  1. 14 1
      packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js
  2. 96 0
      packages/ckeditor5-engine/src/treemodel/delta/delta.js
  3. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js
  4. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js
  5. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/movedelta.js
  6. 8 1
      packages/ckeditor5-engine/src/treemodel/delta/removedelta.js
  7. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/renamedelta.js
  8. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js
  9. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js
  10. 8 1
      packages/ckeditor5-engine/src/treemodel/delta/weakinsertdelta.js
  11. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js
  12. 1 0
      packages/ckeditor5-engine/src/treemodel/operation/operation.js
  13. 11 0
      packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js
  14. 283 0
      packages/ckeditor5-engine/tests/treemodel/delta/delta.js
  15. 18 0
      packages/ckeditor5-engine/tests/treemodel/delta/insertdelta.js
  16. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/mergedelta.js
  17. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/movedelta.js
  18. 7 0
      packages/ckeditor5-engine/tests/treemodel/delta/removedelta.js
  19. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/renamedelta.js
  20. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js
  21. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/unwrapdelta.js
  22. 7 0
      packages/ckeditor5-engine/tests/treemodel/delta/weakinsertdelta.js
  23. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/wrapdelta.js

+ 14 - 1
packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js

@@ -7,6 +7,7 @@
 
 import Delta from './delta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import AttributeOperation from '../operation/attributeoperation.js';
 import RootAttributeOperation from '../operation/rootattributeoperation.js';
 import Position from '../position.js';
@@ -69,6 +70,10 @@ export default class AttributeDelta extends Delta {
 		return AttributeDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.AttributeDelta';
+	}
+
 	static get _priority() {
 		return 20;
 	}
@@ -82,7 +87,11 @@ export default class AttributeDelta extends Delta {
  * @memberOf engine.treeModel.delta
  * @extends engine.treeModel.delta.Delta
  */
-export class RootAttributeDelta extends Delta {}
+export class RootAttributeDelta extends Delta {
+	static get className() {
+		return 'engine.treeModel.delta.RootAttributeDelta';
+	}
+}
 
 /**
  * Sets the value of the attribute of the node or on the range.
@@ -200,3 +209,7 @@ function changeRange( batch, doc, attributeKey, attributeValue, range ) {
 		doc.applyOperation( operation );
 	}
 }
+
+registerDeserializer( AttributeDelta.className, AttributeDelta );
+
+registerDeserializer( RootAttributeDelta.className, RootAttributeDelta );

+ 96 - 0
packages/ckeditor5-engine/src/treemodel/delta/delta.js

@@ -5,6 +5,19 @@
 
 'use strict';
 
+import clone from '../../../utils/lib/lodash/clone.js';
+
+import CKEditorError from '../../../utils/ckeditorerror.js';
+
+import AttributeOperation from '../operation/attributeoperation.js';
+import InsertOperation from '../operation/insertoperation.js';
+import MoveOperation from '../operation/moveoperation.js';
+import NoOperation from '../operation/nooperation.js';
+import Operation from '../operation/operation.js';
+import ReinsertOperation from '../operation/reinsertoperation.js';
+import RemoveOperation from '../operation/removeoperation.js';
+import RootAttributeOperation from '../operation/rootattributeoperation.js';
+
 /**
  * Base class for all deltas.
  *
@@ -117,6 +130,29 @@ export default class Delta {
 		return delta;
 	}
 
+	/**
+	 * Custom toJSON method to make deltas serializable.
+	 *
+	 * @returns {Object} Clone of this delta with added class name.
+	 */
+	toJSON() {
+		let json = clone( this );
+
+		json.__class = this.constructor.className;
+
+		return json;
+	}
+
+	/**
+	 * Delta class name. Used by {@link engine.treeModel.delta.Delta#toJSON} method for serialization and
+	 * {@link engine.treeModel.delta.Delta.fromJSON} during deserialization.
+	 *
+	 * @type {String}
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.Delta';
+	}
+
 	/**
 	 * Delta priority. Used in {@link engine.treeModel.delta.transform delta transformations}. Delta with the higher
 	 * priority will be treated as more important when resolving transformation conflicts. If deltas have same
@@ -128,4 +164,64 @@ export default class Delta {
 	static get _priority() {
 		return 0;
 	}
+
+	/**
+	 * Creates InsertDelta from deserialized object, ie. from parsed JSON string
+	 *
+	 * @param {Object} json
+	 * @param {engine.treeModel.Document} doc Document on which this delta will be applied.
+	 * @returns {engine.treeModel.delta.InsertDelta}
+	 */
+	static fromJSON( json, doc ) {
+		if ( !deserializers.has( json.__class ) ) {
+			/**
+			 * This delta has no defined deserializer.
+			 *
+			 * @error delta-fromjson-no-deserializer
+			 * @param {String} name
+			 */
+			throw new CKEditorError(
+				'delta-fromjson-no-deserializer: This delta has no defined deserializer',
+				{ name: json.__class }
+			);
+		}
+
+		let Constructor = deserializers.get( json.__class );
+
+		let delta = new Constructor();
+
+		if ( json.operations.length ) {
+			json.operations.forEach( ( operation ) => delta.addOperation( operations[ operation.__class ].fromJSON( operation, doc ) ) );
+		}
+
+		return delta;
+	}
+}
+
+const operations = {};
+operations[ AttributeOperation.className ] = AttributeOperation;
+operations[ InsertOperation.className ] = InsertOperation;
+operations[ MoveOperation.className ] = MoveOperation;
+operations[ NoOperation.className ] = NoOperation;
+operations[ Operation.className ] = Operation;
+operations[ ReinsertOperation.className ] = ReinsertOperation;
+operations[ RemoveOperation.className ] = RemoveOperation;
+operations[ RootAttributeOperation.className ] = RootAttributeOperation;
+
+const deserializers = new Map();
+
+export function registerDeserializer( className, constructor ) {
+	if ( deserializers.has( className ) ) {
+		/**
+		 * This delta name is already defined.
+		 *
+		 * @error delta-register-deserializer-defined
+		 * @param {String} name
+		 */
+		throw new CKEditorError(
+			'delta-register-deserializer-defined: This delta name is already defined.',
+			{ name: className } );
+	}
+
+	deserializers.set( className, constructor );
 }

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js

@@ -8,6 +8,7 @@
 import Delta from './delta.js';
 import RemoveDelta from './removedelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import InsertOperation from '../operation/insertoperation.js';
 
 /**
@@ -50,6 +51,10 @@ export default class InsertDelta extends Delta {
 		return RemoveDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.InsertDelta';
+	}
+
 	static get _priority() {
 		return 20;
 	}
@@ -73,3 +78,5 @@ register( 'insert', function( position, nodes ) {
 
 	return this;
 } );
+
+registerDeserializer( InsertDelta.className, InsertDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js

@@ -8,6 +8,7 @@
 import Delta from './delta.js';
 import SplitDelta from './splitdelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import Position from '../position.js';
 import Element from '../element.js';
 import RemoveOperation from '../operation/removeoperation.js';
@@ -47,6 +48,10 @@ export default class MergeDelta extends Delta {
 	get _reverseDeltaClass() {
 		return SplitDelta;
 	}
+
+	static get className() {
+		return 'engine.treeModel.delta.MergeDelta';
+	}
 }
 
 /**
@@ -100,3 +105,5 @@ register( 'merge', function( position ) {
 
 	return this;
 } );
+
+registerDeserializer( MergeDelta.className, MergeDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/movedelta.js

@@ -7,6 +7,7 @@
 
 import Delta from './delta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import MoveOperation from '../operation/moveoperation.js';
 import Position from '../position.js';
 import Range from '../range.js';
@@ -65,6 +66,10 @@ export default class MoveDelta extends Delta {
 		return MoveDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.MoveDelta';
+	}
+
 	static get _priority() {
 		return 20;
 	}
@@ -105,3 +110,5 @@ register( 'move', function( nodeOrRange, targetPosition ) {
 
 	return this;
 } );
+
+registerDeserializer( MoveDelta.className, MoveDelta );

+ 8 - 1
packages/ckeditor5-engine/src/treemodel/delta/removedelta.js

@@ -7,6 +7,7 @@
 
 import MoveDelta from './movedelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import RemoveOperation from '../operation/removeoperation.js';
 import Position from '../position.js';
 import Range from '../range.js';
@@ -18,7 +19,11 @@ import Range from '../range.js';
  *
  * @memberOf engine.treeModel.delta
  */
-export default class RemoveDelta extends MoveDelta {}
+export default class RemoveDelta extends MoveDelta {
+	static get className() {
+		return 'engine.treeModel.delta.RemoveDelta';
+	}
+}
 
 function addRemoveOperation( batch, delta, position, howMany ) {
 	const operation = new RemoveOperation( position, howMany, batch.doc.version );
@@ -50,3 +55,5 @@ register( 'remove', function( nodeOrRange ) {
 
 	return this;
 } );
+
+registerDeserializer( RemoveDelta.className, RemoveDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/renamedelta.js

@@ -7,6 +7,7 @@
 
 import Delta from './delta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import InsertOperation from '../operation/insertoperation.js';
 import RemoveOperation from '../operation/removeoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
@@ -23,6 +24,10 @@ export default class RenameDelta extends Delta {
 	get _reverseDeltaClass() {
 		return RenameDelta;
 	}
+
+	static get className() {
+		return 'engine.treeModel.delta.RenameDelta';
+	}
 }
 
 function apply( batch, delta, operation ) {
@@ -60,3 +65,5 @@ register( 'rename', function( newName, element ) {
 
 	return this;
 } );
+
+registerDeserializer( RenameDelta.className, RenameDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js

@@ -7,6 +7,7 @@
 
 import Delta from './delta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import Position from '../position.js';
 import Element from '../element.js';
 import InsertOperation from '../operation/insertoperation.js';
@@ -61,6 +62,10 @@ export default class SplitDelta extends Delta {
 		return MergeDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.SplitDelta';
+	}
+
 	static get _priority() {
 		return 5;
 	}
@@ -111,3 +116,5 @@ register( 'split', function( position ) {
 
 	return this;
 } );
+
+registerDeserializer( SplitDelta.className, SplitDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js

@@ -8,6 +8,7 @@
 import Delta from './delta.js';
 import WrapDelta from './wrapdelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import Position from '../position.js';
 import RemoveOperation from '../operation/removeoperation.js';
 import MoveOperation from '../operation/moveoperation.js';
@@ -44,6 +45,10 @@ export default class UnwrapDelta extends Delta {
 		return WrapDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.UnwrapDelta';
+	}
+
 	static get _priority() {
 		return 10;
 	}
@@ -85,3 +90,5 @@ register( 'unwrap', function( element ) {
 
 	return this;
 } );
+
+registerDeserializer( UnwrapDelta.className, UnwrapDelta );

+ 8 - 1
packages/ckeditor5-engine/src/treemodel/delta/weakinsertdelta.js

@@ -7,6 +7,7 @@
 
 import InsertDelta from './insertdelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import InsertOperation from '../operation/insertoperation.js';
 import NodeList from '../nodelist.js';
 
@@ -17,7 +18,11 @@ import NodeList from '../nodelist.js';
  *
  * @memberOf engine.treeModel.delta
  */
-export default class WeakInsertDelta extends InsertDelta {}
+export default class WeakInsertDelta extends InsertDelta {
+	static get className() {
+		return 'engine.treeModel.delta.WeakInsertDelta';
+	}
+}
 
 /**
  * Inserts a node or nodes at the given position. {@link engine.treeModel.Batch#weakInsert weakInsert} is commonly used for actions
@@ -54,3 +59,5 @@ register( 'weakInsert', function( position, nodes ) {
 
 	return this;
 } );
+
+registerDeserializer( WeakInsertDelta.className, WeakInsertDelta );

+ 7 - 0
packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js

@@ -8,6 +8,7 @@
 import Delta from './delta.js';
 import UnwrapDelta from './unwrapdelta.js';
 import { register } from '../batch.js';
+import { registerDeserializer } from './delta.js';
 import Position from '../position.js';
 import Range from '../range.js';
 import Element from '../element.js';
@@ -69,6 +70,10 @@ export default class WrapDelta extends Delta {
 		return UnwrapDelta;
 	}
 
+	static get className() {
+		return 'engine.treeModel.delta.WrapDelta';
+	}
+
 	static get _priority() {
 		return 10;
 	}
@@ -127,3 +132,5 @@ register( 'wrap', function( range, elementOrString ) {
 
 	return this;
 } );
+
+registerDeserializer( WrapDelta.className, WrapDelta );

+ 1 - 0
packages/ckeditor5-engine/src/treemodel/operation/operation.js

@@ -111,5 +111,6 @@ export default class Operation {
 	static fromJSON( json, doc ) {
 		return new Operation( json.baseVersion );
 	}
+
 	/*jshint unused: false*/
 }

+ 11 - 0
packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js

@@ -15,6 +15,7 @@ import Position from '/ckeditor5/engine/treemodel/position.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 
 import AttributeDelta from '/ckeditor5/engine/treemodel/delta/attributedelta.js';
+import { RootAttributeDelta } from '/ckeditor5/engine/treemodel/delta/attributedelta.js';
 import AttributeOperation from '/ckeditor5/engine/treemodel/operation/attributeoperation.js';
 
 let doc, root;
@@ -494,4 +495,14 @@ describe( 'AttributeDelta', () => {
 			expect( reversed.operations[ 1 ].newValue ).to.equal( 'oldA' );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( AttributeDelta.className ).to.equal( 'engine.treeModel.delta.AttributeDelta' );
+	} );
+} );
+
+describe( 'RootAttributeDelta', () => {
+	it( 'should provide proper className', () => {
+		expect( RootAttributeDelta.className ).to.equal( 'engine.treeModel.delta.RootAttributeDelta' );
+	} );
 } );

+ 283 - 0
packages/ckeditor5-engine/tests/treemodel/delta/delta.js

@@ -10,6 +10,19 @@
 import count from '/ckeditor5/utils/count.js';
 import Delta from '/ckeditor5/engine/treemodel/delta/delta.js';
 import Operation from '/ckeditor5/engine/treemodel/operation/operation.js';
+import AttributeOperation from '/ckeditor5/engine/treemodel/operation/attributeoperation.js';
+import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperation.js';
+import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
+import NoOperation from '/ckeditor5/engine/treemodel/operation/nooperation.js';
+import ReinsertOperation from '/ckeditor5/engine/treemodel/operation/reinsertoperation.js';
+import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
+import RootAttributeOperation from '/ckeditor5/engine/treemodel/operation/rootattributeoperation.js';
+import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { registerDeserializer } from '/ckeditor5/engine/treemodel/delta/delta.js';
+import Document from '/ckeditor5/engine/treemodel/document.js';
+import Position from '/ckeditor5/engine/treemodel/position.js';
+import Range from '/ckeditor5/engine/treemodel/range.js';
+import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 // Some test examples of operations.
 class FooOperation extends Operation {
@@ -31,6 +44,24 @@ class BarOperation extends FooOperation {
 	}
 }
 
+class FooDelta extends Delta {
+	static get className() {
+		return 'tets.delta.foo';
+	}
+}
+
+class BarDelta extends Delta {
+	static get className() {
+		return 'tets.delta.bar';
+	}
+}
+
+class FooBarDelta extends Delta {
+	static get className() {
+		return 'tets.delta.foobar';
+	}
+}
+
 describe( 'Delta', () => {
 	describe( 'constructor', () => {
 		it( 'should create an delta with empty properties', () => {
@@ -101,4 +132,256 @@ describe( 'Delta', () => {
 			expect( reversed.operations[ 1 ].string ).to.equal( 'a' );
 		} );
 	} );
+
+	describe( 'toJSON', () => {
+		let delta, root, doc;
+
+		before( () => {
+			registerDeserializer( FooDelta.className, FooDelta );
+		} );
+
+		beforeEach( () => {
+			delta = new FooDelta();
+
+			doc = new Document();
+			root = doc.createRoot( 'root' );
+		} );
+
+		it( 'should return JSON representation for empty delta', () => {
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: []
+			} );
+		} );
+
+		it( 'should create delta with AttributeOperation', () => {
+			let operation = new AttributeOperation(
+				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
+				'foo',
+				true,
+				null,
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with InsertOperation', () => {
+			let operation = new InsertOperation(
+				new Position( root, [ 0 ] ),
+				'x',
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with MoveOperation', () => {
+			let operation = new MoveOperation(
+				new Position( root, [ 0, 0 ] ),
+				1,
+				new Position( root, [ 1, 0 ] ),
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with NoOperation', () => {
+			let operation = new NoOperation( 0 );
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with ReinsertOperation', () => {
+			let operation = new ReinsertOperation(
+				new Position( doc.graveyard, [ 0 ] ),
+				2,
+				new Position( root, [ 0 ] ),
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with RemoveOperation', () => {
+			let operation = new RemoveOperation(
+				new Position( root, [ 2 ] ),
+				2,
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with RootAttributeOperation', () => {
+			let operation = new RootAttributeOperation( root, 'key', null, 'newValue', doc.version );
+
+			delta.addOperation( operation );
+
+			expect( treeModelTestUtils.jsonParseStringify( delta ) ).to.deep.equal( {
+				__class: FooDelta.className,
+				batch: null,
+				operations: [ treeModelTestUtils.jsonParseStringify( operation ) ]
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		let delta, root, doc;
+
+		before( () => {
+			registerDeserializer( FooBarDelta.className, FooDelta );
+		} );
+
+		beforeEach( () => {
+			delta = new FooBarDelta();
+
+			doc = new Document();
+			root = doc.createRoot( 'root' );
+		} );
+
+		it( 'should throw error for unregistered delta', () => {
+			expect( () => {
+				Delta.fromJSON( treeModelTestUtils.jsonParseStringify( new BarDelta() ), {} );
+			} ).to.throw( CKEditorError, /^delta-fromjson-no-deserializer/ );
+		} );
+
+		it( 'should create delta with AttributeOperation', () => {
+			delta.addOperation( new AttributeOperation(
+				new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) ),
+				'foo',
+				true,
+				null,
+				doc.version
+			) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with InsertOperation', () => {
+			delta.addOperation( new InsertOperation(
+				new Position( root, [ 0 ] ),
+				'x',
+				doc.version
+			) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with MoveOperation', () => {
+			delta.addOperation( new MoveOperation(
+				new Position( root, [ 0, 0 ] ),
+				1,
+				new Position( root, [ 1, 0 ] ),
+				doc.version
+			) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with NoOperation', () => {
+			delta.addOperation( new NoOperation( 0 ) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with ReinsertOperation', () => {
+			delta.addOperation( new ReinsertOperation(
+				new Position( doc.graveyard, [ 0 ] ),
+				2,
+				new Position( root, [ 0 ] ),
+				doc.version
+			) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with RemoveOperation', () => {
+			delta.addOperation( new RemoveOperation(
+				new Position( root, [ 2 ] ),
+				2,
+				doc.version
+			) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with RootAttributeOperation', () => {
+			delta.addOperation( new RootAttributeOperation( root, 'key', null, 'newValue', doc.version ) );
+
+			let deserialized = Delta.fromJSON( treeModelTestUtils.jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+	} );
+
+	describe( 'registerDeserializer', () => {
+		it( 'should add delta deserializer', ( done ) => {
+			registerDeserializer( 'foo', deserializer );
+
+			Delta.fromJSON( { __class: 'foo', operations: [] } );
+
+			function deserializer() {
+				done();
+			}
+		} );
+
+		it( 'should throw error for adding already defined deserializer', () => {
+			registerDeserializer( 'BAR', FooDelta );
+
+			expect( () => {
+				registerDeserializer( 'BAR', FooDelta );
+			} ).to.throw( CKEditorError, /^delta-register-deserializer-defined/ );
+		} );
+	} );
 } );

+ 18 - 0
packages/ckeditor5-engine/tests/treemodel/delta/insertdelta.js

@@ -14,9 +14,13 @@ import Position from '/ckeditor5/engine/treemodel/position.js';
 import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperation.js';
 import InsertDelta from '/ckeditor5/engine/treemodel/delta/insertdelta.js';
 
+import Delta from '/ckeditor5/engine/treemodel/delta/delta.js';
+
 import RemoveDelta from '/ckeditor5/engine/treemodel/delta/removedelta.js';
 import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
 
+import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
+
 describe( 'Batch', () => {
 	let doc, root, batch, p, ul, chain;
 
@@ -122,4 +126,18 @@ describe( 'InsertDelta', () => {
 			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
 		} );
 	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create InsertDelta instance from serialized JSON object', () => {
+			let serialized = treeModelTestUtils.jsonParseStringify( insertDelta );
+			let deserialized = Delta.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.be.instanceOf( InsertDelta );
+			expect( deserialized.operations ).to.have.length( 0 );
+		} );
+	} );
+
+	it( 'should provide proper className', () => {
+		expect( InsertDelta.className ).to.equal( 'engine.treeModel.delta.InsertDelta' );
+	} );
 } );

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/mergedelta.js

@@ -137,4 +137,8 @@ describe( 'MergeDelta', () => {
 			expect( reversed.operations[ 1 ].targetPosition.path ).to.deep.equal( [ 1, 2, 0 ] );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( MergeDelta.className ).to.equal( 'engine.treeModel.delta.MergeDelta' );
+	} );
 } );

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/movedelta.js

@@ -156,4 +156,8 @@ describe( 'MoveDelta', () => {
 			expect( reversed.operations[ 0 ].targetPosition.path ).to.deep.equal( [ 1, 1 ] );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( MoveDelta.className ).to.equal( 'engine.treeModel.delta.MoveDelta' );
+	} );
 } );

+ 7 - 0
packages/ckeditor5-engine/tests/treemodel/delta/removedelta.js

@@ -12,6 +12,7 @@ import Document from '/ckeditor5/engine/treemodel/document.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
+import RemoveDelta from '/ckeditor5/engine/treemodel/delta/removedelta.js';
 
 const getNodesAndText = treeModelTestUtils.getNodesAndText;
 
@@ -77,3 +78,9 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'RemoveDelta', ()=> {
+	it( 'should provide proper className', () => {
+		expect( RemoveDelta.className ).to.equal( 'engine.treeModel.delta.RemoveDelta' );
+	} );
+} );

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/renamedelta.js

@@ -91,4 +91,8 @@ describe( 'RenameDelta', () => {
 			expect( root.getChild( 0 ).getText() ).to.equal( 'abc' );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( RenameDelta.className ).to.equal( 'engine.treeModel.delta.RenameDelta' );
+	} );
 } );

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js

@@ -174,5 +174,9 @@ describe( 'SplitDelta', () => {
 			expect( splitDelta._cloneOperation.position.path ).to.deep.equal( [ 1, 2 ] );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( SplitDelta.className ).to.equal( 'engine.treeModel.delta.SplitDelta' );
+	} );
 } );
 

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/unwrapdelta.js

@@ -129,4 +129,8 @@ describe( 'UnwrapDelta', () => {
 			expect( reversed.operations[ 1 ].targetPosition.path ).to.deep.equal( [ 1, 6, 0 ] );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( UnwrapDelta.className ).to.equal( 'engine.treeModel.delta.UnwrapDelta' );
+	} );
 } );

+ 7 - 0
packages/ckeditor5-engine/tests/treemodel/delta/weakinsertdelta.js

@@ -9,6 +9,7 @@
 
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
+import WeakInsertDelta from '/ckeditor5/engine/treemodel/delta/weakinsertdelta.js';
 
 describe( 'Batch', () => {
 	let doc, root, batch, chain, attrs;
@@ -58,3 +59,9 @@ describe( 'Batch', () => {
 		} );
 	} );
 } );
+
+describe( 'WeakInsertDelta', ()=> {
+	it( 'should provide proper className', () => {
+		expect( WeakInsertDelta.className ).to.equal( 'engine.treeModel.delta.WeakInsertDelta' );
+	} );
+} );

+ 4 - 0
packages/ckeditor5-engine/tests/treemodel/delta/wrapdelta.js

@@ -192,5 +192,9 @@ describe( 'WrapDelta', () => {
 			expect( wrapDelta._insertOperation ).to.equal( insertOperation );
 		} );
 	} );
+
+	it( 'should provide proper className', () => {
+		expect( WrapDelta.className ).to.equal( 'engine.treeModel.delta.WrapDelta' );
+	} );
 } );