/**
 * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
 */

import { ClassicTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js';
import { VirtualTestEditor } from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor.js';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { UndoEditing } from '@ckeditor/ckeditor5-undo';
import { ClipboardPipeline } from '@ckeditor/ckeditor5-clipboard';
import { ArticlePluginSet } from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset.js';
import { testUtils } from '@ckeditor/ckeditor5-core/tests/_utils/utils.js';
import { toWidgetEditable } from '@ckeditor/ckeditor5-widget';
import { _getViewData, _setModelData, _getModelData } from '@ckeditor/ckeditor5-engine';
import { modelTable, viewTable } from '../_utils/utils.js';
import { Table } from '../../src/table.js';
import { TableCaption } from '../../src/tablecaption.js';
import { TableProperties } from '../../src/tableproperties.js';

import { TableEditing } from '../../src/tableediting.js';
import { TableLayout } from '../../src/tablelayout.js';

describe( 'downcast converters', () => {
	let editor, model, root, view, viewRoot;

	testUtils.createSinonSandbox();

	beforeEach( async () => {
		editor = await VirtualTestEditor.create( {
			plugins: [ Paragraph, TableEditing, UndoEditing ],
			table: {
				enableFooters: true
			}
		} );

		model = editor.model;
		root = model.document.getRoot( 'main' );
		view = editor.editing.view;
		viewRoot = view.document.getRoot();
	} );

	afterEach( () => {
		return editor.destroy();
	} );

	describe( 'downcastTable()', () => {
		describe( 'editing pipeline', () => {
			it( 'should create table as a widget', () => {
				_setModelData( model, modelTable( [ [ '' ] ] ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should reconvert table on headingRows attribute change', () => {
				_setModelData( model, modelTable( [
					[ '00' ],
					[ '10' ]
				] ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureBefore = viewRoot.getChild( 0 );
				const viewTableBefore = viewFigureBefore.getChild( 1 );
				const viewTableRow0Before = viewTableBefore.getChild( 0 ).getChild( 0 );
				const viewTableRow1Before = viewTableBefore.getChild( 0 ).getChild( 1 );
				const viewTableCell0Before = viewTableRow0Before.getChild( 0 );
				const viewTableCell1Before = viewTableRow1Before.getChild( 1 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 1, root.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<thead>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</th>' +
								'</tr>' +
							'</thead>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureAfter = viewRoot.getChild( 0 );
				const viewTableAfter = viewFigureAfter.getChild( 1 );
				const viewTableRow0After = viewTableAfter.getChild( 0 ).getChild( 0 );
				const viewTableRow1After = viewTableAfter.getChild( 1 ).getChild( 0 );
				const viewTableCell0After = viewTableRow0After.getChild( 0 );
				const viewTableCell1After = viewTableRow1After.getChild( 1 );

				expect( viewFigureAfter ).to.not.equal( viewFigureBefore );
				expect( viewTableAfter ).to.not.equal( viewTableBefore );
				expect( viewTableRow0After ).to.equal( viewTableRow0Before );
				expect( viewTableCell0After ).to.not.equal( viewTableCell0Before );
				expect( viewTableRow1After ).to.equal( viewTableRow1Before );
				expect( viewTableCell1After ).to.equal( viewTableCell1Before );
			} );

			it( 'should reconvert if `footerRows` attribute changes', () => {
				_setModelData( model, modelTable( [
					[ '00' ],
					[ '10' ]
				] ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureBefore = viewRoot.getChild( 0 );
				const viewTableBefore = viewFigureBefore.getChild( 1 );
				const viewTableRow0Before = viewTableBefore.getChild( 0 ).getChild( 0 );
				const viewTableRow1Before = viewTableBefore.getChild( 0 ).getChild( 1 );
				const viewTableCell0Before = viewTableRow0Before.getChild( 0 );
				const viewTableCell1Before = viewTableRow1Before.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'footerRows', 1, root.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
							'<tfoot>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tfoot>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureAfter = viewRoot.getChild( 0 );
				const viewTableAfter = viewFigureAfter.getChild( 1 );
				const viewTableRow0After = viewTableAfter.getChild( 0 ).getChild( 0 );
				const viewTableRow1After = viewTableAfter.getChild( 1 ).getChild( 0 );
				const viewTableCell0After = viewTableRow0After.getChild( 0 );
				const viewTableCell1After = viewTableRow1After.getChild( 0 );

				expect( viewFigureAfter ).to.not.equal( viewFigureBefore );
				expect( viewTableAfter ).to.not.equal( viewTableBefore );
				expect( viewTableRow0After ).to.equal( viewTableRow0Before );
				expect( viewTableCell0After ).to.equal( viewTableCell0Before );

				// Only last row is updated but elements are reused.
				expect( viewTableRow1After ).to.equal( viewTableRow1Before );
				expect( viewTableCell1After ).to.equal( viewTableCell1Before );
			} );

			it( 'should reconvert if remove `footerRows` attribute', () => {
				_setModelData( model, modelTable( [
					[ '00' ],
					[ '10' ]
				], { footerRows: 1 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
							'<tfoot>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tfoot>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureBefore = viewRoot.getChild( 0 );
				const viewTableBefore = viewFigureBefore.getChild( 1 );
				const viewTableRow0Before = viewTableBefore.getChild( 0 ).getChild( 0 );
				const viewTableRow1Before = viewTableBefore.getChild( 1 ).getChild( 0 );
				const viewTableCell0Before = viewTableRow0Before.getChild( 0 );
				const viewTableCell1Before = viewTableRow1Before.getChild( 0 );

				model.change( writer => {
					writer.removeAttribute( 'footerRows', root.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				const viewFigureAfter = viewRoot.getChild( 0 );
				const viewTableAfter = viewFigureAfter.getChild( 1 );
				const viewTableRow0After = viewTableAfter.getChild( 0 ).getChild( 0 );
				const viewTableRow1After = viewTableAfter.getChild( 0 ).getChild( 1 );
				const viewTableCell0After = viewTableRow0After.getChild( 0 );
				const viewTableCell1After = viewTableRow1After.getChild( 0 );

				expect( viewFigureAfter ).to.not.equal( viewFigureBefore );
				expect( viewTableAfter ).to.not.equal( viewTableBefore );
				expect( viewTableRow0After ).to.equal( viewTableRow0Before );
				expect( viewTableCell0After ).to.equal( viewTableCell0Before );

				// Only last row is updated but elements are reused.
				expect( viewTableRow1After ).to.equal( viewTableRow1Before );
				expect( viewTableCell1After ).to.equal( viewTableCell1Before );
			} );
		} );

		describe( 'data pipeline', () => {
			it( 'should create table with tbody and thead', () => {
				_setModelData( model, modelTable( [
					[ '00' ],
					[ '10' ]
				], { headingRows: 1 } ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<thead>' +
								'<tr><th>00</th></tr>' +
							'</thead>' +
							'<tbody>' +
								'<tr><td>10</td></tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with thead', () => {
				_setModelData( model, modelTable( [
					[ '00' ],
					[ '10' ]
				], { headingRows: 2 } ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<thead>' +
								'<tr><th>00</th></tr>' +
								'<tr><th>10</th></tr>' +
							'</thead>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with heading columns and rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02', '03' ],
					[ '10', '11', '12', '13' ]
				], { headingColumns: 3, headingRows: 1 } ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<thead>' +
								'<tr><th>00</th><th>01</th><th>02</th><th>03</th></tr>' +
							'</thead>' +
							'<tbody>' +
								'<tr><th>10</th><th>11</th><th>12</th><td>13</td></tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should push table items without dedicated slot outside the table', () => {
				editor.model.schema.register( 'foo', { allowIn: 'table' } );
				editor.conversion.elementToElement( { model: 'foo', view: 'foo' } );

				editor.setData(
					`<figure class="table">
						<table>
							<foo></foo>
							<tbody>
								<tr>
									<td>01</td>
									<td>02</td>
								</tr>
							</tbody>
						</table>
					</figure>`
				);

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td>01</td>' +
									'<td>02</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
						'<foo>&nbsp;</foo>' +
					'</figure>'
				);
			} );

			it( 'should create table with custom slot', () => {
				editor.model.schema.register( 'foo', { allowIn: 'table' } );
				editor.conversion.elementToElement( { model: 'foo', view: 'foo' } );

				editor.plugins.get( 'TableEditing' ).registerAdditionalSlot( {
					filter: element => element.is( 'element', 'foo' ),
					positionOffset: 0
				} );

				editor.setData(
					`<figure class="table">
						<table>
							<foo></foo>
							<tbody>
								<tr>
									<td>01</td>
									<td>02</td>
								</tr>
							</tbody>
						</table>
					</figure>`
				);

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<foo>&nbsp;</foo>' +
							'<tbody>' +
								'<tr>' +
									'<td>01</td>' +
									'<td>02</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with custom slot at the `end` position', () => {
				editor.model.schema.register( 'foo', { allowIn: 'table' } );
				editor.conversion.elementToElement( { model: 'foo', view: 'foo' } );

				editor.plugins.get( 'TableEditing' ).registerAdditionalSlot( {
					filter: element => element.is( 'element', 'foo' ),
					positionOffset: 'end'
				} );

				editor.setData(
					`<figure class="table">
						<table>
							<foo></foo>
							<tbody>
								<tr>
									<td>01</td>
									<td>02</td>
								</tr>
							</tbody>
						</table>
					</figure>`
				);

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td>01</td>' +
									'<td>02</td>' +
								'</tr>' +
							'</tbody>' +
							'<foo>&nbsp;</foo>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with custom slot at the `after` position', () => {
				editor.model.schema.register( 'foo', { allowIn: 'table' } );
				editor.conversion.elementToElement( { model: 'foo', view: 'foo' } );

				editor.plugins.get( 'TableEditing' ).registerAdditionalSlot( {
					filter: element => element.is( 'element', 'foo' ),
					positionOffset: 'after'
				} );

				editor.setData(
					`<figure class="table">
						<table>
							<foo></foo>
							<tbody>
								<tr>
									<td>01</td>
									<td>02</td>
								</tr>
							</tbody>
						</table>
					</figure>`
				);

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td>01</td>' +
									'<td>02</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
						'<foo>&nbsp;</foo>' +
					'</figure>'
				);
			} );

			it( 'should create table with custom slot at the `before` position', () => {
				editor.model.schema.register( 'foo', { allowIn: 'table' } );
				editor.conversion.elementToElement( { model: 'foo', view: 'foo' } );

				editor.plugins.get( 'TableEditing' ).registerAdditionalSlot( {
					filter: element => element.is( 'element', 'foo' ),
					positionOffset: 'before'
				} );

				editor.setData(
					`<figure class="table">
						<table>
							<foo></foo>
							<tbody>
								<tr>
									<td>01</td>
									<td>02</td>
								</tr>
							</tbody>
						</table>
					</figure>`
				);

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<foo>&nbsp;</foo>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td>01</td>' +
									'<td>02</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with block content', () => {
				_setModelData( model, modelTable( [
					[ '<paragraph>00</paragraph><paragraph>foo</paragraph>', '01' ]
				] ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td><p>00</p><p>foo</p></td>' +
									'<td>01</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should create table with block content (attribute on paragraph)', () => {
				editor.conversion.attributeToAttribute(
					{
						model: { key: 'alignment', values: [ 'right', 'center', 'justify' ] },
						view: {
							right: { key: 'style', value: { 'text-align': 'right' } },
							center: { key: 'style', value: { 'text-align': 'center' } },
							justify: { key: 'style', value: { 'text-align': 'justify' } }
						}
					}
				);

				_setModelData( model, modelTable( [
					[ '<paragraph alignment="right">00</paragraph>' ]
				] ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td><p style="text-align:right;">00</p></td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			// https://github.com/ckeditor/ckeditor5/issues/8941
			// https://github.com/ckeditor/ckeditor5/issues/8979
			it( 'should create table with an empty cell', () => {
				model.schema.register( 'block', {
					allowWhere: '$block',
					allowContentOf: '$root'
				} );
				editor.conversion.elementToElement( { model: 'block', view: 'block' } );

				editor.setData(
					'<block>' +
						'<table>' +
							'<tr>' +
								'<td>&nbsp;</td>' +
							'</tr>' +
						'</table>' +
					'</block>'
				);

				expect( editor.getData() ).to.equalMarkup(
					'<block>' +
						'<figure class="table">' +
							'<table>' +
								'<tbody>' +
									'<tr>' +
										'<td>&nbsp;</td>' +
									'</tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>' +
					'</block>'
				);
			} );

			it( 'should be possible to overwrite', () => {
				editor.conversion.elementToElement( { model: 'tableRow', view: 'tr', converterPriority: 'high' } );
				editor.conversion.elementToElement( { model: 'tableCell', view: 'td', converterPriority: 'high' } );
				editor.conversion.elementToElement( { model: 'paragraph', view: 'p', converterPriority: 'highest' } );
				editor.conversion.for( 'downcast' ).add( dispatcher => {
					dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
						conversionApi.consumable.consume( data.item, 'insert' );

						const tableElement = conversionApi.writer.createContainerElement( 'table', { foo: 'bar' } );
						const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );

						conversionApi.mapper.bindElements( data.item, tableElement );
						conversionApi.writer.insert( viewPosition, tableElement );
					}, { priority: 'high' } );
				} );

				_setModelData( model, modelTable( [ [ '' ] ] ) );

				expect( editor.getData() ).to.equalMarkup(
					'<table foo="bar">' +
						'<tr><td><p>&nbsp;</p></td></tr>' +
					'</table>'
				);
			} );

			it( 'should re-create table on reinsert', () => {
				model.schema.register( 'wrapper', {
					allowWhere: '$block',
					allowContentOf: '$root'
				} );
				editor.conversion.elementToElement( { model: 'wrapper', view: 'div' } );

				_setModelData( model, modelTable( [ [ '[]' ] ] ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr><td>&nbsp;</td></tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				model.change( writer => {
					const table = model.document.getRoot().getChild( 0 );
					const range = writer.createRange( writer.createPositionBefore( table ), writer.createPositionAfter( table ) );
					const wrapper = writer.createElement( 'wrapper' );

					writer.wrap( range, wrapper );
				} );

				expect( editor.getData() ).to.equalMarkup(
					'<div>' +
						'<figure class="table">' +
							'<table>' +
								'<tbody>' +
									'<tr><td>&nbsp;</td></tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>' +
					'</div>'
				);
			} );

			describe( 'headingColumns attribute', () => {
				it( 'should mark heading columns table cells', () => {
					_setModelData( model, modelTable( [
						[ '00', '01', '02' ],
						[ '10', '11', '12' ]
					], { headingColumns: 2 } ) );

					expect( editor.getData() ).to.equalMarkup(
						'<figure class="table">' +
							'<table>' +
								'<tbody>' +
									'<tr><th>00</th><th>01</th><td>02</td></tr>' +
									'<tr><th>10</th><th>11</th><td>12</td></tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>'
					);
				} );

				it( 'should mark heading columns table cells when one has colspan attribute', () => {
					_setModelData( model, modelTable( [
						[ '00', '01', '02', '03' ],
						[ { colspan: 2, contents: '10' }, '12', '13' ]
					], { headingColumns: 3 } ) );

					expect( editor.getData() ).to.equalMarkup(
						'<figure class="table">' +
							'<table>' +
								'<tbody>' +
									'<tr><th>00</th><th>01</th><th>02</th><td>03</td></tr>' +
									'<tr><th colspan="2">10</th><th>12</th><td>13</td></tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>'
					);
				} );

				it( 'should work with colspan and rowspan attributes on table cells', () => {
					// The table in this test looks like a table below:
					//
					//   Row headings | Normal cells
					//                |
					// +----+----+----+----+
					// | 00 | 01 | 02 | 03 |
					// |    +----+    +----+
					// |    | 11 |    | 13 |
					// |----+----+    +----+
					// | 20      |    | 23 |
					// |         +----+----+
					// |         | 32 | 33 |
					// +----+----+----+----+
					_setModelData( model, modelTable( [
						[ { rowspan: 2, contents: '00' }, '01', { rowspan: 3, contents: '02' }, '03' ],
						[ '11', '13' ],
						[ { colspan: 2, rowspan: 2, contents: '20' }, '23' ],
						[ '32', '33' ]
					], { headingColumns: 3 } ) );

					expect( editor.getData() ).to.equalMarkup(
						'<figure class="table">' +
							'<table>' +
								'<tbody>' +
									'<tr><th rowspan="2">00</th><th>01</th><th rowspan="3">02</th><td>03</td></tr>' +
									'<tr><th>11</th><td>13</td></tr>' +
									'<tr><th colspan="2" rowspan="2">20</th><td>23</td></tr>' +
									'<tr><th>32</th><td>33</td></tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>'
					);
				} );
			} );

			it( 'should create table with tbody', () => {
				_setModelData( model, modelTable( [ [ '' ] ] ) );

				expect( editor.getData() ).to.equalMarkup(
					'<figure class="table">' +
						'<table>' +
							'<tbody>' +
								'<tr><td>&nbsp;</td></tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			describe( 'footerRows attribute', () => {
				it( 'should properly downcast table with `footerRows=1`', () => {
					_setModelData( model, modelTable( [ [ '00', '01' ] ], { footerRows: 1 } ) );

					expect( editor.getData() ).to.equalMarkup(
						viewTable( [ [ '00', '01' ] ], { footerRows: 1 } )
					);
				} );

				it( 'should properly downcast table with `footerRows=1` where the first `tr` is normal row', () => {
					_setModelData( model, modelTable( [
						[ '00', '01' ],
						[ '10', '11' ]
					], { footerRows: 1 } ) );

					expect( editor.getData() ).to.equalMarkup(
						viewTable( [
							[ '00', '01' ],
							[ '10', '11' ]
						], { footerRows: 1 } )
					);
				} );

				it( 'should properly downcast table with `headingRows` and `footerRows`', () => {
					_setModelData( model, modelTable( [
						[ '00', '01' ],
						[ '10', '11' ],
						[ '20', '21' ]
					], { headingRows: 1, footerRows: 1 } ) );

					expect( editor.getData() ).to.equalMarkup(
						viewTable( [
							[ '00', '01' ],
							[ '10', '11' ],
							[ '20', '21' ]
						], { headingRows: 1, footerRows: 1 } )
					);
				} );

				it( 'should properly downcast table with `headingColumns` and `footerRows`', () => {
					_setModelData( model, modelTable( [
						[ '00', '01', '02' ],
						[ '10', '11', '12' ]
					], { headingColumns: 1, footerRows: 1 } ) );

					expect( editor.getData() ).to.equalMarkup(
						viewTable( [
							[ { isHeading: true, contents: '00' }, '01', '02' ],
							[ { isHeading: true, contents: '10' }, '11', '12' ]
						], { footerRows: 1 } )
					);
				} );
			} );
		} );
	} );

	describe( 'downcastRow()', () => {
		describe( 'editing pipeline', () => {
			it( 'should react to changed rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 1 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ]
				], { asWidget: true } ) );
			} );

			it( 'should properly consume already added rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 1 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ]
				], { asWidget: true } ) );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 2 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ],
					[ '', '' ]
				], { asWidget: true } ) );
			} );

			it( 'should insert row on proper index', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '21', '22' ],
					[ '31', '32' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 1 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ],
					[ '21', '22' ],
					[ '31', '32' ]
				], { asWidget: true } ) );
			} );

			it( 'should insert row on proper index when table has heading rows defined - insert in body', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '21', '22' ],
					[ '31', '32' ]
				], { headingRows: 1, asWidget: true } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 1 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ],
					[ '21', '22' ],
					[ '31', '32' ]
				], { headingRows: 1, asWidget: true } ) );
			} );

			it( 'should insert row on proper index when table has heading rows defined - insert in heading', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '21', '22' ],
					[ '31', '32' ]
				], { headingRows: 2 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 1 );

					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );

					writer.setAttribute( 'headingRows', 3, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ],
					[ '21', '22' ],
					[ '31', '32' ]
				], { headingRows: 3, asWidget: true } ) );
			} );

			it( 'should react to changed rows when previous rows\' cells has rowspans', () => {
				_setModelData( model, modelTable( [
					[ { rowspan: 2, contents: '00' }, '01' ],
					[ '22' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = writer.createElement( 'tableRow' );

					writer.insert( row, table, 2 );
					writer.insertElement( 'tableCell', row, 'end' );
					writer.insertElement( 'tableCell', row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { rowspan: 2, contents: '00' }, '01' ],
					[ '22' ],
					[ '', '' ]
				], { asWidget: true } ) );
			} );

			it( 'should properly create row headings', () => {
				_setModelData( model, modelTable( [
					[ { rowspan: 2, contents: '00' }, '01' ],
					[ '22' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const firstRow = writer.createElement( 'tableRow' );

					writer.insert( firstRow, table, 2 );
					writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' );

					const secondRow = writer.createElement( 'tableRow' );

					writer.insert( secondRow, table, 3 );
					writer.insert( writer.createElement( 'tableCell' ), secondRow, 'end' );
					writer.insert( writer.createElement( 'tableCell' ), secondRow, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { rowspan: 2, contents: '00', isHeading: true }, '01' ],
					[ '22' ],
					[ { contents: '', isHeading: true }, '' ],
					[ { contents: '', isHeading: true }, '' ]
				], { asWidget: true } ) );
			} );

			it( 'should create table cell inside inserted row as a widget', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const firstRow = writer.createElement( 'tableRow' );

					writer.insert( firstRow, table, 1 );
					writer.insert( writer.createElement( 'tableCell' ), firstRow, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should react to removed row from the beginning of a body rows (no heading rows)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.remove( table.getChild( 1 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">01</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should react to removed row from the end of a body rows (no heading rows)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.remove( table.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">11</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should react to removed row from the beginning of a heading rows (no body rows)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				], { headingRows: 2 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					// Removing row from a heading section changes requires changing heading rows attribute.
					writer.setAttribute( 'headingRows', 1, table );
					writer.remove( table.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<thead>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</th>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">11</span>' +
									'</th>' +
								'</tr>' +
							'</thead>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should react to removed row from the end of a heading rows (no body rows)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				], { headingRows: 2 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					// Removing row from a heading section changes requires changing heading rows attribute.
					writer.setAttribute( 'headingRows', 1, table );
					writer.remove( table.getChild( 1 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<thead>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</th>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">01</span>' +
									'</th>' +
								'</tr>' +
							'</thead>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should react to removed row from the end of a heading rows (first cell in body has colspan)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01', '02', '03' ],
					[ { rowspan: 2, colspan: 2, contents: '10' }, '12', '13' ],
					[ '22', '23' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					// Removing row from a heading section changes requires changing heading rows attribute.
					writer.remove( table.getChild( 0 ) );
					writer.setAttribute( 'headingRows', 0, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" ' +
											'colspan="2" contenteditable="true" ' +
											'rowspan="2" tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">12</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">13</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">22</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">23</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should remove empty thead if a last row was removed from a heading rows (has heading and body)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					// Removing row from a heading section changes requires changing heading rows attribute.
					writer.removeAttribute( 'headingRows', table );
					writer.remove( table.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">10</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">11</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should remove empty tbody if a last row was removed a body rows (has heading and body)', () => {
				_setModelData( model, modelTable( [
					[ '00[]', '01' ],
					[ '10', '11' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.remove( table.getChild( 1 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<thead>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</th>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">01</span>' +
									'</th>' +
								'</tr>' +
							'</thead>' +
						'</table>' +
					'</figure>'
				);
			} );
		} );
	} );

	describe( 'downcastCell()', () => {
		describe( 'editing pipeline', () => {
			it( 'should add tableCell on proper index in tr', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = table.getChild( 0 );

					writer.insertElement( 'tableCell', row, 1 );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '', '01' ]
				], { asWidget: true } ) );
			} );

			it( 'should add tableCell on proper index in tr when previous have colspans', () => {
				_setModelData( model, modelTable( [
					[ { colspan: 2, contents: '00' }, '13' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = table.getChild( 0 );

					writer.insertElement( 'tableCell', row, 1 );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { colspan: 2, contents: '00' }, '', '13' ]
				], { asWidget: true } ) );
			} );

			it( 'should add tableCell on proper index in tr when previous row have rowspans', () => {
				_setModelData( model, modelTable( [
					[ { rowspan: 2, contents: '00' }, '01', '02' ],
					[ '11', '12' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.insertElement( 'tableCell', table.getChild( 0 ), 1 );
					writer.insertElement( 'tableCell', table.getChild( 1 ), 0 );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { rowspan: 2, contents: '00' }, '', '01', '02' ],
					[ '', '11', '12' ]
				], { asWidget: true } ) );
			} );

			it( 'split cell simulation - simple', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const firstRow = table.getChild( 0 );
					const secondRow = table.getChild( 1 );

					writer.insertElement( 'tableCell', firstRow, 1 );
					writer.setAttribute( 'colspan', 2, secondRow.getChild( 0 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '', '01' ],
					[ { colspan: 2, contents: '10' }, '11' ]
				], { asWidget: true } ) );
			} );

			it( 'merge simulation - simple', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const firstRow = table.getChild( 0 );

					writer.setAttribute( 'colspan', 2, firstRow.getChild( 0 ) );
					writer.remove( firstRow.getChild( 1 ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { colspan: 2, contents: '00' } ],
					[ '10', '11' ]
				], { asWidget: true } ) );
			} );

			it( 'should create inserted table cell as a widget', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = table.getChild( 0 );

					writer.insert( writer.createElement( 'tableCell' ), row, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );
		} );
	} );

	describe( 'heading columns conversion', () => {
		describe( 'editing pipeline', () => {
			it( 'should work for adding heading columns', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 1, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { isHeading: true, contents: '00' }, '01' ],
					[ { isHeading: true, contents: '10' }, '11' ]
				], { asWidget: true } ) );
			} );

			it( 'should work for changing heading columns to a bigger number', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02', '03' ],
					[ '10', '11', '12', '13' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 3, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { isHeading: true, contents: '00' }, { isHeading: true, contents: '01' }, { isHeading: true, contents: '02' }, '03' ],
					[ { isHeading: true, contents: '10' }, { isHeading: true, contents: '11' }, { isHeading: true, contents: '12' }, '13' ]
				], { asWidget: true } ) );
			} );

			it( 'should work for changing heading columns to a smaller number', () => {
				_setModelData( model, modelTable( [
					[ { isHeading: true, contents: '00' }, { isHeading: true, contents: '01' }, { isHeading: true, contents: '02' }, '03' ],
					[ { isHeading: true, contents: '10' }, { isHeading: true, contents: '11' }, { isHeading: true, contents: '12' }, '13' ]
				], { headingColumns: 3 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 1, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { isHeading: true, contents: '00' }, '01', '02', '03' ],
					[ { isHeading: true, contents: '10' }, '11', '12', '13' ]
				], { asWidget: true } ) );
			} );

			it( 'should work for removing heading columns', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { headingColumns: 1 } ) );
				const table = root.getChild( 0 );

				model.change( writer => {
					writer.removeAttribute( 'headingColumns', table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { asWidget: true } ) );
			} );

			it( 'should be possible to overwrite', () => {
				editor.conversion.attributeToAttribute( { model: 'headingColumns', view: 'headingColumns', converterPriority: 'high' } );
				editor.conversion.elementToElement( {
					model: 'tableCell',
					view: ( tableCell, { writer } ) => toWidgetEditable( writer.createEditableElement( 'td' ), writer ),
					converterPriority: 'high'
				} );
				_setModelData( model, modelTable( [ [ '00[] ' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 1, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false" headingColumns="1">' +
					'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'role="textbox" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should work with adding table cells', () => {
				// +----+----+----+----+
				// | 00 | 01 | 02 | 03 |
				// +    +----+----+----+
				// |    | 11 | 12 | 13 |
				// +----+----+----+----+
				// | 20      | 22 | 23 |
				// +----+----+----+----+
				_setModelData( model, modelTable( [
					[ { contents: '00', rowspan: 2 }, '01', '02', '03' ],
					[ '11', '12', '13' ],
					[ { contents: '20', colspan: 2 }, '22', '23' ]
				], { headingColumns: 2 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					// Inserting column in heading columns so update table's attribute also
					writer.setAttribute( 'headingColumns', 3, table );

					writer.insertElement( 'tableCell', table.getChild( 0 ), 2 );
					writer.insertElement( 'tableCell', table.getChild( 1 ), 1 );
					writer.insertElement( 'tableCell', table.getChild( 2 ), 1 );
				} );

				expect( _getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( modelTable( [
					[ { contents: '00', rowspan: 2 }, '01', '', '02', '03' ],
					[ '11', '', '12', '13' ],
					[ { contents: '20', colspan: 2 }, '', '22', '23' ]
				], { headingColumns: 3 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[
						{ isHeading: true, rowspan: 2, contents: '00' },
						{ isHeading: true, contents: '01' },
						{ isHeading: true, contents: '' },
						'02',
						'03'
					],
					[
						{ isHeading: true, contents: '11' },
						{ isHeading: true, contents: '' },
						'12',
						'13'
					],
					[
						{ isHeading: true, colspan: 2, contents: '20' },
						{ isHeading: true, contents: '' },
						'22',
						'23'
					]
				], { asWidget: true } ) );
			} );

			it( 'should create renamed cell as a widget', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 1, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<thead>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</th>' +
								'</tr>' +
							'</thead>' +
						'</table>' +
					'</figure>'
				);
			} );
		} );
	} );

	describe( 'heading rows conversion', () => {
		describe( 'editing pipeline', () => {
			it( 'should work for adding heading rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should work for changing number of heading rows to a bigger number', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should work for changing number of heading rows to a smaller number', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ],
					[ '30', '31' ]
				], { headingRows: 3 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ],
					[ '30', '31' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should work for removing heading rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { headingRows: 2 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.removeAttribute( 'headingRows', table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { asWidget: true } ) );
			} );

			it( 'should work for making heading rows without tbody', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should work with adding table rows at the beginning of a table', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );

					const tableRow = writer.createElement( 'tableRow' );

					writer.insert( tableRow, table, 0 );
					writer.insertElement( 'tableCell', tableRow, 'end' );
					writer.insertElement( 'tableCell', tableRow, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '', '' ],
					[ '00', '01' ],
					[ '10', '11' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should work with adding a table row and expanding heading', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );

					const tableRow = writer.createElement( 'tableRow' );

					writer.insert( tableRow, table, 1 );
					writer.insertElement( 'tableCell', tableRow, 'end' );
					writer.insertElement( 'tableCell', tableRow, 'end' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '', '' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 2, asWidget: true } ) );
			} );

			it( 'should reorder rows with header correctly - up direction', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02' ],
					[ '10', '11', '12' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				editor.model.change( writer => {
					writer.move(
						writer.createRangeOn( table.getChild( 1 ) ),
						writer.createPositionAt( table, 0 )
					);
				} );

				expect( _getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( modelTable( [
					[ '10', '11', '12' ],
					[ '00', '01', '02' ]
				], { headingRows: 1 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '10', '11', '12' ],
					[ '00', '01', '02' ]
				], { headingRows: 1, asWidget: true } ) );
			} );

			it( 'should reorder rows with header correctly - down direction', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02' ],
					[ '10', '11', '12' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				editor.model.change( writer => {
					writer.move(
						writer.createRangeOn( table.getChild( 0 ) ),
						writer.createPositionAt( table, 2 )
					);
				} );

				expect( _getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( modelTable( [
					[ '10', '11', '12' ],
					[ '00', '01', '02' ]
				], { headingRows: 1 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '10', '11', '12' ],
					[ '00', '01', '02' ]
				], { headingRows: 1, asWidget: true } ) );
			} );

			it( 'should reorder columns with header correctly - left direction', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02' ],
					[ '10', '11', '12' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				editor.model.change( writer => {
					for ( const tableRow of table.getChildren() ) {
						writer.move(
							writer.createRangeOn( tableRow.getChild( 1 ) ),
							writer.createPositionAt( tableRow, 0 )
						);
					}
				} );

				expect( _getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( modelTable( [
					[ '01', '00', '02' ],
					[ '11', '10', '12' ]
				], { headingColumns: 1 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { isHeading: true, contents: '01' }, '00', '02' ],
					[ { isHeading: true, contents: '11' }, '10', '12' ]
				], { asWidget: true } ) );
			} );

			it( 'should reorder columns with header correctly - right direction', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02' ],
					[ '10', '11', '12' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				editor.model.change( writer => {
					for ( const tableRow of table.getChildren() ) {
						writer.move(
							writer.createRangeOn( tableRow.getChild( 0 ) ),
							writer.createPositionAt( tableRow, 2 )
						);
					}
				} );

				expect( _getModelData( model, { withoutSelection: true } ) ).to.equalMarkup( modelTable( [
					[ '01', '00', '02' ],
					[ '11', '10', '12' ]
				], { headingColumns: 1 } ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ { isHeading: true, contents: '01' }, '00', '02' ],
					[ { isHeading: true, contents: '11' }, '10', '12' ]
				], { asWidget: true } ) );
			} );

			it( 'should create renamed cell as a widget', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 1, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
					'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<th class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</th>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should properly integrate with undo', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 2, asWidget: true } ) );

				editor.execute( 'undo' );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup( viewTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 1, asWidget: true } ) );
			} );
		} );
	} );

	describe( 'marker highlight conversion on table cell', () => {
		describe( 'single class in highlight descriptor', () => {
			beforeEach( async () => {
				editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } );

				model = editor.model;
				root = model.document.getRoot( 'main' );
				view = editor.editing.view;

				markerConversion( editor.conversion );
			} );

			it( 'should apply marker class on tableCell - on inserting a table', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 0, 0 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );

					checkCustomPropertyForHighlight( editor.editing.mapper.toViewElement( cell ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable highlight-yellow" contenteditable="true" ' +
										'tabindex="-1">' +
											'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should apply marker class on tableCell - on inserting a row', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const firstRow = writer.createElement( 'tableRow' );
					const cell = writer.createElement( 'tableCell' );

					writer.insert( firstRow, table, 1 );
					writer.insert( cell, firstRow, 'end' );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );
				} );

				const cell = root.getNodeByPath( [ 0, 1, 0 ] );
				checkCustomPropertyForHighlight( editor.editing.mapper.toViewElement( cell ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
					'<div class="ck ck-widget__selection-handle"></div>' +
					'<table>' +
						'<tbody>' +
							'<tr>' +
								'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
								'tabindex="-1">' +
									'<span class="ck-table-bogus-paragraph">00</span>' +
								'</td>' +
							'</tr>' +
							'<tr>' +
								'<td class="ck-editor__editable ck-editor__nested-editable highlight-yellow" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
								'</td>' +
							'</tr>' +
						'</tbody>' +
					'</table>' +
					'</figure>'
				);

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should apply marker class on tableCell - on inserting a table cell', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const row = table.getChild( 0 );
					const cell = writer.createElement( 'tableCell' );

					writer.insert( cell, row, 'end' );
					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );
				} );

				const cell = root.getNodeByPath( [ 0, 0, 1 ] );
				checkCustomPropertyForHighlight( editor.editing.mapper.toViewElement( cell ) );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable highlight-yellow" contenteditable="true" ' +
										'tabindex="-1">' +
											'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph"></span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should preserve marker class on tableCell - when changing heading columns', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02', '03' ],
					[ '10', '11', '12', '13' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 0, 1 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );
				} );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 3, table );
				} );

				const cell = root.getNodeByPath( [ 0, 0, 1 ] );
				const viewElement = editor.editing.mapper.toViewElement( cell );

				checkCustomPropertyForHighlight( viewElement );
				expect( viewElement.hasClass( 'highlight-yellow' ) ).to.be.true;

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( viewElement.hasClass( 'highlight-yellow' ) ).to.be.false;
			} );

			it( 'should preserve marker class on tableCell - when changing heading rows', () => {
				_setModelData( model, modelTable( [
					[ '00', '01' ],
					[ '10', '11' ],
					[ '20', '21' ]
				], { headingRows: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 1, 0 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );
				} );

				model.change( writer => {
					writer.setAttribute( 'headingRows', 2, table );
				} );

				const cell = root.getNodeByPath( [ 0, 1, 0 ] );
				const viewElement = editor.editing.mapper.toViewElement( cell );

				checkCustomPropertyForHighlight( viewElement );
				expect( viewElement.hasClass( 'highlight-yellow' ) ).to.be.true;

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( viewElement.hasClass( 'highlight-yellow' ) ).to.be.false;
			} );
		} );

		describe( 'multiple classes in highlight descriptor', () => {
			beforeEach( async () => {
				editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } );

				model = editor.model;
				root = model.document.getRoot( 'main' );
				view = editor.editing.view;

				markerConversion( editor.conversion, [ 'marker', 'user-marker' ] );
			} );

			it( 'should apply marker class on tableCell - on inserting a table', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 0, 0 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );

					checkCustomPropertyForHighlight( editor.editing.mapper.toViewElement( cell ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable highlight-yellow marker user-marker"' +
										' contenteditable="true" ' +
										'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );
		} );

		describe( 'attributes in highlight descriptor', () => {
			beforeEach( async () => {
				editor = await VirtualTestEditor.create( { plugins: [ Paragraph, TableEditing ] } );

				model = editor.model;
				root = model.document.getRoot( 'main' );
				view = editor.editing.view;

				markerConversion( editor.conversion, [], { 'data-foo': 'bar', 'data-abc': 'xyz' } );
			} );

			it( 'should apply attributes on tableCell - on inserting a table', () => {
				_setModelData( model, modelTable( [ [ '00' ] ] ) );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 0, 0 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );

					checkCustomPropertyForHighlight( editor.editing.mapper.toViewElement( cell ) );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable highlight-yellow"' +
										' contenteditable="true" data-abc="xyz" data-foo="bar" ' +
										'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( _getViewData( view, { withoutSelection: true } ) ).to.equalMarkup(
					'<figure class="ck-widget ck-widget_with-selection-handle table" contenteditable="false">' +
						'<div class="ck ck-widget__selection-handle"></div>' +
						'<table>' +
							'<tbody>' +
								'<tr>' +
									'<td class="ck-editor__editable ck-editor__nested-editable" contenteditable="true" ' +
									'tabindex="-1">' +
										'<span class="ck-table-bogus-paragraph">00</span>' +
									'</td>' +
								'</tr>' +
							'</tbody>' +
						'</table>' +
					'</figure>'
				);
			} );

			it( 'should preserve attributes on tableCell - when changing heading columns', () => {
				_setModelData( model, modelTable( [
					[ '00', '01', '02', '03' ],
					[ '10', '11', '12', '13' ]
				], { headingColumns: 1 } ) );

				const table = root.getChild( 0 );

				model.change( writer => {
					const cell = root.getNodeByPath( [ 0, 0, 1 ] );

					writer.addMarker( 'marker:yellow', {
						range: writer.createRangeOn( cell ),
						usingOperation: false
					} );
				} );

				model.change( writer => {
					writer.setAttribute( 'headingColumns', 3, table );
				} );

				const cell = root.getNodeByPath( [ 0, 0, 1 ] );
				const viewElement = editor.editing.mapper.toViewElement( cell );

				expect( viewElement.getAttribute( 'data-foo' ) ).to.equal( 'bar' );
				expect( viewElement.getAttribute( 'data-abc' ) ).to.equal( 'xyz' );

				model.change( writer => {
					writer.removeMarker( 'marker:yellow' );
				} );

				expect( viewElement.hasAttribute( 'data-foo' ) ).to.be.false;
				expect( viewElement.hasAttribute( 'data-abc' ) ).to.be.false;
			} );
		} );

		function markerConversion( conversion, extraClasses = null, extraAttributes ) {
			conversion.for( 'editingDowncast' ).markerToHighlight( {
				model: 'marker',
				view: data => {
					const className = 'highlight-' + data.markerName.split( ':' )[ 1 ];
					const descriptor = {};

					descriptor.classes = extraClasses ? [ ...extraClasses, className ] : className;

					if ( extraAttributes ) {
						descriptor.attributes = extraAttributes;
					}

					return descriptor;
				}
			} );
		}

		function checkCustomPropertyForHighlight( viewElement ) {
			const set = viewElement.getCustomProperty( 'addHighlight' );
			const remove = viewElement.getCustomProperty( 'removeHighlight' );

			expect( typeof set ).to.equal( 'function' );
			expect( typeof remove ).to.equal( 'function' );
		}
	} );

	describe( 'conversion in clipboard pipeline', () => {
		let editor, editorElement, model;

		beforeEach( async () => {
			editorElement = document.createElement( 'div' );
			document.body.appendChild( editorElement );

			editor = await ClassicTestEditor.create( editorElement, {
				plugins: [ Paragraph, Table, TableCaption, TableProperties, ClipboardPipeline ]
			} );

			model = editor.model;
		} );

		afterEach( async () => {
			editorElement.remove();
			await editor.destroy();
		} );

		describe( 'model to view', () => {
			it( 'should create tbody section', () => {
				_setModelData( model, modelTable( [
					[ 'foo' ]
				] ) );

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<tbody>' +
							'<tr><td>foo</td></tr>' +
						'</tbody>' +
					'</table>'
				);
			} );

			it( 'should create heading rows', () => {
				_setModelData( model, modelTable( [
					[ '1', '2' ],
					[ '3', '4' ],
					[ '5', '6' ]
				], { headingRows: 2 } ) );

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<thead>' +
							'<tr><th>1</th><th>2</th></tr>' +
							'<tr><th>3</th><th>4</th></tr>' +
						'</thead>' +
						'<tbody>' +
							'<tr><td>5</td><td>6</td></tr>' +
						'</tbody>' +
					'</table>'
				);
			} );

			it( 'should create heading columns', () => {
				_setModelData( model, modelTable( [
					[ '1', '2' ],
					[ '3', '4' ],
					[ '5', '6' ]
				], { headingColumns: 1 } ) );

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<tbody>' +
							'<tr><th>1</th><td>2</td></tr>' +
							'<tr><th>3</th><td>4</td></tr>' +
							'<tr><th>5</th><td>6</td></tr>' +
						'</tbody>' +
					'</table>'
				);
			} );

			it( 'should create heading rows and columns', () => {
				_setModelData( model, modelTable( [
					[ '1', '2' ],
					[ '3', '4' ],
					[ '5', '6' ]
				], { headingRows: 1, headingColumns: 1 } ) );

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<thead>' +
							'<tr><th>1</th><th>2</th></tr>' +
						'</thead>' +
						'<tbody>' +
							'<tr><th>3</th><td>4</td></tr>' +
							'<tr><th>5</th><td>6</td></tr>' +
						'</tbody>' +
					'</table>'
				);
			} );

			it( 'should work when heading rows number is bigger than number of rows', () => {
				_setModelData( model, modelTable( [
					[ '1', '2' ],
					[ '3', '4' ]
				], { headingRows: 3 } ) );

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<thead>' +
							'<tr><th>1</th><th>2</th></tr>' +
							'<tr><th>3</th><th>4</th></tr>' +
						'</thead>' +
					'</table>'
				);
			} );

			it( 'should create caption element', () => {
				_setModelData( model,
					'<table>' +
						'<tableRow>' +
							'<tableCell><paragraph>1</paragraph></tableCell>' +
							'<tableCell><paragraph>2</paragraph></tableCell>' +
						'</tableRow>' +
						'<caption>Foo</caption>' +
					'</table>'
				);

				expect( getClipboardData( editor ) ).to.equal(
					'<table class="table table-style-align-center" style="margin-left:auto;margin-right:auto;" align="center">' +
						'<caption>Foo</caption>' +
						'<tbody>' +
							'<tr><td>1</td><td>2</td></tr>' +
						'</tbody>' +
					'</table>'
				);
			} );

			it( 'should not create caption element without TableCaption plugin', async () => {
				const testEditor = await ClassicTestEditor.create( editorElement, {
					plugins: [ Paragraph, Table, ClipboardPipeline ]
				} );

				testEditor.setData(
					'<table>' +
						'<caption>Foo</caption>' +
						'<tbody>' +
							'<tr><td>1</td><td>2</td></tr>' +
						'</tbody>' +
					'</table>'
				);

				expect( getClipboardData( testEditor ) ).to.equal(
					'<table class="table">' +
						'<tbody>' +
							'<tr><td>1</td><td>2</td></tr>' +
						'</tbody>' +
					'</table>'
				);

				await testEditor.destroy();
			} );

			it( 'should be overridable', () => {
				const table = createEmptyTable();

				editor.conversion.for( 'dataDowncast' ).add( dispatcher =>
					dispatcher.on( 'attribute:tableBorderColor:table', ( evt, data, conversionApi ) => {
						conversionApi.consumable.consume( data.item, evt.name );
					}, { priority: 'highest' } ) );

				model.change( writer => writer.setAttribute( 'tableBorderColor', '#f00', table ) );

				assertPlainTableStyle(
					editor,
					'margin-left:auto;margin-right:auto;',
					'table table-style-align-center',
					'align="center"'
				);
			} );

			describe( 'should create attribute', () => {
				let table;

				beforeEach( () => {
					table = createEmptyTable();
				} );

				it( 'tableBorderStyle', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );

					assertPlainTableStyle(
						editor,
						'border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBorderColor', () => {
					model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );

					assertPlainTableStyle(
						editor,
						'border-color:red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBorderWidth', () => {
					model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) );

					assertPlainTableStyle(
						editor,
						'border-width:1px;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'border shorthand', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) );

					assertPlainTableStyle(
						editor,
						'border:1px dotted red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableAlignment right', () => {
					model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) );

					assertPlainTableStyle(
						editor,
						'float:right;',
						'table table-style-align-right',
						'align="right"'
					);
				} );

				it( 'tableAlignment blockRight', () => {
					model.change( writer => writer.setAttribute( 'tableAlignment', 'blockRight', table ) );

					assertPlainTableStyle( editor, 'margin-left:auto;margin-right:0;', 'table table-style-block-align-right' );
				} );

				it( 'tableAlignment left', () => {
					model.change( writer => writer.setAttribute( 'tableAlignment', 'left', table ) );

					assertPlainTableStyle(
						editor,
						'float:left;',
						'table table-style-align-left',
						'align="left"'
					);
				} );

				it( 'tableAlignment blockLeft', () => {
					model.change( writer => writer.setAttribute( 'tableAlignment', 'blockLeft', table ) );

					assertPlainTableStyle( editor, 'margin-left:0;margin-right:auto;', 'table table-style-block-align-left' );
				} );

				it( ' with TableLayout plugin', async () => {
					const testEditor = await ClassicTestEditor.create( editorElement, {
						plugins: [ Paragraph, Table, TableCaption, TableLayout, TableProperties, ClipboardPipeline ]
					} );

					_setModelData(
						testEditor.model,
						'<table>' +
							'<tableRow>' +
								'<tableCell>' +
									'<paragraph>foo</paragraph>' +
								'</tableCell>' +
							'</tableRow>' +
						'</table>'
					);

					const table = testEditor.model.document.getRoot().getNodeByPath( [ 0 ] );

					testEditor.model.change( writer => writer.setAttribute( 'tableType', 'layout', table ) );
					testEditor.model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) );

					expect( getClipboardData( testEditor ) ).to.equalMarkup(
						'<table class="table table-style-align-right layout-table" ' +
						'style="float:right;" ' +
						'align="right" role="presentation">' +
							'<tbody><tr><td>foo</td></tr></tbody>' +
						'</table>'
					);

					await testEditor.destroy();
				} );

				it( 'tableAlignment blockRight with TableLayout plugin', async () => {
					const testEditor = await ClassicTestEditor.create( editorElement, {
						plugins: [ Paragraph, Table, TableCaption, TableLayout, TableProperties, ClipboardPipeline ]
					} );

					_setModelData(
						testEditor.model,
						'<table>' +
							'<tableRow>' +
								'<tableCell>' +
									'<paragraph>foo</paragraph>' +
								'</tableCell>' +
							'</tableRow>' +
						'</table>'
					);

					const table = testEditor.model.document.getRoot().getNodeByPath( [ 0 ] );

					testEditor.model.change( writer => writer.setAttribute( 'tableType', 'layout', table ) );
					testEditor.model.change( writer => writer.setAttribute( 'tableAlignment', 'blockRight', table ) );

					expect( getClipboardData( testEditor ) ).to.equalMarkup(
						'<table class="table table-style-block-align-right layout-table" ' +
						'style="margin-left:auto;margin-right:0;" role="presentation">' +
							'<tbody><tr><td>foo</td></tr></tbody>' +
						'</table>'
					);

					await testEditor.destroy();
				} );

				it( 'tableAlignment left with TableLayout plugin', async () => {
					const testEditor = await ClassicTestEditor.create( editorElement, {
						plugins: [ Paragraph, Table, TableCaption, TableLayout, TableProperties, ClipboardPipeline ]
					} );

					_setModelData(
						testEditor.model,
						'<table>' +
							'<tableRow>' +
								'<tableCell>' +
									'<paragraph>foo</paragraph>' +
								'</tableCell>' +
							'</tableRow>' +
						'</table>'
					);

					const table = testEditor.model.document.getRoot().getNodeByPath( [ 0 ] );

					testEditor.model.change( writer => writer.setAttribute( 'tableType', 'layout', table ) );
					testEditor.model.change( writer => writer.setAttribute( 'tableAlignment', 'left', table ) );

					expect( getClipboardData( testEditor ) ).to.equalMarkup(
						'<table class="table table-style-align-left layout-table" ' +
						'style="float:left;" align="left" role="presentation">' +
							'<tbody><tr><td>foo</td></tr></tbody>' +
						'</table>'
					);

					await testEditor.destroy();
				} );

				it( 'tableAlignment blockLeft with TableLayout plugin', async () => {
					const testEditor = await ClassicTestEditor.create( editorElement, {
						plugins: [ Paragraph, Table, TableCaption, TableLayout, TableProperties, ClipboardPipeline ]
					} );

					_setModelData(
						testEditor.model,
						'<table>' +
							'<tableRow>' +
								'<tableCell>' +
									'<paragraph>foo</paragraph>' +
								'</tableCell>' +
							'</tableRow>' +
						'</table>'
					);

					const table = testEditor.model.document.getRoot().getNodeByPath( [ 0 ] );

					testEditor.model.change( writer => writer.setAttribute( 'tableType', 'layout', table ) );
					testEditor.model.change( writer => writer.setAttribute( 'tableAlignment', 'blockLeft', table ) );

					expect( getClipboardData( testEditor ) ).to.equalMarkup(
						'<table class="table table-style-block-align-left layout-table" ' +
						'style="margin-left:0;margin-right:auto;" role="presentation">' +
							'<tbody><tr><td>foo</td></tr></tbody>' +
						'</table>'
					);

					await testEditor.destroy();
				} );

				it( 'tableWidth', () => {
					model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;width:500px;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableHeight', () => {
					model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) );

					assertPlainTableStyle(
						editor,
						'height:500px;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBackgroundColor', () => {
					model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) );

					assertPlainTableStyle(
						editor,
						'background-color:red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'and not strip figure element when PlainTableOutput is not loaded and not in clipboard pipeline', async () => {
					model.change( writer => {
						writer.setAttribute( 'tableBorderWidth', '2px', table );
						writer.setAttribute( 'tableBorderColor', 'red', table );
						writer.setAttribute( 'tableBorderStyle', 'solid', table );
						writer.setAttribute( 'tableBackgroundColor', 'blue', table );
					} );

					expect( editor.getData() ).to.equalMarkup(
						'<figure class="table">' +
							'<table style="background-color:blue;border:2px solid red;">' +
								'<tbody>' +
									'<tr><td>foo</td></tr>' +
								'</tbody>' +
							'</table>' +
						'</figure>'
					);
				} );
			} );

			describe( 'should remove attribute', () => {
				let table;

				beforeEach( () => {
					table = createEmptyTable();
				} );

				it( 'tableBorderStyle', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );

					assertPlainTableStyle(
						editor,
						'border-color:red;border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.setAttribute( 'tableBorderStyle', '', table ) );

					assertPlainTableStyle(
						editor,
						'border-color:red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBorderColor', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );

					assertPlainTableStyle(
						editor,
						'border-color:red;border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.setAttribute( 'tableBorderColor', '', table ) );

					assertPlainTableStyle(
						editor,
						'border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBorderWidth', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) );

					assertPlainTableStyle(
						editor,
						'border-style:dotted;border-width:1px;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) );

					assertPlainTableStyle(
						editor,
						'border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'from border shorthand', () => {
					model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );
					model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) );

					assertPlainTableStyle(
						editor,
						'border:1px dotted red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) );

					assertPlainTableStyle(
						editor,
						'border-color:red;border-style:dotted;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableAlignment', () => {
					model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) );

					assertPlainTableStyle(
						editor,
						'float:right;',
						'table table-style-align-right',
						'align="right"'
					);

					model.change( writer => writer.removeAttribute( 'tableAlignment', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableWidth', () => {
					model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;width:500px;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.removeAttribute( 'tableWidth', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableHeight', () => {
					model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) );

					assertPlainTableStyle(
						editor,
						'height:500px;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.removeAttribute( 'tableHeight', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );

				it( 'tableBackgroundColor', () => {
					model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) );

					assertPlainTableStyle(
						editor,
						'background-color:red;margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);

					model.change( writer => writer.removeAttribute( 'tableBackgroundColor', table ) );

					assertPlainTableStyle(
						editor,
						'margin-left:auto;margin-right:auto;',
						'table table-style-align-center',
						'align="center"'
					);
				} );
			} );

			describe( 'should not create attribute', () => {
				let table, testEditor;

				beforeEach( async () => {
					testEditor = await ClassicTestEditor.create( editorElement, {
						plugins: [ Paragraph, Table, ClipboardPipeline ]
					} );

					model = testEditor.model;
					table = createEmptyTable();
				} );

				afterEach( async () => {
					await testEditor.destroy();
				} );

				it( 'tableBorderStyle without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableBorderStyle', 'dotted', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableBorderColor without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableBorderColor', 'red', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableBorderWidth without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableBorderWidth', '1px', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'border shorthand without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableBorderStyle', 'dotted', table )
					);
					model.change( writer =>
						writer.setAttribute( 'tableBorderColor', 'red', table )
					);
					model.change( writer =>
						writer.setAttribute( 'tableBorderWidth', '1px', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableAlignment without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableAlignment', 'right', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableWidth without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableWidth', '500px', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableHeight without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableHeight', '500px', table )
					);

					assertPlainTableStyle( testEditor );
				} );

				it( 'tableBackgroundColor without TableProperties plugin', () => {
					model.change( writer =>
						writer.setAttribute( 'tableBackgroundColor', 'red', table )
					);

					assertPlainTableStyle( testEditor );
				} );
			} );

			it( 'should not convert image captions', async () => {
				const testEditor = await ClassicTestEditor.create( editorElement, {
					plugins: [ ArticlePluginSet, Table, TableCaption, ClipboardPipeline ],
					image: { toolbar: [ '|' ] }
				} );

				testEditor.setData(
					'<figure class="image">' +
						'<img src="/assets/sample.png" />' +
						'<figcaption>Caption</figcaption>' +
					'</figure>'
				);

				expect( testEditor.getData() ).to.equal(
					'<figure class="image">' +
						'<img src="/assets/sample.png">' +
						'<figcaption>Caption</figcaption>' +
					'</figure>'
				);

				await testEditor.destroy();
			} );

			// See: https://github.com/ckeditor/ckeditor5/issues/11394
			it( 'should allow overriding image caption converters', async () => {
				const testEditor = await ClassicTestEditor.create( editorElement, {
					plugins: [ ArticlePluginSet, Table, TableCaption, ClipboardPipeline ],
					image: { toolbar: [ '|' ] }
				} );

				testEditor.conversion.for( 'dataDowncast' ).elementToElement( {
					model: 'caption',
					view: ( modelElement, { writer } ) => {
						return writer.createContainerElement( 'foobar' );
					},
					converterPriority: 'high'
				} );

				testEditor.setData(
					'<figure class="image">' +
						'<img src="/assets/sample.png" />' +
						'<figcaption>Caption</figcaption>' +
					'</figure>'
				);

				expect( testEditor.getData() ).to.equal(
					'<figure class="image">' +
						'<img src="/assets/sample.png">' +
						'<foobar>Caption</foobar>' +
					'</figure>'
				);

				await testEditor.destroy();
			} );

			function createEmptyTable() {
				_setModelData(
					model,
					'<table>' +
						'<tableRow>' +
							'<tableCell>' +
								'<paragraph>foo</paragraph>' +
							'</tableCell>' +
						'</tableRow>' +
					'</table>'
				);

				return model.document.getRoot().getNodeByPath( [ 0 ] );
			}

			function assertPlainTableStyle( editor, tableStyle, tableClasses, align ) {
				const tableStyleEntry = tableStyle ? ` style="${ tableStyle }"` : '';
				const classes = tableClasses ? tableClasses : 'table';
				const alignAttribute = align ? ` ${ align }` : '';

				expect( getClipboardData( editor ) ).to.equalMarkup(
					`<table class="${ classes }"${ tableStyleEntry }${ alignAttribute }>` +
						'<tbody><tr><td>foo</td></tr></tbody>' +
					'</table>'
				);
			}
		} );
	} );
} );

function getClipboardData( editor ) {
	const viewFragment = editor.data.toView( editor.model.document.getRoot(), { isClipboardPipeline: true } );

	return editor.data.htmlProcessor.toData( viewFragment );
}
