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

Tests: Added tests for TablePropertiesUI and TablePropertiesView. Code refactoring.

Aleksander Nowodzinski 6 лет назад
Родитель
Сommit
996105783e

+ 2 - 2
packages/ckeditor5-table/src/tableproperties/ui/tablepropertiesview.js

@@ -523,7 +523,7 @@ export default class TablePropertiesView extends View {
 		// -- Operator ---------------------------------------------------
 
 		const operatorLabel = new View( locale );
-		operatorLabel.setTemplate( { text: ' × ' } );
+		operatorLabel.setTemplate( { text: '×' } );
 
 		// -- Height ---------------------------------------------------
 
@@ -636,7 +636,7 @@ export default class TablePropertiesView extends View {
 
 		return {
 			left: t( 'Align table to the left' ),
-			center: t( 'Center the table' ),
+			center: t( 'Center table' ),
 			right: t( 'Align table to the right' )
 		};
 	}

+ 6 - 0
packages/ckeditor5-table/tests/tablecellproperties/tablecellpropertiesui.js

@@ -103,7 +103,13 @@ describe( 'table cell properties', () => {
 		} );
 
 		describe( 'destroy()', () => {
+			it( 'should destroy the #view', () => {
+				const spy = sinon.spy( tableCellPropertiesView, 'destroy' );
 
+				tableCellPropertiesUI.destroy();
+
+				sinon.assert.calledOnce( spy );
+			} );
 		} );
 
 		describe( 'Properties #view', () => {

+ 4 - 3
packages/ckeditor5-table/tests/tablecellproperties/ui/tablecellpropertiesview.js

@@ -57,6 +57,7 @@ describe( 'table cell properties', () => {
 			it( 'should create element from template', () => {
 				expect( view.element.classList.contains( 'ck' ) ).to.be.true;
 				expect( view.element.classList.contains( 'ck-form' ) ).to.be.true;
+				expect( view.element.classList.contains( 'ck-table-form' ) ).to.be.true;
 				expect( view.element.classList.contains( 'ck-table-cell-properties-form' ) ).to.be.true;
 				expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
 			} );
@@ -88,7 +89,7 @@ describe( 'table cell properties', () => {
 						const row = view.element.childNodes[ 1 ];
 
 						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
-						expect( row.classList.contains( 'ck-table-cell-properties-form__border-row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-form__border-row' ) ).to.be.true;
 						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Border' );
 						expect( row.childNodes[ 1 ] ).to.equal( view.borderStyleDropdown.element );
 						expect( row.childNodes[ 2 ] ).to.equal( view.borderColorInput.element );
@@ -104,7 +105,7 @@ describe( 'table cell properties', () => {
 
 						it( 'should have properties set', () => {
 							expect(	labeledDropdown.label ).to.equal( 'Style' );
-							expect(	labeledDropdown.class ).to.equal( 'ck-table-cell-properties-form__border-style' );
+							expect(	labeledDropdown.class ).to.equal( 'ck-table-form__border-style' );
 						} );
 
 						it( 'should have a button with properties set', () => {
@@ -148,7 +149,7 @@ describe( 'table cell properties', () => {
 						it( 'should be created', () => {
 							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
 							expect( labeledInput.label ).to.equal( 'Width' );
-							expect( labeledInput.class ).to.equal( 'ck-table-cell-properties-form__border-width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__border-width' );
 						} );
 
 						it( 'should reflect #borderWidth property', () => {

+ 3 - 3
packages/ckeditor5-table/tests/tableproperties/tablepropertiesediting.js

@@ -7,7 +7,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 
 import TableEditing from '../../src/tableediting';
-import TableProperties from '../../src/tableproperties';
+import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
 
 import TableBorderColorCommand from '../../src/tableproperties/commands/tablebordercolorcommand';
 import TableBorderStyleCommand from '../../src/tableproperties/commands/tableborderstylecommand';
@@ -28,7 +28,7 @@ describe( 'table properties', () => {
 		beforeEach( () => {
 			return VirtualTestEditor
 				.create( {
-					plugins: [ TableProperties, Paragraph, TableEditing ]
+					plugins: [ TablePropertiesEditing, Paragraph, TableEditing ]
 				} )
 				.then( newEditor => {
 					editor = newEditor;
@@ -42,7 +42,7 @@ describe( 'table properties', () => {
 		} );
 
 		it( 'should have pluginName', () => {
-			expect( TableProperties.pluginName ).to.equal( 'TableProperties' );
+			expect( TablePropertiesEditing.pluginName ).to.equal( 'TablePropertiesEditing' );
 		} );
 
 		it( 'adds tableBorderColor command', () => {

+ 354 - 0
packages/ckeditor5-table/tests/tableproperties/tablepropertiesui.js

@@ -0,0 +1,354 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals document, Event */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+
+import Table from '../../src/table';
+import TablePropertiesEditing from '../../src/tableproperties/tablepropertiesediting';
+import TablePropertiesUI from '../../src/tableproperties/tablepropertiesui';
+import TablePropertiesUIView from '../../src/tableproperties/ui/tablepropertiesview';
+
+describe( 'table properties', () => {
+	describe( 'TablePropertiesUI', () => {
+		let editor, editorElement, contextualBalloon,
+			tablePropertiesUI, tablePropertiesView, tablePropertiesButton;
+
+		testUtils.createSinonSandbox();
+
+		beforeEach( () => {
+			editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Table, TablePropertiesEditing, TablePropertiesUI, Paragraph, Undo ],
+					initialData: '<table><tr><td>foo</td></tr></table><p>bar</p>'
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					tablePropertiesUI = editor.plugins.get( TablePropertiesUI );
+					tablePropertiesButton = editor.ui.componentFactory.create( 'tableProperties' );
+					contextualBalloon = editor.plugins.get( ContextualBalloon );
+					tablePropertiesView = tablePropertiesUI.view;
+
+					// There is no point to execute BalloonPanelView attachTo and pin methods so lets override it.
+					testUtils.sinon.stub( contextualBalloon.view, 'attachTo' ).returns( {} );
+					testUtils.sinon.stub( contextualBalloon.view, 'pin' ).returns( {} );
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should be named', () => {
+			expect( TablePropertiesUI.pluginName ).to.equal( 'TablePropertiesUI' );
+		} );
+
+		it( 'should load ContextualBalloon', () => {
+			expect( editor.plugins.get( ContextualBalloon ) ).to.be.instanceOf( ContextualBalloon );
+		} );
+
+		describe( 'init()', () => {
+			it( 'should set a batch', () => {
+				expect( tablePropertiesUI._undoStepBatch ).to.be.null;
+			} );
+
+			describe( '#view', () => {
+				it( 'should be created', () => {
+					expect( tablePropertiesUI.view ).to.be.instanceOf( TablePropertiesUIView );
+				} );
+
+				it( 'should be rendered', () => {
+					expect( tablePropertiesUI.view.isRendered ).to.be.true;
+				} );
+			} );
+
+			describe( 'toolbar button', () => {
+				it( 'should be registered', () => {
+					expect( tablePropertiesButton ).to.be.instanceOf( ButtonView );
+				} );
+
+				it( 'should have a label', () => {
+					expect( tablePropertiesButton.label ).to.equal( 'Table properties' );
+				} );
+
+				it( 'should have a tooltip', () => {
+					expect( tablePropertiesButton.tooltip ).to.be.true;
+				} );
+
+				it( 'should call #_showView upon #execute', () => {
+					const spy = testUtils.sinon.stub( tablePropertiesUI, '_showView' ).returns( {} );
+
+					tablePropertiesButton.fire( 'execute' );
+					sinon.assert.calledOnce( spy );
+				} );
+			} );
+		} );
+
+		describe( 'destroy()', () => {
+			it( 'should destroy the #view', () => {
+				const spy = sinon.spy( tablePropertiesView, 'destroy' );
+
+				tablePropertiesUI.destroy();
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+
+		describe( 'Properties #view', () => {
+			beforeEach( () => {
+				editor.model.change( writer => {
+					writer.setSelection( editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ), 0 );
+				} );
+			} );
+
+			it( 'should hide on #submit', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				tablePropertiesView.fire( 'submit' );
+				expect( contextualBalloon.visibleView ).to.be.null;
+			} );
+
+			it( 'should undo the entire batch of changes on #cancel', () => {
+				// Show the view. New batch will be created.
+				tablePropertiesButton.fire( 'execute' );
+
+				// Do the changes like a user.
+				tablePropertiesView.borderStyle = 'dotted';
+				tablePropertiesView.backgroundColor = 'red';
+
+				expect( getModelData( editor.model ) ).to.equal(
+					'<table backgroundColor="red" borderStyle="dotted">' +
+						'<tableRow>' +
+							'<tableCell>' +
+								'<paragraph>[]foo</paragraph>' +
+							'</tableCell>' +
+						'</tableRow>' +
+					'</table>' +
+					'<paragraph>bar</paragraph>'
+				);
+
+				tablePropertiesView.fire( 'cancel' );
+
+				expect( getModelData( editor.model ) ).to.equal(
+					'<table>' +
+						'<tableRow>' +
+							'<tableCell>' +
+								'<paragraph>[]foo</paragraph>' +
+							'</tableCell>' +
+						'</tableRow>' +
+					'</table>' +
+					'<paragraph>bar</paragraph>'
+				);
+			} );
+
+			it( 'should hide on #cancel', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				tablePropertiesView.fire( 'cancel' );
+				expect( contextualBalloon.visibleView ).to.be.null;
+			} );
+
+			it( 'should hide on the Esc key press', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.esc,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				tablePropertiesView.keystrokes.press( keyEvtData );
+				expect( contextualBalloon.visibleView ).to.be.null;
+			} );
+
+			it( 'should hide if the table is no longer selected on EditorUI#update', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				editor.model.change( writer => {
+					// Set selection in the paragraph.
+					writer.setSelection( editor.model.document.getRoot().getChild( 1 ), 0 );
+				} );
+
+				expect( contextualBalloon.visibleView ).to.be.null;
+			} );
+
+			it( 'should reposition if table is still selected on on EditorUI#update', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				editor.model.change( writer => {
+					writer.insertText( 'qux', editor.model.document.selection.getFirstPosition() );
+				} );
+
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+			} );
+
+			it( 'should hide if clicked outside the balloon', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				document.body.dispatchEvent( new Event( 'mousedown', { bubbles: true } ) );
+
+				expect( contextualBalloon.visibleView ).to.be.null;
+			} );
+
+			describe( 'property changes', () => {
+				it( 'should affect the editor state', () => {
+					const spy = testUtils.sinon.stub( editor, 'execute' );
+
+					tablePropertiesUI._undoStepBatch = 'foo';
+					tablePropertiesView.borderStyle = 'dotted';
+
+					sinon.assert.calledOnce( spy );
+					sinon.assert.calledWithExactly( spy, 'tableBorderStyle', { value: 'dotted', batch: 'foo' } );
+				} );
+
+				it( 'should not affect the editor state if internal property has changed', () => {
+					const spy = testUtils.sinon.stub( editor, 'execute' );
+
+					tablePropertiesView.set( 'internalProp', 'foo' );
+
+					tablePropertiesUI._undoStepBatch = 'foo';
+					tablePropertiesView.internalProp = 'bar';
+
+					sinon.assert.notCalled( spy );
+				} );
+			} );
+		} );
+
+		describe( 'Showing the #view', () => {
+			beforeEach( () => {
+				editor.model.change( writer => {
+					writer.setSelection( editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ), 0 );
+				} );
+			} );
+
+			it( 'should create a new undoable batch for further #view cancel', () => {
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				const firstBatch = tablePropertiesUI._undoStepBatch;
+				expect( firstBatch ).to.be.instanceOf( Batch );
+
+				tablePropertiesView.fire( 'submit' );
+				expect( contextualBalloon.visibleView ).to.be.null;
+
+				tablePropertiesButton.fire( 'execute' );
+
+				const secondBatch = tablePropertiesUI._undoStepBatch;
+				expect( secondBatch ).to.be.instanceOf( Batch );
+				expect( firstBatch ).to.not.equal( secondBatch );
+			} );
+
+			describe( 'initial data', () => {
+				it( 'should be set from the command values', () => {
+					editor.commands.get( 'tableBorderStyle' ).value = 'a';
+					editor.commands.get( 'tableBorderColor' ).value = 'b';
+					editor.commands.get( 'tableBorderWidth' ).value = 'c';
+					editor.commands.get( 'tableBackgroundColor' ).value = 'd';
+					editor.commands.get( 'tableWidth' ).value = 'e';
+					editor.commands.get( 'tableHeight' ).value = 'f';
+					editor.commands.get( 'tableAlignment' ).value = 'g';
+
+					tablePropertiesButton.fire( 'execute' );
+
+					expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+					expect( tablePropertiesView ).to.include( {
+						borderStyle: 'a',
+						borderColor: 'b',
+						borderWidth: 'c',
+						backgroundColor: 'd',
+						width: 'e',
+						height: 'f',
+						alignment: 'g'
+					} );
+				} );
+
+				it( 'should use default values when command has no value', () => {
+					editor.commands.get( 'tableBorderStyle' ).value = null;
+					editor.commands.get( 'tableBorderColor' ).value = null;
+					editor.commands.get( 'tableBorderWidth' ).value = null;
+					editor.commands.get( 'tableBackgroundColor' ).value = null;
+					editor.commands.get( 'tableWidth' ).value = null;
+					editor.commands.get( 'tableHeight' ).value = null;
+					editor.commands.get( 'tableAlignment' ).value = null;
+
+					tablePropertiesButton.fire( 'execute' );
+
+					expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+					expect( tablePropertiesView ).to.include( {
+						borderStyle: 'none',
+						borderColor: '',
+						borderWidth: '',
+						backgroundColor: '',
+						width: '',
+						height: '',
+						alignment: 'center'
+					} );
+				} );
+			} );
+
+			it( 'should focus the form view', () => {
+				const spy = testUtils.sinon.spy( tablePropertiesView, 'focus' );
+
+				tablePropertiesButton.fire( 'execute' );
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+
+		describe( 'Hiding the #view', () => {
+			beforeEach( () => {
+				editor.model.change( writer => {
+					writer.setSelection( editor.model.document.getRoot().getChild( 0 ).getChild( 0 ).getChild( 0 ), 0 );
+				} );
+			} );
+
+			it( 'should stop listening to EditorUI#update', () => {
+				const spy = testUtils.sinon.spy( tablePropertiesUI, 'stopListening' );
+
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				tablePropertiesView.fire( 'submit' );
+				expect( contextualBalloon.visibleView ).to.be.null;
+
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWithExactly( spy, editor.ui, 'update' );
+			} );
+
+			it( 'should focus the editing view so the focus is not lost', () => {
+				const spy = testUtils.sinon.spy( editor.editing.view, 'focus' );
+
+				tablePropertiesButton.fire( 'execute' );
+				expect( contextualBalloon.visibleView ).to.equal( tablePropertiesView );
+
+				tablePropertiesView.fire( 'submit' );
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+} );

+ 547 - 0
packages/ckeditor5-table/tests/tableproperties/ui/tablepropertiesview.js

@@ -0,0 +1,547 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/* globals Event */
+
+import TablePropertiesView from '../../../src/tableproperties/ui/tablepropertiesview';
+import LabeledView from '@ckeditor/ckeditor5-ui/src/labeledview/labeledview';
+import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
+
+describe( 'table properties', () => {
+	describe( 'TablePropertiesView', () => {
+		let view, locale;
+
+		testUtils.createSinonSandbox();
+
+		beforeEach( () => {
+			locale = { t: val => val };
+			view = new TablePropertiesView( locale );
+			view.render();
+		} );
+
+		afterEach( () => {
+			view.destroy();
+		} );
+
+		describe( 'constructor()', () => {
+			it( 'should set view#locale', () => {
+				expect( view.locale ).to.equal( locale );
+			} );
+
+			it( 'should create view#children collection', () => {
+				expect( view.children ).to.be.instanceOf( ViewCollection );
+			} );
+
+			it( 'should define the public data interface (observable properties)', () => {
+				expect( view ).to.include( {
+					borderStyle: 'none',
+					borderWidth: '',
+					borderColor: '',
+					backgroundColor: '',
+					width: '',
+					height: '',
+					alignment: 'center'
+				} );
+			} );
+
+			it( 'should create element from template', () => {
+				expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+				expect( view.element.classList.contains( 'ck-form' ) ).to.be.true;
+				expect( view.element.classList.contains( 'ck-table-form' ) ).to.be.true;
+				expect( view.element.classList.contains( 'ck-table-properties-form' ) ).to.be.true;
+				expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
+			} );
+
+			it( 'should create child views (and references)', () => {
+				expect( view.borderStyleDropdown ).to.be.instanceOf( LabeledView );
+				expect( view.borderWidthInput ).to.be.instanceOf( LabeledView );
+				expect( view.borderColorInput ).to.be.instanceOf( LabeledView );
+				expect( view.backgroundInput ).to.be.instanceOf( LabeledView );
+				expect( view.widthInput ).to.be.instanceOf( LabeledView );
+				expect( view.heightInput ).to.be.instanceOf( LabeledView );
+				expect( view.alignmentToolbar ).to.be.instanceOf( ToolbarView );
+
+				expect( view.saveButtonView ).to.be.instanceOf( ButtonView );
+				expect( view.cancelButtonView ).to.be.instanceOf( ButtonView );
+			} );
+
+			it( 'should have a header', () => {
+				const header = view.element.firstChild;
+
+				expect( header.classList.contains( 'ck' ) ).to.be.true;
+				expect( header.classList.contains( 'ck-form__header' ) ).to.be.true;
+				expect( header.textContent ).to.equal( 'Table properties' );
+			} );
+
+			describe( 'form rows', () => {
+				describe( 'border row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 1 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-form__border-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Border' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.borderStyleDropdown.element );
+						expect( row.childNodes[ 2 ] ).to.equal( view.borderColorInput.element );
+						expect( row.childNodes[ 3 ] ).to.equal( view.borderWidthInput.element );
+					} );
+
+					describe( 'border style labeled dropdown', () => {
+						let labeledDropdown;
+
+						beforeEach( () => {
+							labeledDropdown = view.borderStyleDropdown;
+						} );
+
+						it( 'should have properties set', () => {
+							expect(	labeledDropdown.label ).to.equal( 'Style' );
+							expect(	labeledDropdown.class ).to.equal( 'ck-table-form__border-style' );
+						} );
+
+						it( 'should have a button with properties set', () => {
+							expect(	labeledDropdown.view.buttonView.isOn ).to.be.false;
+							expect(	labeledDropdown.view.buttonView.withText ).to.be.true;
+							expect(	labeledDropdown.view.buttonView.tooltip ).to.equal( 'Style' );
+						} );
+
+						it( 'should bind button\'s label to #borderStyle property', () => {
+							view.borderStyle = 'dotted';
+							expect( labeledDropdown.view.buttonView.label ).to.equal( 'Dotted' );
+
+							view.borderStyle = 'dashed';
+							expect( labeledDropdown.view.buttonView.label ).to.equal( 'Dashed' );
+						} );
+
+						it( 'should change #borderStyle when executed', () => {
+							labeledDropdown.view.listView.items.first.children.first.fire( 'execute' );
+							expect( view.borderStyle ).to.equal( 'none' );
+
+							labeledDropdown.view.listView.items.last.children.first.fire( 'execute' );
+							expect( view.borderStyle ).to.equal( 'outset' );
+						} );
+
+						it( 'should come with a set of pre–defined border styles', () => {
+							expect( labeledDropdown.view.listView.items.map( item => {
+								return item.children.first.label;
+							} ) ).to.have.ordered.members( [
+								'None', 'Solid', 'Dotted', 'Dashed', 'Double', 'Groove', 'Ridge', 'Inset', 'Outset',
+							] );
+						} );
+					} );
+
+					describe( 'border width input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.borderWidthInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-form__border-width' );
+						} );
+
+						it( 'should reflect #borderWidth property', () => {
+							view.borderWidth = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.borderWidth = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should be enabled only when #borderStyle is different than "none"', () => {
+							view.borderStyle = 'none';
+							expect( labeledInput.isEnabled ).to.be.false;
+
+							view.borderStyle = 'dotted';
+							expect( labeledInput.isEnabled ).to.be.true;
+						} );
+
+						it( 'should update #borderWidth on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.borderWidth ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.borderWidth ).to.equal( 'bar' );
+						} );
+					} );
+
+					describe( 'border color input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.borderColorInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Color' );
+						} );
+
+						it( 'should reflect #borderColor property', () => {
+							view.borderColor = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.borderColor = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should be enabled only when #borderStyle is different than "none"', () => {
+							view.borderStyle = 'none';
+							expect( labeledInput.isEnabled ).to.be.false;
+
+							view.borderStyle = 'dotted';
+							expect( labeledInput.isEnabled ).to.be.true;
+						} );
+
+						it( 'should update #borderColor on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.borderColor ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.borderColor ).to.equal( 'bar' );
+						} );
+					} );
+				} );
+
+				describe( 'background row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 2 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ] ).to.equal( view.backgroundInput.element );
+					} );
+
+					describe( 'background color input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.backgroundInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Background' );
+							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__background' );
+						} );
+
+						it( 'should reflect #backgroundColor property', () => {
+							view.backgroundColor = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.backgroundColor = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should update #backgroundColor on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.backgroundColor ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.backgroundColor ).to.equal( 'bar' );
+						} );
+					} );
+				} );
+
+				describe( 'dimensions row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 3 ].childNodes[ 0 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-properties-form__dimensions-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Dimensions' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.widthInput.element );
+						expect( row.childNodes[ 2 ].textContent ).to.equal( '×' );
+						expect( row.childNodes[ 3 ] ).to.equal( view.heightInput.element );
+					} );
+
+					describe( 'width input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.widthInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Width' );
+							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__width' );
+						} );
+
+						it( 'should reflect #width property', () => {
+							view.width = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.width = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should update #width on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.width ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.width ).to.equal( 'bar' );
+						} );
+					} );
+
+					describe( 'height input', () => {
+						let labeledInput;
+
+						beforeEach( () => {
+							labeledInput = view.heightInput;
+						} );
+
+						it( 'should be created', () => {
+							expect( labeledInput.view ).to.be.instanceOf( InputTextView );
+							expect( labeledInput.label ).to.equal( 'Height' );
+							expect( labeledInput.class ).to.equal( 'ck-table-properties-form__height' );
+						} );
+
+						it( 'should reflect #height property', () => {
+							view.height = 'foo';
+							expect( labeledInput.view.value ).to.equal( 'foo' );
+
+							view.height = 'bar';
+							expect( labeledInput.view.value ).to.equal( 'bar' );
+						} );
+
+						it( 'should update #height on DOM "input" event', () => {
+							labeledInput.view.element.value = 'foo';
+							labeledInput.view.fire( 'input' );
+							expect( view.height ).to.equal( 'foo' );
+
+							labeledInput.view.element.value = 'bar';
+							labeledInput.view.fire( 'input' );
+							expect( view.height ).to.equal( 'bar' );
+						} );
+					} );
+				} );
+
+				describe( 'dimensions alignment row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 3 ].childNodes[ 1 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-properties-form__alignment-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ].textContent ).to.equal( 'Alignment' );
+						expect( row.childNodes[ 1 ] ).to.equal( view.alignmentToolbar.element );
+					} );
+
+					describe( 'alignment toolbar', () => {
+						let toolbar;
+
+						beforeEach( () => {
+							toolbar = view.alignmentToolbar;
+						} );
+
+						it( 'should be defined', () => {
+							expect( toolbar ).to.be.instanceOf( ToolbarView );
+						} );
+
+						it( 'should have an ARIA label', () => {
+							expect( toolbar.ariaLabel ).to.equal( 'Table alignment toolbar' );
+						} );
+
+						it( 'should bring alignment buttons', () => {
+							expect( toolbar.items.map( ( { label } ) => label ) ).to.have.ordered.members( [
+								'Align table to the left',
+								'Center table',
+								'Align table to the right'
+							] );
+
+							expect( toolbar.items.map( ( { isOn } ) => isOn ) ).to.have.ordered.members( [
+								false, true, false
+							] );
+						} );
+
+						it( 'should change the #horizontalAlignment value', () => {
+							toolbar.items.last.fire( 'execute' );
+							expect( view.alignment ).to.equal( 'right' );
+							expect( toolbar.items.last.isOn ).to.be.true;
+
+							toolbar.items.first.fire( 'execute' );
+							expect( view.alignment ).to.equal( 'left' );
+							expect( toolbar.items.last.isOn ).to.be.false;
+							expect( toolbar.items.first.isOn ).to.be.true;
+						} );
+					} );
+				} );
+
+				describe( 'action row', () => {
+					it( 'should be defined', () => {
+						const row = view.element.childNodes[ 4 ];
+
+						expect( row.classList.contains( 'ck-form__row' ) ).to.be.true;
+						expect( row.classList.contains( 'ck-table-form__action-row' ) ).to.be.true;
+						expect( row.childNodes[ 0 ] ).to.equal( view.saveButtonView.element );
+						expect( row.childNodes[ 1 ] ).to.equal( view.cancelButtonView.element );
+					} );
+
+					it( 'should have buttons with right properties', () => {
+						expect( view.saveButtonView.label ).to.equal( 'Save' );
+						expect( view.saveButtonView.type ).to.equal( 'submit' );
+						expect( view.saveButtonView.withText ).to.be.true;
+						expect( view.saveButtonView.class ).to.equal( 'ck-button-save' );
+
+						expect( view.cancelButtonView.label ).to.equal( 'Cancel' );
+						expect( view.cancelButtonView.withText ).to.be.true;
+						expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' );
+					} );
+
+					it( 'should make the cancel button fire the #cancel event when executed', () => {
+						const spy = sinon.spy();
+
+						view.on( 'cancel', spy );
+
+						view.cancelButtonView.fire( 'execute' );
+
+						expect( spy.calledOnce ).to.be.true;
+					} );
+				} );
+			} );
+
+			it( 'should create #focusTracker instance', () => {
+				expect( view.focusTracker ).to.be.instanceOf( FocusTracker );
+			} );
+
+			it( 'should create #keystrokes instance', () => {
+				expect( view.keystrokes ).to.be.instanceOf( KeystrokeHandler );
+			} );
+
+			it( 'should create #_focusCycler instance', () => {
+				expect( view._focusCycler ).to.be.instanceOf( FocusCycler );
+			} );
+
+			it( 'should create #_focusables view collection', () => {
+				expect( view._focusables ).to.be.instanceOf( ViewCollection );
+			} );
+		} );
+
+		describe( 'render()', () => {
+			it( 'should register child views in #_focusables', () => {
+				expect( view._focusables.map( f => f ) ).to.have.members( [
+					view.borderStyleDropdown,
+					view.borderColorInput,
+					view.borderWidthInput,
+					view.backgroundInput,
+					view.widthInput,
+					view.heightInput,
+					view.alignmentToolbar,
+					view.saveButtonView,
+					view.cancelButtonView,
+				] );
+			} );
+
+			it( 'should register child views\' #element in #focusTracker', () => {
+				const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
+				const view = new TablePropertiesView( { t: val => val } );
+				view.render();
+
+				sinon.assert.calledWith( spy, view.borderStyleDropdown.element );
+				sinon.assert.calledWith( spy, view.borderColorInput.element );
+				sinon.assert.calledWith( spy, view.borderWidthInput.element );
+				sinon.assert.calledWith( spy, view.backgroundInput.element );
+				sinon.assert.calledWith( spy, view.widthInput.element );
+				sinon.assert.calledWith( spy, view.heightInput.element );
+				sinon.assert.calledWith( spy, view.alignmentToolbar.element );
+				sinon.assert.calledWith( spy, view.saveButtonView.element );
+				sinon.assert.calledWith( spy, view.cancelButtonView.element );
+
+				view.destroy();
+			} );
+
+			it( 'starts listening for #keystrokes coming from #element', () => {
+				const view = new TablePropertiesView( { t: val => val } );
+				const spy = sinon.spy( view.keystrokes, 'listenTo' );
+
+				view.render();
+				sinon.assert.calledOnce( spy );
+				sinon.assert.calledWithExactly( spy, view.element );
+			} );
+
+			describe( 'activates keyboard navigation for the form', () => {
+				it( 'so "tab" focuses the next focusable item', () => {
+					const keyEvtData = {
+						keyCode: keyCodes.tab,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					// Mock the border style dropdown button is focused.
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.borderStyleDropdown.element;
+
+					const spy = sinon.spy( view.borderColorInput, 'focus' );
+
+					view.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( keyEvtData.stopPropagation );
+					sinon.assert.calledOnce( spy );
+				} );
+
+				it( 'so "shift + tab" focuses the previous focusable item', () => {
+					const keyEvtData = {
+						keyCode: keyCodes.tab,
+						shiftKey: true,
+						preventDefault: sinon.spy(),
+						stopPropagation: sinon.spy()
+					};
+
+					// Mock the border style dropdown button is focused.
+					view.focusTracker.isFocused = true;
+					view.focusTracker.focusedElement = view.borderStyleDropdown.element;
+
+					const spy = sinon.spy( view.cancelButtonView, 'focus' );
+
+					view.keystrokes.press( keyEvtData );
+					sinon.assert.calledOnce( keyEvtData.preventDefault );
+					sinon.assert.calledOnce( keyEvtData.stopPropagation );
+					sinon.assert.calledOnce( spy );
+				} );
+			} );
+		} );
+
+		describe( 'DOM bindings', () => {
+			describe( 'submit event', () => {
+				it( 'should trigger submit event', () => {
+					const spy = sinon.spy();
+
+					view.on( 'submit', spy );
+					view.element.dispatchEvent( new Event( 'submit' ) );
+
+					expect( spy.calledOnce ).to.be.true;
+				} );
+			} );
+		} );
+
+		describe( 'focus()', () => {
+			it( 'focuses the #borderStyleDropdown', () => {
+				const spy = sinon.spy( view.borderStyleDropdown, 'focus' );
+
+				view.focus();
+
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+} );

+ 86 - 36
packages/ckeditor5-table/tests/ui/utils.js

@@ -38,51 +38,101 @@ describe( 'Utils', () => {
 		return editor.destroy();
 	} );
 
-	describe( 'repositionContextualBalloon', () => {
-		it( 'should re-position the ContextualBalloon when the table cell is selected', () => {
-			const spy = sinon.spy( balloon, 'updatePosition' );
-			const defaultPositions = BalloonPanelView.defaultPositions;
-			const view = new View();
-
-			view.element = global.document.createElement( 'div' );
-
-			balloon.add( {
-				view,
-				position: {
-					target: global.document.body
-				}
+	describe( 'repositionContextualBalloon()', () => {
+		describe( 'with respect to the table cell', () => {
+			it( 'should re-position the ContextualBalloon when the table cell is selected', () => {
+				const spy = sinon.spy( balloon, 'updatePosition' );
+				const defaultPositions = BalloonPanelView.defaultPositions;
+				const view = new View();
+
+				view.element = global.document.createElement( 'div' );
+
+				balloon.add( {
+					view,
+					position: {
+						target: global.document.body
+					}
+				} );
+
+				setData( editor.model,
+					'<table><tableRow>' +
+						'<tableCell><paragraph>foo</paragraph></tableCell>' +
+						'<tableCell><paragraph>[bar]</paragraph></tableCell>' +
+					'</tableRow></table>' );
+				repositionContextualBalloon( editor, 'cell' );
+
+				const modelCell = findAncestor( 'tableCell', editor.model.document.selection.getFirstPosition() );
+				const viewCell = editor.editing.mapper.toViewElement( modelCell );
+
+				sinon.assert.calledWithExactly( spy, {
+					target: editingView.domConverter.viewToDom( viewCell ),
+					positions: [
+						defaultPositions.northArrowSouth,
+						defaultPositions.northArrowSouthWest,
+						defaultPositions.northArrowSouthEast,
+						defaultPositions.southArrowNorth,
+						defaultPositions.southArrowNorthWest,
+						defaultPositions.southArrowNorthEast
+					]
+				} );
 			} );
 
-			setData( editor.model,
-				'<table><tableRow>' +
-					'<tableCell><paragraph>foo</paragraph></tableCell>' +
-					'<tableCell><paragraph>[bar]</paragraph></tableCell>' +
-				'</tableRow></table>' );
-			repositionContextualBalloon( editor );
+			it( 'should not engage with no table is selected', () => {
+				const spy = sinon.spy( balloon, 'updatePosition' );
 
-			const modelCell = findAncestor( 'tableCell', editor.model.document.selection.getFirstPosition() );
-			const viewCell = editor.editing.mapper.toViewElement( modelCell );
+				setData( editor.model, '<paragraph>foo</paragraph>' );
 
-			sinon.assert.calledWithExactly( spy, {
-				target: editingView.domConverter.viewToDom( viewCell ),
-				positions: [
-					defaultPositions.northArrowSouth,
-					defaultPositions.northArrowSouthWest,
-					defaultPositions.northArrowSouthEast,
-					defaultPositions.southArrowNorth,
-					defaultPositions.southArrowNorthWest,
-					defaultPositions.southArrowNorthEast
-				]
+				repositionContextualBalloon( editor, 'cell' );
+				sinon.assert.notCalled( spy );
 			} );
 		} );
 
-		it( 'should not engage with no table is selected', () => {
-			const spy = sinon.spy( balloon, 'updatePosition' );
+		describe( 'with respect to the table', () => {
+			it( 'should re-position the ContextualBalloon when the table is selected', () => {
+				const spy = sinon.spy( balloon, 'updatePosition' );
+				const defaultPositions = BalloonPanelView.defaultPositions;
+				const view = new View();
+
+				view.element = global.document.createElement( 'div' );
+
+				balloon.add( {
+					view,
+					position: {
+						target: global.document.body
+					}
+				} );
+
+				setData( editor.model,
+					'<table><tableRow>' +
+						'<tableCell><paragraph>foo</paragraph></tableCell>' +
+						'<tableCell><paragraph>[bar]</paragraph></tableCell>' +
+					'</tableRow></table>' );
+				repositionContextualBalloon( editor, 'table' );
+
+				const modelTable = findAncestor( 'table', editor.model.document.selection.getFirstPosition() );
+				const viewTable = editor.editing.mapper.toViewElement( modelTable );
+
+				sinon.assert.calledWithExactly( spy, {
+					target: editingView.domConverter.viewToDom( viewTable ),
+					positions: [
+						defaultPositions.northArrowSouth,
+						defaultPositions.northArrowSouthWest,
+						defaultPositions.northArrowSouthEast,
+						defaultPositions.southArrowNorth,
+						defaultPositions.southArrowNorthWest,
+						defaultPositions.southArrowNorthEast
+					]
+				} );
+			} );
+
+			it( 'should not engage with no table is selected', () => {
+				const spy = sinon.spy( balloon, 'updatePosition' );
 
-			setData( editor.model, '<paragraph>foo</paragraph>' );
+				setData( editor.model, '<paragraph>foo</paragraph>' );
 
-			repositionContextualBalloon( editor );
-			sinon.assert.notCalled( spy );
+				repositionContextualBalloon( editor, 'table' );
+				sinon.assert.notCalled( spy );
+			} );
 		} );
 	} );