Commit 5b7c9e08 authored by jrw's avatar jrw Committed by Commit bot

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}
parent c29919ce
......@@ -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);
};
/**
......
......@@ -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();
}
......
......@@ -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
......
......@@ -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);
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);
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) {
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);
// 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);
// 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));
});
}
);
......
......@@ -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));
}
/**
......
......@@ -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();
})();
......@@ -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({
return new remoting.Xhr({
method: 'GET',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
encodeURIComponent(that.hostId_),
onDone: resolve,
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.
......
......@@ -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_,
......
......@@ -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} */
......
......@@ -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;
......
......@@ -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_,
......
......@@ -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.
*/
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;
}
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
* @param {Object<string,string>} paramHash The parameter key/value pairs.
* @return {string} URLEncoded version of paramHash.
*/
remoting.xhr.startInternal_ = function(
method, url, onDone, content, headers, withCredentials) {
/** @type {XMLHttpRequest} */
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState != 4) {
return;
remoting.Xhr.urlencodeParamHash = function(paramHash) {
var paramArray = [];
for (var key in paramHash) {
paramArray.push(encodeURIComponent(key) +
'=' + encodeURIComponent(paramHash[key]));
}
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;
......
......@@ -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, {}, '');
});
})();
......@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment