Convert testrunner to an AMD module and ensure jQuery is on the page when executing the testrunner (another race condition amplified by swarm)

This commit is contained in:
Timmy Willison 2013-09-06 17:11:04 -04:00
parent 4ca5a0c691
commit 5093b89f08
5 changed files with 302 additions and 292 deletions

View File

@ -22,6 +22,7 @@
"globals": { "globals": {
"require": false, "require": false,
"define": false,
"DOMParser": false, "DOMParser": false,
"QUnit": false, "QUnit": false,
"ok": false, "ok": false,

View File

@ -259,9 +259,13 @@ this.iframeCallback = undefined;
QUnit.config.autostart = false; QUnit.config.autostart = false;
this.loadTests = function() { this.loadTests = function() {
var loadSwarm, var loadSwarm,
url = window.location.search, url = window.location.search;
tests = [ url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + "swarmURL=".length ) );
"data/testrunner.js", loadSwarm = url && url.indexOf("http") === 0;
// Get testSubproject from testrunner first
require([ "data/testrunner.js" ], function( testSubproject ) {
var tests = [
"unit/core.js", "unit/core.js",
"unit/callbacks.js", "unit/callbacks.js",
"unit/deferred.js", "unit/deferred.js",
@ -281,8 +285,6 @@ this.loadTests = function() {
"unit/offset.js", "unit/offset.js",
"unit/dimensions.js" "unit/dimensions.js"
]; ];
url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + "swarmURL=".length ) );
loadSwarm = url && url.indexOf("http") === 0;
// Ensure load order (to preserve test numbers) // Ensure load order (to preserve test numbers)
(function loadDep() { (function loadDep() {
@ -291,19 +293,10 @@ this.loadTests = function() {
require( [ dep ], loadDep ); require( [ dep ], loadDep );
} else { } else {
// Subproject tests must be last because they replace our test fixture // Subproject tests must be last because they replace our test fixture
window.testSubproject( "Sizzle", "../bower_components/sizzle/test/", /^unit\/.*\.js$/, function() { testSubproject( "Sizzle", "../bower_components/sizzle/test/", /^unit\/.*\.js$/, function() {
// Call load to build module filter select element // Call load to build module filter select element
QUnit.load(); QUnit.load();
// Load the TestSwarm listener if swarmURL is in the address.
if ( loadSwarm ) {
require( [ "http://swarm.jquery.org/js/inject.js?" + (new Date()).getTime() ], function() {
QUnit.start();
});
} else {
QUnit.start();
}
/** /**
* Run in noConflict mode * Run in noConflict mode
*/ */
@ -317,7 +310,17 @@ this.loadTests = function() {
supportjQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, name ) { supportjQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, name ) {
window[ name ] = window[ name ]; window[ name ] = window[ name ];
}); });
// Load the TestSwarm listener if swarmURL is in the address.
if ( loadSwarm ) {
require( [ "http://swarm.jquery.org/js/inject.js?" + (new Date()).getTime() ], function() {
QUnit.start();
});
} else {
QUnit.start();
}
}); });
} }
})(); })();
});
}; };

View File

@ -1,7 +1,236 @@
define(function() {
// Allow subprojects to test against their own fixtures // Allow subprojects to test against their own fixtures
var qunitModule = QUnit.module, var oldStart = window.start,
qunitTest = QUnit.test; qunitModule = QUnit.module,
qunitTest = QUnit.test,
// Store the old counts so that we only assert on tests that have actually leaked,
// instead of asserting every time a test has leaked sometime in the past
oldCacheLength = 0,
oldFragmentsLength = 0,
oldActive = 0,
expectedDataKeys = {},
splice = [].splice,
reset = QUnit.reset,
ajaxSettings = jQuery.ajaxSettings;
/**
* QUnit configuration
*/
// Max time for stop() and asyncTest() until it aborts test
// and start()'s the next test.
QUnit.config.testTimeout = 20 * 1000; // 20 seconds
// Enforce an "expect" argument or expect() call in all test bodies.
QUnit.config.requireExpects = true;
/**
* QUnit hooks
*/
// Sandbox start for great justice
window.start = function() {
oldStart();
};
function keys(o) {
var ret, key;
if ( Object.keys ) {
ret = Object.keys( o );
} else {
ret = [];
for ( key in o ) {
ret.push( key );
}
}
ret.sort();
return ret;
}
/**
* @param {jQuery|HTMLElement|Object|Array} elems Target (or array of targets) for jQuery.data.
* @param {string} key
*/
QUnit.expectJqData = function( elems, key ) {
var i, elem, expando;
// As of jQuery 2.0, there will be no "cache"-data is
// stored and managed completely below the API surface
if ( jQuery.cache ) {
QUnit.current_testEnvironment.checkJqData = true;
if ( elems.jquery && elems.toArray ) {
elems = elems.toArray();
}
if ( !supportjQuery.isArray( elems ) ) {
elems = [ elems ];
}
for ( i = 0; i < elems.length; i++ ) {
elem = elems[i];
// jQuery.data only stores data for nodes in jQuery.cache,
// for other data targets the data is stored in the object itself,
// in that case we can't test that target for memory leaks.
// But we don't have to since in that case the data will/must will
// be available as long as the object is not garbage collected by
// the js engine, and when it is, the data will be removed with it.
if ( !elem.nodeType ) {
// Fixes false positives for dataTests(window), dataTests({}).
continue;
}
expando = elem[ jQuery.expando ];
if ( expando === undefined ) {
// In this case the element exists fine, but
// jQuery.data (or internal data) was never (in)directly
// called.
// Since this method was called it means some data was
// expected to be found, but since there is nothing, fail early
// (instead of in teardown).
notStrictEqual( expando, undefined, "Target for expectJqData must have an expando, for else there can be no data to expect." );
} else {
if ( expectedDataKeys[expando] ) {
expectedDataKeys[expando].push( key );
} else {
expectedDataKeys[expando] = [ key ];
}
}
}
}
};
QUnit.config.urlConfig.push({
id: "jqdata",
label: "Always check jQuery.data",
tooltip: "Trigger QUnit.expectJqData detection for all tests instead of just the ones that call it"
});
/**
* Ensures that tests have cleaned up properly after themselves. Should be passed as the
* teardown function on all modules' lifecycle object.
*/
window.moduleTeardown = function() {
var i,
expectedKeys, actualKeys,
fragmentsLength = 0,
cacheLength = 0;
// Only look for jQuery data problems if this test actually
// provided some information to compare against.
if ( QUnit.urlParams.jqdata || this.checkJqData ) {
for ( i in jQuery.cache ) {
expectedKeys = expectedDataKeys[i];
actualKeys = jQuery.cache[i] ? keys( jQuery.cache[i] ) : jQuery.cache[i];
if ( !QUnit.equiv( expectedKeys, actualKeys ) ) {
deepEqual( actualKeys, expectedKeys, "Expected keys exist in jQuery.cache" );
}
delete jQuery.cache[i];
delete expectedDataKeys[i];
}
// In case it was removed from cache before (or never there in the first place)
for ( i in expectedDataKeys ) {
deepEqual( expectedDataKeys[i], undefined, "No unexpected keys were left in jQuery.cache (#" + i + ")" );
delete expectedDataKeys[i];
}
}
// Reset data register
expectedDataKeys = {};
// Check for (and clean up, if possible) incomplete animations/requests/etc.
if ( jQuery.timers && jQuery.timers.length !== 0 ) {
equal( jQuery.timers.length, 0, "No timers are still running" );
splice.call( jQuery.timers, 0, jQuery.timers.length );
jQuery.fx.stop();
}
if ( jQuery.active !== undefined && jQuery.active !== oldActive ) {
equal( jQuery.active, oldActive, "No AJAX requests are still active" );
if ( ajaxTest.abort ) {
ajaxTest.abort("active requests");
}
oldActive = jQuery.active;
}
// Allow QUnit.reset to clean up any attached elements before checking for leaks
QUnit.reset();
for ( i in jQuery.cache ) {
++cacheLength;
}
jQuery.fragments = {};
for ( i in jQuery.fragments ) {
++fragmentsLength;
}
// Because QUnit doesn't have a mechanism for retrieving the number of expected assertions for a test,
// if we unconditionally assert any of these, the test will fail with too many assertions :|
if ( cacheLength !== oldCacheLength ) {
equal( cacheLength, oldCacheLength, "No unit tests leak memory in jQuery.cache" );
oldCacheLength = cacheLength;
}
if ( fragmentsLength !== oldFragmentsLength ) {
equal( fragmentsLength, oldFragmentsLength, "No unit tests leak memory in jQuery.fragments" );
oldFragmentsLength = fragmentsLength;
}
};
QUnit.done(function() {
// Remove our own fixtures outside #qunit-fixture
supportjQuery("#qunit ~ *").remove();
});
// jQuery-specific QUnit.reset
QUnit.reset = function() {
// Ensure jQuery events and data on the fixture are properly removed
jQuery("#qunit-fixture").empty();
// ...even if the jQuery under test has a broken .empty()
supportjQuery("#qunit-fixture").empty();
// Reset internal jQuery state
jQuery.event.global = {};
if ( ajaxSettings ) {
jQuery.ajaxSettings = jQuery.extend( true, {}, ajaxSettings );
} else {
delete jQuery.ajaxSettings;
}
// Cleanup globals
Globals.cleanup();
// Let QUnit reset the fixture
reset.apply( this, arguments );
};
// Register globals for cleanup and the cleanup code itself
// Explanation at http://perfectionkills.com/understanding-delete/#ie_bugs
window.Globals = (function() {
var globals = {};
return {
register: function( name ) {
globals[ name ] = true;
supportjQuery.globalEval( "var " + name + " = undefined;" );
},
cleanup: function() {
var name,
current = globals;
globals = {};
for ( name in current ) {
supportjQuery.globalEval( "try { " +
"delete " + ( supportjQuery.support.deleteExpando ? "window['" + name + "']" : name ) +
"; } catch( x ) {}" );
}
}
};
})();
/** /**
* Test a subproject with its own fixture * Test a subproject with its own fixture
@ -9,7 +238,7 @@ var qunitModule = QUnit.module,
* @param {String} url Test folder location * @param {String} url Test folder location
* @param {RegExp} risTests To filter script sources * @param {RegExp} risTests To filter script sources
*/ */
this.testSubproject = function( label, url, risTests, complete ) { function testSubproject( label, url, risTests, complete ) {
var sub, fixture, fixtureHTML, var sub, fixture, fixtureHTML,
fixtureReplaced = false; fixtureReplaced = false;
@ -131,236 +360,8 @@ this.testSubproject = function( label, url, risTests, complete ) {
fn.apply( this, arguments ); fn.apply( this, arguments );
}; };
} }
}; }
// Register globals for cleanup and the cleanup code itself return testSubproject;
// Explanation at http://perfectionkills.com/understanding-delete/#ie_bugs
this.Globals = (function() {
var globals = {};
return {
register: function( name ) {
globals[ name ] = true;
supportjQuery.globalEval( "var " + name + " = undefined;" );
},
cleanup: function() {
var name,
current = globals;
globals = {};
for ( name in current ) {
supportjQuery.globalEval( "try { " +
"delete " + ( supportjQuery.support.deleteExpando ? "window['" + name + "']" : name ) +
"; } catch( x ) {}" );
}
}
};
})();
});
/**
* QUnit hooks
*/
// Sandbox start for great justice
(function() {
var oldStart = window.start;
window.start = function() {
oldStart();
};
})();
(function() {
// Store the old counts so that we only assert on tests that have actually leaked,
// instead of asserting every time a test has leaked sometime in the past
var oldCacheLength = 0,
oldFragmentsLength = 0,
oldActive = 0,
expectedDataKeys = {},
splice = [].splice,
reset = QUnit.reset,
ajaxSettings = jQuery.ajaxSettings;
function keys(o) {
var ret, key;
if ( Object.keys ) {
ret = Object.keys( o );
} else {
ret = [];
for ( key in o ) {
ret.push( key );
}
}
ret.sort();
return ret;
}
/**
* @param {jQuery|HTMLElement|Object|Array} elems Target (or array of targets) for jQuery.data.
* @param {string} key
*/
QUnit.expectJqData = function( elems, key ) {
var i, elem, expando;
// As of jQuery 2.0, there will be no "cache"-data is
// stored and managed completely below the API surface
if ( jQuery.cache ) {
QUnit.current_testEnvironment.checkJqData = true;
if ( elems.jquery && elems.toArray ) {
elems = elems.toArray();
}
if ( !supportjQuery.isArray( elems ) ) {
elems = [ elems ];
}
for ( i = 0; i < elems.length; i++ ) {
elem = elems[i];
// jQuery.data only stores data for nodes in jQuery.cache,
// for other data targets the data is stored in the object itself,
// in that case we can't test that target for memory leaks.
// But we don't have to since in that case the data will/must will
// be available as long as the object is not garbage collected by
// the js engine, and when it is, the data will be removed with it.
if ( !elem.nodeType ) {
// Fixes false positives for dataTests(window), dataTests({}).
continue;
}
expando = elem[ jQuery.expando ];
if ( expando === undefined ) {
// In this case the element exists fine, but
// jQuery.data (or internal data) was never (in)directly
// called.
// Since this method was called it means some data was
// expected to be found, but since there is nothing, fail early
// (instead of in teardown).
notStrictEqual( expando, undefined, "Target for expectJqData must have an expando, for else there can be no data to expect." );
} else {
if ( expectedDataKeys[expando] ) {
expectedDataKeys[expando].push( key );
} else {
expectedDataKeys[expando] = [ key ];
}
}
}
}
};
QUnit.config.urlConfig.push({
id: "jqdata",
label: "Always check jQuery.data",
tooltip: "Trigger QUnit.expectJqData detection for all tests instead of just the ones that call it"
});
/**
* Ensures that tests have cleaned up properly after themselves. Should be passed as the
* teardown function on all modules' lifecycle object.
*/
this.moduleTeardown = function() {
var i,
expectedKeys, actualKeys,
fragmentsLength = 0,
cacheLength = 0;
// Only look for jQuery data problems if this test actually
// provided some information to compare against.
if ( QUnit.urlParams.jqdata || this.checkJqData ) {
for ( i in jQuery.cache ) {
expectedKeys = expectedDataKeys[i];
actualKeys = jQuery.cache[i] ? keys( jQuery.cache[i] ) : jQuery.cache[i];
if ( !QUnit.equiv( expectedKeys, actualKeys ) ) {
deepEqual( actualKeys, expectedKeys, "Expected keys exist in jQuery.cache" );
}
delete jQuery.cache[i];
delete expectedDataKeys[i];
}
// In case it was removed from cache before (or never there in the first place)
for ( i in expectedDataKeys ) {
deepEqual( expectedDataKeys[i], undefined, "No unexpected keys were left in jQuery.cache (#" + i + ")" );
delete expectedDataKeys[i];
}
}
// Reset data register
expectedDataKeys = {};
// Check for (and clean up, if possible) incomplete animations/requests/etc.
if ( jQuery.timers && jQuery.timers.length !== 0 ) {
equal( jQuery.timers.length, 0, "No timers are still running" );
splice.call( jQuery.timers, 0, jQuery.timers.length );
jQuery.fx.stop();
}
if ( jQuery.active !== undefined && jQuery.active !== oldActive ) {
equal( jQuery.active, oldActive, "No AJAX requests are still active" );
if ( ajaxTest.abort ) {
ajaxTest.abort("active requests");
}
oldActive = jQuery.active;
}
// Allow QUnit.reset to clean up any attached elements before checking for leaks
QUnit.reset();
for ( i in jQuery.cache ) {
++cacheLength;
}
jQuery.fragments = {};
for ( i in jQuery.fragments ) {
++fragmentsLength;
}
// Because QUnit doesn't have a mechanism for retrieving the number of expected assertions for a test,
// if we unconditionally assert any of these, the test will fail with too many assertions :|
if ( cacheLength !== oldCacheLength ) {
equal( cacheLength, oldCacheLength, "No unit tests leak memory in jQuery.cache" );
oldCacheLength = cacheLength;
}
if ( fragmentsLength !== oldFragmentsLength ) {
equal( fragmentsLength, oldFragmentsLength, "No unit tests leak memory in jQuery.fragments" );
oldFragmentsLength = fragmentsLength;
}
};
QUnit.done(function() {
// Remove our own fixtures outside #qunit-fixture
supportjQuery("#qunit ~ *").remove();
});
// jQuery-specific QUnit.reset
QUnit.reset = function() {
// Ensure jQuery events and data on the fixture are properly removed
jQuery("#qunit-fixture").empty();
// ...even if the jQuery under test has a broken .empty()
supportjQuery("#qunit-fixture").empty();
// Reset internal jQuery state
jQuery.event.global = {};
if ( ajaxSettings ) {
jQuery.ajaxSettings = jQuery.extend( true, {}, ajaxSettings );
} else {
delete jQuery.ajaxSettings;
}
// Cleanup globals
Globals.cleanup();
// Let QUnit reset the fixture
reset.apply( this, arguments );
};
})();
/**
* QUnit configuration
*/
// Max time for stop() and asyncTest() until it aborts test
// and start()'s the next test.
QUnit.config.testTimeout = 20 * 1000; // 20 seconds
// Enforce an "expect" argument or expect() call in all test bodies.
QUnit.config.requireExpects = true;

View File

@ -29,6 +29,13 @@
).replace(/\w+/g, function(n) { ).replace(/\w+/g, function(n) {
document.createElement(n); document.createElement(n);
}); });
// Load tests if they have not been loaded
// This is in a different script tag to ensure that
// jQuery is on the page when the testrunner executes
if ( !QUnit.urlParams.amd ) {
loadTests();
}
</script> </script>
</head> </head>

6
test/jquery.js vendored
View File

@ -54,11 +54,9 @@
// Load jQuery // Load jQuery
document.write( "<script id='jquery-js' src='" + path + src + "'><\x2Fscript>" ); document.write( "<script id='jquery-js' src='" + path + src + "'><\x2Fscript>" );
// Load tests if available
// These can be loaded async as QUnit won't start until finished
if ( typeof loadTests !== "undefined" ) {
loadTests();
// Synchronous-only tests // Synchronous-only tests
// Other tests are loaded from the test page
if ( typeof loadTests !== "undefined" ) {
document.write( "<script src='" + path + "test/unit/ready.js'><\x2Fscript>"); document.write( "<script src='" + path + "test/unit/ready.js'><\x2Fscript>");
} }