Răsfoiți Sursa

Update tests.

panr 5 ani în urmă
părinte
comite
22b879a1cb

+ 3 - 2
packages/ckeditor5-image/tests/imageupload/imageuploadcommand.js

@@ -81,9 +81,9 @@ describe( 'ImageUploadCommand', () => {
 			expect( command.isEnabled ).to.be.true;
 		} );
 
-		it( 'should be false when the selection is on other image', () => {
+		it( 'should be true when the selection is on other image', () => {
 			setModelData( model, '[<image></image>]' );
-			expect( command.isEnabled ).to.be.false;
+			expect( command.isEnabled ).to.be.true;
 		} );
 
 		it( 'should be false when the selection is inside other image', () => {
@@ -94,6 +94,7 @@ describe( 'ImageUploadCommand', () => {
 			} );
 			editor.conversion.for( 'downcast' ).elementToElement( { model: 'caption', view: 'figcaption' } );
 			setModelData( model, '<image><caption>[]</caption></image>' );
+
 			expect( command.isEnabled ).to.be.false;
 		} );
 

+ 1 - 1
packages/ckeditor5-image/tests/imageupload/imageuploadediting.js

@@ -157,7 +157,7 @@ describe( 'ImageUploadEditing', () => {
 
 		const command = editor.commands.get( 'imageUpload' );
 
-		expect( command.isEnabled ).to.be.false;
+		expect( command.isEnabled ).to.be.true;
 
 		const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 0 ), model.createPositionAt( doc.getRoot(), 0 ) );
 		const targetViewRange = editor.editing.mapper.toViewRange( targetRange );

+ 210 - 20
packages/ckeditor5-image/tests/imageupload/imageuploadui.js

@@ -9,6 +9,7 @@ import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Image from '../../src/image';
+import DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
 import FileDialogButtonView from '@ckeditor/ckeditor5-upload/src/ui/filedialogbuttonview';
 import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
 import ImageUploadUI from '../../src/imageupload/imageuploadui';
@@ -16,6 +17,11 @@ import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
 import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import Notification from '@ckeditor/ckeditor5-ui/src/notification/notification';
 import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard';
+import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 
 import { createNativeFileMock, UploadAdapterMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
 import { setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
@@ -56,18 +62,19 @@ describe( 'ImageUploadUI', () => {
 		return editor.destroy();
 	} );
 
-	it( 'should register imageUpload button', () => {
+	it( 'should register imageUpload dropdown', () => {
 		const button = editor.ui.componentFactory.create( 'imageUpload' );
 
-		expect( button ).to.be.instanceOf( FileDialogButtonView );
+		expect( button ).to.be.instanceOf( DropdownView );
 	} );
 
 	it( 'should set proper accepted mime-types for imageUpload button as defined in configuration', () => {
 		editor.config.set( 'image.upload.types', [ 'svg+xml', 'jpeg', 'vnd.microsoft.icon', 'x-xbitmap' ] );
 
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 
-		expect( button.acceptedType ).to.equal( 'image/svg+xml,image/jpeg,image/vnd.microsoft.icon,image/x-xbitmap' );
+		expect( fileDialogButton.acceptedType ).to.equal( 'image/svg+xml,image/jpeg,image/vnd.microsoft.icon,image/x-xbitmap' );
 	} );
 
 	it( 'should be disabled while ImageUploadCommand is disabled', () => {
@@ -85,27 +92,28 @@ describe( 'ImageUploadUI', () => {
 
 	// ckeditor5-upload/#77
 	it( 'should be properly bound with ImageUploadCommand', () => {
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
 		const command = editor.commands.get( 'imageUpload' );
 		const spy = sinon.spy();
 
-		button.render();
+		dropdown.render();
 
-		button.buttonView.on( 'execute', spy );
+		dropdown.buttonView.on( 'execute', spy );
 
 		command.isEnabled = false;
 
-		button.buttonView.element.dispatchEvent( new Event( 'click' ) );
+		dropdown.buttonView.element.dispatchEvent( new Event( 'click' ) );
 
 		sinon.assert.notCalled( spy );
 	} );
 
 	it( 'should execute imageUpload command', () => {
 		const executeStub = sinon.stub( editor, 'execute' );
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const files = [ createNativeFileMock() ];
 
-		button.fire( 'done', files );
+		fileDialogButton.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
 		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
@@ -113,22 +121,24 @@ describe( 'ImageUploadUI', () => {
 
 	it( 'should execute imageUpload command with multiple files', () => {
 		const executeStub = sinon.stub( editor, 'execute' );
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const files = [ createNativeFileMock(), createNativeFileMock(), createNativeFileMock() ];
 
-		button.fire( 'done', files );
+		fileDialogButton.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
 		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( files );
 	} );
 
 	it( 'should optimize the insertion position', () => {
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const files = [ createNativeFileMock() ];
 
 		setModelData( model, '<paragraph>f[]oo</paragraph>' );
 
-		button.fire( 'done', files );
+		fileDialogButton.fire( 'done', files );
 
 		const id = fileRepository.getLoader( files[ 0 ] ).id;
 
@@ -139,12 +149,13 @@ describe( 'ImageUploadUI', () => {
 	} );
 
 	it( 'should correctly insert multiple files', () => {
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const files = [ createNativeFileMock(), createNativeFileMock() ];
 
 		setModelData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
 
-		button.fire( 'done', files );
+		fileDialogButton.fire( 'done', files );
 
 		const id1 = fileRepository.getLoader( files[ 0 ] ).id;
 		const id2 = fileRepository.getLoader( files[ 1 ] ).id;
@@ -159,27 +170,206 @@ describe( 'ImageUploadUI', () => {
 
 	it( 'should not execute imageUpload if the file is not an image', () => {
 		const executeStub = sinon.stub( editor, 'execute' );
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const file = {
 			type: 'media/mp3',
 			size: 1024
 		};
 
-		button.fire( 'done', [ file ] );
+		fileDialogButton.fire( 'done', [ file ] );
 		sinon.assert.notCalled( executeStub );
 	} );
 
 	it( 'should work even if the FileList does not support iterators', () => {
 		const executeStub = sinon.stub( editor, 'execute' );
-		const button = editor.ui.componentFactory.create( 'imageUpload' );
+		const plugin = editor.plugins.get( 'ImageUploadUI' );
+		const fileDialogButton = plugin._createFileDialogButtonView( editor.locale );
 		const files = {
 			0: createNativeFileMock(),
 			length: 1
 		};
 
-		button.fire( 'done', files );
+		fileDialogButton.fire( 'done', files );
 		sinon.assert.calledOnce( executeStub );
 		expect( executeStub.firstCall.args[ 0 ] ).to.equal( 'imageUpload' );
 		expect( executeStub.firstCall.args[ 1 ].file ).to.deep.equal( [ files[ 0 ] ] );
 	} );
+
+	describe( 'dropdown action button', () => {
+		it( 'should be an instance of FileDialogButtonView', () => {
+			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+
+			expect( dropdown.buttonView.actionView ).to.be.instanceOf( FileDialogButtonView );
+		} );
+	} );
+
+	describe( 'dropdown panel buttons', () => {
+		it( 'should have "Update" label on submit button when URL input is already filled', () => {
+			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+			const viewDocument = editor.editing.view.document;
+
+			editor.setData( '<figure><img src="image-url.png" /></figure>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+			} );
+
+			const img = viewDocument.selection.getSelectedElement();
+
+			const data = fakeEventData();
+			const eventInfo = new EventInfo( img, 'click' );
+			const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+			viewDocument.fire( 'click', domEventDataMock );
+
+			dropdown.isOpen = true;
+
+			const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+
+			expect( dropdown.isOpen ).to.be.true;
+			expect( inputValue ).to.equal( 'image-url.png' );
+			expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' );
+		} );
+
+		it( 'should have "Insert" label on submit button on uploading a new image', () => {
+			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+			const viewDocument = editor.editing.view.document;
+
+			editor.setData( '<p>test</p>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' );
+			} );
+
+			const el = viewDocument.selection.getSelectedElement();
+
+			const data = fakeEventData();
+			const eventInfo = new EventInfo( el, 'click' );
+			const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+			viewDocument.fire( 'click', domEventDataMock );
+
+			dropdown.isOpen = true;
+
+			const inputValue = dropdown.panelView.children.first.imageURLInputValue;
+
+			expect( dropdown.isOpen ).to.be.true;
+			expect( inputValue ).to.equal( '' );
+			expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' );
+		} );
+	} );
+
+	it( 'should remove all attributes from model except "src" when updating the image source URL', () => {
+		const viewDocument = editor.editing.view.document;
+		const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+		const insertButtonView = dropdown.panelView.children.first.insertButtonView;
+		const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+		const submitSpy = sinon.spy();
+
+		dropdown.isOpen = true;
+
+		editor.setData( '<figure><img src="image-url-800w.jpg"' +
+			'srcset="image-url-480w.jpg 480w,image-url-800w.jpg 800w"' +
+			'sizes="(max-width: 600px) 480px,800px"' +
+			'alt="test-image"></figure>' );
+
+		editor.editing.view.change( writer => {
+			writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+		} );
+
+		const selectedElement = editor.model.document.selection.getSelectedElement();
+
+		expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' );
+		expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true;
+
+		dropdown.panelView.children.first.imageURLInputValue = 'new-url.png';
+
+		dropdown.on( 'submit', submitSpy );
+
+		insertButtonView.fire( 'execute' );
+
+		sinon.assert.notCalled( commandSpy );
+		sinon.assert.calledOnce( submitSpy );
+		expect( dropdown.isOpen ).to.be.false;
+		expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'new-url.png' );
+		expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false;
+		expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false;
+	} );
+
+	describe( 'events', () => {
+		it( 'should emit "submit" event when clicking on submit button', () => {
+			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+			const insertButtonView = dropdown.panelView.children.first.insertButtonView;
+			const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+			const submitSpy = sinon.spy();
+
+			dropdown.isOpen = true;
+
+			dropdown.on( 'submit', submitSpy );
+
+			insertButtonView.fire( 'execute' );
+
+			expect( dropdown.isOpen ).to.be.false;
+			sinon.assert.calledOnce( commandSpy );
+			sinon.assert.calledOnce( submitSpy );
+		} );
+
+		it( 'should emit "cancel" event when clicking on cancel button', () => {
+			const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+			const cancelButtonView = dropdown.panelView.children.first.cancelButtonView;
+			const commandSpy = sinon.spy( editor.commands.get( 'imageInsert' ), 'execute' );
+			const cancelSpy = sinon.spy();
+
+			dropdown.isOpen = true;
+
+			dropdown.on( 'cancel', cancelSpy );
+
+			cancelButtonView.fire( 'execute' );
+
+			expect( dropdown.isOpen ).to.be.false;
+			sinon.assert.notCalled( commandSpy );
+			sinon.assert.calledOnce( cancelSpy );
+		} );
+	} );
+
+	it( 'should inject integrations to the dropdown panel view from the config', async () => {
+		const editor = await ClassicEditor
+			.create( editorElement, {
+				plugins: [
+					CKFinder,
+					Paragraph,
+					Image,
+					ImageUploadEditing,
+					ImageUploadUI,
+					FileRepository,
+					UploadAdapterPluginMock,
+					Clipboard
+				],
+				image: {
+					upload: {
+						panel: {
+							items: [
+								'insertImageViaUrl',
+								'openCKFinder'
+							]
+						}
+					}
+				}
+			} );
+
+		const dropdown = editor.ui.componentFactory.create( 'imageUpload' );
+
+		expect( dropdown.panelView.children.first._integrations.length ).to.equal( 2 );
+		expect( dropdown.panelView.children.first._integrations.first ).to.be.instanceOf( LabeledFieldView );
+		expect( dropdown.panelView.children.first._integrations.last ).to.be.instanceOf( ButtonView );
+
+		editor.destroy();
+	} );
 } );
+
+function fakeEventData() {
+	return {
+		preventDefault: sinon.spy()
+	};
+}

+ 100 - 0
packages/ckeditor5-image/tests/imageupload/ui/imageuploadformrowview.js

@@ -0,0 +1,100 @@
+/**
+ * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ImageUploadFormRowView from '../../../src/imageupload/ui/imageuploadformrowview';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+
+describe( 'ImageUploadFormRowView', () => {
+	let view, locale;
+
+	beforeEach( () => {
+		locale = { t: val => val };
+		view = new ImageUploadFormRowView( locale );
+		view.render();
+	} );
+
+	afterEach( () => {
+		view.element.remove();
+	} );
+
+	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 );
+			expect( view.children ).to.have.length( 0 );
+		} );
+
+		it( 'should set view#class', () => {
+			expect( view.class ).to.be.null;
+		} );
+
+		it( 'should set the template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.be.true;
+			expect( view.element.classList.contains( 'ck-form__row' ) ).to.be.true;
+		} );
+
+		describe( 'options', () => {
+			it( 'should set view#class when class was passed', () => {
+				const view = new ImageUploadFormRowView( locale, {
+					class: 'foo'
+				} );
+
+				expect( view.class ).to.equal( 'foo' );
+
+				view.destroy();
+			} );
+
+			it( 'should fill view#children when children were passed', () => {
+				const view = new ImageUploadFormRowView( locale, {
+					children: [
+						new View()
+					]
+				} );
+
+				expect( view.children ).to.have.length( 1 );
+
+				view.destroy();
+			} );
+
+			it( 'should use a label view when passed', () => {
+				const labelView = new View();
+				labelView.id = '123';
+
+				const view = new ImageUploadFormRowView( locale, {
+					labelView
+				} );
+
+				view.render();
+
+				expect( view.element.getAttribute( 'role' ) ).to.equal( 'group' );
+				expect( view.element.getAttribute( 'aria-labelledby' ) ).to.equal( '123' );
+
+				view.destroy();
+			} );
+		} );
+
+		describe( 'template bindings', () => {
+			it( 'should bind #class to the template', () => {
+				view.class = 'foo';
+				expect( view.element.classList.contains( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should bind #children to the template', () => {
+				const child = new View();
+				child.setTemplate( { tag: 'div' } );
+
+				view.children.add( child );
+
+				expect( view.element.firstChild ).to.equal( child.element );
+
+				view.destroy();
+			} );
+		} );
+	} );
+} );

+ 291 - 0
packages/ckeditor5-image/tests/imageupload/ui/imageuploadpanelview.js

@@ -0,0 +1,291 @@
+/**
+ * @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 DropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/dropdownview';
+import LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview';
+
+import ImageUploadPanelView from '../../../src/imageupload/ui/imageuploadpanelview';
+import ImageUploadFormRowView from '../../../src/imageupload/ui/imageuploadformrowview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+
+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 View from '@ckeditor/ckeditor5-ui/src/view';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'ImageUploadPanelView', () => {
+	let view;
+
+	beforeEach( () => {
+		view = new ImageUploadPanelView( { t: val => val } );
+		view.render();
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should contain instance of LabeledInputView as #labeledInputView', () => {
+			expect( view.labeledInputView ).to.be.instanceOf( LabeledFieldView );
+		} );
+
+		it( 'should contain instance of ButtonView as #insertButtonView', () => {
+			expect( view.insertButtonView ).to.be.instanceOf( ButtonView );
+			expect( view.insertButtonView.label ).to.equal( 'Insert' );
+		} );
+
+		it( 'should contain instance of ButtonView as #cancelButtonView', () => {
+			expect( view.cancelButtonView ).to.be.instanceOf( ButtonView );
+			expect( view.cancelButtonView.label ).to.equal( 'Cancel' );
+		} );
+
+		it( 'should contain instance of DropdownView as #dropdownView', () => {
+			expect( view.dropdownView ).to.be.instanceOf( DropdownView );
+		} );
+
+		it( 'should contain instance of SplitButtonView for the #dropdownView button', () => {
+			expect( view.dropdownView ).to.be.instanceOf( DropdownView );
+			expect( view.dropdownView.buttonView ).to.be.instanceOf( SplitButtonView );
+		} );
+
+		it( 'should contain #imageURLInputValue', () => {
+			expect( view.imageURLInputValue ).to.equal( '' );
+		} );
+
+		it( 'should contain #_integrations as an instance of Collection', () => {
+			expect( view._integrations ).to.be.instanceOf( Collection );
+		} );
+
+		describe( 'integrations', () => {
+			it( 'should contain 2 integrations when they were passed to the ImageUloadPanelView as options.integrations', () => {
+				const view = new ImageUploadPanelView( { t: val => val }, {
+					integrations: {
+						'integration1': new View(),
+						'integration2': new ButtonView()
+					}
+				} );
+
+				expect( view._integrations ).to.be.instanceOf( Collection );
+				expect( view._integrations.length ).to.equal( 2 );
+			} );
+
+			it( 'should contain insertImageViaUrl view when "insertImageViaUrl" token is passed via options.integrations', () => {
+				const view = new ImageUploadPanelView( { t: val => val }, {
+					integrations: {
+						'insertImageViaUrl': 'insertImageViaUrl',
+						'integration1': new View(),
+						'integration2': new ButtonView()
+					}
+				} );
+
+				expect( view._integrations ).to.be.instanceOf( Collection );
+				expect( view._integrations.length ).to.equal( 3 );
+				expect( view._integrations.first ).to.be.instanceOf( LabeledFieldView );
+			} );
+		} );
+
+		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( 'events', () => {
+			it( 'should fire "submit" event on insertButtonView#execute', () => {
+				const spy = sinon.spy();
+
+				view.on( 'submit', spy );
+
+				view.insertButtonView.fire( 'execute' );
+
+				expect( spy.calledOnce ).to.true;
+			} );
+
+			it( 'should fire "cancel" event on cancelButtonView#execute', () => {
+				const spy = sinon.spy();
+
+				view.on( 'cancel', spy );
+
+				view.cancelButtonView.fire( 'execute' );
+
+				expect( spy.calledOnce ).to.true;
+			} );
+		} );
+	} );
+
+	describe( 'image URL input view', () => {
+		it( 'should have placeholder', () => {
+			expect( view.labeledInputView.fieldView.placeholder ).to.equal( 'https://example.com/src/image.png' );
+		} );
+
+		it( 'should have info text', () => {
+			expect( view.labeledInputView.infoText ).to.match( /^Paste the image source URL/ );
+		} );
+	} );
+
+	describe( 'template', () => {
+		it( 'should create element from the template', () => {
+			expect( view.element.classList.contains( 'ck' ) ).to.true;
+			expect( view.element.classList.contains( 'ck-image-upload-form' ) ).to.true;
+			expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' );
+		} );
+
+		it( 'should have URL input view', () => {
+			expect( view.template.children[ 0 ] ).to.equal( view.labeledInputView );
+		} );
+
+		it( 'should have form row view with buttons', () => {
+			expect( view.template.children[ 1 ] ).to.be.instanceOf( ImageUploadFormRowView );
+			expect( view.template.children[ 1 ].children.first ).to.equal( view.insertButtonView );
+			expect( view.template.children[ 1 ].children.last ).to.equal( view.cancelButtonView );
+		} );
+	} );
+
+	describe( 'render()', () => {
+		it( 'should register child views in #_focusables', () => {
+			expect( view._focusables.map( f => f ) ).to.have.members( [
+				view.labeledInputView,
+				view.insertButtonView,
+				view.cancelButtonView
+			] );
+		} );
+
+		it( 'should register child views\' #element in #focusTracker', () => {
+			const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' );
+
+			view = new ImageUploadPanelView( { t: () => {} } );
+			view.render();
+
+			sinon.assert.calledWithExactly( spy.getCall( 0 ), view.labeledInputView.element );
+			sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element );
+			sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element );
+		} );
+
+		it( 'starts listening for #keystrokes coming from #element', () => {
+			view = new ImageUploadPanelView( { t: () => {} } );
+
+			const spy = sinon.spy( view.keystrokes, 'listenTo' );
+
+			view.render();
+			sinon.assert.calledOnce( spy );
+			sinon.assert.calledWithExactly( spy, view.element );
+		} );
+
+		it( 'intercepts the arrow* events and overrides the default toolbar behavior', () => {
+			const keyEvtData = {
+				stopPropagation: sinon.spy()
+			};
+
+			keyEvtData.keyCode = keyCodes.arrowdown;
+			view.keystrokes.press( keyEvtData );
+			sinon.assert.calledOnce( keyEvtData.stopPropagation );
+
+			keyEvtData.keyCode = keyCodes.arrowup;
+			view.keystrokes.press( keyEvtData );
+			sinon.assert.calledTwice( keyEvtData.stopPropagation );
+
+			keyEvtData.keyCode = keyCodes.arrowleft;
+			view.keystrokes.press( keyEvtData );
+			sinon.assert.calledThrice( keyEvtData.stopPropagation );
+
+			keyEvtData.keyCode = keyCodes.arrowright;
+			view.keystrokes.press( keyEvtData );
+			sinon.assert.callCount( keyEvtData.stopPropagation, 4 );
+		} );
+
+		it( 'intercepts the "selectstart" event of the #labeledInputView with the high priority', () => {
+			const spy = sinon.spy();
+			const event = new Event( 'selectstart', {
+				bubbles: true,
+				cancelable: true
+			} );
+
+			event.stopPropagation = spy;
+
+			view.labeledInputView.element.dispatchEvent( event );
+			sinon.assert.calledOnce( spy );
+		} );
+
+		describe( 'activates keyboard navigation for the toolbar', () => {
+			it( 'so "tab" focuses the next focusable item', () => {
+				const keyEvtData = {
+					keyCode: keyCodes.tab,
+					preventDefault: sinon.spy(),
+					stopPropagation: sinon.spy()
+				};
+
+				// Mock the url input is focused.
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.labeledInputView.element;
+
+				const spy = sinon.spy( view.insertButtonView, '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 cancel button is focused.
+				view.focusTracker.isFocused = true;
+				view.focusTracker.focusedElement = view.cancelButtonView.element;
+
+				const spy = sinon.spy( view.insertButtonView, 'focus' );
+
+				view.keystrokes.press( keyEvtData );
+				sinon.assert.calledOnce( keyEvtData.preventDefault );
+				sinon.assert.calledOnce( keyEvtData.stopPropagation );
+				sinon.assert.calledOnce( spy );
+			} );
+		} );
+	} );
+
+	describe( 'focus()', () => {
+		it( 'should focus on the #labeledInputView', () => {
+			const spy = sinon.spy( view.labeledInputView, 'focus' );
+
+			view.focus();
+
+			sinon.assert.calledOnce( spy );
+		} );
+	} );
+
+	describe( 'URL input', () => {
+		it( 'should be bound with #imageURLInputValue', () => {
+			view.labeledInputView.fieldView.element.value = 'abc';
+			view.labeledInputView.fieldView.fire( 'input' );
+
+			expect( view.imageURLInputValue ).to.equal( 'abc' );
+
+			view.labeledInputView.fieldView.element.value = 'xyz';
+			view.labeledInputView.fieldView.fire( 'input' );
+
+			expect( view.imageURLInputValue ).to.equal( 'xyz' );
+		} );
+	} );
+} );

+ 131 - 1
packages/ckeditor5-image/tests/imageupload/utils.js

@@ -3,7 +3,18 @@
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  */
 
-import { createImageTypeRegExp } from '../../src/imageupload/utils';
+/* globals document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Image from '../../src/image';
+import ImageUploadUI from '../../src/imageupload/imageuploadui';
+import ImageUploadEditing from '../../src/imageupload/imageuploadediting';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Link from '@ckeditor/ckeditor5-link/src/link';
+import CKFinder from '@ckeditor/ckeditor5-ckfinder/src/ckfinder';
+import { createImageTypeRegExp, prepareIntegrations } from '../../src/imageupload/utils';
 
 describe( 'upload utils', () => {
 	describe( 'createImageTypeRegExp()', () => {
@@ -31,4 +42,123 @@ describe( 'upload utils', () => {
 			expect( createImageTypeRegExp( [ 'png' ] ).test( 'svg+xml' ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'prepareIntegrations()', () => {
+		it( 'should return "insetImageViaUrl" and "openCKFinder" integrations', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						CKFinder,
+						Paragraph,
+						Image,
+						ImageUploadEditing,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl',
+									'openCKFinder'
+								]
+							}
+						}
+					}
+				} );
+
+			const openCKFinderExtendedView = Object.values( prepareIntegrations( editor ) )[ 1 ];
+
+			expect( openCKFinderExtendedView.class ).contains( 'ck-image-upload__ck-finder-button' );
+			expect( openCKFinderExtendedView.label ).to.equal( 'Insert image or file' );
+			expect( openCKFinderExtendedView.withText ).to.be.true;
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should return only "inserImageViaUrl" integration', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Image,
+						ImageUploadEditing,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'insertImageViaUrl',
+									'openCKFinder'
+								]
+							}
+						}
+					}
+				} );
+
+			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should return only "link" integration', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Link,
+						Image,
+						ImageUploadEditing,
+						ImageUploadUI
+					],
+					image: {
+						upload: {
+							panel: {
+								items: [
+									'link'
+								]
+							}
+						}
+					}
+				} );
+
+			expect( Object.values( prepareIntegrations( editor ) ).length ).to.equal( 1 );
+			expect( Object.values( prepareIntegrations( editor ) )[ 0 ].label ).to.equal( 'Link' );
+			expect( Object.values( prepareIntegrations( editor ) )[ 0 ] ).to.be.instanceOf( ButtonView );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+
+		it( 'should not return any integrations, should return "null" instead', async () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			const editor = await ClassicEditor
+				.create( editorElement, {
+					plugins: [
+						Paragraph,
+						Image,
+						ImageUploadEditing,
+						ImageUploadUI
+					]
+				} );
+
+			expect( prepareIntegrations( editor ) ).to.equal( null );
+
+			editor.destroy();
+			editorElement.remove();
+		} );
+	} );
 } );