8
0
Просмотр исходного кода

Merge pull request #9 from ckeditor/t/5

Buttons
Aleksander Nowodzinski 9 лет назад
Родитель
Сommit
9d773a7dac

+ 40 - 52
packages/ckeditor5-undo/src/undo.js

@@ -6,75 +6,63 @@
 'use strict';
 'use strict';
 
 
 import Feature from '../feature.js';
 import Feature from '../feature.js';
-import UndoCommand from './undocommand.js';
+import UndoEngine from './undoengine.js';
+import Model from '../ui/model.js';
+import Button from '../ui/button/button.js';
+import ButtonView from '../ui/button/buttonview.js';
 
 
 /**
 /**
- * Undo feature.
- *
- * Undo features brings in possibility to undo and re-do changes done in Tree Model by deltas through Batch API.
+ * Undo feature. Introduces the "Undo" and "Redo" buttons to the editor.
  *
  *
  * @memberOf undo
  * @memberOf undo
+ * @extends ckeditor5.Feature
  */
  */
 export default class Undo extends Feature {
 export default class Undo extends Feature {
-	constructor( editor ) {
-		super( editor );
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ UndoEngine ];
+	}
 
 
-		/**
-		 * Undo command which manages undo {@link engine.model.Batch batches} stack (history).
-		 * Created and registered during {@link undo.Undo#init feature initialization}.
-		 *
-		 * @private
-		 * @member {undo.UndoCommand} undo.Undo#_undoCommand
-		 */
-		this._undoCommand = null;
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const t = editor.t;
 
 
-		/**
-		 * Undo command which manages redo {@link engine.model.Batch batches} stack (history).
-		 * Created and registered during {@link undo.Undo#init feature initialization}.
-		 *
-		 * @private
-		 * @member {undo.UndoCommand} undo.Undo#_redoCommand
-		 */
-		this._redoCommand = null;
+		this._addButton( 'undo', t( 'Undo' ) );
+		this._addButton( 'redo', t( 'Redo' ) );
 
 
-		/**
-		 * Keeps track of which batch has already been added to undo manager.
-		 *
-		 * @private
-		 * @member {WeakSet.<engine.model.Batch>} undo.Undo#_batchRegistry
-		 */
-		this._batchRegistry = new WeakSet();
+		editor.keystrokes.set( 'CTRL+Z', 'undo' );
+		editor.keystrokes.set( 'CTRL+Y', 'redo' );
+		editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
 	}
 	}
 
 
 	/**
 	/**
-	 * @inheritDoc
+	 * Creates a button for the specified command.
+	 *
+	 * @private
+	 * @param {String} name Command name.
+	 * @param {String} label Button label.
 	 */
 	 */
-	init() {
-		// Create commands.
-		this._redoCommand = new UndoCommand( this.editor );
-		this._undoCommand = new UndoCommand( this.editor );
+	_addButton( name, label ) {
+		const editor = this.editor;
 
 
-		// Register command to the editor.
-		this.editor.commands.set( 'redo', this._redoCommand );
-		this.editor.commands.set( 'undo', this._undoCommand );
+		const command = editor.commands.get( name );
 
 
-		this.listenTo( this.editor.document, 'change', ( evt, type, changes, batch ) => {
-			// Whenever a new batch is created add it to the undo history and clear redo history.
-			if ( batch && !this._batchRegistry.has( batch ) ) {
-				this._batchRegistry.add( batch );
-				this._undoCommand.addBatch( batch );
-				this._redoCommand.clearStack();
-			}
+		const model = new Model( {
+			isOn: false,
+			label: label,
+			icon: name,
+			iconAlign: 'LEFT'
 		} );
 		} );
 
 
-		// Whenever batch is reverted by undo command, add it to redo history.
-		this.listenTo( this._redoCommand, 'revert', ( evt, batch ) => {
-			this._undoCommand.addBatch( batch );
-		} );
+		model.bind( 'isEnabled' ).to( command, 'isEnabled' );
 
 
-		// Whenever batch is reverted by redo command, add it to undo history.
-		this.listenTo( this._undoCommand, 'revert', ( evt, batch ) => {
-			this._redoCommand.addBatch( batch );
-		} );
+		this.listenTo( model, 'execute', () => editor.execute( name ) );
+
+		editor.ui.featureComponents.add( name, Button, ButtonView, model );
 	}
 	}
 }
 }

+ 85 - 0
packages/ckeditor5-undo/src/undoengine.js

@@ -0,0 +1,85 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import Feature from '../feature.js';
+import UndoCommand from './undocommand.js';
+
+/**
+ * Undo engine feature.
+ *
+ * Undo brings in possibility to undo and redo changes done in the model by deltas through
+ * the {@link engine.model.Document#batch Batch API}.
+ *
+ * @memberOf undo
+ * @extends ckeditor5.Feature
+ */
+export default class UndoEngine extends Feature {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( editor ) {
+		super( editor );
+
+		/**
+		 * Undo command which manages undo {@link engine.model.Batch batches} stack (history).
+		 * Created and registered during {@link undo.UndoEngine#init feature initialization}.
+		 *
+		 * @private
+		 * @member {undo.UndoEngineCommand} undo.UndoEngine#_undoCommand
+		 */
+		this._undoCommand = null;
+
+		/**
+		 * Undo command which manages redo {@link engine.model.Batch batches} stack (history).
+		 * Created and registered during {@link undo.UndoEngine#init feature initialization}.
+		 *
+		 * @private
+		 * @member {undo.UndoEngineCommand} undo.UndoEngine#_redoCommand
+		 */
+		this._redoCommand = null;
+
+		/**
+		 * Keeps track of which batch has already been added to undo manager.
+		 *
+		 * @private
+		 * @member {WeakSet.<engine.model.Batch>} undo.UndoEngine#_batchRegistry
+		 */
+		this._batchRegistry = new WeakSet();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		// Create commands.
+		this._redoCommand = new UndoCommand( this.editor );
+		this._undoCommand = new UndoCommand( this.editor );
+
+		// Register command to the editor.
+		this.editor.commands.set( 'redo', this._redoCommand );
+		this.editor.commands.set( 'undo', this._undoCommand );
+
+		this.listenTo( this.editor.document, 'change', ( evt, type, changes, batch ) => {
+			// Whenever a new batch is created add it to the undo history and clear redo history.
+			if ( batch && !this._batchRegistry.has( batch ) ) {
+				this._batchRegistry.add( batch );
+				this._undoCommand.addBatch( batch );
+				this._redoCommand.clearStack();
+			}
+		} );
+
+		// Whenever batch is reverted by undo command, add it to redo history.
+		this.listenTo( this._redoCommand, 'revert', ( evt, batch ) => {
+			this._undoCommand.addBatch( batch );
+		} );
+
+		// Whenever batch is reverted by redo command, add it to undo history.
+		this.listenTo( this._undoCommand, 'revert', ( evt, batch ) => {
+			this._redoCommand.addBatch( batch );
+		} );
+	}
+}

+ 8 - 0
packages/ckeditor5-undo/tests/manual/undo.html

@@ -0,0 +1,8 @@
+<head>
+	<link rel="stylesheet" href="%APPS_DIR%ckeditor/build/amd/theme/ckeditor.css">
+</head>
+
+<div id="editor">
+	<h1>Hello world!</h1>
+	<p>This is an editor instance.</p>
+</div>

+ 21 - 0
packages/ckeditor5-undo/tests/manual/undo.js

@@ -0,0 +1,21 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global console:false */
+
+'use strict';
+
+import ClassicEditor from '/ckeditor5/creator-classic/classic.js';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	features: [ 'delete', 'enter', 'typing', 'paragraph', 'undo', 'basic-styles/bold', 'basic-styles/italic' ],
+	toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
+} )
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 3 - 0
packages/ckeditor5-undo/tests/manual/undo.md

@@ -0,0 +1,3 @@
+@bender-ui: collapsed
+
+Test the undo/redo features.

+ 66 - 295
packages/ckeditor5-undo/tests/undo.js

@@ -3,332 +3,103 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* bender-tags: browser-only */
-
 'use strict';
 'use strict';
 
 
-import Editor from '/ckeditor5/editor.js';
-import ModelDocument from '/ckeditor5/engine/model/document.js';
-import Range from '/ckeditor5/engine/model/range.js';
-import Position from '/ckeditor5/engine/model/position.js';
+import ClassicTestEditor from '/tests/ckeditor5/_utils/classictesteditor.js';
 import Undo from '/ckeditor5/undo/undo.js';
 import Undo from '/ckeditor5/undo/undo.js';
-import Creator from '/ckeditor5/creator/creator.js';
-
-import { setData, getData } from '/tests/engine/_utils/model.js';
+import UndoEngine from '/ckeditor5/undo/undoengine.js';
+import ButtonController from '/ckeditor5/ui/button/button.js';
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
 
 
-// import deleteContents from '/ckeditor5/engine/model/composer/deletecontents.js';
+testUtils.createSinonSandbox();
 
 
-let element, editor, doc, root;
+describe( 'Undo', () => {
+	let editor;
 
 
-beforeEach( () => {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
+	beforeEach( () => {
+		const editorElement = document.createElement( 'div' );
 
 
-	doc = new ModelDocument();
-	root = doc.createRoot( 'root' );
-
-	editor = new Editor( element, {
-		creator: Creator,
-		features: [ Undo ]
+		return ClassicTestEditor.create( editorElement, {
+				features: [ 'undo' ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+			} );
 	} );
 	} );
 
 
-	editor.document = doc;
-
-	return editor.init();
-} );
-
-function setSelection( pathA, pathB ) {
-	doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
-}
-
-function input( input ) {
-	setData( doc, input, { rootName: 'root' } );
-}
-
-function output( output ) {
-	expect( getData( doc, { rootName: 'root' } ) ).to.equal( output );
-}
-
-function undoDisabled() {
-	expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
-}
-
-describe( 'undo integration', () => {
-	describe( 'adding and removing content', () => {
-		it( 'add and undo', () => {
-			input( '<p>fo<selection />o</p><p>bar</p>' );
-
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz<selection />o</p><p>bar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fo<selection />o</p><p>bar</p>' );
-
-			undoDisabled();
-		} );
-
-		it( 'multiple adding and undo', () => {
-			input( '<p>fo<selection />o</p><p>bar</p>' );
-
-			doc.batch()
-				.insert( doc.selection.getFirstPosition(), 'zzz' )
-				.insert( new Position( root, [ 1, 0 ] ), 'xxx' );
-			output( '<p>fozzz<selection />o</p><p>xxxbar</p>' );
-
-			setSelection( [ 1, 0 ], [ 1, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
-			output( '<p>fozzzo</p><p>yyy<selection />xxxbar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p><selection />xxxbar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fo<selection />o</p><p>bar</p>' );
-
-			undoDisabled();
-		} );
-
-		it( 'multiple adding mixed with undo', () => {
-			input( '<p>fo<selection />o</p><p>bar</p>' );
-
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz<selection />o</p><p>bar</p>' );
-
-			setSelection( [ 1, 0 ], [ 1, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
-			output( '<p>fozzzo</p><p>yyy<selection />bar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p><selection />bar</p>' );
-
-			setSelection( [ 0, 0 ], [ 0, 0 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
-			output( '<p>xxx<selection />fozzzo</p><p>bar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p><selection />fozzzo</p><p>bar</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fo<selection />o</p><p>bar</p>' );
-
-			undoDisabled();
-		} );
-
-		it( 'multiple remove and undo', () => {
-			input( '<p><selection />foo</p><p>bar</p>' );
-
-			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
-			output( '<p><selection />o</p><p>bar</p>' );
-
-			setSelection( [ 1, 1 ], [ 1, 1 ] );
-			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
-			output( '<p>o</p><p>b<selection /></p>' );
-
-			editor.execute( 'undo' );
-			// Here is an edge case that selection could be before or after `ar` but selection always ends up after.
-			output( '<p>o</p><p>bar<selection /></p>' );
-
-			editor.execute( 'undo' );
-			// As above.
-			output( '<p>fo<selection />o</p><p>bar</p>' );
-
-			undoDisabled();
-		} );
-
-		it( 'add and remove different parts and undo', () => {
-			input( '<p>fo<selection />o</p><p>bar</p>' );
-
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fozzz<selection />o</p><p>bar</p>' );
-
-			setSelection( [ 1, 2 ], [ 1, 2 ] );
-			doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
-			output( '<p>fozzzo</p><p>b<selection />r</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fozzzo</p><p>ba<selection />r</p>' );
-
-			editor.execute( 'undo' );
-			output( '<p>fo<selection />o</p><p>bar</p>' );
-
-			undoDisabled();
-		} );
-
-		//it( 'add and remove same part and undo', () => {
-		//	// This test case fails because some operations are transformed to NoOperations incorrectly.
-		//	input( '<p>fo<selection />o</p><p>bar</p>' );
-		//
-		//	doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-		//	output( '<p>fozzz<selection />o</p><p>bar</p>' );
-		//
-		//	doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
-		//	output( '<p>fo<selection />o</p><p>bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	output( '<p>fozzz<selection />o</p><p>bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	output( '<p>fo<selection />o</p><p>bar</p>' );
-		//
-		//	undoDisabled();
-		//} );
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( Undo ) ).to.be.instanceOf( Undo );
 	} );
 	} );
 
 
-	describe( 'moving', () => {
-		//it( 'move same content twice then undo', () => {
-		//	// This test case fails because some operations are transformed to NoOperations incorrectly.
-		//	input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
-		//
-		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
-		//	output( '<p>fz</p><p><selection>o</selection>bar</p>' );
-		//
-		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
-		//	output( '<p>fz<selection>o</selection></p><p>bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	output( '<p>fz</p><p><selection>o</selection>bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
-		//
-		//	undoDisabled();
-		//} );
-
-		it( 'move content and new parent then undo', () => {
-			input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
-
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
-			output( '<p>fz</p><p><selection>o</selection>bar</p>' );
+	it( 'should load UndoEngine', () => {
+		expect( editor.plugins.get( UndoEngine ) ).to.be.instanceOf( UndoEngine );
+	} );
 
 
-			setSelection( [ 1 ], [ 2 ] );
-			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
-			output( '<selection><p>obar</p></selection><p>fz</p>' );
+	testButton( 'undo' );
+	testButton( 'redo' );
 
 
-			editor.execute( 'undo' );
-			output( '<p>fz</p><selection><p>obar</p></selection>' );
+	it( 'should set CTRL+Z keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
 
 
-			editor.execute( 'undo' );
-			output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
+		const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true } );
 
 
-			undoDisabled();
-		} );
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'undo' ) ).to.be.true;
 	} );
 	} );
 
 
-	describe( 'attributes with other', () => {
-		it( 'attributes then insert inside then undo', () => {
-			input( '<p>fo<selection>ob</selection>ar</p>' );
+	it( 'should set CTRL+Y keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
 
 
-			doc.batch().setAttr( 'bold', true, doc.selection.getFirstRange() );
-			output( '<p>fo<selection><$text bold=true>ob</$text></selection>ar</p>' );
+		const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.y, ctrlKey: true } );
 
 
-			setSelection( [ 0, 3 ], [ 0, 3 ] );
-			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
-			output( '<p>fo<$text bold=true>o</$text>zzz<selection bold=true /><$text bold=true>b</$text>ar</p>' );
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
+	} );
 
 
-			editor.execute( 'undo' );
-			output( '<p>fo<$text bold=true>o<selection bold=true />b</$text>ar</p>' );
+	it( 'should set CTRL+SHIFT+Z keystroke', () => {
+		const spy = sinon.stub( editor, 'execute' );
 
 
-			editor.execute( 'undo' );
-			output( '<p>fo<selection>ob</selection>ar</p>' );
+		const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true, shiftKey: true } );
 
 
-			undoDisabled();
-		} );
+		expect( wasHandled ).to.be.true;
+		expect( spy.calledWithExactly( 'redo' ) ).to.be.true;
 	} );
 	} );
 
 
-	describe( 'wrapping, unwrapping, merging, splitting', () => {
-		it( 'wrap and undo', () => {
-			input( 'fo<selection>zb</selection>ar' );
+	function testButton( featureName ) {
+		describe( `${ featureName } button`, () => {
+			let buttonController;
 
 
-			doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
-			output( 'fo<p><selection>zb</selection></p>ar' );
+			beforeEach( () => {
+				buttonController = editor.ui.featureComponents.create( featureName );
+			} );
 
 
-			editor.execute( 'undo' );
-			output( 'fo<selection>zb</selection>ar' );
+			it( 'should register feature component', () => {
+				expect( buttonController ).to.be.instanceOf( ButtonController );
+			} );
 
 
-			undoDisabled();
-		} );
-
-		//it( 'wrap, move and undo', () => {
-		//	input( 'fo<selection>zb</selection>ar' );
-		//
-		//	doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
-		//	output( 'fo<p><selection>zb</selection></p>ar' );
-		//
-		//	setSelection( [ 2, 0 ], [ 2, 1 ] );
-		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
-		//	output( '<selection>z</selection>fo<p>b</p>ar' );
-		//
-		//	editor.execute( 'undo' );
-		//	output( 'fo<p><selection>z</selection>b</p>ar' );
-		//
-		//	// This test case fails here for unknown reason, but "z" letter magically disappears.
-		//	// AssertionError: expected 'fo<selection>b</selection>ar' to equal 'fo<selection>zb</selection>ar'
-		//	editor.execute( 'undo' );
-		//	output( 'fo<selection>zb</selection>ar' );
-		//
-		//	undoDisabled();
-		//} );
+			it( `should execute ${ featureName } command on model execute event`, () => {
+				const executeSpy = testUtils.sinon.stub( editor, 'execute' );
+				const model = buttonController.model;
 
 
-		it( 'unwrap and undo', () => {
-			input( '<p>foo<selection />bar</p>' );
+				model.fire( 'execute' );
 
 
-			doc.batch().unwrap( doc.selection.getFirstPosition().parent );
-			output( 'foo<selection />bar' );
+				sinon.assert.calledOnce( executeSpy );
+				sinon.assert.calledWithExactly( executeSpy, featureName );
+			} );
 
 
-			editor.execute( 'undo' );
-			output( '<p>foo<selection />bar</p>' );
+			it( `should bind model to ${ featureName } command`, () => {
+				const model = buttonController.model;
+				const command = editor.commands.get( featureName );
 
 
-			undoDisabled();
-		} );
+				expect( model.isOn ).to.be.false;
 
 
-		//it( 'merge and undo', () => {
-		//	input( '<p>foo</p><p><selection />bar</p>' );
-		//
-		//	doc.batch().merge( new Position( root, [ 1 ] ) );
-		//	// This test fails here because selection is stuck with <p> element and ends up in graveyard.
-		//	// AssertionError: expected '<p>foobar</p>' to equal '<p>foo<selection />bar</p>'
-		//	output( '<p>foo<selection />bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	// This test fails because when selection is transformed it is first in empty <p> but when
-		//	// "bar" is inserted, it gets moved to the right.
-		//	// AssertionError: expected '<p>foo</p><p>bar<selection /></p>' to equal '<p>foo</p><p><selection />bar</p>'
-		//	output( '<p>foo</p><p><selection />bar</p>' );
-		//
-		//	undoDisabled();
-		//} );
+				const initState = command.isEnabled;
+				expect( model.isEnabled ).to.equal( initState );
 
 
-		//it( 'split and undo', () => {
-		//	input( '<p>foo<selection />bar</p>' );
-		//
-		//	doc.batch().split( doc.selection.getFirstPosition() );
-		//	// This test fails because selection ends up in wrong node after splitting.
-		//	// AssertionError: expected '<p>foo<selection /></p><p>bar</p>' to equal '<p>foo</p><p><selection />bar</p>'
-		//	output( '<p>foo</p><p><selection />bar</p>' );
-		//
-		//	editor.execute( 'undo' );
-		//	// This test fails because selection after transforming ends up after inserted test.
-		//	// AssertionError: expected '<p>foobar<selection /></p>' to equal '<p>foo<selection />bar</p>'
-		//	output( '<p>foo<selection />bar</p>' );
-		//
-		//	undoDisabled();
-		//} );
-	} );
-
-	describe( 'other edge cases', () => {
-		//it( 'deleteContents between two nodes', () => {
-		//	input( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
-		//
-		//	deleteContents( doc.batch(), doc.selection, { merge: true } );
-		//	output( '<p>fo<selection />ar</p>' );
-		//
-		//	// This test case fails because of OT problems.
-		//	// When the batch is undone, first MergeDelta is reversed to SplitDelta and it is undone.
-		//	// Then RemoveOperations are reversed to ReinsertOperation.
-		//	// Unfortunately, ReinsertOperation that inserts "o" points to the same position were split happened.
-		//	// Then, when ReinsertOperation is transformed by operations of SplitDelta, it ends up in wrong <p>.
-		//	editor.execute( 'undo' );
-		//	output( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
-		//} );
-	} );
+				command.isEnabled = !initState;
+				expect( model.isEnabled ).to.equal( !initState );
+			} );
+		} );
+	}
 } );
 } );

+ 5 - 12
packages/ckeditor5-undo/tests/undocommand.js

@@ -3,29 +3,22 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* bender-tags: browser-only */
-
 'use strict';
 'use strict';
 
 
-import Editor from '/ckeditor5/editor.js';
-import ModelDocument from '/ckeditor5/engine/model/document.js';
+import ModelTestEditor from '/tests/ckeditor5/_utils/modeltesteditor.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import UndoCommand from '/ckeditor5/undo/undocommand.js';
 import UndoCommand from '/ckeditor5/undo/undocommand.js';
 
 
-let element, editor, doc, root, undo;
+let editor, doc, root, undo;
 
 
 beforeEach( () => {
 beforeEach( () => {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
-
-	editor = new Editor( element );
+	editor = new ModelTestEditor();
 	undo = new UndoCommand( editor );
 	undo = new UndoCommand( editor );
 
 
-	doc = new ModelDocument();
-	editor.document = doc;
+	doc = editor.document;
 
 
-	root = doc.createRoot( 'root' );
+	root = doc.getRoot();
 } );
 } );
 
 
 afterEach( () => {
 afterEach( () => {

+ 324 - 0
packages/ckeditor5-undo/tests/undoengine-integration.js

@@ -0,0 +1,324 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ModelTestEditor from '/tests/ckeditor5/_utils/modeltesteditor.js';
+import Range from '/ckeditor5/engine/model/range.js';
+import Position from '/ckeditor5/engine/model/position.js';
+import UndoEngine from '/ckeditor5/undo/undoengine.js';
+
+import { setData, getData } from '/tests/engine/_utils/model.js';
+
+// import deleteContents from '/ckeditor5/engine/model/composer/deletecontents.js';
+
+let editor, doc, root;
+
+beforeEach( () => {
+	return ModelTestEditor.create( {
+			features: [ UndoEngine ]
+		} )
+		.then( newEditor => {
+			editor = newEditor;
+			doc = editor.document;
+			root = doc.getRoot();
+		} );
+} );
+
+function setSelection( pathA, pathB ) {
+	doc.selection.setRanges( [ new Range( new Position( root, pathA ), new Position( root, pathB ) ) ] );
+}
+
+function input( input ) {
+	setData( doc, input );
+}
+
+function output( output ) {
+	expect( getData( doc ) ).to.equal( output );
+}
+
+function undoDisabled() {
+	expect( editor.commands.get( 'undo' ).isEnabled ).to.be.false;
+}
+
+describe( 'UndoEngine integration', () => {
+	describe( 'adding and removing content', () => {
+		it( 'add and undo', () => {
+			input( '<p>fo<selection />o</p><p>bar</p>' );
+
+			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			output( '<p>fozzz<selection />o</p><p>bar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<selection />o</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+
+		it( 'multiple adding and undo', () => {
+			input( '<p>fo<selection />o</p><p>bar</p>' );
+
+			doc.batch()
+				.insert( doc.selection.getFirstPosition(), 'zzz' )
+				.insert( new Position( root, [ 1, 0 ] ), 'xxx' );
+			output( '<p>fozzz<selection />o</p><p>xxxbar</p>' );
+
+			setSelection( [ 1, 0 ], [ 1, 0 ] );
+			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+			output( '<p>fozzzo</p><p>yyy<selection />xxxbar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fozzzo</p><p><selection />xxxbar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<selection />o</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+
+		it( 'multiple adding mixed with undo', () => {
+			input( '<p>fo<selection />o</p><p>bar</p>' );
+
+			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			output( '<p>fozzz<selection />o</p><p>bar</p>' );
+
+			setSelection( [ 1, 0 ], [ 1, 0 ] );
+			doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' );
+			output( '<p>fozzzo</p><p>yyy<selection />bar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fozzzo</p><p><selection />bar</p>' );
+
+			setSelection( [ 0, 0 ], [ 0, 0 ] );
+			doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' );
+			output( '<p>xxx<selection />fozzzo</p><p>bar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p><selection />fozzzo</p><p>bar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<selection />o</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+
+		it( 'multiple remove and undo', () => {
+			input( '<p><selection />foo</p><p>bar</p>' );
+
+			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+			output( '<p><selection />o</p><p>bar</p>' );
+
+			setSelection( [ 1, 1 ], [ 1, 1 ] );
+			doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) );
+			output( '<p>o</p><p>b<selection /></p>' );
+
+			editor.execute( 'undo' );
+			// Here is an edge case that selection could be before or after `ar` but selection always ends up after.
+			output( '<p>o</p><p>bar<selection /></p>' );
+
+			editor.execute( 'undo' );
+			// As above.
+			output( '<p>fo<selection />o</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+
+		it( 'add and remove different parts and undo', () => {
+			input( '<p>fo<selection />o</p><p>bar</p>' );
+
+			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			output( '<p>fozzz<selection />o</p><p>bar</p>' );
+
+			setSelection( [ 1, 2 ], [ 1, 2 ] );
+			doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) );
+			output( '<p>fozzzo</p><p>b<selection />r</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fozzzo</p><p>ba<selection />r</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<selection />o</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+
+		//it( 'add and remove same part and undo', () => {
+		//	// This test case fails because some operations are transformed to NoOperations incorrectly.
+		//	input( '<p>fo<selection />o</p><p>bar</p>' );
+		//
+		//	doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+		//	output( '<p>fozzz<selection />o</p><p>bar</p>' );
+		//
+		//	doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) );
+		//	output( '<p>fo<selection />o</p><p>bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	output( '<p>fozzz<selection />o</p><p>bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	output( '<p>fo<selection />o</p><p>bar</p>' );
+		//
+		//	undoDisabled();
+		//} );
+	} );
+
+	describe( 'moving', () => {
+		//it( 'move same content twice then undo', () => {
+		//	// This test case fails because some operations are transformed to NoOperations incorrectly.
+		//	input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
+		//
+		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+		//	output( '<p>fz</p><p><selection>o</selection>bar</p>' );
+		//
+		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) );
+		//	output( '<p>fz<selection>o</selection></p><p>bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	output( '<p>fz</p><p><selection>o</selection>bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
+		//
+		//	undoDisabled();
+		//} );
+
+		it( 'move content and new parent then undo', () => {
+			input( '<p>f<selection>o</selection>z</p><p>bar</p>' );
+
+			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) );
+			output( '<p>fz</p><p><selection>o</selection>bar</p>' );
+
+			setSelection( [ 1 ], [ 2 ] );
+			doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+			output( '<selection><p>obar</p></selection><p>fz</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fz</p><selection><p>obar</p></selection>' );
+
+			editor.execute( 'undo' );
+			output( '<p>f<selection>o</selection>z</p><p>bar</p>' );
+
+			undoDisabled();
+		} );
+	} );
+
+	describe( 'attributes with other', () => {
+		it( 'attributes then insert inside then undo', () => {
+			input( '<p>fo<selection>ob</selection>ar</p>' );
+
+			doc.batch().setAttr( 'bold', true, doc.selection.getFirstRange() );
+			output( '<p>fo<selection><$text bold=true>ob</$text></selection>ar</p>' );
+
+			setSelection( [ 0, 3 ], [ 0, 3 ] );
+			doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' );
+			output( '<p>fo<$text bold=true>o</$text>zzz<selection bold=true /><$text bold=true>b</$text>ar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<$text bold=true>o<selection bold=true />b</$text>ar</p>' );
+
+			editor.execute( 'undo' );
+			output( '<p>fo<selection>ob</selection>ar</p>' );
+
+			undoDisabled();
+		} );
+	} );
+
+	describe( 'wrapping, unwrapping, merging, splitting', () => {
+		it( 'wrap and undo', () => {
+			input( 'fo<selection>zb</selection>ar' );
+
+			doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
+			output( 'fo<p><selection>zb</selection></p>ar' );
+
+			editor.execute( 'undo' );
+			output( 'fo<selection>zb</selection>ar' );
+
+			undoDisabled();
+		} );
+
+		//it( 'wrap, move and undo', () => {
+		//	input( 'fo<selection>zb</selection>ar' );
+		//
+		//	doc.batch().wrap( doc.selection.getFirstRange(), 'p' );
+		//	output( 'fo<p><selection>zb</selection></p>ar' );
+		//
+		//	setSelection( [ 2, 0 ], [ 2, 1 ] );
+		//	doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) );
+		//	output( '<selection>z</selection>fo<p>b</p>ar' );
+		//
+		//	editor.execute( 'undo' );
+		//	output( 'fo<p><selection>z</selection>b</p>ar' );
+		//
+		//	// This test case fails here for unknown reason, but "z" letter magically disappears.
+		//	// AssertionError: expected 'fo<selection>b</selection>ar' to equal 'fo<selection>zb</selection>ar'
+		//	editor.execute( 'undo' );
+		//	output( 'fo<selection>zb</selection>ar' );
+		//
+		//	undoDisabled();
+		//} );
+
+		it( 'unwrap and undo', () => {
+			input( '<p>foo<selection />bar</p>' );
+
+			doc.batch().unwrap( doc.selection.getFirstPosition().parent );
+			output( 'foo<selection />bar' );
+
+			editor.execute( 'undo' );
+			output( '<p>foo<selection />bar</p>' );
+
+			undoDisabled();
+		} );
+
+		//it( 'merge and undo', () => {
+		//	input( '<p>foo</p><p><selection />bar</p>' );
+		//
+		//	doc.batch().merge( new Position( root, [ 1 ] ) );
+		//	// This test fails here because selection is stuck with <p> element and ends up in graveyard.
+		//	// AssertionError: expected '<p>foobar</p>' to equal '<p>foo<selection />bar</p>'
+		//	output( '<p>foo<selection />bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	// This test fails because when selection is transformed it is first in empty <p> but when
+		//	// "bar" is inserted, it gets moved to the right.
+		//	// AssertionError: expected '<p>foo</p><p>bar<selection /></p>' to equal '<p>foo</p><p><selection />bar</p>'
+		//	output( '<p>foo</p><p><selection />bar</p>' );
+		//
+		//	undoDisabled();
+		//} );
+
+		//it( 'split and undo', () => {
+		//	input( '<p>foo<selection />bar</p>' );
+		//
+		//	doc.batch().split( doc.selection.getFirstPosition() );
+		//	// This test fails because selection ends up in wrong node after splitting.
+		//	// AssertionError: expected '<p>foo<selection /></p><p>bar</p>' to equal '<p>foo</p><p><selection />bar</p>'
+		//	output( '<p>foo</p><p><selection />bar</p>' );
+		//
+		//	editor.execute( 'undo' );
+		//	// This test fails because selection after transforming ends up after inserted test.
+		//	// AssertionError: expected '<p>foobar<selection /></p>' to equal '<p>foo<selection />bar</p>'
+		//	output( '<p>foo<selection />bar</p>' );
+		//
+		//	undoDisabled();
+		//} );
+	} );
+
+	describe( 'other edge cases', () => {
+		//it( 'deleteContents between two nodes', () => {
+		//	input( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
+		//
+		//	deleteContents( doc.batch(), doc.selection, { merge: true } );
+		//	output( '<p>fo<selection />ar</p>' );
+		//
+		//	// This test case fails because of OT problems.
+		//	// When the batch is undone, first MergeDelta is reversed to SplitDelta and it is undone.
+		//	// Then RemoveOperations are reversed to ReinsertOperation.
+		//	// Unfortunately, ReinsertOperation that inserts "o" points to the same position were split happened.
+		//	// Then, when ReinsertOperation is transformed by operations of SplitDelta, it ends up in wrong <p>.
+		//	editor.execute( 'undo' );
+		//	output( '<p>fo<selection>o</p><p>b</selection>ar</p>' );
+		//} );
+	} );
+} );

+ 8 - 15
packages/ckeditor5-undo/tests/undofeature.js → packages/ckeditor5-undo/tests/undoengine.js

@@ -3,29 +3,22 @@
  * For licensing, see LICENSE.md.
  * For licensing, see LICENSE.md.
  */
  */
 
 
-/* bender-tags: browser-only */
-
 'use strict';
 'use strict';
 
 
-import Editor from '/ckeditor5/editor.js';
-import ModelDocument from '/ckeditor5/engine/model/document.js';
+import ModelTestEditor from '/tests/ckeditor5/_utils/modeltesteditor.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import Position from '/ckeditor5/engine/model/position.js';
-import UndoFeature from '/ckeditor5/undo/undo.js';
+import UndoEngine from '/ckeditor5/undo/undoengine.js';
 
 
-let element, editor, undo, batch, doc, root;
+let editor, undo, batch, doc, root;
 
 
 beforeEach( () => {
 beforeEach( () => {
-	element = document.createElement( 'div' );
-	document.body.appendChild( element );
-
-	editor = new Editor( element );
+	editor = new ModelTestEditor();
 
 
-	doc = new ModelDocument();
-	editor.document = doc;
+	doc = editor.document;
 	batch = doc.batch();
 	batch = doc.batch();
-	root = doc.createRoot( 'root' );
+	root = doc.getRoot();
 
 
-	undo = new UndoFeature( editor );
+	undo = new UndoEngine( editor );
 	undo.init();
 	undo.init();
 } );
 } );
 
 
@@ -33,7 +26,7 @@ afterEach( () => {
 	undo.destroy();
 	undo.destroy();
 } );
 } );
 
 
-describe( 'UndoFeature', () => {
+describe( 'UndoEngine', () => {
 	it( 'should register undo command and redo command', () => {
 	it( 'should register undo command and redo command', () => {
 		expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
 		expect( editor.commands.get( 'undo' ) ).to.equal( undo._undoCommand );
 		expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );
 		expect( editor.commands.get( 'redo' ) ).to.equal( undo._redoCommand );

+ 14 - 0
packages/ckeditor5-undo/theme/icons/redo.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 3.8.2 (29753) - http://www.bohemiancoding.com/sketch -->
+    <title>redo</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Mockups" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="redo" fill="#454545">
+            <g id="undo">
+                <path d="M10.00002,15.99998 L11,12.0184937 C7.58334,12.0184937 2.38444,12.88666 1,16 C1,10.01667 7.60112,8.03985596 11,8.03985596 C10.99998,7.18907596 9.99999,4.71945 10,4 L18,10 L10.00002,15.99998 Z"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
packages/ckeditor5-undo/theme/icons/undo.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 3.8.2 (29753) - http://www.bohemiancoding.com/sketch -->
+    <title>undo</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Mockups" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="undo" fill="#454545">
+            <g>
+                <path d="M2,10 L9.99998,15.99998 L9,12.0184937 C12.41666,12.0184937 17.61556,12.88666 19,16 C19,10.01667 12.39888,8.03985596 9,8.03985596 C9.00002,7.18907596 10.00001,4.71945 10,4 L2,10 Z"></path>
+            </g>
+        </g>
+    </g>
+</svg>