diff --git a/src/ParseFile.js b/src/ParseFile.js index 4d475fad3..a9eab3ce6 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -10,6 +10,7 @@ */ /* global File */ import CoreManager from './CoreManager'; +import type { FullOptions } from './RESTController'; type Base64 = { base64: string }; type FileData = Array | Base64 | File; @@ -157,9 +158,14 @@ class ParseFile { /** * Saves the file to the Parse cloud. * @param {Object} options + * * Valid options are: * @return {Promise} Promise that is resolved when the save finishes. */ - save(options?: { useMasterKey?: boolean, success?: any, error?: any }) { + save(options?: FullOptions) { options = options || {}; var controller = CoreManager.getFileController(); if (!this._previousSave) { @@ -236,7 +242,7 @@ class ParseFile { } var DefaultController = { - saveFile: function(name: string, source: FileSource) { + saveFile: function(name: string, source: FileSource, options?: FullOptions) { if (source.format !== 'file') { throw new Error('saveFile can only be used with File-type sources.'); } @@ -254,10 +260,10 @@ var DefaultController = { url += '/'; } url += 'files/' + name; - return CoreManager.getRESTController().ajax('POST', url, source.file, headers).then(res=>res.response) + return CoreManager.getRESTController().ajax('POST', url, source.file, headers, options).then(res=>res.response) }, - saveBase64: function(name: string, source: FileSource, options?: { useMasterKey?: boolean, success?: any, error?: any }) { + saveBase64: function(name: string, source: FileSource, options?: FullOptions) { if (source.format !== 'base64') { throw new Error('saveBase64 can only be used with Base64-type sources.'); } diff --git a/src/RESTController.js b/src/RESTController.js index 3fea05752..a95530f39 100644 --- a/src/RESTController.js +++ b/src/RESTController.js @@ -18,6 +18,7 @@ export type RequestOptions = { installationId?: string; batchSize?: number; include?: any; + progress?: any; }; export type FullOptions = { @@ -26,6 +27,7 @@ export type FullOptions = { useMasterKey?: boolean; sessionToken?: string; installationId?: string; + progress?: any; }; var XHR = null; @@ -42,7 +44,7 @@ if (typeof XDomainRequest !== 'undefined' && useXDomainRequest = true; } -function ajaxIE9(method: string, url: string, data: any) { +function ajaxIE9(method: string, url: string, data: any, options?: FullOptions) { return new Promise((resolve, reject) => { var xdr = new XDomainRequest(); xdr.onload = function() { @@ -66,16 +68,20 @@ function ajaxIE9(method: string, url: string, data: any) { }; reject(fakeResponse); }; - xdr.onprogress = function() { }; + xdr.onprogress = function() { + if(options && typeof options.progress === 'function') { + options.progress(xdr.responseText); + } + }; xdr.open(method, url); xdr.send(data); }); } const RESTController = { - ajax(method: string, url: string, data: any, headers?: any) { + ajax(method: string, url: string, data: any, headers?: any, options?: FullOptions) { if (useXDomainRequest) { - return ajaxIE9(method, url, data, headers); + return ajaxIE9(method, url, data, headers, options); } var res, rej; @@ -141,7 +147,28 @@ const RESTController = { ' (NodeJS ' + process.versions.node + ')'; } + if(options && typeof options.progress === 'function') { + if (xhr.upload) { + xhr.upload.addEventListener('progress', (oEvent) => { + if (oEvent.lengthComputable) { + options.progress(oEvent.loaded / oEvent.total); + } else { + options.progress(null); + } + }); + } else if (xhr.addEventListener) { + xhr.addEventListener('progress', (oEvent) => { + if (oEvent.lengthComputable) { + options.progress(oEvent.loaded / oEvent.total); + } else { + options.progress(null); + } + }); + } + } + xhr.open(method, url, true); + for (var h in headers) { xhr.setRequestHeader(h, headers[h]); } @@ -225,8 +252,7 @@ const RESTController = { } var payloadString = JSON.stringify(payload); - - return RESTController.ajax(method, url, payloadString).then(({ response }) => { + return RESTController.ajax(method, url, payloadString, {}, options).then(({ response }) => { return response; }); }).catch(function(response: { responseText: string }) { diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 009c046a3..1879533f0 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -6,17 +6,20 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ - +/* global File */ jest.autoMockOff(); const ParseFile = require('../ParseFile').default; const CoreManager = require('../CoreManager'); function generateSaveMock(prefix) { - return function(name) { + return function(name, payload, options) { + if (options && typeof options.progress === 'function') { + options.progress(0.5); + } return Promise.resolve({ name: name, - url: prefix + name + url: prefix + name, }); }; } @@ -183,6 +186,22 @@ describe('ParseFile', () => { expect(a.equals(b)).toBe(false); expect(b.equals(a)).toBe(false); }); + + it('reports progress during save when source is a File', () => { + const file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt")); + + const options = { + progress: function(){} + }; + jest.spyOn(options, 'progress'); + + return file.save(options).then(function(f) { + expect(options.progress).toHaveBeenCalledWith(0.5); + expect(f).toBe(file); + expect(f.name()).toBe('progress.txt'); + expect(f.url()).toBe('http://files.parsetfss.com/a/progress.txt'); + }); + }); }); describe('FileController', () => { diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js index 2eb18e3de..c35bc5893 100644 --- a/src/__tests__/RESTController-test.js +++ b/src/__tests__/RESTController-test.js @@ -376,4 +376,31 @@ describe('RESTController', () => { 'Cannot use the Master Key, it has not been provided.' ); }); + + it('reports upload progress of the AJAX request when callback is provided', (done) => { + const xhr = mockXHR([{ status: 200, response: { success: true }}], { + addEventListener: (name, callback) => { + if(name === "progress") { + callback({ + lengthComputable: true, + loaded: 5, + total: 10 + }); + } + } + }); + RESTController._setXHR(xhr); + + const options = { + progress: function(){} + }; + jest.spyOn(options, 'progress'); + + RESTController.ajax('POST', 'files/upload.txt', {}, {}, options).then(({ response, status }) => { + expect(options.progress).toHaveBeenCalledWith(0.5); + expect(response).toEqual({ success: true }); + expect(status).toBe(200); + done(); + }); + }); }); diff --git a/src/__tests__/test_helpers/mockXHR.js b/src/__tests__/test_helpers/mockXHR.js index 87b50f10a..a1446425a 100644 --- a/src/__tests__/test_helpers/mockXHR.js +++ b/src/__tests__/test_helpers/mockXHR.js @@ -14,8 +14,9 @@ * { status: ..., response: ... } * where status is a HTTP status number and result is a JSON object to pass * alongside it. + * `upload` can be provided to mock the XMLHttpRequest.upload property. */ -function mockXHR(results) { +function mockXHR(results, upload) { const XHR = function() { }; let attempts = 0; XHR.prototype = { @@ -27,7 +28,8 @@ function mockXHR(results) { this.readyState = 4; attempts++; this.onreadystatechange(); - } + }, + upload: upload }; return XHR; }