├── README.textile
├── lib
├── jquery.js
├── jshamcrest.js
└── json2.js
├── src
└── mockAjax.js
└── test
├── JsTestDriver.conf
├── JsTestDriver.jar
├── mockAjax-test.js
└── test-plugins.js
/README.textile:
--------------------------------------------------------------------------------
1 | h1. MockAjax
2 |
3 | MockAjax is an mock XMLHttpRequest implemetation designed to allow asyncronous
4 | xhr requests to be run inside a synchronous testing framework. It is specifically
5 | designed to run with JsHamcrest and jQuery or PrototypeJS and inside many different unit testing frameworks.
6 | Includes support for jsTestDriver, jasmine, JsUnitTest, jsUnity, QUnit, Rhino, YIUTest and screwunit
7 |
8 | h2. So what does it do?
9 |
10 | * Mock out Ajax requests, so a server is not required to test server dependant code
11 | * Allow asyncronous requests to run synchronously, allowing tests to run much faster than normal
12 | * Allow you to test multiple simultaneous inflight requests
13 | * Allow tricky edge cases to be tested with ease
14 | ** server timeouts
15 | ** receiving server responses out of order
16 | ** 404's
17 | ** server errors
18 | * Allows tests that use setTimeout to run instantly and reliably
19 | * also supports asynchronous and synchronous ajax without blocking
20 |
21 | h2. Show me
22 |
23 | pre. // fetch an article from the server and put it in a div on the page
24 | // set up our expectations (you can think of this as a virtual server)
25 | whenRequest({ url: is("/api/article/1") })
26 | .thenRespond({ type: "html", data: "
Article Title
this is the body
" });
27 | // run the code under test
28 | jQuery.ajax( {
29 | url: "/api/article/1",
30 | success: function(article) {
31 | $("#doc").html(article);
32 | }
33 | } );
34 | // trigger the respond from the server
35 | MockAjax.respond();
36 | // assert that the test ran correctly
37 | assertThat($("#doc > H2").text(), equalTo("Article Title"), "title correctly applied to article heading");
38 | assertThat($("#doc > P").text(), equalTo("this is the body"), "body correctly applied to article paragraph");
39 |
40 | h2. How do I use it
41 |
42 | First of all, this framework requires JsHamcrest (or something which uses hamcrest compatible matchers), so you need to be using that.
43 | Right after the JsHamcrest.Integration is preformed, add this line
44 |
45 | @MockAjax.Integration.JsTestDriver().jQuery();@
46 |
47 | Integration commands can be chained and the following frameworks are supported
48 |
49 | h3. testing frameworks
50 |
51 | * jasmine
52 | * JsTestDriver
53 | * JsUnitTest
54 | * jsUnity
55 | * QUnit
56 | * Rhino
57 | * YUITest
58 | * screwunit
59 |
60 | h3. development frameworks
61 |
62 | * jQuery
63 | * Prototype
64 | * Zepto
65 |
66 |
67 | h2. I want more
68 |
69 | h3. whenRequest
70 |
71 | Each parameter to @whenRequest@ takes a "hamcrest matcher":http://jshamcrest.destaquenet.com/modules/matchers.html . All parameters are optional
72 | You can create as many whenRequest / thenRespond pairs as you like
73 |
74 | * @method@ matches the request method (GET, POST, HEAD etc)
75 | * @url@ matches the url component
76 | * @data@ matches the request body (usually blank for GET requests, or contains form data)
77 | * @headers@ matches the map of request headers (eg Cookie, Referer)
78 | * @async@ matches the async flag (either @true@ or @false@ )
79 | * @username@ matches the HTTP username
80 | * @password@ matches the HTTP password
81 |
82 | examples:
83 |
84 | pre. whenRequest({ method: anyOf("PUT", "DELETE"), url: matches(/blog\/\d+/), username: is("admin") })
85 |
86 | h3. thenRespond
87 |
88 | Each parameter passed to @thenRespond@ constitutes some part of the response from the server
89 |
90 | * @status@ (default = 200) the "HTTP status code":http://en.wikipedia.org/wiki/List_of_HTTP_status_codes for the response
91 | * @data@ (default = "") the responseText or responseXML that forms the body of the response
92 | * @type@ (default = "json") a short cut for setting the Content-Type header (acceptable values are @json@, @html@, @text@, @script@, @xml@, @default@ )
93 | * @headers@ additional headers that you want to include in the response.
94 | Note that by default MockAjax provides some basic headers that are required to get the mock working in jQuery, any that are provided override the built in ones.
95 |
96 | By default there is one built in response that will be returned if no request matches and that is a 404 - file not found response
97 |
98 | examples:
99 |
100 | pre. thenRespond({ type: "html", data: "hello world
" });
101 | thenRespond({ data: '{"this":"is","some":["json","data"]}' });
102 | thenRespond({ status: 500, data: "Fatal error: Call to undefined function: mysql_set_charset() in /pages/includes/comments.php on line 152" });
103 | thenRespond({ status: 401, headers: { "WWW-Authenticate": "Basic realm=\"intranet\"" }, data: "401 - Unauthorized" });
104 |
105 | h3. Dynamic Responses
106 |
107 | Rather than specifying the responses for each request statically, you can define a callback function which returns an object containing any response parameters. The callback is provided with the original request.
108 |
109 | * @function callback(request, mockXHRObject) { return { [status | data | type | headers ] }; }@
110 |
111 | examples:
112 |
113 | pre. // echo the request method and url back as the response
114 | thenRespond( function( req ) { return { type: "text", data: ( req.method + req.url ) } } );
115 |
116 | pre. // modify the body based on the url parameter
117 | thenRespond( function (req) { return { type: "text", data: "hello " + req.url.match(/?name=(\w+)/ ) } } );
118 |
119 | pre. // set the last modified header to right now
120 | thenRespond( function (req) { return { headers: { "Last-Modified" : (new Date()).toUTCString() } } } );
121 |
122 | h2. MockAjax.respond()
123 |
124 | the @respond@ method causes MockAjax to respond with the oldest ajax response in it's queue. The respond method is not required for synchronous ajax requests.
125 | if there are multiple inflight requests at once, you can cause responses to return out-of-order by passing a non-zero number.
126 | @MockAjax.respond(3)@ causes the fourth queued response to be returned
127 |
128 | h2. MockAjax.respondAll()
129 |
130 | the @respondAll@ method causes MockAjax to respond to all of the ajax responses in it's queue, oldest first. The respondAll method is not required for synchronous ajax requests.
131 |
132 | h2. MockAjax.timeout()
133 |
134 | the @timeout@ method causes any queued calls to setTimeout to be executed. All calls to setTimeout are trapped, so if you have multiple timers you will need to call timeout multiple times to clear them all.
135 |
136 | pre. // example testing the timeout feature of some code
137 | // fire off the ajax request
138 | jQuery.ajax( {
139 | url: "/article/12",
140 | timeout: 60000, // one minute (but we don't want wait that long for the test!)
141 | success: this.doSomething,
142 | error: function(x, e) { if(e === "timeout") { $("#doc").text("Error: the request timed out. Perhaps the server died"); } }
143 | } );
144 | // force the timeout
145 | MockAjax.timeout();
146 | // assert that the test ran correctly
147 | assertThat( $("#doc").text(), contains("timed out"), "testing request timeout handling");
148 |
149 |
150 |
151 |
152 |
153 |
--------------------------------------------------------------------------------
/lib/jshamcrest.js:
--------------------------------------------------------------------------------
1 | /*
2 | * JsHamcrest v0.6.2
3 | * http://jshamcrest.destaquenet.com
4 | *
5 | * Library of matcher objects for JavaScript.
6 | *
7 | * Copyright (c) 2009-2010 Daniel Fernandes Martins
8 | * Licensed under the BSD license.
9 | *
10 | * Revision: 40a0bcaf1807573802fb6f5bb7d6e2502a38f325
11 | * Date: Thu Sep 30 15:32:06 2010 -0300
12 | */
13 |
14 | JsHamcrest = {
15 | /**
16 | * Library version.
17 | */
18 | version: '0.6.2',
19 |
20 | /**
21 | * Returns whether the given object is a matcher.
22 | */
23 | isMatcher: function(obj) {
24 | return obj instanceof JsHamcrest.SimpleMatcher;
25 | },
26 |
27 | /**
28 | * Returns whether the given arrays are equivalent.
29 | */
30 | areArraysEqual: function(array, anotherArray) {
31 | if (array instanceof Array || anotherArray instanceof Array) {
32 | if (array.length != anotherArray.length) {
33 | return false;
34 | }
35 |
36 | for (var i = 0; i < array.length; i++) {
37 | var a = array[i];
38 | var b = anotherArray[i];
39 |
40 | if (a instanceof Array || b instanceof Array) {
41 | return JsHamcrest.areArraysEqual(a, b);
42 | } else if (a != b) {
43 | return false;
44 | }
45 | }
46 | return true;
47 | } else {
48 | return array == anotherArray;
49 | }
50 | },
51 |
52 | /**
53 | * Builds a matcher object that uses external functions provided by the
54 | * caller in order to define the current matching logic.
55 | */
56 | SimpleMatcher: function(params) {
57 | params = params || {};
58 |
59 | this.matches = params.matches;
60 | this.describeTo = params.describeTo;
61 |
62 | // Replace the function to describe the actual value
63 | if (params.describeValueTo) {
64 | this.describeValueTo = params.describeValueTo;
65 | }
66 | },
67 |
68 | /**
69 | * Matcher that provides an easy way to wrap several matchers into one.
70 | */
71 | CombinableMatcher: function(params) {
72 | // Call superclass' constructor
73 | JsHamcrest.SimpleMatcher.apply(this, arguments);
74 |
75 | params = params || {};
76 |
77 | this.and = function(anotherMatcher) {
78 | var all = JsHamcrest.Matchers.allOf(this, anotherMatcher);
79 | return new JsHamcrest.CombinableMatcher({
80 | matches: all.matches,
81 |
82 | describeTo: function(description) {
83 | description.appendDescriptionOf(all);
84 | }
85 | });
86 | };
87 |
88 | this.or = function(anotherMatcher) {
89 | var any = JsHamcrest.Matchers.anyOf(this, anotherMatcher);
90 | return new JsHamcrest.CombinableMatcher({
91 | matches: any.matches,
92 |
93 | describeTo: function(description) {
94 | description.appendDescriptionOf(any);
95 | }
96 | });
97 | };
98 | },
99 |
100 | /**
101 | * Class that builds assertion error messages.
102 | */
103 | Description: function() {
104 | var value = '';
105 |
106 | this.get = function() {
107 | return value;
108 | };
109 |
110 | this.appendDescriptionOf = function(selfDescribingObject) {
111 | if (selfDescribingObject) {
112 | selfDescribingObject.describeTo(this);
113 | }
114 | return this;
115 | };
116 |
117 | this.append = function(text) {
118 | if (text != null) {
119 | value += text;
120 | }
121 | return this;
122 | };
123 |
124 | this.appendLiteral = function(literal) {
125 | var undefined;
126 | if (literal === undefined) {
127 | this.append('undefined');
128 | } else if (literal === null) {
129 | this.append('null');
130 | } else if (literal instanceof Array) {
131 | this.appendValueList('[', ', ', ']', literal);
132 | } else if (typeof literal == 'string') {
133 | this.append('"' + literal + '"');
134 | } else if (literal instanceof Function) {
135 | this.append('Function');
136 | } else {
137 | this.append(literal);
138 | }
139 | return this;
140 | };
141 |
142 | this.appendValueList = function(start, separator, end, list) {
143 | this.append(start);
144 | for (var i = 0; i < list.length; i++) {
145 | if (i > 0) {
146 | this.append(separator);
147 | }
148 | this.appendLiteral(list[i]);
149 | }
150 | this.append(end);
151 | return this;
152 | };
153 |
154 | this.appendList = function(start, separator, end, list) {
155 | this.append(start);
156 | for (var i = 0; i < list.length; i++) {
157 | if (i > 0) {
158 | this.append(separator);
159 | }
160 | this.appendDescriptionOf(list[i]);
161 | }
162 | this.append(end);
163 | return this;
164 | };
165 | }
166 | };
167 |
168 |
169 | /**
170 | * Describes the actual value to the given description. This method is optional
171 | * and, if it's not present, the actual value will be described as a JavaScript
172 | * literal.
173 | */
174 | JsHamcrest.SimpleMatcher.prototype.describeValueTo = function(actual, description) {
175 | description.appendLiteral(actual);
176 | };
177 |
178 |
179 | // CombinableMatcher is a specialization of SimpleMatcher
180 | JsHamcrest.CombinableMatcher.prototype = new JsHamcrest.SimpleMatcher();
181 | JsHamcrest.CombinableMatcher.prototype.constructor = JsHamcrest.CombinableMatcher;
182 |
183 | JsHamcrest.Matchers = {};
184 |
185 | /**
186 | * The actual value must be any value considered truth by the JavaScript
187 | * engine.
188 | */
189 | JsHamcrest.Matchers.truth = function() {
190 | return new JsHamcrest.SimpleMatcher({
191 | matches: function(actual) {
192 | return actual;
193 | },
194 |
195 | describeTo: function(description) {
196 | description.append('truth');
197 | }
198 | });
199 | };
200 |
201 | /**
202 | * Delegate-only matcher frequently used to improve readability.
203 | */
204 | JsHamcrest.Matchers.is = function(matcherOrValue) {
205 | // Uses 'equalTo' matcher if the given object is not a matcher
206 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
207 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
208 | }
209 |
210 | return new JsHamcrest.SimpleMatcher({
211 | matches: function(actual) {
212 | return matcherOrValue.matches(actual);
213 | },
214 |
215 | describeTo: function(description) {
216 | description.append('is ').appendDescriptionOf(matcherOrValue);
217 | }
218 | });
219 | };
220 |
221 | /**
222 | * The actual value must not match the given matcher or value.
223 | */
224 | JsHamcrest.Matchers.not = function(matcherOrValue) {
225 | // Uses 'equalTo' matcher if the given object is not a matcher
226 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
227 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
228 | }
229 |
230 | return new JsHamcrest.SimpleMatcher({
231 | matches: function(actual) {
232 | return !matcherOrValue.matches(actual);
233 | },
234 |
235 | describeTo: function(description) {
236 | description.append('not ').appendDescriptionOf(matcherOrValue);
237 | }
238 | });
239 | };
240 |
241 | /**
242 | * The actual value must be equal to the given value.
243 | */
244 | JsHamcrest.Matchers.equalTo = function(expected) {
245 | return new JsHamcrest.SimpleMatcher({
246 | matches: function(actual) {
247 | if (expected instanceof Array || actual instanceof Array) {
248 | return JsHamcrest.areArraysEqual(expected, actual);
249 | }
250 | return actual == expected;
251 | },
252 |
253 | describeTo: function(description) {
254 | description.append('equal to ').appendLiteral(expected);
255 | }
256 | });
257 | };
258 |
259 | /**
260 | * Useless always-match matcher.
261 | */
262 | JsHamcrest.Matchers.anything = function() {
263 | return new JsHamcrest.SimpleMatcher({
264 | matches: function(actual) {
265 | return true;
266 | },
267 |
268 | describeTo: function(description) {
269 | description.append('anything');
270 | }
271 | });
272 | };
273 |
274 | /**
275 | * The actual value must be null (or undefined).
276 | */
277 | JsHamcrest.Matchers.nil = function() {
278 | return new JsHamcrest.SimpleMatcher({
279 | matches: function(actual) {
280 | return actual == null;
281 | },
282 |
283 | describeTo: function(description) {
284 | description.appendLiteral(null);
285 | }
286 | });
287 | };
288 |
289 | /**
290 | * The actual value must be the same as the given value.
291 | */
292 | JsHamcrest.Matchers.sameAs = function(expected) {
293 | return new JsHamcrest.SimpleMatcher({
294 | matches: function(actual) {
295 | return actual === expected;
296 | },
297 |
298 | describeTo: function(description) {
299 | description.append('same as ').appendLiteral(expected);
300 | }
301 | });
302 | };
303 |
304 | /**
305 | * The actual value is a function and, when invoked, it should thrown an
306 | * exception with the given name.
307 | */
308 | JsHamcrest.Matchers.raises = function(exceptionName) {
309 | return new JsHamcrest.SimpleMatcher({
310 | matches: function(actualFunction) {
311 | try {
312 | actualFunction();
313 | } catch (e) {
314 | if (e.name == exceptionName) {
315 | return true;
316 | } else {
317 | throw e;
318 | }
319 | }
320 | return false;
321 | },
322 |
323 | describeTo: function(description) {
324 | description.append('raises ').append(exceptionName);
325 | }
326 | });
327 | };
328 |
329 | /**
330 | * The actual value is a function and, when invoked, it should raise any
331 | * exception.
332 | */
333 | JsHamcrest.Matchers.raisesAnything = function() {
334 | return new JsHamcrest.SimpleMatcher({
335 | matches: function(actualFunction) {
336 | try {
337 | actualFunction();
338 | } catch (e) {
339 | return true;
340 | }
341 | return false;
342 | },
343 |
344 | describeTo: function(description) {
345 | description.append('raises anything');
346 | }
347 | });
348 | };
349 |
350 | /**
351 | * Combinable matcher where the actual value must match both of the given
352 | * matchers.
353 | */
354 | JsHamcrest.Matchers.both = function(matcherOrValue) {
355 | // Uses 'equalTo' matcher if the given object is not a matcher
356 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
357 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
358 | }
359 |
360 | return new JsHamcrest.CombinableMatcher({
361 | matches: matcherOrValue.matches,
362 | describeTo: function(description) {
363 | description.append('both ').appendDescriptionOf(matcherOrValue);
364 | }
365 | });
366 | };
367 |
368 | /**
369 | * Combinable matcher where the actual value must match at least one of the
370 | * given matchers.
371 | */
372 | JsHamcrest.Matchers.either = function(matcherOrValue) {
373 | // Uses 'equalTo' matcher if the given object is not a matcher
374 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
375 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
376 | }
377 |
378 | return new JsHamcrest.CombinableMatcher({
379 | matches: matcherOrValue.matches,
380 | describeTo: function(description) {
381 | description.append('either ').appendDescriptionOf(matcherOrValue);
382 | }
383 | });
384 | };
385 |
386 | /**
387 | * All the given values or matchers should match the actual value to be
388 | * sucessful. This matcher behaves pretty much like the && operator.
389 | */
390 | JsHamcrest.Matchers.allOf = function() {
391 | var args = arguments;
392 | if (args[0] instanceof Array) {
393 | args = args[0];
394 | }
395 | return new JsHamcrest.SimpleMatcher({
396 | matches: function(actual) {
397 | for (var i = 0; i < args.length; i++) {
398 | var matcher = args[i];
399 | if (!JsHamcrest.isMatcher(matcher)) {
400 | matcher = JsHamcrest.Matchers.equalTo(matcher);
401 | }
402 | if (!matcher.matches(actual)) {
403 | return false;
404 | }
405 | }
406 | return true;
407 | },
408 |
409 | describeTo: function(description) {
410 | description.appendList('(', ' and ', ')', args);
411 | }
412 | });
413 | };
414 |
415 | /**
416 | * At least one of the given matchers should match the actual value. This
417 | * matcher behaves pretty much like the || (or) operator.
418 | */
419 | JsHamcrest.Matchers.anyOf = function() {
420 | var args = arguments;
421 | if (args[0] instanceof Array) {
422 | args = args[0];
423 | }
424 | return new JsHamcrest.SimpleMatcher({
425 | matches: function(actual) {
426 | for (var i = 0; i < args.length; i++) {
427 | var matcher = args[i];
428 | if (!JsHamcrest.isMatcher(matcher)) {
429 | matcher = JsHamcrest.Matchers.equalTo(matcher);
430 | }
431 | if (matcher.matches(actual)) {
432 | return true;
433 | }
434 | }
435 | return false;
436 | },
437 |
438 | describeTo: function(description) {
439 | description.appendList('(', ' or ', ')', args);
440 | }
441 | });
442 | };
443 |
444 | /**
445 | * The actual number must be greater than the expected number.
446 | */
447 | JsHamcrest.Matchers.greaterThan = function(expected) {
448 | return new JsHamcrest.SimpleMatcher({
449 | matches: function(actual) {
450 | return actual > expected;
451 | },
452 |
453 | describeTo: function(description) {
454 | description.append('greater than ').appendLiteral(expected);
455 | }
456 | });
457 | };
458 |
459 | /**
460 | * The actual number must be greater than or equal to the expected number
461 | */
462 | JsHamcrest.Matchers.greaterThanOrEqualTo = function(expected) {
463 | return new JsHamcrest.SimpleMatcher({
464 | matches: function(actual) {
465 | return actual >= expected;
466 | },
467 |
468 | describeTo: function(description) {
469 | description.append('greater than or equal to ').appendLiteral(expected);
470 | }
471 | });
472 | };
473 |
474 | /**
475 | * The actual number must be less than the expected number.
476 | */
477 | JsHamcrest.Matchers.lessThan = function(expected) {
478 | return new JsHamcrest.SimpleMatcher({
479 | matches: function(actual) {
480 | return actual < expected;
481 | },
482 |
483 | describeTo: function(description) {
484 | description.append('less than ').appendLiteral(expected);
485 | }
486 | });
487 | };
488 |
489 | /**
490 | * The actual number must be less than or equal to the expected number.
491 | */
492 | JsHamcrest.Matchers.lessThanOrEqualTo = function(expected) {
493 | return new JsHamcrest.SimpleMatcher({
494 | matches: function(actual) {
495 | return actual <= expected;
496 | },
497 |
498 | describeTo: function(description) {
499 | description.append('less than or equal to ').append(expected);
500 | }
501 | });
502 | };
503 |
504 | /**
505 | * The actual value must not be a number.
506 | */
507 | JsHamcrest.Matchers.notANumber = function() {
508 | return new JsHamcrest.SimpleMatcher({
509 | matches: function(actual) {
510 | return isNaN(actual);
511 | },
512 |
513 | describeTo: function(description) {
514 | description.append('not a number');
515 | }
516 | });
517 | };
518 |
519 | /**
520 | * The actual value must be divisible by the given number.
521 | */
522 | JsHamcrest.Matchers.divisibleBy = function(divisor) {
523 | return new JsHamcrest.SimpleMatcher({
524 | matches: function(actual) {
525 | return actual % divisor === 0;
526 | },
527 |
528 | describeTo: function(description) {
529 | description.append('divisible by ').appendLiteral(divisor);
530 | }
531 | });
532 | };
533 |
534 | /**
535 | * The actual value must be even.
536 | */
537 | JsHamcrest.Matchers.even = function() {
538 | return new JsHamcrest.SimpleMatcher({
539 | matches: function(actual) {
540 | return actual % 2 === 0;
541 | },
542 |
543 | describeTo: function(description) {
544 | description.append('even');
545 | }
546 | });
547 | };
548 |
549 | /**
550 | * The actual number must be odd.
551 | */
552 | JsHamcrest.Matchers.odd = function() {
553 | return new JsHamcrest.SimpleMatcher({
554 | matches: function(actual) {
555 | return actual % 2 !== 0;
556 | },
557 |
558 | describeTo: function(description) {
559 | description.append('odd');
560 | }
561 | });
562 | };
563 |
564 | /**
565 | * The actual number must be between the given range (inclusive).
566 | */
567 | JsHamcrest.Matchers.between = function(start) {
568 | return {
569 | and: function(end) {
570 | var greater = end;
571 | var lesser = start;
572 |
573 | if (start > end) {
574 | greater = start;
575 | lesser = end;
576 | }
577 |
578 | return new JsHamcrest.SimpleMatcher({
579 | matches: function(actual) {
580 | return actual >= lesser && actual <= greater;
581 | },
582 |
583 | describeTo: function(description) {
584 | description.append('between ').appendLiteral(lesser)
585 | .append(' and ').appendLiteral(greater);
586 | }
587 | });
588 | }
589 | };
590 | };
591 |
592 | /**
593 | * The actual number must be close enough to *expected*, that is, the actual
594 | * number is equal to a value within some range of acceptable error.
595 | */
596 | JsHamcrest.Matchers.closeTo = function(expected, delta) {
597 | if (!delta) {
598 | delta = 0;
599 | }
600 |
601 | return new JsHamcrest.SimpleMatcher({
602 | matches: function(actual) {
603 | return (Math.abs(actual - expected) - delta) <= 0;
604 | },
605 |
606 | describeTo: function(description) {
607 | description.append('number within ')
608 | .appendLiteral(delta).append(' of ').appendLiteral(expected);
609 | }
610 | });
611 | };
612 |
613 | /**
614 | * The actual number must be zero.
615 | */
616 | JsHamcrest.Matchers.zero = function() {
617 | return new JsHamcrest.SimpleMatcher({
618 | matches: function(actual) {
619 | return actual === 0;
620 | },
621 |
622 | describeTo: function(description) {
623 | description.append('zero');
624 | }
625 | });
626 | };
627 |
628 | /**
629 | * The actual string must be equal to the given string, ignoring case.
630 | */
631 | JsHamcrest.Matchers.equalIgnoringCase = function(str) {
632 | return new JsHamcrest.SimpleMatcher({
633 | matches: function(actual) {
634 | return actual.toUpperCase() == str.toUpperCase();
635 | },
636 |
637 | describeTo: function(description) {
638 | description.append('equal ignoring case "').append(str).append('"');
639 | }
640 | });
641 | };
642 |
643 | /**
644 | * The actual string must have a substring equals to the given string.
645 | */
646 | JsHamcrest.Matchers.containsString = function(str) {
647 | return new JsHamcrest.SimpleMatcher({
648 | matches: function(actual) {
649 | return actual.indexOf(str) >= 0;
650 | },
651 |
652 | describeTo: function(description) {
653 | description.append('contains string "').append(str).append('"');
654 | }
655 | });
656 | };
657 |
658 | /**
659 | * The actual string must start with the given string.
660 | */
661 | JsHamcrest.Matchers.startsWith = function(str) {
662 | return new JsHamcrest.SimpleMatcher({
663 | matches: function(actual) {
664 | return actual.indexOf(str) === 0;
665 | },
666 |
667 | describeTo: function(description) {
668 | description.append('starts with ').appendLiteral(str);
669 | }
670 | });
671 | };
672 |
673 | /**
674 | * The actual string must end with the given string.
675 | */
676 | JsHamcrest.Matchers.endsWith = function(str) {
677 | return new JsHamcrest.SimpleMatcher({
678 | matches: function(actual) {
679 | return actual.lastIndexOf(str) + str.length == actual.length;
680 | },
681 |
682 | describeTo: function(description) {
683 | description.append('ends with ').appendLiteral(str);
684 | }
685 | });
686 | };
687 |
688 | /**
689 | * The actual string must match the given regular expression.
690 | */
691 | JsHamcrest.Matchers.matches = function(regex) {
692 | return new JsHamcrest.SimpleMatcher({
693 | matches: function(actual) {
694 | return regex.test(actual);
695 | },
696 |
697 | describeTo: function(description) {
698 | description.append('matches ').appendLiteral(regex);
699 | }
700 | });
701 | };
702 |
703 | /**
704 | * The actual string must look like an e-mail address.
705 | */
706 | JsHamcrest.Matchers.emailAddress = function() {
707 | var regex = /^([a-z0-9_\.\-\+])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+$/i;
708 |
709 | return new JsHamcrest.SimpleMatcher({
710 | matches: function(actual) {
711 | return regex.test(actual);
712 | },
713 |
714 | describeTo: function(description) {
715 | description.append('email address');
716 | }
717 | });
718 | };
719 |
720 | /**
721 | * The actual value has a member with the given name.
722 | */
723 | JsHamcrest.Matchers.hasMember = function(memberName) {
724 | return new JsHamcrest.SimpleMatcher({
725 | matches: function(actual) {
726 | if (actual) {
727 | return memberName in actual;
728 | }
729 | return false;
730 | },
731 |
732 | describeTo: function(description) {
733 | description.append('has member ').appendLiteral(memberName);
734 | }
735 | });
736 | };
737 |
738 | /**
739 | * The actual value has a function with the given name.
740 | */
741 | JsHamcrest.Matchers.hasFunction = function(functionName) {
742 | return new JsHamcrest.SimpleMatcher({
743 | matches: function(actual) {
744 | if (actual) {
745 | return functionName in actual &&
746 | actual[functionName] instanceof Function;
747 | }
748 | return false;
749 | },
750 |
751 | describeTo: function(description) {
752 | description.append('has function ').appendLiteral(functionName);
753 | }
754 | });
755 | };
756 |
757 | /**
758 | * The actual value must be an instance of the given class.
759 | */
760 | JsHamcrest.Matchers.instanceOf = function(clazz) {
761 | return new JsHamcrest.SimpleMatcher({
762 | matches: function(actual) {
763 | return !!(actual instanceof clazz);
764 | },
765 |
766 | describeTo: function(description) {
767 | var className = clazz.name ? clazz.name : 'a class';
768 | description.append('instance of ').append(className);
769 | }
770 | });
771 | };
772 |
773 | /**
774 | * The actual value must be an instance of the given type.
775 | */
776 | JsHamcrest.Matchers.typeOf = function(typeName) {
777 | return new JsHamcrest.SimpleMatcher({
778 | matches: function(actual) {
779 | return (typeof actual == typeName);
780 | },
781 |
782 | describeTo: function(description) {
783 | description.append('typeof ').append('"').append(typeName).append('"');
784 | }
785 | });
786 | };
787 |
788 | /**
789 | * The actual value must be an object.
790 | */
791 | JsHamcrest.Matchers.object = function() {
792 | return new JsHamcrest.Matchers.instanceOf(Object);
793 | };
794 |
795 | /**
796 | * The actual value must be a string.
797 | */
798 | JsHamcrest.Matchers.string = function() {
799 | return new JsHamcrest.Matchers.typeOf('string');
800 | };
801 |
802 | /**
803 | * The actual value must be a number.
804 | */
805 | JsHamcrest.Matchers.number = function() {
806 | return new JsHamcrest.Matchers.typeOf('number');
807 | };
808 |
809 | /**
810 | * The actual value must be a boolean.
811 | */
812 | JsHamcrest.Matchers.bool = function() {
813 | return new JsHamcrest.Matchers.typeOf('boolean');
814 | };
815 |
816 | /**
817 | * The actual value must be a function.
818 | */
819 | JsHamcrest.Matchers.func = function() {
820 | return new JsHamcrest.Matchers.instanceOf(Function);
821 | };
822 |
823 | /**
824 | * The actual value should be an array and it must contain at least one value
825 | * that matches the given value or matcher.
826 | */
827 | JsHamcrest.Matchers.hasItem = function(matcherOrValue) {
828 | // Uses 'equalTo' matcher if the given object is not a matcher
829 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
830 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
831 | }
832 |
833 | return new JsHamcrest.SimpleMatcher({
834 | matches: function(actual) {
835 | // Should be an array
836 | if (!(actual instanceof Array)) {
837 | return false;
838 | }
839 |
840 | for (var i = 0; i < actual.length; i++) {
841 | if (matcherOrValue.matches(actual[i])) {
842 | return true;
843 | }
844 | }
845 | return false;
846 | },
847 |
848 | describeTo: function(description) {
849 | description.append('array contains item ')
850 | .appendDescriptionOf(matcherOrValue);
851 | }
852 | });
853 | };
854 |
855 | /**
856 | * The actual value should be an array and the given values or matchers must
857 | * match at least one item.
858 | */
859 | JsHamcrest.Matchers.hasItems = function() {
860 | var items = [];
861 | for (var i = 0; i < arguments.length; i++) {
862 | items.push(JsHamcrest.Matchers.hasItem(arguments[i]));
863 | }
864 | return JsHamcrest.Matchers.allOf(items);
865 | };
866 |
867 | /**
868 | * The actual value should be an array and the given value or matcher must
869 | * match all items.
870 | */
871 | JsHamcrest.Matchers.everyItem = function(matcherOrValue) {
872 | // Uses 'equalTo' matcher if the given object is not a matcher
873 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
874 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
875 | }
876 |
877 | return new JsHamcrest.SimpleMatcher({
878 | matches: function(actual) {
879 | // Should be an array
880 | if (!(actual instanceof Array)) {
881 | return false;
882 | }
883 |
884 | for (var i = 0; i < actual.length; i++) {
885 | if (!matcherOrValue.matches(actual[i])) {
886 | return false;
887 | }
888 | }
889 | return true;
890 | },
891 |
892 | describeTo: function(description) {
893 | description.append('every item ')
894 | .appendDescriptionOf(matcherOrValue);
895 | }
896 | });
897 | };
898 |
899 | /**
900 | * The given array must contain the actual value.
901 | */
902 | JsHamcrest.Matchers.isIn = function() {
903 | var equalTo = JsHamcrest.Matchers.equalTo;
904 |
905 | var args = arguments;
906 | if (args[0] instanceof Array) {
907 | args = args[0];
908 | }
909 |
910 | return new JsHamcrest.SimpleMatcher({
911 | matches: function(actual) {
912 | for (var i = 0; i < args.length; i++) {
913 | if (equalTo(args[i]).matches(actual)) {
914 | return true;
915 | }
916 | }
917 | return false;
918 | },
919 |
920 | describeTo: function(description) {
921 | description.append('one of ').appendLiteral(args);
922 | }
923 | });
924 | };
925 |
926 | /**
927 | * Alias to 'isIn' matcher.
928 | */
929 | JsHamcrest.Matchers.oneOf = JsHamcrest.Matchers.isIn;
930 |
931 | /**
932 | * The actual value should be an array and it must be empty to be sucessful.
933 | */
934 | JsHamcrest.Matchers.empty = function() {
935 | return new JsHamcrest.SimpleMatcher({
936 | matches: function(actual) {
937 | return actual.length === 0;
938 | },
939 |
940 | describeTo: function(description) {
941 | description.append('empty');
942 | }
943 | });
944 | };
945 |
946 | /**
947 | * The length of the actual value value must match the given value or matcher.
948 | */
949 | JsHamcrest.Matchers.hasSize = function(matcherOrValue) {
950 | // Uses 'equalTo' matcher if the given object is not a matcher
951 | if (!JsHamcrest.isMatcher(matcherOrValue)) {
952 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
953 | }
954 |
955 | return new JsHamcrest.SimpleMatcher({
956 | matches: function(actual) {
957 | return matcherOrValue.matches(actual.length);
958 | },
959 |
960 | describeTo: function(description) {
961 | description.append('has size ').appendDescriptionOf(matcherOrValue);
962 | },
963 |
964 | describeValueTo: function(actual, description) {
965 | description.append(actual.length);
966 | }
967 | });
968 | };
969 |
970 | JsHamcrest.Operators = {};
971 |
972 | /**
973 | * Returns those items of the array for which matcher matches.
974 | */
975 | JsHamcrest.Operators.filter = function(array, matcherOrValue) {
976 | if (!(array instanceof Array) || matcherOrValue == null) {
977 | return array;
978 | }
979 | if (!(matcherOrValue instanceof JsHamcrest.SimpleMatcher)) {
980 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
981 | }
982 |
983 | var result = [];
984 | for (var i = 0; i < array.length; i++) {
985 | if (matcherOrValue.matches(array[i])) {
986 | result.push(array[i]);
987 | }
988 | }
989 | return result;
990 | };
991 |
992 | /**
993 | * Generic assert function.
994 | */
995 | JsHamcrest.Operators.assert = function(actualValue, matcherOrValue, options) {
996 | options = options ? options : {};
997 | var description = new JsHamcrest.Description();
998 |
999 | if (matcherOrValue == null) {
1000 | matcherOrValue = JsHamcrest.Matchers.truth();
1001 | } else if (!JsHamcrest.isMatcher(matcherOrValue)) {
1002 | matcherOrValue = JsHamcrest.Matchers.equalTo(matcherOrValue);
1003 | }
1004 |
1005 | if (options.message) {
1006 | description.append(options.message).append('. ');
1007 | }
1008 |
1009 | description.append('Expected ');
1010 | matcherOrValue.describeTo(description);
1011 |
1012 | if (!matcherOrValue.matches(actualValue)) {
1013 | description.passed = false;
1014 | description.append(' but was ');
1015 | matcherOrValue.describeValueTo(actualValue, description);
1016 | if (options.fail) {
1017 | options.fail(description.get());
1018 | }
1019 | } else {
1020 | description.append(': Success');
1021 | description.passed = true;
1022 | if (options.pass) {
1023 | options.pass(description.get());
1024 | }
1025 | }
1026 | return description;
1027 | };
1028 |
1029 | /**
1030 | * Delegate function, useful when used along with raises() and raisesAnything().
1031 | */
1032 | JsHamcrest.Operators.callTo = function() {
1033 | var func = [].shift.call(arguments);
1034 | var args = arguments;
1035 | return function() {
1036 | return func.apply(this, args);
1037 | };
1038 | }
1039 |
1040 | /**
1041 | * Integration utilities.
1042 | */
1043 |
1044 | JsHamcrest.Integration = (function() {
1045 |
1046 | var self = this;
1047 |
1048 | return {
1049 |
1050 | /**
1051 | * Copies all members of an object to another.
1052 | */
1053 | copyMembers: function(source, target) {
1054 | if (arguments.length == 1) {
1055 | target = source;
1056 | JsHamcrest.Integration.copyMembers(JsHamcrest.Matchers, target);
1057 | JsHamcrest.Integration.copyMembers(JsHamcrest.Operators, target);
1058 | } else if (source) {
1059 | for (var method in source) {
1060 | if (!(method in target)) {
1061 | target[method] = source[method];
1062 | }
1063 | }
1064 | }
1065 | },
1066 |
1067 | /**
1068 | * Adds the members of the given object to JsHamcrest.Matchers
1069 | * namespace.
1070 | */
1071 | installMatchers: function(matchersNamespace) {
1072 | var target = JsHamcrest.Matchers;
1073 | JsHamcrest.Integration.copyMembers(matchersNamespace, target);
1074 | },
1075 |
1076 | /**
1077 | * Adds the members of the given object to JsHamcrest.Operators
1078 | * namespace.
1079 | */
1080 | installOperators: function(operatorsNamespace) {
1081 | var target = JsHamcrest.Operators;
1082 | JsHamcrest.Integration.copyMembers(operatorsNamespace, target);
1083 | },
1084 |
1085 | /**
1086 | * Uses the web browser's alert() function to display the assertion
1087 | * results. Great for quick prototyping.
1088 | */
1089 | WebBrowser: function() {
1090 | JsHamcrest.Integration.copyMembers(self);
1091 |
1092 | self.assertThat = function (actual, matcher, message) {
1093 | return JsHamcrest.Operators.assert(actual, matcher, {
1094 | message: message,
1095 | fail: function(message) {
1096 | alert('[FAIL] ' + message);
1097 | },
1098 | pass: function(message) {
1099 | alert('[SUCCESS] ' + message);
1100 | }
1101 | });
1102 | };
1103 | },
1104 |
1105 | /**
1106 | * Uses the Rhino's print() function to display the assertion results.
1107 | * Great for prototyping.
1108 | */
1109 | Rhino: function() {
1110 | JsHamcrest.Integration.copyMembers(self);
1111 |
1112 | self.assertThat = function (actual, matcher, message) {
1113 | return JsHamcrest.Operators.assert(actual, matcher, {
1114 | message: message,
1115 | fail: function(message) {
1116 | print('[FAIL] ' + message + '\n');
1117 | },
1118 | pass: function(message) {
1119 | print('[SUCCESS] ' + message + '\n');
1120 | }
1121 | });
1122 | };
1123 | },
1124 |
1125 | /**
1126 | * JsTestDriver integration.
1127 | */
1128 | JsTestDriver: function(params) {
1129 | params = params ? params : {};
1130 | var target = params.scope || self;
1131 |
1132 | JsHamcrest.Integration.copyMembers(target);
1133 |
1134 | // Function called when an assertion fails.
1135 | function fail(message) {
1136 | var exc = new Error(message);
1137 | exc.name = 'AssertError';
1138 |
1139 | try {
1140 | // Removes all jshamcrest-related entries from error stack
1141 | var re = new RegExp('jshamcrest.*\.js\:', 'i');
1142 | var stack = exc.stack.split('\n');
1143 | var newStack = '';
1144 | for (var i = 0; i < stack.length; i++) {
1145 | if (!re.test(stack[i])) {
1146 | newStack += stack[i] + '\n';
1147 | }
1148 | }
1149 | exc.stack = newStack;
1150 | } catch (e) {
1151 | // It's okay, do nothing
1152 | }
1153 | throw exc;
1154 | }
1155 |
1156 | // Assertion method exposed to JsTestDriver.
1157 | target.assertThat = function (actual, matcher, message) {
1158 | return JsHamcrest.Operators.assert(actual, matcher, {
1159 | message: message,
1160 | fail: fail
1161 | });
1162 | };
1163 | },
1164 |
1165 | /**
1166 | * JsUnitTest integration.
1167 | */
1168 | JsUnitTest: function(params) {
1169 | params = params ? params : {};
1170 | var target = params.scope || JsUnitTest.Unit.Testcase.prototype;
1171 |
1172 | JsHamcrest.Integration.copyMembers(target);
1173 |
1174 | // Assertion method exposed to JsUnitTest.
1175 | target.assertThat = function (actual, matcher, message) {
1176 | var self = this;
1177 |
1178 | return JsHamcrest.Operators.assert(actual, matcher, {
1179 | message: message,
1180 | fail: function(message) {
1181 | self.fail(message);
1182 | },
1183 | pass: function() {
1184 | self.pass();
1185 | }
1186 | });
1187 | };
1188 | },
1189 |
1190 | /**
1191 | * YUITest (Yahoo UI) integration.
1192 | */
1193 | YUITest: function(params) {
1194 | params = params ? params : {};
1195 | var target = params.scope || self;
1196 |
1197 | JsHamcrest.Integration.copyMembers(target);
1198 |
1199 | target.Assert = YAHOO.util.Assert;
1200 |
1201 | // Assertion method exposed to YUITest.
1202 | YAHOO.util.Assert.that = function(actual, matcher, message) {
1203 | return JsHamcrest.Operators.assert(actual, matcher, {
1204 | message: message,
1205 | fail: function(message) {
1206 | YAHOO.util.Assert.fail(message);
1207 | }
1208 | });
1209 | };
1210 | },
1211 |
1212 | /**
1213 | * QUnit (JQuery) integration.
1214 | */
1215 | QUnit: function(params) {
1216 | params = params ? params : {};
1217 | var target = params.scope || self;
1218 |
1219 | JsHamcrest.Integration.copyMembers(target);
1220 |
1221 | // Assertion method exposed to QUnit.
1222 | target.assertThat = function(actual, matcher, message) {
1223 | return JsHamcrest.Operators.assert(actual, matcher, {
1224 | message: message,
1225 | fail: function(message) {
1226 | QUnit.ok(false, message);
1227 | },
1228 | pass: function(message) {
1229 | QUnit.ok(true, message);
1230 | }
1231 | });
1232 | };
1233 | },
1234 |
1235 | /**
1236 | * jsUnity integration.
1237 | */
1238 | jsUnity: function(params) {
1239 | params = params ? params : {};
1240 | var target = params.scope || jsUnity.env.defaultScope;
1241 | var assertions = params.attachAssertions || false;
1242 |
1243 | JsHamcrest.Integration.copyMembers(target);
1244 |
1245 | if (assertions) {
1246 | jsUnity.attachAssertions(target);
1247 | }
1248 |
1249 | // Assertion method exposed to jsUnity.
1250 | target.assertThat = function(actual, matcher, message) {
1251 | return JsHamcrest.Operators.assert(actual, matcher, {
1252 | message: message,
1253 | fail: function(message) {
1254 | throw message;
1255 | }
1256 | });
1257 | };
1258 | },
1259 |
1260 | /**
1261 | * Screw.Unit integration.
1262 | */
1263 | screwunit: function(params) {
1264 | params = params ? params : {};
1265 | var target = params.scope || Screw.Matchers;
1266 |
1267 | JsHamcrest.Integration.copyMembers(target);
1268 |
1269 | // Assertion method exposed to jsUnity.
1270 | target.assertThat = function(actual, matcher, message) {
1271 | return JsHamcrest.Operators.assert(actual, matcher, {
1272 | message: message,
1273 | fail: function(message) {
1274 | throw message;
1275 | }
1276 | });
1277 | };
1278 | }
1279 | };
1280 | })();
1281 |
--------------------------------------------------------------------------------
/lib/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | http://www.JSON.org/json2.js
3 | 2011-02-23
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, strict: false, regexp: false */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | var JSON;
163 | if (!JSON) {
164 | JSON = {};
165 | }
166 |
167 | (function () {
168 | "use strict";
169 |
170 | function f(n) {
171 | // Format integers to have at least two digits.
172 | return n < 10 ? '0' + n : n;
173 | }
174 |
175 | if (typeof Date.prototype.toJSON !== 'function') {
176 |
177 | Date.prototype.toJSON = function (key) {
178 |
179 | return isFinite(this.valueOf()) ?
180 | this.getUTCFullYear() + '-' +
181 | f(this.getUTCMonth() + 1) + '-' +
182 | f(this.getUTCDate()) + 'T' +
183 | f(this.getUTCHours()) + ':' +
184 | f(this.getUTCMinutes()) + ':' +
185 | f(this.getUTCSeconds()) + 'Z' : null;
186 | };
187 |
188 | String.prototype.toJSON =
189 | Number.prototype.toJSON =
190 | Boolean.prototype.toJSON = function (key) {
191 | return this.valueOf();
192 | };
193 | }
194 |
195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | gap,
198 | indent,
199 | meta = { // table of character substitutions
200 | '\b': '\\b',
201 | '\t': '\\t',
202 | '\n': '\\n',
203 | '\f': '\\f',
204 | '\r': '\\r',
205 | '"' : '\\"',
206 | '\\': '\\\\'
207 | },
208 | rep;
209 |
210 |
211 | function quote(string) {
212 |
213 | // If the string contains no control characters, no quote characters, and no
214 | // backslash characters, then we can safely slap some quotes around it.
215 | // Otherwise we must also replace the offending characters with safe escape
216 | // sequences.
217 |
218 | escapable.lastIndex = 0;
219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
220 | var c = meta[a];
221 | return typeof c === 'string' ? c :
222 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
223 | }) + '"' : '"' + string + '"';
224 | }
225 |
226 |
227 | function str(key, holder) {
228 |
229 | // Produce a string from holder[key].
230 |
231 | var i, // The loop counter.
232 | k, // The member key.
233 | v, // The member value.
234 | length,
235 | mind = gap,
236 | partial,
237 | value = holder[key];
238 |
239 | // If the value has a toJSON method, call it to obtain a replacement value.
240 |
241 | if (value && typeof value === 'object' &&
242 | typeof value.toJSON === 'function') {
243 | value = value.toJSON(key);
244 | }
245 |
246 | // If we were called with a replacer function, then call the replacer to
247 | // obtain a replacement value.
248 |
249 | if (typeof rep === 'function') {
250 | value = rep.call(holder, key, value);
251 | }
252 |
253 | // What happens next depends on the value's type.
254 |
255 | switch (typeof value) {
256 | case 'string':
257 | return quote(value);
258 |
259 | case 'number':
260 |
261 | // JSON numbers must be finite. Encode non-finite numbers as null.
262 |
263 | return isFinite(value) ? String(value) : 'null';
264 |
265 | case 'boolean':
266 | case 'null':
267 |
268 | // If the value is a boolean or null, convert it to a string. Note:
269 | // typeof null does not produce 'null'. The case is included here in
270 | // the remote chance that this gets fixed someday.
271 |
272 | return String(value);
273 |
274 | // If the type is 'object', we might be dealing with an object or an array or
275 | // null.
276 |
277 | case 'object':
278 |
279 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
280 | // so watch out for that case.
281 |
282 | if (!value) {
283 | return 'null';
284 | }
285 |
286 | // Make an array to hold the partial results of stringifying this object value.
287 |
288 | gap += indent;
289 | partial = [];
290 |
291 | // Is the value an array?
292 |
293 | if (Object.prototype.toString.apply(value) === '[object Array]') {
294 |
295 | // The value is an array. Stringify every element. Use null as a placeholder
296 | // for non-JSON values.
297 |
298 | length = value.length;
299 | for (i = 0; i < length; i += 1) {
300 | partial[i] = str(i, value) || 'null';
301 | }
302 |
303 | // Join all of the elements together, separated with commas, and wrap them in
304 | // brackets.
305 |
306 | v = partial.length === 0 ? '[]' : gap ?
307 | '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
308 | '[' + partial.join(',') + ']';
309 | gap = mind;
310 | return v;
311 | }
312 |
313 | // If the replacer is an array, use it to select the members to be stringified.
314 |
315 | if (rep && typeof rep === 'object') {
316 | length = rep.length;
317 | for (i = 0; i < length; i += 1) {
318 | if (typeof rep[i] === 'string') {
319 | k = rep[i];
320 | v = str(k, value);
321 | if (v) {
322 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
323 | }
324 | }
325 | }
326 | } else {
327 |
328 | // Otherwise, iterate through all of the keys in the object.
329 |
330 | for (k in value) {
331 | if (Object.prototype.hasOwnProperty.call(value, k)) {
332 | v = str(k, value);
333 | if (v) {
334 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
335 | }
336 | }
337 | }
338 | }
339 |
340 | // Join all of the member texts together, separated with commas,
341 | // and wrap them in braces.
342 |
343 | v = partial.length === 0 ? '{}' : gap ?
344 | '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
345 | '{' + partial.join(',') + '}';
346 | gap = mind;
347 | return v;
348 | }
349 | }
350 |
351 | // If the JSON object does not yet have a stringify method, give it one.
352 |
353 | if (typeof JSON.stringify !== 'function') {
354 | JSON.stringify = function (value, replacer, space) {
355 |
356 | // The stringify method takes a value and an optional replacer, and an optional
357 | // space parameter, and returns a JSON text. The replacer can be a function
358 | // that can replace values, or an array of strings that will select the keys.
359 | // A default replacer method can be provided. Use of the space parameter can
360 | // produce text that is more easily readable.
361 |
362 | var i;
363 | gap = '';
364 | indent = '';
365 |
366 | // If the space parameter is a number, make an indent string containing that
367 | // many spaces.
368 |
369 | if (typeof space === 'number') {
370 | for (i = 0; i < space; i += 1) {
371 | indent += ' ';
372 | }
373 |
374 | // If the space parameter is a string, it will be used as the indent string.
375 |
376 | } else if (typeof space === 'string') {
377 | indent = space;
378 | }
379 |
380 | // If there is a replacer, it must be a function or an array.
381 | // Otherwise, throw an error.
382 |
383 | rep = replacer;
384 | if (replacer && typeof replacer !== 'function' &&
385 | (typeof replacer !== 'object' ||
386 | typeof replacer.length !== 'number')) {
387 | throw new Error('JSON.stringify');
388 | }
389 |
390 | // Make a fake root object containing our value under the key of ''.
391 | // Return the result of stringifying the value.
392 |
393 | return str('', {'': value});
394 | };
395 | }
396 |
397 |
398 | // If the JSON object does not yet have a parse method, give it one.
399 |
400 | if (typeof JSON.parse !== 'function') {
401 | JSON.parse = function (text, reviver) {
402 |
403 | // The parse method takes a text and an optional reviver function, and returns
404 | // a JavaScript value if the text is a valid JSON text.
405 |
406 | var j;
407 |
408 | function walk(holder, key) {
409 |
410 | // The walk method is used to recursively walk the resulting structure so
411 | // that modifications can be made.
412 |
413 | var k, v, value = holder[key];
414 | if (value && typeof value === 'object') {
415 | for (k in value) {
416 | if (Object.prototype.hasOwnProperty.call(value, k)) {
417 | v = walk(value, k);
418 | if (v !== undefined) {
419 | value[k] = v;
420 | } else {
421 | delete value[k];
422 | }
423 | }
424 | }
425 | }
426 | return reviver.call(holder, key, value);
427 | }
428 |
429 |
430 | // Parsing happens in four stages. In the first stage, we replace certain
431 | // Unicode characters with escape sequences. JavaScript handles many characters
432 | // incorrectly, either silently deleting them, or treating them as line endings.
433 |
434 | text = String(text);
435 | cx.lastIndex = 0;
436 | if (cx.test(text)) {
437 | text = text.replace(cx, function (a) {
438 | return '\\u' +
439 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
440 | });
441 | }
442 |
443 | // In the second stage, we run the text against regular expressions that look
444 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
445 | // because they can cause invocation, and '=' because it can cause mutation.
446 | // But just to be safe, we want to reject all unexpected forms.
447 |
448 | // We split the second stage into 4 regexp operations in order to work around
449 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
450 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
451 | // replace all simple value tokens with ']' characters. Third, we delete all
452 | // open brackets that follow a colon or comma or that begin the text. Finally,
453 | // we look to see that the remaining characters are only whitespace or ']' or
454 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
455 |
456 | if (/^[\],:{}\s]*$/
457 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
458 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
459 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
460 |
461 | // In the third stage we use the eval function to compile the text into a
462 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
463 | // in JavaScript: it can begin a block or an object literal. We wrap the text
464 | // in parens to eliminate the ambiguity.
465 |
466 | j = eval('(' + text + ')');
467 |
468 | // In the optional fourth stage, we recursively walk the new structure, passing
469 | // each name/value pair to a reviver function for possible transformation.
470 |
471 | return typeof reviver === 'function' ?
472 | walk({'': j}, '') : j;
473 | }
474 |
475 | // If the text is not JSON parseable, then a SyntaxError is thrown.
476 |
477 | throw new SyntaxError('JSON.parse');
478 | };
479 | }
480 | }());
481 |
--------------------------------------------------------------------------------
/src/mockAjax.js:
--------------------------------------------------------------------------------
1 | /**
2 | * MockAjax is a Mock Object for simulating asyncronous server requests in a synchronous testing environment
3 | * it is specifically designed for use with jsHamcrest and works well with jsTestDriver and jQuery
4 | *
5 | * setup like this
6 | * MockAjax.integrate.jsTestDriver().jQuery()
7 | *
8 | * when MockAjax is used it responds as a normal XMLHttpRequest, but rather than going to the server
9 | * it responds with a response based on applying hamcrest matches against the requests that the test make
10 | *
11 | * by default there is only one response (which matches all requests) which is a 404
12 | * add additional responses using the following pattern
13 | *
14 | * whenRequest({ url: startsWith("/article/1") })
15 | * .respondWith({ data: '{"title":"Something","body":"This is a body"}');
16 | *
17 | * note in particular that whenRequest uses hamcrest style matchers
18 | * both whenRequest and respondWith contain many options which support common tests simply
19 | * while allowing you to simulate quite complex server interaction
20 | *
21 | * see github.com/mobz/mock-ajax
22 | */
23 | (function() {
24 | var version = "1.2";
25 |
26 | var defaultAction = {
27 | req: { }, // always matches
28 | res: { status: 404, data: "Mock Error: No matching response to request" }
29 | };
30 |
31 | var actionCache, // maps requests to responses
32 | responseQueue, // array of responses awaiting delivery
33 | timerQueue; // array of timers waiting to respond
34 |
35 | function MockXHR() { // Mock XMLHttpRequest constructor
36 | this._action; // the matching record in the actionCache
37 | this._sig; // signature for this request
38 |
39 | this.readyState = 0;
40 | this.status = 0;
41 | }
42 |
43 | MockXHR.prototype = {
44 | _removeFromResponseQueue: function() {
45 | for(var i=0; i < responseQueue.length; i++) {
46 | if(responseQueue[i] === this) {
47 | responseQueue.splice(i, 1);
48 | return true;
49 | }
50 | }
51 | },
52 | _respond: function() {
53 | var res = this._response;
54 | this.status = res.status || 200;
55 | this.responseXML = res.data || null;
56 | this.responseText = res.data || "";
57 | this._removeFromResponseQueue();
58 | this._readystatechange(4);
59 | },
60 | _readystatechange: function(state) {
61 | this.readyState = state;
62 | if(this.onreadystatechange) {
63 | this.onreadystatechange.call(null);
64 | }
65 | if( state === 4 && this.onload ) {
66 | this.onload.call(null);
67 | }
68 | },
69 | open: function(pmethod, purl, pasync, pusername, ppassword) {
70 | this._async = pasync !== false;
71 | this._sig = { method: pmethod, url: purl, async: this._async, username: pusername || null, password: ppassword || null, headers: {} };
72 | this._readystatechange(1);
73 | },
74 | send: function(data) {
75 | var sig = this._sig;
76 | sig.data = data;
77 |
78 | actions: for(var i = actionCache.length - 1; i >= 0; i--) {
79 | var req = actionCache[i].req;
80 | for(var param in req) {
81 | if(Object.hasOwnProperty.call(req, param) && (param in sig)) {
82 | if(! req[param].matches(sig[param])) {
83 | continue actions;
84 | }
85 | }
86 | }
87 |
88 | this._action = actionCache[i];
89 |
90 | this._response = (typeof this._action.res === 'function') ? this._action.res(sig, this) : this._action.res;
91 |
92 | // serialise objects if needed
93 | if ((this._response.type === undefined || this._response.type === "json") && typeof this._response.data !== "string" ) {
94 | if (JSON && JSON.stringify) {
95 | this._response.data = JSON.stringify(this._response.data);
96 | } else {
97 | throw "JSON required for in-line serialisation, but not available";
98 | }
99 | }
100 |
101 | responseQueue.push(this);
102 |
103 | break;
104 | }
105 |
106 | // if it is a synch request respond immediatly
107 | if(!this._async) {
108 | this._respond();
109 | }
110 | },
111 | abort: function() {
112 | if(this._action) {
113 | this._removeFromResponseQueue();
114 | this.readyState = 0; // do not send readystatechange event
115 | } else {
116 | this._readystatechange(0);
117 | }
118 | },
119 | setRequestHeader: function(header, value) {
120 | if(this.readyState !== 1) {
121 | throw "INVALID_STATE_ERR";
122 | } else {
123 | this._sig.headers[header] = value;
124 | }
125 | },
126 | getAllResponseHeaders: function() {
127 | var self = this,
128 | resHeaders = this._response.headers || {},
129 | cannedHeaders = "last-modified,server,content-length,content-type".split(","),
130 | headers = [],
131 | pushHeader = function(h) { headers.push(h + ": " + self.getResponseHeader(h)); };
132 | for(var i = 0; i < cannedHeaders.length; i++) {
133 | pushHeader(cannedHeaders[i]);
134 | }
135 | for(var k in resHeaders) {
136 | if(Object.hasOwnProperty.call(resHeaders, k)) {
137 | pushHeader(k);
138 | }
139 | }
140 | return headers.join("\n\r");
141 | },
142 | getResponseHeader: function(header) {
143 | var res = this._response;
144 | if(res.headers && res.headers[header]) {
145 | return res.headers[header];
146 | }
147 | if(/^last-modified/i.test(header)) {
148 | return "Thu, 01 Jan 1970 00:00:00 GMT";
149 | } else if(/^server/i.test(header)) {
150 | return "MockAjax/"+version;
151 | } else if(/^content-length/i.test(header)) {
152 | return (res.data || "").length.toString();
153 | } else if(/^content-type/i.test(header)) {
154 | switch(res.type) {
155 | case "xml" : return "application/xml";
156 | case "html" : return "text/html";
157 | case "script" : return "text/javascript";
158 | case "text" : return "text/plain";
159 | case "default" : return "*/*";
160 | default : return "application/json";
161 | }
162 | }
163 | return null;
164 | }
165 | }
166 |
167 | var ma = window.MockAjax = {
168 | Integration: {
169 | savedGlobals: {},
170 | // integrates with src lib such that our MockXHR is called rather than the browser implementation
171 | integrateWithSrcLib: function(cx, name) {
172 | cx[name] = function() { return new MockXHR(); };
173 | return this;
174 | },
175 | // integrates with test lib such that whenRequest, request and timeout are in the scope that tests are run
176 | integrateWithTestLib: function(cx) {
177 | cx.whenRequest = ma.whenRequest;
178 | cx.respond = ma.respond;
179 | cx.respondAll = ma.respondAll;
180 | cx.timeout = ma.timeout;
181 | return this;
182 | },
183 | stealTimers: function(cx) {
184 | this.savedGlobals.setTimeout = cx.setTimeout;
185 | this.savedGlobals.clearTimeout = cx.clearTimeout;
186 | cx.setTimeout = function(f, t) {
187 | timerQueue.push(f);
188 | return timerQueue.length - 1;
189 | };
190 | cx.clearTimeout = function(i) {
191 | if(timerQueue[i]) {
192 | timerQueue[i] = null;
193 | }
194 | }
195 | return this;
196 | },
197 | returnTimers: function(cx) {
198 | cx.setTimeout = this.savedGlobals.setTimeout;
199 | cx.clearTimeout = this.savedGlobals.clearTimeout;
200 | return this;
201 | },
202 | jasmine: function() { return this.integrateWithTestLib(window); },
203 | JsTestDriver: function() { return this.integrateWithTestLib(window); },
204 | JsUnitTest: function() { return this.integrateWithTestLib(JsUnitTest.Unit.Testcase.prototype); },
205 | jsUnity: function() { return this.integrateWithTestLib(jsUnity.env.defaultScope); },
206 | QUnit: function() { return this.integrateWithTestLib(window); },
207 | Rhino: function() { return this.integrateWithTestLib(window); },
208 | YUITest: function() { return this.integrateWithTestLib(window); },
209 | screwunit: function() { return this.integrateWithTestLib(Screw.Matchers); },
210 |
211 | jQuery: function(cx) {
212 | this.stealTimers(window);
213 | this.integrateWithSrcLib(( cx || jQuery || $).ajaxSettings, "xhr");
214 | return this;
215 | },
216 | Prototype: function(cx) {
217 | this.stealTimers(window); // prototypejs does not appear to handle request timeouts natively
218 | this.integrateWithSrcLib( cx || Ajax, "getTransport" );
219 | return this;
220 | },
221 | Zepto: function() {
222 | this.stealTimers(window);
223 | window.XMLHttpRequest = MockXHR;
224 | return this;
225 | }
226 | },
227 | whenRequest: function(req) {
228 | var action = { req: req, res: {} };
229 | actionCache.push(action);
230 | return {
231 | thenRespond: function(res) {
232 | action.res = res;
233 | }
234 | };
235 | },
236 | respond: function(n) {
237 | responseQueue[n || 0]._respond();
238 | },
239 | respondAll: function() {
240 | while (responseQueue.length > 0) {
241 | this.respond();
242 | };
243 | },
244 | timeout: function() {
245 | for(var i = 0; i < timerQueue.length; i++) {
246 | if(timerQueue[i]) {
247 | timerQueue[i].call(null)
248 | timerQueue[i] = null;
249 | break;
250 | }
251 | }
252 | },
253 | reset: function() {
254 | actionCache = [ defaultAction ];
255 | responseQueue = [];
256 | timerQueue = [];
257 | }
258 | };
259 |
260 | ma.reset();
261 | })();
262 |
--------------------------------------------------------------------------------
/test/JsTestDriver.conf:
--------------------------------------------------------------------------------
1 | server: http://127.0.0.1:4224
2 |
3 | load:
4 | - ../src/*.js
5 | - ../lib/*.js
6 | - ../test/test-plugins.js
7 | - ../test/mockAjax-test.js
--------------------------------------------------------------------------------
/test/JsTestDriver.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mobz/mock-ajax/bff346f7182ebe25a3b94a23959715ef24dbf49f/test/JsTestDriver.jar
--------------------------------------------------------------------------------
/test/mockAjax-test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * this is a test that tests mockAjax, NOT an example of how to use mockAjax in real life
3 | * Note that JsHamcrest is so awesome that I used in both as the tests assert library, AND inside the code under test
4 | */
5 |
6 | MockAjaxTest = TestCase("mockAjax");
7 |
8 | MockAjaxTest.prototype = {
9 | testMockAjax: function() {
10 | assertThat(window, hasMember("MockAjax"), "MockAjax exists fail");
11 | },
12 | testIntegration: function() {
13 | assertThat(window.MockAjax, hasMember("Integration"), "MockAjax has an Integration binder fail");
14 | var cx = {};
15 | MockAjax.Integration.integrateWithSrcLib(cx, "xhr");
16 | assertThat(cx, hasFunction("xhr"), "integrateWithSrcLib fail");
17 | MockAjax.Integration.integrateWithTestLib(cx);
18 | assertThat(cx, hasFunction("whenRequest"), "integrateWithTestLib fail");
19 | assertThat(cx, hasFunction("respond"), "respond function integrated with test lib fail");
20 | assertThat(cx, hasFunction("respondAll"), "respondAll function integrated with test lib fail");
21 | assertThat(cx, hasFunction("timeout"), "timeout function integrated with test lib fail");
22 | },
23 | testReadyStateChange: function() {
24 | MockAjax.reset();
25 | var eventRS;
26 | var cx = {};
27 | MockAjax.Integration.integrateWithSrcLib(cx, "xhr");
28 | var xhr = cx.xhr();
29 | xhr.onreadystatechange = function() { eventRS = xhr.readyState; };
30 | assertThat(xhr.readyState, is(0), "readyState intitially 0 fail");
31 | xhr.open("GET", "/articles/1", false);
32 | assertThat(xhr.readyState, is(1), "readyState after open is 1 fail");
33 | assertThat(eventRS, is(1), "onreadystatechange fires after open fail");
34 | xhr.send();
35 | assertThat(xhr.readyState, is(4), "readyState after sync send is 4 fail");
36 | assertThat(eventRS, is(4), "onreadystatechange fires 4 after sync send fail");
37 | },
38 | testAsyncReadyStateChange: function() {
39 | MockAjax.reset();
40 | var eventRS;
41 | var cx = {};
42 | MockAjax.Integration.integrateWithSrcLib(cx, "xhr");
43 | var xhr = cx.xhr();
44 | xhr.onreadystatechange = function() { eventRS = xhr.readyState; };
45 | assertThat(xhr.readyState, is(0), "readyState intitially 0 fail");
46 | xhr.open("GET", "/articles/1", true);
47 | assertThat(xhr.readyState, is(1), "readyState after open is 1 fail");
48 | assertThat(eventRS, is(1), "onreadystatechange fires after open fail");
49 | xhr.send();
50 | MockAjax.respond();
51 | assertThat(xhr.readyState, is(4), "readyState after sync send is 4 fail");
52 | assertThat(eventRS, is(4), "onreadystatechange fires 4 after sync send fail");
53 | },
54 | testHamcrestMatching: function() {
55 | var lastStatus, lastData;
56 | var q = { ajax: function(url) {
57 | var xhr = this.xhr();
58 | xhr.open("GET", url, false); // don't use async
59 | xhr.onreadystatechange = function() {
60 | if(xhr.readyState === 4) {
61 | lastStatus = xhr.status;
62 | lastData = xhr.responseText;
63 | }
64 | };
65 | xhr.send();
66 | } };
67 | MockAjax.Integration.integrateWithSrcLib(q, "xhr");
68 |
69 | MockAjax.reset();
70 | MockAjax
71 | .whenRequest({ url: startsWith("/good") })
72 | .thenRespond({ status: 200, data: '200 - all good' });
73 | MockAjax
74 | .whenRequest({ url: startsWith("/auth") })
75 | .thenRespond({ status: 401, data: '401 - auth required!' });
76 | MockAjax
77 | .whenRequest({ url: startsWith("/error") })
78 | .thenRespond({ status: 500, data: '500 - internal server error' });
79 | q.ajax("/good");
80 | assertThat(lastStatus, is(200), "found 200 url");
81 | q.ajax("/auth");
82 | assertThat(lastStatus, is(401), "found 401 url");
83 | q.ajax("/error");
84 | assertThat(lastStatus, is(500), "found 500 url");
85 | q.ajax("/somethingelse");
86 | assertThat(lastStatus, is(404), "found 404 url");
87 | },
88 | testComplexMatching: function() {
89 | var lastStatus, lastData;
90 | var q = { ajax: function(method, url, username, password) {
91 | var xhr = this.xhr();
92 | xhr.open(method, url, false, username, password); // don't use async
93 | xhr.onreadystatechange = function() {
94 | if(xhr.readyState === 4) {
95 | lastStatus = xhr.status;
96 | lastData = xhr.responseText;
97 | }
98 | };
99 | xhr.send();
100 | } };
101 | MockAjax.Integration.integrateWithSrcLib(q, "xhr");
102 |
103 | MockAjax.reset();
104 | MockAjax
105 | .whenRequest({ method: is("HEAD"), url: startsWith("/good") })
106 | .thenRespond({ data: '' });
107 | MockAjax
108 | .whenRequest({ method: is("GET"), url: startsWith("/good") })
109 | .thenRespond({ data: "200-ok" });
110 | MockAjax
111 | .whenRequest({ username: is("me"), password: is("secret") })
112 | .thenRespond({ data: "auth-ok" });
113 | MockAjax
114 | .whenRequest({ method: anyOf(is("GET"), is("POST")), url: endsWith("&pretty=true") })
115 | .thenRespond({ data: "pretty-print" });
116 | MockAjax
117 | .whenRequest({ method: anyOf("PUT", "DELETE"), url: matches(/blog\/\d+/), username: is("admin") })
118 | .thenRespond({ data: "manipulate-ok" });
119 |
120 | q.ajax("HEAD", "/good?foo=bar");
121 | assertThat(lastData, equalTo(""), "matches multiple params fail");
122 | q.ajax("GET", "/good/1");
123 | assertThat(lastData, equalTo("200-ok"));
124 | q.ajax("GET", "/good/1");
125 | assertThat(lastData, equalTo("200-ok"), "reuse request fail");
126 | q.ajax("POST", "/good");
127 | assertThat(lastStatus, is(404), "should match all matchers fail");
128 | q.ajax("GET", "/something/else?foo=bar&pretty=true");
129 | assertThat(lastData, equalTo("pretty-print"), "end matching with anyOf fail");
130 | q.ajax("PUT", "/my/blog/143", "admin");
131 | assertThat(lastData, equalTo("manipulate-ok"), "3 matchers and regexp fail");
132 | },
133 | testJQueryIntegration: function() {
134 | MockAjax.reset();
135 | MockAjax.Integration.jQuery($);
136 |
137 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
138 | $.ajaxSetup({
139 | context: cx,
140 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
141 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
142 | complete: function(x) { this.callbacks.push("complete"); }
143 | });
144 |
145 | MockAjax.whenRequest({ url: equalTo("/bar") }).thenRespond({ data: '{"foo":"bar"}' });
146 |
147 | cx.reset();
148 | $.ajax({ url: "/foo" });
149 | MockAjax.respond();
150 | assertThat(cx.callbacks.join("|"), equalTo("error=error,404|complete"), "jquery correctly generates error fail");
151 |
152 | cx.reset();
153 | $.ajax({ url: "/bar" });
154 | MockAjax.respond();
155 | assertThat(cx.callbacks.join("|"), equalTo("success=200|complete"), "jquery correctly generates success fail:");
156 | assertThat(cx.data, hasMember("foo"), "jquery correctly processes json response");
157 |
158 | // test timeout behaviour
159 | cx.reset();
160 | $.ajax({ url: "/bar", timeout: 60000 });
161 | MockAjax.timeout();
162 | assertThat(cx.callbacks.join("|"), equalTo("error=timeout,0|complete"), "jquery timeout request fail");
163 |
164 | cx.reset();
165 | $.ajax({ url: "/bar", timeout: 60000 });
166 | MockAjax.respond();
167 | assertThat(cx.callbacks.join("|"), equalTo("success=200|complete"), "jquery did not timeout request fail");
168 |
169 | // test out of order responses
170 | MockAjax.reset();
171 | cx.reset();
172 | MockAjax.whenRequest({ url: is("/foo") }).thenRespond({ data: '{"d":"foo"}' });
173 | MockAjax.whenRequest({ url: is("/bar") }).thenRespond({ data: '{"d":"bar"}' });
174 | MockAjax.whenRequest({ url: is("/baz") }).thenRespond({ data: '{"d":"baz"}' });
175 |
176 | // fire off three simultaneous async requests
177 | $.ajax({ url: "/foo" });
178 | $.ajax({ url: "/bar" });
179 | $.ajax({ url: "/baz" });
180 |
181 | MockAjax.respond(1); // responds with the inflight request in array index 1 (the middle of 3 requests)
182 | assertThat(cx.data.d, equalTo("bar"), "middle request returned first fail");
183 | MockAjax.respond(1); // responds with the inflight request in array index 1 (the last of 2 requests)
184 | assertThat(cx.data.d, equalTo("baz"), "last request returned next fail");
185 | MockAjax.respond(0); // responds with the final inflight request
186 | assertThat(cx.data.d, equalTo("foo"), "first request returned last fail");
187 | },
188 | testReverseIntegration: function() {
189 | MockAjax.Integration.returnTimers(window);
190 | },
191 | testJSObjectResponse: function() {
192 | MockAjax.reset();
193 | MockAjax.Integration.jQuery($);
194 |
195 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
196 | $.ajaxSetup({
197 | context: cx,
198 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
199 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
200 | complete: function(x) { this.callbacks.push("complete"); }
201 | });
202 |
203 | MockAjax.whenRequest({ url: equalTo("/bar") }).thenRespond({ data: {foo:"bar"} });
204 |
205 | cx.reset();
206 | $.ajax({ url: "/foo" });
207 | MockAjax.respond();
208 | assertThat(cx.callbacks.join("|"), equalTo("error=error,404|complete"), "jquery correctly generates error fail");
209 |
210 | cx.reset();
211 | $.ajax({ url: "/bar" });
212 | MockAjax.respond();
213 | assertThat(cx.callbacks.join("|"), equalTo("success=200|complete"), "jquery correctly generates success fail:");
214 | assertThat(cx.data, hasMember("foo"), "jquery correctly processes json response");
215 | assertThat(cx.data.foo, is("bar"), "mockAjax correctly processes JavaScript Object response");
216 | },
217 | testDynamicResponses: function() {
218 | MockAjax.reset();
219 | MockAjax.Integration.jQuery($)
220 |
221 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
222 | $.ajaxSetup({
223 | context: cx,
224 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
225 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
226 | complete: function(x) { this.callbacks.push("complete"); }
227 | });
228 |
229 | MockAjax.whenRequest({ method: is("GET") })
230 | .thenRespond( function( request ) {
231 | return {
232 | status: 200,
233 | type: "text",
234 | data: [ request.method, request.url ].join( "|" )
235 | };
236 | });
237 | MockAjax.whenRequest({ method: is("POST") })
238 | .thenRespond({ status: 500, type: "text", data: "it's an error!"});
239 |
240 | $.ajax({ type: "GET", url: "/a/b/c" });
241 | $.ajax({ type: "GET", url: "/d/e/f" });
242 | $.ajax({ type: "POST", url: "/g/h/i" });
243 | $.ajax({ type: "GET", url: "/j/k/l" }); // set up 4 inflight requests, 3 with the same dynamic result
244 |
245 | cx.reset();
246 | MockAjax.respond(1);
247 | assertThat(cx.callbacks.join("|"), equalTo("success=200|complete"), "default values picked up from function response");
248 | assertThat(cx.data, is("GET|/d/e/f"), "second response succeeded with correct data");
249 |
250 | cx.reset();
251 | MockAjax.respondAll();
252 | assertThat(cx.callbacks.join("|"), equalTo("success=200|complete|error=error,500|complete|success=200|complete"), "mixing dynamic and static results");
253 | assertThat(cx.data, is("GET|/j/k/l"), "data from final dynamic result in cx");
254 | },
255 | testResponseWithFunctionJSON: function() {
256 | MockAjax.reset();
257 | MockAjax.Integration.jQuery($);
258 |
259 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
260 | $.ajaxSetup({
261 | context: cx,
262 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
263 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
264 | complete: function(x) { this.callbacks.push("complete"); }
265 | });
266 |
267 | var callNum = 0;
268 |
269 | MockAjax.whenRequest({ url: equalTo("/bar") }).thenRespond(function(settings) {
270 | callNum += 1;
271 | return { data: {foo: callNum} };
272 | });
273 |
274 | var i=0;
275 | cx.reset();
276 |
277 | for (i; i< 5; i+=1) {
278 | $.ajax({ url: "/bar" });
279 | MockAjax.respond();
280 | assertThat(cx.data, hasMember("foo"), "jquery correctly processes json response");
281 | assertThat(cx.data.foo, is(i+1), "mockAjax correctly processes dynamic response");
282 | }
283 | },
284 | testResponseWithFunctionWithNonDefaultType: function() {
285 | MockAjax.reset();
286 | MockAjax.Integration.jQuery($);
287 |
288 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
289 | $.ajaxSetup({
290 | context: cx,
291 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
292 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
293 | complete: function(x) { this.callbacks.push("complete"); }
294 | });
295 |
296 | var callNum = 0;
297 |
298 | MockAjax.whenRequest({ url: equalTo("/bar/html") }).thenRespond(function(settings) {
299 | callNum += 1;
300 | return {
301 | type : "html",
302 | data: "Page number " + callNum + "
"
303 | };
304 | });
305 |
306 | var i=0;
307 | cx.reset();
308 |
309 | for (i; i< 5; i+=1) {
310 | $.ajax({ url: "/bar/html" });
311 | MockAjax.respond();
312 | assertThat(cx.data, is("Page number " + (i+1) + "
"), "mockAjax correctly processes dynamic html response");
313 | }
314 | },
315 | testResponseWithFunctionWithNonDefaultStatus: function() {
316 | MockAjax.reset();
317 | MockAjax.Integration.jQuery($);
318 |
319 | var cx = { reset: function() { this.data = null; this.callbacks = []; } };
320 | $.ajaxSetup({
321 | context: cx,
322 | success: function(d,s,x) { this.data = d; this.callbacks.push("success="+x.status); },
323 | error: function(x,e) { this.callbacks.push("error="+e+","+x.status); },
324 | complete: function(x) { this.callbacks.push("complete"); }
325 | });
326 |
327 | MockAjax.whenRequest({ url: equalTo("/bar/internalError") }).thenRespond(function(settings) {
328 | return {
329 | status: 500
330 | };
331 | });
332 |
333 | cx.reset();
334 | $.ajax({ url: "/bar/internalError" });
335 |
336 | MockAjax.respond();
337 | assertThat(cx.callbacks.join("|"), equalTo("error=error,500|complete"), "jquery correctly generates error fail");
338 | }
339 | };
--------------------------------------------------------------------------------
/test/test-plugins.js:
--------------------------------------------------------------------------------
1 | JsHamcrest.Integration.JsTestDriver();
--------------------------------------------------------------------------------