Ver código fonte

Merge branch 'master' into t/3

Piotrek Koszuliński 8 anos atrás
pai
commit
305eee8db3

+ 43 - 84
packages/ckeditor5-typing/src/input.js

@@ -8,18 +8,18 @@
  */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import ChangeBuffer from './changebuffer';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewText from '@ckeditor/ckeditor5-engine/src/view/text';
 import diff from '@ckeditor/ckeditor5-utils/src/diff';
 import diffToChanges from '@ckeditor/ckeditor5-utils/src/difftochanges';
 import { getCode } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import InputCommand from './inputcommand';
 
 /**
  * Handles text input coming from the keyboard or other input methods.
  *
- * @extends core.Plugin
+ * @extends module:core/plugin~Plugin
  */
 export default class Input extends Plugin {
 	/**
@@ -28,19 +28,14 @@ export default class Input extends Plugin {
 	init() {
 		const editor = this.editor;
 		const editingView = editor.editing.view;
-
-		/**
-		 * Typing's change buffer used to group subsequent changes into batches.
-		 *
-		 * @protected
-		 * @member {typing.ChangeBuffer} #_buffer
-		 */
-		this._buffer = new ChangeBuffer( editor.document, editor.config.get( 'typing.undoStep' ) || 20 );
+		const inputCommand = new InputCommand( editor, editor.config.get( 'typing.undoStep' ) || 20 );
 
 		// TODO The above default configuration value should be defined using editor.config.define() once it's fixed.
 
+		editor.commands.set( 'input', inputCommand );
+
 		this.listenTo( editingView, 'keydown', ( evt, data ) => {
-			this._handleKeydown( data );
+			this._handleKeydown( data, inputCommand.buffer );
 		}, { priority: 'lowest' } );
 
 		this.listenTo( editingView, 'mutations', ( evt, mutations, viewSelection ) => {
@@ -49,16 +44,6 @@ export default class Input extends Plugin {
 	}
 
 	/**
-	 * @inheritDoc
-	 */
-	destroy() {
-		super.destroy();
-
-		this._buffer.destroy();
-		this._buffer = null;
-	}
-
-	/**
 	 * Handles the keydown event. We need to guess whether such keystroke is going to result
 	 * in typing. If so, then before character insertion happens, any selected content needs
 	 * to be deleted. Otherwise the default browser deletion mechanism would be
@@ -72,8 +57,9 @@ export default class Input extends Plugin {
 	 *
 	 * @private
 	 * @param {module:engine/view/observer/keyobserver~KeyEventData} evtData
+	 * @param {module:typing/changebuffer~ChangeBuffer} buffer
 	 */
-	_handleKeydown( evtData ) {
+	_handleKeydown( evtData, buffer ) {
 		const doc = this.editor.document;
 
 		if ( isSafeKeystroke( evtData ) || doc.selection.isCollapsed ) {
@@ -81,20 +67,19 @@ export default class Input extends Plugin {
 		}
 
 		doc.enqueueChanges( () => {
-			this.editor.data.deleteContent( doc.selection, this._buffer.batch );
+			this.editor.data.deleteContent( doc.selection, buffer.batch );
 		} );
 	}
 
 	/**
 	 * Handles DOM mutations.
 	 *
-	 * @param {Array.<engine.view.Document~MutatatedText|engine.view.Document~MutatatedChildren>} mutations
+	 * @private
+	 * @param {Array.<module:engine/view/document~MutatatedText|module:engine/view/document~MutatatedChildren>} mutations
+	 * @param {module:engine/view/selection~Selection|null} viewSelection
 	 */
 	_handleMutations( mutations, viewSelection ) {
-		const doc = this.editor.document;
-		const handler = new MutationHandler( this.editor.editing, this._buffer );
-
-		doc.enqueueChanges( () => handler.handle( mutations, viewSelection ) );
+		new MutationHandler( this.editor ).handle( mutations, viewSelection );
 	}
 }
 
@@ -107,36 +92,32 @@ class MutationHandler {
 	/**
 	 * Creates an instance of the mutation handler.
 	 *
-	 * @param {module:engine/controller/editingcontroller~EditingController} editing
-	 * @param {module:typing/changebuffer~ChangeBuffer} buffer
+	 * @param {module:core/editor/editor~Editor} editor
 	 */
-	constructor( editing, buffer ) {
+	constructor( editor ) {
 		/**
-		 * The editing controller.
+		 * Editor instance for which mutations are handled.
 		 *
-		 * @member {engine.controller.EditingController} #editing
+		 * @readonly
+		 * @member {module:core/editor/editor~Editor} #editor
 		 */
-		this.editing = editing;
+		this.editor = editor;
 
 		/**
-		 * The change buffer.
-		 *
-		 * @member {engine.controller.EditingController} #buffer
-		 */
-		this.buffer = buffer;
-
-		/**
-		 * The number of inserted characters which need to be fed to the {@link #buffer change buffer}.
+		 * The editing controller.
 		 *
-		 * @member {Number} #insertedCharacterCount
+		 * @readonly
+		 * @member {module:engine/controller/editingcontroller~EditingController} #editing
 		 */
-		this.insertedCharacterCount = 0;
+		this.editing = this.editor.editing;
 	}
 
 	/**
 	 * Handles given mutations.
 	 *
-	 * @param {Array.<engine.view.Document~MutatatedText|engine.view.Document~MutatatedChildren>} mutations
+	 * @param {Array.<module:engine/view/observer/mutationobserver~MutatedText|
+	 * module:engine/view/observer/mutationobserver~MutatatedChildren>} mutations
+	 * @param {module:engine/view/selection~Selection|null} viewSelection
 	 */
 	handle( mutations, viewSelection ) {
 		for ( let mutation of mutations ) {
@@ -144,8 +125,6 @@ class MutationHandler {
 			this._handleTextMutation( mutation, viewSelection );
 			this._handleTextNodeInsertion( mutation );
 		}
-
-		this.buffer.input( Math.max( this.insertedCharacterCount, 0 ) );
 	}
 
 	_handleTextMutation( mutation, viewSelection ) {
@@ -210,21 +189,14 @@ class MutationHandler {
 		// Get the position in view and model where the changes will happen.
 		const viewPos = new ViewPosition( mutation.node, firstChangeAt );
 		const modelPos = this.editing.mapper.toModelPosition( viewPos );
+		const removeRange = ModelRange.createFromPositionAndShift( modelPos, deletions );
+		const insertText = newText.substr( firstChangeAt, insertions );
 
-		// Remove appropriate number of characters from the model text node.
-		if ( deletions > 0 ) {
-			const removeRange = ModelRange.createFromPositionAndShift( modelPos, deletions );
-			this._remove( removeRange, deletions );
-		}
-
-		// Insert appropriate characters basing on `mutation.text`.
-		const insertedText = newText.substr( firstChangeAt, insertions );
-		this._insert( modelPos, insertedText );
-
-		// If there was `viewSelection` and it got correctly mapped, collapse selection at found model position.
-		if ( modelSelectionPosition ) {
-			this.editing.model.selection.collapse( modelSelectionPosition );
-		}
+		this.editor.execute( 'input', {
+			text: insertText,
+			range: removeRange,
+			resultPosition: modelSelectionPosition
+		} );
 	}
 
 	_handleTextNodeInsertion( mutation ) {
@@ -255,29 +227,16 @@ class MutationHandler {
 
 		const viewPos = new ViewPosition( mutation.node, change.index );
 		const modelPos = this.editing.mapper.toModelPosition( viewPos );
-		let insertedText = change.values[ 0 ].data;
-
-		// Replace &nbsp; inserted by the browser with normal space.
-		// See comment in `_handleTextMutation`.
-		// In this case we don't need to do this before `diff` because we diff whole nodes.
-		// Just change &nbsp; in case there are some.
-		insertedText = insertedText.replace( /\u00A0/g, ' ' );
-
-		this._insert( modelPos, insertedText );
-
-		this.editing.model.selection.collapse( modelPos.getShiftedBy( insertedText.length ) );
-	}
-
-	_insert( position, text ) {
-		this.buffer.batch.weakInsert( position, text );
-
-		this.insertedCharacterCount += text.length;
-	}
-
-	_remove( range, length ) {
-		this.buffer.batch.remove( range );
-
-		this.insertedCharacterCount -= length;
+		const insertedText = change.values[ 0 ].data;
+
+		this.editor.execute( 'input', {
+			// Replace &nbsp; inserted by the browser with normal space.
+			// See comment in `_handleTextMutation`.
+			// In this case we don't need to do this before `diff` because we diff whole nodes.
+			// Just change &nbsp; in case there are some.
+			text: insertedText.replace( /\u00A0/g, ' ' ),
+			range: new ModelRange( modelPos )
+		} );
 	}
 }
 

+ 97 - 0
packages/ckeditor5-typing/src/inputcommand.js

@@ -0,0 +1,97 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module typing/inputcommand
+ */
+
+import Command from '@ckeditor/ckeditor5-core/src/command/command';
+import ChangeBuffer from './changebuffer';
+
+/**
+ * The input command. Used by the {@link module:typing/input~Input input feature} to handle typing.
+ *
+ * @extends module:core/command/command~Command
+ */
+export default class InputCommand extends Command {
+	/**
+	 * Creates an instance of the command.
+	 *
+	 * @param {module:core/editor/editor~Editor} editor
+	 * @param {Number} undoStepSize The maximum number of atomic changes
+	 * which can be contained in one batch in the command buffer.
+	 */
+	constructor( editor, undoStepSize ) {
+		super( editor );
+
+		/**
+		 * Typing's change buffer used to group subsequent changes into batches.
+		 *
+		 * @readonly
+		 * @private
+		 * @member {module:typing/changebuffer~ChangeBuffer} #_buffer
+		 */
+		this._buffer = new ChangeBuffer( editor.document, undoStepSize );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	destroy() {
+		super.destroy();
+
+		this._buffer.destroy();
+		this._buffer = null;
+	}
+
+	/**
+	 * The current change buffer.
+	 *
+	 * @type {module:typing/changebuffer~ChangeBuffer}
+	 */
+	get buffer() {
+		return this._buffer;
+	}
+
+	/**
+	 * Executes the input command. It replaces the content within the given range with the given text.
+	 * Replacing is a two step process, first content within the range is removed and then new text is inserted
+	 * on the beginning of the range (which after removal is a collapsed range).
+	 *
+	 * @param {Object} [options] The command options.
+	 * @param {String} [options.text=''] Text to be inserted.
+	 * @param {module:engine/model/range~Range} [options.range] Range in which the text is inserted. Defaults
+	 * to the first range in the current selection.
+	 * @param {module:engine/model/position~Position} [options.resultPosition] Position at which the selection
+	 * should be placed after the insertion. If not specified, the selection will be placed right after
+	 * the inserted text.
+	 */
+	_doExecute( options = {} ) {
+		const doc = this.editor.document;
+		const text = options.text || '';
+		const textInsertions = text.length;
+		const range = options.range || doc.selection.getFirstRange();
+		const resultPosition = options.resultPosition;
+
+		doc.enqueueChanges( () => {
+			const isCollapsedRange = range.isCollapsed;
+
+			if ( !isCollapsedRange ) {
+				this._buffer.batch.remove( range );
+			}
+
+			this._buffer.batch.weakInsert( range.start, text );
+
+			if ( resultPosition ) {
+				this.editor.data.model.selection.collapse( resultPosition );
+			} else if ( isCollapsedRange ) {
+				// If range was collapsed just shift the selection by the number of inserted characters.
+				this.editor.data.model.selection.collapse( range.start.getShiftedBy( textInsertions ) );
+			}
+
+			this._buffer.input( textInsertions );
+		} );
+	}
+}

+ 81 - 40
packages/ckeditor5-typing/tests/input.js

@@ -4,8 +4,9 @@
  */
 
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
-import Input from '../src/input';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Input from '../src/input';
 
 import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
 import ModelRange from '@ckeditor/ckeditor5-engine/src/model/range';
@@ -25,6 +26,8 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils
 describe( 'Input feature', () => {
 	let editor, model, modelRoot, view, viewRoot, listenter;
 
+	testUtils.createSinonSandbox();
+
 	before( () => {
 		listenter = Object.create( EmitterMixin );
 
@@ -65,22 +68,6 @@ describe( 'Input feature', () => {
 		listenter.stopListening();
 	} );
 
-	it( 'has a buffer configured to default value of config.typing.undoStep', () => {
-		expect( editor.plugins.get( Input )._buffer ).to.have.property( 'limit', 20 );
-	} );
-
-	it( 'has a buffer configured to config.typing.undoStep', () => {
-		return VirtualTestEditor.create( {
-				plugins: [ Input ],
-				typing: {
-					undoStep: 5
-				}
-			} )
-			.then( editor => {
-				expect( editor.plugins.get( Input )._buffer ).to.have.property( 'limit', 5 );
-			} );
-	} );
-
 	describe( 'mutations handling', () => {
 		it( 'should handle text mutation', () => {
 			view.fire( 'mutations', [
@@ -222,14 +209,14 @@ describe( 'Input feature', () => {
 			expect( getViewData( view ) ).to.equal( '<p>foodar{}</p>' );
 		} );
 
-		it( 'should use up to one insert and remove operations', () => {
+		it( 'should use up to one insert and remove operations (spellchecker)', () => {
 			// This test case emulates spellchecker correction.
 
 			const viewSelection = new ViewSelection();
 			viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 6 );
 
-			sinon.spy( Batch.prototype, 'weakInsert' );
-			sinon.spy( Batch.prototype, 'remove' );
+			testUtils.sinon.spy( Batch.prototype, 'weakInsert' );
+			testUtils.sinon.spy( Batch.prototype, 'remove' );
 
 			view.fire( 'mutations',
 				[ {
@@ -245,6 +232,69 @@ describe( 'Input feature', () => {
 			expect( Batch.prototype.remove.calledOnce ).to.be.true;
 		} );
 
+		it( 'should place selection after when correcting to longer word (spellchecker)', () => {
+			// This test case emulates spellchecker correction.
+			editor.setData( '<p>Foo hous a</p>' );
+
+			const viewSelection = new ViewSelection();
+			viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
+
+			view.fire( 'mutations',
+				[ {
+					type: 'text',
+					oldText: 'Foo hous a',
+					newText: 'Foo house a',
+					node: viewRoot.getChild( 0 ).getChild( 0 )
+				} ],
+				viewSelection
+			);
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[] a</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p>Foo house{} a</p>' );
+		} );
+
+		it( 'should place selection after when correcting to shorter word (spellchecker)', () => {
+			// This test case emulates spellchecker correction.
+			editor.setData( '<p>Bar athat foo</p>' );
+
+			const viewSelection = new ViewSelection();
+			viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 8 );
+
+			view.fire( 'mutations',
+				[ {
+					type: 'text',
+					oldText: 'Bar athat foo',
+					newText: 'Bar that foo',
+					node: viewRoot.getChild( 0 ).getChild( 0 )
+				} ],
+				viewSelection
+			);
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Bar that[] foo</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p>Bar that{} foo</p>' );
+		} );
+
+		it( 'should place selection after when merging two words (spellchecker)', () => {
+			// This test case emulates spellchecker correction.
+			editor.setData( '<p>Foo hous e</p>' );
+
+			const viewSelection = new ViewSelection();
+			viewSelection.collapse( viewRoot.getChild( 0 ).getChild( 0 ), 9 );
+
+			view.fire( 'mutations',
+				[ {
+					type: 'text',
+					oldText: 'Foo hous e',
+					newText: 'Foo house',
+					node: viewRoot.getChild( 0 ).getChild( 0 )
+				} ],
+				viewSelection
+			);
+
+			expect( getModelData( model ) ).to.equal( '<paragraph>Foo house[]</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p>Foo house{}</p>' );
+		} );
+
 		it( 'should replace last &nbsp; with space', () => {
 			model.enqueueChanges( () => {
 				model.selection.setRanges( [
@@ -268,7 +318,7 @@ describe( 'Input feature', () => {
 		it( 'should replace first &nbsp; with space', () => {
 			model.enqueueChanges( () => {
 				model.selection.setRanges( [
-					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 0, modelRoot.getChild( 0 ), 0 )
 				] );
 			} );
 
@@ -281,11 +331,17 @@ describe( 'Input feature', () => {
 				}
 			] );
 
-			expect( getModelData( model ) ).to.equal( '<paragraph> foobar[]</paragraph>' );
-			expect( getViewData( view ) ).to.equal( '<p> foobar{}</p>' );
+			expect( getModelData( model ) ).to.equal( '<paragraph> []foobar</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p> {}foobar</p>' );
 		} );
 
 		it( 'should replace all &nbsp; with spaces', () => {
+			model.enqueueChanges( () => {
+				model.selection.setRanges( [
+					ModelRange.createFromParentsAndOffsets( modelRoot.getChild( 0 ), 6, modelRoot.getChild( 0 ), 6 )
+				] );
+			} );
+
 			view.fire( 'mutations', [
 				{
 					type: 'text',
@@ -295,9 +351,8 @@ describe( 'Input feature', () => {
 				}
 			] );
 
-			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar   baz</paragraph>' );
-			expect( getViewData( view ) ).to.equal( '<p>foo{}bar' +
-				'   baz</p>' );
+			expect( getModelData( model ) ).to.equal( '<paragraph>foobar   baz[]</paragraph>' );
+			expect( getViewData( view ) ).to.equal( '<p>foobar   baz{}</p>' );
 		} );
 	} );
 
@@ -380,19 +435,5 @@ describe( 'Input feature', () => {
 			expect( getModelData( model ) ).to.equal( '<paragraph>foo[]bar</paragraph>' );
 		} );
 	} );
-
-	describe( 'destroy', () => {
-		it( 'should destroy change buffer', () => {
-			const typing = new Input( new VirtualTestEditor() );
-			typing.init();
-
-			const destroy = typing._buffer.destroy = sinon.spy();
-
-			typing.destroy();
-
-			expect( destroy.calledOnce ).to.be.true;
-			expect( typing._buffer ).to.be.null;
-		} );
-	} );
 } );
 

+ 193 - 0
packages/ckeditor5-typing/tests/inputcommand.js

@@ -0,0 +1,193 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import InputCommand from '../src/inputcommand';
+import ChangeBuffer from '../src/changebuffer';
+import Input from '../src/input';
+
+describe( 'InputCommand', () => {
+	let editor, doc, buffer;
+
+	testUtils.createSinonSandbox();
+
+	before( () => {
+		return ModelTestEditor.create( )
+			.then( newEditor => {
+				editor = newEditor;
+				doc = editor.document;
+
+				const inputCommand = new InputCommand( editor, 20 );
+				editor.commands.set( 'input', inputCommand );
+
+				buffer = inputCommand.buffer;
+
+				doc.schema.registerItem( 'p', '$block' );
+				doc.schema.registerItem( 'h1', '$block' );
+			} );
+	} );
+
+	beforeEach( () => {
+		buffer.size = 0;
+	} );
+
+	describe( 'buffer', () => {
+		it( 'has buffer getter', () => {
+			expect( editor.commands.get( 'input' ).buffer ).to.be.an.instanceof( ChangeBuffer );
+		} );
+
+		it( 'has a buffer limit configured to default value of 20', () => {
+			expect( editor.commands.get( 'input' )._buffer ).to.have.property( 'limit', 20 );
+		} );
+
+		it( 'has a buffer configured to config.typing.undoStep', () => {
+			return VirtualTestEditor.create( {
+				plugins: [ Input ],
+				typing: {
+					undoStep: 5
+				}
+			} )
+				.then( editor => {
+					expect( editor.commands.get( 'input' )._buffer ).to.have.property( 'limit', 5 );
+				} );
+		} );
+	} );
+
+	describe( 'execute', () => {
+		it( 'uses enqueueChanges', () => {
+			setData( doc, '<p>foo[]bar</p>' );
+
+			const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
+
+			editor.execute( 'input', {
+				text: ''
+			} );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+
+		it( 'inserts text for collapsed range', () => {
+			setData( doc, '<p>foo[]</p>' );
+
+			editor.execute( 'input', {
+				text: 'bar',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobar[]</p>' );
+			expect( buffer.size ).to.be.equal( 3 );
+		} );
+
+		it( 'replaces text for range within single element on the beginning', () => {
+			setData( doc, '<p>[fooba]r</p>' );
+
+			editor.execute( 'input', {
+				text: 'rab',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>rab[]r</p>' );
+			expect( buffer.size ).to.be.equal( 3 );
+		} );
+
+		it( 'replaces text for range within single element in the middle', () => {
+			setData( doc, '<p>fo[oba]r</p>' );
+
+			editor.execute( 'input', {
+				text: 'bazz',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>fobazz[]r</p>' );
+			expect( buffer.size ).to.be.equal( 4 );
+		} );
+
+		it( 'replaces text for range within single element on the end', () => {
+			setData( doc, '<p>fooba[r]</p>' );
+
+			editor.execute( 'input', {
+				text: 'zzz',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobazzz[]</p>' );
+			expect( buffer.size ).to.be.equal( 3 );
+		} );
+
+		it( 'replaces text for range within multiple elements', () => {
+			setData( doc, '<h1>F[OO</h1><p>b]ar</p>' );
+
+			editor.execute( 'input', {
+				text: 'unny c',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<h1>Funny c[</h1><p>]ar</p>' );
+			expect( buffer.size ).to.be.equal( 6 );
+		} );
+
+		it( 'uses current selection when range is not given', () => {
+			setData( doc, '<p>foob[ar]</p>' );
+
+			editor.execute( 'input', {
+				text: 'az'
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>foobaz[]</p>' );
+			expect( buffer.size ).to.be.equal( 2 );
+		} );
+
+		it( 'only removes content when empty text given', () => {
+			setData( doc, '<p>[fo]obar</p>' );
+
+			editor.execute( 'input', {
+				text: '',
+				range: editor.document.selection.getFirstRange()
+			} );
+
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
+			expect( buffer.size ).to.be.equal( 0 );
+		} );
+
+		it( 'only removes content when no text given (with default non-collapsed range)', () => {
+			setData( doc, '<p>[fo]obar</p>' );
+
+			const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
+
+			editor.execute( 'input' );
+
+			expect( spy.callCount ).to.be.equal( 1 );
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>[]obar</p>' );
+			expect( buffer.size ).to.be.equal( 0 );
+		} );
+
+		it( 'does not change selection and content when no text given (with default collapsed range)', () => {
+			setData( doc, '<p>fo[]obar</p>' );
+
+			const spy = testUtils.sinon.spy( doc, 'enqueueChanges' );
+
+			editor.execute( 'input' );
+
+			expect( spy.callCount ).to.be.equal( 1 );
+			expect( getData( doc, { selection: true } ) ).to.be.equal( '<p>fo[]obar</p>' );
+			expect( buffer.size ).to.be.equal( 0 );
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'should destroy change buffer', () => {
+			const command = editor.commands.get( 'input' );
+			const destroy = command._buffer.destroy = testUtils.sinon.spy();
+
+			command.destroy();
+
+			expect( destroy.calledOnce ).to.be.true;
+			expect( command._buffer ).to.be.null;
+		} );
+	} );
+} );