├── tests ├── ajax.htm ├── index.htm ├── qunit.css ├── tests.js └── qunit.js ├── README.markdown ├── example.htm └── jquery.create.js /tests/ajax.htm: -------------------------------------------------------------------------------- 1 |

2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # jQuery "create" event 2 | 3 | This event wraps `$.fn.domManip` and `$.fn.html` to fire a custom "create" event when new, matching elements are inserted into the DOM. 4 | 5 | Use it like: 6 | 7 | // bind a handler that will fire when new "div.foo" 8 | // elements are inserted into the DOM 9 | $("div.foo").live("create", function(e){ 10 | $(this).doSomething(); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

jQuery Create Event Unit Tests

14 |

15 |

16 |
    17 | 18 |
    19 |
    20 |
    21 | 22 |
    23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jQuery Create Event Demos - Eric Hynds 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 |
    17 |

    erichynds

    18 |
    « Return to Blog Post
    19 |
    20 | 21 |
    22 |

    jQuery "create" Event Demo

    23 | 24 |

    This new event duck punches $.fn.domManip and $.fn.html to fire a custom "create" event when new, matching elements are inserted into the DOM.

    25 | 26 | 30 | 31 |

    Code for this demo:

    32 | 33 |
    34 | $.fn.geocities(function(){
    35 |    /* 
    36 |       code to make a div look friggin sweet.
    37 |       wraps text in marquee and applies a 
    38 |       beautiful color scheme.
    39 |    */
    40 | });
    41 | 
    42 | // apply plugin to all current div.geocities
    43 | $("div.geocities").geocities();
    44 | 
    45 | // apply plugin to all future div.geocities
    46 | $("div.geocities").live("create", function(e){
    47 | 	$(this).text("I am a new element!").geocities();
    48 | });
    49 | 
    50 | // the "create another" link
    51 | $("#create-new-div").click(function(){
    52 | 	$("div.geocities:last").after('<div class="geocities"></div>');
    53 | 	return false;
    54 | });
    55 | 
    56 | 57 |

    Create another

    58 |
    Welcome to 1999. Look at me go!!
    59 |
    60 | 61 | 62 | 63 | 64 | 65 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /tests/qunit.css: -------------------------------------------------------------------------------- 1 | 2 | ol#qunit-tests { 3 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 4 | margin:0; 5 | padding:0; 6 | list-style-position:inside; 7 | 8 | font-size: smaller; 9 | } 10 | ol#qunit-tests li{ 11 | padding:0.4em 0.5em 0.4em 2.5em; 12 | border-bottom:1px solid #fff; 13 | font-size:small; 14 | list-style-position:inside; 15 | } 16 | ol#qunit-tests li ol{ 17 | box-shadow: inset 0px 2px 13px #999; 18 | -moz-box-shadow: inset 0px 2px 13px #999; 19 | -webkit-box-shadow: inset 0px 2px 13px #999; 20 | margin-top:0.5em; 21 | margin-left:0; 22 | padding:0.5em; 23 | background-color:#fff; 24 | border-radius:15px; 25 | -moz-border-radius: 15px; 26 | -webkit-border-radius: 15px; 27 | } 28 | ol#qunit-tests li li{ 29 | border-bottom:none; 30 | margin:0.5em; 31 | background-color:#fff; 32 | list-style-position: inside; 33 | padding:0.4em 0.5em 0.4em 0.5em; 34 | } 35 | 36 | ol#qunit-tests li li.pass{ 37 | border-left:26px solid #C6E746; 38 | background-color:#fff; 39 | color:#5E740B; 40 | } 41 | ol#qunit-tests li li.fail{ 42 | border-left:26px solid #EE5757; 43 | background-color:#fff; 44 | color:#710909; 45 | } 46 | ol#qunit-tests li.pass{ 47 | background-color:#D2E0E6; 48 | color:#528CE0; 49 | } 50 | ol#qunit-tests li.fail{ 51 | background-color:#EE5757; 52 | color:#000; 53 | } 54 | ol#qunit-tests li strong { 55 | cursor:pointer; 56 | } 57 | h1#qunit-header{ 58 | background-color:#0d3349; 59 | margin:0; 60 | padding:0.5em 0 0.5em 1em; 61 | color:#fff; 62 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 63 | border-top-right-radius:15px; 64 | border-top-left-radius:15px; 65 | -moz-border-radius-topright:15px; 66 | -moz-border-radius-topleft:15px; 67 | -webkit-border-top-right-radius:15px; 68 | -webkit-border-top-left-radius:15px; 69 | text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; 70 | } 71 | h2#qunit-banner{ 72 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 73 | height:5px; 74 | margin:0; 75 | padding:0; 76 | } 77 | h2#qunit-banner.qunit-pass{ 78 | background-color:#C6E746; 79 | } 80 | h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { 81 | background-color:#EE5757; 82 | } 83 | #qunit-testrunner-toolbar { 84 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 85 | padding:0; 86 | /*width:80%;*/ 87 | padding:0em 0 0.5em 2em; 88 | font-size: small; 89 | } 90 | h2#qunit-userAgent { 91 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 92 | background-color:#2b81af; 93 | margin:0; 94 | padding:0; 95 | color:#fff; 96 | font-size: small; 97 | padding:0.5em 0 0.5em 2.5em; 98 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 99 | } 100 | p#qunit-testresult{ 101 | font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 102 | margin:0; 103 | font-size: small; 104 | color:#2b81af; 105 | border-bottom-right-radius:15px; 106 | border-bottom-left-radius:15px; 107 | -moz-border-radius-bottomright:15px; 108 | -moz-border-radius-bottomleft:15px; 109 | -webkit-border-bottom-right-radius:15px; 110 | -webkit-border-bottom-left-radius:15px; 111 | background-color:#D2E0E6; 112 | padding:0.5em 0.5em 0.5em 2.5em; 113 | } 114 | strong b.fail{ 115 | color:#710909; 116 | } 117 | strong b.pass{ 118 | color:#5E740B; 119 | } 120 | -------------------------------------------------------------------------------- /jquery.create.js: -------------------------------------------------------------------------------- 1 | // written by eric hynds (erichynds.com) 2 | // http://www.erichynds.com/jquery/jquery-create-event/ 3 | // version 1.4 - 10/12/2010 4 | 5 | (function($, _domManip, _html){ 6 | var selectors = [], gen = [], guid = 0, old = {}; 7 | 8 | $.event.special.create = { 9 | add: function( data ){ 10 | selectors.push( data.selector ); 11 | }, 12 | 13 | // won't fire in 1.4.2 http://dev.jquery.com/ticket/6202 14 | remove: function( data ){ 15 | var len = selectors.length; 16 | 17 | while( len-- ){ 18 | if( selectors[len] === data.selector ){ 19 | selectors.splice(len, 1); 20 | break; 21 | } 22 | } 23 | } 24 | }; 25 | 26 | // deal with 99% of DOM manip methods 27 | $.fn.domManip = function( args, table, callback ){ 28 | 29 | // if no create events are bound, just fire the original domManip method 30 | if( !selectors.length || $.isFunction(args[0]) ){ 31 | return _domManip.apply( this, arguments ); 32 | } 33 | 34 | return logic.call( this, _domManip, arguments ); 35 | }; 36 | 37 | // deal with the remaining 1% (html method) 38 | $.fn.html = function( value ){ 39 | 40 | // if no create events are bound, html() is being called as a setter, 41 | // or the value is a function, fire the original and peace out. only string values use innerHTML; 42 | // function values use append() which is covered by $.fn.domManip 43 | if( !selectors.length || $.isFunction(value) || typeof value === "undefined" || !value.length ){ 44 | return _html.apply( this, arguments ); 45 | } 46 | 47 | // make value an array 48 | arguments[0] = [value]; 49 | return logic.call( this, _html, arguments ); 50 | }; 51 | 52 | function logic( method, args ){ 53 | var node, nodes = args[0], html = $(), numSelectors = selectors.length, matches = []; 54 | 55 | // crawl through the html structure passed in looking for matching elements. 56 | for( var i=0, len=nodes.length; i'); 8 | target = $("#target"); 9 | }; 10 | 11 | function handler(){ 12 | ok(true, 'create handler fired'); 13 | return false; 14 | } 15 | 16 | test("multiple elements, complex html structure", function(){ 17 | var html = []; 18 | 19 | expect(8); 20 | 21 | $("span.one, #test-p, #test-span").live("create", handler); 22 | 23 | html.push('
    '); 24 | html.push('

    the

    '); 25 | html.push('

    quickbrown

    '); 26 | html.push('fox'); 27 | html.push('jumped'); 28 | html.push('

    overthe

    lazy dog
    '); 29 | html.push('
    '); 30 | html.push('

    '); 31 | html.push(''); 32 | html.push(''); 33 | 34 | $( html.join('') ).appendTo( target ); 35 | 36 | // now, make sure everything actually made it to the DOM 37 | equals( $(".one").length, 4, "node 1: div element with 4 nested .one elements" ); 38 | equals( $("#test-p").length, 1, "node 2: p element with id test-p" ); 39 | equals( $("#test-span").length, 1, "node 3: span element with id test-span" ); 40 | equals( $("select").length, 1, "node 4: select element" ); 41 | }); 42 | 43 | test("generated/enumerated IDs", function(){ 44 | expect(4); 45 | var el; 46 | 47 | $("span.test-id, #test-id").live("create", handler); 48 | 49 | el = $('').appendTo(target); 50 | equals( el.attr("id"), "", "generated id was removed" ); 51 | el = $('').appendTo(target); 52 | equals( el.attr("id"), "test-id", "enumerated ID was not removed" ); 53 | }); 54 | 55 | module("methods"); 56 | 57 | test("append", function(){ 58 | expect(1); 59 | 60 | $("#test-append").live("create", handler); 61 | target.append('
    '); 62 | }); 63 | 64 | test("append (fn)", function(){ 65 | expect(1); 66 | 67 | $("#test-append-fn").live("create", handler); 68 | target.append(function(){ 69 | return '
    ' 70 | }); 71 | }); 72 | 73 | test("prepend", function(){ 74 | expect(1); 75 | 76 | $("#test-prepend").live("create", handler); 77 | target.prepend('
    '); 78 | }); 79 | 80 | test("prepend (fn)", function(){ 81 | expect(1); 82 | 83 | $("#test-prepend-fn").live("create", handler); 84 | target.prepend(function(){ 85 | return '
    ' 86 | }); 87 | }); 88 | 89 | test("before", function(){ 90 | expect(1); 91 | 92 | $("#test-before").live("create", handler); 93 | target.before('
    '); 94 | }); 95 | 96 | test("before (fn)", function(){ 97 | expect(1); 98 | 99 | $("#test-before-fn").live("create", handler); 100 | target.before(function(){ 101 | return '
    ' 102 | }); 103 | }); 104 | 105 | test("after", function(){ 106 | expect(1); 107 | 108 | $("#test-after").live("create", handler); 109 | target.after('
    '); 110 | }); 111 | 112 | test("after (fn)", function(){ 113 | expect(1); 114 | 115 | $("#test-after-fn").live("create", handler); 116 | target.after(function(){ 117 | return '
    ' 118 | }); 119 | }); 120 | 121 | test("insertBefore", function(){ 122 | expect(1); 123 | 124 | $("#test-insertBefore").live("create", handler); 125 | $('
    ').insertBefore( target ); 126 | }); 127 | 128 | test("insertAfter", function(){ 129 | expect(1); 130 | 131 | $("#test-insertAfter").live("create", handler); 132 | $('
    ').insertAfter( target ); 133 | }); 134 | 135 | test("wrap", function(){ 136 | expect(1); 137 | 138 | $("#test-wrap").live("create", handler); 139 | target.wrap('
    '); 140 | }); 141 | 142 | test("wrap (fn)", function(){ 143 | expect(1); 144 | 145 | $("#test-wrap-fn").live("create", handler); 146 | target.wrap(function(){ 147 | return '
    '; 148 | }); 149 | }); 150 | 151 | test("replaceWith", function(){ 152 | expect(2); 153 | 154 | $(".test-replaceWith").live("create", handler); 155 | 156 | // add a new node 157 | var elem = $('
    ').appendTo("body"); 158 | 159 | // replace target with new html 160 | target.replaceWith('
    '); 161 | 162 | // replace target with an existing node.. should not trigger event 163 | target.replaceWith( elem ); 164 | 165 | elem.remove(); 166 | }); 167 | 168 | test("replaceWith (fn)", function(){ 169 | expect(2); 170 | 171 | $(".test-replaceWith-fn").live("create", handler); 172 | 173 | // add a new node 174 | var elem = $('
    ').appendTo("body"); 175 | 176 | // replace target with new html 177 | target.replaceWith(function(){ 178 | return '
    '; 179 | }); 180 | 181 | // replace target with an existing node.. should not trigger event 182 | target.replaceWith(function(){ 183 | return elem; 184 | }); 185 | 186 | elem.remove(); 187 | }); 188 | 189 | test("replaceAll", function(){ 190 | expect(1); 191 | 192 | target.html(''); 193 | 194 | $("p.replaceAll").live("create", handler); 195 | 196 | // replace target with new html 197 | $('

    ').replaceAll('#target span'); 198 | 199 | // one is returned because the p element is created once, then cloned 200 | // two more times. TODO look into this 201 | }); 202 | 203 | test("cloning then insert", function(){ 204 | expect(1); 205 | 206 | target.append('
    '); 207 | $(".test-clone").live("create", handler); 208 | target.find(".test-clone").clone().appendTo( target ); 209 | }); 210 | 211 | test("html", function(){ 212 | expect(3); 213 | 214 | $("#test-html-div, #test-html-p, span.test-html-span").live("create", handler); 215 | target.html('

    '); 216 | }); 217 | 218 | test("html (fn)", function(){ 219 | expect(3); 220 | 221 | $("#test-html-fn, #test-html-fn-span, #test-html-fn span").live("create", handler); 222 | $("#htmltarget").html(function(){ 223 | return '

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