From 5093b89f08aa4565d1987666d998d16e2e3b6f4a Mon Sep 17 00:00:00 2001 From: Timmy Willison Date: Fri, 6 Sep 2013 17:11:04 -0400 Subject: [PATCH] Convert testrunner to an AMD module and ensure jQuery is on the page when executing the testrunner (another race condition amplified by swarm) --- test/.jshintrc | 1 + test/data/testinit.js | 111 +++++----- test/data/testrunner.js | 469 ++++++++++++++++++++-------------------- test/index.html | 7 + test/jquery.js | 6 +- 5 files changed, 302 insertions(+), 292 deletions(-) diff --git a/test/.jshintrc b/test/.jshintrc index 34f8a5b2d..b702f6fa5 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -22,6 +22,7 @@ "globals": { "require": false, + "define": false, "DOMParser": false, "QUnit": false, "ok": false, diff --git a/test/data/testinit.js b/test/data/testinit.js index 2a3c33d76..c35383c6b 100644 --- a/test/data/testinit.js +++ b/test/data/testinit.js @@ -259,65 +259,68 @@ this.iframeCallback = undefined; QUnit.config.autostart = false; this.loadTests = function() { var loadSwarm, - url = window.location.search, - tests = [ - "data/testrunner.js", - "unit/core.js", - "unit/callbacks.js", - "unit/deferred.js", - "unit/support.js", - "unit/data.js", - "unit/queue.js", - "unit/attributes.js", - "unit/event.js", - "unit/selector.js", - "unit/traversing.js", - "unit/manipulation.js", - "unit/wrap.js", - "unit/css.js", - "unit/serialize.js", - "unit/ajax.js", - "unit/effects.js", - "unit/offset.js", - "unit/dimensions.js" - ]; + url = window.location.search; url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + "swarmURL=".length ) ); loadSwarm = url && url.indexOf("http") === 0; - // Ensure load order (to preserve test numbers) - (function loadDep() { - var dep = tests.shift(); - if ( dep ) { - require( [ dep ], loadDep ); - } else { - // Subproject tests must be last because they replace our test fixture - window.testSubproject( "Sizzle", "../bower_components/sizzle/test/", /^unit\/.*\.js$/, function() { - // Call load to build module filter select element - QUnit.load(); + // Get testSubproject from testrunner first + require([ "data/testrunner.js" ], function( testSubproject ) { + var tests = [ + "unit/core.js", + "unit/callbacks.js", + "unit/deferred.js", + "unit/support.js", + "unit/data.js", + "unit/queue.js", + "unit/attributes.js", + "unit/event.js", + "unit/selector.js", + "unit/traversing.js", + "unit/manipulation.js", + "unit/wrap.js", + "unit/css.js", + "unit/serialize.js", + "unit/ajax.js", + "unit/effects.js", + "unit/offset.js", + "unit/dimensions.js" + ]; - // 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(); + // Ensure load order (to preserve test numbers) + (function loadDep() { + var dep = tests.shift(); + if ( dep ) { + require( [ dep ], loadDep ); + } else { + // Subproject tests must be last because they replace our test fixture + testSubproject( "Sizzle", "../bower_components/sizzle/test/", /^unit\/.*\.js$/, function() { + // Call load to build module filter select element + QUnit.load(); + + /** + * Run in noConflict mode + */ + jQuery.noConflict(); + + // Expose Sizzle for Sizzle's selector tests + // We remove Sizzle's globalization in jQuery + window.Sizzle = window.Sizzle || jQuery.find; + + // For checking globals pollution despite auto-created globals in various environments + supportjQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, name ) { + window[ name ] = window[ name ]; }); - } else { - QUnit.start(); - } - /** - * Run in noConflict mode - */ - jQuery.noConflict(); - - // Expose Sizzle for Sizzle's selector tests - // We remove Sizzle's globalization in jQuery - window.Sizzle = window.Sizzle || jQuery.find; - - // For checking globals pollution despite auto-created globals in various environments - supportjQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, 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(); + } }); - }); - } - })(); + } + })(); + }); }; diff --git a/test/data/testrunner.js b/test/data/testrunner.js index c766e13ca..3f1a85a03 100644 --- a/test/data/testrunner.js +++ b/test/data/testrunner.js @@ -1,7 +1,236 @@ +define(function() { // Allow subprojects to test against their own fixtures -var qunitModule = QUnit.module, - qunitTest = QUnit.test; +var oldStart = window.start, + 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 @@ -9,7 +238,7 @@ var qunitModule = QUnit.module, * @param {String} url Test folder location * @param {RegExp} risTests To filter script sources */ -this.testSubproject = function( label, url, risTests, complete ) { +function testSubproject( label, url, risTests, complete ) { var sub, fixture, fixtureHTML, fixtureReplaced = false; @@ -131,236 +360,8 @@ this.testSubproject = function( label, url, risTests, complete ) { fn.apply( this, arguments ); }; } -}; +} -// Register globals for cleanup and the cleanup code itself -// 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 ) {}" ); - } - } - }; -})(); +return testSubproject; - -/** - * 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; +}); diff --git a/test/index.html b/test/index.html index 070624036..1b5e33a3b 100644 --- a/test/index.html +++ b/test/index.html @@ -29,6 +29,13 @@ ).replace(/\w+/g, function(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(); + } diff --git a/test/jquery.js b/test/jquery.js index 3c937d598..fd0be6759 100644 --- a/test/jquery.js +++ b/test/jquery.js @@ -54,11 +54,9 @@ // Load jQuery document.write( "