1 // Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview A mock version of remoting.Xhr. Compared to
7 * sinon.useMockXhr, this allows unit tests to be written at a higher
8 * level, and it eliminates a fair amount of boilerplate involved in
9 * making the sinon mocks work with asynchronous calls
10 * (cf. gcd_client_unittest.js vs. gcd_client_with_mock_xhr.js for an
19 * @param {remoting.Xhr.Params} params
21 remoting.MockXhr = function(params) {
22 origXhr['checkParams_'](params);
24 /** @const {remoting.Xhr.Params} */
25 this.params = normalizeParams(params);
27 /** @private {base.Deferred<!remoting.Xhr.Response>} */
28 this.deferred_ = null;
30 /** @type {remoting.Xhr.Response} */
31 this.response_ = null;
33 /** @type {boolean} */
34 this.aborted_ = false;
38 * Converts constuctor parameters to a normalized form that hides
39 * details of how the constructor is called.
40 * @param {remoting.Xhr.Params} params
41 * @return {remoting.Xhr.Params} The argument with all missing fields
42 * filled in with default values.
44 var normalizeParams = function(params) {
46 method: params.method,
48 urlParams: typeof params.urlParams == 'object' ?
49 base.copyWithoutNullFields(params.urlParams) :
51 textContent: params.textContent,
52 jsonContent: params.jsonContent,
53 formContent: params.formContent === undefined ? undefined :
54 base.copyWithoutNullFields(params.formContent),
55 headers: base.copyWithoutNullFields(params.headers),
56 withCredentials: Boolean(params.withCredentials),
57 oauthToken: params.oauthToken,
58 useIdentity: Boolean(params.useIdentity),
59 acceptJson: Boolean(params.acceptJson)
64 * Psuedo-override from remoting.Xhr.
67 remoting.MockXhr.prototype.abort = function() {
72 * Psuedo-override from remoting.Xhr.
73 * @return {!Promise<!remoting.Xhr.Response>}
75 remoting.MockXhr.prototype.start = function() {
76 runMatchingHandler(this);
77 if (!this.deferred_) {
78 this.deferred_ = new base.Deferred();
81 return this.deferred_.promise();
85 * Tells this object to send an empty response to the current or next
87 * @param {number} status The HTTP status code to respond with.
89 remoting.MockXhr.prototype.setEmptyResponse = function(status) {
90 this.setResponse_(new remoting.Xhr.Response(
92 'mock status text from setEmptyResponse',
99 * Tells this object to send a text/plain response to the current or
101 * @param {number} status The HTTP status code to respond with.
102 * @param {string} body The content to respond with.
104 remoting.MockXhr.prototype.setTextResponse = function(status, body) {
105 this.setResponse_(new remoting.Xhr.Response(
107 'mock status text from setTextResponse',
114 * Tells this object to send an application/json response to the
115 * current or next request.
116 * @param {number} status The HTTP status code to respond with.
117 * @param {*} body The content to respond with.
119 remoting.MockXhr.prototype.setJsonResponse = function(status, body) {
120 if (!this.params.acceptJson) {
121 throw new Error('client does not want JSON response');
123 this.setResponse_(new remoting.Xhr.Response(
125 'mock status text from setJsonResponse',
127 JSON.stringify(body),
132 * Sets the response to be used for the current or next request.
133 * @param {!remoting.Xhr.Response} response
136 remoting.MockXhr.prototype.setResponse_ = function(response) {
137 base.debug.assert(this.response_ == null);
138 this.response_ = response;
139 this.maybeRespond_();
143 * Sends a response if one is available.
146 remoting.MockXhr.prototype.maybeRespond_ = function() {
147 if (this.deferred_ && this.response_ && !this.aborted_) {
148 this.deferred_.resolve(this.response_);
153 * The original value of the remoting.Xhr constructor. The JSDoc type
154 * is that of the remoting.Xhr constructor function.
155 * @type {?function(this: remoting.Xhr, remoting.Xhr.Params):void}
160 * @type {!Array<remoting.MockXhr.UrlHandler>}
165 * Registers a handler for a given method and URL. The |urlPattern|
166 * argument may either be a string, which must equal a URL to match
169 * Matching handlers are run when a FakeXhr's |start| method is
170 * called. The handler should generally call one of
171 * |set{Test,Json,Empty}Response|
173 * @param {?string} method The HTTP method to respond to, or null to
174 * respond to any method.
175 * @param {?string|!RegExp} urlPattern The URL or pattern to respond
176 * to, or null to match any URL.
177 * @param {function(!remoting.MockXhr):void} callback The handler
178 * function to call when a matching XHR is started.
179 * @param {boolean=} opt_reuse If true, the response can be used for
182 remoting.MockXhr.setResponseFor = function(
183 method, urlPattern, callback, opt_reuse) {
186 urlPattern: urlPattern,
193 * Installs a response with no content. See |setResponseFor| for
194 * more details on how the parameters work.
196 * @param {?string} method
197 * @param {?string|!RegExp} urlPattern
198 * @param {number=} opt_status The status code to return.
199 * @param {boolean=} opt_reuse
201 remoting.MockXhr.setEmptyResponseFor = function(
202 method, urlPattern, opt_status, opt_reuse) {
203 remoting.MockXhr.setResponseFor(
204 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
205 xhr.setEmptyResponse(opt_status === undefined ? 204 : opt_status);
210 * Installs a 200 response with text content. See |setResponseFor|
211 * for more details on how the parameters work.
213 * @param {?string} method
214 * @param {?string|!RegExp} urlPattern
215 * @param {string} content
216 * @param {boolean=} opt_reuse
218 remoting.MockXhr.setTextResponseFor = function(
219 method, urlPattern, content, opt_reuse) {
220 remoting.MockXhr.setResponseFor(
221 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
222 xhr.setTextResponse(200, content);
227 * Installs a 200 response with JSON content. See |setResponseFor|
228 * for more details on how the parameters work.
230 * @param {?string} method
231 * @param {?string|!RegExp} urlPattern
233 * @param {boolean=} opt_reuse
235 remoting.MockXhr.setJsonResponseFor = function(
236 method, urlPattern, content, opt_reuse) {
237 remoting.MockXhr.setResponseFor(
238 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
239 xhr.setJsonResponse(200, content);
244 * Runs the most first handler for a given method and URL.
245 * @param {!remoting.MockXhr} xhr
247 var runMatchingHandler = function(xhr) {
248 for (var i = 0; i < handlers.length; i++) {
249 var handler = handlers[i];
250 if (handler.method == null || handler.method != xhr.params.method) {
253 if (handler.urlPattern == null) {
254 // Let the handler run.
255 } else if (typeof handler.urlPattern == 'string') {
256 if (xhr.params.url != handler.urlPattern) {
260 var regexp = /** @type {RegExp} */ (handler.urlPattern);
261 if (!regexp.test(xhr.params.url)) {
265 if (!handler.reuse) {
266 handlers.splice(i, 1);
268 handler.callback(xhr);
272 'No handler registered for ' + xhr.params.method +
273 ' to '+ xhr.params.url);
277 * Activates this mock.
279 remoting.MockXhr.activate = function() {
282 'Xhr mocking already active');
283 origXhr = remoting.Xhr;
284 remoting.MockXhr.Response = remoting.Xhr.Response;
285 remoting['Xhr'] = remoting.MockXhr;
289 * Restores the original definiton of |remoting.Xhr|.
291 remoting.MockXhr.restore = function() {
294 'Xhr mocking not active');
295 remoting['Xhr'] = origXhr;
302 // Can't put put typedefs inside a function :-(
306 * urlPattern:(?string|RegExp),
307 * callback:function(!remoting.MockXhr):void,
311 remoting.MockXhr.UrlHandler;