Browse Source

Update merge cell command to support ranges not table selection plugin.

Maciej Gołaszewski 7 years ago
parent
commit
bb73f96b0d

+ 0 - 282
packages/ckeditor5-table/src/commands/mergecellcommand.js

@@ -1,282 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-/**
- * @module table/commands/mergecellcommand
- */
-
-import Command from '@ckeditor/ckeditor5-core/src/command';
-import Position from '@ckeditor/ckeditor5-engine/src/model/position';
-import Range from '@ckeditor/ckeditor5-engine/src/model/range';
-import TableWalker from '../tablewalker';
-import { findAncestor, updateNumericAttribute } from './utils';
-import TableUtils from '../tableutils';
-
-/**
- * The merge cell command.
- *
- * The command is registered by {@link module:table/tableediting~TableEditing} as `'mergeTableCellRight'`, `'mergeTableCellLeft'`,
- * `'mergeTableCellUp'` and `'mergeTableCellDown'` editor commands.
- *
- * To merge a table cell at the current selection with another cell, execute the command corresponding with the preferred direction.
- *
- * For example, to merge with a cell to the right:
- *
- *		editor.execute( 'mergeTableCellRight' );
- *
- * **Note**: If a table cell has a different [`rowspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-rowspan)
- * (for `'mergeTableCellRight'` and `'mergeTableCellLeft'`) or [`colspan`](https://www.w3.org/TR/html50/tabular-data.html#attr-tdth-colspan)
- * (for `'mergeTableCellUp'` and `'mergeTableCellDown'`), the command will be disabled.
- *
- * @extends module:core/command~Command
- */
-export default class MergeCellCommand extends Command {
-	/**
-	 * Creates a new `MergeCellCommand` instance.
-	 *
-	 * @param {module:core/editor/editor~Editor} editor The editor on which this command will be used.
-	 * @param {Object} options
-	 * @param {String} options.direction Indicates which cell to merge with the currently selected one.
-	 * Possible values are: `'left'`, `'right'`, `'up'` and `'down'`.
-	 */
-	constructor( editor, options ) {
-		super( editor );
-
-		/**
-		 * The direction that indicates which cell will be merged with the currently selected one.
-		 *
-		 * @readonly
-		 * @member {String} #direction
-		 */
-		this.direction = options.direction;
-
-		/**
-		 * Whether the merge is horizontal (left/right) or vertical (up/down).
-		 *
-		 * @readonly
-		 * @member {Boolean} #isHorizontal
-		 */
-		this.isHorizontal = this.direction == 'right' || this.direction == 'left';
-	}
-
-	/**
-	 * @inheritDoc
-	 */
-	refresh() {
-		const cellToMerge = this._getMergeableCell();
-
-		this.isEnabled = !!cellToMerge;
-		// In order to check if currently selected cell can be merged with one defined by #direction some computation are done beforehand.
-		// As such we can cache it as a command's value.
-		this.value = cellToMerge;
-	}
-
-	/**
-	 * Executes the command.
-	 *
-	 * Depending on the command's {@link #direction} value, it will merge the cell that is to the `'left'`, `'right'`, `'up'` or `'down'`.
-	 *
-	 * @fires execute
-	 */
-	execute() {
-		const model = this.editor.model;
-		const doc = model.document;
-		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
-		const cellToMerge = this.value;
-		const direction = this.direction;
-
-		model.change( writer => {
-			const isMergeNext = direction == 'right' || direction == 'down';
-
-			// The merge mechanism is always the same so sort cells to be merged.
-			const cellToExpand = isMergeNext ? tableCell : cellToMerge;
-			const cellToRemove = isMergeNext ? cellToMerge : tableCell;
-
-			// Cache the parent of cell to remove for later check.
-			const removedTableCellRow = cellToRemove.parent;
-
-			mergeTableCells( cellToRemove, cellToExpand, writer );
-
-			const spanAttribute = this.isHorizontal ? 'colspan' : 'rowspan';
-			const cellSpan = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );
-			const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );
-
-			// Update table cell span attribute and merge set selection on merged contents.
-			writer.setAttribute( spanAttribute, cellSpan + cellToMergeSpan, cellToExpand );
-			writer.setSelection( Range.createIn( cellToExpand ) );
-
-			// Remove empty row after merging.
-			if ( !removedTableCellRow.childCount ) {
-				removeEmptyRow( removedTableCellRow, writer );
-			}
-		} );
-	}
-
-	/**
-	 * Returns a cell that can be merged with the current cell depending on the command's direction.
-	 *
-	 * @returns {module:engine/model/element|undefined}
-	 * @private
-	 */
-	_getMergeableCell() {
-		const model = this.editor.model;
-		const doc = model.document;
-		const tableCell = findAncestor( 'tableCell', doc.selection.getFirstPosition() );
-
-		if ( !tableCell ) {
-			return;
-		}
-
-		const tableUtils = this.editor.plugins.get( TableUtils );
-
-		// First get the cell on proper direction.
-		const cellToMerge = this.isHorizontal ?
-			getHorizontalCell( tableCell, this.direction, tableUtils ) :
-			getVerticalCell( tableCell, this.direction );
-
-		if ( !cellToMerge ) {
-			return;
-		}
-
-		// If found check if the span perpendicular to merge direction is equal on both cells.
-		const spanAttribute = this.isHorizontal ? 'rowspan' : 'colspan';
-		const span = parseInt( tableCell.getAttribute( spanAttribute ) || 1 );
-
-		const cellToMergeSpan = parseInt( cellToMerge.getAttribute( spanAttribute ) || 1 );
-
-		if ( cellToMergeSpan === span ) {
-			return cellToMerge;
-		}
-	}
-}
-
-// Returns the cell that can be merged horizontally.
-//
-// @param {module:engine/model/element~Element} tableCell
-// @param {String} direction
-// @returns {module:engine/model/node~Node|null}
-function getHorizontalCell( tableCell, direction, tableUtils ) {
-	const horizontalCell = direction == 'right' ? tableCell.nextSibling : tableCell.previousSibling;
-
-	if ( !horizontalCell ) {
-		return;
-	}
-
-	// Sort cells:
-	const cellOnLeft = direction == 'right' ? tableCell : horizontalCell;
-	const cellOnRight = direction == 'right' ? horizontalCell : tableCell;
-
-	// Get their column indexes:
-	const { column: leftCellColumn } = tableUtils.getCellLocation( cellOnLeft );
-	const { column: rightCellColumn } = tableUtils.getCellLocation( cellOnRight );
-
-	const leftCellSpan = parseInt( cellOnLeft.getAttribute( 'colspan' ) || 1 );
-
-	// The cell on the right must have index that is distant to the cell on the left by the left cell's width (colspan).
-	const cellsAreTouching = leftCellColumn + leftCellSpan === rightCellColumn;
-
-	// If the right cell's column index is different it means that there are rowspanned cells between them.
-	return cellsAreTouching ? horizontalCell : undefined;
-}
-
-// Returns the cell that can be merged vertically.
-//
-// @param {module:engine/model/element~Element} tableCell
-// @param {String} direction
-// @returns {module:engine/model/node~Node|null}
-function getVerticalCell( tableCell, direction ) {
-	const tableRow = tableCell.parent;
-	const table = tableRow.parent;
-
-	const rowIndex = table.getChildIndex( tableRow );
-
-	// Don't search for mergeable cell if direction points out of the table.
-	if ( ( direction == 'down' && rowIndex === table.childCount - 1 ) || ( direction == 'up' && rowIndex === 0 ) ) {
-		return;
-	}
-
-	const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
-	const headingRows = table.getAttribute( 'headingRows' ) || 0;
-
-	const isMergeWithBodyCell = direction == 'down' && ( rowIndex + rowspan ) === headingRows;
-	const isMergeWithHeadCell = direction == 'up' && rowIndex === headingRows;
-
-	// Don't search for mergeable cell if direction points out of the current table section.
-	if ( headingRows && ( isMergeWithBodyCell || isMergeWithHeadCell ) ) {
-		return;
-	}
-
-	const currentCellRowSpan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
-	const rowOfCellToMerge = direction == 'down' ? rowIndex + currentCellRowSpan : rowIndex;
-
-	const tableMap = [ ...new TableWalker( table, { endRow: rowOfCellToMerge } ) ];
-
-	const currentCellData = tableMap.find( value => value.cell === tableCell );
-	const mergeColumn = currentCellData.column;
-
-	const cellToMergeData = tableMap.find( ( { row, rowspan, column } ) => {
-		if ( column !== mergeColumn ) {
-			return false;
-		}
-
-		if ( direction == 'down' ) {
-			// If merging a cell below the mergeRow is already calculated.
-			return row === rowOfCellToMerge;
-		} else {
-			// If merging a cell above calculate if it spans to mergeRow.
-			return rowOfCellToMerge === row + rowspan;
-		}
-	} );
-
-	return cellToMergeData && cellToMergeData.cell;
-}
-
-// Properly removes empty row from a table. Will update `rowspan` attribute of cells that overlaps removed row.
-//
-// @param {module:engine/model/element~Element} removedTableCellRow
-// @param {module:engine/model/writer~Writer} writer
-function removeEmptyRow( removedTableCellRow, writer ) {
-	const table = removedTableCellRow.parent;
-
-	const removedRowIndex = table.getChildIndex( removedTableCellRow );
-
-	for ( const { cell, row, rowspan } of new TableWalker( table, { endRow: removedRowIndex } ) ) {
-		const overlapsRemovedRow = row + rowspan - 1 >= removedRowIndex;
-
-		if ( overlapsRemovedRow ) {
-			updateNumericAttribute( 'rowspan', rowspan - 1, cell, writer );
-		}
-	}
-
-	writer.remove( removedTableCellRow );
-}
-
-// Merges two table cells - will ensure that after merging cells with empty paragraph the result table cell will only have one paragraph.
-// If one of the merged table cell is empty the merged table cell will have contents of the non-empty table cell.
-// If both are empty the merged table cell will have only one empty paragraph.
-//
-// @param {module:engine/model/element~Element} cellToRemove
-// @param {module:engine/model/element~Element} cellToExpand
-// @param {module:engine/model/writer~Writer} writer
-function mergeTableCells( cellToRemove, cellToExpand, writer ) {
-	if ( !isEmpty( cellToRemove ) ) {
-		if ( isEmpty( cellToExpand ) ) {
-			writer.remove( Range.createIn( cellToExpand ) );
-		}
-
-		writer.move( Range.createIn( cellToRemove ), Position.createAt( cellToExpand, 'end' ) );
-	}
-
-	// Remove merged table cell.
-	writer.remove( cellToRemove );
-}
-
-// Checks if passed table cell contains empty paragraph.
-//
-// @param {module:engine/model/element~Element} tableCell
-// @returns {Boolean}
-function isEmpty( tableCell ) {
-	return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
-}

+ 108 - 11
packages/ckeditor5-table/src/commands/mergecellscommand.js

@@ -11,9 +11,8 @@ import Command from '@ckeditor/ckeditor5-core/src/command';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 import TableWalker from '../tablewalker';
-import { updateNumericAttribute } from './utils';
+import { findAncestor, updateNumericAttribute } from './utils';
 import TableUtils from '../tableutils';
-import TableSelection from '../tableselection';
 
 /**
  * The merge cells command.
@@ -38,9 +37,7 @@ export default class MergeCellsCommand extends Command {
 	 * @inheritDoc
 	 */
 	refresh() {
-		const tableSelection = this.editor.plugins.get( TableSelection );
-
-		this.isEnabled = !!tableSelection.size && canMerge( Array.from( tableSelection.getSelection() ) );
+		this.isEnabled = canMergeCells( this.editor.model.document.selection, this.editor.plugins.get( TableUtils ) );
 	}
 
 	/**
@@ -53,13 +50,10 @@ export default class MergeCellsCommand extends Command {
 	execute() {
 		const model = this.editor.model;
 
-		const tableSelection = this.editor.plugins.get( TableSelection );
 		const tableUtils = this.editor.plugins.get( TableUtils );
 
 		model.change( writer => {
-			const selectedTableCells = [ ... tableSelection.getSelection() ];
-
-			tableSelection.clearSelection();
+			const selectedTableCells = [ ... this.editor.model.document.selection.getRanges() ].map( range => range.start.nodeAfter );
 
 			const firstTableCell = selectedTableCells.shift();
 
@@ -160,6 +154,109 @@ function isEmpty( tableCell ) {
 	return tableCell.childCount == 1 && tableCell.getChild( 0 ).is( 'paragraph' ) && tableCell.getChild( 0 ).isEmpty;
 }
 
-function canMerge() {
-	return true;
+// Check if selection contains mergeable cells.
+//
+// In a table below:
+//
+//   +---+---+---+---+
+//   | a | b | c | d |
+//   +---+---+---+   +
+//   | e     | f |   |
+//   +       +---+---+
+//   |       | g | h |
+//   +---+---+---+---+
+//
+// Valid selections are those which creates a solid rectangle (without gaps), such as:
+//   - a, b (two horizontal cells)
+//   - c, f (two vertical cells)
+//   - a, b, e (cell "e" spans over four cells)
+//   - c, d, f (cell d spans over cell in row below)
+//
+// While invalid selection would be:
+//   - a, c (cell "b" not selected creates a gap)
+//   - f, g, h (cell "d" spans over a cell from row of "f" cell - thus creates a gap)
+//
+// @param {module:engine/model/selection~Selection} selection
+// @param {module:table/tableUtils~TableUtils} tableUtils
+// @returns {boolean}
+function canMergeCells( selection, tableUtils ) {
+	// Collapsed selection or selection only one range can't contain mergeable table cells.
+	if ( selection.isCollapsed || selection.rangeCount < 2 ) {
+		return false;
+	}
+
+	// All cells must be inside the same table.
+	let firstRangeTable;
+
+	const tableCells = [];
+
+	for ( const range of selection.getRanges() ) {
+		// Selection ranges must be set on whole <tableCell> element.
+		if ( range.isCollapsed || !range.isFlat || !range.start.nodeAfter.is( 'tableCell' ) ) {
+			return false;
+		}
+
+		const parentTable = findAncestor( 'table', range.start );
+
+		if ( !firstRangeTable ) {
+			firstRangeTable = parentTable;
+		} else if ( firstRangeTable !== parentTable ) {
+			return false;
+		}
+
+		tableCells.push( range.start.nodeAfter );
+	}
+
+	// At this point selection contains ranges over table cells in the same table.
+	// The valid selection is a fully occupied rectangle composed of table cells.
+	// Below we calculate area of selected cells and the area of valid selection.
+	// The area of valid selection is defined by top-left and bottom-right cells.
+	const rows = new Set();
+	const columns = new Set();
+
+	let areaOfSelectedCells = 0;
+
+	for ( const tableCell of tableCells ) {
+		const { row, column } = tableUtils.getCellLocation( tableCell );
+		const rowspan = parseInt( tableCell.getAttribute( 'rowspan' ) || 1 );
+		const colspan = parseInt( tableCell.getAttribute( 'colspan' ) || 1 );
+
+		// Record row & column indexes of current cell.
+		rows.add( row );
+		columns.add( column );
+
+		// For cells that spans over multiple rows add also the last row that this cell spans over.
+		if ( rowspan > 1 ) {
+			rows.add( row + rowspan - 1 );
+		}
+
+		// For cells that spans over multiple columns add also the last column that this cell spans over.
+		if ( colspan > 1 ) {
+			columns.add( column + colspan - 1 );
+		}
+
+		areaOfSelectedCells += ( rowspan * colspan );
+	}
+
+	// We can only merge table cells that are in adjacent rows...
+	const areaOfValidSelection = getBiggestRectangleArea( rows, columns );
+
+	return areaOfValidSelection == areaOfSelectedCells;
+}
+
+// Calculates the area of a maximum rectangle that can span over provided row & column indexes.
+//
+// @param {Array.<Number>} rows
+// @param {Array.<Number>} columns
+// @returns {Number}
+function getBiggestRectangleArea( rows, columns ) {
+	const rowsIndexes = Array.from( rows.values() );
+	const columnIndexes = Array.from( columns.values() );
+
+	const lastRow = Math.max( ...rowsIndexes );
+	const firstRow = Math.min( ...rowsIndexes );
+	const lastColumn = Math.max( ...columnIndexes );
+	const firstColumn = Math.min( ...columnIndexes );
+
+	return ( lastRow - firstRow + 1 ) * ( lastColumn - firstColumn + 1 );
 }

+ 1 - 7
packages/ckeditor5-table/src/tableediting.js

@@ -25,11 +25,11 @@ import InsertTableCommand from './commands/inserttablecommand';
 import InsertRowCommand from './commands/insertrowcommand';
 import InsertColumnCommand from './commands/insertcolumncommand';
 import SplitCellCommand from './commands/splitcellcommand';
-import MergeCellCommand from './commands/mergecellcommand';
 import RemoveRowCommand from './commands/removerowcommand';
 import RemoveColumnCommand from './commands/removecolumncommand';
 import SetHeaderRowCommand from './commands/setheaderrowcommand';
 import SetHeaderColumnCommand from './commands/setheadercolumncommand';
+import MergeCellsCommand from './commands/mergecellscommand';
 import { findAncestor } from './commands/utils';
 import TableUtils from '../src/tableutils';
 
@@ -37,7 +37,6 @@ import injectTablePostFixer from './converters/table-post-fixer';
 import injectTableCellPostFixer from './converters/tablecell-post-fixer';
 
 import '../theme/tableediting.css';
-import MergeCellsCommand from './commands/mergecellscommand';
 
 /**
  * The table editing feature.
@@ -134,11 +133,6 @@ export default class TableEditing extends Plugin {
 		editor.commands.add( 'splitTableCellVertically', new SplitCellCommand( editor, { direction: 'vertically' } ) );
 		editor.commands.add( 'splitTableCellHorizontally', new SplitCellCommand( editor, { direction: 'horizontally' } ) );
 
-		editor.commands.add( 'mergeTableCellRight', new MergeCellCommand( editor, { direction: 'right' } ) );
-		editor.commands.add( 'mergeTableCellLeft', new MergeCellCommand( editor, { direction: 'left' } ) );
-		editor.commands.add( 'mergeTableCellDown', new MergeCellCommand( editor, { direction: 'down' } ) );
-		editor.commands.add( 'mergeTableCellUp', new MergeCellCommand( editor, { direction: 'up' } ) );
-
 		editor.commands.add( 'mergeTableCells', new MergeCellsCommand( editor ) );
 
 		editor.commands.add( 'setTableColumnHeader', new SetHeaderColumnCommand( editor ) );

+ 31 - 16
packages/ckeditor5-table/src/tableselection.js

@@ -7,13 +7,13 @@
  * @module table/tableediting
  */
 
-import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 
 import TableWalker from './tablewalker';
 import TableUtils from './tableutils';
 import { findAncestor } from './commands/utils';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
 
 export default class TableSelection extends Plugin {
 	/**
@@ -73,14 +73,16 @@ export default class TableSelection extends Plugin {
 				domEventData.preventDefault();
 
 				if ( wasOne ) {
-					editor.editing.view.change( writer => {
-						const viewElement = editor.editing.mapper.toViewElement( this._startElement );
-
-						writer.setSelection( ViewRange.createIn( viewElement ), {
-							fake: true,
-							label: 'fake selection over table cell'
-						} );
-					} );
+					// TODO:
+					// editor.editing.view.change( writer => {
+					// 	// TODO const viewElement = editor.editing.mapper.toViewElement( this._startElement );
+					//
+					// 	// Set selection to the first selected table cell.
+					// 	// writer.setSelection( ViewRange.createIn( viewElement ), {
+					// 	// 	fake: true,
+					// 	// 	label: 'fake selection over table cell'
+					// 	// } );
+					// } );
 				}
 
 				this.redrawSelection();
@@ -111,6 +113,9 @@ export default class TableSelection extends Plugin {
 		this._isSelecting = true;
 		this._startElement = tableCell;
 		this._endElement = tableCell;
+
+		// todo: stop rendering
+		this.editor.editing.view._renderer.renderSelection = false;
 	}
 
 	updateSelection( tableCell ) {
@@ -143,6 +148,7 @@ export default class TableSelection extends Plugin {
 		}
 
 		this._isSelecting = false;
+		this.editor.editing.view._renderer.renderSelection = true;
 	}
 
 	clearSelection() {
@@ -151,6 +157,8 @@ export default class TableSelection extends Plugin {
 		this._isSelecting = false;
 		this.clearPreviousSelection();
 		this._highlighted.clear();
+
+		this.editor.editing.view._renderer.renderSelection = true;
 	}
 
 	* getSelection() {
@@ -179,7 +187,12 @@ export default class TableSelection extends Plugin {
 	}
 
 	redrawSelection() {
-		const viewRanges = [];
+		const editor = this.editor;
+		const mapper = editor.editing.mapper;
+		const view = editor.editing.view;
+		const model = editor.model;
+
+		const modelRanges = [];
 
 		const selected = [ ...this.getSelection() ];
 		const previous = [ ...this._highlighted.values() ];
@@ -187,13 +200,18 @@ export default class TableSelection extends Plugin {
 		this._highlighted.clear();
 
 		for ( const tableCell of selected ) {
-			const viewElement = this.editor.editing.mapper.toViewElement( tableCell );
-			viewRanges.push( ViewRange.createOn( viewElement ) );
+			const viewElement = mapper.toViewElement( tableCell );
+			modelRanges.push( Range.createOn( tableCell ) );
 
 			this._highlighted.add( viewElement );
 		}
 
-		this.editor.editing.view.change( writer => {
+		// Update model's selection
+		model.change( writer => {
+			writer.setSelection( modelRanges );
+		} );
+
+		view.change( writer => {
 			for ( const previouslyHighlighted of previous ) {
 				if ( !selected.includes( previouslyHighlighted ) ) {
 					writer.removeClass( 'selected', previouslyHighlighted );
@@ -203,9 +221,6 @@ export default class TableSelection extends Plugin {
 			for ( const currently of this._highlighted ) {
 				writer.addClass( 'selected', currently );
 			}
-
-			// TODO works on FF ony... :|
-			writer.setSelection( viewRanges, { fake: true, label: 'fake selection over table cell' } );
 		} );
 	}
 

+ 0 - 887
packages/ckeditor5-table/tests/commands/mergecellcommand.js

@@ -1,887 +0,0 @@
-/**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
-
-import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
-import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
-
-import MergeCellCommand from '../../src/commands/mergecellcommand';
-import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
-import TableUtils from '../../src/tableutils';
-
-describe( 'MergeCellCommand', () => {
-	let editor, model, command, root;
-
-	beforeEach( () => {
-		return ModelTestEditor
-			.create( {
-				plugins: [ TableUtils ]
-			} )
-			.then( newEditor => {
-				editor = newEditor;
-				model = editor.model;
-				root = model.document.getRoot( 'main' );
-
-				defaultSchema( model.schema );
-				defaultConversion( editor.conversion );
-			} );
-	} );
-
-	afterEach( () => {
-		return editor.destroy();
-	} );
-
-	describe( 'direction=right', () => {
-		beforeEach( () => {
-			command = new MergeCellCommand( editor, { direction: 'right' } );
-		} );
-
-		describe( 'isEnabled', () => {
-			it( 'should be true if in cell that has sibling on the right', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if last cell of a row', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true if in a cell that has sibling on the right with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 2, contents: '01' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false when next cell is rowspanned', () => {
-				setData( model, modelTable( [
-					[ '00', { rowspan: 3, contents: '01' }, '02' ],
-					[ '10[]', '12' ],
-					[ '20', '22' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true when current cell is colspanned', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00[]' }, '02' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-		} );
-
-		describe( 'value', () => {
-			it( 'should be set to mergeable sibling if in cell that has sibling on the right', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) );
-			} );
-
-			it( 'should be set to mergeable sibling if in cell that has sibling on the right (selection in block content)', () => {
-				setData( model, modelTable( [
-					[ '00', '<paragraph>[]01</paragraph>', '02' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 2 ] ) );
-			} );
-
-			it( 'should be undefined if last cell of a row', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be set to mergeable sibling if in a cell that has sibling on the right with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 2, contents: '01' } ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) );
-			} );
-
-			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, { rowspan: 3, contents: '01' } ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.value ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'execute()', () => {
-			it( 'should merge table cells', () => {
-				setData( model, modelTable( [
-					[ '[]00', '01' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[00</paragraph><paragraph>01]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single empty paragraph if both cells are empty', () => {
-				setData( model, modelTable( [
-					[ '[]', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (other cell is empty)', () => {
-				setData( model, modelTable( [
-					[ 'foo[]', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (selection cell is empty)', () => {
-				setData( model, modelTable( [
-					[ '[]', 'foo' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should not merge other empty blocks to single block', () => {
-				model.schema.register( 'block', {
-					allowWhere: '$block',
-					allowContentOf: '$block',
-					isBlock: true
-				} );
-
-				setData( model, modelTable( [
-					[ '<block>[]</block>', '<block></block>' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<block>[</block><block>]</block>' } ]
-				] ) );
-			} );
-		} );
-	} );
-
-	describe( 'direction=left', () => {
-		beforeEach( () => {
-			command = new MergeCellCommand( editor, { direction: 'left' } );
-		} );
-
-		describe( 'isEnabled', () => {
-			it( 'should be true if in cell that has sibling on the left', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if first cell of a row', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true if in a cell that has sibling on the left with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 2, contents: '01[]' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in a cell that has sibling but with different rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false when next cell is rowspanned', () => {
-				setData( model, modelTable( [
-					[ '00', { rowspan: 3, contents: '01' }, '02' ],
-					[ '10', '12[]' ],
-					[ '20', '22' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true when mergeable cell is colspanned', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02[]' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-		} );
-
-		describe( 'value', () => {
-			it( 'should be set to mergeable sibling if in cell that has sibling on the left', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
-			} );
-
-			it( 'should be set to mergeable sibling if in cell that has sibling on the left (selection in block content)', () => {
-				setData( model, modelTable( [
-					[ '00', '<paragraph>01[]</paragraph>', '02' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
-			} );
-
-			it( 'should be undefined if first cell of a row', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be set to mergeable sibling if in a cell that has sibling on the left with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 2, contents: '01[]' } ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
-			} );
-
-			it( 'should be undefined if in a cell that has sibling but with different rowspan', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, { rowspan: 3, contents: '01[]' } ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.value ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'execute()', () => {
-			it( 'should merge table cells', () => {
-				setData( model, modelTable( [
-					[ '00', '[]01' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[00</paragraph><paragraph>01]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single empty paragraph if both cells are empty', () => {
-				setData( model, modelTable( [
-					[ '', '[]' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (other cell is empty)', () => {
-				setData( model, modelTable( [
-					[ '', 'foo[]' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (selection cell is empty)', () => {
-				setData( model, modelTable( [
-					[ 'foo', '[]' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<paragraph>[foo]</paragraph>' } ]
-				] ) );
-			} );
-
-			it( 'should not merge other empty blocks to single block', () => {
-				model.schema.register( 'block', {
-					allowWhere: '$block',
-					allowContentOf: '$block',
-					isBlock: true
-				} );
-
-				setData( model, modelTable( [
-					[ '<block></block>', '<block>[]</block>' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { colspan: 2, contents: '<block>[</block><block>]</block>' } ]
-				] ) );
-			} );
-		} );
-	} );
-
-	describe( 'direction=down', () => {
-		beforeEach( () => {
-			command = new MergeCellCommand( editor, { direction: 'down' } );
-		} );
-
-		describe( 'isEnabled', () => {
-			it( 'should be true if in cell that has mergeable cell in next row', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ],
-					[ '10', '11' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in last row', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10[]', '11' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true if in a cell that has mergeable cell with the same colspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00[]' }, '02' ],
-					[ { colspan: 2, contents: '01' }, '12' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in a cell that potential mergeable cell has different colspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00[]' }, '02' ],
-					[ { colspan: 3, contents: '01' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false if mergeable cell is in other table section then current cell', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ],
-					[ '10', '11' ]
-				], { headingRows: 1 } ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-		} );
-
-		describe( 'value', () => {
-			it( 'should be set to mergeable cell', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ],
-					[ '10', '11' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 1 ] ) );
-			} );
-
-			it( 'should be set to mergeable cell (selection in block content)', () => {
-				setData( model, modelTable( [
-					[ '00' ],
-					[ '<paragraph>10[]</paragraph>' ],
-					[ '20' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 2, 0 ] ) );
-			} );
-
-			it( 'should be undefined if in last row', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10[]', '11' ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be set to mergeable cell with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00[]' }, '02' ],
-					[ { colspan: 2, contents: '01' }, '12' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 0 ] ) );
-			} );
-
-			it( 'should be undefined if in a cell that potential mergeable cell has different rowspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00[]' }, '02' ],
-					[ { colspan: 3, contents: '01' } ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be undefined if mergable cell is in other table section', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00[]' }, '02' ],
-					[ '12' ],
-					[ '21', '22' ]
-				], { headingRows: 2 } ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.value ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'execute()', () => {
-			it( 'should merge table cells', () => {
-				setData( model, modelTable( [
-					[ '00', '01[]' ],
-					[ '10', '11' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', { rowspan: 2, contents: '<paragraph>[01</paragraph><paragraph>11]</paragraph>' } ],
-					[ '10' ]
-				] ) );
-			} );
-
-			it( 'should result in single empty paragraph if both cells are empty', () => {
-				setData( model, modelTable( [
-					[ '[]', '' ],
-					[ '', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (other cell is empty)', () => {
-				setData( model, modelTable( [
-					[ 'foo[]', '' ],
-					[ '', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[foo]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (selection cell is empty)', () => {
-				setData( model, modelTable( [
-					[ '[]', '' ],
-					[ 'foo', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[foo]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should not merge other empty blocks to single block', () => {
-				model.schema.register( 'block', {
-					allowWhere: '$block',
-					allowContentOf: '$block',
-					isBlock: true
-				} );
-
-				setData( model, modelTable( [
-					[ '<block>[]</block>', '' ],
-					[ '<block></block>', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '<block>[</block><block>]</block>' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should remove empty row if merging table cells ', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01[]', { rowspan: 3, contents: '02' } ],
-					[ '11' ],
-					[ '20', '21' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>', { rowspan: 2, contents: '02' } ],
-					[ '20', '21' ]
-				] ) );
-			} );
-
-			it( 'should not reduce rowspan on cells above removed empty row when merging table cells ', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
-					[ '11', '12' ],
-					[ { rowspan: 2, contents: '20' }, '21[]', { rowspan: 3, contents: '22' } ],
-					[ '31' ],
-					[ '40', '41' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
-					[ '11', '12' ],
-					[ '20', '<paragraph>[21</paragraph><paragraph>31]</paragraph>', { rowspan: 2, contents: '22' } ],
-					[ '40', '41' ]
-				] ) );
-			} );
-		} );
-	} );
-
-	describe( 'direction=up', () => {
-		beforeEach( () => {
-			command = new MergeCellCommand( editor, { direction: 'up' } );
-		} );
-
-		describe( 'isEnabled', () => {
-			it( 'should be true if in cell that has mergeable cell in previous row', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10', '11[]' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in first row', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ],
-					[ '10', '11' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be true if in a cell that has mergeable cell with the same colspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02' ],
-					[ { colspan: 2, contents: '01[]' }, '12' ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.true;
-			} );
-
-			it( 'should be false if in a cell that potential mergeable cell has different colspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02' ],
-					[ { colspan: 3, contents: '01[]' } ]
-				] ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-
-			it( 'should be false if mergeable cell is in other table section then current cell', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10[]', '11' ]
-				], { headingRows: 1 } ) );
-
-				expect( command.isEnabled ).to.be.false;
-			} );
-		} );
-
-		describe( 'value', () => {
-			it( 'should be set to mergeable cell', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10', '11[]' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 1 ] ) );
-			} );
-
-			it( 'should be set to mergeable cell (selection in block content)', () => {
-				setData( model, modelTable( [
-					[ '00' ],
-					[ '<paragraph>10[]</paragraph>' ],
-					[ '20' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
-			} );
-
-			it( 'should be undefined if in first row', () => {
-				setData( model, modelTable( [
-					[ '00[]', '01' ],
-					[ '10', '11' ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be set to mergeable cell with the same rowspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02' ],
-					[ { colspan: 2, contents: '01[]' }, '12' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 0, 0 ] ) );
-			} );
-
-			it( 'should be set to mergeable cell in rows with spanned cells', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 3, contents: '00' }, '11', '12', '13' ],
-					[ { rowspan: 2, contents: '21' }, '22', '23' ],
-					[ '32', { rowspan: 2, contents: '33[]' } ],
-					[ { colspan: 2, contents: '40' }, '42' ]
-				] ) );
-
-				expect( command.value ).to.equal( root.getNodeByPath( [ 0, 1, 2 ] ) );
-			} );
-
-			it( 'should be undefined if in a cell that potential mergeable cell has different rowspan', () => {
-				setData( model, modelTable( [
-					[ { colspan: 2, contents: '00' }, '02' ],
-					[ { colspan: 3, contents: '01[]' } ]
-				] ) );
-
-				expect( command.value ).to.be.undefined;
-			} );
-
-			it( 'should be undefined if not in a cell', () => {
-				setData( model, '<paragraph>11[]</paragraph>' );
-
-				expect( command.value ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'execute()', () => {
-			it( 'should merge table cells', () => {
-				setData( model, modelTable( [
-					[ '00', '01' ],
-					[ '10', '[]11' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', { rowspan: 2, contents: '<paragraph>[01</paragraph><paragraph>11]</paragraph>' } ],
-					[ '10' ]
-				] ) );
-			} );
-
-			it( 'should result in single empty paragraph if both cells are empty', () => {
-				setData( model, modelTable( [
-					[ '', '' ],
-					[ '[]', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (other cell is empty)', () => {
-				setData( model, modelTable( [
-					[ '', '' ],
-					[ 'foo[]', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[foo]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should result in single paragraph (selection cell is empty)', () => {
-				setData( model, modelTable( [
-					[ 'foo', '' ],
-					[ '[]', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '[foo]' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should not merge other empty blocks to single block', () => {
-				model.schema.register( 'block', {
-					allowWhere: '$block',
-					allowContentOf: '$block',
-					isBlock: true
-				} );
-
-				setData( model, modelTable( [
-					[ '<block></block>', '' ],
-					[ '<block>[]</block>', '' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '<block>[</block><block>]</block>' }, '' ],
-					[ '' ]
-				] ) );
-			} );
-
-			it( 'should properly merge cells in rows with spaned cells', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 3, contents: '00' }, '11', '12', '13' ],
-					[ { rowspan: 2, contents: '21' }, '22', '23' ],
-					[ '32', { rowspan: 2, contents: '33[]' } ],
-					[ { colspan: 2, contents: '40' }, '42' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 3, contents: '00' }, '11', '12', '13' ],
-					[
-						{ rowspan: 2, contents: '21' },
-						'22',
-						{ rowspan: 3, contents: '<paragraph>[23</paragraph><paragraph>33]</paragraph>' }
-					],
-					[ '32' ],
-					[ { colspan: 2, contents: '40' }, '42' ]
-				] ) );
-			} );
-
-			it( 'should remove empty row if merging table cells ', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' } ],
-					[ '11[]' ],
-					[ '20', '21' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ '00', '<paragraph>[01</paragraph><paragraph>11]</paragraph>', { rowspan: 2, contents: '02' } ],
-					[ '20', '21' ]
-				] ) );
-			} );
-
-			it( 'should not reduce rowspan on cells above removed empty row when merging table cells ', () => {
-				setData( model, modelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
-					[ '11', '12' ],
-					[ { rowspan: 2, contents: '20' }, '21', { rowspan: 3, contents: '22' } ],
-					[ '31[]' ],
-					[ '40', '41' ]
-				] ) );
-
-				command.execute();
-
-				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
-					[ { rowspan: 2, contents: '00' }, '01', '02' ],
-					[ '11', '12' ],
-					[ '20', '<paragraph>[21</paragraph><paragraph>31]</paragraph>', { rowspan: 2, contents: '22' } ],
-					[ '40', '41' ]
-				] ) );
-			} );
-		} );
-	} );
-} );

+ 421 - 0
packages/ckeditor5-table/tests/commands/mergecellscommand.js

@@ -0,0 +1,421 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor';
+import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import MergeCellsCommand from '../../src/commands/mergecellscommand';
+import { defaultConversion, defaultSchema, formatTable, formattedModelTable, modelTable } from '../_utils/utils';
+import TableUtils from '../../src/tableutils';
+import TableSelection from '../../src/tableselection';
+import Range from '../../../ckeditor5-engine/src/model/range';
+
+describe( 'MergeCellsCommand', () => {
+	let editor, model, command, root;
+
+	beforeEach( () => {
+		return ModelTestEditor
+			.create( {
+				plugins: [ TableUtils, TableSelection ]
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = editor.model;
+				root = model.document.getRoot( 'main' );
+
+				command = new MergeCellsCommand( editor );
+
+				defaultSchema( model.schema );
+				defaultConversion( editor.conversion );
+			} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy();
+	} );
+
+	describe( 'isEnabled', () => {
+		it( 'should be false if collapsed selection in table cell', () => {
+			setData( model, modelTable( [
+				[ '00[]', '01' ]
+			] ) );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if only one table cell is selected', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ] ] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be true if at least two adjacent table cells are selected', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if many table cells are selected', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02', '03' ],
+				[ '10', '11', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 1 ], [ 0, 0, 2 ],
+				[ 0, 1, 1 ], [ 0, 1, 2 ],
+				[ 0, 2, 1 ], [ 0, 2, 2 ],
+				[ 0, 3, 1 ], [ 0, 3, 2 ]
+			] );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false if at least one table cell is not selected from an area', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02', '03' ],
+				[ '10', '11', '12', '13' ],
+				[ '20', '21', '22', '23' ],
+				[ '30', '31', '32', '33' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 1 ], [ 0, 0, 2 ],
+				[ 0, 1, 2 ], // one table cell not selected from this row
+				[ 0, 2, 1 ], [ 0, 2, 2 ],
+				[ 0, 3, 1 ], [ 0, 3, 2 ]
+			] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if table cells are not in adjacent rows', () => {
+			setData( model, modelTable( [
+				[ '00', '01' ],
+				[ '10', '11' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 1, 0 ],
+				[ 0, 0, 1 ]
+			] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if table cells are not in adjacent columns', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 2 ] ] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if any range is collapsed in selection', () => {
+			setData( model, modelTable( [
+				[ '00', '01', '02' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0, 0, 0 ], // The "00" text node
+				[ 0, 0, 1 ]
+			] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if any ranges are on different tables', () => {
+			setData( model,
+				modelTable( [ [ '00', '01' ] ] ) +
+				modelTable( [ [ 'aa', 'ab' ] ] )
+			);
+
+			selectNodes( [
+				[ 0, 0, 0 ], // first table
+				[ 1, 0, 1 ] // second table
+			] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be false if any table cell with colspan attribute extends over selection area', () => {
+			setData( model, modelTable( [
+				[ '00', { colspan: 2, contents: '01' } ],
+				[ '10', '11', '12' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ],
+				[ 0, 1, 0 ], [ 0, 1, 1 ]
+			] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be true if none table cell with colspan attribute extends over selection area', () => {
+			setData( model, modelTable( [
+				[ '00', { colspan: 2, contents: '01' } ],
+				[ '10', '11', '12' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ],
+				[ 0, 1, 0 ], [ 0, 1, 1 ],
+				[ 0, 1, 2 ]
+			] );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be true if first table cell is inside selection area', () => {
+			setData( model, modelTable( [
+				[ { colspan: 2, rowspan: 2, contents: '00' }, '02', '03' ],
+				[ '12', '13' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ],
+				[ 0, 1, 0 ]
+			] );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false if any table cell with rowspan attribute extends over selection area', () => {
+			setData( model, modelTable( [
+				[ '00', { rowspan: 2, contents: '01' } ],
+				[ '10' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+
+		it( 'should be true if none table cell with rowspan attribute extends over selection area', () => {
+			setData( model, modelTable( [
+				[ '00', { rowspan: 2, contents: '01' } ],
+				[ '10' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ],
+				[ 0, 1, 0 ]
+			] );
+
+			expect( command.isEnabled ).to.be.true;
+		} );
+
+		it( 'should be false if not in a cell', () => {
+			setData( model, '<paragraph>11[]</paragraph>' );
+
+			expect( command.isEnabled ).to.be.false;
+		} );
+	} );
+
+	describe( 'execute()', () => {
+		it( 'should merge table cells', () => {
+			setData( model, modelTable( [
+				[ '[]00', '01' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ]
+			] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 2, contents: '[<paragraph>00</paragraph><paragraph>01</paragraph>]' } ]
+			] ) );
+		} );
+
+		it( 'should merge table cells - extend colspan attribute', () => {
+			setData( model, modelTable( [
+				[ { colspan: 2, contents: '00' }, '02', '03' ],
+				[ '10', '11', '12', '13' ]
+			] ) );
+
+			selectNodes( [
+				[ 0, 0, 0 ], [ 0, 0, 1 ],
+				[ 0, 1, 0 ], [ 0, 1, 1 ], [ 0, 1, 2 ]
+			] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ {
+					colspan: 3,
+					rowspan: 2,
+					contents: '[<paragraph>00</paragraph>' +
+						'<paragraph>02</paragraph>' +
+						'<paragraph>10</paragraph>' +
+						'<paragraph>11</paragraph>' +
+						'<paragraph>12</paragraph>]'
+				}, '03' ],
+				[ '13' ]
+			] ) );
+		} );
+
+		it( 'should merge to a single paragraph - every cell is empty', () => {
+			setData( model, modelTable( [
+				[ '[]', '' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 2, contents: '[<paragraph></paragraph>]' } ]
+			] ) );
+		} );
+
+		it( 'should merge to a single paragraph - merged cell is empty', () => {
+			setData( model, modelTable( [
+				[ 'foo', '' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 2, contents: '[<paragraph>foo</paragraph>]' } ]
+			] ) );
+		} );
+
+		it( 'should merge to a single paragraph - cell to which others are merged is empty', () => {
+			setData( model, modelTable( [
+				[ '', 'foo' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 2, contents: '[<paragraph>foo</paragraph>]' } ]
+			] ) );
+		} );
+
+		it( 'should not merge empty blocks other then <paragraph> to a single block', () => {
+			model.schema.register( 'block', {
+				allowWhere: '$block',
+				allowContentOf: '$block',
+				isBlock: true
+			} );
+
+			setData( model, modelTable( [
+				[ '<block>[]</block>', '<block></block>' ]
+			] ) );
+
+			selectNodes( [ [ 0, 0, 0 ], [ 0, 0, 1 ] ] );
+
+			command.execute();
+
+			expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+				[ { colspan: 2, contents: '[<block></block><block></block>]' } ]
+			] ) );
+		} );
+
+		describe( 'removing empty row', () => {
+			it( 'should remove empty row if merging all table cells from that row', () => {
+				setData( model, modelTable( [
+					[ '00' ],
+					[ '10' ],
+					[ '20' ]
+				] ) );
+
+				selectNodes( [
+					[ 0, 0, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 2, 0 ]
+				] );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[
+						'[<paragraph>00</paragraph><paragraph>10</paragraph><paragraph>20</paragraph>]'
+					]
+				] ) );
+			} );
+
+			it( 'should decrease rowspan if cell overlaps removed row', () => {
+				setData( model, modelTable( [
+					[ '00', { rowspan: 2, contents: '01' }, { rowspan: 3, contents: '02' } ],
+					[ '10' ],
+					[ '20', '21' ]
+				] ) );
+
+				selectNodes( [
+					[ 0, 0, 0 ],
+					[ 0, 1, 0 ],
+					[ 0, 2, 0 ]
+				] );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[
+						{ rowspan: 2, contents: '[<paragraph>00</paragraph><paragraph>10</paragraph><paragraph>20</paragraph>]' },
+						'01',
+						{ rowspan: 2, contents: '02' }
+					],
+					[ '21' ]
+				] ) );
+			} );
+
+			it( 'should not decrease rowspan if cell from previous row does not overlaps removed row', () => {
+				setData( model, modelTable( [
+					[ '00', { rowspan: 2, contents: '01' } ],
+					[ '10' ],
+					[ '20', '21' ],
+					[ '30', '31' ]
+				] ) );
+
+				selectNodes( [
+					[ 0, 2, 0 ], [ 0, 2, 1 ],
+					[ 0, 3, 0 ], [ 0, 3, 1 ]
+				] );
+
+				command.execute();
+
+				expect( formatTable( getData( model ) ) ).to.equal( formattedModelTable( [
+					[ '00', { rowspan: 2, contents: '01' } ],
+					[ '10' ],
+					[
+						{
+							colspan: 2,
+							contents: '[<paragraph>20</paragraph><paragraph>21</paragraph>' +
+								'<paragraph>30</paragraph><paragraph>31</paragraph>]'
+						}
+					]
+				] ) );
+			} );
+		} );
+	} );
+
+	function selectNodes( paths ) {
+		const ranges = paths.map( path => Range.createOn( root.getNodeByPath( path ) ) );
+
+		model.change( writer => {
+			writer.setSelection( ranges );
+		} );
+	}
+} );

+ 11 - 23
packages/ckeditor5-table/tests/tableediting.js

@@ -17,7 +17,7 @@ import InsertColumnCommand from '../src/commands/insertcolumncommand';
 import RemoveRowCommand from '../src/commands/removerowcommand';
 import RemoveColumnCommand from '../src/commands/removecolumncommand';
 import SplitCellCommand from '../src/commands/splitcellcommand';
-import MergeCellCommand from '../src/commands/mergecellcommand';
+import MergeCellsCommand from '../src/commands/mergecellscommand';
 import SetHeaderRowCommand from '../src/commands/setheaderrowcommand';
 import SetHeaderColumnCommand from '../src/commands/setheadercolumncommand';
 import TableSelection from '../src/tableselection';
@@ -113,20 +113,8 @@ describe( 'TableEditing', () => {
 		expect( editor.commands.get( 'splitTableCellHorizontally' ) ).to.be.instanceOf( SplitCellCommand );
 	} );
 
-	it( 'adds mergeCellRight command', () => {
-		expect( editor.commands.get( 'mergeTableCellRight' ) ).to.be.instanceOf( MergeCellCommand );
-	} );
-
-	it( 'adds mergeCellLeft command', () => {
-		expect( editor.commands.get( 'mergeTableCellLeft' ) ).to.be.instanceOf( MergeCellCommand );
-	} );
-
-	it( 'adds mergeCellDown command', () => {
-		expect( editor.commands.get( 'mergeTableCellDown' ) ).to.be.instanceOf( MergeCellCommand );
-	} );
-
-	it( 'adds mergeCellUp command', () => {
-		expect( editor.commands.get( 'mergeTableCellUp' ) ).to.be.instanceOf( MergeCellCommand );
+	it( 'adds mergeTableCells command', () => {
+		expect( editor.commands.get( 'mergeTableCells' ) ).to.be.instanceOf( MergeCellsCommand );
 	} );
 
 	it( 'adds setColumnHeader command', () => {
@@ -256,7 +244,7 @@ describe( 'TableEditing', () => {
 				sinon.assert.calledOnce( domEvtDataStub.preventDefault );
 				sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
-					[ '11', '[12]' ]
+					[ '11', '[<paragraph>12</paragraph>]' ]
 				] ) );
 			} );
 
@@ -269,7 +257,7 @@ describe( 'TableEditing', () => {
 
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
 					[ '11', '12' ],
-					[ '[]', '' ]
+					[ '[<paragraph></paragraph>]', '' ]
 				] ) );
 			} );
 
@@ -283,7 +271,7 @@ describe( 'TableEditing', () => {
 
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
 					[ '11', '12' ],
-					[ '[21]', '22' ]
+					[ '[<paragraph>21</paragraph>]', '22' ]
 				] ) );
 			} );
 
@@ -298,7 +286,7 @@ describe( 'TableEditing', () => {
 					[
 						'11',
 						'<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
-						'[13]'
+						'[<paragraph>13</paragraph>]'
 					],
 				] ) );
 			} );
@@ -347,7 +335,7 @@ describe( 'TableEditing', () => {
 					sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
 
 					expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
-						[ '[11]', '12' ]
+						[ '[<paragraph>11</paragraph>]', '12' ]
 					] ) );
 
 					// Should cancel event - so no other tab handler is called.
@@ -407,7 +395,7 @@ describe( 'TableEditing', () => {
 				sinon.assert.calledOnce( domEvtDataStub.stopPropagation );
 
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
-					[ '[11]', '12' ]
+					[ '[<paragraph>11</paragraph>]', '12' ]
 				] ) );
 			} );
 
@@ -432,7 +420,7 @@ describe( 'TableEditing', () => {
 				editor.editing.view.document.fire( 'keydown', domEvtDataStub );
 
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
-					[ '11', '[12]' ],
+					[ '11', '[<paragraph>12</paragraph>]' ],
 					[ '21', '22' ]
 				] ) );
 			} );
@@ -446,7 +434,7 @@ describe( 'TableEditing', () => {
 
 				expect( formatTable( getModelData( model ) ) ).to.equal( formattedModelTable( [
 					[
-						'[11]',
+						'[<paragraph>11</paragraph>]',
 						'<paragraph>12</paragraph><paragraph>foo</paragraph><paragraph>bar</paragraph>',
 						'13'
 					],

+ 4 - 56
packages/ckeditor5-table/tests/tableselection.js

@@ -6,7 +6,7 @@
 import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
 import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 
-import { defaultConversion, defaultSchema, modelTable } from './_utils/utils';
+import { defaultConversion, defaultSchema, formatTable, modelTable } from './_utils/utils';
 
 import TableSelection from '../src/tableselection';
 import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view';
@@ -64,30 +64,6 @@ describe( 'TableSelection', () => {
 
 			expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ nodeByPath ] );
 		} );
-
-		it( 'should set view selection', () => {
-			setData( model, modelTable( [
-				[ '00[]', '01', '02' ],
-				[ '10', '11', '12' ]
-			] ) );
-
-			tableSelection.startSelection( root.getNodeByPath( [ 0, 0, 0 ] ) );
-
-			expect( getViewData( editor.editing.view ) ).to.equal(
-				'<figure class="table">' +
-					'<table>' +
-						'<tbody>' +
-							'<tr>' +
-								'[<td>00</td>]<td>01</td><td>02</td>' +
-							'</tr>' +
-							'<tr>' +
-								'<td>10</td><td>11</td><td>12</td>' +
-							'</tr>' +
-						'</tbody>' +
-					'</table>' +
-				'</figure>'
-			);
-		} );
 	} );
 
 	describe( 'stop()', () => {
@@ -162,34 +138,6 @@ describe( 'TableSelection', () => {
 			expect( tableSelection.isSelecting ).to.be.false;
 			expect( Array.from( tableSelection.getSelection() ) ).to.deep.equal( [ root.getNodeByPath( [ 0, 0, 0 ] ) ] );
 		} );
-
-		it( 'should update view selection', () => {
-			setData( model, modelTable( [
-				[ '00[]', '01', '02' ],
-				[ '10', '11', '12' ]
-			] ) );
-
-			const startNode = root.getNodeByPath( [ 0, 0, 0 ] );
-			const firstEndNode = root.getNodeByPath( [ 0, 0, 1 ] );
-
-			tableSelection.startSelection( startNode );
-			tableSelection.stopSelection( firstEndNode );
-
-			expect( getViewData( editor.editing.view ) ).to.equal(
-				'<figure class="table">' +
-					'<table>' +
-						'<tbody>' +
-							'<tr>' +
-								'[<td>00</td>][<td>01</td>]<td>02</td>' +
-							'</tr>' +
-							'<tr>' +
-								'<td>10</td><td>11</td><td>12</td>' +
-							'</tr>' +
-						'</tbody>' +
-					'</table>' +
-				'</figure>'
-			);
-		} );
 	} );
 
 	describe( 'update()', () => {
@@ -266,12 +214,12 @@ describe( 'TableSelection', () => {
 			tableSelection.startSelection( startNode );
 			tableSelection.updateSelection( firstEndNode );
 
-			expect( getViewData( editor.editing.view ) ).to.equal(
+			expect( formatTable( getViewData( editor.editing.view ) ) ).to.equal( formatTable(
 				'<figure class="table">' +
 					'<table>' +
 						'<tbody>' +
 							'<tr>' +
-								'[<td>00</td>][<td>01</td>]<td>02</td>' +
+								'[<td class="selected">00</td>][<td class="selected">01</td>]<td>02</td>' +
 							'</tr>' +
 							'<tr>' +
 								'<td>10</td><td>11</td><td>12</td>' +
@@ -279,7 +227,7 @@ describe( 'TableSelection', () => {
 						'</tbody>' +
 					'</table>' +
 				'</figure>'
-			);
+			) );
 		} );
 	} );