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

#396 Serialization/deserialization of operations.

Maciej Gołaszewski 9 жил өмнө
parent
commit
4c1ae4d419
23 өөрчлөгдсөн 467 нэмэгдсэн , 27 устгасан
  1. 3 3
      packages/ckeditor5-engine/src/treemodel/element.js
  2. 3 3
      packages/ckeditor5-engine/src/treemodel/nodelist.js
  3. 14 0
      packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js
  4. 14 0
      packages/ckeditor5-engine/src/treemodel/operation/insertoperation.js
  5. 17 0
      packages/ckeditor5-engine/src/treemodel/operation/moveoperation.js
  6. 7 0
      packages/ckeditor5-engine/src/treemodel/operation/nooperation.js
  7. 26 2
      packages/ckeditor5-engine/src/treemodel/operation/operation.js
  8. 8 0
      packages/ckeditor5-engine/src/treemodel/operation/reinsertoperation.js
  9. 7 0
      packages/ckeditor5-engine/src/treemodel/operation/removeoperation.js
  10. 27 0
      packages/ckeditor5-engine/src/treemodel/operation/rootattributeoperation.js
  11. 13 15
      packages/ckeditor5-engine/src/treemodel/position.js
  12. 11 0
      packages/ckeditor5-engine/src/treemodel/range.js
  13. 10 0
      packages/ckeditor5-engine/src/treemodel/rootelement.js
  14. 45 0
      packages/ckeditor5-engine/tests/treemodel/operation/attributeoperation.js
  15. 30 0
      packages/ckeditor5-engine/tests/treemodel/operation/insertoperation.js
  16. 35 0
      packages/ckeditor5-engine/tests/treemodel/operation/moveoperation.js
  17. 22 0
      packages/ckeditor5-engine/tests/treemodel/operation/nooperation.js
  18. 27 1
      packages/ckeditor5-engine/tests/treemodel/operation/operation.js
  19. 27 0
      packages/ckeditor5-engine/tests/treemodel/operation/reinsertoperation.js
  20. 39 0
      packages/ckeditor5-engine/tests/treemodel/operation/removeoperation.js
  21. 54 0
      packages/ckeditor5-engine/tests/treemodel/operation/rootattributeoperation.js
  22. 15 0
      packages/ckeditor5-engine/tests/treemodel/position.js
  23. 13 3
      packages/ckeditor5-engine/tests/treemodel/range.js

+ 3 - 3
packages/ckeditor5-engine/src/treemodel/element.js

@@ -204,10 +204,10 @@ export default class Element extends Node {
 	 *		let deserialized = JSON.parse( JSON.stringify( someElementObject ) );
 	 *		let element = NodeList.fromJSON( deserialized );
 	 *
-	 * @param object
+	 * @param {Object} json
 	 * @returns {engine.treeModel.Element}
 	 */
-	static fromJSON( object ) {
-		return new Element( object.name, object._attrs, NodeList.fromJSON( object._children ) );
+	static fromJSON( json ) {
+		return new Element( json.name, json._attrs, NodeList.fromJSON( json._children ) );
 	}
 }

+ 3 - 3
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -364,13 +364,13 @@ export default class NodeList {
 	 *		let deserialized = JSON.parse( JSON.stringify( someNodeList ) );
 	 *		let nodeList = NodeList.fromJSON( deserialized );
 	 *
-	 * @param object
+	 * @param {Object} json Deserialized JSON object.
 	 * @returns {engine.treeModel.NodeList}
 	 */
-	static fromJSON( object ) {
+	static fromJSON( json ) {
 		let nodes = [];
 
-		for ( let node of object._nodes ) {
+		for ( let node of json._nodes ) {
 			if ( node.text ) {
 				nodes.push( new Text( node.text, node._attrs ) );
 			} else {

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

@@ -134,4 +134,18 @@ 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';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static fromJSON( json, doc ) {
+		return new AttributeOperation( Range.fromJSON( json.range, doc ), json.key, json.oldValue, json.newValue, json.baseVersion );
+	}
 }

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

@@ -71,4 +71,18 @@ export default class InsertOperation extends Operation {
 			range: Range.createFromPositionAndShift( this.position, this.nodeList.length )
 		};
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.InsertOperation';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static fromJSON( json, doc ) {
+		return new InsertOperation( Position.fromJSON( json.position, doc ), NodeList.fromJSON( json.nodeList ), json.baseVersion );
+	}
 }

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

@@ -172,4 +172,21 @@ export default class MoveOperation extends Operation {
 			range: Range.createFromPositionAndShift( this.movedRangeStart, this.howMany )
 		};
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.MoveOperation';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static fromJSON( json, doc ) {
+		let sourcePosition = Position.fromJSON( json.sourcePosition, doc );
+		let targetPosition = Position.fromJSON( json.targetPosition, doc );
+
+		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';
+	}
 }

+ 26 - 2
packages/ckeditor5-engine/src/treemodel/operation/operation.js

@@ -81,11 +81,35 @@ 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 );
+
+		json.__class = this.constructor.className;
 
 		// Due to circular references we need to remove parent reference.
-		json.delta = this.delta ? '[engine.treeModel.Delta]' : null;
+		json.delta = this.delta ? `[${this.delta.constructor.className}]` : null;
 
 		return json;
 	}
+
+	/**
+	 * Name of the operation class used for serialization.
+	 *
+	 * @type {String}
+	 */
+	static get className() {
+		return 'engine.treeModel.operation.Operation';
+	}
+
+	/*jshint unused: true*/
+	/**
+	 * 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.operationOperation}
+	 */
+	static fromJSON( json, doc ) {
+		return new Operation( json.baseVersion );
+	}
+	/*jshint unused: false*/
 }

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

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

@@ -128,4 +128,31 @@ 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';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static fromJSON( json, doc ) {
+		if ( !doc.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( doc.getRoot( json.root ), json.key, json.oldValue, json.newValue, json.baseVersion );
+	}
 }

+ 13 - 15
packages/ckeditor5-engine/src/treemodel/position.js

@@ -10,7 +10,6 @@ import Element from './element.js';
 import last from '../../utils/lib/lodash/last.js';
 import compareArrays from '../../utils/comparearrays';
 import CKEditorError from '../../utils/ckeditorerror.js';
-import clone from '../../utils/lib/lodash/clone.js';
 
 /**
  * Position in the tree. Position is always located before or after a node.
@@ -444,18 +443,6 @@ export default class Position {
 		return this.offset == this.parent.getChildCount();
 	}
 
-	/**
-	 * Custom toJSON method.
-	 *
-	 * @returns {Object} Object representation of this position with the root property replaced with its rootName.
-	 */
-	toJSON() {
-		return {
-			root: this.root.rootName,
-			path: clone( this.path )
-		};
-	}
-
 	/**
 	 * Creates position at the given location. The location can be specified as:
 	 *
@@ -577,13 +564,24 @@ export default class Position {
 	}
 
 	/**
+	 * Creates Element object from deserilized object, ie. from parsed JSON string.
 	 *
-	 * @param {Object} json
-	 * @param {engine.treeModel.Document} doc
+	 * @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 }

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

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

@@ -40,4 +40,14 @@ export default class RootElement extends Element {
 		 */
 		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 typeof this.rootName === 'symbol' ? '$$graveyard' : this.rootName;
+	}
 }

+ 45 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'AttributeOperation', () => {
 	let doc, root;
@@ -365,4 +366,48 @@ 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 = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized.__class ).to.equal( 'engine.treeModel.operation.AttributeOperation' );
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.AttributeOperation',
+				baseVersion: 0,
+				delta: null,
+				key: 'key',
+				newValue: 'newValue',
+				oldValue: null,
+				range: treeModelTestUtils.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 = treeModelTestUtils.jsonParseStringify( op );
+			const deserialized = AttributeOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 30 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'InsertOperation', () => {
 	let doc, root;
@@ -178,4 +179,33 @@ 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 = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.InsertOperation',
+				baseVersion: 0,
+				delta: null,
+				nodeList: treeModelTestUtils.jsonParseStringify( new NodeList( 'x' ) ),
+				position: treeModelTestUtils.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 = treeModelTestUtils.jsonParseStringify( op );
+			const deserialized = InsertOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 35 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'MoveOperation', () => {
 	let doc, root;
@@ -257,4 +258,38 @@ 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 = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.MoveOperation',
+				baseVersion: 0,
+				delta: null,
+				howMany: 1,
+				isSticky: false,
+				movedRangeStart: treeModelTestUtils.jsonParseStringify( op.movedRangeStart ),
+				sourcePosition: treeModelTestUtils.jsonParseStringify( sourcePosition ),
+				targetPosition: treeModelTestUtils.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 = treeModelTestUtils.jsonParseStringify( op );
+			const deserialized = MoveOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 22 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'NoOperation', () => {
 	let noop, doc, root;
@@ -36,4 +37,25 @@ 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 = treeModelTestUtils.jsonParseStringify( noop );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.NoOperation',
+				baseVersion: 0,
+				delta: null
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper NoOperation from json object', () => {
+			const serialized = treeModelTestUtils.jsonParseStringify( noop );
+			const deserialized = NoOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( noop );
+		} );
+	} );
 } );

+ 27 - 1
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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'Operation', () => {
 	it( 'should save its base version', () => {
@@ -27,7 +28,32 @@ describe( 'Operation', () => {
 		let parsedIn = JSON.parse( JSON.stringify( opInDelta ) );
 		let parsedOutside = JSON.parse( JSON.stringify( opOutsideDelta ) );
 
-		expect( parsedIn.delta ).to.equal( '[engine.treeModel.Delta]' );
+		expect( parsedIn.delta ).to.equal( '[engine.treeModel.delta.Delta]' );
 		expect( parsedOutside.delta ).to.be.null;
 	} );
+
+	describe( 'toJSON', () => {
+		it( 'should create proper json object', () => {
+			const op = new Operation( 4 );
+
+			const serialized = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.Operation',
+				baseVersion: 4,
+				delta: null
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper Operation from json object', () => {
+			const op = new Operation( 4 );
+
+			const serialized = treeModelTestUtils.jsonParseStringify( op );
+			const deserialized = Operation.fromJSON( serialized );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 27 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'ReinsertOperation', () => {
 	let doc, root, graveyard, operation, graveyardPosition, rootPosition;
@@ -92,4 +93,30 @@ 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 = treeModelTestUtils.jsonParseStringify( operation );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.ReinsertOperation',
+				baseVersion: 0,
+				delta: null,
+				howMany: 2,
+				isSticky: false,
+				movedRangeStart: treeModelTestUtils.jsonParseStringify( operation.movedRangeStart ),
+				sourcePosition: treeModelTestUtils.jsonParseStringify( operation.sourcePosition ),
+				targetPosition: treeModelTestUtils.jsonParseStringify( operation.targetPosition )
+			} );
+		} );
+	} );
+
+	describe( 'fromJSON', () => {
+		it( 'should create proper ReinsertOperation from json object', () => {
+			const serialized = treeModelTestUtils.jsonParseStringify( operation );
+			const deserialized = ReinsertOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( operation );
+		} );
+	} );
 } );

+ 39 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'RemoveOperation', () => {
 	let doc, root, graveyard;
@@ -116,4 +117,42 @@ 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 = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.RemoveOperation',
+				baseVersion: 0,
+				delta: null,
+				howMany: 2,
+				isSticky: false,
+				movedRangeStart: treeModelTestUtils.jsonParseStringify( op.movedRangeStart ),
+				sourcePosition: treeModelTestUtils.jsonParseStringify( op.sourcePosition ),
+				targetPosition: treeModelTestUtils.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 = treeModelTestUtils.jsonParseStringify( op );
+			const deserialized = RemoveOperation.fromJSON( serialized, doc );
+
+			expect( deserialized ).to.deep.equal( op );
+		} );
+	} );
 } );

+ 54 - 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 treeModelTestUtils from '/tests/engine/treemodel/_utils/utils.js';
 
 describe( 'RootAttributeOperation', () => {
 	let doc, root;
@@ -223,4 +224,57 @@ 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 = treeModelTestUtils.jsonParseStringify( op );
+
+			expect( serialized.__class ).to.equal( 'engine.treeModel.operation.RootAttributeOperation' );
+			expect( serialized ).to.deep.equal( {
+				__class: 'engine.treeModel.operation.RootAttributeOperation',
+				baseVersion: 0,
+				delta: null,
+				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 = treeModelTestUtils.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 = treeModelTestUtils.jsonParseStringify( op );
+			serialized.root = 'no-root';
+
+			expect( () => {
+				RootAttributeOperation.fromJSON( serialized, doc );
+			} ).to.throw( CKEditorError, /rootattributeoperation-fromjson-no-root/ );
+		} );
+	} );
 } );

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

@@ -691,6 +691,14 @@ describe( '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 = treeModelTestUtils.jsonParseStringify( position );
+
+			expect( serialized ).to.deep.equal( { root: '$$graveyard', path: [ 0 ] } );
+		} );
 	} );
 
 	describe( 'fromJSON', () => {
@@ -701,6 +709,13 @@ describe( 'position', () => {
 			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(
 				() => {

+ 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 treeModelTestUtils 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 = treeModelTestUtils.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 );