diff options
author | jrw <jrw@chromium.org> | 2015-03-20 12:01:15 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-20 19:02:06 +0000 |
commit | 5b7c9e08d4e50f577eb7e058c21ef2f897cfbef1 (patch) | |
tree | 24866dbed947d33708ad4ae6769d5213d9b238b5 /remoting | |
parent | c29919ce9a98620c21e6e7750a3bb7ed2afebbbe (diff) | |
download | chromium_src-5b7c9e08d4e50f577eb7e058c21ef2f897cfbef1.zip chromium_src-5b7c9e08d4e50f577eb7e058c21ef2f897cfbef1.tar.gz chromium_src-5b7c9e08d4e50f577eb7e058c21ef2f897cfbef1.tar.bz2 |
Updated remoting.xhr API to use promises.
Removed access to the native XHR object used by the API.
This is a larger change than one might expect for two reasons: First,
because the native XHR object only allows the response content to be
retrieved while the onreadystatechange handler is executing, and
second, because the unit test for dns_blackhole_checker.js relied on
synchronous semantics which cannot be duplicated with promises because
when a promise is resolved, its "then" handlers are not called until
the next event cycle.
Review URL: https://codereview.chromium.org/1003433002
Cr-Commit-Position: refs/heads/master@{#321608}
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/webapp/app_remoting/js/app_remoting.js | 21 | ||||
-rw-r--r-- | remoting/webapp/app_remoting/js/feedback_consent.js | 10 | ||||
-rw-r--r-- | remoting/webapp/crd/js/dns_blackhole_checker.js | 23 | ||||
-rw-r--r-- | remoting/webapp/crd/js/dns_blackhole_checker_unittest.js | 157 | ||||
-rw-r--r-- | remoting/webapp/crd/js/host_controller.js | 17 | ||||
-rw-r--r-- | remoting/webapp/crd/js/host_list_api_impl.js | 36 | ||||
-rw-r--r-- | remoting/webapp/crd/js/it2me_connect_flow.js | 33 | ||||
-rw-r--r-- | remoting/webapp/crd/js/oauth2.js | 2 | ||||
-rw-r--r-- | remoting/webapp/crd/js/oauth2_api_impl.js | 76 | ||||
-rw-r--r-- | remoting/webapp/crd/js/session_connector_impl.js | 5 | ||||
-rw-r--r-- | remoting/webapp/crd/js/third_party_token_fetcher.js | 2 | ||||
-rw-r--r-- | remoting/webapp/crd/js/xhr.js | 311 | ||||
-rw-r--r-- | remoting/webapp/crd/js/xhr_unittest.js | 238 | ||||
-rw-r--r-- | remoting/webapp/js_proto/qunit_proto.js | 2 | ||||
-rw-r--r-- | remoting/webapp/js_proto/sinon_proto.js | 33 |
15 files changed, 541 insertions, 425 deletions
diff --git a/remoting/webapp/app_remoting/js/app_remoting.js b/remoting/webapp/app_remoting/js/app_remoting.js index 82983eb..b3fd07c 100644 --- a/remoting/webapp/app_remoting/js/app_remoting.js +++ b/remoting/webapp/app_remoting/js/app_remoting.js @@ -105,11 +105,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) { /** @type {remoting.AppRemoting} */ var that = this; - /** @param {XMLHttpRequest} xhr */ - var parseAppHostResponse = function(xhr) { - if (xhr.status == 200) { + /** @param {!remoting.Xhr.Response} xhrResponse */ + var parseAppHostResponse = function(xhrResponse) { + if (xhrResponse.status == 200) { var response = /** @type {remoting.AppRemoting.AppHostResponse} */ - (base.jsonParseSafe(xhr.responseText)); + (base.jsonParseSafe(xhrResponse.getText())); if (response && response.status && response.status == 'done' && @@ -157,16 +157,16 @@ remoting.AppRemoting.prototype.start = function(connector, token) { // TODO(garykac) Start using remoting.Error.fromHttpStatus once it has // been updated to properly report 'unknown' errors (rather than // reporting them as AUTHENTICATION_FAILED). - if (xhr.status == 0) { + if (xhrResponse.status == 0) { that.handleError(new remoting.Error( remoting.Error.Tag.NETWORK_FAILURE)); - } else if (xhr.status == 401) { + } else if (xhrResponse.status == 401) { that.handleError(new remoting.Error( remoting.Error.Tag.AUTHENTICATION_FAILED)); - } else if (xhr.status == 403) { + } else if (xhrResponse.status == 403) { that.handleError(new remoting.Error( remoting.Error.Tag.APP_NOT_AUTHORIZED)); - } else if (xhr.status == 502 || xhr.status == 503) { + } else if (xhrResponse.status == 502 || xhrResponse.status == 503) { that.handleError(new remoting.Error( remoting.Error.Tag.SERVICE_UNAVAILABLE)); } else { @@ -175,12 +175,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) { } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', url: that.runApplicationUrl(), - onDone: parseAppHostResponse, oauthToken: token - }); + }).start().then(parseAppHostResponse); }; /** diff --git a/remoting/webapp/app_remoting/js/feedback_consent.js b/remoting/webapp/app_remoting/js/feedback_consent.js index f792376..1437d05 100644 --- a/remoting/webapp/app_remoting/js/feedback_consent.js +++ b/remoting/webapp/app_remoting/js/feedback_consent.js @@ -127,21 +127,19 @@ function onToken(token) { '/applications/' + remoting.settings.getAppRemotingApplicationId() + '/hosts/' + hostId + '/reportIssue'; - /** @param {XMLHttpRequest} xhr */ - var onDone = function(xhr) { - if (xhr.status >= 200 && xhr.status < 300) { + var onDone = function(/** !remoting.Xhr.Response */ response) { + if (response.status >= 200 && response.status < 300) { getUserInfo(); } else { showError(); } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', url: uri, - onDone: onDone, jsonContent: body, oauthToken: token - }); + }).start().then(onDone); } else { getUserInfo(); } diff --git a/remoting/webapp/crd/js/dns_blackhole_checker.js b/remoting/webapp/crd/js/dns_blackhole_checker.js index 4be4ab8..95b342c 100644 --- a/remoting/webapp/crd/js/dns_blackhole_checker.js +++ b/remoting/webapp/crd/js/dns_blackhole_checker.js @@ -38,7 +38,7 @@ remoting.DnsBlackholeChecker = function(signalStrategy) { /** @private */ this.blackholeState_ = BlackholeState.PENDING; - /** @private {?XMLHttpRequest} */ + /** @private {?remoting.Xhr} */ this.xhr_ = null; }; @@ -85,11 +85,11 @@ remoting.DnsBlackholeChecker.prototype.connect = function(server, this.signalStrategy_.connect(server, username, authToken); - this.xhr_ = remoting.xhr.start({ + this.xhr_ = new remoting.Xhr({ method: 'GET', - url: remoting.DnsBlackholeChecker.URL_TO_REQUEST_, - onDone: this.onHttpRequestDone_.bind(this) + url: remoting.DnsBlackholeChecker.URL_TO_REQUEST_ }); + this.xhr_.start().then(this.onHttpRequestDone_.bind(this)); }; remoting.DnsBlackholeChecker.prototype.getState = function() { @@ -153,12 +153,12 @@ remoting.DnsBlackholeChecker.prototype.onWrappedSignalStrategyStateChanged_ = }; /** - * @param {XMLHttpRequest} xhr + * @param {!remoting.Xhr.Response} response * @private */ -remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(xhr) { +remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(response) { this.xhr_ = null; - if (xhr.status >= 200 && xhr.status <= 299) { + if (response.status >= 200 && response.status <= 299) { console.log("DNS blackhole check succeeded."); this.blackholeState_ = BlackholeState.OPEN; if (this.signalStrategy_.getState() == @@ -166,14 +166,15 @@ remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(xhr) { this.setState_(remoting.SignalStrategy.State.CONNECTED); } } else { - console.error("DNS blackhole check failed: " + xhr.status + " " + - xhr.statusText + ". Response URL: " + xhr.responseURL + - ". Response Text: " + xhr.responseText); + console.error("DNS blackhole check failed: " + response.status + " " + + response.statusText + ". Response URL: " + + response.url + ". Response Text: " + + response.getText()); this.blackholeState_ = BlackholeState.BLOCKED; base.dispose(this.signalStrategy_); this.setState_(remoting.SignalStrategy.State.FAILED); } -} +}; /** * @param {remoting.SignalStrategy.State} newState diff --git a/remoting/webapp/crd/js/dns_blackhole_checker_unittest.js b/remoting/webapp/crd/js/dns_blackhole_checker_unittest.js index 9b0be49..e451fef 100644 --- a/remoting/webapp/crd/js/dns_blackhole_checker_unittest.js +++ b/remoting/webapp/crd/js/dns_blackhole_checker_unittest.js @@ -23,13 +23,15 @@ var checker = null; /** @type {remoting.MockSignalStrategy} */ var signalStrategy = null; -var fakeXhrs; + +/** @type {sinon.FakeXhr} */ +var fakeXhr = null; QUnit.module('dns_blackhole_checker', { beforeEach: function(assert) { - fakeXhrs = []; sinon.useFakeXMLHttpRequest().onCreate = function(xhr) { - fakeXhrs.push(xhr); + QUnit.equal(fakeXhr, null, 'exactly one XHR is issued'); + fakeXhr = xhr; }; onStateChange = sinon.spy(); @@ -46,9 +48,8 @@ QUnit.module('dns_blackhole_checker', { sinon.assert.calledWith(signalStrategy.connect, 'server', 'username', 'authToken'); - assert.equal(fakeXhrs.length, 1, 'exactly one XHR is issued'); assert.equal( - fakeXhrs[0].url, remoting.DnsBlackholeChecker.URL_TO_REQUEST_, + fakeXhr.url, remoting.DnsBlackholeChecker.URL_TO_REQUEST_, 'the correct URL is requested'); }, afterEach: function() { @@ -59,112 +60,136 @@ QUnit.module('dns_blackhole_checker', { onStateChange = null; onIncomingStanzaCallback = null; checker = null; - }, + fakeXhr = null; + } }); QUnit.test('success', function(assert) { - fakeXhrs[0].respond(200); - sinon.assert.notCalled(onStateChange); - - [ - remoting.SignalStrategy.State.CONNECTING, - remoting.SignalStrategy.State.HANDSHAKE, - remoting.SignalStrategy.State.CONNECTED - ].forEach(function(state) { + function checkState(state) { signalStrategy.setStateForTesting(state); sinon.assert.calledWith(onStateChange, state); assert.equal(checker.getState(), state); + } + + return base.SpyPromise.run(function() { + fakeXhr.respond(200); + }).then(function() { + sinon.assert.notCalled(onStateChange); + checkState(remoting.SignalStrategy.State.CONNECTING); + checkState(remoting.SignalStrategy.State.HANDSHAKE); + checkState(remoting.SignalStrategy.State.CONNECTED); }); - } -); + }); QUnit.test('http response after connected', function(assert) { - [ - remoting.SignalStrategy.State.CONNECTING, - remoting.SignalStrategy.State.HANDSHAKE, - ].forEach(function(state) { + function checkState(state) { signalStrategy.setStateForTesting(state); sinon.assert.calledWith(onStateChange, state); assert.equal(checker.getState(), state); - }); + } + + checkState(remoting.SignalStrategy.State.CONNECTING); + checkState(remoting.SignalStrategy.State.HANDSHAKE); onStateChange.reset(); // Verify that DnsBlackholeChecker stays in HANDSHAKE state even if the // signal strategy has connected. - signalStrategy.setStateForTesting(remoting.SignalStrategy.State.CONNECTED); - sinon.assert.notCalled(onStateChange); + return base.SpyPromise.run(function() { + signalStrategy.setStateForTesting( + remoting.SignalStrategy.State.CONNECTED); + }).then(function() { + sinon.assert.notCalled(onStateChange); assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE); - // Verify that DnsBlackholeChecker goes to CONNECTED state after the - // the HTTP request has succeeded. - fakeXhrs[0].respond(200); - sinon.assert.calledWith(onStateChange, - remoting.SignalStrategy.State.CONNECTED); - } -); + // Verify that DnsBlackholeChecker goes to CONNECTED state after the + // the HTTP request has succeeded. + return base.SpyPromise.run(function() { + fakeXhr.respond(200); + }); + }).then(function() { + sinon.assert.calledWith(onStateChange, + remoting.SignalStrategy.State.CONNECTED); + }); + }); QUnit.test('connect failed', function(assert) { - fakeXhrs[0].respond(200); - sinon.assert.notCalled(onStateChange); - - [ - remoting.SignalStrategy.State.CONNECTING, - remoting.SignalStrategy.State.FAILED - ].forEach(function(state) { + function checkState(state) { signalStrategy.setStateForTesting(state); sinon.assert.calledWith(onStateChange, state); + }; + + return base.SpyPromise.run(function() { + fakeXhr.respond(200); + }).then(function() { + sinon.assert.notCalled(onStateChange); + checkState(remoting.SignalStrategy.State.CONNECTING); + checkState(remoting.SignalStrategy.State.FAILED); }); -} -); + }); QUnit.test('blocked', function(assert) { - fakeXhrs[0].respond(400); - sinon.assert.calledWith(onStateChange, - remoting.SignalStrategy.State.FAILED); + function checkState(state) { assert.equal(checker.getError().getTag(), remoting.Error.Tag.NOT_AUTHORIZED); - onStateChange.reset(); - - [ - remoting.SignalStrategy.State.CONNECTING, - remoting.SignalStrategy.State.HANDSHAKE, - remoting.SignalStrategy.State.CONNECTED - ].forEach(function(state) { + onStateChange.reset(); signalStrategy.setStateForTesting(state); sinon.assert.notCalled(onStateChange); - assert.equal(checker.getState(), remoting.SignalStrategy.State.FAILED); + assert.equal(checker.getState(), + checker.getState(), + remoting.SignalStrategy.State.FAILED, + 'checker state is still FAILED'); + }; + + return base.SpyPromise.run(function() { + fakeXhr.respond(400); + }).then(function() { + sinon.assert.calledWith( + onStateChange, remoting.SignalStrategy.State.FAILED); + assert.equal( + checker.getError().getTag(), + remoting.Error.Tag.NOT_AUTHORIZED, + 'checker error is NOT_AUTHORIZED'); + checkState(remoting.SignalStrategy.State.CONNECTING); + checkState(remoting.SignalStrategy.State.HANDSHAKE); + checkState(remoting.SignalStrategy.State.FAILED); }); - } -); + }); QUnit.test('blocked after connected', function(assert) { - [ - remoting.SignalStrategy.State.CONNECTING, - remoting.SignalStrategy.State.HANDSHAKE, - ].forEach(function(state) { + function checkState(state) { signalStrategy.setStateForTesting(state); sinon.assert.calledWith(onStateChange, state); assert.equal(checker.getState(), state); - }); + }; + + checkState(remoting.SignalStrategy.State.CONNECTING); + checkState(remoting.SignalStrategy.State.HANDSHAKE); onStateChange.reset(); - // Verify that DnsBlackholeChecker stays in HANDSHAKE state even if the - // signal strategy has connected. - signalStrategy.setStateForTesting(remoting.SignalStrategy.State.CONNECTED); - sinon.assert.notCalled(onStateChange); + // Verify that DnsBlackholeChecker stays in HANDSHAKE state even + // if the signal strategy has connected. + return base.SpyPromise.run(function() { + signalStrategy.setStateForTesting( + remoting.SignalStrategy.State.CONNECTED); + }).then(function() { + sinon.assert.notCalled(onStateChange); assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE); - // Verify that DnsBlackholeChecker goes to FAILED state after it gets the - // blocked HTTP response. - fakeXhrs[0].respond(400); - sinon.assert.calledWith(onStateChange, - remoting.SignalStrategy.State.FAILED); + // Verify that DnsBlackholeChecker goes to FAILED state after it + // gets the blocked HTTP response. + return base.SpyPromise.run(function() { + fakeXhr.respond(400); + }); + }).then(function() { + sinon.assert.calledWith(onStateChange, + remoting.SignalStrategy.State.FAILED); assert.ok(checker.getError().hasTag(remoting.Error.Tag.NOT_AUTHORIZED)); + }); } ); diff --git a/remoting/webapp/crd/js/host_controller.js b/remoting/webapp/crd/js/host_controller.js index 61529f2..162cb2a 100644 --- a/remoting/webapp/crd/js/host_controller.js +++ b/remoting/webapp/crd/js/host_controller.js @@ -234,14 +234,14 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, * @param {string} hostName * @param {string} publicKey * @param {string} privateKey - * @param {XMLHttpRequest} xhr + * @param {!remoting.Xhr.Response} response */ function onRegistered( - hostName, publicKey, privateKey, xhr) { - var success = (xhr.status == 200); + hostName, publicKey, privateKey, response) { + var success = (response.status == 200); if (success) { - var result = base.jsonParseSafe(xhr.responseText); + var result = base.jsonParseSafe(response.getText()); if ('data' in result && 'authorizationCode' in result['data']) { that.hostDaemonFacade_.getCredentialsFromAuthCode( result['data']['authorizationCode'], @@ -260,8 +260,8 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, }); } } else { - console.log('Failed to register the host. Status: ' + xhr.status + - ' response: ' + xhr.responseText); + console.log('Failed to register the host. Status: ' + response.status + + ' response: ' + response.getText()); onError(new remoting.Error(remoting.Error.Tag.REGISTRATION_FAILED)); } } @@ -281,16 +281,15 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, publicKey: publicKey } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts', urlParams: { hostClientId: hostClientId }, - onDone: onRegistered.bind(null, hostName, publicKey, privateKey), jsonContent: newHostDetails, oauthToken: oauthToken - }); + }).start().then(onRegistered.bind(null, hostName, publicKey, privateKey)); } /** diff --git a/remoting/webapp/crd/js/host_list_api_impl.js b/remoting/webapp/crd/js/host_list_api_impl.js index a4dbe08..52f9377 100644 --- a/remoting/webapp/crd/js/host_list_api_impl.js +++ b/remoting/webapp/crd/js/host_list_api_impl.js @@ -28,17 +28,16 @@ remoting.HostListApiImpl = function() { * @param {function(!remoting.Error):void} onError */ remoting.HostListApiImpl.prototype.get = function(onDone, onError) { - /** @type {function(XMLHttpRequest):void} */ + /** @type {function(!remoting.Xhr.Response):void} */ var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone, onError); /** @param {string} token */ var onToken = function(token) { - remoting.xhr.start({ + new remoting.Xhr({ method: 'GET', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts', - onDone: parseHostListResponse, oauthToken: token - }); + }).start().then(parseHostListResponse); }; remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); }; @@ -63,13 +62,12 @@ remoting.HostListApiImpl.prototype.put = 'publicKey': hostPublicKey } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'PUT', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId, - onDone: remoting.xhr.defaultResponse(onDone, onError), jsonContent: newHostDetails, oauthToken: token - }); + }).start().then(remoting.Xhr.defaultResponse(onDone, onError)); }; remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); }; @@ -84,13 +82,12 @@ remoting.HostListApiImpl.prototype.put = remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) { /** @param {string} token */ var onToken = function(token) { - remoting.xhr.start({ + new remoting.Xhr({ method: 'DELETE', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId, - onDone: remoting.xhr.defaultResponse(onDone, onError, - [remoting.Error.Tag.NOT_FOUND]), oauthToken: token - }); + }).start().then(remoting.Xhr.defaultResponse( + onDone, onError, [remoting.Error.Tag.NOT_FOUND])); }; remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); }; @@ -102,19 +99,19 @@ remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) { * * @param {function(Array<remoting.Host>):void} onDone * @param {function(!remoting.Error):void} onError - * @param {XMLHttpRequest} xhr + * @param {!remoting.Xhr.Response} response * @private */ remoting.HostListApiImpl.prototype.parseHostListResponse_ = - function(onDone, onError, xhr) { - if (xhr.status == 200) { - var response = /** @type {{data: {items: Array}}} */ - (base.jsonParseSafe(xhr.responseText)); - if (!response || !response.data) { + function(onDone, onError, response) { + if (response.status == 200) { + var obj = /** @type {{data: {items: Array}}} */ + (base.jsonParseSafe(response.getText())); + if (!obj || !obj.data) { console.error('Invalid "hosts" response from server.'); onError(remoting.Error.unexpected()); } else { - var items = response.data.items || []; + var items = obj.data.items || []; var hosts = items.map( function(/** Object */ item) { var host = new remoting.Host(); @@ -134,7 +131,7 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ = onDone(hosts); } } else { - onError(remoting.Error.fromHttpStatus(xhr.status)); + onError(remoting.Error.fromHttpStatus(response.status)); } }; @@ -142,4 +139,3 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ = remoting.hostListApi = new remoting.HostListApiImpl(); })(); - diff --git a/remoting/webapp/crd/js/it2me_connect_flow.js b/remoting/webapp/crd/js/it2me_connect_flow.js index 20f42c2..df85817 100644 --- a/remoting/webapp/crd/js/it2me_connect_flow.js +++ b/remoting/webapp/crd/js/it2me_connect_flow.js @@ -54,8 +54,8 @@ remoting.It2MeConnectFlow.prototype.connect_ = function(accessCode) { return remoting.identity.getToken(); }).then(function(/** string */ token) { return that.getHostInfo_(token); - }).then(function(/** XMLHttpRequest */ xhr) { - return that.onHostInfo_(xhr); + }).then(function(/** !remoting.Xhr.Response */ response) { + return that.onHostInfo_(response); }).then(function(/** remoting.Host */ host) { that.sessionConnector_.connect( remoting.DesktopConnectedView.Mode.IT2ME, @@ -88,33 +88,31 @@ remoting.It2MeConnectFlow.prototype.verifyAccessCode_ = function(accessCode) { * Continues an IT2Me connection once an access token has been obtained. * * @param {string} token An OAuth2 access token. - * @return {Promise<XMLHttpRequest>} + * @return {Promise<!remoting.Xhr.Response>} * @private */ remoting.It2MeConnectFlow.prototype.getHostInfo_ = function(token) { var that = this; - return new Promise(function(resolve) { - remoting.xhr.start({ - method: 'GET', - url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + - encodeURIComponent(that.hostId_), - onDone: resolve, - oauthToken: token - }); - }); + return new remoting.Xhr({ + method: 'GET', + url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + + encodeURIComponent(that.hostId_), + oauthToken: token + }).start(); }; /** * Continues an IT2Me connection once the host JID has been looked up. * - * @param {XMLHttpRequest} xhr The server response to the support-hosts query. + * @param {!remoting.Xhr.Response} xhrResponse The server response to the + * support-hosts query. * @return {!Promise<!remoting.Host>} Rejects on error. * @private */ -remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhr) { - if (xhr.status == 200) { +remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhrResponse) { + if (xhrResponse.status == 200) { var response = /** @type {{data: {jabberId: string, publicKey: string}}} */ - (base.jsonParseSafe(xhr.responseText)); + (base.jsonParseSafe(xhrResponse.getText())); if (response && response.data && response.data.jabberId && response.data.publicKey) { var host = new remoting.Host(); @@ -128,11 +126,12 @@ remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhr) { return Promise.reject(remoting.Error.unexpected()); } } else { - return Promise.reject(translateSupportHostsError(xhr.status)); + return Promise.reject(translateSupportHostsError(xhrResponse.status)); } }; /** + * TODO(jrw): Replace with remoting.Error.fromHttpStatus. * @param {number} error An HTTP error code returned by the support-hosts * endpoint. * @return {remoting.Error} The equivalent remoting.Error code. diff --git a/remoting/webapp/crd/js/oauth2.js b/remoting/webapp/crd/js/oauth2.js index d91da5d..37570b8 100644 --- a/remoting/webapp/crd/js/oauth2.js +++ b/remoting/webapp/crd/js/oauth2.js @@ -250,7 +250,7 @@ remoting.OAuth2.prototype.onTokens_ = remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) { var xsrf_token = base.generateXsrfToken(); var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' + - remoting.xhr.urlencodeParamHash({ + remoting.Xhr.urlencodeParamHash({ 'client_id': this.getClientId_(), 'redirect_uri': this.getRedirectUri_(), 'scope': this.SCOPE_, diff --git a/remoting/webapp/crd/js/oauth2_api_impl.js b/remoting/webapp/crd/js/oauth2_api_impl.js index f8ed194..de5d9df 100644 --- a/remoting/webapp/crd/js/oauth2_api_impl.js +++ b/remoting/webapp/crd/js/oauth2_api_impl.js @@ -75,37 +75,36 @@ remoting.OAuth2ApiImpl.prototype.interpretXhrStatus_ = */ remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function( onDone, onError, clientId, clientSecret, refreshToken) { - /** @param {XMLHttpRequest} xhr */ - var onResponse = function(xhr) { - if (xhr.status == 200) { + /** @param {!remoting.Xhr.Response} response */ + var onResponse = function(response) { + if (response.status == 200) { try { // Don't use base.jsonParseSafe here unless you also include base.js, // otherwise this won't work from the OAuth trampoline. // TODO(jamiewalch): Fix this once we're no longer using the trampoline. - var tokens = JSON.parse(xhr.responseText); + var tokens = JSON.parse(response.getText()); onDone(tokens['access_token'], tokens['expires_in']); } catch (/** @type {Error} */ err) { console.error('Invalid "token" response from server:', err); onError(remoting.Error.unexpected()); } } else { - console.error('Failed to refresh token. Status: ' + xhr.status + - ' response: ' + xhr.responseText); - onError(fromHttpStatus(xhr.status)); + console.error('Failed to refresh token. Status: ' + response.status + + ' response: ' + response.getText()); + onError(remoting.Error.fromHttpStatus(response.status)); } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', url: this.getOAuth2TokenEndpoint_(), - onDone: onResponse, formContent: { 'client_id': clientId, 'client_secret': clientSecret, 'refresh_token': refreshToken, 'grant_type': 'refresh_token' } - }); + }).start().then(onResponse); }; /** @@ -124,14 +123,14 @@ remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function( */ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( onDone, onError, clientId, clientSecret, code, redirectUri) { - /** @param {XMLHttpRequest} xhr */ - var onResponse = function(xhr) { - if (xhr.status == 200) { + /** @param {!remoting.Xhr.Response} response */ + var onResponse = function(response) { + if (response.status == 200) { try { // Don't use base.jsonParseSafe here unless you also include base.js, // otherwise this won't work from the OAuth trampoline. // TODO(jamiewalch): Fix this once we're no longer using the trampoline. - var tokens = JSON.parse(xhr.responseText); + var tokens = JSON.parse(response.getText()); onDone(tokens['refresh_token'], tokens['access_token'], tokens['expires_in']); } catch (/** @type {Error} */ err) { @@ -139,16 +138,15 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( onError(remoting.Error.unexpected()); } } else { - console.error('Failed to exchange code for token. Status: ' + xhr.status + - ' response: ' + xhr.responseText); - onError(fromHttpStatus(xhr.status)); + console.error('Failed to exchange code for token. Status: ' + + response.status + ' response: ' + response.getText()); + onError(remoting.Error.fromHttpStatus(response.status)); } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', url: this.getOAuth2TokenEndpoint_(), - onDone: onResponse, formContent: { 'client_id': clientId, 'client_secret': clientSecret, @@ -156,7 +154,7 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( 'code': code, 'grant_type': 'authorization_code' } - }); + }).start().then(onResponse); }; /** @@ -170,28 +168,27 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( * @return {void} Nothing. */ remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) { - /** @param {XMLHttpRequest} xhr */ - var onResponse = function(xhr) { - if (xhr.status == 200) { + /** @param {!remoting.Xhr.Response} response */ + var onResponse = function(response) { + if (response.status == 200) { try { - var result = JSON.parse(xhr.responseText); + var result = JSON.parse(response.getText()); onDone(result['email']); } catch (/** @type {Error} */ err) { console.error('Invalid "userinfo" response from server:', err); onError(remoting.Error.unexpected()); } } else { - console.error('Failed to get email. Status: ' + xhr.status + - ' response: ' + xhr.responseText); - onError(fromHttpStatus(xhr.status)); + console.error('Failed to get email. Status: ' + response.status + + ' response: ' + response.getText()); + onError(remoting.Error.fromHttpStatus(response.status)); } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'GET', url: this.getOAuth2ApiUserInfoEndpoint_(), - onDone: onResponse, oauthToken: token - }); + }).start().then(onResponse); }; /** @@ -206,28 +203,27 @@ remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) { */ remoting.OAuth2ApiImpl.prototype.getUserInfo = function(onDone, onError, token) { - /** @param {XMLHttpRequest} xhr */ - var onResponse = function(xhr) { - if (xhr.status == 200) { + /** @param {!remoting.Xhr.Response} response */ + var onResponse = function(response) { + if (response.status == 200) { try { - var result = JSON.parse(xhr.responseText); + var result = JSON.parse(response.getText()); onDone(result['email'], result['name']); } catch (/** @type {Error} */ err) { console.error('Invalid "userinfo" response from server:', err); onError(remoting.Error.unexpected()); } } else { - console.error('Failed to get user info. Status: ' + xhr.status + - ' response: ' + xhr.responseText); - onError(fromHttpStatus(xhr.status)); + console.error('Failed to get user info. Status: ' + response.status + + ' response: ' + response.getText()); + onError(remoting.Error.fromHttpStatus(response.status)); } }; - remoting.xhr.start({ + new remoting.Xhr({ method: 'GET', url: this.getOAuth2ApiUserInfoEndpoint_(), - onDone: onResponse, oauthToken: token - }); + }).start().then(onResponse); }; /** @returns {!remoting.Error} */ diff --git a/remoting/webapp/crd/js/session_connector_impl.js b/remoting/webapp/crd/js/session_connector_impl.js index 427d6a3..0d9ea5b 100644 --- a/remoting/webapp/crd/js/session_connector_impl.js +++ b/remoting/webapp/crd/js/session_connector_impl.js @@ -75,6 +75,9 @@ remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError, remoting.SessionConnectorImpl.prototype.resetConnection_ = function() { this.closeSession(); + // It's OK to initialize these member variables here because the + // constructor calls this method. + /** @private {remoting.Host} */ this.host_ = null; @@ -87,8 +90,6 @@ remoting.SessionConnectorImpl.prototype.resetConnection_ = function() { /** @private {remoting.ClientSession} */ this.clientSession_ = null; - /** @private {XMLHttpRequest} */ - this.pendingXhr_ = null; /** @private {remoting.CredentialsProvider} */ this.credentialsProvider_ = null; diff --git a/remoting/webapp/crd/js/third_party_token_fetcher.js b/remoting/webapp/crd/js/third_party_token_fetcher.js index b51e1b1..66189e9 100644 --- a/remoting/webapp/crd/js/third_party_token_fetcher.js +++ b/remoting/webapp/crd/js/third_party_token_fetcher.js @@ -132,7 +132,7 @@ remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = * @private */ remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { - return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ + return this.tokenUrl_ + '?' + remoting.Xhr.urlencodeParamHash({ 'redirect_uri': this.redirectUri_, 'scope': this.tokenScope_, 'client_id': this.hostPublicKey_, diff --git a/remoting/webapp/crd/js/xhr.js b/remoting/webapp/crd/js/xhr.js index 0529838..cdefb0aa 100644 --- a/remoting/webapp/crd/js/xhr.js +++ b/remoting/webapp/crd/js/xhr.js @@ -4,7 +4,7 @@ /** * @fileoverview - * Simple utilities for making XHRs more pleasant. + * Utility class for making XHRs more pleasant. */ 'use strict'; @@ -12,26 +12,83 @@ /** @suppress {duplicate} */ var remoting = remoting || {}; -/** Namespace for XHR functions */ -/** @type {Object} */ -remoting.xhr = remoting.xhr || {}; - /** - * Takes an associative array of parameters and urlencodes it. - * - * @param {Object<string,string>} paramHash The parameter key/value pairs. - * @return {string} URLEncoded version of paramHash. + * @constructor + * @param {remoting.Xhr.Params} params */ -remoting.xhr.urlencodeParamHash = function(paramHash) { - var paramArray = []; - for (var key in paramHash) { - paramArray.push(encodeURIComponent(key) + - '=' + encodeURIComponent(paramHash[key])); +remoting.Xhr = function(params) { + /** @private @const {!XMLHttpRequest} */ + this.nativeXhr_ = new XMLHttpRequest(); + this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this); + this.nativeXhr_.withCredentials = params.withCredentials || false; + + /** @private @const */ + this.responseType_ = params.responseType || remoting.Xhr.ResponseType.TEXT; + + // Apply URL parameters. + var url = params.url; + var parameterString = ''; + if (typeof(params.urlParams) === 'string') { + parameterString = params.urlParams; + } else if (typeof(params.urlParams) === 'object') { + parameterString = remoting.Xhr.urlencodeParamHash( + remoting.Xhr.removeNullFields_(params.urlParams)); } - if (paramArray.length > 0) { - return paramArray.join('&'); + if (parameterString) { + base.debug.assert(url.indexOf('?') == -1); + url += '?' + parameterString; } - return ''; + + // Check that the content spec is consistent. + if ((Number(params.textContent !== undefined) + + Number(params.formContent !== undefined) + + Number(params.jsonContent !== undefined)) > 1) { + throw new Error( + 'may only specify one of textContent, formContent, and jsonContent'); + } + + // Prepare the build modified headers. + var headers = remoting.Xhr.removeNullFields_(params.headers); + + // Convert the content fields to a single text content variable. + /** @private {?string} */ + this.content_ = null; + if (params.textContent !== undefined) { + this.content_ = params.textContent; + } else if (params.formContent !== undefined) { + if (!('Content-type' in headers)) { + headers['Content-type'] = 'application/x-www-form-urlencoded'; + } + this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent); + } else if (params.jsonContent !== undefined) { + if (!('Content-type' in headers)) { + headers['Content-type'] = 'application/json; charset=UTF-8'; + } + this.content_ = JSON.stringify(params.jsonContent); + } + + // Apply the oauthToken field. + if (params.oauthToken !== undefined) { + base.debug.assert(!('Authorization' in headers)); + headers['Authorization'] = 'Bearer ' + params.oauthToken; + } + + this.nativeXhr_.open(params.method, url, true); + for (var key in headers) { + this.nativeXhr_.setRequestHeader(key, headers[key]); + } + + /** @private {base.Deferred<!remoting.Xhr.Response>} */ + this.deferred_ = null; +}; + +/** + * @enum {string} + */ +remoting.Xhr.ResponseType = { + TEXT: 'TEXT', // Request a plain text response (default). + JSON: 'JSON', // Request a JSON response. + NONE: 'NONE' // Don't request any response. }; /** @@ -41,8 +98,6 @@ remoting.xhr.urlencodeParamHash = function(paramHash) { * * url: The URL to request. * - * onDone: Function to call when the XHR finishes. - * urlParams: (optional) Parameters to be appended to the URL. * Null-valued parameters are omitted. * @@ -64,20 +119,118 @@ remoting.xhr.urlencodeParamHash = function(paramHash) { * oauthToken: (optional) An OAuth2 token used to construct an * Authentication header. * + * responseType: (optional) Request a response of a specific + * type. Default: TEXT. + * * @typedef {{ * method: string, * url:string, - * onDone:(function(XMLHttpRequest):void), * urlParams:(string|Object<string,?string>|undefined), * textContent:(string|undefined), * formContent:(Object|undefined), * jsonContent:(*|undefined), * headers:(Object<string,?string>|undefined), * withCredentials:(boolean|undefined), - * oauthToken:(string|undefined) + * oauthToken:(string|undefined), + * responseType:(remoting.Xhr.ResponseType|undefined) * }} */ -remoting.XhrParams; +remoting.Xhr.Params; + +/** + * Aborts the HTTP request. Does nothing is the request has finished + * already. + */ +remoting.Xhr.prototype.abort = function() { + this.nativeXhr_.abort(); +}; + +/** + * Starts and HTTP request and gets a promise that is resolved when + * the request completes. + * + * Any error that prevents receiving an HTTP status + * code causes this promise to be rejected. + * + * NOTE: Calling this method more than once will return the same + * promise and not start a new request, despite what the name + * suggests. + * + * @return {!Promise<!remoting.Xhr.Response>} + */ +remoting.Xhr.prototype.start = function() { + if (this.deferred_ == null) { + var xhr = this.nativeXhr_; + xhr.send(this.content_); + this.content_ = null; // for gc + this.deferred_ = new base.Deferred(); + } + return this.deferred_.promise(); +}; + +/** + * @private + */ +remoting.Xhr.prototype.onReadyStateChange_ = function() { + var xhr = this.nativeXhr_; + if (xhr.readyState == 4) { + // See comments at remoting.Xhr.Response. + this.deferred_.resolve(new remoting.Xhr.Response(xhr, this.responseType_)); + } +}; + +/** + * The response-related parts of an XMLHttpRequest. Note that this + * class is not just a facade for XMLHttpRequest; it saves the value + * of the |responseText| field becuase once onReadyStateChange_ + * (above) returns, the value of |responseText| is reset to the empty + * string! This is a documented anti-feature of the XMLHttpRequest + * API. + * + * @constructor + * @param {!XMLHttpRequest} xhr + * @param {remoting.Xhr.ResponseType} type + */ +remoting.Xhr.Response = function(xhr, type) { + /** @private @const */ + this.type_ = type; + + /** + * The HTTP status code. + * @const {number} + */ + this.status = xhr.status; + + /** + * The HTTP status description. + * @const {string} + */ + this.statusText = xhr.statusText; + + /** + * The response URL, if any. + * @const {?string} + */ + this.url = xhr.responseURL; + + /** @private {string} */ + this.text_ = xhr.responseText || ''; +}; + +/** + * @return {string} The text content of the response. + */ +remoting.Xhr.Response.prototype.getText = function() { + return this.text_; +}; + +/** + * @return {*} The parsed JSON content of the response. + */ +remoting.Xhr.Response.prototype.getJson = function() { + base.debug.assert(this.type_ == remoting.Xhr.ResponseType.JSON); + return JSON.parse(this.text_); +}; /** * Returns a copy of the input object with all null or undefined @@ -87,7 +240,7 @@ remoting.XhrParams; * @return {!Object<string,string>} * @private */ -remoting.xhr.removeNullFields_ = function(input) { +remoting.Xhr.removeNullFields_ = function(input) { /** @type {!Object<string,string>} */ var result = {}; if (input) { @@ -102,112 +255,38 @@ remoting.xhr.removeNullFields_ = function(input) { }; /** - * Executes an arbitrary HTTP method asynchronously. + * Takes an associative array of parameters and urlencodes it. * - * @param {remoting.XhrParams} params - * @return {XMLHttpRequest} The XMLHttpRequest object. + * @param {Object<string,string>} paramHash The parameter key/value pairs. + * @return {string} URLEncoded version of paramHash. */ -remoting.xhr.start = function(params) { - // Extract fields that can be used more or less as-is. - var method = params.method; - var url = params.url; - var onDone = params.onDone; - var headers = remoting.xhr.removeNullFields_(params.headers); - var withCredentials = params.withCredentials || false; - - // Apply URL parameters. - var parameterString = ''; - if (typeof(params.urlParams) === 'string') { - parameterString = params.urlParams; - } else if (typeof(params.urlParams) === 'object') { - parameterString = remoting.xhr.urlencodeParamHash( - remoting.xhr.removeNullFields_(params.urlParams)); - } - if (parameterString) { - base.debug.assert(url.indexOf('?') == -1); - url += '?' + parameterString; - } - - // Check that the content spec is consistent. - if ((Number(params.textContent !== undefined) + - Number(params.formContent !== undefined) + - Number(params.jsonContent !== undefined)) > 1) { - throw new Error( - 'may only specify one of textContent, formContent, and jsonContent'); - } - - // Convert the content fields to a single text content variable. - /** @type {?string} */ - var content = null; - if (params.textContent !== undefined) { - content = params.textContent; - } else if (params.formContent !== undefined) { - if (!('Content-type' in headers)) { - headers['Content-type'] = 'application/x-www-form-urlencoded'; - } - content = remoting.xhr.urlencodeParamHash(params.formContent); - } else if (params.jsonContent !== undefined) { - if (!('Content-type' in headers)) { - headers['Content-type'] = 'application/json; charset=UTF-8'; - } - content = JSON.stringify(params.jsonContent); - } - - // Apply the oauthToken field. - if (params.oauthToken !== undefined) { - base.debug.assert(!('Authorization' in headers)); - headers['Authorization'] = 'Bearer ' + params.oauthToken; +remoting.Xhr.urlencodeParamHash = function(paramHash) { + var paramArray = []; + for (var key in paramHash) { + paramArray.push(encodeURIComponent(key) + + '=' + encodeURIComponent(paramHash[key])); } - - return remoting.xhr.startInternal_( - method, url, onDone, content, headers, withCredentials); -}; - -/** - * Executes an arbitrary HTTP method asynchronously. - * - * @param {string} method - * @param {string} url - * @param {function(XMLHttpRequest):void} onDone - * @param {?string} content - * @param {!Object<string,string>} headers - * @param {boolean} withCredentials - * @return {XMLHttpRequest} The XMLHttpRequest object. - * @private - */ -remoting.xhr.startInternal_ = function( - method, url, onDone, content, headers, withCredentials) { - /** @type {XMLHttpRequest} */ - var xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) { - return; - } - onDone(xhr); - }; - - xhr.open(method, url, true); - for (var key in headers) { - xhr.setRequestHeader(key, headers[key]); + if (paramArray.length > 0) { + return paramArray.join('&'); } - xhr.withCredentials = withCredentials; - xhr.send(content); - return xhr; + return ''; }; /** * Generic success/failure response proxy. * + * TODO(jrw): Stop using this and move default error handling directly + * into Xhr class. + * * @param {function():void} onDone * @param {function(!remoting.Error):void} onError * @param {Array<remoting.Error.Tag>=} opt_ignoreErrors - * @return {function(XMLHttpRequest):void} + * @return {function(!remoting.Xhr.Response):void} */ -remoting.xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) { - /** @param {XMLHttpRequest} xhr */ - var result = function(xhr) { - var error = - remoting.Error.fromHttpStatus(/** @type {number} */ (xhr.status)); +remoting.Xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) { + /** @param {!remoting.Xhr.Response} response */ + var result = function(response) { + var error = remoting.Error.fromHttpStatus(response.status); if (error.isNone()) { onDone(); return; diff --git a/remoting/webapp/crd/js/xhr_unittest.js b/remoting/webapp/crd/js/xhr_unittest.js index 7102c2e..b02987e 100644 --- a/remoting/webapp/crd/js/xhr_unittest.js +++ b/remoting/webapp/crd/js/xhr_unittest.js @@ -4,193 +4,185 @@ /** * @fileoverview - * @suppress {checkTypes|checkVars|reportUnknownTypes} */ (function() { 'use strict'; -QUnit.module('xhr'); + +/** @type {sinon.FakeXhr} */ +var fakeXhr; + +QUnit.module('xhr', { + beforeEach: function() { + fakeXhr = null; + sinon.useFakeXMLHttpRequest().onCreate = + function(/** sinon.FakeXhr */ xhr) { + fakeXhr = xhr; + }; + } +}); QUnit.test('urlencodeParamHash', function(assert) { assert.equal( - remoting.xhr.urlencodeParamHash({}), + remoting.Xhr.urlencodeParamHash({}), ''); assert.equal( - remoting.xhr.urlencodeParamHash({'key': 'value'}), + remoting.Xhr.urlencodeParamHash({'key': 'value'}), 'key=value'); assert.equal( - remoting.xhr.urlencodeParamHash({'key /?=&': 'value /?=&'}), + remoting.Xhr.urlencodeParamHash({'key /?=&': 'value /?=&'}), 'key%20%2F%3F%3D%26=value%20%2F%3F%3D%26'); assert.equal( - remoting.xhr.urlencodeParamHash({'k1': 'v1', 'k2': 'v2'}), + remoting.Xhr.urlencodeParamHash({'k1': 'v1', 'k2': 'v2'}), 'k1=v1&k2=v2'); }); QUnit.test('basic GET', function(assert) { - sinon.useFakeXMLHttpRequest(); - var done = assert.async(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'GET', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - } + responseType: remoting.Xhr.ResponseType.TEXT + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'GET'); - assert.equal(request.url, 'http://foo.com'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, null); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.equal(fakeXhr.method, 'GET'); + assert.equal(fakeXhr.url, 'http://foo.com'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, null); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('GET with param string', function(assert) { - var done = assert.async(); - sinon.useFakeXMLHttpRequest(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'GET', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, urlParams: 'the_param_string' + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'GET'); - assert.equal(request.url, 'http://foo.com?the_param_string'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, null); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.equal(fakeXhr.method, 'GET'); + assert.equal(fakeXhr.url, 'http://foo.com?the_param_string'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, null); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('GET with param object', function(assert) { - var done = assert.async(); - sinon.useFakeXMLHttpRequest(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'GET', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, urlParams: {'a': 'b', 'c': 'd'} + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'GET'); - assert.equal(request.url, 'http://foo.com?a=b&c=d'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, null); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.equal(fakeXhr.method, 'GET'); + assert.equal(fakeXhr.url, 'http://foo.com?a=b&c=d'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, null); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('GET with headers', function(assert) { - sinon.useFakeXMLHttpRequest(); - var done = assert.async(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'GET', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, headers: {'Header1': 'headerValue1', 'Header2': 'headerValue2'} + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'GET'); - assert.equal(request.url, 'http://foo.com'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, null); + assert.equal(fakeXhr.method, 'GET'); + assert.equal(fakeXhr.url, 'http://foo.com'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, null); assert.equal( - request.requestHeaders['Header1'], + fakeXhr.requestHeaders['Header1'], 'headerValue1'); assert.equal( - request.requestHeaders['Header2'], + fakeXhr.requestHeaders['Header2'], 'headerValue2'); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('GET with credentials', function(assert) { - sinon.useFakeXMLHttpRequest(); - var done = assert.async(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'GET', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, withCredentials: true + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'GET'); - assert.equal(request.url, 'http://foo.com'); - assert.equal(request.withCredentials, true); - assert.equal(request.requestBody, null); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.equal(fakeXhr.method, 'GET'); + assert.equal(fakeXhr.url, 'http://foo.com'); + assert.equal(fakeXhr.withCredentials, true); + assert.equal(fakeXhr.requestBody, null); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('POST with text content', function(assert) { - sinon.useFakeXMLHttpRequest(); var done = assert.async(); - var request = remoting.xhr.start({ + + var promise = new remoting.Xhr({ method: 'POST', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, textContent: 'the_content_string' + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); + done(); }); - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'http://foo.com'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, 'the_content_string'); - assert.ok(!('Content-type' in request.requestHeaders)); - request.respond(200, {}, 'body'); + assert.equal(fakeXhr.method, 'POST'); + assert.equal(fakeXhr.url, 'http://foo.com'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, 'the_content_string'); + assert.ok(!('Content-type' in fakeXhr.requestHeaders)); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('POST with form content', function(assert) { - sinon.useFakeXMLHttpRequest(); - var done = assert.async(); - var request = remoting.xhr.start({ + var promise = new remoting.Xhr({ method: 'POST', url: 'http://foo.com', - onDone: function(xhr) { - assert.ok(xhr === request); - assert.equal(xhr.status, 200); - assert.equal(xhr.responseText, 'body'); - done(); - }, + responseType: remoting.Xhr.ResponseType.TEXT, formContent: {'a': 'b', 'c': 'd'} + }).start().then(function(response) { + assert.equal(response.status, 200); + assert.equal(response.getText(), 'body'); }); - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'http://foo.com'); - assert.equal(request.withCredentials, false); - assert.equal(request.requestBody, 'a=b&c=d'); + assert.equal(fakeXhr.method, 'POST'); + assert.equal(fakeXhr.url, 'http://foo.com'); + assert.equal(fakeXhr.withCredentials, false); + assert.equal(fakeXhr.requestBody, 'a=b&c=d'); assert.equal( - request.requestHeaders['Content-type'], + fakeXhr.requestHeaders['Content-type'], 'application/x-www-form-urlencoded'); - request.respond(200, {}, 'body'); + fakeXhr.respond(200, {}, 'body'); + return promise; }); QUnit.test('defaultResponse 200', function(assert) { - sinon.useFakeXMLHttpRequest(); var done = assert.async(); var onDone = function() { @@ -203,18 +195,17 @@ QUnit.test('defaultResponse 200', function(assert) { done(); }; - var request = remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', - url: 'http://foo.com', - onDone: remoting.xhr.defaultResponse(onDone, onError) - }); - request.respond(200); + url: 'http://foo.com' + }).start().then(remoting.Xhr.defaultResponse(onDone, onError)); + fakeXhr.respond(200, {}, ''); }); QUnit.test('defaultResponse 404', function(assert) { - sinon.useFakeXMLHttpRequest(); var done = assert.async(); + var onDone = function() { assert.ok(false); done(); @@ -225,12 +216,11 @@ QUnit.test('defaultResponse 404', function(assert) { done(); }; - var request = remoting.xhr.start({ + new remoting.Xhr({ method: 'POST', - url: 'http://foo.com', - onDone: remoting.xhr.defaultResponse(onDone, onError) - }); - request.respond(404); + url: 'http://foo.com' + }).start().then(remoting.Xhr.defaultResponse(onDone, onError)); + fakeXhr.respond(404, {}, ''); }); })(); diff --git a/remoting/webapp/js_proto/qunit_proto.js b/remoting/webapp/js_proto/qunit_proto.js index aed670e..33b30f7 100644 --- a/remoting/webapp/js_proto/qunit_proto.js +++ b/remoting/webapp/js_proto/qunit_proto.js @@ -100,4 +100,4 @@ QUnit.module = function(desc, opt_args) {}; * @param {string} desc * @param {function(QUnit.Assert)} f */ -QUnit.test = function(desc, f) {};
\ No newline at end of file +QUnit.test = function(desc, f) {}; diff --git a/remoting/webapp/js_proto/sinon_proto.js b/remoting/webapp/js_proto/sinon_proto.js index ff7eb96..9e223dc 100644 --- a/remoting/webapp/js_proto/sinon_proto.js +++ b/remoting/webapp/js_proto/sinon_proto.js @@ -126,3 +126,36 @@ sinon.TestStub.prototype.onFirstCall = function() {}; /** @returns {Object} */ sinon.createStubInstance = function (/** * */ constructor) {}; + +/** @return {sinon.FakeXhr} */ +sinon.useFakeXMLHttpRequest = function() {}; + +/** @interface */ +sinon.FakeXhr = function() {}; + +/** @type {string} */ +sinon.FakeXhr.prototype.method; + +/** @type {string} */ +sinon.FakeXhr.prototype.url; + +/** @type {boolean} */ +sinon.FakeXhr.prototype.withCredentials; + +/** @type {?string} */ +sinon.FakeXhr.prototype.requestBody; + +/** @type {!Object<string,string>} */ +sinon.FakeXhr.prototype.requestHeaders; + +/** + * @param {number} status + * @param {!Object<string,string>} headers + * @param {?string} content + */ +sinon.FakeXhr.prototype.respond; + +/** + * @type {?function(!sinon.FakeXhr)} + */ +sinon.FakeXhr.prototype.onCreate;
\ No newline at end of file |