1 /** 2 * QUnit - A JavaScript Unit Testing Framework 3 * 4 * http://docs.jquery.com/QUnit 5 * 6 * Copyright (c) 2011 John Resig, Jrn 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 this.testEnvironment.teardown.call(this.testEnvironment); 118 checkPollution(); 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.module + "-" + 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 gurantee 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 * Prefered 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( "#qunit-fixture" ).html( config.fixture ); 540 } else { 541 var 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>' + escapeHtml(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('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 if ( typeof document !== "undefined" && document.title ) { 782 // show for good, for bad suite result in title 783 // use escape sequences in case file gets loaded with non-utf-8-charset 784 document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; 785 } 786 787 QUnit.done( { 788 failed: config.stats.bad, 789 passed: passed, 790 total: config.stats.all, 791 runtime: runtime 792 } ); 793 } 794 795 function validTest( name ) { 796 var filter = config.filter, 797 run = false; 798 799 if ( !filter ) { 800 return true; 801 } 802 803 var not = filter.charAt( 0 ) === "!"; 804 if ( not ) { 805 filter = filter.slice( 1 ); 806 } 807 808 if ( name.indexOf( filter ) !== -1 ) { 809 return !not; 810 } 811 812 if ( not ) { 813 run = true; 814 } 815 816 return run; 817 } 818 819 // so far supports only Firefox, Chrome and Opera (buggy) 820 // could be extended in the future to use something like https://github.com/csnover/TraceKit 821 function sourceFromStacktrace() { 822 try { 823 throw new Error(); 824 } catch ( e ) { 825 if (e.stacktrace) { 826 // Opera 827 return e.stacktrace.split("\n")[6]; 828 } else if (e.stack) { 829 // Firefox, Chrome 830 return e.stack.split("\n")[4]; 831 } 832 } 833 } 834 835 function escapeHtml(s) { 836 if (!s) { 837 return ""; 838 } 839 s = s + ""; 840 return s.replace(/[\&"<>\\]/g, function(s) { 841 switch(s) { 842 case "&": return "&"; 843 case "\\": return "\\\\"; 844 case '"': return '\"'; 845 case "<": return "<"; 846 case ">": return ">"; 847 default: return s; 848 } 849 }); 850 } 851 852 function synchronize( callback ) { 853 config.queue.push( callback ); 854 855 if ( config.autorun && !config.blocking ) { 856 process(); 857 } 858 } 859 860 function process() { 861 var start = (new Date()).getTime(); 862 863 while ( config.queue.length && !config.blocking ) { 864 if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 865 config.queue.shift()(); 866 } else { 867 window.setTimeout( process, 13 ); 868 break; 869 } 870 } 871 if (!config.blocking && !config.queue.length) { 872 done(); 873 } 874 } 875 876 function saveGlobal() { 877 config.pollution = []; 878 879 if ( config.noglobals ) { 880 for ( var key in window ) { 881 config.pollution.push( key ); 882 } 883 } 884 } 885 886 function checkPollution( name ) { 887 var old = config.pollution; 888 saveGlobal(); 889 890 var newGlobals = diff( config.pollution, old ); 891 if ( newGlobals.length > 0 ) { 892 ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 893 } 894 895 var deletedGlobals = diff( old, config.pollution ); 896 if ( deletedGlobals.length > 0 ) { 897 ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 898 } 899 } 900 901 // returns a new Array with the elements that are in a but not in b 902 function diff( a, b ) { 903 var result = a.slice(); 904 for ( var i = 0; i < result.length; i++ ) { 905 for ( var j = 0; j < b.length; j++ ) { 906 if ( result[i] === b[j] ) { 907 result.splice(i, 1); 908 i--; 909 break; 910 } 911 } 912 } 913 return result; 914 } 915 916 function fail(message, exception, callback) { 917 if ( typeof console !== "undefined" && console.error && console.warn ) { 918 console.error(message); 919 console.error(exception); 920 console.warn(callback.toString()); 921 922 } else if ( window.opera && opera.postError ) { 923 opera.postError(message, exception, callback.toString); 924 } 925 } 926 927 function extend(a, b) { 928 for ( var prop in b ) { 929 if ( b[prop] === undefined ) { 930 delete a[prop]; 931 } else { 932 a[prop] = b[prop]; 933 } 934 } 935 936 return a; 937 } 938 939 function addEvent(elem, type, fn) { 940 if ( elem.addEventListener ) { 941 elem.addEventListener( type, fn, false ); 942 } else if ( elem.attachEvent ) { 943 elem.attachEvent( "on" + type, fn ); 944 } else { 945 fn(); 946 } 947 } 948 949 function id(name) { 950 return !!(typeof document !== "undefined" && document && document.getElementById) && 951 document.getElementById( name ); 952 } 953 954 // Test for equality any JavaScript type. 955 // Discussions and reference: http://philrathe.com/articles/equiv 956 // Test suites: http://philrathe.com/tests/equiv 957 // Author: Philippe Rath <prathe (at) gmail.com> 958 QUnit.equiv = function () { 959 960 var innerEquiv; // the real equiv function 961 var callers = []; // stack to decide between skip/abort functions 962 var parents = []; // stack to avoiding loops from circular referencing 963 964 // Call the o related callback with the given arguments. 965 function bindCallbacks(o, callbacks, args) { 966 var prop = QUnit.objectType(o); 967 if (prop) { 968 if (QUnit.objectType(callbacks[prop]) === "function") { 969 return callbacks[prop].apply(callbacks, args); 970 } else { 971 return callbacks[prop]; // or undefined 972 } 973 } 974 } 975 976 var callbacks = function () { 977 978 // for string, boolean, number and null 979 function useStrictEquality(b, a) { 980 if (b instanceof a.constructor || a instanceof b.constructor) { 981 // to catch short annotaion VS 'new' annotation of a declaration 982 // e.g. var i = 1; 983 // var j = new Number(1); 984 return a == b; 985 } else { 986 return a === b; 987 } 988 } 989 990 return { 991 "string": useStrictEquality, 992 "boolean": useStrictEquality, 993 "number": useStrictEquality, 994 "null": useStrictEquality, 995 "undefined": useStrictEquality, 996 997 "nan": function (b) { 998 return isNaN(b); 999 }, 1000 1001 "date": function (b, a) { 1002 return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1003 }, 1004 1005 "regexp": function (b, a) { 1006 return QUnit.objectType(b) === "regexp" && 1007 a.source === b.source && // the regex itself 1008 a.global === b.global && // and its modifers (gmi) ... 1009 a.ignoreCase === b.ignoreCase && 1010 a.multiline === b.multiline; 1011 }, 1012 1013 // - skip when the property is a method of an instance (OOP) 1014 // - abort otherwise, 1015 // initial === would have catch identical references anyway 1016 "function": function () { 1017 var caller = callers[callers.length - 1]; 1018 return caller !== Object && 1019 typeof caller !== "undefined"; 1020 }, 1021 1022 "array": function (b, a) { 1023 var i, j, loop; 1024 var len; 1025 1026 // b could be an object literal here 1027 if ( ! (QUnit.objectType(b) === "array")) { 1028 return false; 1029 } 1030 1031 len = a.length; 1032 if (len !== b.length) { // safe and faster 1033 return false; 1034 } 1035 1036 //track reference to avoid circular references 1037 parents.push(a); 1038 for (i = 0; i < len; i++) { 1039 loop = false; 1040 for(j=0;j<parents.length;j++){ 1041 if(parents[j] === a[i]){ 1042 loop = true;//dont rewalk array 1043 } 1044 } 1045 if (!loop && ! innerEquiv(a[i], b[i])) { 1046 parents.pop(); 1047 return false; 1048 } 1049 } 1050 parents.pop(); 1051 return true; 1052 }, 1053 1054 "object": function (b, a) { 1055 var i, j, loop; 1056 var eq = true; // unless we can proove it 1057 var aProperties = [], bProperties = []; // collection of strings 1058 1059 // comparing constructors is more strict than using instanceof 1060 if ( a.constructor !== b.constructor) { 1061 return false; 1062 } 1063 1064 // stack constructor before traversing properties 1065 callers.push(a.constructor); 1066 //track reference to avoid circular references 1067 parents.push(a); 1068 1069 for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 1070 loop = false; 1071 for(j=0;j<parents.length;j++){ 1072 if(parents[j] === a[i]) 1073 loop = true; //don't go down the same path twice 1074 } 1075 aProperties.push(i); // collect a's properties 1076 1077 if (!loop && ! innerEquiv(a[i], b[i])) { 1078 eq = false; 1079 break; 1080 } 1081 } 1082 1083 callers.pop(); // unstack, we are done 1084 parents.pop(); 1085 1086 for (i in b) { 1087 bProperties.push(i); // collect b's properties 1088 } 1089 1090 // Ensures identical properties name 1091 return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1092 } 1093 }; 1094 }(); 1095 1096 innerEquiv = function () { // can take multiple arguments 1097 var args = Array.prototype.slice.apply(arguments); 1098 if (args.length < 2) { 1099 return true; // end transition 1100 } 1101 1102 return (function (a, b) { 1103 if (a === b) { 1104 return true; // catch the most you can 1105 } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType(a) !== QUnit.objectType(b)) { 1106 return false; // don't lose time with error prone cases 1107 } else { 1108 return bindCallbacks(a, callbacks, [b, a]); 1109 } 1110 1111 // apply transition with (1..n) arguments 1112 })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); 1113 }; 1114 1115 return innerEquiv; 1116 1117 }(); 1118 1119 /** 1120 * jsDump 1121 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 1122 * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) 1123 * Date: 5/15/2008 1124 * @projectDescription Advanced and extensible data dumping for Javascript. 1125 * @version 1.0.0 1126 * @author Ariel Flesler 1127 * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1128 */ 1129 QUnit.jsDump = (function() { 1130 function quote( str ) { 1131 return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1132 }; 1133 function literal( o ) { 1134 return o + ''; 1135 }; 1136 function join( pre, arr, post ) { 1137 var s = jsDump.separator(), 1138 base = jsDump.indent(), 1139 inner = jsDump.indent(1); 1140 if ( arr.join ) 1141 arr = arr.join( ',' + s + inner ); 1142 if ( !arr ) 1143 return pre + post; 1144 return [ pre, inner + arr, base + post ].join(s); 1145 }; 1146 function array( arr ) { 1147 var i = arr.length, ret = Array(i); 1148 this.up(); 1149 while ( i-- ) 1150 ret[i] = this.parse( arr[i] ); 1151 this.down(); 1152 return join( '[', ret, ']' ); 1153 }; 1154 1155 var reName = /^function (\w+)/; 1156 1157 var jsDump = { 1158 parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance 1159 var parser = this.parsers[ type || this.typeOf(obj) ]; 1160 type = typeof parser; 1161 1162 return type == 'function' ? parser.call( this, obj ) : 1163 type == 'string' ? parser : 1164 this.parsers.error; 1165 }, 1166 typeOf:function( obj ) { 1167 var type; 1168 if ( obj === null ) { 1169 type = "null"; 1170 } else if (typeof obj === "undefined") { 1171 type = "undefined"; 1172 } else if (QUnit.is("RegExp", obj)) { 1173 type = "regexp"; 1174 } else if (QUnit.is("Date", obj)) { 1175 type = "date"; 1176 } else if (QUnit.is("Function", obj)) { 1177 type = "function"; 1178 } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1179 type = "window"; 1180 } else if (obj.nodeType === 9) { 1181 type = "document"; 1182 } else if (obj.nodeType) { 1183 type = "node"; 1184 } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { 1185 type = "array"; 1186 } else { 1187 type = typeof obj; 1188 } 1189 return type; 1190 }, 1191 separator:function() { 1192 return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' '; 1193 }, 1194 indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1195 if ( !this.multiline ) 1196 return ''; 1197 var chr = this.indentChar; 1198 if ( this.HTML ) 1199 chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1200 return Array( this._depth_ + (extra||0) ).join(chr); 1201 }, 1202 up:function( a ) { 1203 this._depth_ += a || 1; 1204 }, 1205 down:function( a ) { 1206 this._depth_ -= a || 1; 1207 }, 1208 setParser:function( name, parser ) { 1209 this.parsers[name] = parser; 1210 }, 1211 // The next 3 are exposed so you can use them 1212 quote:quote, 1213 literal:literal, 1214 join:join, 1215 // 1216 _depth_: 1, 1217 // This is the list of parsers, to modify them, use jsDump.setParser 1218 parsers:{ 1219 window: '[Window]', 1220 document: '[Document]', 1221 error:'[ERROR]', //when no parser is found, shouldn't happen 1222 unknown: '[Unknown]', 1223 'null':'null', 1224 'undefined':'undefined', 1225 'function':function( fn ) { 1226 var ret = 'function', 1227 name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1228 if ( name ) 1229 ret += ' ' + name; 1230 ret += '('; 1231 1232 ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1233 return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1234 }, 1235 array: array, 1236 nodelist: array, 1237 arguments: array, 1238 object:function( map ) { 1239 var ret = [ ]; 1240 QUnit.jsDump.up(); 1241 for ( var key in map ) 1242 ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1243 QUnit.jsDump.down(); 1244 return join( '{', ret, '}' ); 1245 }, 1246 node:function( node ) { 1247 var open = QUnit.jsDump.HTML ? '<' : '<', 1248 close = QUnit.jsDump.HTML ? '>' : '>'; 1249 1250 var tag = node.nodeName.toLowerCase(), 1251 ret = open + tag; 1252 1253 for ( var a in QUnit.jsDump.DOMAttrs ) { 1254 var val = node[QUnit.jsDump.DOMAttrs[a]]; 1255 if ( val ) 1256 ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1257 } 1258 return ret + close + open + '/' + tag + close; 1259 }, 1260 functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1261 var l = fn.length; 1262 if ( !l ) return ''; 1263 1264 var args = Array(l); 1265 while ( l-- ) 1266 args[l] = String.fromCharCode(97+l);//97 is 'a' 1267 return ' ' + args.join(', ') + ' '; 1268 }, 1269 key:quote, //object calls it internally, the key part of an item in a map 1270 functionCode:'[code]', //function calls it internally, it's the content of the function 1271 attribute:quote, //node calls it internally, it's an html attribute value 1272 string:quote, 1273 date:quote, 1274 regexp:literal, //regex 1275 number:literal, 1276 'boolean':literal 1277 }, 1278 DOMAttrs:{//attributes to dump from nodes, name=>realName 1279 id:'id', 1280 name:'name', 1281 'class':'className' 1282 }, 1283 HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1284 indentChar:' ',//indentation unit 1285 multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1286 }; 1287 1288 return jsDump; 1289 })(); 1290 1291 // from Sizzle.js 1292 function getText( elems ) { 1293 var ret = "", elem; 1294 1295 for ( var i = 0; elems[i]; i++ ) { 1296 elem = elems[i]; 1297 1298 // Get the text from text nodes and CDATA nodes 1299 if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1300 ret += elem.nodeValue; 1301 1302 // Traverse everything else, except comment nodes 1303 } else if ( elem.nodeType !== 8 ) { 1304 ret += getText( elem.childNodes ); 1305 } 1306 } 1307 1308 return ret; 1309 }; 1310 1311 /* 1312 * Javascript Diff Algorithm 1313 * By John Resig (http://ejohn.org/) 1314 * Modified by Chu Alan "sprite" 1315 * 1316 * Released under the MIT license. 1317 * 1318 * More Info: 1319 * http://ejohn.org/projects/javascript-diff-algorithm/ 1320 * 1321 * Usage: QUnit.diff(expected, actual) 1322 * 1323 * 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" 1324 */ 1325 QUnit.diff = (function() { 1326 function diff(o, n){ 1327 var ns = new Object(); 1328 var os = new Object(); 1329 1330 for (var i = 0; i < n.length; i++) { 1331 if (ns[n[i]] == null) 1332 ns[n[i]] = { 1333 rows: new Array(), 1334 o: null 1335 }; 1336 ns[n[i]].rows.push(i); 1337 } 1338 1339 for (var i = 0; i < o.length; i++) { 1340 if (os[o[i]] == null) 1341 os[o[i]] = { 1342 rows: new Array(), 1343 n: null 1344 }; 1345 os[o[i]].rows.push(i); 1346 } 1347 1348 for (var i in ns) { 1349 if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1350 n[ns[i].rows[0]] = { 1351 text: n[ns[i].rows[0]], 1352 row: os[i].rows[0] 1353 }; 1354 o[os[i].rows[0]] = { 1355 text: o[os[i].rows[0]], 1356 row: ns[i].rows[0] 1357 }; 1358 } 1359 } 1360 1361 for (var i = 0; i < n.length - 1; i++) { 1362 if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1363 n[i + 1] == o[n[i].row + 1]) { 1364 n[i + 1] = { 1365 text: n[i + 1], 1366 row: n[i].row + 1 1367 }; 1368 o[n[i].row + 1] = { 1369 text: o[n[i].row + 1], 1370 row: i + 1 1371 }; 1372 } 1373 } 1374 1375 for (var i = n.length - 1; i > 0; i--) { 1376 if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1377 n[i - 1] == o[n[i].row - 1]) { 1378 n[i - 1] = { 1379 text: n[i - 1], 1380 row: n[i].row - 1 1381 }; 1382 o[n[i].row - 1] = { 1383 text: o[n[i].row - 1], 1384 row: i - 1 1385 }; 1386 } 1387 } 1388 1389 return { 1390 o: o, 1391 n: n 1392 }; 1393 } 1394 1395 return function(o, n){ 1396 o = o.replace(/\s+$/, ''); 1397 n = n.replace(/\s+$/, ''); 1398 var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1399 1400 var str = ""; 1401 1402 var oSpace = o.match(/\s+/g); 1403 if (oSpace == null) { 1404 oSpace = [" "]; 1405 } 1406 else { 1407 oSpace.push(" "); 1408 } 1409 var nSpace = n.match(/\s+/g); 1410 if (nSpace == null) { 1411 nSpace = [" "]; 1412 } 1413 else { 1414 nSpace.push(" "); 1415 } 1416 1417 if (out.n.length == 0) { 1418 for (var i = 0; i < out.o.length; i++) { 1419 str += '<del>' + out.o[i] + oSpace[i] + "</del>"; 1420 } 1421 } 1422 else { 1423 if (out.n[0].text == null) { 1424 for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1425 str += '<del>' + out.o[n] + oSpace[n] + "</del>"; 1426 } 1427 } 1428 1429 for (var i = 0; i < out.n.length; i++) { 1430 if (out.n[i].text == null) { 1431 str += '<ins>' + out.n[i] + nSpace[i] + "</ins>"; 1432 } 1433 else { 1434 var pre = ""; 1435 1436 for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1437 pre += '<del>' + out.o[n] + oSpace[n] + "</del>"; 1438 } 1439 str += " " + out.n[i].text + nSpace[i] + pre; 1440 } 1441 } 1442 } 1443 1444 return str; 1445 }; 1446 })(); 1447 1448 })(this); 1449