8
0
Эх сурвалжийг харах

Merge pull request #418 from ckeditor/t/396

T/396 TreeModel serialization / deseralization.
Piotr Jasiun 9 жил өмнө
parent
commit
db9655eacd
64 өөрчлөгдсөн 1550 нэмэгдсэн , 107 устгасан
  1. 20 1
      packages/ckeditor5-engine/src/treemodel/delta/attributedelta.js
  2. 32 0
      packages/ckeditor5-engine/src/treemodel/delta/delta.js
  3. 65 0
      packages/ckeditor5-engine/src/treemodel/delta/deltafactory.js
  4. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/insertdelta.js
  5. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/mergedelta.js
  6. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/movedelta.js
  7. 11 1
      packages/ckeditor5-engine/src/treemodel/delta/removedelta.js
  8. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/renamedelta.js
  9. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/splitdelta.js
  10. 10 0
      packages/ckeditor5-engine/src/treemodel/delta/unwrapdelta.js
  11. 11 1
      packages/ckeditor5-engine/src/treemodel/delta/weakinsertdelta.js
  12. 7 0
      packages/ckeditor5-engine/src/treemodel/delta/wrapdelta.js
  13. 15 5
      packages/ckeditor5-engine/src/treemodel/document.js
  14. 34 0
      packages/ckeditor5-engine/src/treemodel/element.js
  15. 4 4
      packages/ckeditor5-engine/src/treemodel/node.js
  16. 47 16
      packages/ckeditor5-engine/src/treemodel/nodelist.js
  17. 18 0
      packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js
  18. 18 0
      packages/ckeditor5-engine/src/treemodel/operation/insertoperation.js
  19. 21 0
      packages/ckeditor5-engine/src/treemodel/operation/moveoperation.js
  20. 7 0
      packages/ckeditor5-engine/src/treemodel/operation/nooperation.js
  21. 25 3
      packages/ckeditor5-engine/src/treemodel/operation/operation.js
  22. 44 0
      packages/ckeditor5-engine/src/treemodel/operation/operationfactory.js
  23. 8 0
      packages/ckeditor5-engine/src/treemodel/operation/reinsertoperation.js
  24. 7 0
      packages/ckeditor5-engine/src/treemodel/operation/removeoperation.js
  25. 31 0
      packages/ckeditor5-engine/src/treemodel/operation/rootattributeoperation.js
  26. 28 0
      packages/ckeditor5-engine/src/treemodel/position.js
  27. 11 0
      packages/ckeditor5-engine/src/treemodel/range.js
  28. 21 2
      packages/ckeditor5-engine/src/treemodel/rootelement.js
  29. 18 1
      packages/ckeditor5-engine/src/treemodel/text.js
  30. 31 26
      packages/ckeditor5-engine/tests/treemodel/_utils/utils.js
  31. 11 0
      packages/ckeditor5-engine/tests/treemodel/delta/attributedelta.js
  32. 150 0
      packages/ckeditor5-engine/tests/treemodel/delta/delta.js
  33. 170 0
      packages/ckeditor5-engine/tests/treemodel/delta/deltafactory.js
  34. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/insertdelta.js
  35. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/mergedelta.js
  36. 5 3
      packages/ckeditor5-engine/tests/treemodel/delta/movedelta.js
  37. 8 3
      packages/ckeditor5-engine/tests/treemodel/delta/removedelta.js
  38. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/renamedelta.js
  39. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/splitdelta.js
  40. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/insertdelta.js
  41. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/mergedelta.js
  42. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/movedelta.js
  43. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/removedelta.js
  44. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/splitdelta.js
  45. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/unwrapdelta.js
  46. 1 2
      packages/ckeditor5-engine/tests/treemodel/delta/transform/wrapdelta.js
  47. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/unwrapdelta.js
  48. 7 0
      packages/ckeditor5-engine/tests/treemodel/delta/weakinsertdelta.js
  49. 4 0
      packages/ckeditor5-engine/tests/treemodel/delta/wrapdelta.js
  50. 14 0
      packages/ckeditor5-engine/tests/treemodel/document/document.js
  51. 92 1
      packages/ckeditor5-engine/tests/treemodel/element.js
  52. 0 9
      packages/ckeditor5-engine/tests/treemodel/node.js
  53. 123 7
      packages/ckeditor5-engine/tests/treemodel/nodelist.js
  54. 44 0
      packages/ckeditor5-engine/tests/treemodel/operation/attributeoperation.js
  55. 29 0
      packages/ckeditor5-engine/tests/treemodel/operation/insertoperation.js
  56. 34 0
      packages/ckeditor5-engine/tests/treemodel/operation/moveoperation.js
  57. 21 0
      packages/ckeditor5-engine/tests/treemodel/operation/nooperation.js
  58. 27 4
      packages/ckeditor5-engine/tests/treemodel/operation/operation.js
  59. 26 0
      packages/ckeditor5-engine/tests/treemodel/operation/reinsertoperation.js
  60. 38 0
      packages/ckeditor5-engine/tests/treemodel/operation/removeoperation.js
  61. 53 0
      packages/ckeditor5-engine/tests/treemodel/operation/rootattributeoperation.js
  62. 43 0
      packages/ckeditor5-engine/tests/treemodel/position.js
  63. 13 3
      packages/ckeditor5-engine/tests/treemodel/range.js
  64. 17 3
      packages/ckeditor5-engine/tests/treemodel/utils-tests/utils.js

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
 import AttributeOperation from '../operation/attributeoperation.js';
 import RootAttributeOperation from '../operation/rootattributeoperation.js';
@@ -69,6 +70,13 @@ export default class AttributeDelta extends Delta {
 		return AttributeDelta;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.AttributeDelta';
+	}
+
 	static get _priority() {
 		return 20;
 	}
@@ -82,7 +90,14 @@ 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 {
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.RootAttributeDelta';
+	}
+}
 
 /**
  * Sets the value of the attribute of the node or on the range.
@@ -200,3 +215,7 @@ function changeRange( batch, doc, attributeKey, attributeValue, range ) {
 		doc.applyOperation( operation );
 	}
 }
+
+DeltaFactory.register( AttributeDelta );
+
+DeltaFactory.register( RootAttributeDelta );

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

@@ -5,6 +5,9 @@
 
 'use strict';
 
+import clone from '../../../utils/lib/lodash/clone.js';
+import DeltaFactory from './deltafactory.js';
+
 /**
  * Base class for all deltas.
  *
@@ -117,6 +120,33 @@ 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.__className = this.constructor.className;
+
+		// Remove parent batch to avoid circular dependencies.
+		delete json.batch;
+
+		return json;
+	}
+
+	/**
+	 * Delta class name. Used by {@link engine.treeModel.delta.Delta#toJSON} method for serialization and
+	 * {@link engine.treeModel.delta.DeltaFactory.fromJSON} during deserialization.
+	 *
+	 * @type {String}
+	 * @readonly
+	 */
+	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
@@ -129,3 +159,5 @@ export default class Delta {
 		return 0;
 	}
 }
+
+DeltaFactory.register( Delta );

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

@@ -0,0 +1,65 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import CKEditorError from '../../../utils/ckeditorerror.js';
+
+import OperationFactory from '../operation/operationfactory.js';
+
+/**
+ * A factory class for creating operations.
+ *
+ * Delta is a single, from the user action point of view, change in the editable document, like insert, split or
+ * rename element. Delta is composed of operations, which are unit changes needed to be done to execute user action.
+ *
+ * Multiple deltas are grouped into a single {@link engine.treeModel.Batch}.
+ *
+ * @memberOf engine.treeModel.delta
+ */
+export default class DeltaFactory {
+	/**
+	 * Creates InsertDelta from deserialized object, i.e. 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.__className ) ) {
+			/**
+			 * 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.__className }
+			);
+		}
+
+		let Delta = deserializers.get( json.__className );
+
+		let delta = new Delta();
+
+		for ( let operation of json.operations ) {
+			delta.addOperation( OperationFactory.fromJSON( operation, doc ) );
+		}
+
+		return delta;
+	}
+
+	/**
+	 * Registers a class for delta factory.
+	 *
+	 * @param {Function} Delta A delta class to register.
+	 */
+	static register( Delta ) {
+		deserializers.set( Delta.className, Delta );
+	}
+}
+
+const deserializers = new Map();

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

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

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import SplitDelta from './splitdelta.js';
 import { register } from '../batch.js';
 import Position from '../position.js';
@@ -47,6 +48,13 @@ export default class MergeDelta extends Delta {
 	get _reverseDeltaClass() {
 		return SplitDelta;
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.MergeDelta';
+	}
 }
 
 /**
@@ -100,3 +108,5 @@ register( 'merge', function( position ) {
 
 	return this;
 } );
+
+DeltaFactory.register( MergeDelta );

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
 import MoveOperation from '../operation/moveoperation.js';
 import Position from '../position.js';
@@ -65,6 +66,13 @@ export default class MoveDelta extends Delta {
 		return MoveDelta;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.MoveDelta';
+	}
+
 	static get _priority() {
 		return 20;
 	}
@@ -105,3 +113,5 @@ register( 'move', function( nodeOrRange, targetPosition ) {
 
 	return this;
 } );
+
+DeltaFactory.register( MoveDelta );

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

@@ -7,6 +7,7 @@
 
 import MoveDelta from './movedelta.js';
 import { register } from '../batch.js';
+import DeltaFactory from './deltafactory.js';
 import RemoveOperation from '../operation/removeoperation.js';
 import Position from '../position.js';
 import Range from '../range.js';
@@ -18,7 +19,14 @@ import Range from '../range.js';
  *
  * @memberOf engine.treeModel.delta
  */
-export default class RemoveDelta extends MoveDelta {}
+export default class RemoveDelta extends MoveDelta {
+	/**
+	 * @inheritDoc
+	 */
+	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 +58,5 @@ register( 'remove', function( nodeOrRange ) {
 
 	return this;
 } );
+
+DeltaFactory.register( RemoveDelta );

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
 import InsertOperation from '../operation/insertoperation.js';
 import RemoveOperation from '../operation/removeoperation.js';
@@ -23,6 +24,13 @@ export default class RenameDelta extends Delta {
 	get _reverseDeltaClass() {
 		return RenameDelta;
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.RenameDelta';
+	}
 }
 
 function apply( batch, delta, operation ) {
@@ -60,3 +68,5 @@ register( 'rename', function( newName, element ) {
 
 	return this;
 } );
+
+DeltaFactory.register( RenameDelta );

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import { register } from '../batch.js';
 import Position from '../position.js';
 import Element from '../element.js';
@@ -61,6 +62,13 @@ export default class SplitDelta extends Delta {
 		return MergeDelta;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.SplitDelta';
+	}
+
 	static get _priority() {
 		return 5;
 	}
@@ -111,3 +119,5 @@ register( 'split', function( position ) {
 
 	return this;
 } );
+
+DeltaFactory.register( SplitDelta );

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import WrapDelta from './wrapdelta.js';
 import { register } from '../batch.js';
 import Position from '../position.js';
@@ -44,6 +45,13 @@ export default class UnwrapDelta extends Delta {
 		return WrapDelta;
 	}
 
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.delta.UnwrapDelta';
+	}
+
 	static get _priority() {
 		return 10;
 	}
@@ -85,3 +93,5 @@ register( 'unwrap', function( element ) {
 
 	return this;
 } );
+
+DeltaFactory.register( UnwrapDelta );

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

@@ -7,6 +7,7 @@
 
 import InsertDelta from './insertdelta.js';
 import { register } from '../batch.js';
+import DeltaFactory from './deltafactory.js';
 import InsertOperation from '../operation/insertoperation.js';
 import NodeList from '../nodelist.js';
 
@@ -17,7 +18,14 @@ import NodeList from '../nodelist.js';
  *
  * @memberOf engine.treeModel.delta
  */
-export default class WeakInsertDelta extends InsertDelta {}
+export default class WeakInsertDelta extends InsertDelta {
+	/**
+	 * @inheritDoc
+	 */
+	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 +62,5 @@ register( 'weakInsert', function( position, nodes ) {
 
 	return this;
 } );
+
+DeltaFactory.register( WeakInsertDelta );

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

@@ -6,6 +6,7 @@
 'use strict';
 
 import Delta from './delta.js';
+import DeltaFactory from './deltafactory.js';
 import UnwrapDelta from './unwrapdelta.js';
 import { register } from '../batch.js';
 import Position from '../position.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;
 } );
+
+DeltaFactory.register( WrapDelta );

+ 15 - 5
packages/ckeditor5-engine/src/treemodel/document.js

@@ -20,7 +20,7 @@ import Schema from './schema.js';
 import Composer from './composer/composer.js';
 import clone from '../../utils/lib/lodash/clone.js';
 
-const graveyardSymbol = Symbol( 'graveyard' );
+const graveyardName = '$graveyard';
 
 /**
  * Document tree model describes all editable data in the editor. It may contain multiple
@@ -103,7 +103,7 @@ export default class Document {
 		} );
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
-		this.createRoot( graveyardSymbol );
+		this.createRoot( graveyardName );
 
 		/**
 		 * Document's history.
@@ -121,7 +121,7 @@ export default class Document {
 	 * @type {engine.treeModel.RootElement}
 	 */
 	get graveyard() {
-		return this.getRoot( graveyardSymbol );
+		return this.getRoot( graveyardName );
 	}
 
 	/**
@@ -131,7 +131,7 @@ export default class Document {
 	 * @type {Iterable.<String>}
 	 */
 	get rootNames() {
-		return Array.from( this._roots.keys() ).filter( ( name ) => name != graveyardSymbol );
+		return Array.from( this._roots.keys() ).filter( ( name ) => name != graveyardName );
 	}
 
 	/**
@@ -202,7 +202,7 @@ export default class Document {
 			);
 		}
 
-		const root = new RootElement( this, elementName );
+		const root = new RootElement( this, elementName, rootName );
 		this._roots.set( rootName, root );
 
 		return root;
@@ -263,6 +263,16 @@ export default class Document {
 		return this._roots.get( name );
 	}
 
+	/**
+	 * Checks if root with given name is defined.
+	 *
+	 * @param {String} name Name of root to check.
+	 * @returns {Boolean}
+	 */
+	hasRoot( name ) {
+		return this._roots.has( name );
+	}
+
 	/**
 	 * Custom toJSON method to solve child-parent circular dependencies.
 	 *

+ 34 - 0
packages/ckeditor5-engine/src/treemodel/element.js

@@ -197,4 +197,38 @@ export default class Element extends Node {
 
 		return text;
 	}
+
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 */
+	toJSON() {
+		let json = super.toJSON();
+
+		if ( this._children.length ) {
+			json.children = this._children.toJSON();
+		}
+
+		json.name = this.name;
+
+		return json;
+	}
+
+	/**
+	 * Creates Element object from deserilized object, ie. from parsed JSON string.
+	 *
+	 *		let deserialized = JSON.parse( JSON.stringify( someElementObject ) );
+	 *		let element = NodeList.fromJSON( deserialized );
+	 *
+	 * @param {Object} json
+	 * @returns {engine.treeModel.Element}
+	 */
+	static fromJSON( json ) {
+		if ( json.children ) {
+			return new Element( json.name, json.attributes, NodeList.fromJSON( json.children ) );
+		}
+
+		return new Element( json.name, json.attributes );
+	}
 }

+ 4 - 4
packages/ckeditor5-engine/src/treemodel/node.js

@@ -5,7 +5,6 @@
 
 'use strict';
 
-import clone from '../../utils/lib/lodash/clone.js';
 import toMap from '../../utils/tomap.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
@@ -183,10 +182,11 @@ export default class Node {
 	 * @returns {Object} Clone of this object with the parent property replaced with its name.
 	 */
 	toJSON() {
-		const json = clone( this );
+		let json = {};
 
-		// Due to circular references we need to remove parent reference.
-		json.parent = this.parent ? this.parent.name : null;
+		if ( this._attrs.size ) {
+			json.attributes = [ ...this._attrs ];
+		}
 
 		return json;
 	}

+ 47 - 16
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -7,10 +7,10 @@
 
 import CharacterProxy from './characterproxy.js';
 import Text from './text.js';
+import Element from './element.js';
 import DocumentFragment from './documentfragment.js';
 import mapsEqual from '../../utils/mapsequal.js';
 import isIterable from '../../utils/isiterable.js';
-import clone from '../../utils/lib/lodash/clone.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
 /**
@@ -48,19 +48,6 @@ class NodeListText extends Text {
 
 		return new CharacterProxy( this, index );
 	}
-
-	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
-	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
-	 */
-	toJSON() {
-		const json = clone( this );
-
-		json.parent = json.parent ? this.parent.name : null;
-
-		return json;
-	}
 }
 
 /**
@@ -293,7 +280,7 @@ export default class NodeList {
 
 		this._indexMap.splice( index, number );
 
-		for ( let i = index; i < this._indexMap.length ; i++ ) {
+		for ( let i = index; i < this._indexMap.length; i++ ) {
 			this._indexMap[ i ] -= removed.length;
 		}
 
@@ -353,6 +340,50 @@ export default class NodeList {
 		this._mergeNodeAt( index + number );
 	}
 
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 */
+	toJSON() {
+		if ( !this._nodes.length ) {
+			return {};
+		}
+
+		let json = { nodes: [] };
+
+		for ( let node of this._nodes ) {
+			json.nodes.push( node.toJSON() );
+		}
+
+		return json;
+	}
+
+	/**
+	 * Creates NodeList object from deserilized object, ie. from parsed JSON string.
+	 *
+	 *		let deserialized = JSON.parse( JSON.stringify( someNodeList ) );
+	 *		let nodeList = NodeList.fromJSON( deserialized );
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @returns {engine.treeModel.NodeList}
+	 */
+	static fromJSON( json ) {
+		let nodes = [];
+
+		if ( json.nodes ) {
+			for ( let node of json.nodes ) {
+				if ( node.text ) {
+					nodes.push( new Text( node.text, node.attributes ) );
+				} else {
+					nodes.push( Element.fromJSON( node ) );
+				}
+			}
+		}
+
+		return new NodeList( nodes );
+	}
+
 	/**
 	 * Checks whether given index is inside a text and if so, splits that text node. This method should be used
 	 * to split text objects whenever there are some changes made on a part of text object (i.e. removing part of text,
@@ -425,7 +456,7 @@ export default class NodeList {
 			// Remove text node after index.
 			this._nodes.splice( realIndexAfter, 1 );
 
-			for ( let i = index; i < this._indexMap.length ; i++ ) {
+			for ( let i = index; i < this._indexMap.length; i++ ) {
 				this._indexMap[ i ]--;
 			}
 		}

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

@@ -134,4 +134,22 @@ export default class AttributeOperation extends Operation {
 
 		return { range: this.range, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.AttributeOperation';
+	}
+
+	/**
+	 * Creates AttributeOperation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} document Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.AttributeOperation}
+	 */
+	static fromJSON( json, document ) {
+		return new AttributeOperation( Range.fromJSON( json.range, document ), json.key, json.oldValue, json.newValue, json.baseVersion );
+	}
 }

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

@@ -71,4 +71,22 @@ export default class InsertOperation extends Operation {
 			range: Range.createFromPositionAndShift( this.position, this.nodeList.length )
 		};
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.InsertOperation';
+	}
+
+	/**
+	 * Creates InsertOperation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} document Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.InsertOperation}
+	 */
+	static fromJSON( json, document ) {
+		return new InsertOperation( Position.fromJSON( json.position, document ), NodeList.fromJSON( json.nodeList ), json.baseVersion );
+	}
 }

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

@@ -172,4 +172,25 @@ export default class MoveOperation extends Operation {
 			range: Range.createFromPositionAndShift( this.movedRangeStart, this.howMany )
 		};
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.MoveOperation';
+	}
+
+	/**
+	 * Creates MoveOperation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} document Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.MoveOperation}
+	 */
+	static fromJSON( json, document ) {
+		let sourcePosition = Position.fromJSON( json.sourcePosition, document );
+		let targetPosition = Position.fromJSON( json.targetPosition, document );
+
+		return new MoveOperation( sourcePosition, json.howMany, targetPosition, json.baseVersion );
+	}
 }

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

@@ -37,4 +37,11 @@ export default class NoOperation extends Operation {
 	_execute() {
 		// Do nothing.
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.NoOperation';
+	}
 }

+ 25 - 3
packages/ckeditor5-engine/src/treemodel/operation/operation.js

@@ -81,11 +81,33 @@ export default class Operation {
 	 * @returns {Object} Clone of this object with the delta property replaced with string.
 	 */
 	toJSON() {
-		const json = clone( this );
+		const json = clone( this, true );
 
-		// Due to circular references we need to remove parent reference.
-		json.delta = this.delta ? '[engine.treeModel.Delta]' : null;
+		json.__className = this.constructor.className;
+
+		// Remove parent delta to avoid circular dependencies.
+		delete json.delta;
 
 		return json;
 	}
+
+	/**
+	 * Name of the operation class used for serialization.
+	 *
+	 * @type {String}
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.Operation';
+	}
+
+	/**
+	 * Creates Operation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} doc Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.Operation}
+	 */
+	static fromJSON( json ) {
+		return new Operation( json.baseVersion );
+	}
 }

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

@@ -0,0 +1,44 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+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';
+
+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;
+
+/**
+ * A factory class for creating operations.
+ *
+ * @abstract
+ * @memberOf engine.treeModel.operation
+ */
+export default class OperationFactory {
+	/**
+	 * Creates concrete Operation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} document Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.Operation}
+	 */
+	static fromJSON( json, document ) {
+		return operations[ json.__className ].fromJSON( json, document );
+	}
+}

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

@@ -44,4 +44,12 @@ export default class ReinsertOperation extends MoveOperation {
 	getReversed() {
 		return new RemoveOperation( this.targetPosition, this.howMany, this.baseVersion + 1 );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.ReinsertOperation';
+	}
+
 }

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

@@ -48,4 +48,11 @@ export default class RemoveOperation extends MoveOperation {
 	clone() {
 		return new RemoveOperation( this.sourcePosition, this.howMany, this.baseVersion );
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.RemoveOperation';
+	}
 }

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

@@ -128,4 +128,35 @@ export default class RootAttributeOperation extends Operation {
 
 		return { root: this.root, key: this.key, oldValue: this.oldValue, newValue: this.newValue };
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.RootAttributeOperation';
+	}
+
+	/**
+	 * Creates RootAttributeOperation object from deserilized object, i.e. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} document Document on which this operation will be applied.
+	 * @returns {engine.treeModel.operation.RootAttributeOperation}
+	 */
+	static fromJSON( json, document ) {
+		if ( !document.hasRoot( json.root ) ) {
+			/**
+			 * Cannot create RootAttributeOperation for document. Root with specified name does not exist.
+			 *
+			 * @error rootattributeoperation-fromjson-no-root
+			 * @param {String} rootName
+			 */
+			throw new CKEditorError(
+				'rootattributeoperation-fromjson-no-root: Cannot create RootAttributeOperation. Root with specified name does not exist.',
+				{ rootName: json }
+			);
+		}
+
+		return new RootAttributeOperation( document.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion );
+	}
 }

+ 28 - 0
packages/ckeditor5-engine/src/treemodel/position.js

@@ -563,6 +563,34 @@ export default class Position {
 		return new this( position.root, position.path.slice() );
 	}
 
+	/**
+	 * Creates Element object from deserilized object, ie. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} doc Document on which this operation will be applied.
+	 * @returns {engine.treeModel.Position}
+	 */
+	static fromJSON( json, doc ) {
+		if ( json.root === '$graveyard' ) {
+			return new Position( doc.graveyard, json.path );
+		}
+
+		if ( !doc.hasRoot( json.root ) ) {
+			/**
+			 * Cannot create position for document. Root with specified name does not exist.
+			 *
+			 * @error position-fromjson-no-root
+			 * @param {String} rootName
+			 */
+			throw new CKEditorError(
+				'position-fromjson-no-root: Cannot create position for document. Root with specified name does not exist.',
+				{ rootName: json.root }
+			);
+		}
+
+		return new Position( doc.getRoot( json.root ), json.path );
+	}
+
 	/**
 	 * Returns a new position that is a combination of this position and given positions. The combined
 	 * position is this position transformed by moving a range starting at `from` to `to` position.

+ 11 - 0
packages/ckeditor5-engine/src/treemodel/range.js

@@ -533,4 +533,15 @@ export default class Range {
 	static createFromRange( range ) {
 		return new this( range.start, range.end );
 	}
+
+	/**
+	 * Creates Range from deserilized object, ie. from parsed JSON string.
+	 *
+	 * @param {Object} json Deserialized JSON object.
+	 * @param {engine.treeModel.Document} doc Document on which this operation will be applied.
+	 * @returns {engine.treeModel.Range}
+	 */
+	static fromJSON( json, doc ) {
+		return new this( Position.fromJSON( json.start, doc ), Position.fromJSON( json.end, doc ) );
+	}
 }

+ 21 - 2
packages/ckeditor5-engine/src/treemodel/rootelement.js

@@ -19,16 +19,35 @@ export default class RootElement extends Element {
 	 *
 	 * @param {engine.treeModel.Document} doc {@link engine.treeModel.Document} that is an owner of the root.
 	 * @param {String} name Node name.
+	 * @param {String} rootName Root name inside parent {@link engine.treeModel.Document}.
 	 */
-	constructor( doc, name ) {
+	constructor( doc, name, rootName ) {
 		super( name );
 
 		/**
 		 * {@link engine.treeModel.Document} that is an owner of this root.
 		 *
 		 * @readonly
-		 * @member {engine.treeModel.Document} engine.treeModel.RootElement#doc
+		 * @member {engine.treeModel.Document} engine.treeModel.RootElement#document
 		 */
 		this.document = doc;
+
+		/**
+		 * Name of this root inside {@link engine.treeModel.Document} that is an owner of this root.
+		 *
+		 * @readonly
+		 * @member {String} engine.treeModel.RootElement#rootName
+		 */
+		this.rootName = rootName;
+	}
+
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @method engine.treeModel.RootElement#toJSON
+	 * @returns {String} Name of this root inside {@link engine.treeModel.Document} that is an owner of this root.
+	 */
+	toJSON() {
+		return this.rootName;
 	}
 }

+ 18 - 1
packages/ckeditor5-engine/src/treemodel/text.js

@@ -11,7 +11,7 @@ import toMap from '../../utils/tomap.js';
  * Data structure for text with attributes. Note that `Text` is not a {@link engine.treeModel.Node}. This class is used
  * as an aggregator for multiple characters that have same attributes. Example usage:
  *
- *		let myElem = new Element( 'li', [], new Text( 'text with attributes', { foo: true, bar: true } ) );
+ *        let myElem = new Element( 'li', [], new Text( 'text with attributes', { foo: true, bar: true } ) );
  *
  * @memberOf engine.treeModel
  */
@@ -104,4 +104,21 @@ export default class Text {
 	clearAttributes() {
 		this._attrs.clear();
 	}
+
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 */
+	toJSON() {
+		let json = {
+			text: this.text
+		};
+
+		if ( this._attrs.size ) {
+			json.attributes = [ ...this._attrs ];
+		}
+
+		return json;
+	}
 }

+ 31 - 26
packages/ckeditor5-engine/tests/treemodel/_utils/utils.js

@@ -7,34 +7,39 @@
 
 import TreeWalker from '/ckeditor5/engine/treemodel/treewalker.js';
 
-const utils = {
-	/**
-	 * Returns tree structure as a simplified string. Elements are uppercase and characters are lowercase.
-	 * Start and end of an element is marked the same way, by the element's name (in uppercase).
-	 *
-	 *		let element = new Element( 'div', [], [ 'abc', new Element( 'p', [], 'foo' ), 'xyz' ] );
-	 *		treemodelUtils.getNodesAndText( element ); // abcPfooPxyz
-	 *
-	 * @param {engine.treeModel.Range} range Range to stringify.
-	 * @returns {String} String representing element inner structure.
-	 */
-	getNodesAndText( range ) {
-		let txt = '';
-		const treeWalker = new TreeWalker( { boundaries: range } );
+/**
+ * Returns tree structure as a simplified string. Elements are uppercase and characters are lowercase.
+ * Start and end of an element is marked the same way, by the element's name (in uppercase).
+ *
+ *		let element = new Element( 'div', [], [ 'abc', new Element( 'p', [], 'foo' ), 'xyz' ] );
+ *		treemodelUtils.getNodesAndText( element ); // abcPfooPxyz
+ *
+ * @param {engine.treeModel.Range} range Range to stringify.
+ * @returns {String} String representing element inner structure.
+ */
+export function getNodesAndText( range ) {
+	let txt = '';
+	const treeWalker = new TreeWalker( { boundaries: range } );
 
-		for ( let value of treeWalker ) {
-			let node = value.item;
-			let nodeText = node.text || node.character;
+	for ( let value of treeWalker ) {
+		let node = value.item;
+		let nodeText = node.text || node.character;
 
-			if ( nodeText ) {
-				txt += nodeText.toLowerCase();
-			} else {
-				txt += node.name.toUpperCase();
-			}
+		if ( nodeText ) {
+			txt += nodeText.toLowerCase();
+		} else {
+			txt += node.name.toUpperCase();
 		}
-
-		return txt;
 	}
-};
 
-export default utils;
+	return txt;
+}
+
+/**
+ * Returns object JSON representation. It pases an object by JSON.stringify and JSON.parse functions.
+ *
+ * @param {Object|Array} object
+ */
+export function jsonParseStringify( object ) {
+	return JSON.parse( JSON.stringify( object ) );
+}

+ 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' );
+	} );
 } );

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

@@ -10,6 +10,18 @@
 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 DeltaFactory from '/ckeditor5/engine/treemodel/delta/deltafactory.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 { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 // Some test examples of operations.
 class FooOperation extends Operation {
@@ -31,6 +43,12 @@ class BarOperation extends FooOperation {
 	}
 }
 
+class FooDelta extends Delta {
+	static get className() {
+		return 'tets.delta.foo';
+	}
+}
+
 describe( 'Delta', () => {
 	describe( 'constructor', () => {
 		it( 'should create an delta with empty properties', () => {
@@ -101,4 +119,136 @@ describe( 'Delta', () => {
 			expect( reversed.operations[ 1 ].string ).to.equal( 'a' );
 		} );
 	} );
+
+	describe( 'toJSON', () => {
+		let delta, root, doc;
+
+		before( () => {
+			DeltaFactory.register( FooDelta );
+		} );
+
+		beforeEach( () => {
+			delta = new FooDelta();
+
+			doc = new Document();
+			root = doc.createRoot( 'root' );
+		} );
+
+		it( 'should return JSON representation for empty delta', () => {
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				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( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with InsertOperation', () => {
+			let operation = new InsertOperation(
+				new Position( root, [ 0 ] ),
+				'x',
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ 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( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with NoOperation', () => {
+			let operation = new NoOperation( 0 );
+
+			delta.addOperation( operation );
+
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ 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( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with RemoveOperation', () => {
+			let operation = new RemoveOperation(
+				new Position( root, [ 2 ] ),
+				2,
+				doc.version
+			);
+
+			delta.addOperation( operation );
+
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should create delta with RootAttributeOperation', () => {
+			let operation = new RootAttributeOperation( root, 'key', null, 'newValue', doc.version );
+
+			delta.addOperation( operation );
+
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: [ jsonParseStringify( operation ) ]
+			} );
+		} );
+
+		it( 'should remove batch reference', () => {
+			delta.batch = { foo: 'bar' };
+
+			expect( jsonParseStringify( delta ) ).to.deep.equal( {
+				__className: FooDelta.className,
+				operations: []
+			} );
+		} );
+	} );
 } );

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

@@ -0,0 +1,170 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: treemodel, delta */
+
+'use strict';
+
+import Delta from '/ckeditor5/engine/treemodel/delta/delta.js';
+import InsertDelta from '/ckeditor5/engine/treemodel/delta/insertdelta.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 DeltaFactory from '/ckeditor5/engine/treemodel/delta/deltafactory.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 { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
+
+class FooDelta extends Delta {
+	static get className() {
+		return 'tets.delta.foo';
+	}
+}
+
+class BarDelta extends Delta {
+	static get className() {
+		return 'tets.delta.bar';
+	}
+}
+
+describe( 'DeltaFactory', () => {
+	describe( 'fromJSON', () => {
+		let delta, root, doc;
+
+		before( () => {
+			DeltaFactory.register( FooDelta );
+		} );
+
+		beforeEach( () => {
+			delta = new FooDelta();
+
+			doc = new Document();
+			root = doc.createRoot( 'root' );
+		} );
+
+		it( 'should throw error for unregistered delta', () => {
+			expect( () => {
+				DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create delta with NoOperation', () => {
+			delta.addOperation( new NoOperation( 0 ) );
+
+			let deserialized = DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( 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 = DeltaFactory.fromJSON( jsonParseStringify( delta ), doc );
+
+			expect( deserialized ).to.deep.equal( delta );
+		} );
+
+		it( 'should create InsertDelta instance from serialized JSON object', () => {
+			let insertDelta = new InsertDelta();
+			let serialized = jsonParseStringify( insertDelta );
+			let deserialized = DeltaFactory.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.be.instanceOf( InsertDelta );
+			expect( deserialized.operations ).to.have.length( 0 );
+		} );
+	} );
+
+	describe( 'register', () => {
+		it( 'should add delta deserializer', ( done ) => {
+			class SomeDelta {
+				constructor() {
+					done();
+				}
+
+				static get className() {
+					return 'foo';
+				}
+			}
+
+			DeltaFactory.register( SomeDelta );
+
+			DeltaFactory.fromJSON( { __className: 'foo', operations: [] } );
+		} );
+	} );
+} );

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

@@ -122,4 +122,8 @@ describe( 'InsertDelta', () => {
 			expect( reversed.operations[ 0 ].howMany ).to.equal( 1 );
 		} );
 	} );
+
+	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' );
+	} );
 } );

+ 5 - 3
packages/ckeditor5-engine/tests/treemodel/delta/movedelta.js

@@ -7,7 +7,7 @@
 
 'use strict';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
+import { getNodesAndText } from '/tests/engine/treemodel/_utils/utils.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';
@@ -17,8 +17,6 @@ import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import MoveDelta from '/ckeditor5/engine/treemodel/delta/movedelta.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
-
 describe( 'Batch', () => {
 	let doc, root, div, p, batch, chain;
 
@@ -156,4 +154,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' );
+	} );
 } );

+ 8 - 3
packages/ckeditor5-engine/tests/treemodel/delta/removedelta.js

@@ -7,13 +7,12 @@
 
 'use strict';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
+import { getNodesAndText } from '/tests/engine/treemodel/_utils/utils.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 Element from '/ckeditor5/engine/treemodel/element.js';
-
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import RemoveDelta from '/ckeditor5/engine/treemodel/delta/removedelta.js';
 
 describe( 'Batch', () => {
 	let doc, root, div, p, batch, chain, range;
@@ -77,3 +76,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' );
+	} );
 } );
 

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/insertdelta.js

@@ -23,8 +23,7 @@ import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperati
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import ReinsertOperation from '/ckeditor5/engine/treemodel/operation/reinsertoperation.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/mergedelta.js

@@ -22,8 +22,7 @@ import MergeDelta from '/ckeditor5/engine/treemodel/delta/mergedelta.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/movedelta.js

@@ -20,8 +20,7 @@ import SplitDelta from '/ckeditor5/engine/treemodel/delta/splitdelta.js';
 
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/removedelta.js

@@ -20,8 +20,7 @@ import SplitDelta from '/ckeditor5/engine/treemodel/delta/splitdelta.js';
 
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/splitdelta.js

@@ -22,8 +22,7 @@ import SplitDelta from '/ckeditor5/engine/treemodel/delta/splitdelta.js';
 import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperation.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/unwrapdelta.js

@@ -21,8 +21,7 @@ import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.j
 import MergeDelta from '/ckeditor5/engine/treemodel/delta/mergedelta.js';
 import UnwrapDelta from '/ckeditor5/engine/treemodel/delta/unwrapdelta.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 1 - 2
packages/ckeditor5-engine/tests/treemodel/delta/transform/wrapdelta.js

@@ -22,8 +22,7 @@ import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperati
 import MergeDelta from '/ckeditor5/engine/treemodel/delta/mergedelta.js';
 import WrapDelta from '/ckeditor5/engine/treemodel/delta/wrapdelta.js';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 import {
 	applyDelta,

+ 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' );
+	} );
 } );
 

+ 14 - 0
packages/ckeditor5-engine/tests/treemodel/document/document.js

@@ -56,12 +56,14 @@ describe( 'Document', () => {
 			expect( root ).to.be.instanceof( RootElement );
 			expect( root.getChildCount() ).to.equal( 0 );
 			expect( root ).to.have.property( 'name', '$root' );
+			expect( root ).to.have.property( 'rootName', 'root' );
 		} );
 
 		it( 'should create a new RootElement with the specified name', () => {
 			let root = doc.createRoot( 'root', 'foo' );
 
 			expect( root ).to.have.property( 'name', 'foo' );
+			expect( root ).to.have.property( 'rootName', 'root' );
 		} );
 
 		it( 'should throw an error when trying to create a second root with the same name', () => {
@@ -92,6 +94,18 @@ describe( 'Document', () => {
 		} );
 	} );
 
+	describe( 'hasRoot', () => {
+		it( 'should return true when Document has RootElement with given name', () => {
+			doc.createRoot( 'root' );
+
+			expect( doc.hasRoot( 'root' ) ).to.be.true;
+		} );
+
+		it( 'should return false when Document does not have RootElement with given name', () => {
+			expect( doc.hasRoot( 'noroot' ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'applyOperation', () => {
 		it( 'should increase document version, execute operation and fire event with proper data', () => {
 			const changeCallback = sinon.spy();

+ 92 - 1
packages/ckeditor5-engine/tests/treemodel/element.js

@@ -11,6 +11,7 @@ import Node from '/ckeditor5/engine/treemodel/node.js';
 import NodeList from '/ckeditor5/engine/treemodel/nodelist.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Element', () => {
 	describe( 'constructor', () => {
@@ -233,10 +234,100 @@ describe( 'Element', () => {
 				new Element( 'p', null, 'abc' ),
 				'def',
 				new Element( 'p', null, 'ghi' ),
-				'jkl',
+				'jkl'
 			] );
 
 			expect( el.getText() ).to.equal( 'abcdefghijkl' );
 		} );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should serialize empty element', () => {
+			let element = new Element( 'one' );
+
+			expect( jsonParseStringify( element ) ).to.deep.equal( { name: 'one' } );
+		} );
+
+		it( 'should serialize element with attributes', () => {
+			let element = new Element( 'one', { foo: true, bar: false } );
+
+			expect( jsonParseStringify( element ) ).to.deep.equal( {
+				attributes: [ [ 'foo', true ], [ 'bar', false ] ],
+				name: 'one'
+			} );
+		} );
+
+		it( 'should serialize node with children', () => {
+			let img = new Element( 'img' );
+			let one = new Element( 'one' );
+			let two = new Element( 'two', null, [ 'b', 'a', img, 'r' ] );
+			let three = new Element( 'three' );
+
+			let node = new Element( null, null, [ one, two, three ] );
+
+			expect( jsonParseStringify( node ) ).to.deep.equal( {
+				children: {
+					nodes: [
+						{ name: 'one' },
+						{
+							children: {
+								nodes: [
+									{ text: 'ba' },
+									{ name: 'img' },
+									{ text: 'r' }
+								]
+							},
+							name: 'two'
+						},
+						{ name: 'three' }
+					]
+				},
+				name: null
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create element without attributes', () => {
+			const el = new Element( 'el' );
+
+			let serialized = jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized.getChildCount() ).to.equal( 0 );
+		} );
+
+		it( 'should create element with attributes', () => {
+			const el = new Element( 'el', { foo: true } );
+
+			let serialized = jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized.getChildCount() ).to.equal( 0 );
+			expect( deserialized.hasAttribute( 'foo' ) ).to.be.true;
+			expect( deserialized.getAttribute( 'foo' ) ).to.be.true;
+		} );
+
+		it( 'should create element with children', () => {
+			const p = new Element( 'p' );
+			const el = new Element( 'el', null, p );
+
+			let serialized = jsonParseStringify( el );
+
+			let deserialized = Element.fromJSON( serialized );
+
+			expect( deserialized.parent ).to.be.null;
+			expect( deserialized.name ).to.equal( 'el' );
+			expect( deserialized.getChildCount() ).to.equal( 1 );
+
+			expect( deserialized.getChild( 0 ).name ).to.equal( 'p' );
+			expect( deserialized.getChild( 0 ).parent ).to.equal( deserialized );
+		} );
+	} );
 } );

+ 0 - 9
packages/ckeditor5-engine/tests/treemodel/node.js

@@ -97,15 +97,6 @@ describe( 'Node', () => {
 		} );
 	} );
 
-	it( 'should create proper JSON string using toJSON method', () => {
-		let foo = new Element( 'foo', [], [ 'b' ] );
-
-		let parsedFoo = JSON.parse( JSON.stringify( foo ) );
-
-		expect( parsedFoo.parent ).to.equal( null );
-		expect( parsedFoo._children._nodes[ 0 ].parent ).to.equal( 'foo' );
-	} );
-
 	describe( 'getIndex', () => {
 		it( 'should return null if the parent is null', () => {
 			expect( root.getIndex() ).to.be.null;

+ 123 - 7
packages/ckeditor5-engine/tests/treemodel/nodelist.js

@@ -12,6 +12,7 @@ import DocumentFragment from '/ckeditor5/engine/treemodel/documentfragment.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'NodeList', () => {
 	describe( 'constructor', () => {
@@ -153,7 +154,7 @@ describe( 'NodeList', () => {
 		it( 'should merge inserted text objects if possible', () => {
 			let attr = { foo: 'bar' };
 			let outerList = new NodeList( [ 'foo', new Text( 'bar', [ attr ] ) ] );
-			let innerList = new NodeList( [ 'x' , new Text( 'y', [ attr ] ) ] );
+			let innerList = new NodeList( [ 'x', new Text( 'y', [ attr ] ) ] );
 
 			outerList.insert( 3, innerList );
 
@@ -358,13 +359,128 @@ describe( 'NodeList', () => {
 		} );
 	} );
 
-	// Additional test for code coverage.
-	describe( 'NodeListText', () => {
-		it( 'should create proper JSON string using toJSON method', () => {
-			let nodeList = new NodeList( 'foo' );
-			let parsed = JSON.parse( JSON.stringify( nodeList ) );
+	describe( 'toJSON', () => {
+		it( 'should return serialized empty object', () => {
+			let nodeList = new NodeList();
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( {} );
+		} );
+
+		it( 'should return serialized object', () => {
+			let p = new Element( 'p' );
+			let nodeList = new NodeList( p );
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( { nodes: [ jsonParseStringify( p ) ] } );
+		} );
+
+		it( 'should return serialized object with child text', () => {
+			let p = new Element( 'p', null, 'bar' );
+			let nodeList = new NodeList( p );
+
+			let newVar = {
+				nodes: [ {
+					children: { nodes: [ { text: 'bar' } ] },
+					name: 'p'
+				} ]
+			};
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( newVar );
+		} );
+
+		it( 'should return serialized object for text', () => {
+			let text = new Text( 'bar' );
+			let nodeList = new NodeList( text );
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( { nodes: [ { text: 'bar' } ] } );
+		} );
+
+		it( 'should return serialized object for text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( { nodes: [ { attributes: [ [ 'bold', true ] ], text: 'bar' } ] } );
+		} );
+
+		it( 'should return serialized object for text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			expect( jsonParseStringify( nodeList ) ).to.deep.equal( {
+				nodes: [ { attributes: [ [ 'bold', true ] ], text: 'bar' } ]
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create instance from empty serialized element', () => {
+			let nodeList = new NodeList();
+
+			let serialized = jsonParseStringify( nodeList );
 
-			expect( parsed._nodes[ 0 ].parent ).to.equal( null );
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized.length ).to.equal( nodeList.length );
+		} );
+
+		it( 'should create instance from serialized text with attributes', () => {
+			let text = new Text( 'bar', { bold: true } );
+			let nodeList = new NodeList( text );
+
+			let serialized = jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized.length ).to.equal( nodeList.length );
+
+			for ( let i = 0; i < 3; i++ ) {
+				expect( deserialized.get( i ).character ).to.equal( nodeList.get( i ).character );
+				expect( deserialized.get( i ).hasAttribute( 'bold' ) ).to.equal( nodeList.get( i ).hasAttribute( 'bold' ) );
+				expect( deserialized.get( i ).getAttribute( 'bold' ) ).to.equal( nodeList.get( i ).getAttribute( 'bold' ) );
+			}
+		} );
+
+		it( 'should create instance from serialized element', () => {
+			let p = new Element( 'p' );
+			let nodeList = new NodeList( p );
+
+			let serialized = jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized.length ).to.equal( nodeList.length );
+			expect( deserialized.get( 0 ).name ).to.deep.equal( nodeList.get( 0 ).name );
+		} );
+
+		it( 'should create instance from serialized element with attributes', () => {
+			let p = new Element( 'p', { bold: true } );
+			let nodeList = new NodeList( p );
+
+			let serialized = jsonParseStringify( nodeList );
+
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized.length ).to.equal( nodeList.length );
+			expect( deserialized.get( 0 ).name ).to.deep.equal( nodeList.get( 0 ).name );
+			expect( deserialized.get( 0 ).hasAttribute( 'bold' ) ).to.equal( nodeList.get( 0 ).hasAttribute( 'bold' ) );
+			expect( deserialized.get( 0 ).getAttribute( 'bold' ) ).to.equal( nodeList.get( 0 ).getAttribute( 'bold' ) );
+		} );
+
+		it( 'should create instance from serialized element with parent', () => {
+			let p = new Element( 'p', null, 'bar' );
+			let nodeList = new NodeList( p );
+
+			let serialized = jsonParseStringify( nodeList );
+			let deserialized = NodeList.fromJSON( serialized );
+
+			expect( deserialized.length ).to.equal( nodeList.length );
+			expect( deserialized.get( 0 ).name ).to.equal( nodeList.get( 0 ).name );
+			expect( deserialized.get( 0 ).getChildCount() ).to.equal( nodeList.get( 0 ).getChildCount() );
+
+			for ( let i = 0; i < 3; i++ ) {
+				expect( deserialized.get( 0 ).getChild( i ).character ).to.equal( nodeList.get( 0 ).getChild( i ).character );
+				expect( deserialized.get( 0 ).getChild( i ).hasAttribute( 'bold' ) ).to.equal( nodeList.get( 0 ).getChild( i ).hasAttribute( 'bold' ) );
+				expect( deserialized.get( 0 ).getChild( i ).getAttribute( 'bold' ) ).to.equal( nodeList.get( 0 ).getChild( i ).getAttribute( 'bold' ) );
+			}
 		} );
 	} );
 } );

+ 44 - 0
packages/ckeditor5-engine/tests/treemodel/operation/attributeoperation.js

@@ -14,6 +14,7 @@ import Position from '/ckeditor5/engine/treemodel/position.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'AttributeOperation', () => {
 	let doc, root;
@@ -365,4 +366,47 @@ describe( 'AttributeOperation', () => {
 		expect( root._children._nodes[ 0 ].text ).to.equal( 'a' );
 		expect( root._children._nodes[ 1 ].text ).to.equal( 'bcxyz' );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper serialized object', () => {
+			const range = new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) );
+			const op = new AttributeOperation(
+				range,
+				'key',
+				null,
+				'newValue',
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized.__className ).to.equal( 'engine.treeModel.operation.AttributeOperation' );
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.AttributeOperation',
+				baseVersion: 0,
+				key: 'key',
+				newValue: 'newValue',
+				oldValue: null,
+				range: jsonParseStringify( range )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper AttributeOperation from json object', () => {
+			const range = new Range( new Position( root, [ 0 ] ), new Position( root, [ 2 ] ) );
+			const op = new AttributeOperation(
+				range,
+				'key',
+				null,
+				'newValue',
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = AttributeOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 29 - 0
packages/ckeditor5-engine/tests/treemodel/operation/insertoperation.js

@@ -14,6 +14,7 @@ import InsertOperation from '/ckeditor5/engine/treemodel/operation/insertoperati
 import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'InsertOperation', () => {
 	let doc, root;
@@ -178,4 +179,32 @@ describe( 'InsertOperation', () => {
 		expect( clone.nodeList.length ).to.equal( 2 );
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const position = new Position( root, [ 0 ] );
+			const op = new InsertOperation( position, new Text( 'x' ), doc.version );
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.InsertOperation',
+				baseVersion: 0,
+				nodeList: jsonParseStringify( new NodeList( 'x' ) ),
+				position: jsonParseStringify( position )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper InsertOperation from json object', () => {
+			const position = new Position( root, [ 0 ] );
+			const op = new InsertOperation( position, new Text( 'x' ), doc.version );
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = InsertOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 34 - 0
packages/ckeditor5-engine/tests/treemodel/operation/moveoperation.js

@@ -12,6 +12,7 @@ import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.j
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'MoveOperation', () => {
 	let doc, root;
@@ -276,4 +277,37 @@ describe( 'MoveOperation', () => {
 		expect( clone.howMany ).to.equal( howMany );
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const sourcePosition = new Position( root, [ 0, 0 ] );
+			const targetPosition = new Position( root, [ 1, 0 ] );
+			const op = new MoveOperation( sourcePosition, 1, targetPosition, doc.version );
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.MoveOperation',
+				baseVersion: 0,
+				howMany: 1,
+				isSticky: false,
+				movedRangeStart: jsonParseStringify( op.movedRangeStart ),
+				sourcePosition: jsonParseStringify( sourcePosition ),
+				targetPosition: jsonParseStringify( targetPosition )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper MoveOperation from json object', () => {
+			const sourcePosition = new Position( root, [ 0, 0 ] );
+			const targetPosition = new Position( root, [ 1, 0 ] );
+			const op = new MoveOperation( sourcePosition, 1, targetPosition, doc.version );
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = MoveOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 21 - 0
packages/ckeditor5-engine/tests/treemodel/operation/nooperation.js

@@ -9,6 +9,7 @@
 
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import NoOperation from '/ckeditor5/engine/treemodel/operation/nooperation.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'NoOperation', () => {
 	let noop, doc, root;
@@ -36,4 +37,24 @@ describe( 'NoOperation', () => {
 		expect( clone ).to.be.an.instanceof( NoOperation );
 		expect( clone.baseVersion ).to.equal( 0 );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const serialized = jsonParseStringify( noop );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.NoOperation',
+				baseVersion: 0
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper NoOperation from json object', () => {
+			const serialized = jsonParseStringify( noop );
+			const deserialized = NoOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( noop );
+		} );
+	} );
 } );

+ 27 - 4
packages/ckeditor5-engine/tests/treemodel/operation/operation.js

@@ -9,6 +9,7 @@
 
 import Delta from '/ckeditor5/engine/treemodel/delta/delta.js';
 import Operation from '/ckeditor5/engine/treemodel/operation/operation.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Operation', () => {
 	it( 'should save its base version', () => {
@@ -24,10 +25,32 @@ describe( 'Operation', () => {
 
 		let opOutsideDelta = new Operation( 0 );
 
-		let parsedIn = JSON.parse( JSON.stringify( opInDelta ) );
-		let parsedOutside = JSON.parse( JSON.stringify( opOutsideDelta ) );
+		let parsedOutside = jsonParseStringify( opOutsideDelta );
 
-		expect( parsedIn.delta ).to.equal( '[engine.treeModel.Delta]' );
-		expect( parsedOutside.delta ).to.be.null;
+		expect( parsedOutside.delta ).to.be.undefined;
+	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const op = new Operation( 4 );
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.Operation',
+				baseVersion: 4
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper Operation from json object', () => {
+			const op = new Operation( 4 );
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = Operation.fromJSON( serialized );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
 	} );
 } );

+ 26 - 0
packages/ckeditor5-engine/tests/treemodel/operation/reinsertoperation.js

@@ -12,6 +12,7 @@ import ReinsertOperation from '/ckeditor5/engine/treemodel/operation/reinsertope
 import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'ReinsertOperation', () => {
 	let doc, root, graveyard, operation, graveyardPosition, rootPosition;
@@ -92,4 +93,29 @@ describe( 'ReinsertOperation', () => {
 		expect( graveyard.getChild( 1 ).character ).to.equal( 'a' );
 		expect( graveyard.getChild( 2 ).character ).to.equal( 'r' );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const serialized = jsonParseStringify( operation );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.ReinsertOperation',
+				baseVersion: 0,
+				howMany: 2,
+				isSticky: false,
+				movedRangeStart: jsonParseStringify( operation.movedRangeStart ),
+				sourcePosition: jsonParseStringify( operation.sourcePosition ),
+				targetPosition: jsonParseStringify( operation.targetPosition )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper ReinsertOperation from json object', () => {
+			const serialized = jsonParseStringify( operation );
+			const deserialized = ReinsertOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( operation );
+		} );
+	} );
 } );

+ 38 - 0
packages/ckeditor5-engine/tests/treemodel/operation/removeoperation.js

@@ -12,6 +12,7 @@ import ReinsertOperation from '/ckeditor5/engine/treemodel/operation/reinsertope
 import RemoveOperation from '/ckeditor5/engine/treemodel/operation/removeoperation.js';
 import MoveOperation from '/ckeditor5/engine/treemodel/operation/moveoperation.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'RemoveOperation', () => {
 	let doc, root, graveyard;
@@ -116,4 +117,41 @@ describe( 'RemoveOperation', () => {
 		expect( root.getChild( 1 ).character ).to.equal( 'a' );
 		expect( root.getChild( 2 ).character ).to.equal( 'r' );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const op = new RemoveOperation(
+				new Position( root, [ 2 ] ),
+				2,
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.RemoveOperation',
+				baseVersion: 0,
+				howMany: 2,
+				isSticky: false,
+				movedRangeStart: jsonParseStringify( op.movedRangeStart ),
+				sourcePosition: jsonParseStringify( op.sourcePosition ),
+				targetPosition: jsonParseStringify( op.targetPosition )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper RemoveOperation from json object', () => {
+			const op = new RemoveOperation(
+				new Position( root, [ 2 ] ),
+				2,
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = RemoveOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 53 - 0
packages/ckeditor5-engine/tests/treemodel/operation/rootattributeoperation.js

@@ -10,6 +10,7 @@
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import RootAttributeOperation from '/ckeditor5/engine/treemodel/operation/rootattributeoperation.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'RootAttributeOperation', () => {
 	let doc, root;
@@ -223,4 +224,56 @@ describe( 'RootAttributeOperation', () => {
 		expect( clone.newValue ).to.equal( 'new' );
 		expect( clone.baseVersion ).to.equal( baseVersion );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper serialized object', () => {
+			const op = new RootAttributeOperation(
+				root,
+				'key',
+				null,
+				'newValue',
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+
+			expect( serialized.__className ).to.equal( 'engine.treeModel.operation.RootAttributeOperation' );
+			expect( serialized ).to.deep.equal( {
+				__className: 'engine.treeModel.operation.RootAttributeOperation',
+				baseVersion: 0,
+				key: 'key',
+				newValue: 'newValue',
+				oldValue: null,
+				root: 'root'
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper RootAttributeOperation from json object', () => {
+			const op = new RootAttributeOperation( root, 'key', null, 'newValue', doc.version );
+
+			const serialized = jsonParseStringify( op );
+			const deserialized = RootAttributeOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+
+		it( 'should throw an error when root does not exists', () => {
+			const op = new RootAttributeOperation(
+				root,
+				'key',
+				null,
+				'newValue',
+				doc.version
+			);
+
+			const serialized = jsonParseStringify( op );
+			serialized.root = 'no-root';
+
+			expect( () => {
+				RootAttributeOperation.fromJSON( serialized, doc );
+			} ).to.throw( CKEditorError, /rootattributeoperation-fromjson-no-root/ );
+		} );
+	} );
 } );

+ 43 - 0
packages/ckeditor5-engine/tests/treemodel/position.js

@@ -14,6 +14,7 @@ import Text from '/ckeditor5/engine/treemodel/text.js';
 import Position from '/ckeditor5/engine/treemodel/position.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 testUtils.createSinonSandbox();
 
@@ -681,4 +682,46 @@ describe( 'position', () => {
 			expect( shifted.path ).to.deep.equal( [ 1, 2, 0 ] );
 		} );
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should serialize position', () => {
+			let position = new Position( root, [ 0 ] );
+
+			let serialized = jsonParseStringify( position );
+
+			expect( serialized ).to.deep.equal( { root: 'root', path: [ 0 ] } );
+		} );
+
+		it( 'should serialize position from graveyard', () => {
+			let position = new Position( doc.graveyard, [ 0 ] );
+
+			let serialized = jsonParseStringify( position );
+
+			expect( serialized ).to.deep.equal( { root: '$graveyard', path: [ 0 ] } );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create object with given document', () => {
+			let deserialized = Position.fromJSON( { root: 'root', path: [ 0, 1, 2 ] }, doc );
+
+			expect( deserialized.root ).to.equal( root );
+			expect( deserialized.path ).to.deep.equal( [ 0, 1, 2 ] );
+		} );
+
+		it( 'should create object from graveyard', () => {
+			let deserialized = Position.fromJSON( { root: '$graveyard', path: [ 0, 1, 2 ] }, doc );
+
+			expect( deserialized.root ).to.equal( doc.graveyard );
+			expect( deserialized.path ).to.deep.equal( [ 0, 1, 2 ] );
+		} );
+
+		it( 'should throw error when creating object in document that does not have provided root', () => {
+			expect(
+				() => {
+					Position.fromJSON( { root: 'noroot', path: [ 0 ] }, doc );
+				}
+			).to.throw( CKEditorError, /position-fromjson-no-root/ );
+		} );
+	} );
 } );

+ 13 - 3
packages/ckeditor5-engine/tests/treemodel/range.js

@@ -13,12 +13,13 @@ import Element from '/ckeditor5/engine/treemodel/element.js';
 import Text from '/ckeditor5/engine/treemodel/text.js';
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import TreeWalker from '/ckeditor5/engine/treemodel/treewalker.js';
+import { jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Range', () => {
-	let range, start, end, root, otherRoot;
+	let doc, range, start, end, root, otherRoot;
 
 	beforeEach( () => {
-		let doc = new Document();
+		doc = new Document();
 		root = doc.createRoot( 'root' );
 		otherRoot = doc.createRoot( 'otherRoot' );
 
@@ -225,7 +226,7 @@ describe( 'Range', () => {
 
 			expect( mapNodesToNames( items ) ).to.deep.equal(
 				[ 'T:st', 'E:h', 'E:p', 'T:lorem ipsum', 'E:p', 'E:div', 'E:p',
-				'T:foo', 'E:p', 'E:p', 'T:bar', 'E:p', 'E:div', 'E:h', 'T:se' ] );
+					'T:foo', 'E:p', 'E:p', 'T:bar', 'E:p', 'E:div', 'E:h', 'T:se' ] );
 		} );
 
 		it( 'should return treewalker with given options', () => {
@@ -700,6 +701,15 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	describe( 'fromJSON', () => {
+		it( 'should create range from given JSON object', () => {
+			const serialized = jsonParseStringify( range );
+			const deserialized = Range.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( range );
+		} );
+	} );
+
 	function mapNodesToNames( nodes ) {
 		return nodes.map( ( node ) => {
 			return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + ( node.text || node.character );

+ 17 - 3
packages/ckeditor5-engine/tests/treemodel/utils-tests/utils.js

@@ -5,13 +5,11 @@
 
 'use strict';
 
-import treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
+import { getNodesAndText, jsonParseStringify } from '/tests/engine/treemodel/_utils/utils.js';
 import Document from '/ckeditor5/engine/treemodel/document.js';
 import Range from '/ckeditor5/engine/treemodel/range.js';
 import Element from '/ckeditor5/engine/treemodel/element.js';
 
-const getNodesAndText = treeModelTestUtils.getNodesAndText;
-
 describe( 'getNodesAndText', () => {
 	let doc, root, div, p;
 
@@ -29,3 +27,19 @@ describe( 'getNodesAndText', () => {
 		expect( getNodesAndText( Range.createFromElement( root ) ) ).to.equal( 'DIVfoobarDIVPabcxyzP' );
 	} );
 } );
+
+describe( 'jsonParseStringify', () => {
+	class Foo {
+		constructor( ra ) {
+			this.ra = ra;
+		}
+	}
+
+	it( 'should return cleaned object', () => {
+		let foo = new Foo( { bar: 'bar' } );
+
+		let fooJsoned = jsonParseStringify( foo );
+		expect( fooJsoned ).to.not.be.instanceOf( Foo );
+		expect( fooJsoned ).to.deep.equal( { ra: { bar: 'bar' } } );
+	} );
+} );