No ticket: Revise unit tests in anticipation of Sizzle-free builds
[jquery.git] / test / data / testrunner.js
blobddbe2b09f9f586452a9462c272c9b20206a25bf3
1 /**
2  * Allow the test suite to run with other libs or jQuery's.
3  */
4 jQuery.noConflict();
6 // For checking globals pollution despite auto-created globals in various environments
7 jQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, name ) {
8         window[ name ] = window[ name ];
9 });
11 // Expose Sizzle for Sizzle's selector tests
12 // We remove Sizzle's globalization in jQuery
13 var Sizzle = Sizzle || jQuery.find;
15 // Allow subprojects to test against their own fixtures
16 var qunitModule = QUnit.module,
17         qunitTest = QUnit.test;
19 function testSubproject( label, url, risTests ) {
20         var sub, fixture, fixtureHTML,
21                 fixtureReplaced = false;
23         // Don't let subproject tests jump the gun
24         QUnit.config.reorder = false;
26         // Create module
27         module( label );
29         // Duckpunch QUnit
30         // TODO restore parent fixture on teardown to support reordering
31         module = QUnit.module = function( name ) {
32                 var args = arguments;
34                 // Remember subproject-scoped module name
35                 sub = name;
37                 // Override
38                 args[0] = label;
39                 return qunitModule.apply( this, args );
40         };
41         test = function( name ) {
42                 var args = arguments,
43                         i = args.length - 1;
45                 // Prepend subproject-scoped module name to test name
46                 args[0] = sub + ": " + name;
48                 // Find test function and wrap to require subproject fixture
49                 for ( ; i >= 0; i-- ) {
50                         if ( originaljQuery.isFunction( args[i] ) ) {
51                                 args[i] = requireFixture( args[i] );
52                                 break;
53                         }
54                 }
56                 return qunitTest.apply( this, args );
57         };
59         // Load tests and fixture from subproject
60         // Test order matters, so we must be synchronous and throw an error on load failure
61         originaljQuery.ajax( url, {
62                 async: false,
63                 dataType: "html",
64                 error: function( jqXHR, status ) {
65                         throw new Error( "Could not load: " + url + " (" + status + ")" );
66                 },
67                 success: function( data, status, jqXHR ) {
68                         var page = originaljQuery.parseHTML(
69                                 // replace html/head with dummy elements so they are represented in the DOM
70                                 ( data || "" ).replace( /<\/?((!DOCTYPE|html|head)\b.*?)>/gi, "[$1]" ),
71                                 document,
72                                 true
73                         );
75                         if ( !page || !page.length ) {
76                                 this.error( jqXHR, "no data" );
77                         }
78                         page = originaljQuery( page );
80                         // Include subproject tests
81                         page.filter("script[src]").add( page.find("script[src]") ).each(function() {
82                                 var src = originaljQuery( this ).attr("src"),
83                                         html = "<script src='" + url + src + "'></script>";
84                                 if ( risTests.test( src ) ) {
85                                         if ( originaljQuery.isReady ) {
86                                                 originaljQuery("head").first().append( html );
87                                         } else {
88                                                 document.write( html );
89                                         }
90                                 }
91                         });
93                         // Get the fixture, including content outside of #qunit-fixture
94                         fixture = page.find("[id='qunit-fixture']");
95                         fixtureHTML = fixture.html();
96                         fixture.empty();
97                         while ( fixture.length && !fixture.prevAll("[id='qunit']").length ) {
98                                 fixture = fixture.parent();
99                         }
100                         fixture = fixture.add( fixture.nextAll() );
101                 }
102         });
104         function requireFixture( fn ) {
105                 return function() {
106                         if ( !fixtureReplaced ) {
107                                 // Make sure that we retrieved a fixture for the subproject
108                                 if ( !fixture.length ) {
109                                         ok( false, "Found subproject fixture" );
110                                         return;
111                                 }
113                                 // Replace the current fixture, including content outside of #qunit-fixture
114                                 var oldFixture = originaljQuery("#qunit-fixture");
115                                 while ( oldFixture.length && !oldFixture.prevAll("[id='qunit']").length ) {
116                                         oldFixture = oldFixture.parent();
117                                 }
118                                 oldFixture.nextAll().remove();
119                                 oldFixture.replaceWith( fixture );
121                                 // WARNING: UNDOCUMENTED INTERFACE
122                                 QUnit.config.fixture = fixtureHTML;
123                                 QUnit.reset();
124                                 if ( originaljQuery("#qunit-fixture").html() !== fixtureHTML ) {
125                                         ok( false, "Copied subproject fixture" );
126                                         return;
127                                 }
129                                 fixtureReplaced = true;
130                         }
132                         fn.apply( this, arguments );
133                 };
134         }
137 // Register globals for cleanup and the cleanup code itself
138 // Explanation at http://perfectionkills.com/understanding-delete/#ie_bugs
139 var Globals = (function() {
140         var globals = {};
141         return {
142                 register: function( name ) {
143                         globals[ name ] = true;
144                         jQuery.globalEval( "var " + name + " = undefined;" );
145                 },
146                 cleanup: function() {
147                         var name,
148                                 current = globals;
149                         globals = {};
150                         for ( name in current ) {
151                                 jQuery.globalEval( "try { " +
152                                         "delete " + ( jQuery.support.deleteExpando ? "window['" + name + "']" : name ) +
153                                 "; } catch( x ) {}" );
154                         }
155                 }
156         };
157 })();
159 // Sandbox start for great justice
160 (function() {
161         var oldStart = window.start;
162         window.start = function() {
163                 oldStart();
164         };
165 })();
168  * QUnit hooks
169  */
170 (function() {
171         // Store the old counts so that we only assert on tests that have actually leaked,
172         // instead of asserting every time a test has leaked sometime in the past
173         var oldCacheLength = 0,
174                 oldFragmentsLength = 0,
175                 oldActive = 0,
177                 expectedDataKeys = {},
179                 splice = [].splice,
180                 reset = QUnit.reset,
181                 ajaxSettings = jQuery.ajaxSettings;
183         function keys(o) {
184                 var ret, key;
185                 if ( Object.keys ) {
186                         ret = Object.keys( o );
187                 } else {
188                         ret = [];
189                         for ( key in o ) {
190                                 ret.push( key );
191                         }
192                 }
193                 ret.sort();
194                 return ret;
195         }
197         /**
198          * @param {jQuery|HTMLElement|Object|Array} elems Target (or array of targets) for jQuery.data.
199          * @param {string} key
200          */
201         QUnit.expectJqData = function( elems, key ) {
202                 var i, elem, expando;
204                 // As of jQuery 2.0, there will be no "cache"-data is
205                 // stored and managed completely below the API surface
206                 if ( jQuery.cache ) {
207                         QUnit.current_testEnvironment.checkJqData = true;
209                         if ( elems.jquery && elems.toArray ) {
210                                 elems = elems.toArray();
211                         }
212                         if ( !jQuery.isArray( elems ) ) {
213                                 elems = [ elems ];
214                         }
216                         for ( i = 0; i < elems.length; i++ ) {
217                                 elem = elems[i];
219                                 // jQuery.data only stores data for nodes in jQuery.cache,
220                                 // for other data targets the data is stored in the object itself,
221                                 // in that case we can't test that target for memory leaks.
222                                 // But we don't have to since in that case the data will/must will
223                                 // be available as long as the object is not garbage collected by
224                                 // the js engine, and when it is, the data will be removed with it.
225                                 if ( !elem.nodeType ) {
226                                         // Fixes false positives for dataTests(window), dataTests({}).
227                                         continue;
228                                 }
230                                 expando = elem[ jQuery.expando ];
232                                 if ( expando === undefined ) {
233                                         // In this case the element exists fine, but
234                                         // jQuery.data (or internal data) was never (in)directly
235                                         // called.
236                                         // Since this method was called it means some data was
237                                         // expected to be found, but since there is nothing, fail early
238                                         // (instead of in teardown).
239                                         notStrictEqual( expando, undefined, "Target for expectJqData must have an expando, for else there can be no data to expect." );
240                                 } else {
241                                         if ( expectedDataKeys[expando] ) {
242                                                 expectedDataKeys[expando].push( key );
243                                         } else {
244                                                 expectedDataKeys[expando] = [ key ];
245                                         }
246                                 }
247                         }
248                 }
250         };
251         QUnit.config.urlConfig.push( {
252                 id: "jqdata",
253                 label: "Always check jQuery.data",
254                 tooltip: "Trigger QUnit.expectJqData detection for all tests instead of just the ones that call it"
255         } );
257         /**
258          * Ensures that tests have cleaned up properly after themselves. Should be passed as the
259          * teardown function on all modules' lifecycle object.
260          */
261         this.moduleTeardown = function() {
262                 var i,
263                         expectedKeys, actualKeys,
264                         fragmentsLength = 0,
265                         cacheLength = 0;
267                 // Only look for jQuery data problems if this test actually
268                 // provided some information to compare against.
269                 if ( QUnit.urlParams.jqdata || this.checkJqData ) {
270                         for ( i in jQuery.cache ) {
271                                 expectedKeys = expectedDataKeys[i];
272                                 actualKeys = jQuery.cache[i] ? keys( jQuery.cache[i] ) : jQuery.cache[i];
273                                 if ( !QUnit.equiv( expectedKeys, actualKeys ) ) {
274                                         deepEqual( actualKeys, expectedKeys, "Expected keys exist in jQuery.cache" );
275                                 }
276                                 delete jQuery.cache[i];
277                                 delete expectedDataKeys[i];
278                         }
279                         // In case it was removed from cache before (or never there in the first place)
280                         for ( i in expectedDataKeys ) {
281                                 deepEqual( expectedDataKeys[i], undefined, "No unexpected keys were left in jQuery.cache (#" + i + ")" );
282                                 delete expectedDataKeys[i];
283                         }
284                 }
286                 // Reset data register
287                 expectedDataKeys = {};
289                 // Check for (and clean up, if possible) incomplete animations/requests/etc.
290                 if ( jQuery.timers && jQuery.timers.length !== 0 ) {
291                         equal( jQuery.timers.length, 0, "No timers are still running" );
292                         splice.call( jQuery.timers, 0, jQuery.timers.length );
293                         jQuery.fx.stop();
294                 }
295                 if ( jQuery.active !== undefined && jQuery.active !== oldActive ) {
296                         equal( jQuery.active, oldActive, "No AJAX requests are still active" );
297                         if ( ajaxTest.abort ) {
298                                 ajaxTest.abort("active requests");
299                         }
300                         oldActive = jQuery.active;
301                 }
303                 // Allow QUnit.reset to clean up any attached elements before checking for leaks
304                 QUnit.reset();
306                 for ( i in jQuery.cache ) {
307                         ++cacheLength;
308                 }
310                 jQuery.fragments = {};
312                 for ( i in jQuery.fragments ) {
313                         ++fragmentsLength;
314                 }
316                 // Because QUnit doesn't have a mechanism for retrieving the number of expected assertions for a test,
317                 // if we unconditionally assert any of these, the test will fail with too many assertions :|
318                 if ( cacheLength !== oldCacheLength ) {
319                         equal( cacheLength, oldCacheLength, "No unit tests leak memory in jQuery.cache" );
320                         oldCacheLength = cacheLength;
321                 }
322                 if ( fragmentsLength !== oldFragmentsLength ) {
323                         equal( fragmentsLength, oldFragmentsLength, "No unit tests leak memory in jQuery.fragments" );
324                         oldFragmentsLength = fragmentsLength;
325                 }
326         };
328         QUnit.done(function() {
329                 // Remove our own fixtures outside #qunit-fixture
330                 jQuery("#qunit ~ *").remove();
331         });
333         // jQuery-specific QUnit.reset
334         QUnit.reset = function() {
336                 // Ensure jQuery events and data on the fixture are properly removed
337                 jQuery("#qunit-fixture").empty();
339                 // Reset internal jQuery state
340                 jQuery.event.global = {};
341                 if ( ajaxSettings ) {
342                         jQuery.ajaxSettings = jQuery.extend( true, {}, ajaxSettings );
343                 } else {
344                         delete jQuery.ajaxSettings;
345                 }
347                 // Cleanup globals
348                 Globals.cleanup();
350                 // Let QUnit reset the fixture
351                 reset.apply( this, arguments );
352         };
353 })();
356  * QUnit configuration
357  */
358 // Max time for stop() and asyncTest() until it aborts test
359 // and start()'s the next test.
360 QUnit.config.testTimeout = 20 * 1000; // 20 seconds
362 // Enforce an "expect" argument or expect() call in all test bodies.
363 QUnit.config.requireExpects = true;
366  * Load the TestSwarm listener if swarmURL is in the address.
367  */
368 (function() {
369         var url = window.location.search;
370         url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + "swarmURL=".length ) );
372         if ( !url || url.indexOf("http") !== 0 ) {
373                 return;
374         }
376         document.write("<scr" + "ipt src='http://swarm.jquery.org/js/inject.js?" + (new Date()).getTime() + "'></scr" + "ipt>");
377 })();