├── 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(); --------------------------------------------------------------------------------