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 console.assert(this.response_ == null,
138 'Duplicate setResponse_() invocation.');
139 this.response_ = response;
140 this.maybeRespond_();
144 * Sends a response if one is available.
147 remoting.MockXhr.prototype.maybeRespond_ = function() {
148 if (this.deferred_ && this.response_ && !this.aborted_) {
149 this.deferred_.resolve(this.response_);
154 * The original value of the remoting.Xhr constructor. The JSDoc type
155 * is that of the remoting.Xhr constructor function.
156 * @type {?function(this: remoting.Xhr, remoting.Xhr.Params):void}
161 * @type {!Array<remoting.MockXhr.UrlHandler>}
166 * Registers a handler for a given method and URL. The |urlPattern|
167 * argument may either be a string, which must equal a URL to match
170 * Matching handlers are run when a FakeXhr's |start| method is
171 * called. The handler should generally call one of
172 * |set{Test,Json,Empty}Response|
174 * @param {?string} method The HTTP method to respond to, or null to
175 * respond to any method.
176 * @param {?string|!RegExp} urlPattern The URL or pattern to respond
177 * to, or null to match any URL.
178 * @param {function(!remoting.MockXhr):void} callback The handler
179 * function to call when a matching XHR is started.
180 * @param {boolean=} opt_reuse If true, the response can be used for
183 remoting.MockXhr.setResponseFor = function(
184 method, urlPattern, callback, opt_reuse) {
187 urlPattern: urlPattern,
194 * Installs a response with no content. See |setResponseFor| for
195 * more details on how the parameters work.
197 * @param {?string} method
198 * @param {?string|!RegExp} urlPattern
199 * @param {number=} opt_status The status code to return.
200 * @param {boolean=} opt_reuse
202 remoting.MockXhr.setEmptyResponseFor = function(
203 method, urlPattern, opt_status, opt_reuse) {
204 remoting.MockXhr.setResponseFor(
205 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
206 xhr.setEmptyResponse(opt_status === undefined ? 204 : opt_status);
211 * Installs a 200 response with text content. See |setResponseFor|
212 * for more details on how the parameters work.
214 * @param {?string} method
215 * @param {?string|!RegExp} urlPattern
216 * @param {string} content
217 * @param {boolean=} opt_reuse
219 remoting.MockXhr.setTextResponseFor = function(
220 method, urlPattern, content, opt_reuse) {
221 remoting.MockXhr.setResponseFor(
222 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
223 xhr.setTextResponse(200, content);
228 * Installs a 200 response with JSON content. See |setResponseFor|
229 * for more details on how the parameters work.
231 * @param {?string} method
232 * @param {?string|!RegExp} urlPattern
234 * @param {boolean=} opt_reuse
236 remoting.MockXhr.setJsonResponseFor = function(
237 method, urlPattern, content, opt_reuse) {
238 remoting.MockXhr.setResponseFor(
239 method, urlPattern, function(/** remoting.MockXhr */ xhr) {
240 xhr.setJsonResponse(200, content);
245 * Runs the most first handler for a given method and URL.
246 * @param {!remoting.MockXhr} xhr
248 var runMatchingHandler = function(xhr) {
249 for (var i = 0; i < handlers.length; i++) {
250 var handler = handlers[i];
251 if (handler.method == null || handler.method != xhr.params.method) {
254 if (handler.urlPattern == null) {
255 // Let the handler run.
256 } else if (typeof handler.urlPattern == 'string') {
257 if (xhr.params.url != handler.urlPattern) {
261 var regexp = /** @type {RegExp} */ (handler.urlPattern);
262 if (!regexp.test(xhr.params.url)) {
266 if (!handler.reuse) {
267 handlers.splice(i, 1);
269 handler.callback(xhr);
273 'No handler registered for ' + xhr.params.method +
274 ' to '+ xhr.params.url);
278 * Activates this mock.
280 remoting.MockXhr.activate = function() {
281 console.assert(origXhr == null, 'Xhr mocking already active');
282 origXhr = remoting.Xhr;
283 remoting.MockXhr.Response = remoting.Xhr.Response;
284 remoting['Xhr'] = remoting.MockXhr;
288 * Restores the original definiton of |remoting.Xhr|.
290 remoting.MockXhr.restore = function() {
291 console.assert(origXhr != null, 'Xhr mocking not active');
292 remoting['Xhr'] = origXhr;
299 // Can't put put typedefs inside a function :-(
303 * urlPattern:(?string|RegExp),
304 * callback:function(!remoting.MockXhr):void,
308 remoting.MockXhr.UrlHandler;