test
' ); - - 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.buttonView.fire( 'open' ); - - 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' ); - } ); + expect( insertImageUI.isImageSelected ).to.be.false; } ); + } ); - describe( 'dropdown panel integrations', () => { - describe( 'insert image via URL form', () => { - it( 'should have "Insert image via URL" label on inserting new image', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( 'test
' ); - - 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.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' ); - } ); - - it( 'should have "Update image URL" label on updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); - - 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.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' ); - } ); - } ); + describe( '#registerIntegration()', () => { + beforeEach( async () => { + await createEditor( { plugins: [ ImageInsertUI ] } ); } ); - it( 'should remove all attributes from model except "src" when updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); + it( 'should store the integration definition', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; - const insertButtonView = dropdown.panelView.children.first.insertButtonView; - - editor.setData( '' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator } ); - 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 = '/assets/sample3.png'; + expect( insertImageUI._integrations.has( 'foobar' ) ).to.be.true; - dropdown.on( 'submit', submitSpy ); + const integrationData = insertImageUI._integrations.get( 'foobar' ); - insertButtonView.fire( 'execute' ); - - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( submitSpy ); - expect( dropdown.isOpen ).to.be.false; - expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false; - expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false; + expect( integrationData.observable ).to.equal( observable ); + expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); + expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.requiresForm ).to.be.false; } ); - describe( 'events', () => { - it( 'should emit "submit" event when clicking on submit button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); - - dropdown.on( 'submit', submitSpy ); - - const insertButtonView = dropdown.panelView.children.first.insertButtonView; - - insertButtonView.fire( 'execute' ); - - expect( dropdown.isOpen ).to.be.false; - sinon.assert.calledOnce( commandSpy ); - sinon.assert.calledOnce( submitSpy ); + it( 'should store the integration definition (with optional data)', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; + + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - it( 'should emit "cancel" event when clicking on cancel button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const cancelSpy = sinon.spy(); - - dropdown.buttonView.fire( 'open' ); - - dropdown.on( 'cancel', cancelSpy ); + expect( insertImageUI._integrations.has( 'foobar' ) ).to.be.true; - const cancelButtonView = dropdown.panelView.children.first.cancelButtonView; + const integrationData = insertImageUI._integrations.get( 'foobar' ); - cancelButtonView.fire( 'execute' ); + expect( integrationData.observable ).to.equal( observable ); + expect( integrationData.buttonViewCreator ).to.equal( buttonViewCreator ); + expect( integrationData.formViewCreator ).to.equal( formViewCreator ); + expect( integrationData.requiresForm ).to.be.true; + } ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( cancelSpy ); + it( 'should warn if multiple integrations with the same name are registered', () => { + const observable = new Model( { isEnabled: true } ); + const buttonViewCreator = () => {}; + const formViewCreator = () => {}; + const warnStub = sinon.stub( console, 'warn' ); + + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - it( 'should focus on "insert image via URL" input after opening', () => { - let spy; - - // The ImageInsertPanelView is added on first open. - // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 - dropdown.on( 'change:isOpen', () => { - const imageInsertPanelView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertPanelView, 'focus' ); - } ); + expect( warnStub.notCalled ).to.be.true; - dropdown.buttonView.fire( 'open' ); - sinon.assert.calledOnce( spy ); + insertImageUI.registerIntegration( { + name: 'foobar', + observable, + buttonViewCreator, + formViewCreator, + requiresForm: true } ); - } ); - - it( 'should inject integrations to the dropdown panel view from the config', async () => { - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Link, - Image, - CKFinderUploadAdapter, - CKFinder, - Paragraph, - ImageInsert, - ImageInsertUI, - FileRepository, - UploadAdapterPluginMock, - Clipboard - ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } - } - } ); - - const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - - dropdown.isOpen = true; - 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(); + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integration-exists' ); } ); } ); - describe( 'dropdown (without uploadImage command)', () => { - beforeEach( async () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); + describe( 'integrations', () => { + let observableUpload, observableUrl; - editor = await ClassicEditor.create( editorElement, { - plugins: [ Paragraph, Image, ImageInsertViaUrl ], - toolbar: [ 'insertImage' ] - } ); - - dropdown = editor.ui.view.toolbar.children.first.children.first; + beforeEach( async () => { + await createEditor( { plugins: [ Image, Essentials, Paragraph ] } ); } ); - afterEach( async () => { - editorElement.remove(); + it( 'should warn if empty list of integrations is configured', () => { + editor.config.set( 'image.insert.integrations', [] ); - await editor.destroy(); - } ); - - it( 'should register the "insertImage" dropdown', () => { + const warnStub = sinon.stub( console, 'warn' ); const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown ).to.be.null; + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-specified' ); } ); - it( 'should make the "insertImage" dropdown accessible via the property of the plugin', () => { - expect( editor.plugins.get( 'ImageInsertUI' ).dropdownView ).to.be.instanceOf( DropdownView ); - } ); + it( 'should warn if unknown integration is requested by config', () => { + editor.config.set( 'image.insert.integrations', [ 'foo' ] ); - it( 'should register "imageInsert" dropdown as an alias for the "insertImage" dropdown', () => { - const dropdownCreator = editor.ui.componentFactory._components.get( 'insertImage'.toLowerCase() ); - const dropdownAliasCreator = editor.ui.componentFactory._components.get( 'imageInsert'.toLowerCase() ); + const warnStub = sinon.stub( console, 'warn' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdownCreator.callback ).to.equal( dropdownAliasCreator.callback ); + expect( dropdown ).to.be.null; + expect( warnStub.calledTwice ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-unknown-integration' ); + expect( warnStub.firstCall.args[ 1 ].item ).to.equal( 'foo' ); + expect( warnStub.secondCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-registered' ); } ); - it( 'should bind the enabled state of the dropdown to the InsertImageCommand command', () => { - const command = editor.commands.get( 'insertImage' ); + it( 'should not warn if known but not registered integration is requested by config', () => { + editor.config.set( 'image.insert.integrations', [ 'url', 'assetManager', 'upload' ] ); - expect( command.isEnabled, 'command state' ).to.be.true; - expect( dropdown.isEnabled, 'dropdown state #1' ).to.be.true; - - command.forceDisabled( 'foo' ); + const warnStub = sinon.stub( console, 'warn' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - expect( dropdown.isEnabled, 'dropdown state #2' ).to.be.false; + expect( dropdown ).to.be.null; + expect( warnStub.calledOnce ).to.be.true; + expect( warnStub.firstCall.args[ 0 ] ).to.equal( 'image-insert-integrations-not-registered' ); } ); - it( 'should not insert panel view children until dropdown is not open for the first time', () => { - expect( dropdown.panelView.children.length ).to.equal( 0 ); - - dropdown.buttonView.fire( 'open' ); + describe( 'single integration without form view required', () => { + beforeEach( async () => { + registerUploadIntegration(); + } ); - expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.panelView.children.first ).to.be.instanceOf( ImageInsertPanelView ); - } ); + it( 'should create a toolbar button', () => { + const button = editor.ui.componentFactory.create( 'insertImage' ); - describe( 'dropdown button', () => { - it( 'should be an instance of DropdownButtonView', () => { - expect( dropdown.buttonView ).to.be.instanceOf( DropdownButtonView ); + expect( button ).to.be.instanceOf( ButtonView ); + expect( button.label ).to.equal( 'button upload single' ); } ); } ); - describe( 'dropdown panel buttons', () => { - it( 'should have "Update" label on submit button when URL input is already filled', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '' ); - - 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.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Update' ); + describe( 'single integration with form view required', () => { + beforeEach( async () => { + registerUrlIntegration(); } ); - it( 'should have "Insert" label on submit button on uploading a new image', () => { - const viewDocument = editor.editing.view.document; + it( 'should create a toolbar dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - editor.setData( 'test
' ); + expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown.buttonView.label ).to.equal( 'button url single' ); + expect( dropdown.isEnabled ).to.be.true; + } ); - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'end' ); - } ); + it( 'should bind isEnabled state to observable', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const el = viewDocument.selection.getSelectedElement(); + observableUrl.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); + observableUrl.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; + } ); - viewDocument.fire( 'click', domEventDataMock ); + it( 'should create panel view on dropdown first open', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + expect( dropdown.panelView.children.length ).to.equal( 0 ); - const inputValue = dropdown.panelView.children.first.imageURLInputValue; + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( dropdown.panelView.children.first.insertButtonView.label ).to.equal( 'Insert' ); + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 0 ).label ).to.equal( 'dropdown url single' ); } ); } ); - describe( 'dropdown panel integrations', () => { - describe( 'insert image via URL form', () => { - it( 'should have "Insert image via URL" label on inserting new image', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( 'test
' ); - - 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.buttonView.fire( 'open' ); - - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); - - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Insert image via URL' ); - } ); - - it( 'should have "Update image URL" label on updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - - editor.setData( '' ); - - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); - } ); + describe( 'multiple integrations', () => { + beforeEach( async () => { + registerUploadIntegration(); + registerUrlIntegration(); + } ); - const el = viewDocument.selection.getSelectedElement(); + it( 'should create a toolbar split button dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - const data = fakeEventData(); - const eventInfo = new EventInfo( el, 'click' ); - const domEventDataMock = new DomEventData( viewDocument, eventInfo, data ); + expect( dropdown ).to.be.instanceOf( DropdownView ); + expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.actionView.label ).to.equal( 'button upload multiple' ); + expect( dropdown.isEnabled ).to.be.true; + } ); - viewDocument.fire( 'click', domEventDataMock ); + it( 'should bind split button label to #isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + expect( insertImageUI.isImageSelected ).to.be.false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); - const inputValue = dropdown.panelView.children.first.imageURLInputValue; - const insertImageViaUrlForm = dropdown.panelView.children.first.getIntegration( 'insertImageViaUrl' ); + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); - expect( dropdown.isOpen ).to.be.true; - expect( inputValue ).to.equal( '/assets/sample.png' ); - expect( insertImageViaUrlForm.label ).to.equal( 'Update image URL' ); - } ); + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); } ); - } ); - it( 'should remove all attributes from model except "src" when updating the image source URL', () => { - const viewDocument = editor.editing.view.document; - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); + it( 'should bind isEnabled state to observables', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.buttonView.fire( 'open' ); + observableUrl.isEnabled = false; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.false; - const insertButtonView = dropdown.panelView.children.first.insertButtonView; + observableUrl.isEnabled = true; + observableUpload.isEnabled = false; + expect( dropdown.isEnabled ).to.be.true; - editor.setData( '' ); + observableUrl.isEnabled = false; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; - editor.editing.view.change( writer => { - writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' ); + observableUrl.isEnabled = true; + observableUpload.isEnabled = true; + expect( dropdown.isEnabled ).to.be.true; } ); - const selectedElement = editor.model.document.selection.getSelectedElement(); - - expect( selectedElement.getAttribute( 'src' ) ).to.equal( 'image-url-800w.jpg' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.true; + it( 'should create panel view on dropdown first open', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); - dropdown.panelView.children.first.imageURLInputValue = '/assets/sample3.png'; + expect( dropdown.panelView.children.length ).to.equal( 0 ); - dropdown.on( 'submit', submitSpy ); + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); - insertButtonView.fire( 'execute' ); + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( submitSpy ); - expect( dropdown.isOpen ).to.be.false; - expect( selectedElement.getAttribute( 'src' ) ).to.equal( '/assets/sample3.png' ); - expect( selectedElement.hasAttribute( 'srcset' ) ).to.be.false; - expect( selectedElement.hasAttribute( 'sizes' ) ).to.be.false; + expect( formView.children.length ).to.equal( 2 ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 0 ).label ).to.equal( 'dropdown upload multiple' ); + expect( formView.children.get( 1 ) ).to.be.instanceOf( ButtonView ); + expect( formView.children.get( 1 ).label ).to.equal( 'dropdown url multiple' ); + } ); } ); - describe( 'events', () => { - it( 'should emit "submit" event when clicking on submit button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const submitSpy = sinon.spy(); + function registerUrlIntegration() { + observableUrl = new Model( { isEnabled: true } ); - dropdown.buttonView.fire( 'open' ); + insertImageUI.registerIntegration( { + name: 'url', + observable: observableUrl, + requiresForm: true, + buttonViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - dropdown.on( 'submit', submitSpy ); + button.label = 'button url ' + ( isOnlyOne ? 'single' : 'multiple' ); - const insertButtonView = dropdown.panelView.children.first.insertButtonView; + return button; + }, + formViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - insertButtonView.fire( 'execute' ); + button.label = 'dropdown url ' + ( isOnlyOne ? 'single' : 'multiple' ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.calledOnce( commandSpy ); - sinon.assert.calledOnce( submitSpy ); + return button; + } } ); + } - it( 'should emit "cancel" event when clicking on cancel button', () => { - const commandSpy = sinon.spy( editor.commands.get( 'insertImage' ), 'execute' ); - const cancelSpy = sinon.spy(); + function registerUploadIntegration() { + observableUpload = new Model( { isEnabled: true } ); - dropdown.buttonView.fire( 'open' ); + insertImageUI.registerIntegration( { + name: 'upload', + observable: observableUpload, + buttonViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - dropdown.on( 'cancel', cancelSpy ); + button.label = 'button upload ' + ( isOnlyOne ? 'single' : 'multiple' ); - const cancelButtonView = dropdown.panelView.children.first.cancelButtonView; + return button; + }, + formViewCreator( isOnlyOne ) { + const button = new ButtonView( editor.locale ); - cancelButtonView.fire( 'execute' ); + button.label = 'dropdown upload ' + ( isOnlyOne ? 'single' : 'multiple' ); - expect( dropdown.isOpen ).to.be.false; - sinon.assert.notCalled( commandSpy ); - sinon.assert.calledOnce( cancelSpy ); + return button; + } } ); + } + } ); - it( 'should focus on "insert image via URL" input after opening', () => { - let spy; + async function createEditor( config ) { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); - // The ImageInsertPanelView is added on first open. - // See https://github.com/ckeditor/ckeditor5/pull/8019#discussion_r484069652 - dropdown.on( 'change:isOpen', () => { - const imageInsertPanelView = dropdown.panelView.children.first; - spy = sinon.spy( imageInsertPanelView, 'focus' ); - } ); + editor = await ClassicTestEditor.create( editorElement, config ); - dropdown.buttonView.fire( 'open' ); - sinon.assert.calledOnce( spy ); - } ); - } ); - } ); + insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + } } ); - -function fakeEventData() { - return { - preventDefault: sinon.spy() - }; -} diff --git a/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js new file mode 100644 index 00000000000..24980785553 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/imageinsertviaurlui.js @@ -0,0 +1,417 @@ +/** + * @license Copyright (c) 2003-2023, 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 Model from '@ckeditor/ckeditor5-ui/src/model'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview'; + +import { CollapsibleView, DropdownButtonView } from '@ckeditor/ckeditor5-ui'; +import { icons } from '@ckeditor/ckeditor5-core'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import Image from '../../src/image'; +import ImageInsertFormView from '../../src/imageinsert/ui/imageinsertformview'; +import ImageInsertViaUrlUI from '../../src/imageinsert/imageinsertviaurlui'; +import { ImageInsertViaUrl } from '../../src'; +import ImageInsertUrlView from '../../src/imageinsert/ui/imageinserturlview'; + +describe( 'ImageInsertViaUrlUI', () => { + let editor, editorElement, insertImageUI; + + testUtils.createSinonSandbox(); + + afterEach( async () => { + if ( editorElement ) { + editorElement.remove(); + } + + if ( editor ) { + await editor.destroy(); + } + } ); + + it( 'should have pluginName', () => { + expect( ImageInsertViaUrlUI.pluginName ).to.equal( 'ImageInsertViaUrlUI' ); + } ); + + describe( 'single integration', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); + } ); + + it( 'should create toolbar dropdown button', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView ).to.be.instanceOf( DropdownButtonView ); + expect( dropdown.buttonView.icon ).to.equal( icons.imageUrl ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should create form view on first open of dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.panelView.children.length ).to.equal( 0 ); + + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); + + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.length ).to.equal( 1 ); + expect( formView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); + } ); + + describe( 'form bindings', () => { + let dropdown, formView, urlView; + + beforeEach( () => { + dropdown = editor.ui.componentFactory.create( 'insertImage' ); + dropdown.isOpen = true; + formView = dropdown.panelView.children.get( 0 ); + urlView = formView.children.get( 0 ); + } ); + + it( 'should bind #isImageSelected', () => { + expect( urlView.isImageSelected ).to.be.false; + + insertImageUI.isImageSelected = true; + expect( urlView.isImageSelected ).to.be.true; + + insertImageUI.isImageSelected = false; + expect( urlView.isImageSelected ).to.be.false; + } ); + + it( 'should bind #isEnabled', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const insertImageCommand = editor.commands.get( 'insertImage' ); + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.false; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + } ); + + it( 'should set #imageURLInputValue at first open', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'foobar'; + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const urlView = formView.children.get( 0 ); + + expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); + } ); + + it( 'should reset #imageURLInputValue on dropdown reopen', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'abc'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( 'abc' ); + + replaceImageSourceCommand.value = '123'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '123' ); + + replaceImageSourceCommand.value = undefined; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '' ); + } ); + + it( 'should execute replaceImageSource command and close dropdown', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should execute insertImage command', () => { + const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should close dropdown', () => { + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + urlView.fire( 'cancel' ); + + expect( stubExecute.notCalled ).to.be.true; + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + } ); + + describe( 'multiple integrations', () => { + beforeEach( async () => { + await createEditor( { + plugins: [ Image, ImageInsertViaUrl ] + } ); + + const observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'foo', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + + editor.config.set( 'image.insert.integrations', [ 'url', 'foo' ] ); + } ); + + it( 'should create toolbar dropdown button', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView ).to.be.instanceOf( SplitButtonView ); + expect( dropdown.buttonView.tooltip ).to.be.true; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.icon ).to.equal( icons.imageUrl ); + expect( dropdown.buttonView.actionView.tooltip ).to.be.true; + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind button label to ImageInsertUI#isImageSelected', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + + insertImageUI.isImageSelected = true; + expect( dropdown.buttonView.label ).to.equal( 'Replace image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( dropdown.buttonView.label ).to.equal( 'Insert image' ); + expect( dropdown.buttonView.actionView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should create form view on first open of dropdown', () => { + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + expect( dropdown.panelView.children.length ).to.equal( 0 ); + + dropdown.isOpen = true; + expect( dropdown.panelView.children.length ).to.equal( 1 ); + + const formView = dropdown.panelView.children.get( 0 ); + expect( formView ).to.be.instanceOf( ImageInsertFormView ); + expect( formView.children.length ).to.equal( 2 ); + + const collapsibleView = formView.children.get( 0 ); + expect( collapsibleView ).to.be.instanceOf( CollapsibleView ); + expect( collapsibleView.children.get( 0 ) ).to.be.instanceOf( ImageInsertUrlView ); + } ); + + describe( 'form bindings', () => { + let dropdown, formView, collapsibleView, urlView; + + beforeEach( () => { + dropdown = editor.ui.componentFactory.create( 'insertImage' ); + dropdown.isOpen = true; + formView = dropdown.panelView.children.get( 0 ); + collapsibleView = formView.children.get( 0 ); + urlView = collapsibleView.children.get( 0 ); + } ); + + it( 'should bind #isImageSelected', () => { + expect( urlView.isImageSelected ).to.be.false; + + insertImageUI.isImageSelected = true; + expect( urlView.isImageSelected ).to.be.true; + expect( collapsibleView.label ).to.equal( 'Update image URL' ); + + insertImageUI.isImageSelected = false; + expect( urlView.isImageSelected ).to.be.false; + expect( collapsibleView.label ).to.equal( 'Insert image via URL' ); + } ); + + it( 'should bind #isEnabled', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const insertImageCommand = editor.commands.get( 'insertImage' ); + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.false; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = false; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = false; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + + replaceImageSourceCommand.isEnabled = true; + insertImageCommand.isEnabled = true; + expect( urlView.isEnabled ).to.be.true; + } ); + + it( 'should set #imageURLInputValue and CollapsibleView#isCollapsed at first open', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'foobar'; + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const collapsibleView = formView.children.get( 0 ); + const urlView = collapsibleView.children.get( 0 ); + + expect( urlView.imageURLInputValue ).to.equal( 'foobar' ); + expect( collapsibleView.isCollapsed ).to.be.true; + } ); + + it( 'should reset #imageURLInputValue and CollapsibleView#isCollapsed on dropdown reopen', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + + replaceImageSourceCommand.value = 'abc'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( 'abc' ); + expect( collapsibleView.isCollapsed ).to.be.true; + + replaceImageSourceCommand.value = '123'; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '123' ); + expect( collapsibleView.isCollapsed ).to.be.true; + + replaceImageSourceCommand.value = undefined; + dropdown.isOpen = false; + dropdown.isOpen = true; + expect( urlView.imageURLInputValue ).to.equal( '' ); + expect( collapsibleView.isCollapsed ).to.be.true; + } ); + + it( 'should execute replaceImageSource command and close dropdown', () => { + const replaceImageSourceCommand = editor.commands.get( 'replaceImageSource' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'replaceImageSource' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should execute insertImage command', () => { + const replaceImageSourceCommand = editor.commands.get( 'insertImage' ); + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + replaceImageSourceCommand.isEnabled = true; + urlView.imageURLInputValue = 'foo'; + + urlView.fire( 'submit' ); + + expect( stubExecute.calledOnce ).to.be.true; + expect( stubExecute.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( stubExecute.firstCall.args[ 1 ] ).to.deep.equal( { source: 'foo' } ); + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + + it( 'should close dropdown', () => { + const stubExecute = sinon.stub( editor, 'execute' ); + const stubFocus = sinon.stub( editor.editing.view, 'focus' ); + + urlView.fire( 'cancel' ); + + expect( stubExecute.notCalled ).to.be.true; + expect( stubFocus.calledOnce ).to.be.true; + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + } ); + + async function createEditor( config ) { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + editor = await ClassicTestEditor.create( editorElement, config ); + + insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + } +} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js deleted file mode 100644 index 2d52d391bda..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformrowview.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. 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/imageinsert/ui/imageinsertformrowview'; -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(); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js new file mode 100644 index 00000000000..641d109bee8 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertformview.js @@ -0,0 +1,622 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals document, Event */ + +import ImageInsertFormView from '../../../src/imageinsert/ui/imageinsertformview'; +import ImageInsertUrlView from '../../../src/imageinsert/ui/imageinserturlview'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +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 CollapsibleView from '@ckeditor/ckeditor5-ui/src/collapsible/collapsibleview'; + +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +describe( 'ImageInsertFormView', () => { + let view; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val } ); + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + describe( 'constructor()', () => { + 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 ); + } ); + + it( 'should have #children view collection', () => { + expect( view.children ).to.be.instanceOf( ViewCollection ); + } ); + } ); + + describe( 'integrations', () => { + it( 'single integrations', () => { + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + + const view = new ImageInsertFormView( { t: val => val }, [ + inputIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + inputIntegrationView + ] ); + } ); + + it( 'multiple integrations', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + + const view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + inputIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + inputIntegrationView + ] ); + } ); + + it( 'integrations with collapsible view', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + const collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ + inputIntegrationView + ] ); + + const view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + expect( view._focusables.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + collapsibleIntegrationView, + inputIntegrationView + ] ); + + expect( view.children.map( f => f ) ).to.have.members( [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + } ); + } ); + + describe( 'template', () => { + it( 'should create element from the template', () => { + expect( view.element.tagName ).to.equal( 'FORM' ); + expect( view.element.classList.contains( 'ck' ) ).to.true; + expect( view.element.classList.contains( 'ck-image-insert-form' ) ).to.true; + expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); + } ); + + it( 'should bind #children', () => { + expect( view.template.children[ 0 ] ).to.equal( view.children ); + } ); + } ); + + describe( 'render()', () => { + it( 'should handle and delegate DOM submit event', () => { + const spy = sinon.spy(); + + view.on( 'submit', spy ); + view.element.dispatchEvent( new Event( 'submit' ) ); + + expect( spy.calledOnce ).to.true; + } ); + + it( 'should register focusables in #focusTracker', () => { + const buttonIntegrationView = new ButtonView( { t: val => val } ); + const inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + const collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ + inputIntegrationView + ] ); + + const view = new ImageInsertFormView( { t: () => {} }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + const spy = sinon.spy( view.focusTracker, 'add' ); + + view.render(); + + sinon.assert.calledWithExactly( spy.getCall( 0 ), buttonIntegrationView.element ); + sinon.assert.calledWithExactly( spy.getCall( 1 ), collapsibleIntegrationView.element ); + sinon.assert.calledWithExactly( spy.getCall( 2 ), inputIntegrationView.element ); + sinon.assert.calledThrice( spy ); + + view.destroy(); + } ); + + describe( 'activates keyboard navigation', () => { + let view, firstIntegrationView, secondIntegrationView; + + beforeEach( () => { + firstIntegrationView = new ButtonView( { t: val => val } ); + secondIntegrationView = new ButtonView( { t: val => val } ); + + view = new ImageInsertFormView( { t: () => {} }, [ + firstIntegrationView, + secondIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'so "tab" focuses the next focusable item', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + // Mock the first integration focused. + view.focusTracker.isFocused = true; + view.focusTracker.focusedElement = firstIntegrationView.element; + + const spy = sinon.spy( secondIntegrationView, '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 = secondIntegrationView.element; + + const spy = sinon.spy( firstIntegrationView, 'focus' ); + + view.keystrokes.press( keyEvtData ); + sinon.assert.calledOnce( keyEvtData.preventDefault ); + sinon.assert.calledOnce( keyEvtData.stopPropagation ); + sinon.assert.calledOnce( spy ); + } ); + + 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 ); + } ); + } ); + } ); + + describe( 'destroy()', () => { + it( 'should destroy the FocusTracker instance', () => { + const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + + it( 'should destroy the KeystrokeHandler instance', () => { + const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + } ); + + describe( 'focus()', () => { + it( 'should focus first focusable', () => { + const spy = sinon.spy( view._focusCycler, 'focusFirst' ); + + view.focus(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'focus cycling', () => { + let view, inputIntegrationView, buttonIntegrationView, otherButtonIntegrationView, collapsibleIntegrationView; + + beforeEach( () => { + inputIntegrationView = new ImageInsertUrlView( { t: val => val } ); + buttonIntegrationView = new ButtonView( { t: val => val } ); + otherButtonIntegrationView = new ButtonView( { t: val => val } ); + } ); + + describe( 'single button integration', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + } ); + + describe( 'single URL input integration', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + inputIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + } ); + } ); + + describe( 'multiple button integrations', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + otherButtonIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( otherButtonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( otherButtonIntegrationView.element ); + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + } ); + } ); + + describe( 'mixed integrations (button and URL input)', () => { + beforeEach( () => { + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + inputIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + } ); + + describe( 'mixed integrations (button and URL input inside a collapsible view)', () => { + beforeEach( () => { + collapsibleIntegrationView = new CollapsibleView( { t: val => val }, [ inputIntegrationView ] ); + + view = new ImageInsertFormView( { t: val => val }, [ + buttonIntegrationView, + collapsibleIntegrationView + ] ); + + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + it( 'forward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: false, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); + expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + + it( 'backward cycling', () => { + const keyEvtData = { + keyCode: keyCodes.tab, + shiftKey: true, + stopPropagation: sinon.spy(), + preventDefault: sinon.spy() + }; + + view.focus(); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.cancelButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.insertButtonView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( inputIntegrationView.element ); + expect( inputIntegrationView.focusTracker.focusedElement ).to.equal( inputIntegrationView.urlInputView.element ); + + inputIntegrationView.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( collapsibleIntegrationView.element ); + expect( collapsibleIntegrationView.focusTracker ).to.be.undefined; + + view.keystrokes.press( keyEvtData ); + expect( view.focusTracker.focusedElement ).to.equal( buttonIntegrationView.element ); + expect( buttonIntegrationView.focusTracker ).to.be.undefined; + } ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js deleted file mode 100644 index a6cd2122e17..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/ui/imageinsertpanelview.js +++ /dev/null @@ -1,329 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, 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 LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; - -import ImageInsertPanelView from '../../../src/imageinsert/ui/imageinsertpanelview'; -import ImageUploadFormRowView from '../../../src/imageinsert/ui/imageinsertformrowview'; -import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -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 { createLabeledInputView } from '../../../src/imageinsert/utils'; - -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; - -describe( 'ImageInsertPanelView', () => { - let view; - - beforeEach( () => { - view = new ImageInsertPanelView( { t: val => val }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) - } ); - view.render(); - document.body.appendChild( view.element ); - } ); - - afterEach( () => { - view.element.remove(); - view.destroy(); - sinon.restore(); - } ); - - describe( 'constructor()', () => { - 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 #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 ImageInsertPanelView as integrations object', () => { - const view = new ImageInsertPanelView( { t: val => val }, { - '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 it is passed via integrations object', () => { - const view = new ImageInsertPanelView( { t: val => val }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ), - '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 contain no integrations when they were not provided', () => { - const view = new ImageInsertPanelView( { t: val => val } ); - - expect( view._integrations ).to.be.instanceOf( Collection ); - expect( view._integrations.length ).to.equal( 0 ); - } ); - } ); - - 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( 'template', () => { - it( 'should create element from the template', () => { - expect( view.element.classList.contains( 'ck' ) ).to.true; - expect( view.element.classList.contains( 'ck-image-insert-form' ) ).to.true; - expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); - } ); - - 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._integrations, - view.insertButtonView, - view.cancelButtonView - ] ); - } ); - - it( 'should register child views\' #element in #focusTracker with no integrations', () => { - const view = new ImageInsertPanelView( { t: () => {} } ); - - const spy = testUtils.sinon.spy( view.focusTracker, 'add' ); - view.render(); - - sinon.assert.calledWithExactly( spy.getCall( 0 ), view.insertButtonView.element ); - sinon.assert.calledWithExactly( spy.getCall( 1 ), view.cancelButtonView.element ); - - view.destroy(); - } ); - - it( 'should register child views\' #element in #focusTracker with "insertImageViaUrl" integration', () => { - const view = new ImageInsertPanelView( { t: () => {} }, { - 'insertImageViaUrl': createLabeledInputView( { t: val => val } ) - } ); - - const spy = testUtils.sinon.spy( view.focusTracker, 'add' ); - - view.render(); - - sinon.assert.calledWithExactly( spy.getCall( 0 ), view.getIntegration( 'insertImageViaUrl' ).element ); - sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element ); - sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element ); - - view.destroy(); - } ); - - it( 'starts listening for #keystrokes coming from #element', () => { - const view = new ImageInsertPanelView( { t: () => {} } ); - - const spy = sinon.spy( view.keystrokes, 'listenTo' ); - - view.render(); - sinon.assert.calledOnce( spy ); - sinon.assert.calledWithExactly( spy, view.element ); - - view.destroy(); - } ); - - 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 ); - } ); - - 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.getIntegration( 'insertImageViaUrl' ).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( 'destroy()', () => { - it( 'should destroy the FocusTracker instance', () => { - const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); - - view.destroy(); - - sinon.assert.calledOnce( destroySpy ); - } ); - - it( 'should destroy the KeystrokeHandler instance', () => { - const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); - - view.destroy(); - - sinon.assert.calledOnce( destroySpy ); - } ); - } ); - - describe( 'focus()', () => { - it( 'should focus on the first integration', () => { - const spy = sinon.spy( view.getIntegration( 'insertImageViaUrl' ), 'focus' ); - - view.focus(); - - sinon.assert.calledOnce( spy ); - } ); - } ); - - describe( 'Insert image via URL integration input', () => { - it( 'should be bound with #imageURLInputValue', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - - form.fieldView.element.value = 'abc'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'abc' ); - - form.fieldView.element.value = 'xyz'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'xyz' ); - } ); - - it( 'should trim input value', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - - form.fieldView.element.value = ' '; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( '' ); - - form.fieldView.element.value = ' test '; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'test' ); - } ); - - it( 'binds saveButtonView#isEnabled to URL input value', () => { - const form = view.getIntegration( 'insertImageViaUrl' ); - const saveButtonView = view.template.children[ 1 ].children.first; - - expect( saveButtonView.isEnabled ).to.be.false; - - form.fieldView.element.value = 'test'; - form.fieldView.fire( 'input' ); - - expect( view.imageURLInputValue ).to.equal( 'test' ); - expect( !!saveButtonView.isEnabled ).to.be.true; - } ); - } ); -} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js new file mode 100644 index 00000000000..a8ee56ed608 --- /dev/null +++ b/packages/ckeditor5-image/tests/imageinsert/ui/imageinserturlview.js @@ -0,0 +1,377 @@ +/** + * @license Copyright (c) 2003-2023, 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 LabeledFieldView from '@ckeditor/ckeditor5-ui/src/labeledfield/labeledfieldview'; + +import ImageInsertUrlView from '../../../src/imageinsert/ui/imageinserturlview'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +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 InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview'; +import { icons } from '@ckeditor/ckeditor5-core'; + +describe( 'ImageInsertUrlView', () => { + let view; + + testUtils.createSinonSandbox(); + + beforeEach( () => { + view = new ImageInsertUrlView( { t: val => val } ); + view.render(); + document.body.appendChild( view.element ); + } ); + + afterEach( () => { + view.element.remove(); + view.destroy(); + } ); + + describe( 'constructor()', () => { + it( 'should have #imageURLInputValue', () => { + expect( view.imageURLInputValue ).to.equal( '' ); + } ); + + it( 'should have #isImageSelected', () => { + expect( view.isImageSelected ).to.be.false; + } ); + + it( 'should have #isEnabled', () => { + expect( view.isEnabled ).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( 'template', () => { + it( 'should create element from the template', () => { + expect( view.element.tagName ).to.equal( 'DIV' ); + expect( view.element.classList.contains( 'ck' ) ).to.true; + expect( view.element.classList.contains( 'ck-image-insert-url' ) ).to.true; + + const childNodes = view.element.childNodes; + + expect( childNodes[ 0 ] ).to.equal( view.urlInputView.element ); + expect( childNodes[ 1 ].tagName ).to.equal( 'DIV' ); + expect( childNodes[ 1 ].classList.contains( 'ck' ) ).to.be.true; + expect( childNodes[ 1 ].classList.contains( 'ck-image-insert-url__action-row' ) ).to.be.true; + + const childNodes2 = childNodes[ 1 ].childNodes; + + expect( childNodes2[ 0 ] ).to.equal( view.insertButtonView.element ); + expect( childNodes2[ 1 ] ).to.equal( view.cancelButtonView.element ); + } ); + + it( 'should use dedicated views', () => { + expect( view.template.children[ 0 ] ).to.equal( view.urlInputView ); + expect( view.template.children[ 1 ].children[ 0 ] ).to.equal( view.insertButtonView ); + expect( view.template.children[ 1 ].children[ 1 ] ).to.equal( view.cancelButtonView ); + } ); + } ); + + describe( 'render()', () => { + it( 'should register child views in #_focusables', () => { + expect( view._focusables.map( f => f ) ).to.have.members( [ + view.urlInputView, + view.insertButtonView, + view.cancelButtonView + ] ); + } ); + + it( 'should register child views\' #element in #focusTracker', () => { + const view = new ImageInsertUrlView( { t: () => {} } ); + + const spy = sinon.spy( view.focusTracker, 'add' ); + view.render(); + + sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element ); + sinon.assert.calledWithExactly( spy.getCall( 1 ), view.insertButtonView.element ); + sinon.assert.calledWithExactly( spy.getCall( 2 ), view.cancelButtonView.element ); + + view.destroy(); + } ); + + 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.urlInputView.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( 'destroy()', () => { + it( 'should destroy the FocusTracker instance', () => { + const destroySpy = sinon.spy( view.focusTracker, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + + it( 'should destroy the KeystrokeHandler instance', () => { + const destroySpy = sinon.spy( view.keystrokes, 'destroy' ); + + view.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); + } ); + + describe( 'focus()', () => { + it( 'should focus the url input', () => { + const spy = sinon.spy( view.urlInputView, 'focus' ); + + view.focus(); + + sinon.assert.calledOnce( spy ); + } ); + + it( 'should focus the last focusable', () => { + const spy = sinon.spy( view.cancelButtonView, 'focus' ); + + view.focus( -1 ); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( '#urlInputView', () => { + it( 'should be an instance of the LabeledFieldView', () => { + expect( view.urlInputView ).to.be.instanceOf( LabeledFieldView ); + } ); + + it( 'should accept text', () => { + expect( view.urlInputView.fieldView ).to.be.instanceOf( InputTextView ); + } ); + + it( 'should bind label to #isImageSelected', () => { + view.isImageSelected = false; + + expect( view.urlInputView.label ).to.equal( 'Insert image via URL' ); + + view.isImageSelected = true; + + expect( view.urlInputView.label ).to.equal( 'Update image URL' ); + } ); + + it( 'should bind isEnabled to #isEnabled', () => { + view.isEnabled = false; + + expect( view.urlInputView.isEnabled ).to.be.false; + + view.isEnabled = true; + + expect( view.urlInputView.isEnabled ).to.be.true; + } ); + + it( 'should set placeholder', () => { + expect( view.urlInputView.placeholder ).to.equal( 'https://example.com/image.png' ); + expect( view.urlInputView.fieldView.placeholder ).to.equal( 'https://example.com/image.png' ); + } ); + + it( 'should bind value to #imageURLInputValue', () => { + view.imageURLInputValue = 'abc'; + + expect( view.urlInputView.fieldView.value ).to.equal( 'abc' ); + + view.imageURLInputValue = null; + + expect( view.urlInputView.fieldView.value ).to.equal( '' ); + } ); + + it( 'should be bound with #imageURLInputValue', () => { + view.urlInputView.fieldView.element.value = 'abc'; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'abc' ); + + view.urlInputView.fieldView.element.value = 'xyz'; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'xyz' ); + } ); + + it( 'should trim input value', () => { + view.urlInputView.fieldView.element.value = ' '; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( '' ); + + view.urlInputView.fieldView.element.value = ' test '; + view.urlInputView.fieldView.fire( 'input' ); + + expect( view.imageURLInputValue ).to.equal( 'test' ); + } ); + } ); + + describe( '#insertButtonView', () => { + it( 'should be an instance of the ButtonView', () => { + expect( view.insertButtonView ).to.be.instanceOf( ButtonView ); + } ); + + it( 'should have an icon', () => { + expect( view.insertButtonView.icon ).to.equal( icons.check ); + } ); + + it( 'should have a class', () => { + expect( view.insertButtonView.class ).to.equal( 'ck-button-save' ); + } ); + + it( 'should be a submit button', () => { + expect( view.insertButtonView.type ).to.equal( 'submit' ); + } ); + + it( 'should have text', () => { + expect( view.insertButtonView.withText ).to.be.true; + } ); + + it( 'should bind label to #isImageSelected', () => { + view.isImageSelected = false; + + expect( view.insertButtonView.label ).to.equal( 'Insert' ); + + view.isImageSelected = true; + + expect( view.insertButtonView.label ).to.equal( 'Update' ); + } ); + + it( 'should bind isEnabled to #isEnabled and #imageURLInputValue', () => { + view.isEnabled = false; + view.imageURLInputValue = ''; + + expect( view.insertButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + view.imageURLInputValue = 'abc'; + + expect( view.insertButtonView.isEnabled ).to.be.true; + + view.isEnabled = false; + view.imageURLInputValue = 'abc'; + + expect( view.insertButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + view.imageURLInputValue = ''; + + expect( view.insertButtonView.isEnabled ).to.be.false; + } ); + + 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; + } ); + } ); + + describe( '#cancelButtonView', () => { + it( 'should be an instance of the ButtonView', () => { + expect( view.cancelButtonView ).to.be.instanceOf( ButtonView ); + } ); + + it( 'should have an icon', () => { + expect( view.cancelButtonView.icon ).to.equal( icons.cancel ); + } ); + + it( 'should have a class', () => { + expect( view.cancelButtonView.class ).to.equal( 'ck-button-cancel' ); + } ); + + it( 'should be a plain button', () => { + expect( view.cancelButtonView.type ).to.equal( 'button' ); + } ); + + it( 'should have text', () => { + expect( view.cancelButtonView.withText ).to.be.true; + } ); + + it( 'should have label', () => { + expect( view.cancelButtonView.label ).to.equal( 'Cancel' ); + } ); + + it( 'should bind isEnabled to #isEnabled', () => { + view.isEnabled = false; + + expect( view.cancelButtonView.isEnabled ).to.be.false; + + view.isEnabled = true; + + expect( view.cancelButtonView.isEnabled ).to.be.true; + + view.isEnabled = false; + + expect( view.cancelButtonView.isEnabled ).to.be.false; + } ); + + 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; + } ); + } ); +} ); diff --git a/packages/ckeditor5-image/tests/imageinsert/utils.js b/packages/ckeditor5-image/tests/imageinsert/utils.js deleted file mode 100644 index 4bd56c46995..00000000000 --- a/packages/ckeditor5-image/tests/imageinsert/utils.js +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @license Copyright (c) 2003-2023, 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 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/imageinsert/imageinsertui'; -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 { prepareIntegrations, createLabeledInputView } from '../../src/imageinsert/utils'; -import CKFinderUploadAdapter from '@ckeditor/ckeditor5-adapter-ckfinder/src/uploadadapter'; - -describe( 'Upload utils', () => { - 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: [ - Paragraph, - Link, - Image, - ImageUploadUI, - CKFinderUploadAdapter, - CKFinder - ], - image: { - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } - } - } ); - - const openCKFinderExtendedView = Object.values( prepareIntegrations( editor ) )[ 1 ]; - - expect( openCKFinderExtendedView.class ).contains( 'ck-image-insert__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 "insertImageViaUrl" integration and throw warning ' + - 'for "image-upload-integrations-invalid-view" error', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Image, - ImageUploadUI - ], - image: { - insert: { - integrations: [ - '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, - ImageUploadUI - ], - image: { - insert: { - integrations: [ - '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 return "insertImageViaUrl" integration, when no integrations were configured', async () => { - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - const editor = await ClassicEditor - .create( editorElement, { - plugins: [ - Paragraph, - Image, - ImageUploadUI - ] - } ); - - expect( Object.keys( prepareIntegrations( editor ) ).length ).to.equal( 1 ); - - editor.destroy(); - editorElement.remove(); - } ); - } ); - - describe( 'createLabeledInputView()', () => { - describe( 'image URL input view', () => { - it( 'should have placeholder', () => { - const view = createLabeledInputView( { t: val => val } ); - expect( view.fieldView.placeholder ).to.equal( 'https://example.com/image.png' ); - } ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-image/tests/imageinsertviaurl.js b/packages/ckeditor5-image/tests/imageinsertviaurl.js index 2f760789851..8b614080785 100644 --- a/packages/ckeditor5-image/tests/imageinsertviaurl.js +++ b/packages/ckeditor5-image/tests/imageinsertviaurl.js @@ -6,6 +6,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import ImageInsertUI from '../src/imageinsert/imageinsertui'; import ImageInsertViaUrl from '../src/imageinsertviaurl'; +import ImageInsertViaUrlUI from '../src/imageinsert/imageinsertviaurlui'; import global from '@ckeditor/ckeditor5-utils/src/dom/global'; @@ -23,7 +24,6 @@ describe( 'ImageInsertViaUrl', () => { afterEach( async () => { editorElement.remove(); - await editor.destroy(); } ); @@ -35,6 +35,10 @@ describe( 'ImageInsertViaUrl', () => { expect( editor.plugins.get( 'ImageInsertUI' ) ).to.instanceOf( ImageInsertUI ); } ); + it( 'should load ImageInsertViaUrlUI plugin', () => { + expect( editor.plugins.get( 'ImageInsertViaUrlUI' ) ).to.instanceOf( ImageInsertViaUrlUI ); + } ); + it( 'should not load ImageUpload plugin', () => { expect( editor.plugins.has( 'ImageUpload' ) ).to.be.false; } ); diff --git a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js index ea08d8f86ef..b5cac6f247b 100644 --- a/packages/ckeditor5-image/tests/imageupload/imageuploadui.js +++ b/packages/ckeditor5-image/tests/imageupload/imageuploadui.js @@ -16,13 +16,19 @@ 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 ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; +import { icons } from 'ckeditor5/src/core'; 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'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; describe( 'ImageUploadUI', () => { let editor, model, editorElement, fileRepository; + testUtils.createSinonSandbox(); + class UploadAdapterPluginMock extends Plugin { init() { fileRepository = this.editor.plugins.get( FileRepository ); @@ -83,11 +89,11 @@ describe( 'ImageUploadUI', () => { command.isEnabled = true; - expect( button.buttonView.isEnabled ).to.true; + expect( button.isEnabled ).to.true; command.isEnabled = false; - expect( button.buttonView.isEnabled ).to.false; + expect( button.isEnabled ).to.false; } ); // ckeditor5-upload/#77 @@ -98,11 +104,11 @@ describe( 'ImageUploadUI', () => { button.render(); - button.buttonView.on( 'execute', spy ); + button.on( 'execute', spy ); command.isEnabled = false; - button.buttonView.element.dispatchEvent( new Event( 'click' ) ); + button.element.dispatchEvent( new Event( 'click' ) ); sinon.assert.notCalled( spy ); } ); @@ -201,4 +207,106 @@ describe( 'ImageUploadUI', () => { expect( spy ).to.be.calledOnce; } ); + + describe( 'InsertImageUI integration', () => { + it( 'should create FileDialogButtonView in split button dropdown button', () => { + mockAssetManagerIntegration(); + + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const dropdownButton = dropdown.buttonView.actionView; + + expect( dropdownButton ).to.be.instanceOf( FileDialogButtonView ); + expect( dropdownButton.withText ).to.be.false; + expect( dropdownButton.icon ).to.equal( icons.imageUpload ); + + expect( spy.calledTwice ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'insertImage' ); + expect( spy.secondCall.args[ 0 ] ).to.equal( 'uploadImage' ); + expect( spy.firstCall.returnValue ).to.equal( dropdown.buttonView.actionView ); + } ); + + it( 'should create FileDialogButtonView in dropdown panel', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + const spy = sinon.spy( editor.ui.componentFactory, 'create' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + expect( buttonView ).to.be.instanceOf( FileDialogButtonView ); + expect( buttonView.withText ).to.be.true; + expect( buttonView.icon ).to.equal( icons.imageUpload ); + + expect( spy.calledOnce ).to.be.true; + expect( spy.firstCall.args[ 0 ] ).to.equal( 'uploadImage' ); + expect( spy.firstCall.returnValue ).to.equal( buttonView ); + } ); + + it( 'should bind to #isImageSelected', () => { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const dropdownButton = dropdown.buttonView.actionView; + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + insertImageUI.isImageSelected = false; + expect( dropdownButton.label ).to.equal( 'Upload image from computer' ); + expect( buttonView.label ).to.equal( 'Upload from computer' ); + + insertImageUI.isImageSelected = true; + expect( dropdownButton.label ).to.equal( 'Replace image from computer' ); + expect( buttonView.label ).to.equal( 'Replace from computer' ); + } ); + + it( 'should close dropdown on execute', () => { + mockAssetManagerIntegration(); + + const dropdown = editor.ui.componentFactory.create( 'insertImage' ); + + dropdown.isOpen = true; + + const formView = dropdown.panelView.children.get( 0 ); + const buttonView = formView.children.get( 0 ); + + sinon.stub( editor, 'execute' ); + + buttonView.fire( 'execute' ); + + expect( dropdown.isOpen ).to.be.false; + } ); + } ); + + function mockAssetManagerIntegration() { + const insertImageUI = editor.plugins.get( 'ImageInsertUI' ); + const observable = new Model( { isEnabled: true } ); + + insertImageUI.registerIntegration( { + name: 'assetManager', + observable, + buttonViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'foo'; + + return button; + }, + formViewCreator() { + const button = new ButtonView( editor.locale ); + + button.label = 'bar'; + + return button; + } + } ); + } } ); diff --git a/packages/ckeditor5-image/tests/manual/imageblock.js b/packages/ckeditor5-image/tests/manual/imageblock.js index 4cbf2e36f0a..9b057a77b6c 100644 --- a/packages/ckeditor5-image/tests/manual/imageblock.js +++ b/packages/ckeditor5-image/tests/manual/imageblock.js @@ -65,13 +65,7 @@ ClassicEditor 'redo' ], image: { - toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } + toolbar: [ 'imageStyle:block', 'imageStyle:side', '|', 'toggleImageCaption', 'imageTextAlternative' ] }, ckfinder: { // eslint-disable-next-line max-len diff --git a/packages/ckeditor5-image/tests/manual/imageinline.js b/packages/ckeditor5-image/tests/manual/imageinline.js index 83f50205013..3633477567e 100644 --- a/packages/ckeditor5-image/tests/manual/imageinline.js +++ b/packages/ckeditor5-image/tests/manual/imageinline.js @@ -67,13 +67,7 @@ ClassicEditor 'redo' ], image: { - toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', '|', 'imageTextAlternative' ], - insert: { - integrations: [ - 'insertImageViaUrl', - 'openCKFinder' - ] - } + toolbar: [ 'imageStyle:inline', 'imageStyle:wrapText', '|', 'imageTextAlternative' ] }, ckfinder: { // eslint-disable-next-line max-len diff --git a/packages/ckeditor5-image/tests/manual/imageinsert.html b/packages/ckeditor5-image/tests/manual/imageinsert.html index de7cc867e63..f95b98c8ef4 100644 --- a/packages/ckeditor5-image/tests/manual/imageinsert.html +++ b/packages/ckeditor5-image/tests/manual/imageinsert.html @@ -10,6 +10,13 @@ } ++ Insert image integrations: + + + +
+