From 8f30a88338906cb130beb460830b59e09875addd Mon Sep 17 00:00:00 2001 From: Aaron Blondeau Date: Sat, 5 Nov 2016 10:34:25 -0600 Subject: [PATCH 1/3] Add support for providing file upload progress. --- src/ParseFile.js | 10 +++++----- src/RESTController.js | 12 +++++++++++- src/__tests__/ParseFile-test.js | 22 ++++++++++++++++++++- src/__tests__/RESTController-test.js | 28 +++++++++++++++++++++++++++ src/__tests__/test_helpers/mockXHR.js | 6 ++++-- 5 files changed, 69 insertions(+), 9 deletions(-) diff --git a/src/ParseFile.js b/src/ParseFile.js index 6242c1595..394cb63ce 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -161,18 +161,18 @@ export default class ParseFile { * @param {Object} options A Backbone-style options object. * @return {Parse.Promise} Promise that is resolved when the save finishes. */ - save(options?: { success?: any, error?: any }) { + save(options?: { success?: any, error?: any, progress?: any }) { options = options || {}; var controller = CoreManager.getFileController(); if (!this._previousSave) { if (this._source.format === 'file') { - this._previousSave = controller.saveFile(this._name, this._source).then((res) => { + this._previousSave = controller.saveFile(this._name, this._source, options.progress).then((res) => { this._name = res.name; this._url = res.url; return this; }); } else { - this._previousSave = controller.saveBase64(this._name, this._source).then((res) => { + this._previousSave = controller.saveBase64(this._name, this._source, options.progress).then((res) => { this._name = res.name; this._url = res.url; return this; @@ -238,7 +238,7 @@ export default class ParseFile { } var DefaultController = { - saveFile: function(name: string, source: FileSource) { + saveFile: function(name: string, source: FileSource, progress?: any) { if (source.format !== 'file') { throw new Error('saveFile can only be used with File-type sources.'); } @@ -253,7 +253,7 @@ var DefaultController = { url += '/'; } url += 'files/' + name; - return CoreManager.getRESTController().ajax('POST', url, source.file, headers); + return CoreManager.getRESTController().ajax('POST', url, source.file, headers, progress); }, saveBase64: function(name: string, source: FileSource) { diff --git a/src/RESTController.js b/src/RESTController.js index 8dc6af587..2602de495 100644 --- a/src/RESTController.js +++ b/src/RESTController.js @@ -73,7 +73,7 @@ function ajaxIE9(method: string, url: string, data: any) { } const RESTController = { - ajax(method: string, url: string, data: any, headers?: any) { + ajax(method: string, url: string, data: any, headers?: any, progress?: any) { if (useXDomainRequest) { return ajaxIE9(method, url, data, headers); } @@ -134,6 +134,16 @@ const RESTController = { } xhr.open(method, url, true); + + // Report progress to a callback when one is provided + if(typeof progress === "function" && xhr.upload) { + xhr.upload.addEventListener("progress", function(oEvent) { + if (oEvent.lengthComputable) { + progress(oEvent.loaded / oEvent.total); + } + }); + } + for (var h in headers) { xhr.setRequestHeader(h, headers[h]); } diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 92bc2c790..cceca1004 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -14,7 +14,11 @@ var ParsePromise = require('../ParsePromise').default; var CoreManager = require('../CoreManager'); function generateSaveMock(prefix) { - return function(name, payload) { + return function(name, payload, progress) { + // When save is called with a progress callback, call it with 0.5 + if (typeof progress === "function") { + progress(0.5); + } return ParsePromise.as({ name: name, url: prefix + name @@ -160,6 +164,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', () => { + var file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt")); + + var options = { + progress: function(){} + }; + 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 20965e0fb..07bf7484d 100644 --- a/src/__tests__/RESTController-test.js +++ b/src/__tests__/RESTController-test.js @@ -329,4 +329,32 @@ 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) => { + var xhr = mockXHR([{ status: 200, response: { success: true }}], { + addEventListener: (name, callback) => { + if(name === "progress") { + callback({ + lengthComputable: true, + loaded: 5, + total: 10 + }); + } + } + }); + RESTController._setXHR(xhr); + + var progress = { + callback: function(){} + } + spyOn(progress, 'callback'); + + RESTController.ajax('POST', 'files/upload.txt', {}, {}, progress.callback).then((response, status, xhr) => { + expect(progress.callback).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 59779347d..2003dc898 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) { var XHR = function() { }; var attempts = 0; XHR.prototype = { @@ -27,7 +28,8 @@ function mockXHR(results) { this.readyState = 4; attempts++; this.onreadystatechange(); - } + }, + upload: upload }; return XHR; } From 196ba8e53a851824fdb704c1aa0e47238868c057 Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 30 Nov 2018 16:03:07 -0600 Subject: [PATCH 2/3] clean up --- integration/test/ParseFileTest.js | 34 ++++++++++++++++++++ src/ParseFile.js | 14 ++++++--- src/RESTController.js | 46 +++++++++++++++++++--------- src/__tests__/ParseFile-test.js | 19 ++++++------ src/__tests__/RESTController-test.js | 15 +++++---- 5 files changed, 91 insertions(+), 37 deletions(-) create mode 100644 integration/test/ParseFileTest.js diff --git a/integration/test/ParseFileTest.js b/integration/test/ParseFileTest.js new file mode 100644 index 000000000..542892e73 --- /dev/null +++ b/integration/test/ParseFileTest.js @@ -0,0 +1,34 @@ +'use strict'; + +const assert = require('assert'); +const clear = require('./clear'); +const Parse = require('../../node'); + +const str = 'Hello World!'; +const data = []; +for (let i = 0; i < str.length; i += 1) { + data.push(str.charCodeAt(i)); +} + +describe('Parse File', () => { + beforeEach((done) => { + Parse.initialize('integration', null, 'notsosecret'); + Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse'); + Parse.Storage._clear(); + clear().then(() => { + done(); + }); + }); + + it('can get file progress', async (done) => { + const file = new Parse.File('hello.txt', data, 'text/plain'); + let flag = false; + await file.save({ + progress: () => { + flag = true; + } + }); + assert.equal(flag, false); + done(); + }); +}); diff --git a/src/ParseFile.js b/src/ParseFile.js index 4ee7aad38..2f6ef100f 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:
    + *
  • useMasterKey: In Cloud Code and Node only, causes the Master Key to + * be used for this request. + *
  • progress: Callback for upload progress + *
* @return {Promise} Promise that is resolved when the save finishes. */ - save(options?: { useMasterKey?: boolean, success?: any, error?: any, progress?: 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, progress?: any) { + 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, progress).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 6bd2ebd88..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, progress?: 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,17 +147,28 @@ const RESTController = { ' (NodeJS ' + process.versions.node + ')'; } - xhr.open(method, url, true); - - // Report progress to a callback when one is provided - if(typeof progress === "function" && xhr.upload) { - xhr.upload.addEventListener("progress", function(oEvent) { - if (oEvent.lengthComputable) { - progress(oEvent.loaded / oEvent.total); - } - }); + 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]); } @@ -235,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 273da96a7..1879533f0 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -6,21 +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, payload, progress) { - // When save is called with a progress callback, call it with 0.5 - if (typeof progress === "function") { - progress(0.5); + return function(name, payload, options) { + if (options && typeof options.progress === 'function') { + options.progress(0.5); } - return ParsePromise.as({ + return Promise.resolve({ name: name, - url: prefix + name + url: prefix + name, }); }; } @@ -189,12 +188,12 @@ describe('ParseFile', () => { }); it('reports progress during save when source is a File', () => { - var file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt")); + const file = new ParseFile('progress.txt', new File(["Parse"], "progress.txt")); - var options = { + const options = { progress: function(){} }; - spyOn(options, 'progress'); + jest.spyOn(options, 'progress'); return file.save(options).then(function(f) { expect(options.progress).toHaveBeenCalledWith(0.5); diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js index 374c5559a..c35bc5893 100644 --- a/src/__tests__/RESTController-test.js +++ b/src/__tests__/RESTController-test.js @@ -378,7 +378,7 @@ describe('RESTController', () => { }); it('reports upload progress of the AJAX request when callback is provided', (done) => { - var xhr = mockXHR([{ status: 200, response: { success: true }}], { + const xhr = mockXHR([{ status: 200, response: { success: true }}], { addEventListener: (name, callback) => { if(name === "progress") { callback({ @@ -391,17 +391,16 @@ describe('RESTController', () => { }); RESTController._setXHR(xhr); - var progress = { - callback: function(){} - } - spyOn(progress, 'callback'); + const options = { + progress: function(){} + }; + jest.spyOn(options, 'progress'); - RESTController.ajax('POST', 'files/upload.txt', {}, {}, progress.callback).then((response, status, xhr) => { - expect(progress.callback).toHaveBeenCalledWith(0.5); + 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(); }); }); - }); From 82970fbe1b5f87444fbcd32489cf92659431bf9d Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 18 Dec 2018 11:10:32 -0600 Subject: [PATCH 3/3] improve documentation --- integration/test/ParseFileTest.js | 34 ------------------------------- src/ParseFile.js | 2 +- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 integration/test/ParseFileTest.js diff --git a/integration/test/ParseFileTest.js b/integration/test/ParseFileTest.js deleted file mode 100644 index 542892e73..000000000 --- a/integration/test/ParseFileTest.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -const assert = require('assert'); -const clear = require('./clear'); -const Parse = require('../../node'); - -const str = 'Hello World!'; -const data = []; -for (let i = 0; i < str.length; i += 1) { - data.push(str.charCodeAt(i)); -} - -describe('Parse File', () => { - beforeEach((done) => { - Parse.initialize('integration', null, 'notsosecret'); - Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse'); - Parse.Storage._clear(); - clear().then(() => { - done(); - }); - }); - - it('can get file progress', async (done) => { - const file = new Parse.File('hello.txt', data, 'text/plain'); - let flag = false; - await file.save({ - progress: () => { - flag = true; - } - }); - assert.equal(flag, false); - done(); - }); -}); diff --git a/src/ParseFile.js b/src/ParseFile.js index 2f6ef100f..a9eab3ce6 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -161,7 +161,7 @@ class ParseFile { * * Valid options are:
    *
  • useMasterKey: In Cloud Code and Node only, causes the Master Key to * be used for this request. - *
  • progress: Callback for upload progress + *
  • progress: In Browser only, callback for upload progress *
* @return {Promise} Promise that is resolved when the save finishes. */