8
0
Просмотр исходного кода

Tests and implementation of SimpleUploadAdapter.

Kamil Piechaczek 6 лет назад
Родитель
Сommit
39fbf25906

+ 200 - 0
packages/ckeditor5-upload/src/simpleuploadadapter.js

@@ -0,0 +1,200 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module upload/simpleuploadadapter
+ */
+
+/* globals XMLHttpRequest, FormData */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FileRepository from './filerepository';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+/**
+ * @extends module:core/plugin~Plugin
+ */
+export default class SimpleUploadAdapter extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ FileRepository ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'SimpleUploadAdapter';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const options = this.editor.config.get( 'simpleUpload' );
+
+		if ( !options ) {
+			return;
+		}
+
+		if ( !options.uploadUrl ) {
+			/**
+			 * Configuration passed to the editor is missing a URL specified as `simpleUpload.uploadUrl` which is required because,
+			 * under the specified URL, all images will be uploaded.
+			 *
+			 * @error simple-upload-adapter
+			 */
+			log.warn( 'simple-upload-adapter-missing-uploadUrl: Missing "uploadUrl" in the "simpleUpload" editor configuration.' );
+
+			return;
+		}
+
+		this.editor.plugins.get( FileRepository ).createUploadAdapter = loader => {
+			return new Adapter( loader, options );
+		};
+	}
+}
+
+/**
+ * Upload adapter.
+ *
+ * @private
+ * @implements module:upload/filerepository~UploadAdapter
+ */
+class Adapter {
+	/**
+	 * Creates a new adapter instance.
+	 *
+	 * @param {module:upload/filerepository~FileLoader} loader
+	 * @param {Object} options
+	 * @param {String} options.uploadUrl A URL where the image will be sent.
+	 */
+	constructor( loader, options ) {
+		/**
+		 * FileLoader instance to use during the upload.
+		 *
+		 * @member {module:upload/filerepository~FileLoader} #loader
+		 */
+		this.loader = loader;
+
+		/**
+		 * Test.
+		 *
+		 * @member {Object} #options
+		 */
+		this.options = options;
+	}
+
+	/**
+	 * Starts the upload process.
+	 *
+	 * @see module:upload/filerepository~UploadAdapter#upload
+	 * @returns {Promise}
+	 */
+	upload() {
+		return this.loader.file
+			.then( file => new Promise( ( resolve, reject ) => {
+				this._initRequest();
+				this._initListeners( resolve, reject, file );
+				this._sendRequest( file );
+			} ) );
+	}
+
+	/**
+	 * Aborts the upload process.
+	 *
+	 * @see module:upload/filerepository~UploadAdapter#abort
+	 * @returns {Promise}
+	 */
+	abort() {
+		if ( this.xhr ) {
+			this.xhr.abort();
+		}
+	}
+
+	/**
+	 * Initializes the XMLHttpRequest object using the URL passed to the constructor.
+	 *
+	 * @private
+	 */
+	_initRequest() {
+		const xhr = this.xhr = new XMLHttpRequest();
+
+		xhr.open( 'POST', this.options.uploadUrl, true );
+		xhr.responseType = 'json';
+	}
+
+	/**
+	 * Initializes XMLHttpRequest listeners
+	 *
+	 * @private
+	 * @param {Function} resolve Callback function to be called when the request is successful.
+	 * @param {Function} reject Callback function to be called when the request cannot be completed.
+	 * @param {File} file Native File object.
+	 */
+	_initListeners( resolve, reject, file ) {
+		const xhr = this.xhr;
+		const loader = this.loader;
+		const genericErrorText = `Couldn't upload file: ${ file.name }.`;
+
+		xhr.addEventListener( 'error', () => reject( genericErrorText ) );
+		xhr.addEventListener( 'abort', () => reject() );
+		xhr.addEventListener( 'load', () => {
+			const response = xhr.response;
+
+			// This example assumes the XHR server's "response" object will come with
+			// an "error" which has its own "message" that can be passed to reject()
+			// in the upload promise.
+			//
+			// Your integration may handle upload errors in a different way so make sure
+			// it is done properly. The reject() function must be called when the upload fails.
+			if ( !response || response.error ) {
+				return reject( response && response.error && response.error.message ? response.error.message : genericErrorText );
+			}
+
+			// If the upload is successful, resolve the upload promise with an object containing
+			// at least the "default" URL, pointing to the image on the server.
+			// This URL will be used to display the image in the content. Learn more in the
+			// UploadAdapter#upload documentation.
+			resolve( {
+				default: response.url
+			} );
+		} );
+
+		// Upload progress when it is supported.
+		/* istanbul ignore else */
+		if ( xhr.upload ) {
+			xhr.upload.addEventListener( 'progress', evt => {
+				if ( evt.lengthComputable ) {
+					loader.uploadTotal = evt.total;
+					loader.uploaded = evt.loaded;
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Prepares the data and sends the request.
+	 *
+	 * @private
+	 * @param {File} file File instance to be uploaded.
+	 */
+	_sendRequest( file ) {
+		// Prepare the form data.
+		const data = new FormData();
+
+		data.append( 'upload', file );
+
+		// Important note: This is the right place to implement security mechanisms
+		// like authentication and CSRF protection. For instance, you can use
+		// XMLHttpRequest.setRequestHeader() to set the request headers containing
+		// the CSRF token generated earlier by your application.
+
+		// Send the request.
+		this.xhr.send( data );
+	}
+}

+ 252 - 0
packages/ckeditor5-upload/tests/simpleuploadadapter.js

@@ -0,0 +1,252 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. 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 SimpleUploadAdapter from '../src/simpleuploadadapter';
+import FileRepository from '@ckeditor/ckeditor5-upload/src/filerepository';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import { createNativeFileMock } from '@ckeditor/ckeditor5-upload/tests/_utils/mocks';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'SimpleUploadAdapter', () => {
+	let editor, editorElement, sinonXHR, logStub, fileRepository;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		editorElement = document.createElement( 'div' );
+		document.body.appendChild( editorElement );
+
+		sinonXHR = testUtils.sinon.useFakeServer();
+		logStub = testUtils.sinon.stub( log, 'warn' );
+
+		return ClassicTestEditor
+			.create( editorElement, {
+				plugins: [ SimpleUploadAdapter ],
+				simpleUpload: {
+					uploadUrl: 'http://example.com'
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				fileRepository = editor.plugins.get( FileRepository );
+			} );
+	} );
+
+	afterEach( () => {
+		sinonXHR.restore();
+	} );
+
+	it( 'should require FileRepository plugin', () => {
+		expect( SimpleUploadAdapter.requires ).to.deep.equal( [ FileRepository ] );
+	} );
+
+	it( 'should be named', () => {
+		expect( SimpleUploadAdapter.pluginName ).to.equal( 'SimpleUploadAdapter' );
+	} );
+
+	describe( 'init()', () => {
+		it( 'should set loader', () => {
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ SimpleUploadAdapter ],
+					simpleUpload: {
+						uploadUrl: 'http://example.com'
+					}
+				} )
+				.then( editor => {
+					expect( editor.plugins.get( FileRepository ).createUploadAdapter ).is.a( 'function' );
+
+					return editor.destroy();
+				} );
+		} );
+	} );
+
+	describe( 'UploadAdapter', () => {
+		let adapter, loader;
+
+		beforeEach( () => {
+			const file = createNativeFileMock();
+			file.name = 'image.jpeg';
+
+			loader = fileRepository.createLoader( file );
+
+			adapter = editor.plugins.get( FileRepository ).createUploadAdapter( loader );
+		} );
+
+		it( 'crateAdapter method should be registered and have upload and abort methods', () => {
+			expect( adapter ).to.not.be.undefined;
+			expect( adapter.upload ).to.be.a( 'function' );
+			expect( adapter.abort ).to.be.a( 'function' );
+		} );
+
+		it( 'should not set the FileRepository.createUploadAdapter factory if not configured', () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ SimpleUploadAdapter ],
+				} )
+				.then( editor => {
+					const fileRepository = editor.plugins.get( FileRepository );
+
+					expect( fileRepository ).to.not.have.property( 'createUploadAdapter' );
+
+					editorElement.remove();
+
+					return editor.destroy();
+				} );
+		} );
+
+		it( 'should not set the FileRepository.createUploadAdapter factory if not configured properly', () => {
+			const editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ SimpleUploadAdapter ],
+					simpleUpload: {
+						// Missing "uploadUrl".
+						foo: 'bar'
+					}
+				} )
+				.then( editor => {
+					expect( logStub.callCount ).to.equal( 1 );
+					expect( logStub.firstCall.args[ 0 ] ).to.match( /^simple-upload-adapter-missing-uploadUrl/ );
+
+					const fileRepository = editor.plugins.get( FileRepository );
+
+					expect( fileRepository ).to.not.have.property( 'createUploadAdapter' );
+
+					editorElement.remove();
+
+					return editor.destroy();
+				} );
+		} );
+
+		describe( 'upload', () => {
+			it( 'should return promise', () => {
+				expect( adapter.upload() ).to.be.instanceof( Promise );
+			} );
+
+			it( 'should call url from config', () => {
+				let request;
+				const validResponse = {
+					uploaded: 1
+				};
+
+				adapter.upload();
+
+				return loader.file.then( () => {
+					request = sinonXHR.requests[ 0 ];
+					request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( validResponse ) );
+
+					expect( request.url ).to.equal( 'http://example.com' );
+				} );
+			} );
+
+			it( 'should throw an error on generic request error', () => {
+				const promise = adapter.upload()
+					.then( () => {
+						throw new Error( 'Promise should throw.' );
+					} )
+					.catch( msg => {
+						expect( msg ).to.equal( 'Couldn\'t upload file: image.jpeg.' );
+					} );
+
+				loader.file.then( () => {
+					const request = sinonXHR.requests[ 0 ];
+					request.error();
+				} );
+
+				return promise;
+			} );
+
+			it( 'should throw an error on error from server', () => {
+				const responseError = {
+					error: {
+						message: 'Foo bar baz.'
+					}
+				};
+
+				const promise = adapter.upload()
+					.then( () => {
+						throw new Error( 'Promise should throw.' );
+					} )
+					.catch( msg => {
+						expect( msg ).to.equal( 'Foo bar baz.' );
+					} );
+
+				loader.file.then( () => {
+					const request = sinonXHR.requests[ 0 ];
+					request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( responseError ) );
+				} );
+
+				return promise;
+			} );
+
+			it( 'should throw a generic error on error from server without message', () => {
+				const responseError = {
+					error: {}
+				};
+
+				const promise = adapter.upload()
+					.then( () => {
+						throw new Error( 'Promise should throw.' );
+					} )
+					.catch( msg => {
+						expect( msg ).to.equal( 'Couldn\'t upload file: image.jpeg.' );
+					} );
+
+				loader.file.then( () => {
+					const request = sinonXHR.requests[ 0 ];
+					request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( responseError ) );
+				} );
+
+				return promise;
+			} );
+
+			it( 'should throw an error on abort', () => {
+				let request;
+
+				const promise = adapter.upload()
+					.then( () => {
+						throw new Error( 'Promise should throw.' );
+					} )
+					.catch( () => {
+						expect( request.aborted ).to.be.true;
+					} );
+
+				loader.file.then( () => {
+					request = sinonXHR.requests[ 0 ];
+					adapter.abort();
+				} );
+
+				return promise;
+			} );
+
+			it( 'abort should not throw before upload', () => {
+				expect( () => {
+					adapter.abort();
+				} ).to.not.throw();
+			} );
+
+			it( 'should update progress', () => {
+				adapter.upload();
+
+				return loader.file.then( () => {
+					const request = sinonXHR.requests[ 0 ];
+					request.uploadProgress( { loaded: 4, total: 10 } );
+
+					expect( loader.uploadTotal ).to.equal( 10 );
+					expect( loader.uploaded ).to.equal( 4 );
+				} );
+			} );
+		} );
+	} );
+} );