qunit.js (29005B)
1 /* 2 * QUnit - A JavaScript Unit Testing Framework 3 * 4 * http://docs.jquery.com/QUnit 5 * 6 * Copyright (c) 2009 John Resig, Jörn Zaefferer 7 * Dual licensed under the MIT (MIT-LICENSE.txt) 8 * and GPL (GPL-LICENSE.txt) licenses. 9 */ 10 11 (function(window) { 12 13 var QUnit = { 14 15 // Initialize the configuration options 16 init: function() { 17 config = { 18 stats: { all: 0, bad: 0 }, 19 moduleStats: { all: 0, bad: 0 }, 20 started: +new Date, 21 blocking: false, 22 autorun: false, 23 assertions: [], 24 filters: [], 25 queue: [] 26 }; 27 28 var tests = id("qunit-tests"), 29 banner = id("qunit-banner"), 30 result = id("qunit-testresult"); 31 32 if ( tests ) { 33 tests.innerHTML = ""; 34 } 35 36 if ( banner ) { 37 banner.className = ""; 38 } 39 40 if ( result ) { 41 result.parentNode.removeChild( result ); 42 } 43 }, 44 45 // call on start of module test to prepend name to all tests 46 module: function(name, testEnvironment) { 47 config.currentModule = name; 48 49 synchronize(function() { 50 if ( config.currentModule ) { 51 QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 52 } 53 54 config.currentModule = name; 55 config.moduleTestEnvironment = testEnvironment; 56 config.moduleStats = { all: 0, bad: 0 }; 57 58 QUnit.moduleStart( name, testEnvironment ); 59 }); 60 }, 61 62 asyncTest: function(testName, expected, callback) { 63 if ( arguments.length === 2 ) { 64 callback = expected; 65 expected = 0; 66 } 67 68 QUnit.test(testName, expected, callback, true); 69 }, 70 71 test: function(testName, expected, callback, async) { 72 var name = testName, testEnvironment, testEnvironmentArg; 73 74 if ( arguments.length === 2 ) { 75 callback = expected; 76 expected = null; 77 } 78 // is 2nd argument a testEnvironment? 79 if ( expected && typeof expected === 'object') { 80 testEnvironmentArg = expected; 81 expected = null; 82 } 83 84 if ( config.currentModule ) { 85 name = config.currentModule + " module: " + name; 86 } 87 88 if ( !validTest(name) ) { 89 return; 90 } 91 92 synchronize(function() { 93 QUnit.testStart( testName ); 94 95 testEnvironment = extend({ 96 setup: function() {}, 97 teardown: function() {} 98 }, config.moduleTestEnvironment); 99 if (testEnvironmentArg) { 100 extend(testEnvironment,testEnvironmentArg); 101 } 102 103 // allow utility functions to access the current test environment 104 QUnit.current_testEnvironment = testEnvironment; 105 106 config.assertions = []; 107 config.expected = expected; 108 109 try { 110 if ( !config.pollution ) { 111 saveGlobal(); 112 } 113 114 testEnvironment.setup.call(testEnvironment); 115 } catch(e) { 116 QUnit.ok( false, "Setup failed on " + name + ": " + e.message ); 117 } 118 119 if ( async ) { 120 QUnit.stop(); 121 } 122 123 try { 124 callback.call(testEnvironment); 125 } catch(e) { 126 fail("Test " + name + " died, exception and test follows", e, callback); 127 QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message ); 128 // else next test will carry the responsibility 129 saveGlobal(); 130 131 // Restart the tests if they're blocking 132 if ( config.blocking ) { 133 start(); 134 } 135 } 136 }); 137 138 synchronize(function() { 139 try { 140 checkPollution(); 141 testEnvironment.teardown.call(testEnvironment); 142 } catch(e) { 143 QUnit.ok( false, "Teardown failed on " + name + ": " + e.message ); 144 } 145 146 try { 147 QUnit.reset(); 148 } catch(e) { 149 fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset); 150 } 151 152 if ( config.expected && config.expected != config.assertions.length ) { 153 QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" ); 154 } 155 156 var good = 0, bad = 0, 157 tests = id("qunit-tests"); 158 159 config.stats.all += config.assertions.length; 160 config.moduleStats.all += config.assertions.length; 161 162 if ( tests ) { 163 var ol = document.createElement("ol"); 164 ol.style.display = "none"; 165 166 for ( var i = 0; i < config.assertions.length; i++ ) { 167 var assertion = config.assertions[i]; 168 169 var li = document.createElement("li"); 170 li.className = assertion.result ? "pass" : "fail"; 171 li.appendChild(document.createTextNode(assertion.message || "(no message)")); 172 ol.appendChild( li ); 173 174 if ( assertion.result ) { 175 good++; 176 } else { 177 bad++; 178 config.stats.bad++; 179 config.moduleStats.bad++; 180 } 181 } 182 183 var b = document.createElement("strong"); 184 b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>"; 185 186 addEvent(b, "click", function() { 187 var next = b.nextSibling, display = next.style.display; 188 next.style.display = display === "none" ? "block" : "none"; 189 }); 190 191 addEvent(b, "dblclick", function(e) { 192 var target = e && e.target ? e.target : window.event.srcElement; 193 if ( target.nodeName.toLowerCase() === "strong" ) { 194 var text = "", node = target.firstChild; 195 196 while ( node.nodeType === 3 ) { 197 text += node.nodeValue; 198 node = node.nextSibling; 199 } 200 201 text = text.replace(/(^\s*|\s*$)/g, ""); 202 203 if ( window.location ) { 204 window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text); 205 } 206 } 207 }); 208 209 var li = document.createElement("li"); 210 li.className = bad ? "fail" : "pass"; 211 li.appendChild( b ); 212 li.appendChild( ol ); 213 tests.appendChild( li ); 214 215 if ( bad ) { 216 var toolbar = id("qunit-testrunner-toolbar"); 217 if ( toolbar ) { 218 toolbar.style.display = "block"; 219 id("qunit-filter-pass").disabled = null; 220 id("qunit-filter-missing").disabled = null; 221 } 222 } 223 224 } else { 225 for ( var i = 0; i < config.assertions.length; i++ ) { 226 if ( !config.assertions[i].result ) { 227 bad++; 228 config.stats.bad++; 229 config.moduleStats.bad++; 230 } 231 } 232 } 233 234 QUnit.testDone( testName, bad, config.assertions.length ); 235 236 if ( !window.setTimeout && !config.queue.length ) { 237 done(); 238 } 239 }); 240 241 if ( window.setTimeout && !config.doneTimer ) { 242 config.doneTimer = window.setTimeout(function(){ 243 if ( !config.queue.length ) { 244 done(); 245 } else { 246 synchronize( done ); 247 } 248 }, 13); 249 } 250 }, 251 252 /** 253 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 254 */ 255 expect: function(asserts) { 256 config.expected = asserts; 257 }, 258 259 /** 260 * Asserts true. 261 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 262 */ 263 ok: function(a, msg) { 264 QUnit.log(a, msg); 265 266 config.assertions.push({ 267 result: !!a, 268 message: msg 269 }); 270 }, 271 272 /** 273 * Checks that the first two arguments are equal, with an optional message. 274 * Prints out both actual and expected values. 275 * 276 * Prefered to ok( actual == expected, message ) 277 * 278 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 279 * 280 * @param Object actual 281 * @param Object expected 282 * @param String message (optional) 283 */ 284 equal: function(actual, expected, message) { 285 push(expected == actual, actual, expected, message); 286 }, 287 288 notEqual: function(actual, expected, message) { 289 push(expected != actual, actual, expected, message); 290 }, 291 292 deepEqual: function(a, b, message) { 293 push(QUnit.equiv(a, b), a, b, message); 294 }, 295 296 notDeepEqual: function(a, b, message) { 297 push(!QUnit.equiv(a, b), a, b, message); 298 }, 299 300 strictEqual: function(actual, expected, message) { 301 push(expected === actual, actual, expected, message); 302 }, 303 304 notStrictEqual: function(actual, expected, message) { 305 push(expected !== actual, actual, expected, message); 306 }, 307 308 start: function() { 309 // A slight delay, to avoid any current callbacks 310 if ( window.setTimeout ) { 311 window.setTimeout(function() { 312 if ( config.timeout ) { 313 clearTimeout(config.timeout); 314 } 315 316 config.blocking = false; 317 process(); 318 }, 13); 319 } else { 320 config.blocking = false; 321 process(); 322 } 323 }, 324 325 stop: function(timeout) { 326 config.blocking = true; 327 328 if ( timeout && window.setTimeout ) { 329 config.timeout = window.setTimeout(function() { 330 QUnit.ok( false, "Test timed out" ); 331 QUnit.start(); 332 }, timeout); 333 } 334 }, 335 336 /** 337 * Resets the test setup. Useful for tests that modify the DOM. 338 */ 339 reset: function() { 340 if ( window.jQuery ) { 341 jQuery("#main").html( config.fixture ); 342 jQuery.event.global = {}; 343 jQuery.ajaxSettings = extend({}, config.ajaxSettings); 344 } 345 }, 346 347 /** 348 * Trigger an event on an element. 349 * 350 * @example triggerEvent( document.body, "click" ); 351 * 352 * @param DOMElement elem 353 * @param String type 354 */ 355 triggerEvent: function( elem, type, event ) { 356 if ( document.createEvent ) { 357 event = document.createEvent("MouseEvents"); 358 event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 359 0, 0, 0, 0, 0, false, false, false, false, 0, null); 360 elem.dispatchEvent( event ); 361 362 } else if ( elem.fireEvent ) { 363 elem.fireEvent("on"+type); 364 } 365 }, 366 367 // Safe object type checking 368 is: function( type, obj ) { 369 return Object.prototype.toString.call( obj ) === "[object "+ type +"]"; 370 }, 371 372 // Logging callbacks 373 done: function(failures, total) {}, 374 log: function(result, message) {}, 375 testStart: function(name) {}, 376 testDone: function(name, failures, total) {}, 377 moduleStart: function(name, testEnvironment) {}, 378 moduleDone: function(name, failures, total) {} 379 }; 380 381 // Backwards compatibility, deprecated 382 QUnit.equals = QUnit.equal; 383 QUnit.same = QUnit.deepEqual; 384 385 // Maintain internal state 386 var config = { 387 // The queue of tests to run 388 queue: [], 389 390 // block until document ready 391 blocking: true 392 }; 393 394 // Load paramaters 395 (function() { 396 var location = window.location || { search: "", protocol: "file:" }, 397 GETParams = location.search.slice(1).split('&'); 398 399 for ( var i = 0; i < GETParams.length; i++ ) { 400 GETParams[i] = decodeURIComponent( GETParams[i] ); 401 if ( GETParams[i] === "noglobals" ) { 402 GETParams.splice( i, 1 ); 403 i--; 404 config.noglobals = true; 405 } else if ( GETParams[i].search('=') > -1 ) { 406 GETParams.splice( i, 1 ); 407 i--; 408 } 409 } 410 411 // restrict modules/tests by get parameters 412 config.filters = GETParams; 413 414 // Figure out if we're running the tests from a server or not 415 QUnit.isLocal = !!(location.protocol === 'file:'); 416 })(); 417 418 // Expose the API as global variables, unless an 'exports' 419 // object exists, in that case we assume we're in CommonJS 420 if ( typeof exports === "undefined" || typeof require === "undefined" ) { 421 extend(window, QUnit); 422 window.QUnit = QUnit; 423 } else { 424 extend(exports, QUnit); 425 exports.QUnit = QUnit; 426 } 427 428 if ( typeof document === "undefined" || document.readyState === "complete" ) { 429 config.autorun = true; 430 } 431 432 addEvent(window, "load", function() { 433 // Initialize the config, saving the execution queue 434 var oldconfig = extend({}, config); 435 QUnit.init(); 436 extend(config, oldconfig); 437 438 config.blocking = false; 439 440 var userAgent = id("qunit-userAgent"); 441 if ( userAgent ) { 442 userAgent.innerHTML = navigator.userAgent; 443 } 444 445 var toolbar = id("qunit-testrunner-toolbar"); 446 if ( toolbar ) { 447 toolbar.style.display = "none"; 448 449 var filter = document.createElement("input"); 450 filter.type = "checkbox"; 451 filter.id = "qunit-filter-pass"; 452 filter.disabled = true; 453 addEvent( filter, "click", function() { 454 var li = document.getElementsByTagName("li"); 455 for ( var i = 0; i < li.length; i++ ) { 456 if ( li[i].className.indexOf("pass") > -1 ) { 457 li[i].style.display = filter.checked ? "none" : ""; 458 } 459 } 460 }); 461 toolbar.appendChild( filter ); 462 463 var label = document.createElement("label"); 464 label.setAttribute("for", "qunit-filter-pass"); 465 label.innerHTML = "Hide passed tests"; 466 toolbar.appendChild( label ); 467 468 var missing = document.createElement("input"); 469 missing.type = "checkbox"; 470 missing.id = "qunit-filter-missing"; 471 missing.disabled = true; 472 addEvent( missing, "click", function() { 473 var li = document.getElementsByTagName("li"); 474 for ( var i = 0; i < li.length; i++ ) { 475 if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) { 476 li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block"; 477 } 478 } 479 }); 480 toolbar.appendChild( missing ); 481 482 label = document.createElement("label"); 483 label.setAttribute("for", "qunit-filter-missing"); 484 label.innerHTML = "Hide missing tests (untested code is broken code)"; 485 toolbar.appendChild( label ); 486 } 487 488 var main = id('main'); 489 if ( main ) { 490 config.fixture = main.innerHTML; 491 } 492 493 if ( window.jQuery ) { 494 config.ajaxSettings = window.jQuery.ajaxSettings; 495 } 496 497 QUnit.start(); 498 }); 499 500 function done() { 501 if ( config.doneTimer && window.clearTimeout ) { 502 window.clearTimeout( config.doneTimer ); 503 config.doneTimer = null; 504 } 505 506 if ( config.queue.length ) { 507 config.doneTimer = window.setTimeout(function(){ 508 if ( !config.queue.length ) { 509 done(); 510 } else { 511 synchronize( done ); 512 } 513 }, 13); 514 515 return; 516 } 517 518 config.autorun = true; 519 520 // Log the last module results 521 if ( config.currentModule ) { 522 QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all ); 523 } 524 525 var banner = id("qunit-banner"), 526 tests = id("qunit-tests"), 527 html = ['Tests completed in ', 528 +new Date - config.started, ' milliseconds.<br/>', 529 '<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join(''); 530 531 if ( banner ) { 532 banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 533 } 534 535 if ( tests ) { 536 var result = id("qunit-testresult"); 537 538 if ( !result ) { 539 result = document.createElement("p"); 540 result.id = "qunit-testresult"; 541 result.className = "result"; 542 tests.parentNode.insertBefore( result, tests.nextSibling ); 543 } 544 545 result.innerHTML = html; 546 } 547 548 QUnit.done( config.stats.bad, config.stats.all ); 549 } 550 551 function validTest( name ) { 552 var i = config.filters.length, 553 run = false; 554 555 if ( !i ) { 556 return true; 557 } 558 559 while ( i-- ) { 560 var filter = config.filters[i], 561 not = filter.charAt(0) == '!'; 562 563 if ( not ) { 564 filter = filter.slice(1); 565 } 566 567 if ( name.indexOf(filter) !== -1 ) { 568 return !not; 569 } 570 571 if ( not ) { 572 run = true; 573 } 574 } 575 576 return run; 577 } 578 579 function push(result, actual, expected, message) { 580 message = message || (result ? "okay" : "failed"); 581 QUnit.ok( result, result ? message + ": " + expected : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) ); 582 } 583 584 function synchronize( callback ) { 585 config.queue.push( callback ); 586 587 if ( config.autorun && !config.blocking ) { 588 process(); 589 } 590 } 591 592 function process() { 593 while ( config.queue.length && !config.blocking ) { 594 config.queue.shift()(); 595 } 596 } 597 598 function saveGlobal() { 599 config.pollution = []; 600 601 if ( config.noglobals ) { 602 for ( var key in window ) { 603 config.pollution.push( key ); 604 } 605 } 606 } 607 608 function checkPollution( name ) { 609 var old = config.pollution; 610 saveGlobal(); 611 612 var newGlobals = diff( old, config.pollution ); 613 if ( newGlobals.length > 0 ) { 614 ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 615 config.expected++; 616 } 617 618 var deletedGlobals = diff( config.pollution, old ); 619 if ( deletedGlobals.length > 0 ) { 620 ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 621 config.expected++; 622 } 623 } 624 625 // returns a new Array with the elements that are in a but not in b 626 function diff( a, b ) { 627 var result = a.slice(); 628 for ( var i = 0; i < result.length; i++ ) { 629 for ( var j = 0; j < b.length; j++ ) { 630 if ( result[i] === b[j] ) { 631 result.splice(i, 1); 632 i--; 633 break; 634 } 635 } 636 } 637 return result; 638 } 639 640 function fail(message, exception, callback) { 641 if ( typeof console !== "undefined" && console.error && console.warn ) { 642 console.error(message); 643 console.error(exception); 644 console.warn(callback.toString()); 645 646 } else if ( window.opera && opera.postError ) { 647 opera.postError(message, exception, callback.toString); 648 } 649 } 650 651 function extend(a, b) { 652 for ( var prop in b ) { 653 a[prop] = b[prop]; 654 } 655 656 return a; 657 } 658 659 function addEvent(elem, type, fn) { 660 if ( elem.addEventListener ) { 661 elem.addEventListener( type, fn, false ); 662 } else if ( elem.attachEvent ) { 663 elem.attachEvent( "on" + type, fn ); 664 } else { 665 fn(); 666 } 667 } 668 669 function id(name) { 670 return !!(typeof document !== "undefined" && document && document.getElementById) && 671 document.getElementById( name ); 672 } 673 674 // Test for equality any JavaScript type. 675 // Discussions and reference: http://philrathe.com/articles/equiv 676 // Test suites: http://philrathe.com/tests/equiv 677 // Author: Philippe Rathé <prathe@gmail.com> 678 QUnit.equiv = function () { 679 680 var innerEquiv; // the real equiv function 681 var callers = []; // stack to decide between skip/abort functions 682 683 684 // Determine what is o. 685 function hoozit(o) { 686 if (QUnit.is("String", o)) { 687 return "string"; 688 689 } else if (QUnit.is("Boolean", o)) { 690 return "boolean"; 691 692 } else if (QUnit.is("Number", o)) { 693 694 if (isNaN(o)) { 695 return "nan"; 696 } else { 697 return "number"; 698 } 699 700 } else if (typeof o === "undefined") { 701 return "undefined"; 702 703 // consider: typeof null === object 704 } else if (o === null) { 705 return "null"; 706 707 // consider: typeof [] === object 708 } else if (QUnit.is( "Array", o)) { 709 return "array"; 710 711 // consider: typeof new Date() === object 712 } else if (QUnit.is( "Date", o)) { 713 return "date"; 714 715 // consider: /./ instanceof Object; 716 // /./ instanceof RegExp; 717 // typeof /./ === "function"; // => false in IE and Opera, 718 // true in FF and Safari 719 } else if (QUnit.is( "RegExp", o)) { 720 return "regexp"; 721 722 } else if (typeof o === "object") { 723 return "object"; 724 725 } else if (QUnit.is( "Function", o)) { 726 return "function"; 727 } else { 728 return undefined; 729 } 730 } 731 732 // Call the o related callback with the given arguments. 733 function bindCallbacks(o, callbacks, args) { 734 var prop = hoozit(o); 735 if (prop) { 736 if (hoozit(callbacks[prop]) === "function") { 737 return callbacks[prop].apply(callbacks, args); 738 } else { 739 return callbacks[prop]; // or undefined 740 } 741 } 742 } 743 744 var callbacks = function () { 745 746 // for string, boolean, number and null 747 function useStrictEquality(b, a) { 748 if (b instanceof a.constructor || a instanceof b.constructor) { 749 // to catch short annotaion VS 'new' annotation of a declaration 750 // e.g. var i = 1; 751 // var j = new Number(1); 752 return a == b; 753 } else { 754 return a === b; 755 } 756 } 757 758 return { 759 "string": useStrictEquality, 760 "boolean": useStrictEquality, 761 "number": useStrictEquality, 762 "null": useStrictEquality, 763 "undefined": useStrictEquality, 764 765 "nan": function (b) { 766 return isNaN(b); 767 }, 768 769 "date": function (b, a) { 770 return hoozit(b) === "date" && a.valueOf() === b.valueOf(); 771 }, 772 773 "regexp": function (b, a) { 774 return hoozit(b) === "regexp" && 775 a.source === b.source && // the regex itself 776 a.global === b.global && // and its modifers (gmi) ... 777 a.ignoreCase === b.ignoreCase && 778 a.multiline === b.multiline; 779 }, 780 781 // - skip when the property is a method of an instance (OOP) 782 // - abort otherwise, 783 // initial === would have catch identical references anyway 784 "function": function () { 785 var caller = callers[callers.length - 1]; 786 return caller !== Object && 787 typeof caller !== "undefined"; 788 }, 789 790 "array": function (b, a) { 791 var i; 792 var len; 793 794 // b could be an object literal here 795 if ( ! (hoozit(b) === "array")) { 796 return false; 797 } 798 799 len = a.length; 800 if (len !== b.length) { // safe and faster 801 return false; 802 } 803 for (i = 0; i < len; i++) { 804 if ( ! innerEquiv(a[i], b[i])) { 805 return false; 806 } 807 } 808 return true; 809 }, 810 811 "object": function (b, a) { 812 var i; 813 var eq = true; // unless we can proove it 814 var aProperties = [], bProperties = []; // collection of strings 815 816 // comparing constructors is more strict than using instanceof 817 if ( a.constructor !== b.constructor) { 818 return false; 819 } 820 821 // stack constructor before traversing properties 822 callers.push(a.constructor); 823 824 for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 825 826 aProperties.push(i); // collect a's properties 827 828 if ( ! innerEquiv(a[i], b[i])) { 829 eq = false; 830 } 831 } 832 833 callers.pop(); // unstack, we are done 834 835 for (i in b) { 836 bProperties.push(i); // collect b's properties 837 } 838 839 // Ensures identical properties name 840 return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 841 } 842 }; 843 }(); 844 845 innerEquiv = function () { // can take multiple arguments 846 var args = Array.prototype.slice.apply(arguments); 847 if (args.length < 2) { 848 return true; // end transition 849 } 850 851 return (function (a, b) { 852 if (a === b) { 853 return true; // catch the most you can 854 } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) { 855 return false; // don't lose time with error prone cases 856 } else { 857 return bindCallbacks(a, callbacks, [b, a]); 858 } 859 860 // apply transition with (1..n) arguments 861 })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1)); 862 }; 863 864 return innerEquiv; 865 866 }(); 867 868 /** 869 * jsDump 870 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 871 * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) 872 * Date: 5/15/2008 873 * @projectDescription Advanced and extensible data dumping for Javascript. 874 * @version 1.0.0 875 * @author Ariel Flesler 876 * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 877 */ 878 QUnit.jsDump = (function() { 879 function quote( str ) { 880 return '"' + str.toString().replace(/"/g, '\\"') + '"'; 881 }; 882 function literal( o ) { 883 return o + ''; 884 }; 885 function join( pre, arr, post ) { 886 var s = jsDump.separator(), 887 base = jsDump.indent(), 888 inner = jsDump.indent(1); 889 if ( arr.join ) 890 arr = arr.join( ',' + s + inner ); 891 if ( !arr ) 892 return pre + post; 893 return [ pre, inner + arr, base + post ].join(s); 894 }; 895 function array( arr ) { 896 var i = arr.length, ret = Array(i); 897 this.up(); 898 while ( i-- ) 899 ret[i] = this.parse( arr[i] ); 900 this.down(); 901 return join( '[', ret, ']' ); 902 }; 903 904 var reName = /^function (\w+)/; 905 906 var jsDump = { 907 parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance 908 var parser = this.parsers[ type || this.typeOf(obj) ]; 909 type = typeof parser; 910 911 return type == 'function' ? parser.call( this, obj ) : 912 type == 'string' ? parser : 913 this.parsers.error; 914 }, 915 typeOf:function( obj ) { 916 var type; 917 if ( obj === null ) { 918 type = "null"; 919 } else if (typeof obj === "undefined") { 920 type = "undefined"; 921 } else if (QUnit.is("RegExp", obj)) { 922 type = "regexp"; 923 } else if (QUnit.is("Date", obj)) { 924 type = "date"; 925 } else if (QUnit.is("Function", obj)) { 926 type = "function"; 927 } else if (QUnit.is("Array", obj)) { 928 type = "array"; 929 } else if (QUnit.is("Window", obj) || QUnit.is("global", obj)) { 930 type = "window"; 931 } else if (QUnit.is("HTMLDocument", obj)) { 932 type = "document"; 933 } else if (QUnit.is("HTMLCollection", obj) || QUnit.is("NodeList", obj)) { 934 type = "nodelist"; 935 } else if (/^\[object HTML/.test(Object.prototype.toString.call( obj ))) { 936 type = "node"; 937 } else { 938 type = typeof obj; 939 } 940 return type; 941 }, 942 separator:function() { 943 return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' '; 944 }, 945 indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 946 if ( !this.multiline ) 947 return ''; 948 var chr = this.indentChar; 949 if ( this.HTML ) 950 chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 951 return Array( this._depth_ + (extra||0) ).join(chr); 952 }, 953 up:function( a ) { 954 this._depth_ += a || 1; 955 }, 956 down:function( a ) { 957 this._depth_ -= a || 1; 958 }, 959 setParser:function( name, parser ) { 960 this.parsers[name] = parser; 961 }, 962 // The next 3 are exposed so you can use them 963 quote:quote, 964 literal:literal, 965 join:join, 966 // 967 _depth_: 1, 968 // This is the list of parsers, to modify them, use jsDump.setParser 969 parsers:{ 970 window: '[Window]', 971 document: '[Document]', 972 error:'[ERROR]', //when no parser is found, shouldn't happen 973 unknown: '[Unknown]', 974 'null':'null', 975 undefined:'undefined', 976 'function':function( fn ) { 977 var ret = 'function', 978 name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 979 if ( name ) 980 ret += ' ' + name; 981 ret += '('; 982 983 ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join(''); 984 return join( ret, this.parse(fn,'functionCode'), '}' ); 985 }, 986 array: array, 987 nodelist: array, 988 arguments: array, 989 object:function( map ) { 990 var ret = [ ]; 991 this.up(); 992 for ( var key in map ) 993 ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) ); 994 this.down(); 995 return join( '{', ret, '}' ); 996 }, 997 node:function( node ) { 998 var open = this.HTML ? '<' : '<', 999 close = this.HTML ? '>' : '>'; 1000 1001 var tag = node.nodeName.toLowerCase(), 1002 ret = open + tag; 1003 1004 for ( var a in this.DOMAttrs ) { 1005 var val = node[this.DOMAttrs[a]]; 1006 if ( val ) 1007 ret += ' ' + a + '=' + this.parse( val, 'attribute' ); 1008 } 1009 return ret + close + open + '/' + tag + close; 1010 }, 1011 functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1012 var l = fn.length; 1013 if ( !l ) return ''; 1014 1015 var args = Array(l); 1016 while ( l-- ) 1017 args[l] = String.fromCharCode(97+l);//97 is 'a' 1018 return ' ' + args.join(', ') + ' '; 1019 }, 1020 key:quote, //object calls it internally, the key part of an item in a map 1021 functionCode:'[code]', //function calls it internally, it's the content of the function 1022 attribute:quote, //node calls it internally, it's an html attribute value 1023 string:quote, 1024 date:quote, 1025 regexp:literal, //regex 1026 number:literal, 1027 'boolean':literal 1028 }, 1029 DOMAttrs:{//attributes to dump from nodes, name=>realName 1030 id:'id', 1031 name:'name', 1032 'class':'className' 1033 }, 1034 HTML:true,//if true, entities are escaped ( <, >, \t, space and \n ) 1035 indentChar:' ',//indentation unit 1036 multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1037 }; 1038 1039 return jsDump; 1040 })(); 1041 1042 })(this);