/** * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Clipboard from '../src/clipboard'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import ClipboardObserver from '../src/clipboardobserver'; import DataTransfer from '../src/datatransfer'; import { stringify as stringifyView, parse as parseView } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { stringify as stringifyModel, setData as setModelData, getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import ViewDocumentFragment from '@ckeditor/ckeditor5-engine/src/view/documentfragment'; import ViewText from '@ckeditor/ckeditor5-engine/src/view/text'; describe( 'Clipboard feature', () => { let editor, view, viewDocument, clipboardPlugin, scrollSpy; beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Clipboard, Paragraph ] } ) .then( newEditor => { editor = newEditor; view = editor.editing.view; viewDocument = editor.editing.view.document; clipboardPlugin = editor.plugins.get( 'Clipboard' ); // VirtualTestEditor has no DOM, so this method must be stubbed for all tests. // Otherwise it will throw as it accesses the DOM to do its job. scrollSpy = sinon.stub( view, 'scrollToTheSelection' ); } ); } ); describe( 'constructor()', () => { it( 'registers ClipboardObserver', () => { expect( view.getObserver( ClipboardObserver ) ).to.be.instanceOf( ClipboardObserver ); } ); } ); describe( 'clipboard paste pipeline', () => { describe( 'takes HTML data from the dataTransfer', () => { it( 'and fires the clipboardInput event on the editingView', done => { const dataTransferMock = createDataTransfer( { 'text/html': '
x
', 'text/plain': 'y' } ); const preventDefaultSpy = sinon.spy(); viewDocument.on( 'clipboardInput', ( evt, data ) => { expect( preventDefaultSpy.calledOnce ).to.be.true; expect( data.dataTransfer ).to.equal( dataTransferMock ); done(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); it( 'and fires the inputTransformation event on the clipboardPlugin', done => { const dataTransferMock = createDataTransfer( { 'text/html': 'x
', 'text/plain': 'y' } ); const preventDefaultSpy = sinon.spy(); clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { expect( data.content ).is.instanceOf( ViewDocumentFragment ); expect( data.dataTransfer ).to.equal( dataTransferMock ); expect( stringifyView( data.content ) ).to.equal( 'x
' ); done(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); } ); describe( 'takes plain text data from the dataTransfer if there is no HTML', () => { it( 'and fires the clipboardInput event on the editingView', done => { const dataTransferMock = createDataTransfer( { 'text/plain': 'x\n\ny z' } ); const preventDefaultSpy = sinon.spy(); viewDocument.on( 'clipboardInput', ( evt, data ) => { expect( preventDefaultSpy.calledOnce ).to.be.true; expect( data.dataTransfer ).to.equal( dataTransferMock ); done(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); it( 'and fires the inputTransformation event on the clipboardPlugin', done => { const dataTransferMock = createDataTransfer( { 'text/plain': 'x\n\ny z' } ); const preventDefaultSpy = sinon.spy(); clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { expect( data.content ).is.instanceOf( ViewDocumentFragment ); expect( data.dataTransfer ).to.equal( dataTransferMock ); expect( stringifyView( data.content ) ).to.equal( 'x
y z
' ); done(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); } ); it( 'fires events with empty data if there is no HTML nor plain text', done => { const dataTransferMock = createDataTransfer( {} ); const preventDefaultSpy = sinon.spy(); const editorViewCalled = sinon.spy(); viewDocument.on( 'clipboardInput', ( evt, data ) => { expect( preventDefaultSpy.calledOnce ).to.be.true; expect( data.dataTransfer ).to.equal( dataTransferMock ); editorViewCalled(); } ); clipboardPlugin.on( 'inputTransformation', ( evt, data ) => { expect( data.content ).is.instanceOf( ViewDocumentFragment ); expect( data.dataTransfer ).to.equal( dataTransferMock ); expect( stringifyView( data.content ) ).to.equal( '' ); expect( editorViewCalled.calledOnce ).to.be.true; done(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); it( 'uses low priority observer for the paste event', () => { const dataTransferMock = createDataTransfer( { 'text/html': 'x' } ); const spy = sinon.spy(); viewDocument.on( 'paste', evt => { evt.stop(); } ); viewDocument.on( 'clipboardInput', spy ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, preventDefault() {} } ); expect( spy.callCount ).to.equal( 0 ); } ); it( 'inserts content to the editor', () => { const dataTransferMock = createDataTransfer( { 'text/html': 'x
', 'text/plain': 'y' } ); const spy = sinon.stub( editor.model, 'insertContent' ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation() {}, preventDefault() {} } ); expect( spy.calledOnce ).to.be.true; expect( stringifyModel( spy.args[ 0 ][ 0 ] ) ).to.equal( 'x
', 'text/plain': 'y' } ); const spy = sinon.stub( editor.model, 'insertContent' ); editor.isReadOnly = true; viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation() {}, preventDefault() {} } ); sinon.assert.notCalled( spy ); } ); it( 'stops `clipboardInput` event on highest priority when editor is read-only', () => { const dataTransferMock = createDataTransfer( { 'text/html': 'x
', 'text/plain': 'y' } ); const spy = sinon.spy(); viewDocument.on( 'clipboardInput', spy, { priority: 'high' } ); editor.isReadOnly = true; viewDocument.fire( 'clipboardInput', { dataTransfer: dataTransferMock } ); sinon.assert.notCalled( spy ); editor.isReadOnly = false; viewDocument.fire( 'clipboardInput', { dataTransfer: dataTransferMock } ); sinon.assert.calledOnce( spy ); } ); it( 'does not insert content if the whole content was invalid', () => { // Whole content is invalid. Even though there is "view" content, the "model" content would be empty. // Do not insert content in this case. const dataTransferMock = createDataTransfer( { 'text/html': 'y
', 'text/plain': 'z' } ); const spy = sinon.stub( editor.model, 'insertContent' ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation() {}, preventDefault() {} } ); expect( spy.calledOnce ).to.be.true; expect( stringifyModel( spy.args[ 0 ][ 0 ] ) ).to.equal( 'xx
', 'text/plain': 'y' } ); const inputTransformationSpy = sinon.spy(); clipboardPlugin.on( 'inputTransformation', inputTransformationSpy ); viewDocument.fire( 'clipboardInput', { dataTransfer: dataTransferMock, content: new ViewDocumentFragment() } ); sinon.assert.calledOnce( scrollSpy ); sinon.assert.callOrder( inputTransformationSpy, scrollSpy ); } ); it( 'uses low priority observer for the clipboardInput event', () => { const dataTransferMock = createDataTransfer( { 'text/html': 'x' } ); const spy = sinon.stub( editor.model, 'insertContent' ); viewDocument.on( 'clipboardInput', evt => { evt.stop(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation() {}, preventDefault() {} } ); expect( spy.callCount ).to.equal( 0 ); } ); // https://github.com/ckeditor/ckeditor5-upload/issues/92 // https://github.com/ckeditor/ckeditor5/issues/6464 it( 'should stop propagation of the original event if CKEditor handled the input', () => { const dataTransferMock = createDataTransfer( { 'text/html': 'x' } ); const spy = sinon.spy(); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation: spy, preventDefault() {} } ); expect( spy.callCount ).to.equal( 1 ); } ); // https://github.com/ckeditor/ckeditor5-upload/issues/92 // https://github.com/ckeditor/ckeditor5/issues/6464 it( 'should stop propagation of the original event if inputTransformation listener called stop (for file drop)', () => { const fileMock = { type: 'application/zip', size: 1024 }; const dataTransferMock = new DataTransfer( { files: [ fileMock ], types: [ 'Files' ], getData: () => {} } ); const spy = sinon.spy(); viewDocument.fire( 'drop', { dataTransfer: dataTransferMock, stopPropagation: spy, preventDefault() {} } ); expect( spy.callCount ).to.equal( 0 ); clipboardPlugin.on( 'inputTransformation', evt => { evt.stop(); } ); viewDocument.fire( 'paste', { dataTransfer: dataTransferMock, stopPropagation: spy, preventDefault() {} } ); expect( spy.callCount ).to.equal( 1 ); } ); function createDataTransfer( data ) { return { getData( type ) { return data[ type ]; } }; } } ); describe( 'clipboard copy/cut pipeline', () => { it( 'fires clipboardOutput for copy with the selected content and correct method', done => { const dataTransferMock = createDataTransfer(); const preventDefaultSpy = sinon.spy(); setModelData( editor.model, 'bc
de
' ); done(); } ); viewDocument.fire( 'copy', { dataTransfer: dataTransferMock, preventDefault: preventDefaultSpy } ); } ); it( 'fires clipboardOutput for cut with the selected content and correct method', done => { const dataTransferMock = createDataTransfer(); const preventDefaultSpy = sinon.spy(); setModelData( editor.model, '' + '' + 'foo
' + 'bar
' + '
foobar
' + '
' +
'' + '' + 'foo
' + 'bar
' + '
foobar
' + '
' + // Weird attributes ordering behavior + no closing "/>".
'
' +
'