ソースを参照

Added: Introduced engine debugging tools.

Szymon Cofalik 8 年 前
コミット
22dfa50912

+ 442 - 0
packages/ckeditor5-engine/src/dev-utils/enableenginedebug.js

@@ -0,0 +1,442 @@
+import ModelPosition from '../model/position';
+import ModelRange from '../model/range';
+import ModelText from '../model/text';
+import ModelElement from '../model/element';
+import Operation from '../model/operation/operation';
+import AttributeOperation from '../model/operation/attributeoperation';
+import InsertOperation from '../model/operation/insertoperation';
+import MarkerOperation from '../model/operation/markeroperation';
+import MoveOperation from '../model/operation/moveoperation';
+import NoOperation from '../model/operation/nooperation';
+import RenameOperation from '../model/operation/renameoperation';
+import RootAttributeOperation from '../model/operation/rootattributeoperation';
+import Delta from '../model/delta/delta';
+import AttributeDelta from '../model/delta/attributedelta';
+import { RootAttributeDelta } from '../model/delta/attributedelta';
+import InsertDelta from '../model/delta/insertdelta';
+import MarkerDelta from '../model/delta/markerdelta';
+import MergeDelta from '../model/delta/mergedelta';
+import MoveDelta from '../model/delta/movedelta';
+import RenameDelta from '../model/delta/renamedelta';
+import SplitDelta from '../model/delta/splitdelta';
+import UnwrapDelta from '../model/delta/unwrapdelta';
+import WrapDelta from '../model/delta/wrapdelta';
+import ModelDocument from '../model/document';
+import ModelDocumentFragment from '../model/documentfragment';
+import ModelRootElement from '../model/rootelement';
+
+import ViewDocument from '../view/document';
+import ViewElement from '../view/element';
+import ViewText from '../view/text';
+import ViewDocumentFragment from '../view/documentfragment';
+
+/* global console */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import Editor from '@ckeditor/ckeditor5-core/src/editor/editor';
+
+const treeDump = Symbol( '_treeDump' );
+const maxTreeDumpLength = 20;
+let enabled = false;
+let log = console.log;
+
+export default function enableEngineDebug( logger ) {
+	if ( logger ) {
+		log = logger;
+	}
+
+	if ( !enabled ) {
+		enabled = true;
+
+		enableLoggingTools();
+		enableDocumentTools();
+	}
+
+	return DebugPlugin;
+}
+
+function enableLoggingTools() {
+	ModelPosition.prototype.toString = function() {
+		return `${ this.root } [ ${ this.path.join( ', ' ) } ]`;
+	};
+
+	ModelPosition.prototype.log = function() {
+		log( 'ModelPosition: ' + this );
+	};
+
+	ModelRange.prototype.toString = function() {
+		return `${ this.root } [ ${ this.start.path.join( ', ' ) } ] - [ ${ this.end.path.join( ', ' ) } ]`;
+	};
+
+	ModelRange.prototype.log = function() {
+		log( 'ModelRange: ' + this );
+	};
+
+	ModelText.prototype.toString = function() {
+		return `#${ this.data }`;
+	};
+
+	ModelText.prototype.logExtended = function() {
+		log( `ModelText: ${ this }, attrs: ${ mapString( this._attrs ) }` );
+	};
+
+	ModelText.prototype.log = function() {
+		log( 'ModelText: ' + this );
+	};
+
+	ModelElement.prototype.toString = function() {
+		return `<${ this.rootName || this.name }>`;
+	};
+
+	ModelElement.prototype.log = function() {
+		log( 'ModelElement: ' + this );
+	};
+
+	ModelElement.prototype.logExtended = function() {
+		log( `ModelElement: ${ this }, ${ this.childCount } children, attrs: ${ mapString( this._attrs ) }` );
+	};
+
+	ModelElement.prototype.logAll = function() {
+		log( '--------------------' );
+
+		this.logExtended();
+		log( 'List of children:' );
+
+		for ( let child of this.getChildren() ) {
+			child.log();
+		}
+	};
+
+	ModelElement.prototype.printTree = function( level = 0 ) {
+		let string = '';
+
+		string += '\t'.repeat( level ) + `<${ this.rootName || this.name }${ mapToTags( this.getAttributes() ) }>`;
+
+		for ( let child of this.getChildren() ) {
+			string += '\n';
+
+			if ( child instanceof ModelText ) {
+				const textAttrs = mapToTags( child._attrs );
+
+				string += '\t'.repeat( level + 1 );
+
+				if ( textAttrs !== '' ) {
+					string += `<$text${ textAttrs }>` + child.data + '</$text>';
+				} else {
+					string += child.data;
+				}
+			} else {
+				string += child.printTree( level + 1 );
+			}
+		}
+
+		if ( this.childCount ) {
+			string += '\n' + '\t'.repeat( level );
+		}
+
+		string += `</${ this.rootName || this.name }>`;
+
+		return string;
+	};
+
+	ModelElement.prototype.logTree = function() {
+		log( this.printTree() );
+	};
+
+	ModelRootElement.prototype.toString = function() {
+		return this.rootName;
+	};
+
+	ModelRootElement.prototype.log = function() {
+		log( 'ModelRootElement: ' + this );
+	};
+
+	ModelDocumentFragment.prototype.toString = function() {
+		return `documentFragment`;
+	};
+
+	ModelDocumentFragment.prototype.log = function() {
+		log( 'ModelDocumentFragment: ' + this );
+	};
+
+	ModelDocumentFragment.prototype.printTree = function() {
+		let string = 'ModelDocumentFragment: [';
+
+		for ( let child of this.getChildren() ) {
+			string += '\n';
+
+			if ( child instanceof ModelText ) {
+				const textAttrs = mapToTags( child._attrs );
+
+				string += '\t'.repeat( 1 );
+
+				if ( textAttrs !== '' ) {
+					string += `<$text${ textAttrs }>` + child.data + '</$text>';
+				} else {
+					string += child.data;
+				}
+			} else {
+				string += child.printTree( 1 );
+			}
+		}
+
+		string += '\n]';
+
+		return string;
+	};
+
+	ModelDocumentFragment.prototype.logTree = function() {
+		log( this.printTree() );
+	};
+
+	Operation.prototype.log = function() {
+		log( getClassName( this ) + ': ' + this );
+	};
+
+	AttributeOperation.prototype.toString = function() {
+		return `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.range }`;
+	};
+
+	InsertOperation.prototype.toString = function() {
+		return `[ ${ this.nodes.length } ] -> ${ this.position }`;
+	};
+
+	MarkerOperation.prototype.toString = function() {
+		return `"${ this.name }": ${ this.oldRange } -> ${ this.newRange }`;
+	};
+
+	MoveOperation.prototype.toString = function() {
+		const range = ModelRange.createFromPositionAndShift( this.sourcePosition, this.howMany );
+
+		return `${ range } -> ${ this.targetPosition }`;
+	};
+
+	NoOperation.prototype.toString = function() {
+		return 'NoOperation';
+	};
+
+	RenameOperation.prototype.toString = function() {
+		return `${ this.position }: "${ this.oldName }" -> "${ this.newName }"`;
+	};
+
+	RootAttributeOperation.prototype.toString = function() {
+		return `"${ this.key }": ${ JSON.stringify( this.oldValue ) } -> ${ JSON.stringify( this.newValue ) }, ${ this.root.rootName }`;
+	};
+
+	Delta.prototype.log = function() {
+		log( getClassName( this ) + ': ' + this );
+	};
+
+	Delta.prototype.logAll = function() {
+		log( '--------------------' );
+
+		this.log();
+
+		for ( let op of this.operations ) {
+			op.log();
+		}
+	};
+
+	AttributeDelta.prototype.toString = function() {
+		return `"${ this.key }": -> ${ JSON.stringify( this.value ) }, ${ this.range }, ${ this.operations.length } ops`;
+	};
+
+	InsertDelta.prototype.toString = function() {
+		return this._insertOperation.toString();
+	};
+
+	MarkerDelta.prototype.toString = function() {
+		return this.operations[ 0 ].toString();
+	};
+
+	MergeDelta.prototype.toString = function() {
+		return this.position.toString();
+	};
+
+	MoveDelta.prototype.toString = function() {
+		const opStrings = [];
+
+		for ( let op of this.operations ) {
+			opStrings.push( op.toString() );
+		}
+
+		return opStrings.join( '; ' );
+	};
+
+	RenameDelta.prototype.toString = function() {
+		return this.operations[ 0 ].toString();
+	};
+
+	RootAttributeDelta.prototype.toString = function() {
+		return this.operations[ 0 ].toString();
+	};
+
+	SplitDelta.prototype.toString = function() {
+		return this.position.toString();
+	};
+
+	UnwrapDelta.prototype.toString = function() {
+		return this.position.toString();
+	};
+
+	WrapDelta.prototype.toString = function() {
+		const wrapElement = this._insertOperation.nodes.getNode( 0 );
+
+		return `${ this.range } -> ${ wrapElement }`;
+	};
+
+	ViewElement.prototype.printTree = function( level = 0 ) {
+		let string = '';
+
+		string += '\t'.repeat( level ) + `<${ this.name }${ mapToTags( this.getAttributes() ) }>`;
+
+		for ( let child of this.getChildren() ) {
+			if ( child instanceof ViewText ) {
+				string += '\n' + '\t'.repeat( level + 1 ) + child.data;
+			} else {
+				string += '\n' + child.printTree( level + 1 );
+			}
+		}
+
+		if ( this.childCount ) {
+			string += '\n' + '\t'.repeat( level );
+		}
+
+		string += `</${ this.name }>`;
+
+		return string;
+	};
+
+	ViewElement.prototype.logTree = function() {
+		log( this.printTree() );
+	};
+
+	ViewDocumentFragment.prototype.printTree = function() {
+		let string = 'ViewDocumentFragment: [';
+
+		for ( let child of this.getChildren() ) {
+			if ( child instanceof ViewText ) {
+				string += '\n' + '\t'.repeat( 1 ) + child.data;
+			} else {
+				string += '\n' + child.printTree( 1 );
+			}
+		}
+
+		string += '\n]';
+
+		return string;
+	};
+
+	ViewDocumentFragment.prototype.logTree = function() {
+		log( this.printTree() );
+	};
+}
+
+function enableDocumentTools() {
+	const _modelDocumentApplyOperation = ModelDocument.prototype.applyOperation;
+
+	ModelDocument.prototype.applyOperation = function( operation ) {
+		log( 'Applying ' + getClassName( operation ) + ': ' + operation );
+
+		_modelDocumentApplyOperation.call( this, operation );
+	};
+
+	ModelDocument.prototype.log = function( version = null ) {
+		version = version === null ? this.version : version;
+
+		logDocument( this, version );
+	};
+
+	ViewDocument.prototype.log = function( version ) {
+		logDocument( this, version );
+	};
+
+	Editor.prototype.logModel = function( version = null ) {
+		version = version === null ? this.document.version : version;
+
+		this.document.log( version );
+	};
+
+	Editor.prototype.logView = function( version ) {
+		this.editing.view.log( version );
+	};
+
+	Editor.prototype.logDocuments = function( version = null ) {
+		version = version === null ? this.document.version : version;
+
+		this.logModel( version );
+		this.logView( version );
+	};
+
+	function logDocument( document, version ) {
+		log( '--------------------' );
+
+		if ( document[ treeDump ][ version ] ) {
+			log( document[ treeDump ][ version ] );
+		} else {
+			log( 'Tree log unavailable for given version: ' + version );
+		}
+	}
+}
+
+class DebugPlugin extends Plugin {
+	constructor( editor ) {
+		super( editor );
+
+		const modelDocument = this.editor.document;
+		const viewDocument = this.editor.editing.view;
+
+		modelDocument[ treeDump ] = [];
+		viewDocument[ treeDump ] = [];
+
+		dumpTrees( modelDocument, modelDocument.version );
+		dumpTrees( viewDocument, modelDocument.version );
+
+		modelDocument.on( 'change', () => {
+			dumpTrees( modelDocument, modelDocument.version );
+			dumpTrees( viewDocument, modelDocument.version );
+		}, { priority: 'lowest' } );
+	}
+}
+
+function dumpTrees( document, version ) {
+	let string = '';
+
+	for ( let root of document.roots.values() ) {
+		string += root.printTree() + '\n';
+	}
+
+	document[ treeDump ][ version ] = string.substr( 0, string.length - 1 ); // Remove the last "\n".
+
+	const overflow = document[ treeDump ].length - maxTreeDumpLength;
+
+	if ( overflow > 0 ) {
+		document[ treeDump ][ overflow - 1 ] = null;
+	}
+}
+
+function getClassName( obj ) {
+	const path = obj.constructor.className.split( '.' );
+
+	return path[ path.length - 1 ];
+}
+
+function mapString( map ) {
+	const obj = {};
+
+	for ( let entry of map ) {
+		obj[ entry[ 0 ] ] = entry[ 1 ];
+	}
+
+	return JSON.stringify( obj );
+}
+
+function mapToTags( map ) {
+	let string = '';
+
+	for ( let entry of map ) {
+		string += ` ${ entry[ 0 ] }=${ JSON.stringify( entry[ 1 ] ) }`;
+	}
+
+	return string;
+}

+ 687 - 0
packages/ckeditor5-engine/tests/dev-utils/enableenginedebug.js

@@ -0,0 +1,687 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import enableEngineDebug from '../../src/dev-utils/enableenginedebug';
+import StandardEditor from '@ckeditor/ckeditor5-core/src/editor/standardeditor';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+
+import ModelPosition from '../../src/model/position';
+import ModelRange from '../../src/model/range';
+import ModelText from '../../src/model/text';
+import ModelElement from '../../src/model/element';
+import AttributeOperation from '../../src/model/operation/attributeoperation';
+import InsertOperation from '../../src/model/operation/insertoperation';
+import MarkerOperation from '../../src/model/operation/markeroperation';
+import MoveOperation from '../../src/model/operation/moveoperation';
+import NoOperation from '../../src/model/operation/nooperation';
+import RenameOperation from '../../src/model/operation/renameoperation';
+import RootAttributeOperation from '../../src/model/operation/rootattributeoperation';
+import RemoveOperation from '../../src/model/operation/removeoperation';
+import Delta from '../../src/model/delta/delta';
+import AttributeDelta from '../../src/model/delta/attributedelta';
+import { RootAttributeDelta } from '../../src/model/delta/attributedelta';
+import InsertDelta from '../../src/model/delta/insertdelta';
+import MarkerDelta from '../../src/model/delta/markerdelta';
+import MergeDelta from '../../src/model/delta/mergedelta';
+import MoveDelta from '../../src/model/delta/movedelta';
+import RenameDelta from '../../src/model/delta/renamedelta';
+import SplitDelta from '../../src/model/delta/splitdelta';
+import UnwrapDelta from '../../src/model/delta/unwrapdelta';
+import WrapDelta from '../../src/model/delta/wrapdelta';
+import ModelDocument from '../../src/model/document';
+import ModelDocumentFragment from '../../src/model/documentfragment';
+
+import ViewDocument from '../../src/view/document';
+import ViewAttributeElement from '../../src/view/attributeelement';
+import ViewContainerElement from '../../src/view/containerelement';
+import ViewText from '../../src/view/text';
+import ViewDocumentFragment from '../../src/view/documentfragment';
+
+/* global document */
+
+describe( 'enableEngineDebug', () => {
+	it( 'should return plugin class', () => {
+		const result = enableEngineDebug();
+
+		expect( result.prototype ).to.be.instanceof( Plugin );
+	} );
+
+	it( 'should not throw when called multiple times', () => {
+		enableEngineDebug();
+		enableEngineDebug();
+		const result = enableEngineDebug();
+
+		expect( result.prototype ).to.be.instanceof( Plugin );
+	} );
+} );
+
+describe( 'debug tools', () => {
+	let DebugPlugin, log;
+
+	class TestEditor extends StandardEditor {
+		constructor( ...args ) {
+			super( ...args );
+
+			this.document.createRoot( 'main' );
+			this.editing.createRoot( this.element, 'main' );
+		}
+	}
+
+	before( () => {
+		log = sinon.spy();
+		DebugPlugin = enableEngineDebug( log );
+	} );
+
+	afterEach( () => {
+		log.reset();
+	} );
+
+	describe( 'should provide logging tools', () => {
+		let modelDoc, modelRoot, modelElement, modelDocFrag;
+
+		beforeEach( () => {
+			modelDoc = new ModelDocument();
+			modelRoot = modelDoc.createRoot();
+			modelElement = new ModelElement( 'paragraph', null, new ModelText( 'foo' ) );
+			modelDocFrag = new ModelDocumentFragment( [ new ModelText( 'bar' ) ] );
+		} );
+
+		it( 'for ModelText', () => {
+			const foo = new ModelText( 'foo', { foo: 'bar' } );
+
+			expect( foo.toString() ).to.equal( '#foo' );
+
+			foo.log();
+			expect( log.calledWithExactly( 'ModelText: #foo' ) ).to.be.true;
+
+			foo.logExtended();
+			expect( log.calledWithExactly( 'ModelText: #foo, attrs: {"foo":"bar"}' ) ).to.be.true;
+		} );
+
+		it( 'for ModelElement', () => {
+			const paragraph = new ModelElement( 'paragraph', { foo: 'bar' }, new ModelText( 'foo' ) );
+
+			expect( paragraph.toString() ).to.equal( '<paragraph>' );
+
+			paragraph.log();
+			expectLog( 'ModelElement: <paragraph>' );
+
+			paragraph.logExtended();
+			expectLog( 'ModelElement: <paragraph>, 1 children, attrs: {"foo":"bar"}' );
+
+			sinon.spy( paragraph, 'logExtended' );
+			paragraph.logAll();
+			expect( paragraph.logExtended.called ).to.be.true;
+			expectLog( 'ModelText: #foo' );
+		} );
+
+		it( 'for ModelRootElement', () => {
+			modelRoot.log();
+			expectLog( 'ModelRootElement: main' );
+		} );
+
+		it( 'for ModelDocumentFragment', () => {
+			modelDocFrag.log();
+			expectLog( 'ModelDocumentFragment: documentFragment' );
+		} );
+
+		it( 'for ModelPosition', () => {
+			const posInRoot = new ModelPosition( modelRoot, [ 0, 1, 0 ] );
+			const posInElement = new ModelPosition( modelElement, [ 0 ] );
+			const posInDocFrag = new ModelPosition( modelDocFrag, [ 2, 3 ] );
+
+			expect( posInRoot.toString() ).to.equal( 'main [ 0, 1, 0 ]' );
+			expect( posInElement.toString() ).to.equal( '<paragraph> [ 0 ]' );
+			expect( posInDocFrag.toString() ).to.equal( 'documentFragment [ 2, 3 ]' );
+
+			posInRoot.log();
+			expectLog( 'ModelPosition: main [ 0, 1, 0 ]' );
+		} );
+
+		it( 'for ModelRange', () => {
+			const rangeInRoot = ModelRange.createIn( modelRoot );
+			const rangeInElement = ModelRange.createIn( modelElement );
+			const rangeInDocFrag = ModelRange.createIn( modelDocFrag );
+
+			expect( rangeInRoot.toString() ).to.equal( 'main [ 0 ] - [ 0 ]' );
+			expect( rangeInElement.toString() ).to.equal( '<paragraph> [ 0 ] - [ 3 ]' );
+			expect( rangeInDocFrag.toString() ).to.equal( 'documentFragment [ 0 ] - [ 3 ]' );
+
+			rangeInRoot.log();
+			expectLog( 'ModelRange: main [ 0 ] - [ 0 ]' );
+		} );
+
+		describe( 'for operations', () => {
+			beforeEach( () => {
+				modelRoot.appendChildren( [ new ModelText( 'foobar' ) ] );
+			} );
+
+			it( 'AttributeOperation', () => {
+				const op = new AttributeOperation( ModelRange.createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
+
+				expect( op.toString() ).to.equal( '"key": null -> {"foo":"bar"}, main [ 0 ] - [ 6 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( 'AttributeOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'InsertOperation', () => {
+				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
+
+				expect( op.toString() ).to.equal( '[ 1 ] -> main [ 3 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( 'InsertOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'MarkerOperation', () => {
+				const op = new MarkerOperation( 'marker', null, ModelRange.createIn( modelRoot ), modelDoc.markers, 0 );
+
+				expect( op.toString() ).to.equal( '"marker": null -> main [ 0 ] - [ 6 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( 'MarkerOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'MoveOperation', () => {
+				const op = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, ModelPosition.createAt( modelRoot, 6 ), 0 );
+
+				expect( op.toString() ).to.equal( 'main [ 1 ] - [ 3 ] -> main [ 6 ]' );
+
+				op.log();
+				expect( log.calledWithExactly( 'MoveOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'NoOperation', () => {
+				const op = new NoOperation( 0 );
+
+				expect( op.toString() ).to.equal( 'NoOperation' );
+
+				op.log();
+				expect( log.calledWithExactly( 'NoOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'RenameOperation', () => {
+				const op = new RenameOperation( ModelPosition.createAt( modelRoot, 1 ), 'old', 'new', 0 );
+
+				expect( op.toString() ).to.equal( 'main [ 1 ]: "old" -> "new"' );
+
+				op.log();
+				expect( log.calledWithExactly( 'RenameOperation: ' + op ) ).to.be.true;
+			} );
+
+			it( 'RootAttributeOperation', () => {
+				const op = new RootAttributeOperation( modelRoot, 'key', 'old', null, 0 );
+
+				expect( op.toString() ).to.equal( '"key": "old" -> null, main' );
+
+				op.log();
+				expect( log.calledWithExactly( 'RootAttributeOperation: ' + op ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'for deltas', () => {
+			it( 'Delta', () => {
+				const delta = new Delta();
+				const op = { log: sinon.spy() };
+				delta.addOperation( op );
+
+				sinon.spy( delta, 'log' );
+				delta.logAll();
+
+				expect( op.log.called ).to.be.true;
+				expect( delta.log.called ).to.be.true;
+			} );
+
+			it( 'AttributeDelta', () => {
+				modelRoot.appendChildren( new ModelText( 'foobar' ) );
+
+				const delta = new AttributeDelta();
+				const op = new AttributeOperation( ModelRange.createIn( modelRoot ), 'key', null, { foo: 'bar' }, 0 );
+
+				delta.addOperation( op );
+
+				expect( delta.toString() ).to.equal( '"key": -> {"foo":"bar"}, main [ 0 ] - [ 6 ], 1 ops' );
+				delta.log();
+
+				expect( log.calledWithExactly( 'AttributeDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'InsertDelta', () => {
+				const delta = new InsertDelta();
+				const op = new InsertOperation( ModelPosition.createAt( modelRoot, 3 ), [ new ModelText( 'abc' ) ], 0 );
+
+				delta.addOperation( op );
+
+				expect( delta.toString() ).to.equal( '[ 1 ] -> main [ 3 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'InsertDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'MarkerDelta', () => {
+				modelRoot.appendChildren( new ModelText( 'foobar' ) );
+
+				const delta = new MarkerDelta();
+				const op = new MarkerOperation( 'marker', null, ModelRange.createIn( modelRoot ), modelDoc.markers, 0 );
+
+				delta.addOperation( op );
+
+				expect( delta.toString() ).to.equal( '"marker": null -> main [ 0 ] - [ 6 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'MarkerDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'MergeDelta', () => {
+				const otherRoot = modelDoc.createRoot( '$root', 'otherRoot' );
+				const firstEle = new ModelElement( 'paragraph' );
+				const removedEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
+
+				otherRoot.appendChildren( [ firstEle, removedEle ] );
+
+				const delta = new MergeDelta();
+				const move = new MoveOperation( ModelPosition.createAt( removedEle, 0 ), 3, ModelPosition.createAt( firstEle, 0 ), 0 );
+				const remove = new RemoveOperation( ModelPosition.createBefore( removedEle ), 1, 1 );
+
+				delta.addOperation( move );
+				delta.addOperation( remove );
+
+				expect( delta.toString() ).to.equal( 'otherRoot [ 1 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'MergeDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'MoveDelta', () => {
+				const delta = new MoveDelta();
+				const move1 = new MoveOperation( ModelPosition.createAt( modelRoot, 0 ), 1, ModelPosition.createAt( modelRoot, 3 ), 0 );
+				const move2 = new MoveOperation( ModelPosition.createAt( modelRoot, 1 ), 1, ModelPosition.createAt( modelRoot, 6 ), 0 );
+
+				delta.addOperation( move1 );
+				delta.addOperation( move2 );
+
+				expect( delta.toString() ).to.equal( 'main [ 0 ] - [ 1 ] -> main [ 3 ]; main [ 1 ] - [ 2 ] -> main [ 6 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'MoveDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'RenameDelta', () => {
+				const delta = new RenameDelta();
+				const op = new RenameOperation( ModelPosition.createAt( modelRoot, 1 ), 'old', 'new', 0 );
+
+				delta.addOperation( op );
+
+				expect( delta.toString() ).to.equal( 'main [ 1 ]: "old" -> "new"' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'RenameDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'RootAttributeDelta', () => {
+				const delta = new RootAttributeDelta();
+				const op = new RootAttributeOperation( modelRoot, 'key', 'old', null, 0 );
+
+				delta.addOperation( op );
+
+				expect( delta.toString() ).to.equal( '"key": "old" -> null, main' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'RootAttributeDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'SplitDelta', () => {
+				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
+				const splitEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
+
+				otherRoot.appendChildren( [ splitEle ] );
+
+				const delta = new SplitDelta();
+				const insert = new InsertOperation( ModelPosition.createAt( otherRoot, 1 ), [ new ModelElement( 'paragraph' ) ], 0 );
+				const move = new MoveOperation( ModelPosition.createAt( splitEle, 1 ), 2, new ModelPosition( otherRoot, [ 1, 0 ] ), 1 );
+
+				delta.addOperation( insert );
+				delta.addOperation( move );
+
+				expect( delta.toString() ).to.equal( 'otherRoot [ 0, 1 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'SplitDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'UnwrapDelta', () => {
+				const otherRoot = modelDoc.createRoot( 'main', 'otherRoot' );
+				const unwrapEle = new ModelElement( 'paragraph', null, [ new ModelText( 'foo' ) ] );
+
+				otherRoot.appendChildren( [ unwrapEle ] );
+
+				const delta = new UnwrapDelta();
+				const move = new MoveOperation( ModelPosition.createAt( unwrapEle, 0 ), 3, ModelPosition.createAt( otherRoot, 0 ), 1 );
+				const remove = new RemoveOperation( ModelPosition.createAt( otherRoot, 3 ), 1, 0 );
+
+				delta.addOperation( move );
+				delta.addOperation( remove );
+
+				expect( delta.toString() ).to.equal( 'otherRoot [ 0 ]' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'UnwrapDelta: ' + delta ) ).to.be.true;
+			} );
+
+			it( 'WrapDelta', () => {
+				const delta = new WrapDelta();
+
+				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 6 ), new ModelElement( 'paragraph' ), 0 );
+				const move = new MoveOperation( ModelPosition.createAt( modelRoot, 0 ), 6, new ModelPosition( modelRoot, [ 1, 0 ] ), 1 );
+
+				delta.addOperation( insert );
+				delta.addOperation( move );
+
+				expect( delta.toString() ).to.equal( 'main [ 0 ] - [ 6 ] -> <paragraph>' );
+
+				delta.log();
+				expect( log.calledWithExactly( 'WrapDelta: ' + delta ) ).to.be.true;
+			} );
+		} );
+
+		it( 'for applied operations', () => {
+			const delta = new InsertDelta();
+			const op = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), [ new ModelText( 'foo' ) ], 0 );
+			delta.addOperation( op );
+
+			modelDoc.applyOperation( op );
+
+			expect( log.calledWithExactly( 'Applying InsertOperation: [ 1 ] -> main [ 0 ]' ) );
+		} );
+	} );
+
+	describe( 'should provide tree printing tools', () => {
+		it( 'for model', () => {
+			const modelDoc = new ModelDocument();
+			const modelRoot = modelDoc.createRoot();
+
+			modelRoot.appendChildren( [
+				new ModelElement( 'paragraph', { foo: 'bar' }, [
+					new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' )
+				] ),
+				new ModelElement( 'listItem', { type: 'numbered', indent: 0 }, new ModelText( 'One.' ) ),
+			] );
+
+			const modelRootTree = modelRoot.printTree();
+
+			expect( modelRootTree ).to.equal(
+				'<main>' +
+				'\n\t<paragraph foo="bar">' +
+				'\n\t\tThis is ' +
+				'\n\t\t<$text bold=true>bold</$text>' +
+				'\n\t\t.' +
+				'\n\t</paragraph>' +
+				'\n\t<listItem type="numbered" indent=0>' +
+				'\n\t\tOne.' +
+				'\n\t</listItem>' +
+				'\n</main>'
+			);
+
+			modelRoot.logTree();
+			expect( log.calledWithExactly( modelRootTree ) ).to.be.true;
+
+			const modelParagraph = modelRoot.getChild( 0 );
+			const modelParagraphTree = modelParagraph.printTree();
+			expect( modelParagraphTree ).to.equal(
+				'<paragraph foo="bar">' +
+				'\n\tThis is ' +
+				'\n\t<$text bold=true>bold</$text>' +
+				'\n\t.' +
+				'\n</paragraph>'
+			);
+
+			log.reset();
+			modelParagraph.logTree();
+			expect( log.calledWithExactly( modelParagraphTree ) ).to.be.true;
+
+			const modelDocFrag = new ModelDocumentFragment( [
+				new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' ),
+				new ModelElement( 'paragraph', { foo: 'bar' }, [
+					new ModelText( 'This is ' ), new ModelText( 'bold', { bold: true } ), new ModelText( '.' )
+				] )
+			] );
+
+			const modelDocFragTree = modelDocFrag.printTree();
+			expect( modelDocFragTree ).to.equal(
+				'ModelDocumentFragment: [' +
+				'\n\tThis is ' +
+				'\n\t<$text bold=true>bold</$text>' +
+				'\n\t.' +
+				'\n\t<paragraph foo="bar">' +
+				'\n\t\tThis is ' +
+				'\n\t\t<$text bold=true>bold</$text>' +
+				'\n\t\t.' +
+				'\n\t</paragraph>' +
+				'\n]'
+			);
+
+			log.reset();
+			modelDocFrag.logTree();
+			expect( log.calledWithExactly( modelDocFragTree ) ).to.be.true;
+		} );
+
+		it( 'for view', () => {
+			const viewDoc = new ViewDocument();
+			const viewRoot = viewDoc.createRoot( 'div' );
+
+			viewRoot.appendChildren( [
+				new ViewContainerElement( 'p', { foo: 'bar' }, [
+					new ViewText( 'This is ' ), new ViewAttributeElement( 'b', null, new ViewText( 'bold' ) ), new ViewText( '.' )
+				] ),
+				new ViewContainerElement( 'ol', null, [
+					new ViewContainerElement( 'li', null, new ViewText( 'One.' ) )
+				] )
+			] );
+
+			const viewRootTree = viewRoot.printTree();
+
+			expect( viewRootTree ).to.equal(
+				'<div>' +
+				'\n\t<p foo="bar">' +
+				'\n\t\tThis is ' +
+				'\n\t\t<b>' +
+				'\n\t\t\tbold' +
+				'\n\t\t</b>' +
+				'\n\t\t.' +
+				'\n\t</p>' +
+				'\n\t<ol>' +
+				'\n\t\t<li>' +
+				'\n\t\t\tOne.' +
+				'\n\t\t</li>' +
+				'\n\t</ol>' +
+				'\n</div>'
+			);
+
+			viewRoot.logTree();
+			expect( log.calledWithExactly( viewRootTree ) ).to.be.true;
+
+			const viewParagraph = viewRoot.getChild( 0 );
+			const viewParagraphTree = viewParagraph.printTree();
+			expect( viewParagraphTree ).to.equal(
+				'<p foo="bar">' +
+				'\n\tThis is ' +
+				'\n\t<b>' +
+				'\n\t\tbold' +
+				'\n\t</b>' +
+				'\n\t.' +
+				'\n</p>'
+			);
+
+			log.reset();
+			viewParagraph.logTree();
+			expect( log.calledWithExactly( viewParagraphTree ) ).to.be.true;
+
+			const viewDocFrag = new ViewDocumentFragment( [
+				new ViewText( 'Text.' ),
+				new ViewContainerElement( 'p', { foo: 'bar' }, [
+					new ViewText( 'This is ' ), new ViewAttributeElement( 'b', null, new ViewText( 'bold' ) ), new ViewText( '.' )
+				] )
+			] );
+
+			const viewDocFragTree = viewDocFrag.printTree();
+			expect( viewDocFragTree ).to.equal(
+				'ViewDocumentFragment: [' +
+				'\n\tText.' +
+				'\n\t<p foo="bar">' +
+				'\n\t\tThis is ' +
+				'\n\t\t<b>' +
+				'\n\t\t\tbold' +
+				'\n\t\t</b>' +
+				'\n\t\t.' +
+				'\n\t</p>' +
+				'\n]'
+			);
+
+			log.reset();
+			viewDocFrag.logTree();
+			expect( log.calledWithExactly( viewDocFragTree ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'should store model and view trees state', () => {
+		let editor;
+
+		beforeEach( () => {
+			const div = document.createElement( 'div' );
+
+			return TestEditor.create( div, {
+				plugins: [ DebugPlugin ]
+			} ).then( ( _editor ) => {
+				editor = _editor;
+			} );
+		} );
+
+		it( 'should store model and view state after each applied operation', () => {
+			const model = editor.document;
+			const modelRoot = model.getRoot();
+			const view = editor.editing.view;
+
+			const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), 0 );
+			model.applyOperation( wrapInDelta( insert ) );
+
+			const remove = new RemoveOperation( ModelPosition.createAt( modelRoot, 1 ), 2, 1 );
+			model.applyOperation( wrapInDelta( remove ) );
+
+			log.reset();
+
+			model.log( 0 );
+			expectLog(
+				'<$graveyard></$graveyard>' +
+				'\n<main></main>'
+			);
+
+			model.log( 1 );
+			expectLog(
+				'<$graveyard></$graveyard>' +
+				'\n<main>' +
+				'\n\tfoobar' +
+				'\n</main>'
+			);
+
+			model.log( 2 );
+			expectLog(
+				'<$graveyard>' +
+				'\n\t<$graveyardHolder>' +
+				'\n\t\too' +
+				'\n\t</$graveyardHolder>' +
+				'\n</$graveyard>' +
+				'\n<main>' +
+				'\n\tfbar' +
+				'\n</main>'
+			);
+
+			model.log();
+			expectLog(
+				'<$graveyard>' +
+				'\n\t<$graveyardHolder>' +
+				'\n\t\too' +
+				'\n\t</$graveyardHolder>' +
+				'\n</$graveyard>' +
+				'\n<main>' +
+				'\n\tfbar' +
+				'\n</main>'
+			);
+
+			view.log( 0 );
+			expectLog(
+				'<div></div>'
+			);
+
+			view.log( 1 );
+			expectLog(
+				'<div>' +
+				'\n\tfoobar' +
+				'\n</div>'
+			);
+
+			view.log( 2 );
+			expectLog(
+				'<div>' +
+				'\n\tfbar' +
+				'\n</div>'
+			);
+
+			sinon.spy( model, 'log' );
+			sinon.spy( view, 'log' );
+
+			editor.logModel( 1 );
+			expect( model.log.calledWithExactly( 1 ) ).to.be.true;
+
+			editor.logView( 2 );
+			expect( view.log.calledWithExactly( 2 ) ).to.be.true;
+
+			model.log.reset();
+			view.log.reset();
+
+			editor.logModel();
+			expect( model.log.calledWithExactly( 2 ) ).to.be.true;
+
+			model.log.reset();
+			view.log.reset();
+
+			editor.logDocuments();
+			expect( model.log.calledWithExactly( 2 ) ).to.be.true;
+			expect( view.log.calledWithExactly( 2 ) ).to.be.true;
+
+			model.log.reset();
+			view.log.reset();
+
+			editor.logDocuments( 1 );
+			expect( model.log.calledWithExactly( 1 ) ).to.be.true;
+			expect( view.log.calledWithExactly( 1 ) ).to.be.true;
+		} );
+
+		it( 'should remove old states', () => {
+			const model = editor.document;
+			const modelRoot = model.getRoot();
+
+			for ( let i = 0; i < 25; i++ ) {
+				const insert = new InsertOperation( ModelPosition.createAt( modelRoot, 0 ), new ModelText( 'foobar' ), model.version );
+				model.applyOperation( wrapInDelta( insert ) );
+			}
+
+			model.log( 0 );
+			expectLog( 'Tree log unavailable for given version: 0' );
+		} );
+	} );
+
+	function expectLog( expectedLogMsg ) {
+		expect( log.calledWithExactly( expectedLogMsg ) ).to.be.true;
+		log.reset();
+	}
+} );
+
+function wrapInDelta( op ) {
+	const delta = new Delta();
+	delta.addOperation( op );
+
+	return op;
+}