|
|
@@ -3,7 +3,7 @@
|
|
|
* For licensing, see LICENSE.md.
|
|
|
*/
|
|
|
|
|
|
-/* globals document */
|
|
|
+/* globals document, atob, Blob, URL */
|
|
|
|
|
|
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor';
|
|
|
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
|
|
|
@@ -49,6 +49,9 @@ export function createDataTransfer( data ) {
|
|
|
* {
|
|
|
* browserName: [ fixtureName1, fixtureName2 ]
|
|
|
* }
|
|
|
+ *
|
|
|
+ * @param {Boolean} [config.withBlobsHandling = false] If special flow with generating and asserting blob URLs data
|
|
|
+ * should be used. This param has effect only for integration tests.
|
|
|
*/
|
|
|
export function generateTests( config ) {
|
|
|
if ( [ 'normalization', 'integration' ].indexOf( config.type ) === -1 ) {
|
|
|
@@ -63,6 +66,7 @@ export function generateTests( config ) {
|
|
|
throw new Error( 'No or empty `config.browsers` option provided.' );
|
|
|
}
|
|
|
|
|
|
+ const withBlobsHandling = config.withBlobsHandling || false;
|
|
|
const groups = groupFixturesByBrowsers( config.browsers, config.input, config.skip );
|
|
|
const generateSuiteFn = config.type === 'normalization' ? generateNormalizationTests : generateIntegrationTests;
|
|
|
|
|
|
@@ -74,7 +78,7 @@ export function generateTests( config ) {
|
|
|
const skip = config.skip && config.skip[ group ] ? config.skip[ group ] : [];
|
|
|
|
|
|
if ( groups[ group ] ) {
|
|
|
- generateSuiteFn( group, groups[ group ], editorConfig, skip );
|
|
|
+ generateSuiteFn( group, groups[ group ], editorConfig, skip, withBlobsHandling );
|
|
|
}
|
|
|
}
|
|
|
} );
|
|
|
@@ -164,9 +168,11 @@ function generateNormalizationTests( title, fixtures, editorConfig, skip ) {
|
|
|
// @param {Object} fixtures Object containing fixtures.
|
|
|
// @param {Object} editorConfig Editor config with which test editor will be created.
|
|
|
// @param {Array.<String>} skip Array of fixtures names which tests should be skipped.
|
|
|
-function generateIntegrationTests( title, fixtures, editorConfig, skip ) {
|
|
|
+// @param {Boolean} [withBlobsHandling = false] If special `expectModelWithBlobs()` function should be used to assert model data.
|
|
|
+function generateIntegrationTests( title, fixtures, editorConfig, skip, withBlobsHandling ) {
|
|
|
describe( title, () => {
|
|
|
let element, editor;
|
|
|
+ let data = {};
|
|
|
|
|
|
before( () => {
|
|
|
element = document.createElement( 'div' );
|
|
|
@@ -182,6 +188,22 @@ function generateIntegrationTests( title, fixtures, editorConfig, skip ) {
|
|
|
|
|
|
beforeEach( () => {
|
|
|
setData( editor.model, '<paragraph>[]</paragraph>' );
|
|
|
+
|
|
|
+ const editorModel = editor.model;
|
|
|
+ const insertContent = editorModel.insertContent;
|
|
|
+
|
|
|
+ data = {};
|
|
|
+
|
|
|
+ sinon.stub( editorModel, 'insertContent' ).callsFake( ( content, selection ) => {
|
|
|
+ // Save model string representation now as it may change after `insertContent()` function call
|
|
|
+ // so accessing it later may not work as it may have emptied/changed structure.
|
|
|
+ data.actual = stringifyModel( content );
|
|
|
+ insertContent.call( editorModel, content, selection );
|
|
|
+ } );
|
|
|
+ } );
|
|
|
+
|
|
|
+ afterEach( () => {
|
|
|
+ sinon.restore();
|
|
|
} );
|
|
|
|
|
|
after( () => {
|
|
|
@@ -191,9 +213,19 @@ function generateIntegrationTests( title, fixtures, editorConfig, skip ) {
|
|
|
} );
|
|
|
|
|
|
for ( const name of Object.keys( fixtures.input ) ) {
|
|
|
- ( skip.indexOf( name ) !== -1 ? it.skip : it )( name, () => {
|
|
|
- expectModel( editor, fixtures.input[ name ], fixtures.model[ name ], fixtures.inputRtf && fixtures.inputRtf[ name ] );
|
|
|
- } );
|
|
|
+ if ( !withBlobsHandling ) {
|
|
|
+ ( skip.indexOf( name ) !== -1 ? it.skip : it )( name, () => {
|
|
|
+ data.input = fixtures.input[ name ];
|
|
|
+ data.model = fixtures.model[ name ];
|
|
|
+ expectModel( data, editor, fixtures.inputRtf && fixtures.inputRtf[ name ] );
|
|
|
+ } );
|
|
|
+ } else {
|
|
|
+ ( skip.indexOf( name ) !== -1 ? it.skip : it )( name, done => {
|
|
|
+ data.input = fixtures.input[ name ];
|
|
|
+ data.model = fixtures.model[ name ];
|
|
|
+ expectModelWithBlobs( data, editor, fixtures.inputBlob && fixtures.inputBlob[ name ], done );
|
|
|
+ } );
|
|
|
+ }
|
|
|
}
|
|
|
} );
|
|
|
}
|
|
|
@@ -232,49 +264,95 @@ function expectNormalized( actualView, expectedHtml ) {
|
|
|
const actualNormalized = stringifyView( actualView ).replace( /\u00A0/g, ' ' );
|
|
|
const expectedNormalized = normalizeHtml( inlineData( expectedHtml ) );
|
|
|
|
|
|
- // Extract base64 images so they do not pollute HTML diff and can be compared separately.
|
|
|
- const { data: actual, images: actualImages } = extractBase64Srcs( actualNormalized );
|
|
|
- const { data: expected, images: expectedImages } = extractBase64Srcs( expectedNormalized );
|
|
|
+ compareContentWithBase64Images( actualNormalized, expectedNormalized );
|
|
|
+}
|
|
|
|
|
|
- expect( actual ).to.equal( expected );
|
|
|
+// Compares two models string representations. The input HTML is processed through paste
|
|
|
+// pipeline where it is transformed into model. This function hooks into {@link module:engine/model/model~Model#insertContent}
|
|
|
+// to get the model representation before it is inserted.
|
|
|
+//
|
|
|
+// @param {Object} data
|
|
|
+// @param {String} data.input Input HTML which will be pasted into the editor.
|
|
|
+// @param {String} data.actual Actual model data.
|
|
|
+// @param {String} data.model Expected model data.
|
|
|
+// @param {module:core/editor/editor~Editor} editor Editor instance.
|
|
|
+// @param {String} [inputRtf] Additional RTF input data which will be pasted into the editor as `text/rtf` together with regular input data.
|
|
|
+function expectModel( data, editor, inputRtf = null ) {
|
|
|
+ firePasteEvent( editor, {
|
|
|
+ 'text/html': data.input,
|
|
|
+ 'text/rtf': inputRtf
|
|
|
+ } );
|
|
|
|
|
|
- if ( actualImages.length > 0 && expectedImages.length > 0 ) {
|
|
|
- expect( actualImages.length ).to.equal( expectedImages.length );
|
|
|
- expect( actualImages ).to.deep.equal( expectedImages );
|
|
|
- }
|
|
|
+ compareContentWithBase64Images( data.actual, inlineData( data.model ) );
|
|
|
}
|
|
|
|
|
|
// Compares two models string representations. The input HTML is processed through paste
|
|
|
// pipeline where it is transformed into model. This function hooks into {@link module:engine/model/model~Model#insertContent}
|
|
|
// to get the model representation before it is inserted.
|
|
|
//
|
|
|
+// @param {Object} data
|
|
|
+// @param {String} data.input Input HTML which will be pasted into the editor.
|
|
|
+// @param {String} data.actual Actual model data.
|
|
|
+// @param {String} data.model Expected model data.
|
|
|
// @param {module:core/editor/editor~Editor} editor Editor instance.
|
|
|
-// @param {String} input Input HTML which will be pasted into the editor.
|
|
|
-// @param {String} expected Expected model.
|
|
|
-// @param {String} [inputRtf] Input RTF data which will be pasted into the editor.
|
|
|
-function expectModel( editor, input, expected, inputRtf = null ) {
|
|
|
- const editorModel = editor.model;
|
|
|
- const insertContent = editorModel.insertContent;
|
|
|
-
|
|
|
- let actual = '';
|
|
|
-
|
|
|
- sinon.stub( editorModel, 'insertContent' ).callsFake( ( content, selection ) => {
|
|
|
- // Save model string representation now as it may change after `insertContent()` function call
|
|
|
- // so accessing it later may not work as it may have emptied/changed structure.
|
|
|
- actual = stringifyModel( content );
|
|
|
- insertContent.call( editorModel, content, selection );
|
|
|
- } );
|
|
|
+// @param {String} inputBlobs Additional data which will be used to generate blobs for a test.
|
|
|
+// @param {Function} done The callback function which should be called when test has finished.
|
|
|
+function expectModelWithBlobs( data, editor, inputBlobs, done ) {
|
|
|
+ // If the browser stores images as blob urls, we need to generate blobs based on a provided images base64 data
|
|
|
+ // and then replace original blob urls with the local ones. This allows Paste from Office to correctly extract
|
|
|
+ // data checking if the transformations flow works in real use cases.
|
|
|
+ const base64 = inputBlobs.split( '------' ).map( item => item.replace( /\s/g, '' ) );
|
|
|
+ const blobUrls = createBlobsFromBase64Data( base64 );
|
|
|
+
|
|
|
+ const input = replaceBlobUrls( data.input, blobUrls );
|
|
|
+ const expected = replaceBlobUrls( inlineData( data.model ), blobUrls );
|
|
|
+
|
|
|
+ let counter = 0;
|
|
|
+ const onChange = function() {
|
|
|
+ counter++;
|
|
|
+
|
|
|
+ // Each blob is fetched asynchronously generating separate `change` event. Also first content insertion
|
|
|
+ // (with blob urls still) generates one `change` event. This means the content is fully transformed when
|
|
|
+ // number of change events is number of blob urls in the content + 1.
|
|
|
+ if ( counter > blobUrls.length ) {
|
|
|
+ editor.editing.model.document.off( 'change', onChange );
|
|
|
+ counter = 0;
|
|
|
+
|
|
|
+ const expectedData = replaceBlobUrls( expected, base64 );
|
|
|
+ const actualData = replaceBlobUrls( data.actual, base64 );
|
|
|
+
|
|
|
+ try {
|
|
|
+ compareContentWithBase64Images( actualData, expectedData );
|
|
|
+ done();
|
|
|
+ } catch ( err ) {
|
|
|
+ done( err );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ editor.editing.model.document.on( 'change', onChange );
|
|
|
|
|
|
firePasteEvent( editor, {
|
|
|
- 'text/html': input,
|
|
|
- 'text/rtf': inputRtf
|
|
|
+ 'text/html': input
|
|
|
} );
|
|
|
|
|
|
- sinon.restore();
|
|
|
+ // In some rare cases there might be ` ` in a model data
|
|
|
+ // (see https://github.com/ckeditor/ckeditor5-paste-from-office/issues/27).
|
|
|
+ data.actual = data.actual.replace( /\u00A0/g, ' ' );
|
|
|
+
|
|
|
+ // Check if initial data with blob urls is correct.
|
|
|
+ expect( data.actual ).to.equal( expected );
|
|
|
+}
|
|
|
|
|
|
+// Compares actual and expected content. Before comparison the base64 images data is extracted so data diff is more readable.
|
|
|
+// If there were any images extracted their base64 data is also compared.
|
|
|
+//
|
|
|
+// @param {String} actual Actual content.
|
|
|
+// @param {String} expected Expected content.
|
|
|
+function compareContentWithBase64Images( actual, expected ) {
|
|
|
// Extract base64 images so they do not pollute model diff and can be compared separately.
|
|
|
const { data: actualModel, images: actualImages } = extractBase64Srcs( actual );
|
|
|
- const { data: expectedModel, images: expectedImages } = extractBase64Srcs( inlineData( expected ) );
|
|
|
+ const { data: expectedModel, images: expectedImages } = extractBase64Srcs( expected );
|
|
|
|
|
|
expect( actualModel ).to.equal( expectedModel );
|
|
|
|
|
|
@@ -329,3 +407,63 @@ function firePasteEvent( editor, data ) {
|
|
|
preventDefault() {}
|
|
|
} );
|
|
|
}
|
|
|
+
|
|
|
+// Replaces all blob urls (`blob:`) in the given HTML with given replacements data.
|
|
|
+//
|
|
|
+// @param {String} html The HTML data in which blob urls will be replaced.
|
|
|
+// @param {Array.<String>} replacements Array of string which will replace found blobs in the order of occurrence.
|
|
|
+// @returns {String} The HTML data with all blob urls replaced.
|
|
|
+function replaceBlobUrls( html, replacements ) {
|
|
|
+ const blobRegexp = /src="(blob:[^"]*)"/g;
|
|
|
+ const toReplace = [];
|
|
|
+
|
|
|
+ let match;
|
|
|
+ while ( ( match = blobRegexp.exec( html ) ) !== null ) {
|
|
|
+ toReplace.push( match[ 1 ] );
|
|
|
+ }
|
|
|
+
|
|
|
+ for ( let i = 0; i < toReplace.length; i++ ) {
|
|
|
+ if ( replacements[ i ] ) {
|
|
|
+ html = html.replace( toReplace[ i ], replacements[ i ] );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return html;
|
|
|
+}
|
|
|
+
|
|
|
+// Creates blob urls from the given base64 data.
|
|
|
+//
|
|
|
+// @param {Array.<String>} base64Data Array of base64 strings from which blob urls will be generated.
|
|
|
+// @returns {Array} Array of generated blob urls.
|
|
|
+function createBlobsFromBase64Data( base64Data ) {
|
|
|
+ const blobUrls = [];
|
|
|
+
|
|
|
+ for ( const data of base64Data ) {
|
|
|
+ blobUrls.push( URL.createObjectURL( base64toBlob( data.trim() ) ) );
|
|
|
+ }
|
|
|
+
|
|
|
+ return blobUrls;
|
|
|
+}
|
|
|
+
|
|
|
+// 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 } );
|
|
|
+}
|