testrunner.js 10.6 KB
Newer Older
王万里's avatar
王万里 committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
/**
 * Allow the test suite to run with other libs or jQuery's.
 */
jQuery.noConflict();

// For checking globals pollution despite auto-created globals in various environments
jQuery.each( [ jQuery.expando, "getInterface", "Packages", "java", "netscape" ], function( i, name ) {
	window[ name ] = window[ name ];
});

// Expose Sizzle for Sizzle's selector tests
// We remove Sizzle's globalization in jQuery
var Sizzle = Sizzle || jQuery.find,

// Allow subprojects to test against their own fixtures
	qunitModule = QUnit.module,
	qunitTest = QUnit.test;

this.testSubproject = function( label, url, risTests ) {
	var sub, fixture, fixtureHTML,
		fixtureReplaced = false;

	// Don't let subproject tests jump the gun
	QUnit.config.reorder = false;

	// Create module
	module( label );

	// Duckpunch QUnit
	// TODO restore parent fixture on teardown to support reordering
	module = QUnit.module = function( name ) {
		var args = arguments;

		// Remember subproject-scoped module name
		sub = name;

		// Override
		args[0] = label;
		return qunitModule.apply( this, args );
	};
	test = function( name ) {
		var args = arguments,
			i = args.length - 1;

		// Prepend subproject-scoped module name to test name
		args[0] = sub + ": " + name;

		// Find test function and wrap to require subproject fixture
		for ( ; i >= 0; i-- ) {
			if ( originaljQuery.isFunction( args[i] ) ) {
				args[i] = requireFixture( args[i] );
				break;
			}
		}

		return qunitTest.apply( this, args );
	};

	// Load tests and fixture from subproject
	// Test order matters, so we must be synchronous and throw an error on load failure
	originaljQuery.ajax( url, {
		async: false,
		dataType: "html",
		error: function( jqXHR, status ) {
			throw new Error( "Could not load: " + url + " (" + status + ")" );
		},
		success: function( data, status, jqXHR ) {
			var page = originaljQuery.parseHTML(
				// replace html/head with dummy elements so they are represented in the DOM
				( data || "" ).replace( /<\/?((!DOCTYPE|html|head)\b.*?)>/gi, "[$1]" ),
				document,
				true
			);

			if ( !page || !page.length ) {
				this.error( jqXHR, "no data" );
			}
			page = originaljQuery( page );

			// Include subproject tests
			page.filter("script[src]").add( page.find("script[src]") ).each(function() {
				var src = originaljQuery( this ).attr("src"),
					html = "<script src='" + url + src + "'></script>";
				if ( risTests.test( src ) ) {
					if ( originaljQuery.isReady ) {
						originaljQuery("head").first().append( html );
					} else {
						document.write( html );
					}
				}
			});

			// Get the fixture, including content outside of #qunit-fixture
			fixture = page.find("[id='qunit-fixture']");
			fixtureHTML = fixture.html();
			fixture.empty();
			while ( fixture.length && !fixture.prevAll("[id='qunit']").length ) {
				fixture = fixture.parent();
			}
			fixture = fixture.add( fixture.nextAll() );
		}
	});

	function requireFixture( fn ) {
		return function() {
			if ( !fixtureReplaced ) {
				// Make sure that we retrieved a fixture for the subproject
				if ( !fixture.length ) {
					ok( false, "Found subproject fixture" );
					return;
				}

				// Replace the current fixture, including content outside of #qunit-fixture
				var oldFixture = originaljQuery("#qunit-fixture");
				while ( oldFixture.length && !oldFixture.prevAll("[id='qunit']").length ) {
					oldFixture = oldFixture.parent();
				}
				oldFixture.nextAll().remove();
				oldFixture.replaceWith( fixture );

				// WARNING: UNDOCUMENTED INTERFACE
				QUnit.config.fixture = fixtureHTML;
				QUnit.reset();
				if ( originaljQuery("#qunit-fixture").html() !== fixtureHTML ) {
					ok( false, "Copied subproject fixture" );
					return;
				}

				fixtureReplaced = true;
			}

			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;
			jQuery.globalEval( "var " + name + " = undefined;" );
		},
		cleanup: function() {
			var name,
				current = globals;
			globals = {};
			for ( name in current ) {
				jQuery.globalEval( "try { " +
					"delete " + ( jQuery.support.deleteExpando ? "window['" + name + "']" : name ) +
				"; } catch( x ) {}" );
			}
		}
	};
})();

// Sandbox start for great justice
(function() {
	var oldStart = window.start;
	window.start = function() {
		oldStart();
	};
})();

/**
 * QUnit hooks
 */
(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 ( !jQuery.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
		jQuery("#qunit ~ *").remove();
	});

	// jQuery-specific QUnit.reset
	QUnit.reset = function() {

		// Ensure jQuery events and data on the fixture are properly removed
		jQuery("#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;

/**
 * Load the TestSwarm listener if swarmURL is in the address.
 */
(function() {
	var url = window.location.search;
	url = decodeURIComponent( url.slice( url.indexOf("swarmURL=") + "swarmURL=".length ) );

	if ( !url || url.indexOf("http") !== 0 ) {
		return;
	}

	document.write("<scr" + "ipt src='http://swarm.jquery.org/js/inject.js?" + (new Date()).getTime() + "'></scr" + "ipt>");
})();