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