Browse Source

Merge pull request #94 from ckeditor/t/ckeditor5/1791

Feature: Implemented the `SimpleUploadAdapter` plugin which enables file uploads in CKEditor 5 using configurable `XMLHttpRequests` to a server. Closes ckeditor/ckeditor5#1791.

BREAKING CHANGE: Moved the `Base64UploadAdapter` plugin file to `ckeditor5-upload/src/adapters/base64uploadadapter.js`. Make sure import paths your project are up–to–date:

```js
import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter'
```
Aleksander Nowodzinski 6 years ago
parent
commit
df4982c9c3

+ 1 - 1
packages/ckeditor5-upload/docs/_snippets/features/base64-upload.js

@@ -6,7 +6,7 @@
 /* globals console, window, document */
 
 import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor';
-import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/base64uploadadapter';
+import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter';
 
 ClassicEditor.builtinPlugins.push( Base64UploadAdapter );
 

+ 2 - 1
packages/ckeditor5-upload/docs/api/upload.md

@@ -16,7 +16,8 @@ See the {@link module:upload/filerepository~FileRepository} plugin documentation
 
 This repository contains the following upload adapters:
 
-* {@link module:upload/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} - plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@glink builds/guides/integration/saving-data editor output}.
+* {@link module:upload/adapters/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} - A plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@link builds/guides/integration/saving-data editor output}.
+* {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter `SimpleUploadAdapter`} - A plugin that uploads images inserted into the editor to your server using the [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) API.
 
 ## Installation
 

+ 8 - 8
packages/ckeditor5-upload/docs/features/base64-upload-adapter.md

@@ -6,7 +6,7 @@ order: 40
 
 # Base64 upload adapter
 
-The {@link module:upload/base64uploadadapter~Base64UploadAdapter Base64 image upload adapter} plugin converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@link builds/guides/integration/saving-data editor output}.
+The {@link module:upload/adapters/base64uploadadapter~Base64UploadAdapter Base64 image upload adapter} plugin converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64) in the {@link builds/guides/integration/saving-data editor output}.
 
 This kind of image upload does not require server processing – images are stored with the rest of the text and displayed by the web browser without additional requests. On the downside, this approach can bloat your database with very long data strings which, in theory, could have a negative impact on the performance.
 
@@ -22,20 +22,20 @@ Use the editor below to see the adapter in action. Open the web browser console
 
 ## Installation
 
-<info-box info>
-	The [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package is available by default in all builds. The installation instructions are for developers interested in building their own, custom WYSIWYG editor.
-</info-box>
-
-To add this feature to your editor, install the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package:
+First, install the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package:
 
 ```bash
 npm install --save @ckeditor/ckeditor5-upload
 ```
 
-Then add the {@link module:upload/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} to your plugin list:
+<info-box info>
+	The [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package is available by default in all {@link builds/guides/overview#available-builds official editor builds}. You do not have to install it, if you are {@link builds/guides/integration/advanced-setup#scenario-1-integrating-existing-builds extending one}.
+</info-box>
+
+Add the {@link module:upload/adapters/base64uploadadapter~Base64UploadAdapter `Base64UploadAdapter`} to your plugin list:
 
 ```js
-import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/base64uploadadapter';
+import Base64UploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/base64uploadadapter';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {

+ 131 - 0
packages/ckeditor5-upload/docs/features/simple-upload-adapter.md

@@ -0,0 +1,131 @@
+---
+category: features-image-upload
+menu-title: Simple upload adapter
+order: 50
+---
+
+# Simple upload adapter
+
+The {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter Simple upload adapter} allows uploading images to your server using the [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) API with a minimal [editor configuration](#configuration).
+
+See the ["Server–side configuration"](#server-side-configuration) section to learn about the requirements your server–side application must meet to support this upload adapter.
+
+<info-box>
+	Check out the comprehensive {@link features/image-upload Image upload overview} to learn about other ways to upload images into CKEditor 5.
+</info-box>
+
+## Installation
+
+First, install the [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package:
+
+ ```bash
+npm install --save @ckeditor/ckeditor5-upload
+```
+
+<info-box info>
+	The [`@ckeditor/ckeditor5-upload`](https://www.npmjs.com/package/@ckeditor/ckeditor5-upload) package is available by default in all {@link builds/guides/overview#available-builds official editor builds}. You do not have to install it, if you are {@link builds/guides/integration/advanced-setup#scenario-1-integrating-existing-builds extending one}.
+</info-box>
+
+Add the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter SimpleUploadAdapter} to your plugin list and [configure](#configuration) the feature. For instance:
+
+ ```js
+import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/adapters/simpleuploadadapter';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ SimpleUploadAdapter, ... ],
+		toolbar: [ ... ],
+		simpleUpload: {
+			// Feature configuration.
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+<info-box info>
+	Read more about {@link builds/guides/integration/installing-plugins installing plugins}.
+</info-box>
+
+## Configuration
+
+The client–side of this feature is configurable using the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig `config.simpleUpload`} object.
+
+ ```js
+import SimpleUploadAdapter from '@ckeditor/ckeditor5-upload/src/simpleuploadadapter';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ SimpleUploadAdapter, ... ],
+		toolbar: [ ... ],
+		simpleUpload: {
+			// The URL the images are uploaded to.
+			uploadUrl: 'http://example.com',
+
+			// Headers sent along with the XMLHttpRequest to the upload server.
+			headers: {
+				'X-CSRF-TOKEN': 'CSFR-Token',
+				Authorization: 'Bearer <JSON Web Token>'
+			}
+		}
+	} )
+	.then( ... )
+	.catch( ... );
+```
+
+## Server-side configuration
+
+To use this upload adapter, you must provide a server–side application that will handle the uploads and communicate with the editor, as described in the following sections.
+
+### Communication protocol
+
+When the image upload process is initiated, the adapter sends a [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) request under {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `config.simpleUpload.uploadUrl`}.
+
+You can sent additional [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) along with the `XMLHttpRequest` to the upload server, e.g. to authenticate the user, using the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `config.simpleUpload.headers`} object.
+
+The [`responseType`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of the request is always `json`. See the ["Successful upload"](#successful-upload) and ["Error handling"](#error-handling) sections to learn more.
+
+### Successful upload
+
+If the upload is successful, the server should return an object containing the `url` property which points out to the uploaded image on the server:
+
+```json
+{
+	"url": "https://example.com/images/foo.jpg"
+}
+```
+
+That image URL is essential because it will be used:
+
+* to display the image during the editing (as seen by the user in the editor),
+* in the editor content {@link builds/guides/integration/saving-data saved to the databse}.
+
+### Error handling
+
+If something went wrong, the server must return an object that containing the `error` property. This will terminate the upload in the editor, e.g. allowing the user to select another image if the previous one was too big or did not meet some other validation criteria.
+
+If the `error` object contains a `message`, it will be passed to the {@link module:ui/notification/notification~Notification#showWarning editor notification system} and displayed to the user. For the convenience of the users, use clear and possibly specific error messages.
+
+```json
+{
+	"error": {
+		"message": "The image upload failed because the image was too big (max 1.5MB)."
+	}
+}
+```
+
+If the `message` property is missing in the `error` object, the {@link module:ui/notification/notification~Notification#showWarning editor notification system} will display the default "Couldn't upload file: `[filename]`." message.
+
+### Upload progress
+
+This upload adapter will notify users about the [file upload progress](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/progress_event) out–of–the–box.
+
+## What's next?
+
+Check out the comprehensive {@link features/image-upload Image upload overview} to learn more about different ways of uploading images in CKEditor 5.
+
+See the {@link features/image Image feature} guide to find out more about handling images in CKEditor 5.
+
+## Contribute
+
+The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-upload.

+ 2 - 2
packages/ckeditor5-upload/src/base64uploadadapter.js

@@ -4,13 +4,13 @@
  */
 
 /**
- * @module upload/base64uploadadapter
+ * @module upload/adapters/base64uploadadapter
  */
 
 /* globals window */
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
-import FileRepository from './filerepository';
+import FileRepository from '../filerepository';
 
 /**
  * A plugin that converts images inserted into the editor into [Base64 strings](https://en.wikipedia.org/wiki/Base64)

+ 286 - 0
packages/ckeditor5-upload/src/adapters/simpleuploadadapter.js

@@ -0,0 +1,286 @@
+/**
+ * @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/adapters/simpleuploadadapter
+ */
+
+/* globals XMLHttpRequest, FormData */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import FileRepository from '../filerepository';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+
+/**
+ * The Simple upload adapter allows uploading images to an application running on your server using
+ * the [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) API with a
+ * minimal {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig editor configuration}.
+ *
+ *		ClassicEditor
+ *			.create( document.querySelector( '#editor' ), {
+ *				simpleUpload: {
+ *					uploadUrl: 'http://example.com',
+ *					headers: {
+ *						...
+ *					}
+ *				}
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See the {@glink features/image-upload/simple-upload-adapter "Simple upload adapter"} guide to learn how to
+ * learn more about the feature (configuration, server–side requirements, etc.).
+ *
+ * Check out the {@glink features/image-upload/image-upload comprehensive "Image upload overview"} to learn about
+ * other ways to upload images into CKEditor 5.
+ *
+ * @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 ) {
+			/**
+			 * The {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `config.simpleUpload.uploadUrl`}
+			 * configuration required by the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter `SimpleUploadAdapter`}
+			 * is missing. Make sure the correct URL is specified for the image upload to work properly.
+			 *
+			 * @error simple-upload-adapter-missing-uploadUrl
+			 */
+			log.warn(
+				'simple-upload-adapter-missing-uploadUrl: Missing the "uploadUrl" property 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 {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} options
+	 */
+	constructor( loader, options ) {
+		/**
+		 * FileLoader instance to use during the upload.
+		 *
+		 * @member {module:upload/filerepository~FileLoader} #loader
+		 */
+		this.loader = loader;
+
+		/**
+		 * The configuration of the adapter.
+		 *
+		 * @member {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} #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 specified as
+	 * {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl `simpleUpload.uploadUrl`} in the editor's
+	 * configuration.
+	 *
+	 * @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;
+
+			if ( !response || response.error ) {
+				return reject( response && response.error && response.error.message ? response.error.message : genericErrorText );
+			}
+
+			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 ) {
+		// Set headers if specified.
+		const headers = this.options.headers || {};
+
+		for ( const headerName of Object.keys( headers ) ) {
+			this.xhr.setRequestHeader( headerName, headers[ headerName ] );
+		}
+
+		// Prepare the form data.
+		const data = new FormData();
+
+		data.append( 'upload', file );
+
+		// Send the request.
+		this.xhr.send( data );
+	}
+}
+
+/**
+ * The configuration of the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter simple upload adapter}.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ *				simpleUpload: {
+ *					// The URL the images are uploaded to.
+ *					uploadUrl: 'http://example.com',
+ *
+ *					// Headers sent along with the XMLHttpRequest to the upload server.
+ *					headers: {
+ *						...
+ *					}
+ *				}
+ *			} );
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * See the {@glink features/image-upload/simple-upload-adapter "Simple upload adapter"} guide to learn more.
+ *
+ * See {@link module:core/editor/editorconfig~EditorConfig all editor configuration options}.
+ *
+ * @interface SimpleUploadConfig
+ */
+
+/**
+ * The configuration of the {@link module:upload/adapters/simpleuploadadapter~SimpleUploadAdapter simple upload adapter}.
+ *
+ * Read more in {@link module:upload/adapters/simpleuploadadapter~SimpleUploadConfig}.
+ *
+ * @member {module:upload/adapters/simpleuploadadapter~SimpleUploadConfig} module:core/editor/editorconfig~EditorConfig#simpleUpload
+ */
+
+/**
+ * The path (URL) to the server (application) which handles the file upload. When specified, enables the automatic
+ * upload of resources (images) inserted into the editor content.
+ *
+ * Learn more about the server application requirements in the
+ * {@glink features/image-upload/simple-upload-adapter#server-side-configuration "Server-side configuration"} section
+ * of the feature guide.
+ *
+ * @member {String} module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#uploadUrl
+ */
+
+/**
+ * An object that defines additional [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) sent with
+ * the request to the server during the upload. This is the right place to implement security mechanisms like
+ * authentication and [CSRF](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) protection.
+ *
+ *		ClassicEditor
+ *			.create( editorElement, {
+ *				simpleUpload: {
+ *					headers: {
+ *						'X-CSRF-TOKEN': 'CSRF-Token',
+ *						Authorization: 'Bearer <JSON Web Token>'
+ *					}
+ *				}
+ *			} );
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * Learn more about the server application requirements in the
+ * {@glink features/image-upload/simple-upload-adapter#server-side-configuration "Server-side configuration"} section
+ * of the feature guide.
+ *
+ * @member {Object.<String, String>} module:upload/adapters/simpleuploadadapter~SimpleUploadConfig#headers
+ */

+ 3 - 3
packages/ckeditor5-upload/tests/base64uploadadapter.js

@@ -5,11 +5,11 @@
 
 /* globals window, setTimeout */
 
-import Base64UploadAdapter from '../src/base64uploadadapter';
-import FileRepository from '../src/filerepository';
+import Base64UploadAdapter from '../../src/adapters/base64uploadadapter';
+import FileRepository from '../../src/filerepository';
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
-import { createNativeFileMock } from './_utils/mocks';
+import { createNativeFileMock } from '../_utils/mocks';
 
 describe( 'Base64UploadAdapter', () => {
 	let div, stubs;

+ 308 - 0
packages/ckeditor5-upload/tests/adapters/simpleuploadadapter.js

@@ -0,0 +1,308 @@
+/**
+ * @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/adapters/simpleuploadadapter';
+import FileRepository from '../../src/filerepository';
+import log from '@ckeditor/ckeditor5-utils/src/log';
+import { createNativeFileMock } from '../_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 the FileRepository plugin', () => {
+		expect( SimpleUploadAdapter.requires ).to.deep.equal( [ FileRepository ] );
+	} );
+
+	it( 'should be named', () => {
+		expect( SimpleUploadAdapter.pluginName ).to.equal( 'SimpleUploadAdapter' );
+	} );
+
+	describe( 'init()', () => {
+		it( 'should activate the adapter', () => {
+			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( 'the 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 a Promise', () => {
+				return loader.file
+					.then( () => {
+						expect( adapter.upload() ).to.be.instanceof( Promise );
+					} );
+			} );
+
+			it( 'should call url from config', () => {
+				let request;
+				const validResponse = {
+					url: 'http://example.com/images/image.jpeg'
+				};
+
+				const uploadPromise = 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' );
+
+						return uploadPromise;
+					} )
+					.then( uploadResponse => {
+						expect( uploadResponse ).to.be.a( 'object' );
+						expect( uploadResponse ).to.have.property( 'default', 'http://example.com/images/image.jpeg' );
+					} );
+			} );
+
+			it( 'should modify headers of uploading request if specified', () => {
+				const editorElement = document.createElement( 'div' );
+				document.body.appendChild( editorElement );
+
+				return ClassicTestEditor
+					.create( editorElement, {
+						plugins: [ SimpleUploadAdapter ],
+						simpleUpload: {
+							uploadUrl: 'http://example.com',
+							headers: {
+								'X-CSRF-TOKEN': 'foo',
+								Authorization: 'Bearer <token>'
+							}
+						}
+					} )
+					.then( editor => {
+						const adapter = editor.plugins.get( FileRepository ).createUploadAdapter( loader );
+						const validResponse = {
+							url: 'http://example.com/images/image.jpeg'
+						};
+
+						const uploadPromise = adapter.upload();
+
+						return loader.file
+							.then( () => {
+								const request = sinonXHR.requests[ 0 ];
+								request.respond( 200, { 'Content-Type': 'application/json' }, JSON.stringify( validResponse ) );
+
+								const requestHeaders = request.requestHeaders;
+
+								expect( requestHeaders ).to.be.a( 'object' );
+								expect( requestHeaders ).to.have.property( 'X-CSRF-TOKEN', 'foo' );
+								expect( requestHeaders ).to.have.property( 'Authorization', 'Bearer <token>' );
+
+								return uploadPromise;
+							} )
+							.then( uploadResponse => {
+								expect( uploadResponse ).to.be.a( 'object' );
+								expect( uploadResponse ).to.have.property( 'default', 'http://example.com/images/image.jpeg' );
+
+								editorElement.remove();
+							} )
+							.then( () => editor.destroy() );
+					} );
+			} );
+
+			it( 'should throw on a 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 on an 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 an error from server without a 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 );
+				} );
+			} );
+		} );
+	} );
+} );