diff --git a/packages/ckeditor5-table/package.json b/packages/ckeditor5-table/package.json
index 2aa89f0068a..55941c8ac8a 100644
--- a/packages/ckeditor5-table/package.json
+++ b/packages/ckeditor5-table/package.json
@@ -24,6 +24,7 @@
"@ckeditor/ckeditor5-editor-classic": "^32.0.0",
"@ckeditor/ckeditor5-engine": "^32.0.0",
"@ckeditor/ckeditor5-horizontal-line": "^32.0.0",
+ "@ckeditor/ckeditor5-html-support": "^32.0.0",
"@ckeditor/ckeditor5-image": "^32.0.0",
"@ckeditor/ckeditor5-indent": "^32.0.0",
"@ckeditor/ckeditor5-list": "^32.0.0",
@@ -35,6 +36,7 @@
"@ckeditor/ckeditor5-undo": "^32.0.0",
"@ckeditor/ckeditor5-utils": "^32.0.0",
"@ckeditor/ckeditor5-widget": "^32.0.0",
+ "@ckeditor/ckeditor5-source-editing": "^32.0.0",
"json-diff": "^0.5.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
diff --git a/packages/ckeditor5-table/src/plaintableoutput.js b/packages/ckeditor5-table/src/plaintableoutput.js
new file mode 100644
index 00000000000..f538c68a0b5
--- /dev/null
+++ b/packages/ckeditor5-table/src/plaintableoutput.js
@@ -0,0 +1,153 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module table/plaintableoutput
+ */
+
+import { Plugin } from 'ckeditor5/src/core';
+import Table from './table';
+
+/**
+ * The plain table output feature.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class PlainTableOutput extends Plugin {
+ /**
+ * @inheritDoc
+ */
+ static get pluginName() {
+ return 'PlainTableOutput';
+ }
+
+ /**
+ * @inheritDoc
+ */
+ static get requires() {
+ return [ Table ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ init() {
+ const editor = this.editor;
+
+ // Override default table data downcast converter.
+ editor.conversion.for( 'dataDowncast' ).elementToStructure( {
+ model: 'table',
+ view: downcastTableElement,
+ converterPriority: 'high'
+ } );
+
+ // Make sure table
is always downcasted to in the data pipeline.
+ if ( editor.plugins.has( 'TableCaption' ) ) {
+ editor.conversion.for( 'dataDowncast' ).elementToElement( {
+ model: 'caption',
+ view: ( modelElement, { writer } ) => {
+ if ( modelElement.parent.name === 'table' ) {
+ return writer.createContainerElement( 'caption' );
+ }
+
+ return writer.createContainerElement( 'figcaption' );
+ },
+ converterPriority: 'high'
+ } );
+ }
+
+ // Handle border-style, border-color, border-width and background-color table attributes.
+ if ( editor.plugins.has( 'TableProperties' ) ) {
+ downcastTableBorderAndBackgroundAttributes( editor );
+ }
+ }
+}
+
+// The plain table downcast converter callback.
+//
+// @private
+// @param {module:engine/model/element~Element} Table model element.
+// @param {module:engine/conversion/downcastdispatcher~DowncastConversionApi} conversionApi The conversion API object.
+// @returns {module:engine/view/containerelement~ContainerElement} Created element.
+function downcastTableElement( table, { writer } ) {
+ const headingRows = table.getAttribute( 'headingRows' ) || 0;
+
+ // Table head rows slot.
+ const headRowsSlot = writer.createSlot( element =>
+ element.is( 'element', 'tableRow' ) && element.index < headingRows
+ );
+
+ // Table body rows slot.
+ const bodyRowsSlot = writer.createSlot( element =>
+ element.is( 'element', 'tableRow' ) && element.index >= headingRows
+ );
+
+ // Table children slot.
+ const childrenSlot = writer.createSlot( element => !element.is( 'element', 'tableRow' ) );
+
+ // Table element with all the heading rows.
+ const theadElement = writer.createContainerElement( 'thead', null, headRowsSlot );
+
+ // Table element with all the body rows.
+ const tbodyElement = writer.createContainerElement( 'tbody', null, bodyRowsSlot );
+
+ // Table contents element containing and when necessary.
+ const tableContentElements = [];
+
+ if ( headingRows ) {
+ tableContentElements.push( theadElement );
+ }
+
+ if ( headingRows < table.childCount ) {
+ tableContentElements.push( tbodyElement );
+ }
+
+ // Create table structure.
+ //
+ //
+ // {children-slot-like-caption}
+ //
+ // {table-head-rows-slot}
+ //
+ //
+ // {table-body-rows-slot}
+ //
+ //
+ return writer.createContainerElement( 'table', null, [ childrenSlot, ...tableContentElements ] );
+}
+
+// Register table border and background attributes converters.
+//
+// @private
+// @param {module:core/editor/editor~Editor} editor
+function downcastTableBorderAndBackgroundAttributes( editor ) {
+ const modelAttributes = {
+ 'border-width': 'tableBorderWidth',
+ 'border-color': 'tableBorderColor',
+ 'border-style': 'tableBorderStyle',
+ 'background-color': 'tableBackgroundColor'
+ };
+
+ for ( const [ styleName, modelAttribute ] of Object.entries( modelAttributes ) ) {
+ editor.conversion.for( 'dataDowncast' ).add( dispatcher => {
+ return dispatcher.on( `attribute:${ modelAttribute }:table`, ( evt, data, conversionApi ) => {
+ const { item, attributeNewValue } = data;
+ const { mapper, writer } = conversionApi;
+
+ if ( !conversionApi.consumable.consume( item, evt.name ) ) {
+ return;
+ }
+
+ const table = mapper.toViewElement( item );
+
+ if ( attributeNewValue ) {
+ writer.setStyle( styleName, attributeNewValue, table );
+ } else {
+ writer.removeStyle( styleName, table );
+ }
+ }, { priority: 'high' } );
+ } );
+ }
+}
diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.html b/packages/ckeditor5-table/tests/manual/plaintableoutput.html
new file mode 100644
index 00000000000..3ad135adf2e
--- /dev/null
+++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.html
@@ -0,0 +1,32 @@
+
+
+ Monthly savings
+
+ Month |
+ Savings |
+
+
+ January |
+ $100 |
+
+
+
+
+
+
+
+
diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.js b/packages/ckeditor5-table/tests/manual/plaintableoutput.js
new file mode 100644
index 00000000000..48647d4c6cb
--- /dev/null
+++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.js
@@ -0,0 +1,90 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals console, document, window */
+
+import { formatHtml } from '@ckeditor/ckeditor5-source-editing/src/utils/formathtml';
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting';
+import GeneralHtmlSupport from '@ckeditor/ckeditor5-html-support/src/generalhtmlsupport';
+import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
+import TableSelection from '../../src/tableselection';
+import TableClipboard from '../../src/tableclipboard';
+import TableProperties from '../../src/tableproperties';
+import TableCellProperties from '../../src/tablecellproperties';
+import TableCaption from '../../src/tablecaption';
+import PlainTableOutput from '../../src/plaintableoutput';
+
+ClassicEditor
+ .create( document.querySelector( '#editor' ), {
+ plugins: [
+ ArticlePluginSet,
+ Table,
+ TableToolbar,
+ TableSelection,
+ TableClipboard,
+ TableProperties,
+ TableCellProperties,
+ TableCaption,
+ PlainTableOutput,
+ GeneralHtmlSupport,
+ SourceEditing
+ ],
+ toolbar: [
+ 'heading',
+ '|',
+ 'insertTable',
+ '|',
+ 'bold',
+ 'italic',
+ 'link',
+ '|',
+ 'bulletedList',
+ 'numberedList',
+ 'blockQuote',
+ '|',
+ 'undo',
+ 'redo',
+ 'sourceEditing'
+ ],
+ table: {
+ contentToolbar: [
+ 'tableColumn',
+ 'tableRow',
+ 'mergeTableCells',
+ 'tableProperties',
+ 'tableCellProperties',
+ 'toggleTableCaption'
+ ]
+ },
+ image: {
+ toolbar: [ 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side', '|', 'imageTextAlternative' ]
+ },
+ htmlSupport: {
+ allow: [
+ {
+ name: /^(table|tbody|thead|tr|td|th|caption)$/,
+ attributes: true,
+ classes: true,
+ styles: true
+ }
+ ]
+ }
+ } )
+ .then( editor => {
+ window.editor = editor;
+
+ const element = document.getElementById( 'editor-data' );
+ element.innerText = formatHtml( editor.getData() );
+
+ editor.model.document.on( 'change:data', () => {
+ element.innerText = formatHtml( editor.getData() );
+ } );
+ } )
+ .catch( err => {
+ console.error( err.stack );
+ } );
diff --git a/packages/ckeditor5-table/tests/manual/plaintableoutput.md b/packages/ckeditor5-table/tests/manual/plaintableoutput.md
new file mode 100644
index 00000000000..b916454a9be
--- /dev/null
+++ b/packages/ckeditor5-table/tests/manual/plaintableoutput.md
@@ -0,0 +1,35 @@
+### Plain table output
+
+Any table added to this editor should be downcasted in the data pipeline to the plain table HTML.
+
+It should have no `` or `` elements.
+
+The markup should be similar to this:
+
+```html
+
+ Monthly savings
+
+
+
+ Month
+ |
+
+ Savings
+ |
+
+
+
+ January
+ |
+
+ $100
+ |
+
+
+
+```
+
+#### Image captions
+
+Image captions should still be `` elements.
diff --git a/packages/ckeditor5-table/tests/plaintableoutput.js b/packages/ckeditor5-table/tests/plaintableoutput.js
new file mode 100644
index 00000000000..8947f1ee7c7
--- /dev/null
+++ b/packages/ckeditor5-table/tests/plaintableoutput.js
@@ -0,0 +1,481 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import Table from '../src/table';
+import PlainTableOutput from '../src/plaintableoutput';
+import { modelTable } from './_utils/utils';
+import TableCaption from '../src/tablecaption';
+import TableProperties from '../src/tableproperties';
+
+describe( 'PlainTableOutput', () => {
+ let editor, editorElement, model;
+
+ beforeEach( async () => {
+ editorElement = document.createElement( 'div' );
+ document.body.appendChild( editorElement );
+
+ editor = await ClassicTestEditor.create( editorElement, {
+ plugins: [ Paragraph, Table, TableCaption, TableProperties, PlainTableOutput ]
+ } );
+
+ model = editor.model;
+ } );
+
+ afterEach( async () => {
+ editorElement.remove();
+ await editor.destroy();
+ } );
+
+ it( 'requires Table', () => {
+ expect( PlainTableOutput.requires ).to.deep.equal( [ Table ] );
+ } );
+
+ it( 'should have pluginName', () => {
+ expect( PlainTableOutput.pluginName ).to.equal( 'PlainTableOutput' );
+ } );
+
+ describe( 'conversion in data pipeline', () => {
+ describe( 'model to view', () => {
+ it( 'should create tbody section', () => {
+ setModelData( model, modelTable( [
+ [ 'foo' ]
+ ] ) );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ '' +
+ 'foo |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should create heading rows', () => {
+ setModelData( model, modelTable( [
+ [ '1', '2' ],
+ [ '3', '4' ],
+ [ '5', '6' ]
+ ], { headingRows: 2 } ) );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ '' +
+ '1 | 2 |
' +
+ '3 | 4 |
' +
+ '' +
+ '' +
+ '5 | 6 |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should create heading columns', () => {
+ setModelData( model, modelTable( [
+ [ '1', '2' ],
+ [ '3', '4' ],
+ [ '5', '6' ]
+ ], { headingColumns: 1 } ) );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ '' +
+ '1 | 2 |
' +
+ '3 | 4 |
' +
+ '5 | 6 |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should create heading rows and columns', () => {
+ setModelData( model, modelTable( [
+ [ '1', '2' ],
+ [ '3', '4' ],
+ [ '5', '6' ]
+ ], { headingRows: 1, headingColumns: 1 } ) );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ '' +
+ '1 | 2 |
' +
+ '' +
+ '' +
+ '3 | 4 |
' +
+ '5 | 6 |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should work when heading rows number is bigger than number of rows', () => {
+ setModelData( model, modelTable( [
+ [ '1', '2' ],
+ [ '3', '4' ]
+ ], { headingRows: 3 } ) );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ '' +
+ '1 | 2 |
' +
+ '3 | 4 |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should create caption element', () => {
+ setModelData( model,
+ '' +
+ '' +
+ '1' +
+ '2' +
+ '' +
+ 'Foo' +
+ '
'
+ );
+
+ expect( editor.getData() ).to.equal(
+ '' +
+ 'Foo' +
+ '' +
+ '1 | 2 |
' +
+ '' +
+ '
'
+ );
+ } );
+
+ it( 'should not create caption element without TableCaption plugin', async () => {
+ const testEditor = await ClassicTestEditor.create( editorElement, {
+ plugins: [ Paragraph, Table, PlainTableOutput ]
+ } );
+
+ testEditor.setData(
+ '' +
+ 'Foo' +
+ '' +
+ '1 | 2 |
' +
+ '' +
+ '
'
+ );
+
+ expect( testEditor.getData() ).to.equal(
+ '' +
+ '' +
+ '1 | 2 |
' +
+ '' +
+ '
'
+ );
+
+ 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, '' );
+ } );
+
+ describe( 'should create attribute', () => {
+ let table;
+
+ beforeEach( () => {
+ table = createEmptyTable();
+ } );
+
+ it( 'tableBorderStyle', () => {
+ model.change( writer => writer.setAttribute( 'tableBorderStyle', 'dotted', table ) );
+
+ assertPlainTableStyle( editor, 'border-style:dotted;' );
+ } );
+
+ it( 'tableBorderColor', () => {
+ model.change( writer => writer.setAttribute( 'tableBorderColor', 'red', table ) );
+
+ assertPlainTableStyle( editor, 'border-color:red;' );
+ } );
+
+ it( 'tableBorderWidth', () => {
+ model.change( writer => writer.setAttribute( 'tableBorderWidth', '1px', table ) );
+
+ assertPlainTableStyle( editor, 'border-width:1px;' );
+ } );
+
+ 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;' );
+ } );
+
+ it( 'tableAlignment', () => {
+ model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) );
+
+ assertPlainTableStyle( editor, 'float:right;' );
+ } );
+
+ it( 'tableWidth', () => {
+ model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) );
+
+ assertPlainTableStyle( editor, 'width:500px;' );
+ } );
+
+ it( 'tableHeight', () => {
+ model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) );
+
+ assertPlainTableStyle( editor, 'height:500px;' );
+ } );
+
+ it( 'tableBackgroundColor', () => {
+ model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) );
+
+ assertPlainTableStyle( editor, 'background-color:red;' );
+ } );
+ } );
+
+ 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;' );
+
+ model.change( writer => writer.setAttribute( 'tableBorderStyle', '', table ) );
+
+ assertPlainTableStyle( editor, 'border-color:red;' );
+ } );
+
+ 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;' );
+
+ model.change( writer => writer.setAttribute( 'tableBorderColor', '', table ) );
+
+ assertPlainTableStyle( editor, 'border-style:dotted;' );
+ } );
+
+ 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;' );
+
+ model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) );
+
+ assertPlainTableStyle( editor, 'border-style:dotted;' );
+ } );
+
+ 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;' );
+
+ model.change( writer => writer.setAttribute( 'tableBorderWidth', '', table ) );
+
+ assertPlainTableStyle( editor, 'border-color:red;border-style:dotted;' );
+ } );
+
+ it( 'tableAlignment', () => {
+ model.change( writer => writer.setAttribute( 'tableAlignment', 'right', table ) );
+
+ assertPlainTableStyle( editor, 'float:right;' );
+
+ model.change( writer => writer.removeAttribute( 'tableAlignment', table ) );
+
+ assertPlainTableStyle( editor, '' );
+ } );
+
+ it( 'tableWidth', () => {
+ model.change( writer => writer.setAttribute( 'tableWidth', '500px', table ) );
+
+ assertPlainTableStyle( editor, 'width:500px;' );
+
+ model.change( writer => writer.removeAttribute( 'tableWidth', table ) );
+
+ assertPlainTableStyle( editor, '' );
+ } );
+
+ it( 'tableHeight', () => {
+ model.change( writer => writer.setAttribute( 'tableHeight', '500px', table ) );
+
+ assertPlainTableStyle( editor, 'height:500px;' );
+
+ model.change( writer => writer.removeAttribute( 'tableHeight', table ) );
+
+ assertPlainTableStyle( editor, '' );
+ } );
+
+ it( 'tableBackgroundColor', () => {
+ model.change( writer => writer.setAttribute( 'tableBackgroundColor', 'red', table ) );
+
+ assertPlainTableStyle( editor, 'background-color:red;' );
+
+ model.change( writer => writer.removeAttribute( 'tableBackgroundColor', table ) );
+
+ assertPlainTableStyle( editor, '' );
+ } );
+ } );
+
+ describe( 'should not create attribute', () => {
+ let table, testEditor;
+
+ beforeEach( async () => {
+ testEditor = await ClassicTestEditor.create( editorElement, {
+ plugins: [ Paragraph, Table, PlainTableOutput ]
+ } );
+
+ 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, PlainTableOutput ],
+ image: { toolbar: [ '|' ] }
+ } );
+
+ testEditor.setData(
+ ''
+ );
+
+ expect( testEditor.getData() ).to.equal(
+ ''
+ );
+
+ testEditor.destroy();
+ } );
+
+ function createEmptyTable() {
+ setModelData(
+ model,
+ '' +
+ '' +
+ '' +
+ 'foo' +
+ '' +
+ '' +
+ '
'
+ );
+
+ return model.document.getRoot().getNodeByPath( [ 0 ] );
+ }
+
+ function assertPlainTableStyle( editor, tableStyle ) {
+ const tableStyleEntry = tableStyle ? ` style="${ tableStyle }"` : '';
+
+ expect( editor.getData() ).to.equalMarkup(
+ `'
+ );
+ }
+ } );
+ } );
+} );
diff --git a/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js b/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js
new file mode 100644
index 00000000000..94acda21d54
--- /dev/null
+++ b/packages/ckeditor5-table/tests/tablecaption/tablecaptionui.js
@@ -0,0 +1,118 @@
+/**
+ * @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import TableCaptionEditing from '../../src/tablecaption/tablecaptionediting';
+import TableCaptionUI from '../../src/tablecaption/tablecaptionui';
+import TableEditing from '../../src/tableediting';
+
+describe( 'TableCaptionUI', () => {
+ let editor, tableCaption, editorElement;
+
+ testUtils.createSinonSandbox();
+
+ beforeEach( () => {
+ editorElement = document.createElement( 'div' );
+ document.body.appendChild( editorElement );
+
+ return ClassicTestEditor.create( editorElement, {
+ plugins: [ Paragraph, TableEditing, TableCaptionEditing, TableCaptionUI ]
+ } ).then( newEditor => {
+ editor = newEditor;
+
+ tableCaption = editor.ui.componentFactory.create( 'toggleTableCaption' );
+ } );
+ } );
+
+ afterEach( () => {
+ editorElement.remove();
+ return editor.destroy();
+ } );
+
+ it( 'should register toggleTableCaption feature component', () => {
+ expect( tableCaption ).to.be.instanceOf( ButtonView );
+ expect( tableCaption.icon ).to.match( /