`;
const dataTransfer = mockDataTransfer( clipboardHtml );
const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
} );
// Skip this test on Edge as we mock `File` object there so there is no sense in testing it.
( isEdgeEnv ? it.skip : it )( 'should get file extension from base64 string', done => {
editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', () => {
tryExpect( done, () => {
loader.file.then( file => expect( file.name.split( '.' ).pop() ).to.equal( 'png' ) );
} );
}, { priority: 'low' } );
setModelData( model, '[]foo' );
const clipboardHtml = ``;
const dataTransfer = mockDataTransfer( clipboardHtml );
const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
// Stub `fetch` to return custom blob without type.
sinon.stub( window, 'fetch' ).callsFake( () => {
return new Promise( res => res( {
blob() {
return new Promise( res => res( new Blob( [ 'foo', 'bar' ] ) ) );
}
} ) );
} );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
} );
// Skip this test on Edge as we mock `File` object there so there is no sense in testing it.
( isEdgeEnv ? it.skip : it )( 'should use fallback file extension', done => {
editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', () => {
tryExpect( done, () => {
loader.file.then( file => expect( file.name.split( '.' ).pop() ).to.equal( 'jpeg' ) );
} );
}, { priority: 'low' } );
setModelData( model, '[]foo' );
const clipboardHtml = ``;
const dataTransfer = mockDataTransfer( clipboardHtml );
const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
// Stub `fetch` to return custom blob without type.
sinon.stub( window, 'fetch' ).callsFake( () => {
return new Promise( res => res( {
blob() {
return new Promise( res => res( new Blob( [ 'foo', 'bar' ] ) ) );
}
} ) );
} );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
} );
it( 'should not show notification when file loader failed with no error', done => {
const notification = editor.plugins.get( Notification );
let notificationsCount = 0;
// Prevent popping up alert window.
notification.on( 'show:warning', evt => {
notificationsCount++;
evt.stop();
}, { priority: 'high' } );
// Check data after paste.
editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', () => {
adapterMocks[ 0 ].loader.file.then( () => {
expect.fail( 'Promise should be rejected.' );
} ).catch( () => {
// Deffer so the promise could be resolved.
setTimeout( () => {
expect( notificationsCount ).to.equal( 0 );
done();
} );
} );
}, { priority: 'low' } );
setModelData( model, '[]foo' );
const clipboardHtml = ``;
const dataTransfer = mockDataTransfer( clipboardHtml );
const targetRange = model.createRange( model.createPositionAt( doc.getRoot(), 1 ), model.createPositionAt( doc.getRoot(), 1 ) );
const targetViewRange = editor.editing.mapper.toViewRange( targetRange );
// Stub `fetch` in a way that it always fails.
sinon.stub( window, 'fetch' ).callsFake( () => Promise.reject() );
viewDocument.fire( 'clipboardInput', { dataTransfer, targetRanges: [ targetViewRange ] } );
} );
// Helper for validating clipboard and model data as a result of a paste operation. This function checks both clipboard
// data and model data synchronously (`expectedClipboardData`, `expectedModel`) and then the model data after `loader.file`
// promise is resolved (so model state after successful/failed file fetch attempt).
//
// @param {String} expectedClipboardData Expected clipboard data on `inputTransformation` event.
// @param {String} expectedModel Expected model data on `inputTransformation` event.
// @param {String} expectedModelOnFile Expected model data after all `file.loader` promises are fetched.
// @param {Function} doneFn Callback function to be called when all assertions are done or error occures.
// @param {Boolean} [onSuccess=true] If `expectedModelOnFile` data should be validated
// on `loader.file` a promise successful resolution or promise rejection.
function expectData( expectedClipboardData, expectedModel, expectedModelOnFile, doneFn, onSuccess ) {
// Check data after paste.
editor.plugins.get( 'Clipboard' ).on( 'inputTransformation', ( evt, data ) => {
const clipboardData = injectLoaderId( expectedClipboardData || '', adapterMocks );
const modelData = injectLoaderId( expectedModel, adapterMocks );
const finalModelData = injectLoaderId( expectedModelOnFile, adapterMocks );
if ( clipboardData.length ) {
expect( stringifyView( data.content ) ).to.equal( clipboardData );
}
expect( getModelData( model ) ).to.equal( modelData );
if ( onSuccess !== false ) {
adapterMocks[ 0 ].loader.file.then( () => {
// Deffer so the promise could be resolved.
setTimeout( () => {
expectModel( doneFn, getModelData( model ), finalModelData );
} );
} );
} else {
adapterMocks[ 0 ].loader.file.then( () => {
expect.fail( 'The `loader.file` should be rejected.' );
} ).catch( () => {
// Deffer so the promise could be resolved.
setTimeout( () => {
expectModel( doneFn, getModelData( model ), finalModelData );
} );
} );
}
}, { priority: 'low' } );
}
} );
// Replaces '#loaderX_id' parameter in the given string with a loader id. It is used
// so data string could be created before loader is initialized.
//
// @param {String} data String which have 'loader params' replaced.
// @param {Array.} adapters Adapters list. Each adapter holds a reference to a loader which id is used.
// @returns {String} Data string with 'loader params' replaced.
function injectLoaderId( data, adapters ) {
let newData = data;
if ( newData.includes( '#loader1_id' ) ) {
newData = newData.replace( '#loader1_id', adapters[ 0 ].loader.id );
}
if ( newData.includes( '#loader2_id' ) ) {
newData = newData.replace( '#loader2_id', adapters[ 1 ].loader.id );
}
if ( newData.includes( '#loader3_id' ) ) {
newData = newData.replace( '#loader3_id', adapters[ 2 ].loader.id );
}
return newData;
}
// Asserts actual and expected model data.
//
// @param {function} done Callback function to be called when assertion is done.
// @param {String} actual Actual model data.
// @param {String} expected Expected model data.
function expectModel( done, actual, expected ) {
tryExpect( done, () => {
expect( actual ).to.equal( expected );
} );
}
// Runs given expect function in a try-catch. It should be used only when `expect` is called as a result of a `Promise`
// resolution as all errors may be caught by tested code and needs to be rethrow to be correctly processed by a testing framework.
//
// @param {Function} doneFn Function to run when assertion is done.
// @param {Function} expectFn Function containing all assertions.
function tryExpect( doneFn, expectFn ) {
try {
expectFn();
doneFn();
} catch ( err ) {
doneFn( err );
}
}
// Creates data transfer object with predefined data.
//
// @param {String} content The content returned as `text/html` when queried.
// @returns {module:clipboard/datatransfer~DataTransfer} DataTransfer object.
function mockDataTransfer( content ) {
return new DataTransfer( {
types: [ 'text/html' ],
getData: type => type === 'text/html' ? content : ''
} );
}
// Creates blob url from the given base64 data.
//
// @param {String} base64 The base64 string from which blob url will be generated.
// @returns {String} Blob url.
function base64ToBlobUrl( base64 ) {
return URL.createObjectURL( base64ToBlob( base64.trim() ) );
}
// Transforms base64 data into a blob object.
//
// @param {String} The base64 data to be transformed.
// @returns {Blob} Blob object representing given base64 data.
function base64ToBlob( base64Data ) {
const [ type, data ] = base64Data.split( ',' );
const byteCharacters = atob( data );
const byteArrays = [];
for ( let offset = 0; offset < byteCharacters.length; offset += 512 ) {
const slice = byteCharacters.slice( offset, offset + 512 );
const byteNumbers = new Array( slice.length );
for ( let i = 0; i < slice.length; i++ ) {
byteNumbers[ i ] = slice.charCodeAt( i );
}
byteArrays.push( new Uint8Array( byteNumbers ) );
}
return new Blob( byteArrays, { type } );
}