1 /* 2 * QUnit - A JavaScript Unit Testing Framework 3 * 4 * http://docs.jquery.com/QUnit 5 * 6 * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 * Dual licensed under the MIT (MIT-LICENSE.txt) 8 * or GPL (GPL-LICENSE.txt) licenses. 9 */ 10 11 (function(window) { 12 13 var defined = { 14 setTimeout: typeof window.setTimeout !== "undefined", 15 sessionStorage: (function() { 16 try { 17 return !!sessionStorage.getItem; 18 } catch(e){ 19 return false; 20 } 21 })() 22 }; 23 24 var testId = 0; 25 26 var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 this.name = name; 28 this.testName = testName; 29 this.expected = expected; 30 this.testEnvironmentArg = testEnvironmentArg; 31 this.async = async; 32 this.callback = callback; 33 this.assertions = []; 34 }; 35 Test.prototype = { 36 init: function() { 37 var tests = id("qunit-tests"); 38 if (tests) { 39 var b = document.createElement("strong"); 40 b.innerHTML = "Running " + this.name; 41 var li = document.createElement("li"); 42 li.appendChild( b ); 43 li.className = "running"; 44 li.id = this.id = "test-output" + testId++; 45 tests.appendChild( li ); 46 } 47 }, 48 setup: function() { 49 if (this.module != config.previousModule) { 50 if ( config.previousModule ) { 51 QUnit.moduleDone( { 52 name: config.previousModule, 53 failed: config.moduleStats.bad, 54 passed: config.moduleStats.all - config.moduleStats.bad, 55 total: config.moduleStats.all 56 } ); 57 } 58 config.previousModule = this.module; 59 config.moduleStats = { all: 0, bad: 0 }; 60 QUnit.moduleStart( { 61 name: this.module 62 } ); 63 } 64 65 config.current = this; 66 this.testEnvironment = extend({ 67 setup: function() {}, 68 teardown: function() {} 69 }, this.moduleTestEnvironment); 70 if (this.testEnvironmentArg) { 71 extend(this.testEnvironment, this.testEnvironmentArg); 72 } 73 74 QUnit.testStart( { 75 name: this.testName 76 } ); 77 78 // allow utility functions to access the current test environment 79 // TODO why?? 80 QUnit.current_testEnvironment = this.testEnvironment; 81 82 try { 83 if ( !config.pollution ) { 84 saveGlobal(); 85 } 86 87 this.testEnvironment.setup.call(this.testEnvironment); 88 } catch(e) { 89 QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 90 } 91 }, 92 run: function() { 93 if ( this.async ) { 94 QUnit.stop(); 95 } 96 97 if ( config.notrycatch ) { 98 this.callback.call(this.testEnvironment); 99 return; 100 } 101 try { 102 this.callback.call(this.testEnvironment); 103 } catch(e) { 104 fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 105 QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 106 // else next test will carry the responsibility 107 saveGlobal(); 108 109 // Restart the tests if they're blocking 110 if ( config.blocking ) { 111 start(); 112 } 113 } 114 }, 115 teardown: function() { 116 try { 117 checkPollution(); 118 this.testEnvironment.teardown.call(this.testEnvironment); 119 } catch(e) { 120 QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 121 } 122 }, 123 finish: function() { 124 if ( this.expected && this.expected != this.assertions.length ) { 125 QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 126 } 127 128 var good = 0, bad = 0, 129 tests = id("qunit-tests"); 130 131 config.stats.all += this.assertions.length; 132 config.moduleStats.all += this.assertions.length; 133 134 if ( tests ) { 135 var ol = document.createElement("ol"); 136 137 for ( var i = 0; i < this.assertions.length; i++ ) { 138 var assertion = this.assertions[i]; 139 140 var li = document.createElement("li"); 141 li.className = assertion.result ? "pass" : "fail"; 142 li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 143 ol.appendChild( li ); 144 145 if ( assertion.result ) { 146 good++; 147 } else { 148 bad++; 149 config.stats.bad++; 150 config.moduleStats.bad++; 151 } 152 } 153 154 // store result when possible 155 if ( QUnit.config.reorder && defined.sessionStorage ) { 156 if (bad) { 157 sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad) 158 } else { 159 sessionStorage.removeItem("qunit-" + this.testName); 160 } 161 } 162 163 if (bad == 0) { 164 ol.style.display = "none"; 165 } 166 167 var b = document.createElement("strong"); 168 b.innerHTML = this.name + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>"; 169 170 var a = document.createElement("a"); 171 a.innerHTML = "Rerun"; 172 a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 173 174 addEvent(b, "click", function() { 175 var next = b.nextSibling.nextSibling, 176 display = next.style.display; 177 next.style.display = display === "none" ? "block" : "none"; 178 }); 179 180 addEvent(b, "dblclick", function(e) { 181 var target = e && e.target ? e.target : window.event.srcElement; 182 if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 183 target = target.parentNode; 184 } 185 if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 186 window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 187 } 188 }); 189 190 var li = id(this.id); 191 li.className = bad ? "fail" : "pass"; 192 li.removeChild( li.firstChild ); 193 li.appendChild( b ); 194 li.appendChild( a ); 195 li.appendChild( ol ); 196 197 } else { 198 for ( var i = 0; i < this.assertions.length; i++ ) { 199 if ( !this.assertions[i].result ) { 200 bad++; 201 config.stats.bad++; 202 config.moduleStats.bad++; 203 } 204 } 205 } 206 207 try { 208 QUnit.reset(); 209 } catch(e) { 210 fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 211 } 212 213 QUnit.testDone( { 214 name: this.testName, 215 failed: bad, 216 passed: this.assertions.length - bad, 217 total: this.assertions.length 218 } ); 219 }, 220 221 queue: function() { 222 var test = this; 223 synchronize(function() { 224 test.init(); 225 }); 226 function run() { 227 // each of these can by async 228 synchronize(function() { 229 test.setup(); 230 }); 231 synchronize(function() { 232 test.run(); 233 }); 234 synchronize(function() { 235 test.teardown(); 236 }); 237 synchronize(function() { 238 test.finish(); 239 }); 240 } 241 // defer when previous test run passed, if storage is available 242 var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 243 if (bad) { 244 run(); 245 } else { 246 synchronize(run); 247 }; 248 } 249 250 }; 251 252 var QUnit = { 253 254 // call on start of module test to prepend name to all tests 255 module: function(name, testEnvironment) { 256 config.currentModule = name; 257 config.currentModuleTestEnviroment = testEnvironment; 258 }, 259 260 asyncTest: function(testName, expected, callback) { 261 if ( arguments.length === 2 ) { 262 callback = expected; 263 expected = 0; 264 } 265 266 QUnit.test(testName, expected, callback, true); 267 }, 268 269 test: function(testName, expected, callback, async) { 270 var name = '<span class="test-name">' + testName + '</span>', testEnvironmentArg; 271 272 if ( arguments.length === 2 ) { 273 callback = expected; 274 expected = null; 275 } 276 // is 2nd argument a testEnvironment? 277 if ( expected && typeof expected === 'object') { 278 testEnvironmentArg = expected; 279 expected = null; 280 } 281 282 if ( config.currentModule ) { 283 name = '<span class="module-name">' + config.currentModule + "</span>: " + name; 284 } 285 286 if ( !validTest(config.currentModule + ": " + testName) ) { 287 return; 288 } 289 290 var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 291 test.module = config.currentModule; 292 test.moduleTestEnvironment = config.currentModuleTestEnviroment; 293 test.queue(); 294 }, 295 296 /** 297 * Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 298 */ 299 expect: function(asserts) { 300 config.current.expected = asserts; 301 }, 302 303 /** 304 * Asserts true. 305 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 306 */ 307 ok: function(a, msg) { 308 a = !!a; 309 var details = { 310 result: a, 311 message: msg 312 }; 313 msg = escapeHtml(msg); 314 QUnit.log(details); 315 config.current.assertions.push({ 316 result: a, 317 message: msg 318 }); 319 }, 320 321 /** 322 * Checks that the first two arguments are equal, with an optional message. 323 * Prints out both actual and expected values. 324 * 325 * Preferred to ok( actual == expected, message ) 326 * 327 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 328 * 329 * @param Object actual 330 * @param Object expected 331 * @param String message (optional) 332 */ 333 equal: function(actual, expected, message) { 334 QUnit.push(expected == actual, actual, expected, message); 335 }, 336 337 notEqual: function(actual, expected, message) { 338 QUnit.push(expected != actual, actual, expected, message); 339 }, 340 341 deepEqual: function(actual, expected, message) { 342 QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 343 }, 344 345 notDeepEqual: function(actual, expected, message) { 346 QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 347 }, 348 349 strictEqual: function(actual, expected, message) { 350 QUnit.push(expected === actual, actual, expected, message); 351 }, 352 353 notStrictEqual: function(actual, expected, message) { 354 QUnit.push(expected !== actual, actual, expected, message); 355 }, 356 357 raises: function(block, expected, message) { 358 var actual, ok = false; 359 360 if (typeof expected === 'string') { 361 message = expected; 362 expected = null; 363 } 364 365 try { 366 block(); 367 } catch (e) { 368 actual = e; 369 } 370 371 if (actual) { 372 // we don't want to validate thrown error 373 if (!expected) { 374 ok = true; 375 // expected is a regexp 376 } else if (QUnit.objectType(expected) === "regexp") { 377 ok = expected.test(actual); 378 // expected is a constructor 379 } else if (actual instanceof expected) { 380 ok = true; 381 // expected is a validation function which returns true is validation passed 382 } else if (expected.call({}, actual) === true) { 383 ok = true; 384 } 385 } 386 387 QUnit.ok(ok, message); 388 }, 389 390 start: function() { 391 config.semaphore--; 392 if (config.semaphore > 0) { 393 // don't start until equal number of stop-calls 394 return; 395 } 396 if (config.semaphore < 0) { 397 // ignore if start is called more often then stop 398 config.semaphore = 0; 399 } 400 // A slight delay, to avoid any current callbacks 401 if ( defined.setTimeout ) { 402 window.setTimeout(function() { 403 if ( config.timeout ) { 404 clearTimeout(config.timeout); 405 } 406 407 config.blocking = false; 408 process(); 409 }, 13); 410 } else { 411 config.blocking = false; 412 process(); 413 } 414 }, 415 416 stop: function(timeout) { 417 config.semaphore++; 418 config.blocking = true; 419 420 if ( timeout && defined.setTimeout ) { 421 clearTimeout(config.timeout); 422 config.timeout = window.setTimeout(function() { 423 QUnit.ok( false, "Test timed out" ); 424 QUnit.start(); 425 }, timeout); 426 } 427 } 428 }; 429 430 // Backwards compatibility, deprecated 431 QUnit.equals = QUnit.equal; 432 QUnit.same = QUnit.deepEqual; 433 434 // Maintain internal state 435 var config = { 436 // The queue of tests to run 437 queue: [], 438 439 // block until document ready 440 blocking: true, 441 442 // by default, run previously failed tests first 443 // very useful in combination with "Hide passed tests" checked 444 reorder: true, 445 446 noglobals: false, 447 notrycatch: false 448 }; 449 450 // Load paramaters 451 (function() { 452 var location = window.location || { search: "", protocol: "file:" }, 453 params = location.search.slice( 1 ).split( "&" ), 454 length = params.length, 455 urlParams = {}, 456 current; 457 458 if ( params[ 0 ] ) { 459 for ( var i = 0; i < length; i++ ) { 460 current = params[ i ].split( "=" ); 461 current[ 0 ] = decodeURIComponent( current[ 0 ] ); 462 // allow just a key to turn on a flag, e.g., test.html?noglobals 463 current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 464 urlParams[ current[ 0 ] ] = current[ 1 ]; 465 if ( current[ 0 ] in config ) { 466 config[ current[ 0 ] ] = current[ 1 ]; 467 } 468 } 469 } 470 471 QUnit.urlParams = urlParams; 472 config.filter = urlParams.filter; 473 474 // Figure out if we're running the tests from a server or not 475 QUnit.isLocal = !!(location.protocol === 'file:'); 476 })(); 477 478 // Expose the API as global variables, unless an 'exports' 479 // object exists, in that case we assume we're in CommonJS 480 if ( typeof exports === "undefined" || typeof require === "undefined" ) { 481 extend(window, QUnit); 482 window.QUnit = QUnit; 483 } else { 484 extend(exports, QUnit); 485 exports.QUnit = QUnit; 486 } 487 488 // define these after exposing globals to keep them in these QUnit namespace only 489 extend(QUnit, { 490 config: config, 491 492 // Initialize the configuration options 493 init: function() { 494 extend(config, { 495 stats: { all: 0, bad: 0 }, 496 moduleStats: { all: 0, bad: 0 }, 497 started: +new Date, 498 updateRate: 1000, 499 blocking: false, 500 autostart: true, 501 autorun: false, 502 filter: "", 503 queue: [], 504 semaphore: 0 505 }); 506 507 var tests = id( "qunit-tests" ), 508 banner = id( "qunit-banner" ), 509 result = id( "qunit-testresult" ); 510 511 if ( tests ) { 512 tests.innerHTML = ""; 513 } 514 515 if ( banner ) { 516 banner.className = ""; 517 } 518 519 if ( result ) { 520 result.parentNode.removeChild( result ); 521 } 522 523 if ( tests ) { 524 result = document.createElement( "p" ); 525 result.id = "qunit-testresult"; 526 result.className = "result"; 527 tests.parentNode.insertBefore( result, tests ); 528 result.innerHTML = 'Running...<br/> '; 529 } 530 }, 531 532 /** 533 * Resets the test setup. Useful for tests that modify the DOM. 534 * 535 * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 536 */ 537 reset: function() { 538 if ( window.jQuery ) { 539 jQuery( "#main, #qunit-fixture" ).html( config.fixture ); 540 } else { 541 var main = id( 'main' ) || id( 'qunit-fixture' ); 542 if ( main ) { 543 main.innerHTML = config.fixture; 544 } 545 } 546 }, 547 548 /** 549 * Trigger an event on an element. 550 * 551 * @example triggerEvent( document.body, "click" ); 552 * 553 * @param DOMElement elem 554 * @param String type 555 */ 556 triggerEvent: function( elem, type, event ) { 557 if ( document.createEvent ) { 558 event = document.createEvent("MouseEvents"); 559 event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 560 0, 0, 0, 0, 0, false, false, false, false, 0, null); 561 elem.dispatchEvent( event ); 562 563 } else if ( elem.fireEvent ) { 564 elem.fireEvent("on"+type); 565 } 566 }, 567 568 // Safe object type checking 569 is: function( type, obj ) { 570 return QUnit.objectType( obj ) == type; 571 }, 572 573 objectType: function( obj ) { 574 if (typeof obj === "undefined") { 575 return "undefined"; 576 577 // consider: typeof null === object 578 } 579 if (obj === null) { 580 return "null"; 581 } 582 583 var type = Object.prototype.toString.call( obj ) 584 .match(/^\[object\s(.*)\]$/)[1] || ''; 585 586 switch (type) { 587 case 'Number': 588 if (isNaN(obj)) { 589 return "nan"; 590 } else { 591 return "number"; 592 } 593 case 'String': 594 case 'Boolean': 595 case 'Array': 596 case 'Date': 597 case 'RegExp': 598 case 'Function': 599 return type.toLowerCase(); 600 } 601 if (typeof obj === "object") { 602 return "object"; 603 } 604 return undefined; 605 }, 606 607 push: function(result, actual, expected, message) { 608 var details = { 609 result: result, 610 message: message, 611 actual: actual, 612 expected: expected 613 }; 614 615 message = escapeHtml(message) || (result ? "okay" : "failed"); 616 message = '<span class="test-message">' + message + "</span>"; 617 expected = escapeHtml(QUnit.jsDump.parse(expected)); 618 actual = escapeHtml(QUnit.jsDump.parse(actual)); 619 var output = message + '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + expected + '</pre></td></tr>'; 620 if (actual != expected) { 621 output += '<tr class="test-actual"><th>Result: </th><td><pre>' + actual + '</pre></td></tr>'; 622 output += '<tr class="test-diff"><th>Diff: </th><td><pre>' + QUnit.diff(expected, actual) +'</pre></td></tr>'; 623 } 624 if (!result) { 625 var source = sourceFromStacktrace(); 626 if (source) { 627 details.source = source; 628 output += '<tr class="test-source"><th>Source: </th><td><pre>' + source +'</pre></td></tr>'; 629 } 630 } 631 output += "</table>"; 632 633 QUnit.log(details); 634 635 config.current.assertions.push({ 636 result: !!result, 637 message: output 638 }); 639 }, 640 641 url: function( params ) { 642 params = extend( extend( {}, QUnit.urlParams ), params ); 643 var querystring = "?", 644 key; 645 for ( key in params ) { 646 querystring += encodeURIComponent( key ) + "=" + 647 encodeURIComponent( params[ key ] ) + "&"; 648 } 649 return window.location.pathname + querystring.slice( 0, -1 ); 650 }, 651 652 // Logging callbacks; all receive a single argument with the listed properties 653 // run test/logs.html for any related changes 654 begin: function() {}, 655 // done: { failed, passed, total, runtime } 656 done: function() {}, 657 // log: { result, actual, expected, message } 658 log: function() {}, 659 // testStart: { name } 660 testStart: function() {}, 661 // testDone: { name, failed, passed, total } 662 testDone: function() {}, 663 // moduleStart: { name } 664 moduleStart: function() {}, 665 // moduleDone: { name, failed, passed, total } 666 moduleDone: function() {} 667 }); 668 669 if ( typeof document === "undefined" || document.readyState === "complete" ) { 670 config.autorun = true; 671 } 672 673 addEvent(window, "load", function() { 674 QUnit.begin({}); 675 676 // Initialize the config, saving the execution queue 677 var oldconfig = extend({}, config); 678 QUnit.init(); 679 extend(config, oldconfig); 680 681 config.blocking = false; 682 683 var userAgent = id("qunit-userAgent"); 684 if ( userAgent ) { 685 userAgent.innerHTML = navigator.userAgent; 686 } 687 var banner = id("qunit-header"); 688 if ( banner ) { 689 banner.innerHTML = '<a href="' + QUnit.url({ filter: undefined }) + '"> ' + banner.innerHTML + '</a> ' + 690 '<label><input name="noglobals" type="checkbox"' + ( config.noglobals ? ' checked="checked"' : '' ) + '>noglobals</label>' + 691 '<label><input name="notrycatch" type="checkbox"' + ( config.notrycatch ? ' checked="checked"' : '' ) + '>notrycatch</label>'; 692 addEvent( banner, "change", function( event ) { 693 var params = {}; 694 params[ event.target.name ] = event.target.checked ? true : undefined; 695 window.location = QUnit.url( params ); 696 }); 697 } 698 699 var toolbar = id("qunit-testrunner-toolbar"); 700 if ( toolbar ) { 701 var filter = document.createElement("input"); 702 filter.type = "checkbox"; 703 filter.id = "qunit-filter-pass"; 704 addEvent( filter, "click", function() { 705 var ol = document.getElementById("qunit-tests"); 706 if ( filter.checked ) { 707 ol.className = ol.className + " hidepass"; 708 } else { 709 var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 710 ol.className = tmp.replace(/ hidepass /, " "); 711 } 712 if ( defined.sessionStorage ) { 713 if (filter.checked) { 714 sessionStorage.setItem("qunit-filter-passed-tests", "true"); 715 } else { 716 sessionStorage.removeItem("qunit-filter-passed-tests"); 717 } 718 } 719 }); 720 if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 721 filter.checked = true; 722 var ol = document.getElementById("qunit-tests"); 723 ol.className = ol.className + " hidepass"; 724 } 725 toolbar.appendChild( filter ); 726 727 var label = document.createElement("label"); 728 label.setAttribute("for", "qunit-filter-pass"); 729 label.innerHTML = "Hide passed tests"; 730 toolbar.appendChild( label ); 731 } 732 733 var main = id('main') || id('qunit-fixture'); 734 if ( main ) { 735 config.fixture = main.innerHTML; 736 } 737 738 if (config.autostart) { 739 QUnit.start(); 740 } 741 }); 742 743 function done() { 744 config.autorun = true; 745 746 // Log the last module results 747 if ( config.currentModule ) { 748 QUnit.moduleDone( { 749 name: config.currentModule, 750 failed: config.moduleStats.bad, 751 passed: config.moduleStats.all - config.moduleStats.bad, 752 total: config.moduleStats.all 753 } ); 754 } 755 756 var banner = id("qunit-banner"), 757 tests = id("qunit-tests"), 758 runtime = +new Date - config.started, 759 passed = config.stats.all - config.stats.bad, 760 html = [ 761 'Tests completed in ', 762 runtime, 763 ' milliseconds.<br/>', 764 '<span class="passed">', 765 passed, 766 '</span> tests of <span class="total">', 767 config.stats.all, 768 '</span> passed, <span class="failed">', 769 config.stats.bad, 770 '</span> failed.' 771 ].join(''); 772 773 if ( banner ) { 774 banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 775 } 776 777 if ( tests ) { 778 id( "qunit-testresult" ).innerHTML = html; 779 } 780 781 QUnit.done( { 782 failed: config.stats.bad, 783 passed: passed, 784 total: config.stats.all, 785 runtime: runtime 786 } ); 787 } 788 789 function validTest( name ) { 790 var filter = config.filter, 791 run = false; 792 793 if ( !filter ) { 794 return true; 795 } 796 797 not = filter.charAt( 0 ) === "!"; 798 if ( not ) { 799 filter = filter.slice( 1 ); 800 } 801 802 if ( name.indexOf( filter ) !== -1 ) { 803 return !not; 804 } 805 806 if ( not ) { 807 run = true; 808 } 809 810 return run; 811 } 812 813 // so far supports only Firefox, Chrome and Opera (buggy) 814 // could be extended in the future to use something like https://github.com/csnover/TraceKit 815 function sourceFromStacktrace() { 816 try { 817 throw new Error(); 818 } catch ( e ) { 819 if (e.stacktrace) { 820 // Opera 821 return e.stacktrace.split("\n")[6]; 822 } else if (e.stack) { 823 // Firefox, Chrome 824 return e.stack.split("\n")[4]; 825 } 826 } 827 } 828 829 function escapeHtml(s) { 830 if (!s) { 831 return ""; 832 } 833 s = s + ""; 834 return s.replace(/[\&"<>\\]/g, function(s) { 835 switch(s) { 836 case "&": return "&"; 837 case "\\": return "\\\\"; 838 case '"': return '\"'; 839 case "<": return "<"; 840 case ">": return ">"; 841 default: return s; 842 } 843 }); 844 } 845 846 function synchronize( callback ) { 847 config.queue.push( callback ); 848 849 if ( config.autorun && !config.blocking ) { 850 process(); 851 } 852 } 853 854 function process() { 855 var start = (new Date()).getTime(); 856 857 while ( config.queue.length && !config.blocking ) { 858 if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 859 config.queue.shift()(); 860 } else { 861 window.setTimeout( process, 13 ); 862 break; 863 } 864 } 865 if (!config.blocking && !config.queue.length) { 866 done(); 867 } 868 } 869 870 function saveGlobal() { 871 config.pollution = []; 872 873 if ( config.noglobals ) { 874 for ( var key in window ) { 875 config.pollution.push( key ); 876 } 877 } 878 } 879 880 function checkPollution( name ) { 881 var old = config.pollution; 882 saveGlobal(); 883 884 var newGlobals = diff( config.pollution, old ); 885 if ( newGlobals.length > 0 ) { 886 ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 887 } 888 889 var deletedGlobals = diff( old, config.pollution ); 890 if ( deletedGlobals.length > 0 ) { 891 ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 892 } 893 } 894 895 // returns a new Array with the elements that are in a but not in b 896 function diff( a, b ) { 897 var result = a.slice(); 898 for ( var i = 0; i < result.length; i++ ) { 899 for ( var j = 0; j < b.length; j++ ) { 900 if ( result[i] === b[j] ) { 901 result.splice(i, 1); 902 i--; 903 break; 904 } 905 } 906 } 907 return result; 908 } 909 910 function fail(message, exception, callback) { 911 if ( typeof console !== "undefined" && console.error && console.warn ) { 912 console.error(message); 913 console.error(exception); 914 console.warn(callback.toString()); 915 916 } else if ( window.opera && opera.postError ) { 917 opera.postError(message, exception, callback.toString); 918 } 919 } 920 921 function extend(a, b) { 922 for ( var prop in b ) { 923 if ( b[prop] === undefined ) { 924 delete a[prop]; 925 } else { 926 a[prop] = b[prop]; 927 } 928 } 929 930 return a; 931 } 932 933 function addEvent(elem, type, fn) { 934 if ( elem.addEventListener ) { 935 elem.addEventListener( type, fn, false ); 936 } else if ( elem.attachEvent ) { 937 elem.attachEvent( "on" + type, fn ); 938 } else { 939 fn(); 940 } 941 } 942 943 function id(name) { 944 return !!(typeof document !== "undefined" && document && document.getElementById) && 945 document.getElementById( name ); 946 } 947 948 // Test for equality any JavaScript type. 949 // Discussions and reference: http://philrathe.com/articles/equiv 950 // Test suites: http://philrathe.com/tests/equiv 951 // Author: Philippe Rathé <prathe@gmail.com> 952 QUnit.equiv = function () { 953 954 var innerEquiv; // the real equiv function 955 var callers = []; // stack to decide between skip/abort functions 956 var parents = []; // stack to avoiding loops from circular referencing 957 958 // Call the o related callback with the given arguments. 959 function bindCallbacks(o, callbacks, args) { 960 var prop = QUnit.objectType(o); 961 if (prop) { 962 if (QUnit.objectType(callbacks[prop]) === "function") { 963 return callbacks[prop].apply(callbacks, args); 964 } else { 965 return callbacks[prop]; // or undefined 966 } 967 } 968 } 969 970 var callbacks = function () { 971 972 // for string, boolean, number and null 973 function useStrictEquality(b, a) { 974 if (b instanceof a.constructor || a instanceof b.constructor) { 975 // to catch short annotation VS 'new' annotation of a declaration 976 // e.g. var i = 1; 977 // var j = new Number(1); 978 return a == b; 979 } else { 980 return a === b; 981 } 982 } 983 984 return { 985 "string": useStrictEquality, 986 "boolean": useStrictEquality, 987 "number": useStrictEquality, 988 "null": useStrictEquality, 989 "undefined": useStrictEquality, 990 991 "nan": function (b) { 992 return isNaN(b); 993 }, 994 995 "date": function (b, a) { 996 return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 997 }, 998 999 "regexp": function (b, a) { 1000 return QUnit.objectType(b) === "regexp" && 1001 a.source === b.source && // the regex itself 1002 a.global === b.global && // and its modifiers (gmi) ... 1003 a.ignoreCase === b.ignoreCase && 1004 a.multiline === b.multiline; 1005 }, 1006 1007 // - skip when the property is a method of an instance (OOP) 1008 // - abort otherwise, 1009 // initial === would have catch identical references anyway 1010 "function": function () { 1011 var caller = callers[callers.length - 1]; 1012 return caller !== Object && 1013 typeof caller !== "undefined"; 1014 }, 1015 1016 "array": function (b, a) { 1017 var i, j, loop; 1018 var len; 1019 1020 // b could be an object literal here 1021 if ( ! (QUnit.objectType(b) === "array")) { 1022 return false; 1023 } 1024 1025 len = a.length; 1026 if (len !== b.length) { // safe and faster 1027 return false; 1028 } 1029 1030 //track reference to avoid circular references 1031 parents.push(a); 1032 for (i = 0; i < len; i++) { 1033 loop = false; 1034 for(j=0;j<parents.length;j++){ 1035 if(parents[j] === a[i]){ 1036 loop = true;//do not rewalk array 1037 } 1038 } 1039 if (!loop && ! innerEquiv(a[i], b[i])) { 1040 parents.pop(); 1041 return false; 1042 } 1043 } 1044 parents.pop(); 1045 return true; 1046 }, 1047 1048 "object": function (b, a) { 1049 var i, j, loop; 1050 var eq = true; // unless we can proove it 1051 var aProperties = [], bProperties = []; // collection of strings 1052 1053 // comparing constructors is more strict than using instanceof 1054 if ( a.constructor !== b.constructor) { 1055 return false; 1056 } 1057 1058 // stack constructor before traversing properties 1059 callers.push(a.constructor); 1060 //track reference to avoid circular references 1061 parents.push(a); 1062 1063 for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 1064 loop = false; 1065 for(j=0;j<parents.length;j++){ 1066 if(parents[j] === a[i]) 1067 loop = true; //don't go down the same path twice 1068 } 1069 aProperties.push(i); // collect a's properties 1070 1071 if (!loop && ! innerEquiv(a[i], b[i])) { 1072 eq = false; 1073 break; 1074 } 1075 } 1076 1077 callers.pop(); // unstack, we are done 1078 parents.pop(); 1079 1080 for (i in b) { 1081 bProperties.push(i); // collect b's properties 1082 } 1083 1084 // Ensures identical properties name 1085 return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1086 } 1087 }; 1088 }(); 1089 1090 innerEquiv = function () { // can take multiple arguments 1091 var args = Array.prototype.slice.apply(arguments); 1092 if (args.length < 2) { 1093 return true; // end transition 1094 } 1095 1096 return (function (a, b) { 1097 if (a === b) { 1098 return true; // catch the most you can 1099 } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType(a) !== QUnit.objectType(b)) { 1100 return false; // don't lose time with error prone cases 1101 } else { 1102 return bindCallbacks(a, callbacks, [b, a]); 1103 } 1104 1105 // apply transition with (1..n) arguments 1106 })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); 1107 }; 1108 1109 return innerEquiv; 1110 1111 }(); 1112 1113 /** 1114 * jsDump 1115 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 1116 * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) 1117 * Date: 5/15/2008 1118 * @projectDescription Advanced and extensible data dumping for Javascript. 1119 * @version 1.0.0 1120 * @author Ariel Flesler 1121 * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1122 */ 1123 QUnit.jsDump = (function() { 1124 function quote( str ) { 1125 return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1126 }; 1127 function literal( o ) { 1128 return o + ''; 1129 }; 1130 function join( pre, arr, post ) { 1131 var s = jsDump.separator(), 1132 base = jsDump.indent(), 1133 inner = jsDump.indent(1); 1134 if ( arr.join ) 1135 arr = arr.join( ',' + s + inner ); 1136 if ( !arr ) 1137 return pre + post; 1138 return [ pre, inner + arr, base + post ].join(s); 1139 }; 1140 function array( arr ) { 1141 var i = arr.length, ret = Array(i); 1142 this.up(); 1143 while ( i-- ) 1144 ret[i] = this.parse( arr[i] ); 1145 this.down(); 1146 return join( '[', ret, ']' ); 1147 }; 1148 1149 var reName = /^function (\w+)/; 1150 1151 var jsDump = { 1152 parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance 1153 var parser = this.parsers[ type || this.typeOf(obj) ]; 1154 type = typeof parser; 1155 1156 return type == 'function' ? parser.call( this, obj ) : 1157 type == 'string' ? parser : 1158 this.parsers.error; 1159 }, 1160 typeOf:function( obj ) { 1161 var type; 1162 if ( obj === null ) { 1163 type = "null"; 1164 } else if (typeof obj === "undefined") { 1165 type = "undefined"; 1166 } else if (QUnit.is("RegExp", obj)) { 1167 type = "regexp"; 1168 } else if (QUnit.is("Date", obj)) { 1169 type = "date"; 1170 } else if (QUnit.is("Function", obj)) { 1171 type = "function"; 1172 } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1173 type = "window"; 1174 } else if (obj.nodeType === 9) { 1175 type = "document"; 1176 } else if (obj.nodeType) { 1177 type = "node"; 1178 } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { 1179 type = "array"; 1180 } else { 1181 type = typeof obj; 1182 } 1183 return type; 1184 }, 1185 separator:function() { 1186 return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' '; 1187 }, 1188 indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1189 if ( !this.multiline ) 1190 return ''; 1191 var chr = this.indentChar; 1192 if ( this.HTML ) 1193 chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1194 return Array( this._depth_ + (extra||0) ).join(chr); 1195 }, 1196 up:function( a ) { 1197 this._depth_ += a || 1; 1198 }, 1199 down:function( a ) { 1200 this._depth_ -= a || 1; 1201 }, 1202 setParser:function( name, parser ) { 1203 this.parsers[name] = parser; 1204 }, 1205 // The next 3 are exposed so you can use them 1206 quote:quote, 1207 literal:literal, 1208 join:join, 1209 // 1210 _depth_: 1, 1211 // This is the list of parsers, to modify them, use jsDump.setParser 1212 parsers:{ 1213 window: '[Window]', 1214 document: '[Document]', 1215 error:'[ERROR]', //when no parser is found, shouldn't happen 1216 unknown: '[Unknown]', 1217 'null':'null', 1218 'undefined':'undefined', 1219 'function':function( fn ) { 1220 var ret = 'function', 1221 name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1222 if ( name ) 1223 ret += ' ' + name; 1224 ret += '('; 1225 1226 ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1227 return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1228 }, 1229 array: array, 1230 nodelist: array, 1231 arguments: array, 1232 object:function( map ) { 1233 var ret = [ ]; 1234 QUnit.jsDump.up(); 1235 for ( var key in map ) 1236 ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1237 QUnit.jsDump.down(); 1238 return join( '{', ret, '}' ); 1239 }, 1240 node:function( node ) { 1241 var open = QUnit.jsDump.HTML ? '<' : '<', 1242 close = QUnit.jsDump.HTML ? '>' : '>'; 1243 1244 var tag = node.nodeName.toLowerCase(), 1245 ret = open + tag; 1246 1247 for ( var a in QUnit.jsDump.DOMAttrs ) { 1248 var val = node[QUnit.jsDump.DOMAttrs[a]]; 1249 if ( val ) 1250 ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1251 } 1252 return ret + close + open + '/' + tag + close; 1253 }, 1254 functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1255 var l = fn.length; 1256 if ( !l ) return ''; 1257 1258 var args = Array(l); 1259 while ( l-- ) 1260 args[l] = String.fromCharCode(97+l);//97 is 'a' 1261 return ' ' + args.join(', ') + ' '; 1262 }, 1263 key:quote, //object calls it internally, the key part of an item in a map 1264 functionCode:'[code]', //function calls it internally, it's the content of the function 1265 attribute:quote, //node calls it internally, it's an html attribute value 1266 string:quote, 1267 date:quote, 1268 regexp:literal, //regex 1269 number:literal, 1270 'boolean':literal 1271 }, 1272 DOMAttrs:{//attributes to dump from nodes, name=>realName 1273 id:'id', 1274 name:'name', 1275 'class':'className' 1276 }, 1277 HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1278 indentChar:' ',//indentation unit 1279 multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1280 }; 1281 1282 return jsDump; 1283 })(); 1284 1285 // from Sizzle.js 1286 function getText( elems ) { 1287 var ret = "", elem; 1288 1289 for ( var i = 0; elems[i]; i++ ) { 1290 elem = elems[i]; 1291 1292 // Get the text from text nodes and CDATA nodes 1293 if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1294 ret += elem.nodeValue; 1295 1296 // Traverse everything else, except comment nodes 1297 } else if ( elem.nodeType !== 8 ) { 1298 ret += getText( elem.childNodes ); 1299 } 1300 } 1301 1302 return ret; 1303 }; 1304 1305 /* 1306 * Javascript Diff Algorithm 1307 * By John Resig (http://ejohn.org/) 1308 * Modified by Chu Alan "sprite" 1309 * 1310 * Released under the MIT license. 1311 * 1312 * More Info: 1313 * http://ejohn.org/projects/javascript-diff-algorithm/ 1314 * 1315 * Usage: QUnit.diff(expected, actual) 1316 * 1317 * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over" 1318 */ 1319 QUnit.diff = (function() { 1320 function diff(o, n){ 1321 var ns = new Object(); 1322 var os = new Object(); 1323 1324 for (var i = 0; i < n.length; i++) { 1325 if (ns[n[i]] == null) 1326 ns[n[i]] = { 1327 rows: new Array(), 1328 o: null 1329 }; 1330 ns[n[i]].rows.push(i); 1331 } 1332 1333 for (var i = 0; i < o.length; i++) { 1334 if (os[o[i]] == null) 1335 os[o[i]] = { 1336 rows: new Array(), 1337 n: null 1338 }; 1339 os[o[i]].rows.push(i); 1340 } 1341 1342 for (var i in ns) { 1343 if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1344 n[ns[i].rows[0]] = { 1345 text: n[ns[i].rows[0]], 1346 row: os[i].rows[0] 1347 }; 1348 o[os[i].rows[0]] = { 1349 text: o[os[i].rows[0]], 1350 row: ns[i].rows[0] 1351 }; 1352 } 1353 } 1354 1355 for (var i = 0; i < n.length - 1; i++) { 1356 if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1357 n[i + 1] == o[n[i].row + 1]) { 1358 n[i + 1] = { 1359 text: n[i + 1], 1360 row: n[i].row + 1 1361 }; 1362 o[n[i].row + 1] = { 1363 text: o[n[i].row + 1], 1364 row: i + 1 1365 }; 1366 } 1367 } 1368 1369 for (var i = n.length - 1; i > 0; i--) { 1370 if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1371 n[i - 1] == o[n[i].row - 1]) { 1372 n[i - 1] = { 1373 text: n[i - 1], 1374 row: n[i].row - 1 1375 }; 1376 o[n[i].row - 1] = { 1377 text: o[n[i].row - 1], 1378 row: i - 1 1379 }; 1380 } 1381 } 1382 1383 return { 1384 o: o, 1385 n: n 1386 }; 1387 } 1388 1389 return function(o, n){ 1390 o = o.replace(/\s+$/, ''); 1391 n = n.replace(/\s+$/, ''); 1392 var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1393 1394 var str = ""; 1395 1396 var oSpace = o.match(/\s+/g); 1397 if (oSpace == null) { 1398 oSpace = [" "]; 1399 } 1400 else { 1401 oSpace.push(" "); 1402 } 1403 var nSpace = n.match(/\s+/g); 1404 if (nSpace == null) { 1405 nSpace = [" "]; 1406 } 1407 else { 1408 nSpace.push(" "); 1409 } 1410 1411 if (out.n.length == 0) { 1412 for (var i = 0; i < out.o.length; i++) { 1413 str += '<del>' + out.o[i] + oSpace[i] + "</del>"; 1414 } 1415 } 1416 else { 1417 if (out.n[0].text == null) { 1418 for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1419 str += '<del>' + out.o[n] + oSpace[n] + "</del>"; 1420 } 1421 } 1422 1423 for (var i = 0; i < out.n.length; i++) { 1424 if (out.n[i].text == null) { 1425 str += '<ins>' + out.n[i] + nSpace[i] + "</ins>"; 1426 } 1427 else { 1428 var pre = ""; 1429 1430 for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1431 pre += '<del>' + out.o[n] + oSpace[n] + "</del>"; 1432 } 1433 str += " " + out.n[i].text + nSpace[i] + pre; 1434 } 1435 } 1436 } 1437 1438 return str; 1439 }; 1440 })(); 1441 1442 })(this); 1443