瀏覽代碼

Merge branch 'master' into ckeditor5/t/1214

Piotrek Koszuliński 7 年之前
父節點
當前提交
fa4f9e598e

+ 1 - 1
packages/ckeditor5-upload/src/filereader.js

@@ -57,7 +57,7 @@ export default class FileReader {
 	 * Reads the provided file.
 	 *
 	 * @param {File} file Native File object.
-	 * @returns {Promise} Returns a promise that will be resolved with file's content.
+	 * @returns {Promise.<String>} Returns a promise that will be resolved with file's content.
 	 * The promise will be rejected in case of an error or when the reading process is aborted.
 	 */
 	read( file ) {

+ 117 - 37
packages/ckeditor5-upload/src/filerepository.js

@@ -65,6 +65,14 @@ export default class FileRepository extends Plugin {
 		this.loaders.on( 'remove', () => this._updatePendingAction() );
 
 		/**
+		 * Loaders mappings used to retrieve loaders references.
+		 *
+		 * @private
+		 * @member {Map<File|Promise, FileLoader>} #_loadersMap
+		 */
+		this._loadersMap = new Map();
+
+		/**
 		 * Reference to a pending action registered in a {@link module:core/pendingactions~PendingActions} plugin
 		 * while upload is in progress. When there is no upload then value is `null`.
 		 *
@@ -120,21 +128,15 @@ export default class FileRepository extends Plugin {
 	}
 
 	/**
-	 * Returns the loader associated with specified file.
+	 * Returns the loader associated with specified file or promise.
 	 *
 	 * To get loader by id use `fileRepository.loaders.get( id )`.
 	 *
-	 * @param {File} file Native file handle.
+	 * @param {File|Promise.<File>} fileOrPromise Native file or promise handle.
 	 * @returns {module:upload/filerepository~FileLoader|null}
 	 */
-	getLoader( file ) {
-		for ( const loader of this.loaders ) {
-			if ( loader.file == file ) {
-				return loader;
-			}
-		}
-
-		return null;
+	getLoader( fileOrPromise ) {
+		return this._loadersMap.get( fileOrPromise ) || null;
 	}
 
 	/**
@@ -142,10 +144,10 @@ export default class FileRepository extends Plugin {
 	 *
 	 * Requires {@link #createUploadAdapter} factory to be defined.
 	 *
-	 * @param {File} file Native File object.
+	 * @param {File|Promise.<File>} fileOrPromise Native File object or native Promise object which resolves to a File.
 	 * @returns {module:upload/filerepository~FileLoader|null}
 	 */
-	createLoader( file ) {
+	createLoader( fileOrPromise ) {
 		if ( !this.createUploadAdapter ) {
 			/**
 			 * You need to enable an upload adapter in order to be able to upload files.
@@ -174,10 +176,17 @@ export default class FileRepository extends Plugin {
 			return null;
 		}
 
-		const loader = new FileLoader( file );
-		loader._adapter = this.createUploadAdapter( loader );
+		const loader = new FileLoader( Promise.resolve( fileOrPromise ), this.createUploadAdapter );
 
 		this.loaders.add( loader );
+		this._loadersMap.set( fileOrPromise, loader );
+
+		// Store also file => loader mapping so loader can be retrieved by file instance returned upon Promise resolution.
+		if ( fileOrPromise instanceof Promise ) {
+			loader.file.then( file => {
+				this._loadersMap.set( file, loader );
+			} );
+		}
 
 		loader.on( 'change:uploaded', () => {
 			let aggregatedUploaded = 0;
@@ -207,15 +216,21 @@ export default class FileRepository extends Plugin {
 	/**
 	 * Destroys the given loader.
 	 *
-	 * @param {File|module:upload/filerepository~FileLoader} fileOrLoader File associated with that loader or loader
-	 * itself.
+	 * @param {File|Promise|module:upload/filerepository~FileLoader} fileOrPromiseOrLoader File or Promise associated
+	 * with that loader or loader itself.
 	 */
-	destroyLoader( fileOrLoader ) {
-		const loader = fileOrLoader instanceof FileLoader ? fileOrLoader : this.getLoader( fileOrLoader );
+	destroyLoader( fileOrPromiseOrLoader ) {
+		const loader = fileOrPromiseOrLoader instanceof FileLoader ? fileOrPromiseOrLoader : this.getLoader( fileOrPromiseOrLoader );
 
 		loader._destroy();
 
 		this.loaders.remove( loader );
+
+		this._loadersMap.forEach( ( value, key ) => {
+			if ( value === loader ) {
+				this._loadersMap.delete( key );
+			}
+		} );
 	}
 
 	/**
@@ -252,10 +267,10 @@ class FileLoader {
 	/**
 	 * Creates a new instance of `FileLoader`.
 	 *
-	 * @param {File} file A native file instance.
-	 * @param {module:upload/filerepository~UploadAdapter} adapter
+	 * @param {Promise.<File>} filePromise A promise which resolves to a file instance.
+	 * @param {Function} uploadAdapterCreator The function which returns {@link module:upload/filerepository~UploadAdapter} instance.
 	 */
-	constructor( file, adapter ) {
+	constructor( filePromise, uploadAdapterCreator ) {
 		/**
 		 * Unique id of FileLoader instance.
 		 *
@@ -265,12 +280,12 @@ class FileLoader {
 		this.id = uid();
 
 		/**
-		 * A `File` instance associated with this file loader.
+		 * Additional wrapper over the initial file promise passed to this loader.
 		 *
-		 * @readonly
-		 * @member {File}
+		 * @private
+		 * @member {module:upload/filerepository~FilePromiseWrapper}
 		 */
-		this.file = file;
+		this._filePromiseWrapper = this._createFilePromiseWrapper( filePromise );
 
 		/**
 		 * Adapter instance associated with this file loader.
@@ -278,7 +293,7 @@ class FileLoader {
 		 * @private
 		 * @member {module:upload/filerepository~UploadAdapter}
 		 */
-		this._adapter = adapter;
+		this._adapter = uploadAdapterCreator( this );
 
 		/**
 		 * FileReader used by FileLoader.
@@ -355,6 +370,28 @@ class FileLoader {
 	}
 
 	/**
+	 * A `Promise` which resolves to a `File` instance associated with this file loader.
+	 *
+	 * @type {Promise.<File|null>}
+	 */
+	get file() {
+		if ( !this._filePromiseWrapper ) {
+			// Loader was destroyed, return promise which resolves to null.
+			return Promise.resolve( null );
+		} else {
+			// The `this._filePromiseWrapper.promise` is chained and not simply returned to handle a case when:
+			//
+			//		* The `loader.file.then( ... )` is called by external code (returned promise is pending).
+			//		* Then `loader._destroy()` is called (call is synchronous) which destroys the `loader`.
+			//		* Promise returned by the first `loader.file.then( ... )` call is resolved.
+			//
+			// Returning `this._filePromiseWrapper.promise` will still resolve to a `File` instance so there
+			// is an additional check needed in the chain to see if `loader` was destroyed in the meantime.
+			return this._filePromiseWrapper.promise.then( file => this._filePromiseWrapper ? file : null );
+		}
+	}
+
+	/**
 	 * Reads file using {@link module:upload/filereader~FileReader}.
 	 *
 	 * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
@@ -372,7 +409,7 @@ class FileLoader {
 	 *			}
 	 *		} );
 	 *
-	 * @returns {Promise} Returns promise that will be resolved with read data. Promise will be rejected if error
+	 * @returns {Promise.<String>} Returns promise that will be resolved with read data. Promise will be rejected if error
 	 * occurs or if read process is aborted.
 	 */
 	read() {
@@ -382,7 +419,8 @@ class FileLoader {
 
 		this.status = 'reading';
 
-		return this._reader.read( this.file )
+		return this._filePromiseWrapper.promise
+			.then( file => this._reader.read( file ) )
 			.then( data => {
 				this.status = 'idle';
 
@@ -395,7 +433,7 @@ class FileLoader {
 				}
 
 				this.status = 'error';
-				throw this._reader.error;
+				throw this._reader.error ? this._reader.error : err;
 			} );
 	}
 
@@ -416,7 +454,7 @@ class FileLoader {
 	 *			}
 	 *		} );
 	 *
-	 * @returns {Promise} Returns promise that will be resolved with response data. Promise will be rejected if error
+	 * @returns {Promise.<Object>} Returns promise that will be resolved with response data. Promise will be rejected if error
 	 * occurs or if read process is aborted.
 	 */
 	upload() {
@@ -426,7 +464,8 @@ class FileLoader {
 
 		this.status = 'uploading';
 
-		return this._adapter.upload()
+		return this._filePromiseWrapper.promise
+			.then( () => this._adapter.upload() )
 			.then( data => {
 				this.uploadResponse = data;
 				this.status = 'idle';
@@ -450,11 +489,11 @@ class FileLoader {
 		const status = this.status;
 		this.status = 'aborted';
 
-		if ( status == 'reading' ) {
+		if ( !this._filePromiseWrapper.isFulfilled ) {
+			this._filePromiseWrapper.rejecter( 'aborted' );
+		} else if ( status == 'reading' ) {
 			this._reader.abort();
-		}
-
-		if ( status == 'uploading' && this._adapter.abort ) {
+		} else if ( status == 'uploading' && this._adapter.abort ) {
 			this._adapter.abort();
 		}
 
@@ -467,11 +506,41 @@ class FileLoader {
 	 * @private
 	 */
 	_destroy() {
+		this._filePromiseWrapper = undefined;
 		this._reader = undefined;
 		this._adapter = undefined;
 		this.data = undefined;
 		this.uploadResponse = undefined;
-		this.file = undefined;
+	}
+
+	/**
+	 * Wraps a given file promise into another promise giving additional
+	 * control (resolving, rejecting, checking if fulfilled) over it.
+	 *
+	 * @private
+	 * @param filePromise The initial file promise to be wrapped.
+	 * @returns {module:upload/filerepository~FilePromiseWrapper}
+	 */
+	_createFilePromiseWrapper( filePromise ) {
+		const wrapper = {};
+
+		wrapper.promise = new Promise( ( resolve, reject ) => {
+			wrapper.resolver = resolve;
+			wrapper.rejecter = reject;
+			wrapper.isFulfilled = false;
+
+			filePromise
+				.then( file => {
+					wrapper.isFulfilled = true;
+					resolve( file );
+				} )
+				.catch( err => {
+					wrapper.isFulfilled = true;
+					reject( err );
+				} );
+		} );
+
+		return wrapper;
 	}
 }
 
@@ -515,7 +584,7 @@ mix( FileLoader, ObservableMixin );
  * {@link module:upload/filerepository~FileRepository#createUploadAdapter createUploadAdapter method}.
  *
  * @method module:upload/filerepository~UploadAdapter#upload
- * @returns {Promise} Promise that should be resolved when data is uploaded.
+ * @returns {Promise.<Object>} Promise that should be resolved when data is uploaded.
  */
 
 /**
@@ -527,3 +596,14 @@ mix( FileLoader, ObservableMixin );
  *
  * @method module:upload/filerepository~UploadAdapter#abort
  */
+
+/**
+ * Object returned by {@link module:upload/filerepository~FileLoader#_createFilePromiseWrapper} method
+ * to add more control over the initial file promise passed to {@link module:upload/filerepository~FileLoader}.
+ *
+ * @typedef {Object} module:upload/filerepository~FilePromiseWrapper
+ * @property {Promise.<File>} promise Wrapper promise which can be chained for further processing.
+ * @property {Function} resolver Resolves the promise when called.
+ * @property {Function} rejecter Rejects the promise when called.
+ * @property {Boolean} isFulfilled Whether original promise is already fulfilled.
+ */

+ 288 - 25
packages/ckeditor5-upload/tests/filerepository.js

@@ -211,12 +211,29 @@ describe( 'FileRepository', () => {
 			expect( fileRepository.getLoader( file1 ) ).to.be.null;
 		} );
 
-		it( 'should return loader by file instance', () => {
+		it( 'should return loader by file instance (initialized with file)', () => {
 			const file = createNativeFileMock();
 			const loader = fileRepository.createLoader( file );
 
 			expect( fileRepository.getLoader( file ) ).to.equal( loader );
 		} );
+
+		it( 'should return loader by promise instance (initialized with promise)', () => {
+			const promise = Promise.resolve( createNativeFileMock() );
+			const loader = fileRepository.createLoader( promise );
+
+			expect( fileRepository.getLoader( promise ) ).to.equal( loader );
+		} );
+
+		it( 'should return loader by file instance (initialized with promise)', done => {
+			const promise = Promise.resolve( createNativeFileMock() );
+			const loader = fileRepository.createLoader( promise );
+
+			loader.file.then( fileInstance => {
+				expect( fileRepository.getLoader( fileInstance ) ).to.equal( loader );
+				done();
+			} );
+		} );
 	} );
 
 	describe( 'destroyLoader()', () => {
@@ -233,13 +250,55 @@ describe( 'FileRepository', () => {
 
 			sinon.assert.calledOnce( destroySpy );
 			expect( fileRepository.getLoader( file ) ).to.be.null;
+			expect( fileRepository.loaders.length ).to.equal( 0 );
+			expect( Array.from( fileRepository._loadersMap.keys ).length ).to.equal( 0 );
 		} );
 
-		it( 'should destroy loader by provided file', () => {
+		it( 'should destroy loader by provided file (initialized with file)', () => {
 			fileRepository.destroyLoader( file );
 
 			sinon.assert.calledOnce( destroySpy );
 			expect( fileRepository.getLoader( file ) ).to.be.null;
+			expect( fileRepository.loaders.length ).to.equal( 0 );
+			expect( Array.from( fileRepository._loadersMap.keys ).length ).to.equal( 0 );
+		} );
+
+		it( 'should destroy loader by provided promise (initialized with promise)', () => {
+			fileRepository.destroyLoader( loader );
+
+			const promise = new Promise( resolve => resolve( createNativeFileMock() ) );
+			const newLoader = fileRepository.createLoader( promise );
+
+			destroySpy = testUtils.sinon.spy( newLoader, '_destroy' );
+
+			fileRepository.destroyLoader( promise );
+
+			sinon.assert.calledOnce( destroySpy );
+			expect( fileRepository.getLoader( promise ) ).to.be.null;
+			expect( fileRepository.loaders.length ).to.equal( 0 );
+			expect( Array.from( fileRepository._loadersMap.keys() ).length ).to.equal( 0 );
+		} );
+
+		it( 'should destroy loader by provided file (initialized with promise)', () => {
+			fileRepository.destroyLoader( loader );
+
+			const promise = Promise.resolve( createNativeFileMock() );
+			const newLoader = fileRepository.createLoader( promise );
+
+			destroySpy = testUtils.sinon.spy( newLoader, '_destroy' );
+
+			return newLoader.file.then( fileInstance => {
+				expect( fileRepository.loaders.length ).to.equal( 1 );
+				expect( Array.from( fileRepository._loadersMap.keys() ).length ).to.equal( 2 );
+
+				fileRepository.destroyLoader( fileInstance );
+
+				sinon.assert.calledOnce( destroySpy );
+
+				expect( fileRepository.getLoader( fileInstance ) ).to.be.null;
+				expect( fileRepository.loaders.length ).to.equal( 0 );
+				expect( Array.from( fileRepository._loadersMap.keys() ).length ).to.equal( 0 );
+			} );
 		} );
 	} );
 
@@ -262,8 +321,12 @@ describe( 'FileRepository', () => {
 				expect( loader.id ).to.be.a( 'string' );
 			} );
 
-			it( 'should initialize file', () => {
-				expect( loader.file ).to.equal( file );
+			it( 'should initialize filePromiseWrapper', () => {
+				expect( loader._filePromiseWrapper ).to.not.be.null;
+				expect( loader._filePromiseWrapper.promise ).to.be.instanceOf( Promise );
+				expect( loader._filePromiseWrapper.resolver ).to.be.instanceOf( Function );
+				expect( loader._filePromiseWrapper.rejecter ).to.be.instanceOf( Function );
+				expect( loader._filePromiseWrapper.isFulfilled ).to.be.false;
 			} );
 
 			it( 'should initialize adapter', () => {
@@ -333,8 +396,48 @@ describe( 'FileRepository', () => {
 			} );
 		} );
 
+		describe( 'file getter', () => {
+			it( 'should return promise', () => {
+				expect( loader.file ).to.instanceof( Promise );
+			} );
+
+			it( 'should return promise which resolves to a file', () => {
+				return loader.file.then( fileInstance => {
+					expect( fileInstance ).to.equal( file );
+				} );
+			} );
+
+			it( 'should return promise which resolves to null after loader is destroyed (destroy before)', () => {
+				loader._destroy();
+
+				return loader.file.then( fileInstance => {
+					expect( fileInstance ).to.be.null;
+				} );
+			} );
+
+			it( 'should return promise which resolves to null after loader is destroyed (destroy after)', () => {
+				const promise = loader.file.then( fileInstance => {
+					expect( fileInstance ).to.be.null;
+				} );
+
+				loader._destroy();
+
+				return promise;
+			} );
+
+			it( 'should return promise which resolves to null after loader is destroyed (file promise resolved, destroy after)', () => {
+				return loader._filePromiseWrapper.promise.then( () => {
+					loader.file.then( fileInstance => {
+						expect( fileInstance ).to.be.null;
+					} );
+
+					loader._destroy();
+				} );
+			} );
+		} );
+
 		describe( 'read()', () => {
-			it( 'should throw error when status is defferent than idle', () => {
+			it( 'should throw error when status is different than idle', () => {
 				loader.status = 'uploading';
 
 				expect( () => {
@@ -352,7 +455,48 @@ describe( 'FileRepository', () => {
 				expect( loader.status ).to.equal( 'reading' );
 			} );
 
-			it( 'should reject promise when reading is aborted', () => {
+			it( 'should resolve promise when file promise is resolved', () => {
+				let resolveFile = null;
+
+				const filePromise = new Promise( resolve => {
+					resolveFile = resolve;
+				} );
+
+				const loader = fileRepository.createLoader( filePromise );
+
+				const promise = loader.read()
+					.then( data => {
+						expect( data ).to.equal( 'result data' );
+						expect( loader.status ).to.equal( 'idle' );
+					} );
+
+				resolveFile( createNativeFileMock() );
+
+				loader.file.then( () => nativeReaderMock.mockSuccess( 'result data' ) );
+
+				return promise;
+			} );
+
+			it( 'should reject promise when file promise is rejected', () => {
+				let rejectFile = null;
+
+				const filePromise = new Promise( ( resolve, reject ) => {
+					rejectFile = reject;
+				} );
+
+				const loader = fileRepository.createLoader( filePromise );
+
+				const promise = loader.read().catch( e => {
+					expect( e ).to.equal( 'File loading error' );
+					expect( loader.status ).to.equal( 'error' );
+				} );
+
+				rejectFile( 'File loading error' );
+
+				return promise;
+			} );
+
+			it( 'should reject promise when reading is aborted (before file promise is resolved)', () => {
 				const promise = loader.read().catch( e => {
 					expect( e ).to.equal( 'aborted' );
 					expect( loader.status ).to.equal( 'aborted' );
@@ -363,32 +507,43 @@ describe( 'FileRepository', () => {
 				return promise;
 			} );
 
-			it( 'should reject promise on reading error', () => {
+			it( 'should reject promise when reading is aborted (after file promise is resolved)', () => {
+				const promise = loader.read().catch( e => {
+					expect( e ).to.equal( 'aborted' );
+					expect( loader.status ).to.equal( 'aborted' );
+				} );
+
+				loader.file.then( () => loader.abort() );
+
+				return promise;
+			} );
+
+			it( 'should reject promise on reading error (after file promise is resolved)', () => {
 				const promise = loader.read().catch( e => {
 					expect( e ).to.equal( 'reading error' );
 					expect( loader.status ).to.equal( 'error' );
 				} );
 
-				nativeReaderMock.mockError( 'reading error' );
+				loader.file.then( () => nativeReaderMock.mockError( 'reading error' ) );
 
 				return promise;
 			} );
 
-			it( 'should resolve promise on reading complete', () => {
+			it( 'should resolve promise on reading complete (after file promise is resolved)', () => {
 				const promise = loader.read()
 					.then( data => {
 						expect( data ).to.equal( 'result data' );
 						expect( loader.status ).to.equal( 'idle' );
 					} );
 
-				nativeReaderMock.mockSuccess( 'result data' );
+				loader.file.then( () => nativeReaderMock.mockSuccess( 'result data' ) );
 
 				return promise;
 			} );
 		} );
 
 		describe( 'upload()', () => {
-			it( 'should throw error when status is defferent than idle', () => {
+			it( 'should throw error when status is different than idle', () => {
 				loader.status = 'reading';
 
 				expect( () => {
@@ -406,7 +561,48 @@ describe( 'FileRepository', () => {
 				expect( loader.status ).to.equal( 'uploading' );
 			} );
 
-			it( 'should reject promise when uploading is aborted', () => {
+			it( 'should resolve promise when file promise is resolved', () => {
+				let resolveFile = null;
+
+				const filePromise = new Promise( resolve => {
+					resolveFile = resolve;
+				} );
+
+				const loader = fileRepository.createLoader( filePromise );
+
+				const promise = loader.upload()
+					.then( data => {
+						expect( data ).to.equal( 'result data' );
+						expect( loader.status ).to.equal( 'idle' );
+					} );
+
+				resolveFile( createNativeFileMock() );
+
+				loader.file.then( () => adapterMock.mockSuccess( 'result data' ) );
+
+				return promise;
+			} );
+
+			it( 'should reject promise when file promise is rejected', () => {
+				let rejectFile = null;
+
+				const filePromise = new Promise( ( resolve, reject ) => {
+					rejectFile = reject;
+				} );
+
+				const loader = fileRepository.createLoader( filePromise );
+
+				const promise = loader.upload().catch( e => {
+					expect( e ).to.equal( 'File loading error' );
+					expect( loader.status ).to.equal( 'error' );
+				} );
+
+				rejectFile( 'File loading error' );
+
+				return promise;
+			} );
+
+			it( 'should reject promise when uploading is aborted (before file promise is resolved)', () => {
 				const promise = loader.upload().catch( e => {
 					expect( e ).to.equal( 'aborted' );
 					expect( loader.status ).to.equal( 'aborted' );
@@ -417,25 +613,36 @@ describe( 'FileRepository', () => {
 				return promise;
 			} );
 
-			it( 'should reject promise on reading error', () => {
+			it( 'should reject promise when uploading is aborted (after file promise is resolved)', () => {
+				const promise = loader.upload().catch( e => {
+					expect( e ).to.equal( 'aborted' );
+					expect( loader.status ).to.equal( 'aborted' );
+				} );
+
+				loader.file.then( () => loader.abort() );
+
+				return promise;
+			} );
+
+			it( 'should reject promise on reading error (after file promise is resolved)', () => {
 				const promise = loader.upload().catch( e => {
 					expect( e ).to.equal( 'uploading error' );
 					expect( loader.status ).to.equal( 'error' );
 				} );
 
-				adapterMock.mockError( 'uploading error' );
+				loader.file.then( () => adapterMock.mockError( 'uploading error' ) );
 
 				return promise;
 			} );
 
-			it( 'should resolve promise on reading complete', () => {
+			it( 'should resolve promise on reading complete (after file promise is resolved)', () => {
 				const promise = loader.upload()
 					.then( data => {
 						expect( data ).to.equal( 'result data' );
 						expect( loader.status ).to.equal( 'idle' );
 					} );
 
-				adapterMock.mockSuccess( 'result data' );
+				loader.file.then( () => adapterMock.mockSuccess( 'result data' ) );
 
 				return promise;
 			} );
@@ -447,18 +654,74 @@ describe( 'FileRepository', () => {
 						expect( loader.status ).to.equal( 'idle' );
 					} );
 
-				expect( loader.uploaded ).to.equal( 0 );
-				expect( loader.uploadTotal ).to.be.null;
+				loader.file.then( () => {
+					expect( loader.uploaded ).to.equal( 0 );
+					expect( loader.uploadTotal ).to.be.null;
+
+					adapterMock.mockProgress( 1, 10 );
+					expect( loader.uploaded ).to.equal( 1 );
+					expect( loader.uploadTotal ).to.equal( 10 );
+
+					adapterMock.mockProgress( 6, 10 );
+					expect( loader.uploaded ).to.equal( 6 );
+					expect( loader.uploadTotal ).to.equal( 10 );
+
+					adapterMock.mockSuccess( 'result data' );
+				} );
+
+				return promise;
+			} );
+		} );
+
+		describe( 'abort()', () => {
+			let filePromiseRejecterSpy, readerAbortSpy, adapterAbortSpy;
+
+			beforeEach( () => {
+				filePromiseRejecterSpy = testUtils.sinon.spy( loader._filePromiseWrapper, 'rejecter' );
+				readerAbortSpy = testUtils.sinon.spy( loader._reader, 'abort' );
+				adapterAbortSpy = testUtils.sinon.spy( loader._adapter, 'abort' );
+			} );
+
+			it( 'should abort correctly before read/upload is called', () => {
+				loader.abort();
+
+				expect( filePromiseRejecterSpy.callCount ).to.equal( 1 );
+				expect( readerAbortSpy.callCount ).to.equal( 0 );
+				expect( adapterAbortSpy.callCount ).to.equal( 0 );
+			} );
+
+			it( 'should abort correctly after successful read', () => {
+				const promise = loader.read()
+					.then( data => {
+						expect( data ).to.equal( 'result data' );
+						expect( loader.status ).to.equal( 'idle' );
+
+						loader.abort();
 
-				adapterMock.mockProgress( 1, 10 );
-				expect( loader.uploaded ).to.equal( 1 );
-				expect( loader.uploadTotal ).to.equal( 10 );
+						expect( filePromiseRejecterSpy.callCount ).to.equal( 0 );
+						expect( readerAbortSpy.callCount ).to.equal( 0 );
+						expect( adapterAbortSpy.callCount ).to.equal( 0 );
+					} );
 
-				adapterMock.mockProgress( 6, 10 );
-				expect( loader.uploaded ).to.equal( 6 );
-				expect( loader.uploadTotal ).to.equal( 10 );
+				loader.file.then( () => nativeReaderMock.mockSuccess( 'result data' ) );
+
+				return promise;
+			} );
+
+			it( 'should abort correctly after successful upload', () => {
+				const promise = loader.upload()
+					.then( data => {
+						expect( data ).to.equal( 'result data' );
+						expect( loader.status ).to.equal( 'idle' );
+
+						loader.abort();
+
+						expect( filePromiseRejecterSpy.callCount ).to.equal( 0 );
+						expect( readerAbortSpy.callCount ).to.equal( 0 );
+						expect( adapterAbortSpy.callCount ).to.equal( 0 );
+					} );
 
-				adapterMock.mockSuccess( 'result data' );
+				loader.file.then( () => adapterMock.mockSuccess( 'result data' ) );
 
 				return promise;
 			} );