-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathajax-helpers.js
More file actions
308 lines (285 loc) · 11.3 KB
/
ajax-helpers.js
File metadata and controls
308 lines (285 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
/**
* These utility methods are used by Jasmine tests to create a mock server or
* get reference to mock requests. In either case, the cleanup (restore) is done with
* an after function.
*
* This pattern is being used instead of the more common beforeEach/afterEach pattern
* because we were seeing sporadic failures in the afterEach restore call. The cause of the
* errors were that one test suite was incorrectly being linked as the parent of an unrelated
* test suite (causing both suites' afterEach methods to be called). No solution for the root
* cause has been found, but initializing sinon and cleaning it up on a method-by-method
* basis seems to work. For more details, see STUD-1264.
*
* @module AjaxHelpers
*/
define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) {
'use strict';
var XHR_READY_STATES, fakeServer, createFakeRequests, withFakeRequests, fakeRequests, currentRequest,
expectRequest, expectNoRequests, expectJsonRequest, expectPostRequest, expectRequestURL, skipResetRequest,
respond, respondWithJson, respondWithError, respondWithTextError, respondWithNoContent;
/**
* An enumeration of valid XHR ready states.
*/
XHR_READY_STATES = {
UNSENT: 0,
OPENED: 1,
LOADING: 3,
DONE: 4
};
/* These utility methods are used by Jasmine tests to create a mock server or
* get reference to mock requests. In either case, the cleanup (restore) is done with
* an after function.
*
* This pattern is being used instead of the more common beforeEach/afterEach pattern
* because we were seeing sporadic failures in the afterEach restore call. The cause of the
* errors were that one test suite was incorrectly being linked as the parent of an unrelated
* test suite (causing both suites' afterEach methods to be called). No solution for the root
* cause has been found, but initializing sinon and cleaning it up on a method-by-method
* basis seems to work. For more details, see STUD-1264.
*/
/**
* Get a reference to the mocked server, and respond to all requests
* with the specified response.
*
* @param {string} response the fake response.
* @returns {*} The current request.
*/
fakeServer = function(response) {
var server = sinon.fakeServer.create();
afterEach(function() {
if (server) {
server.restore();
}
});
server.respondWith(response);
return server;
};
createFakeRequests = function() {
var requests = [],
xhr = sinon.useFakeXMLHttpRequest();
requests.currentIndex = 0;
requests.restore = function() {
if (xhr && 'restore' in xhr) {
xhr.restore();
}
};
xhr.onCreate = function(request) {
requests.push(request);
};
return requests;
};
/**
* Keep track of all requests to a fake server, and return a reference to the Array.
* This allows tests to respond to individual requests.
*
* @returns {Array} An array tracking the fake requests.
*/
fakeRequests = function() {
var requests = createFakeRequests();
afterEach(function() {
requests.restore();
});
return requests;
};
/**
* Wraps a test function so that it is invoked with an additional parameter
* containing an array of fake HTTP requests.
*
* @param {function} test A function to be invoked with the fake requests.
* @returns {function} A wrapped version of the input function.
*/
withFakeRequests = function(test) {
return function() {
var requests = createFakeRequests(),
args = Array.prototype.slice.call(arguments);
test.apply(null, args.concat([requests]));
requests.restore();
};
};
/**
* Returns the request that has not yet been responded to. If no such request
* is available then the current test will fail.
*
* @param {object} requests an array of fired sinon requests
* @returns {*} The current request.
*/
currentRequest = function(requests) {
expect(requests.length).toBeGreaterThan(requests.currentIndex);
return requests[requests.currentIndex];
};
/**
* Expect that a request was made as expected.
*
* @param {object} requests an array of fired sinon requests
* @param {string} method the expected method of the request
* @param {string} url the expected url of the request
* @param {string} body the expected request body
*/
expectRequest = function(requests, method, url, body) {
var request = currentRequest(requests);
expect(request.readyState).toEqual(XHR_READY_STATES.OPENED);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
if (typeof body === 'undefined') {
// The body of the request may not be germane to the current test,
// such as a call by a library, so allow it to be ignored.
return;
}
expect(request.requestBody).toEqual(body);
};
/**
* Verifies that there are no unconsumed requests.
*
* @param {object} requests an array of fired sinon requests
*/
expectNoRequests = function(requests) {
expect(requests.length).toEqual(requests.currentIndex);
};
/**
* Expect that a request with a JSON payload was made as expected.
*
* @param {object} requests an array of fired sinon requests
* @param {string} method the expected method of the request
* @param {string} url the expected url of the request
* @param {object} jsonRequest the expected request body as an object
*/
expectJsonRequest = function(requests, method, url, jsonRequest) {
var request = currentRequest(requests);
expect(request.readyState).toEqual(XHR_READY_STATES.OPENED);
expect(request.url).toEqual(url);
expect(request.method).toEqual(method);
expect(JSON.parse(request.requestBody)).toEqual(jsonRequest === undefined ? null : jsonRequest);
};
/**
* Expect that a JSON request be made with the given URL and parameters.
*
* @param {object} requests The collected requests
* @param {string} expectedUrl The expected URL excluding the parameters
* @param {object} expectedParameters An object representing the URL parameters
*/
expectRequestURL = function(requests, expectedUrl, expectedParameters) {
var request = currentRequest(requests),
parameters;
expect(new URI(request.url).path()).toEqual(expectedUrl);
parameters = new URI(request.url).query(true);
delete parameters._; // Ignore the cache-busting argument
expect(parameters).toEqual(expectedParameters);
};
/**
* Intended for use with POST requests using application/x-www-form-urlencoded.
*
* @param {object} requests an array of fired sinon requests
* @param {string} url the expected url of the request
* @param {string} body the expected body of the request
*/
expectPostRequest = function(requests, url, body) {
var request = currentRequest(requests);
expect(request.readyState).toEqual(XHR_READY_STATES.OPENED);
expect(request.url).toEqual(url);
expect(request.method).toEqual('POST');
expect(_.difference(request.requestBody.split('&'), body.split('&'))).toEqual([]);
};
/**
* Verify that the HTTP request was marked as reset, and then skip it.
*
* Note: this is typically used when code has explicitly canceled a request
* after it has been sent. A good example is when a user chooses to cancel
* a slow running search.
* @param {object} requests an array of fired sinon requests
*/
skipResetRequest = function(requests) {
var request = currentRequest(requests);
expect(request.readyState).toEqual(XHR_READY_STATES.UNSENT);
// Our ESLint config bans mutating params, but fixing this would require breaking AjaxHelpers API
requests.currentIndex += 1; // eslint-disable-line no-param-reassign
};
/**
* Respond to a server request with a set of options:
*
* - `statusCode`: the status code to be returned (defaults to 200)
* - `contentType`: the content type of the response (defaults to 'application/json')
* - `body`: the body of the response (if JSON, it will be converted to a string)
*
* @param {object} requests an array of fired sinon requests
* @param {object} options the options to provide to the response
*/
respond = function(requests, options) {
var request = currentRequest(requests),
statusCode = options.statusCode || 200,
contentType = options.contentType || 'application/json',
body = options.body || '';
request.respond(statusCode,
{'Content-Type': contentType},
contentType === 'application/json' ? JSON.stringify(body || {}) : body
);
requests.currentIndex += 1; // eslint-disable-line no-param-reassign
};
/**
* Respond to a request with JSON.
*
* @param {object} requests an array of fired sinon requests
* @param {object} body an object to be serialized to the response
*/
respondWithJson = function(requests, body) {
respond(requests, {
body: body
});
};
/**
* Respond to a request with an error status code and a JSON
* payload.
*
* @param {object} requests an array of fired sinon requests
* @param {int} statusCode the HTTP status code of the response
* (defaults to 500)
* @param {object} body an object to be serialized to the response
*/
respondWithError = function(requests, statusCode, body) {
respond(requests, {
statusCode: statusCode || 500,
body: body
});
};
/**
* Respond to a request with an error status code.
*
* @param {object} requests an array of fired sinon requests
* @param {int} statusCode the HTTP status code of the response
* (defaults to 500)
* @param {object} body the response body as a string
*/
respondWithTextError = function(requests, statusCode, body) {
respond(requests, {
statusCode: statusCode || 500,
contentType: 'text/plain',
body: body
});
};
/**
* Respond to a request with an HTTP 204.
*
* @param {object} requests an array of fired sinon requests
*/
respondWithNoContent = function(requests) {
respond(requests, {
statusCode: 204
});
};
return {
server: fakeServer,
requests: fakeRequests,
withFakeRequests: withFakeRequests,
currentRequest: currentRequest,
expectRequest: expectRequest,
expectNoRequests: expectNoRequests,
expectJsonRequest: expectJsonRequest,
expectPostRequest: expectPostRequest,
expectRequestURL: expectRequestURL,
skipResetRequest: skipResetRequest,
respond: respond,
respondWithJson: respondWithJson,
respondWithError: respondWithError,
respondWithTextError: respondWithTextError,
respondWithNoContent: respondWithNoContent
};
});