--- a/devtools/data/qunit.js Wed May 20 16:33:01 2015 +0200
+++ b/devtools/data/qunit.js Wed May 20 15:12:58 2015 +0200
@@ -1,643 +1,713 @@
-/*
- * QUnit - A JavaScript Unit Testing Framework
- *
- * http://docs.jquery.com/QUnit
+/*!
+ * QUnit 1.18.0
+ * http://qunitjs.com/
*
- * Copyright (c) 2009 John Resig, Jörn Zaefferer
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
+ * Copyright jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://jquery.org/license
+ *
+ * Date: 2015-04-03T10:23Z
*/
-(function(window) {
-
-var QUnit = {
-
- // Initialize the configuration options
- init: function() {
- config = {
- stats: { all: 0, bad: 0 },
- moduleStats: { all: 0, bad: 0 },
- started: +new Date,
- updateRate: 1000,
- blocking: false,
- autorun: false,
- assertions: [],
- filters: [],
- queue: []
- };
-
- var tests = id("qunit-tests"),
- banner = id("qunit-banner"),
- result = id("qunit-testresult");
-
- if ( tests ) {
- tests.innerHTML = "";
- }
-
- if ( banner ) {
- banner.className = "";
- }
-
- if ( result ) {
- result.parentNode.removeChild( result );
+(function( window ) {
+
+var QUnit,
+ config,
+ onErrorFnPrev,
+ loggingCallbacks = {},
+ fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ // Keep a local reference to Date (GH-283)
+ Date = window.Date,
+ now = Date.now || function() {
+ return new Date().getTime();
+ },
+ globalStartCalled = false,
+ runStarted = false,
+ setTimeout = window.setTimeout,
+ clearTimeout = window.clearTimeout,
+ defined = {
+ document: window.document !== undefined,
+ setTimeout: window.setTimeout !== undefined,
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch ( e ) {
+ return false;
+ }
+ }())
+ },
+ /**
+ * Provides a normalized error string, correcting an issue
+ * with IE 7 (and prior) where Error.prototype.toString is
+ * not properly implemented
+ *
+ * Based on http://es5.github.com/#x15.11.4.4
+ *
+ * @param {String|Error} error
+ * @return {String} error message
+ */
+ errorString = function( error ) {
+ var name, message,
+ errorString = error.toString();
+ if ( errorString.substring( 0, 7 ) === "[object" ) {
+ name = error.name ? error.name.toString() : "Error";
+ message = error.message ? error.message.toString() : "";
+ if ( name && message ) {
+ return name + ": " + message;
+ } else if ( name ) {
+ return name;
+ } else if ( message ) {
+ return message;
+ } else {
+ return "Error";
+ }
+ } else {
+ return errorString;
}
},
-
- // call on start of module test to prepend name to all tests
- module: function(name, testEnvironment) {
- config.currentModule = name;
-
- synchronize(function() {
- if ( config.currentModule ) {
- QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+ /**
+ * Makes a clone of an object using only Array or Object as base,
+ * and copies over the own enumerable properties.
+ *
+ * @param {Object} obj
+ * @return {Object} New object with only the own properties (recursively).
+ */
+ objectValues = function( obj ) {
+ var key, val,
+ vals = QUnit.is( "array", obj ) ? [] : {};
+ for ( key in obj ) {
+ if ( hasOwn.call( obj, key ) ) {
+ val = obj[ key ];
+ vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
}
-
- config.currentModule = name;
- config.moduleTestEnvironment = testEnvironment;
- config.moduleStats = { all: 0, bad: 0 };
-
- QUnit.moduleStart( name, testEnvironment );
- });
+ }
+ return vals;
+ };
+
+QUnit = {};
+
+/**
+ * Config object: Maintain internal state
+ * Later exposed as QUnit.config
+ * `config` initialized at top of scope
+ */
+config = {
+ // The queue of tests to run
+ queue: [],
+
+ // block until document ready
+ blocking: true,
+
+ // by default, run previously failed tests first
+ // very useful in combination with "Hide passed tests" checked
+ reorder: true,
+
+ // by default, modify document.title when suite is done
+ altertitle: true,
+
+ // by default, scroll to top of the page when suite is done
+ scrolltop: true,
+
+ // when enabled, all tests must call expect()
+ requireExpects: false,
+
+ // depth up-to which object will be dumped
+ maxDepth: 5,
+
+ // add checkboxes that are persisted in the query-string
+ // when enabled, the id is set to `true` as a `QUnit.config` property
+ urlConfig: [
+ {
+ id: "hidepassed",
+ label: "Hide passed tests",
+ tooltip: "Only show tests and assertions that fail. Stored as query-strings."
+ },
+ {
+ id: "noglobals",
+ label: "Check for Globals",
+ tooltip: "Enabling this will test if any test introduces new properties on the " +
+ "`window` object. Stored as query-strings."
+ },
+ {
+ id: "notrycatch",
+ label: "No try-catch",
+ tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
+ "exceptions in IE reasonable. Stored as query-strings."
+ }
+ ],
+
+ // Set of all modules.
+ modules: [],
+
+ // The first unnamed module
+ currentModule: {
+ name: "",
+ tests: []
},
- asyncTest: function(testName, expected, callback) {
+ callbacks: {}
+};
+
+// Push a loose unnamed module to the modules collection
+config.modules.push( config.currentModule );
+
+// Initialize more QUnit.config and QUnit.urlParams
+(function() {
+ var i, current,
+ location = window.location || { search: "", protocol: "file:" },
+ params = location.search.slice( 1 ).split( "&" ),
+ length = params.length,
+ urlParams = {};
+
+ if ( params[ 0 ] ) {
+ for ( i = 0; i < length; i++ ) {
+ current = params[ i ].split( "=" );
+ current[ 0 ] = decodeURIComponent( current[ 0 ] );
+
+ // allow just a key to turn on a flag, e.g., test.html?noglobals
+ current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
+ if ( urlParams[ current[ 0 ] ] ) {
+ urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
+ } else {
+ urlParams[ current[ 0 ] ] = current[ 1 ];
+ }
+ }
+ }
+
+ if ( urlParams.filter === true ) {
+ delete urlParams.filter;
+ }
+
+ QUnit.urlParams = urlParams;
+
+ // String search anywhere in moduleName+testName
+ config.filter = urlParams.filter;
+
+ if ( urlParams.maxDepth ) {
+ config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ?
+ Number.POSITIVE_INFINITY :
+ urlParams.maxDepth;
+ }
+
+ config.testId = [];
+ if ( urlParams.testId ) {
+
+ // Ensure that urlParams.testId is an array
+ urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
+ for ( i = 0; i < urlParams.testId.length; i++ ) {
+ config.testId.push( urlParams.testId[ i ] );
+ }
+ }
+
+ // Figure out if we're running the tests from a server or not
+ QUnit.isLocal = location.protocol === "file:";
+
+ // Expose the current QUnit version
+ QUnit.version = "1.18.0";
+}());
+
+// Root QUnit object.
+// `QUnit` initialized at top of scope
+extend( QUnit, {
+
+ // call on start of module test to prepend name to all tests
+ module: function( name, testEnvironment ) {
+ var currentModule = {
+ name: name,
+ testEnvironment: testEnvironment,
+ tests: []
+ };
+
+ // DEPRECATED: handles setup/teardown functions,
+ // beforeEach and afterEach should be used instead
+ if ( testEnvironment && testEnvironment.setup ) {
+ testEnvironment.beforeEach = testEnvironment.setup;
+ delete testEnvironment.setup;
+ }
+ if ( testEnvironment && testEnvironment.teardown ) {
+ testEnvironment.afterEach = testEnvironment.teardown;
+ delete testEnvironment.teardown;
+ }
+
+ config.modules.push( currentModule );
+ config.currentModule = currentModule;
+ },
+
+ // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
+ asyncTest: function( testName, expected, callback ) {
if ( arguments.length === 2 ) {
callback = expected;
- expected = 0;
+ expected = null;
}
- QUnit.test(testName, expected, callback, true);
+ QUnit.test( testName, expected, callback, true );
},
-
- test: function(testName, expected, callback, async) {
- var name = testName, testEnvironment, testEnvironmentArg;
+
+ test: function( testName, expected, callback, async ) {
+ var test;
if ( arguments.length === 2 ) {
callback = expected;
expected = null;
}
- // is 2nd argument a testEnvironment?
- if ( expected && typeof expected === 'object') {
- testEnvironmentArg = expected;
- expected = null;
- }
-
- if ( config.currentModule ) {
- name = config.currentModule + " module: " + name;
- }
-
- if ( !validTest(name) ) {
- return;
- }
-
- synchronize(function() {
- QUnit.testStart( testName );
-
- testEnvironment = extend({
- setup: function() {},
- teardown: function() {}
- }, config.moduleTestEnvironment);
- if (testEnvironmentArg) {
- extend(testEnvironment,testEnvironmentArg);
- }
-
- // allow utility functions to access the current test environment
- QUnit.current_testEnvironment = testEnvironment;
-
- config.assertions = [];
- config.expected = expected;
-
- try {
- if ( !config.pollution ) {
- saveGlobal();
- }
-
- testEnvironment.setup.call(testEnvironment);
- } catch(e) {
- QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
- }
-
- if ( async ) {
- QUnit.stop();
- }
-
- try {
- callback.call(testEnvironment);
- } catch(e) {
- fail("Test " + name + " died, exception and test follows", e, callback);
- QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
- // else next test will carry the responsibility
- saveGlobal();
-
- // Restart the tests if they're blocking
- if ( config.blocking ) {
- start();
- }
- }
+
+ test = new Test({
+ testName: testName,
+ expected: expected,
+ async: async,
+ callback: callback
+ });
+
+ test.queue();
+ },
+
+ skip: function( testName ) {
+ var test = new Test({
+ testName: testName,
+ skip: true
});
- synchronize(function() {
- try {
- checkPollution();
- testEnvironment.teardown.call(testEnvironment);
- } catch(e) {
- QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
- }
-
- try {
- QUnit.reset();
- } catch(e) {
- fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
- }
-
- if ( config.expected && config.expected != config.assertions.length ) {
- QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+ test.queue();
+ },
+
+ // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
+ // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
+ start: function( count ) {
+ var globalStartAlreadyCalled = globalStartCalled;
+
+ if ( !config.current ) {
+ globalStartCalled = true;
+
+ if ( runStarted ) {
+ throw new Error( "Called start() outside of a test context while already started" );
+ } else if ( globalStartAlreadyCalled || count > 1 ) {
+ throw new Error( "Called start() outside of a test context too many times" );
+ } else if ( config.autostart ) {
+ throw new Error( "Called start() outside of a test context when " +
+ "QUnit.config.autostart was true" );
+ } else if ( !config.pageLoaded ) {
+
+ // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
+ config.autostart = true;
+ return;
}
-
- var good = 0, bad = 0,
- tests = id("qunit-tests");
-
- config.stats.all += config.assertions.length;
- config.moduleStats.all += config.assertions.length;
-
- if ( tests ) {
- var ol = document.createElement("ol");
- ol.style.display = "none";
-
- for ( var i = 0; i < config.assertions.length; i++ ) {
- var assertion = config.assertions[i];
-
- var li = document.createElement("li");
- li.className = assertion.result ? "pass" : "fail";
- li.appendChild(document.createTextNode(assertion.message || "(no message)"));
- ol.appendChild( li );
-
- if ( assertion.result ) {
- good++;
- } else {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
-
- var b = document.createElement("strong");
- b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
-
- addEvent(b, "click", function() {
- var next = b.nextSibling, display = next.style.display;
- next.style.display = display === "none" ? "block" : "none";
- });
-
- addEvent(b, "dblclick", function(e) {
- var target = e && e.target ? e.target : window.event.srcElement;
- if ( target.nodeName.toLowerCase() === "strong" ) {
- var text = "", node = target.firstChild;
-
- while ( node.nodeType === 3 ) {
- text += node.nodeValue;
- node = node.nextSibling;
- }
-
- text = text.replace(/(^\s*|\s*$)/g, "");
-
- if ( window.location ) {
- window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text);
- }
- }
- });
-
- var li = document.createElement("li");
- li.className = bad ? "fail" : "pass";
- li.appendChild( b );
- li.appendChild( ol );
- tests.appendChild( li );
-
- if ( bad ) {
- var toolbar = id("qunit-testrunner-toolbar");
- if ( toolbar ) {
- toolbar.style.display = "block";
- id("qunit-filter-pass").disabled = null;
- id("qunit-filter-missing").disabled = null;
- }
- }
-
- } else {
- for ( var i = 0; i < config.assertions.length; i++ ) {
- if ( !config.assertions[i].result ) {
- bad++;
- config.stats.bad++;
- config.moduleStats.bad++;
- }
- }
+ } else {
+
+ // If a test is running, adjust its semaphore
+ config.current.semaphore -= count || 1;
+
+ // Don't start until equal number of stop-calls
+ if ( config.current.semaphore > 0 ) {
+ return;
}
- QUnit.testDone( testName, bad, config.assertions.length );
-
- if ( !window.setTimeout && !config.queue.length ) {
- done();
+ // throw an Error if start is called more often than stop
+ if ( config.current.semaphore < 0 ) {
+ config.current.semaphore = 0;
+
+ QUnit.pushFailure(
+ "Called start() while already started (test's semaphore was 0 already)",
+ sourceFromStacktrace( 2 )
+ );
+ return;
}
- });
-
- if ( window.setTimeout && !config.doneTimer ) {
- config.doneTimer = window.setTimeout(function(){
- if ( !config.queue.length ) {
- done();
- } else {
- synchronize( done );
- }
- }, 13);
}
- },
-
- /**
- * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
- */
- expect: function(asserts) {
- config.expected = asserts;
+
+ resumeProcessing();
},
- /**
- * Asserts true.
- * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
- */
- ok: function(a, msg) {
- QUnit.log(a, msg);
-
- config.assertions.push({
- result: !!a,
- message: msg
- });
- },
-
- /**
- * Checks that the first two arguments are equal, with an optional message.
- * Prints out both actual and expected values.
- *
- * Prefered to ok( actual == expected, message )
- *
- * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
- *
- * @param Object actual
- * @param Object expected
- * @param String message (optional)
- */
- equal: function(actual, expected, message) {
- push(expected == actual, actual, expected, message);
- },
-
- notEqual: function(actual, expected, message) {
- push(expected != actual, actual, expected, message);
- },
-
- deepEqual: function(a, b, message) {
- push(QUnit.equiv(a, b), a, b, message);
- },
-
- notDeepEqual: function(a, b, message) {
- push(!QUnit.equiv(a, b), a, b, message);
- },
-
- strictEqual: function(actual, expected, message) {
- push(expected === actual, actual, expected, message);
- },
-
- notStrictEqual: function(actual, expected, message) {
- push(expected !== actual, actual, expected, message);
+ // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
+ stop: function( count ) {
+
+ // If there isn't a test running, don't allow QUnit.stop() to be called
+ if ( !config.current ) {
+ throw new Error( "Called stop() outside of a test context" );
+ }
+
+ // If a test is running, adjust its semaphore
+ config.current.semaphore += count || 1;
+
+ pauseProcessing();
},
-
- start: function() {
- // A slight delay, to avoid any current callbacks
- if ( window.setTimeout ) {
- window.setTimeout(function() {
- if ( config.timeout ) {
- clearTimeout(config.timeout);
- }
-
- config.blocking = false;
- process();
- }, 13);
- } else {
- config.blocking = false;
- process();
- }
- },
-
- stop: function(timeout) {
- config.blocking = true;
-
- if ( timeout && window.setTimeout ) {
- config.timeout = window.setTimeout(function() {
- QUnit.ok( false, "Test timed out" );
- QUnit.start();
- }, timeout);
- }
- },
-
- /**
- * Resets the test setup. Useful for tests that modify the DOM.
- */
- reset: function() {
- if ( window.jQuery ) {
- jQuery("#main").html( config.fixture );
- jQuery.event.global = {};
- jQuery.ajaxSettings = extend({}, config.ajaxSettings);
- }
- },
-
- /**
- * Trigger an event on an element.
- *
- * @example triggerEvent( document.body, "click" );
- *
- * @param DOMElement elem
- * @param String type
- */
- triggerEvent: function( elem, type, event ) {
- if ( document.createEvent ) {
- event = document.createEvent("MouseEvents");
- event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
- 0, 0, 0, 0, 0, false, false, false, false, 0, null);
- elem.dispatchEvent( event );
-
- } else if ( elem.fireEvent ) {
- elem.fireEvent("on"+type);
- }
- },
-
+
+ config: config,
+
// Safe object type checking
is: function( type, obj ) {
- return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+ return QUnit.objectType( obj ) === type;
},
-
- // Logging callbacks
- done: function(failures, total) {},
- log: function(result, message) {},
- testStart: function(name) {},
- testDone: function(name, failures, total) {},
- moduleStart: function(name, testEnvironment) {},
- moduleDone: function(name, failures, total) {}
-};
-
-// Backwards compatibility, deprecated
-QUnit.equals = QUnit.equal;
-QUnit.same = QUnit.deepEqual;
-
-// Maintain internal state
-var config = {
- // The queue of tests to run
- queue: [],
-
- // block until document ready
- blocking: true
-};
-
-// Load paramaters
-(function() {
- var location = window.location || { search: "", protocol: "file:" },
- GETParams = location.search.slice(1).split('&');
-
- for ( var i = 0; i < GETParams.length; i++ ) {
- GETParams[i] = decodeURIComponent( GETParams[i] );
- if ( GETParams[i] === "noglobals" ) {
- GETParams.splice( i, 1 );
- i--;
- config.noglobals = true;
- } else if ( GETParams[i].search('=') > -1 ) {
- GETParams.splice( i, 1 );
- i--;
+
+ objectType: function( obj ) {
+ if ( typeof obj === "undefined" ) {
+ return "undefined";
+ }
+
+ // Consider: typeof null === object
+ if ( obj === null ) {
+ return "null";
+ }
+
+ var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
+ type = match && match[ 1 ] || "";
+
+ switch ( type ) {
+ case "Number":
+ if ( isNaN( obj ) ) {
+ return "nan";
+ }
+ return "number";
+ case "String":
+ case "Boolean":
+ case "Array":
+ case "Date":
+ case "RegExp":
+ case "Function":
+ return type.toLowerCase();
+ }
+ if ( typeof obj === "object" ) {
+ return "object";
+ }
+ return undefined;
+ },
+
+ extend: extend,
+
+ load: function() {
+ config.pageLoaded = true;
+
+ // Initialize the configuration options
+ extend( config, {
+ stats: { all: 0, bad: 0 },
+ moduleStats: { all: 0, bad: 0 },
+ started: 0,
+ updateRate: 1000,
+ autostart: true,
+ filter: ""
+ }, true );
+
+ config.blocking = false;
+
+ if ( config.autostart ) {
+ resumeProcessing();
}
}
-
- // restrict modules/tests by get parameters
- config.filters = GETParams;
-
- // Figure out if we're running the tests from a server or not
- QUnit.isLocal = !!(location.protocol === 'file:');
-})();
-
-// Expose the API as global variables, unless an 'exports'
-// object exists, in that case we assume we're in CommonJS
-if ( typeof exports === "undefined" || typeof require === "undefined" ) {
- extend(window, QUnit);
- window.QUnit = QUnit;
-} else {
- extend(exports, QUnit);
- exports.QUnit = QUnit;
-}
-
-if ( typeof document === "undefined" || document.readyState === "complete" ) {
- config.autorun = true;
-}
-
-addEvent(window, "load", function() {
- // Initialize the config, saving the execution queue
- var oldconfig = extend({}, config);
- QUnit.init();
- extend(config, oldconfig);
-
- config.blocking = false;
-
- var userAgent = id("qunit-userAgent");
- if ( userAgent ) {
- userAgent.innerHTML = navigator.userAgent;
+});
+
+// Register logging callbacks
+(function() {
+ var i, l, key,
+ callbacks = [ "begin", "done", "log", "testStart", "testDone",
+ "moduleStart", "moduleDone" ];
+
+ function registerLoggingCallback( key ) {
+ var loggingCallback = function( callback ) {
+ if ( QUnit.objectType( callback ) !== "function" ) {
+ throw new Error(
+ "QUnit logging methods require a callback function as their first parameters."
+ );
+ }
+
+ config.callbacks[ key ].push( callback );
+ };
+
+ // DEPRECATED: This will be removed on QUnit 2.0.0+
+ // Stores the registered functions allowing restoring
+ // at verifyLoggingCallbacks() if modified
+ loggingCallbacks[ key ] = loggingCallback;
+
+ return loggingCallback;
}
-
- var toolbar = id("qunit-testrunner-toolbar");
- if ( toolbar ) {
- toolbar.style.display = "none";
-
- var filter = document.createElement("input");
- filter.type = "checkbox";
- filter.id = "qunit-filter-pass";
- filter.disabled = true;
- addEvent( filter, "click", function() {
- var li = document.getElementsByTagName("li");
- for ( var i = 0; i < li.length; i++ ) {
- if ( li[i].className.indexOf("pass") > -1 ) {
- li[i].style.display = filter.checked ? "none" : "";
- }
+
+ for ( i = 0, l = callbacks.length; i < l; i++ ) {
+ key = callbacks[ i ];
+
+ // Initialize key collection of logging callback
+ if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
+ config.callbacks[ key ] = [];
+ }
+
+ QUnit[ key ] = registerLoggingCallback( key );
+ }
+})();
+
+// `onErrorFnPrev` initialized at top of scope
+// Preserve other handlers
+onErrorFnPrev = window.onerror;
+
+// Cover uncaught exceptions
+// Returning true will suppress the default browser handler,
+// returning false will let it run.
+window.onerror = function( error, filePath, linerNr ) {
+ var ret = false;
+ if ( onErrorFnPrev ) {
+ ret = onErrorFnPrev( error, filePath, linerNr );
+ }
+
+ // Treat return value as window.onerror itself does,
+ // Only do our handling if not suppressed.
+ if ( ret !== true ) {
+ if ( QUnit.config.current ) {
+ if ( QUnit.config.current.ignoreGlobalErrors ) {
+ return true;
}
- });
- toolbar.appendChild( filter );
-
- var label = document.createElement("label");
- label.setAttribute("for", "qunit-filter-pass");
- label.innerHTML = "Hide passed tests";
- toolbar.appendChild( label );
-
- var missing = document.createElement("input");
- missing.type = "checkbox";
- missing.id = "qunit-filter-missing";
- missing.disabled = true;
- addEvent( missing, "click", function() {
- var li = document.getElementsByTagName("li");
- for ( var i = 0; i < li.length; i++ ) {
- if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
- li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
- }
- }
- });
- toolbar.appendChild( missing );
-
- label = document.createElement("label");
- label.setAttribute("for", "qunit-filter-missing");
- label.innerHTML = "Hide missing tests (untested code is broken code)";
- toolbar.appendChild( label );
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ } else {
+ QUnit.test( "global failure", extend(function() {
+ QUnit.pushFailure( error, filePath + ":" + linerNr );
+ }, { validTest: true } ) );
+ }
+ return false;
}
- var main = id('main');
- if ( main ) {
- config.fixture = main.innerHTML;
- }
-
- if ( window.jQuery ) {
- config.ajaxSettings = window.jQuery.ajaxSettings;
- }
-
- QUnit.start();
-});
+ return ret;
+};
function done() {
- if ( config.doneTimer && window.clearTimeout ) {
- window.clearTimeout( config.doneTimer );
- config.doneTimer = null;
- }
-
- if ( config.queue.length ) {
- config.doneTimer = window.setTimeout(function(){
- if ( !config.queue.length ) {
- done();
- } else {
- synchronize( done );
- }
- }, 13);
-
- return;
- }
+ var runtime, passed;
config.autorun = true;
// Log the last module results
- if ( config.currentModule ) {
- QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
- }
-
- var banner = id("qunit-banner"),
- tests = id("qunit-tests"),
- html = ['Tests completed in ',
- +new Date - config.started, ' milliseconds.<br/>',
- '<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
-
- if ( banner ) {
- banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
- }
-
- if ( tests ) {
- var result = id("qunit-testresult");
-
- if ( !result ) {
- result = document.createElement("p");
- result.id = "qunit-testresult";
- result.className = "result";
- tests.parentNode.insertBefore( result, tests.nextSibling );
- }
-
- result.innerHTML = html;
+ if ( config.previousModule ) {
+ runLoggingCallbacks( "moduleDone", {
+ name: config.previousModule.name,
+ tests: config.previousModule.tests,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all,
+ runtime: now() - config.moduleStats.started
+ });
}
-
- QUnit.done( config.stats.bad, config.stats.all );
+ delete config.previousModule;
+
+ runtime = now() - config.started;
+ passed = config.stats.all - config.stats.bad;
+
+ runLoggingCallbacks( "done", {
+ failed: config.stats.bad,
+ passed: passed,
+ total: config.stats.all,
+ runtime: runtime
+ });
}
-function validTest( name ) {
- var i = config.filters.length,
- run = false;
-
- if ( !i ) {
- return true;
+// Doesn't support IE6 to IE9, it will return undefined on these browsers
+// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
+function extractStacktrace( e, offset ) {
+ offset = offset === undefined ? 4 : offset;
+
+ var stack, include, i;
+
+ if ( e.stack ) {
+ stack = e.stack.split( "\n" );
+ if ( /^error$/i.test( stack[ 0 ] ) ) {
+ stack.shift();
+ }
+ if ( fileName ) {
+ include = [];
+ for ( i = offset; i < stack.length; i++ ) {
+ if ( stack[ i ].indexOf( fileName ) !== -1 ) {
+ break;
+ }
+ include.push( stack[ i ] );
+ }
+ if ( include.length ) {
+ return include.join( "\n" );
+ }
+ }
+ return stack[ offset ];
+
+ // Support: Safari <=6 only
+ } else if ( e.sourceURL ) {
+
+ // exclude useless self-reference for generated Error objects
+ if ( /qunit.js$/.test( e.sourceURL ) ) {
+ return;
+ }
+
+ // for actual exceptions, this is useful
+ return e.sourceURL + ":" + e.line;
}
-
- while ( i-- ) {
- var filter = config.filters[i],
- not = filter.charAt(0) == '!';
-
- if ( not ) {
- filter = filter.slice(1);
- }
-
- if ( name.indexOf(filter) !== -1 ) {
- return !not;
- }
-
- if ( not ) {
- run = true;
+}
+
+function sourceFromStacktrace( offset ) {
+ var error = new Error();
+
+ // Support: Safari <=7 only, IE <=10 - 11 only
+ // Not all browsers generate the `stack` property for `new Error()`, see also #636
+ if ( !error.stack ) {
+ try {
+ throw error;
+ } catch ( err ) {
+ error = err;
}
}
- return run;
+ return extractStacktrace( error, offset );
}
-function push(result, actual, expected, message) {
- message = message || (result ? "okay" : "failed");
- QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) );
-}
-
-function synchronize( callback ) {
+function synchronize( callback, last ) {
+ if ( QUnit.objectType( callback ) === "array" ) {
+ while ( callback.length ) {
+ synchronize( callback.shift() );
+ }
+ return;
+ }
config.queue.push( callback );
if ( config.autorun && !config.blocking ) {
- process();
+ process( last );
}
}
-function process() {
- var start = (new Date()).getTime();
+function process( last ) {
+ function next() {
+ process( last );
+ }
+ var start = now();
+ config.depth = ( config.depth || 0 ) + 1;
while ( config.queue.length && !config.blocking ) {
- if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+ if ( !defined.setTimeout || config.updateRate <= 0 ||
+ ( ( now() - start ) < config.updateRate ) ) {
+ if ( config.current ) {
+
+ // Reset async tracking for each phase of the Test lifecycle
+ config.current.usedAsync = false;
+ }
config.queue.shift()();
-
} else {
- setTimeout( process, 13 );
+ setTimeout( next, 13 );
break;
}
}
+ config.depth--;
+ if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
+ done();
+ }
+}
+
+function begin() {
+ var i, l,
+ modulesLog = [];
+
+ // If the test run hasn't officially begun yet
+ if ( !config.started ) {
+
+ // Record the time of the test run's beginning
+ config.started = now();
+
+ verifyLoggingCallbacks();
+
+ // Delete the loose unnamed module if unused.
+ if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
+ config.modules.shift();
+ }
+
+ // Avoid unnecessary information by not logging modules' test environments
+ for ( i = 0, l = config.modules.length; i < l; i++ ) {
+ modulesLog.push({
+ name: config.modules[ i ].name,
+ tests: config.modules[ i ].tests
+ });
+ }
+
+ // The test run is officially beginning now
+ runLoggingCallbacks( "begin", {
+ totalTests: Test.count,
+ modules: modulesLog
+ });
+ }
+
+ config.blocking = false;
+ process( true );
+}
+
+function resumeProcessing() {
+ runStarted = true;
+
+ // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
+ if ( defined.setTimeout ) {
+ setTimeout(function() {
+ if ( config.current && config.current.semaphore > 0 ) {
+ return;
+ }
+ if ( config.timeout ) {
+ clearTimeout( config.timeout );
+ }
+
+ begin();
+ }, 13 );
+ } else {
+ begin();
+ }
+}
+
+function pauseProcessing() {
+ config.blocking = true;
+
+ if ( config.testTimeout && defined.setTimeout ) {
+ clearTimeout( config.timeout );
+ config.timeout = setTimeout(function() {
+ if ( config.current ) {
+ config.current.semaphore = 0;
+ QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
+ } else {
+ throw new Error( "Test timed out" );
+ }
+ resumeProcessing();
+ }, config.testTimeout );
+ }
}
function saveGlobal() {
config.pollution = [];
-
+
if ( config.noglobals ) {
for ( var key in window ) {
- config.pollution.push( key );
+ if ( hasOwn.call( window, key ) ) {
+ // in Opera sometimes DOM element ids show up here, ignore them
+ if ( /^qunit-test-output/.test( key ) ) {
+ continue;
+ }
+ config.pollution.push( key );
+ }
}
}
}
-function checkPollution( name ) {
- var old = config.pollution;
+function checkPollution() {
+ var newGlobals,
+ deletedGlobals,
+ old = config.pollution;
+
saveGlobal();
-
- var newGlobals = diff( old, config.pollution );
+
+ newGlobals = diff( config.pollution, old );
if ( newGlobals.length > 0 ) {
- ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
- config.expected++;
+ QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
}
- var deletedGlobals = diff( config.pollution, old );
+ deletedGlobals = diff( old, config.pollution );
if ( deletedGlobals.length > 0 ) {
- ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
- config.expected++;
+ QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
}
}
// returns a new Array with the elements that are in a but not in b
function diff( a, b ) {
- var result = a.slice();
- for ( var i = 0; i < result.length; i++ ) {
- for ( var j = 0; j < b.length; j++ ) {
- if ( result[i] === b[j] ) {
- result.splice(i, 1);
+ var i, j,
+ result = a.slice();
+
+ for ( i = 0; i < result.length; i++ ) {
+ for ( j = 0; j < b.length; j++ ) {
+ if ( result[ i ] === b[ j ] ) {
+ result.splice( i, 1 );
i--;
break;
}
@@ -646,424 +716,3113 @@
return result;
}
-function fail(message, exception, callback) {
- if ( typeof console !== "undefined" && console.error && console.warn ) {
- console.error(message);
- console.error(exception);
- console.warn(callback.toString());
-
- } else if ( window.opera && opera.postError ) {
- opera.postError(message, exception, callback.toString);
- }
-}
-
-function extend(a, b) {
+function extend( a, b, undefOnly ) {
for ( var prop in b ) {
- a[prop] = b[prop];
+ if ( hasOwn.call( b, prop ) ) {
+
+ // Avoid "Member not found" error in IE8 caused by messing with window.constructor
+ if ( !( prop === "constructor" && a === window ) ) {
+ if ( b[ prop ] === undefined ) {
+ delete a[ prop ];
+ } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
+ a[ prop ] = b[ prop ];
+ }
+ }
+ }
}
return a;
}
-function addEvent(elem, type, fn) {
- if ( elem.addEventListener ) {
- elem.addEventListener( type, fn, false );
- } else if ( elem.attachEvent ) {
- elem.attachEvent( "on" + type, fn );
+function runLoggingCallbacks( key, args ) {
+ var i, l, callbacks;
+
+ callbacks = config.callbacks[ key ];
+ for ( i = 0, l = callbacks.length; i < l; i++ ) {
+ callbacks[ i ]( args );
+ }
+}
+
+// DEPRECATED: This will be removed on 2.0.0+
+// This function verifies if the loggingCallbacks were modified by the user
+// If so, it will restore it, assign the given callback and print a console warning
+function verifyLoggingCallbacks() {
+ var loggingCallback, userCallback;
+
+ for ( loggingCallback in loggingCallbacks ) {
+ if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
+
+ userCallback = QUnit[ loggingCallback ];
+
+ // Restore the callback function
+ QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
+
+ // Assign the deprecated given callback
+ QUnit[ loggingCallback ]( userCallback );
+
+ if ( window.console && window.console.warn ) {
+ window.console.warn(
+ "QUnit." + loggingCallback + " was replaced with a new value.\n" +
+ "Please, check out the documentation on how to apply logging callbacks.\n" +
+ "Reference: http://api.qunitjs.com/category/callbacks/"
+ );
+ }
+ }
+ }
+}
+
+// from jquery.js
+function inArray( elem, array ) {
+ if ( array.indexOf ) {
+ return array.indexOf( elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+function Test( settings ) {
+ var i, l;
+
+ ++Test.count;
+
+ extend( this, settings );
+ this.assertions = [];
+ this.semaphore = 0;
+ this.usedAsync = false;
+ this.module = config.currentModule;
+ this.stack = sourceFromStacktrace( 3 );
+
+ // Register unique strings
+ for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
+ if ( this.module.tests[ i ].name === this.testName ) {
+ this.testName += " ";
+ }
+ }
+
+ this.testId = generateHash( this.module.name, this.testName );
+
+ this.module.tests.push({
+ name: this.testName,
+ testId: this.testId
+ });
+
+ if ( settings.skip ) {
+
+ // Skipped tests will fully ignore any sent callback
+ this.callback = function() {};
+ this.async = false;
+ this.expected = 0;
} else {
- fn();
+ this.assert = new Assert( this );
}
}
-function id(name) {
- return !!(typeof document !== "undefined" && document && document.getElementById) &&
- document.getElementById( name );
+Test.count = 0;
+
+Test.prototype = {
+ before: function() {
+ if (
+
+ // Emit moduleStart when we're switching from one module to another
+ this.module !== config.previousModule ||
+
+ // They could be equal (both undefined) but if the previousModule property doesn't
+ // yet exist it means this is the first test in a suite that isn't wrapped in a
+ // module, in which case we'll just emit a moduleStart event for 'undefined'.
+ // Without this, reporters can get testStart before moduleStart which is a problem.
+ !hasOwn.call( config, "previousModule" )
+ ) {
+ if ( hasOwn.call( config, "previousModule" ) ) {
+ runLoggingCallbacks( "moduleDone", {
+ name: config.previousModule.name,
+ tests: config.previousModule.tests,
+ failed: config.moduleStats.bad,
+ passed: config.moduleStats.all - config.moduleStats.bad,
+ total: config.moduleStats.all,
+ runtime: now() - config.moduleStats.started
+ });
+ }
+ config.previousModule = this.module;
+ config.moduleStats = { all: 0, bad: 0, started: now() };
+ runLoggingCallbacks( "moduleStart", {
+ name: this.module.name,
+ tests: this.module.tests
+ });
+ }
+
+ config.current = this;
+
+ this.testEnvironment = extend( {}, this.module.testEnvironment );
+ delete this.testEnvironment.beforeEach;
+ delete this.testEnvironment.afterEach;
+
+ this.started = now();
+ runLoggingCallbacks( "testStart", {
+ name: this.testName,
+ module: this.module.name,
+ testId: this.testId
+ });
+
+ if ( !config.pollution ) {
+ saveGlobal();
+ }
+ },
+
+ run: function() {
+ var promise;
+
+ config.current = this;
+
+ if ( this.async ) {
+ QUnit.stop();
+ }
+
+ this.callbackStarted = now();
+
+ if ( config.notrycatch ) {
+ promise = this.callback.call( this.testEnvironment, this.assert );
+ this.resolvePromise( promise );
+ return;
+ }
+
+ try {
+ promise = this.callback.call( this.testEnvironment, this.assert );
+ this.resolvePromise( promise );
+ } catch ( e ) {
+ this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
+ this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
+
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Restart the tests if they're blocking
+ if ( config.blocking ) {
+ QUnit.start();
+ }
+ }
+ },
+
+ after: function() {
+ checkPollution();
+ },
+
+ queueHook: function( hook, hookName ) {
+ var promise,
+ test = this;
+ return function runHook() {
+ config.current = test;
+ if ( config.notrycatch ) {
+ promise = hook.call( test.testEnvironment, test.assert );
+ test.resolvePromise( promise, hookName );
+ return;
+ }
+ try {
+ promise = hook.call( test.testEnvironment, test.assert );
+ test.resolvePromise( promise, hookName );
+ } catch ( error ) {
+ test.pushFailure( hookName + " failed on " + test.testName + ": " +
+ ( error.message || error ), extractStacktrace( error, 0 ) );
+ }
+ };
+ },
+
+ // Currently only used for module level hooks, can be used to add global level ones
+ hooks: function( handler ) {
+ var hooks = [];
+
+ // Hooks are ignored on skipped tests
+ if ( this.skip ) {
+ return hooks;
+ }
+
+ if ( this.module.testEnvironment &&
+ QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
+ hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
+ }
+
+ return hooks;
+ },
+
+ finish: function() {
+ config.current = this;
+ if ( config.requireExpects && this.expected === null ) {
+ this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
+ "not called.", this.stack );
+ } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
+ this.pushFailure( "Expected " + this.expected + " assertions, but " +
+ this.assertions.length + " were run", this.stack );
+ } else if ( this.expected === null && !this.assertions.length ) {
+ this.pushFailure( "Expected at least one assertion, but none were run - call " +
+ "expect(0) to accept zero assertions.", this.stack );
+ }
+
+ var i,
+ bad = 0;
+
+ this.runtime = now() - this.started;
+ config.stats.all += this.assertions.length;
+ config.moduleStats.all += this.assertions.length;
+
+ for ( i = 0; i < this.assertions.length; i++ ) {
+ if ( !this.assertions[ i ].result ) {
+ bad++;
+ config.stats.bad++;
+ config.moduleStats.bad++;
+ }
+ }
+
+ runLoggingCallbacks( "testDone", {
+ name: this.testName,
+ module: this.module.name,
+ skipped: !!this.skip,
+ failed: bad,
+ passed: this.assertions.length - bad,
+ total: this.assertions.length,
+ runtime: this.runtime,
+
+ // HTML Reporter use
+ assertions: this.assertions,
+ testId: this.testId,
+
+ // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
+ duration: this.runtime
+ });
+
+ // QUnit.reset() is deprecated and will be replaced for a new
+ // fixture reset function on QUnit 2.0/2.1.
+ // It's still called here for backwards compatibility handling
+ QUnit.reset();
+
+ config.current = undefined;
+ },
+
+ queue: function() {
+ var bad,
+ test = this;
+
+ if ( !this.valid() ) {
+ return;
+ }
+
+ function run() {
+
+ // each of these can by async
+ synchronize([
+ function() {
+ test.before();
+ },
+
+ test.hooks( "beforeEach" ),
+
+ function() {
+ test.run();
+ },
+
+ test.hooks( "afterEach" ).reverse(),
+
+ function() {
+ test.after();
+ },
+ function() {
+ test.finish();
+ }
+ ]);
+ }
+
+ // `bad` initialized at top of scope
+ // defer when previous test run passed, if storage is available
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
+
+ if ( bad ) {
+ run();
+ } else {
+ synchronize( run, true );
+ }
+ },
+
+ push: function( result, actual, expected, message ) {
+ var source,
+ details = {
+ module: this.module.name,
+ name: this.testName,
+ result: result,
+ message: message,
+ actual: actual,
+ expected: expected,
+ testId: this.testId,
+ runtime: now() - this.started
+ };
+
+ if ( !result ) {
+ source = sourceFromStacktrace();
+
+ if ( source ) {
+ details.source = source;
+ }
+ }
+
+ runLoggingCallbacks( "log", details );
+
+ this.assertions.push({
+ result: !!result,
+ message: message
+ });
+ },
+
+ pushFailure: function( message, source, actual ) {
+ if ( !this instanceof Test ) {
+ throw new Error( "pushFailure() assertion outside test context, was " +
+ sourceFromStacktrace( 2 ) );
+ }
+
+ var details = {
+ module: this.module.name,
+ name: this.testName,
+ result: false,
+ message: message || "error",
+ actual: actual || null,
+ testId: this.testId,
+ runtime: now() - this.started
+ };
+
+ if ( source ) {
+ details.source = source;
+ }
+
+ runLoggingCallbacks( "log", details );
+
+ this.assertions.push({
+ result: false,
+ message: message
+ });
+ },
+
+ resolvePromise: function( promise, phase ) {
+ var then, message,
+ test = this;
+ if ( promise != null ) {
+ then = promise.then;
+ if ( QUnit.objectType( then ) === "function" ) {
+ QUnit.stop();
+ then.call(
+ promise,
+ QUnit.start,
+ function( error ) {
+ message = "Promise rejected " +
+ ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
+ " " + test.testName + ": " + ( error.message || error );
+ test.pushFailure( message, extractStacktrace( error, 0 ) );
+
+ // else next test will carry the responsibility
+ saveGlobal();
+
+ // Unblock
+ QUnit.start();
+ }
+ );
+ }
+ }
+ },
+
+ valid: function() {
+ var include,
+ filter = config.filter && config.filter.toLowerCase(),
+ module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
+ fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
+
+ // Internally-generated tests are always valid
+ if ( this.callback && this.callback.validTest ) {
+ return true;
+ }
+
+ if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
+ return false;
+ }
+
+ if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
+ return false;
+ }
+
+ if ( !filter ) {
+ return true;
+ }
+
+ include = filter.charAt( 0 ) !== "!";
+ if ( !include ) {
+ filter = filter.slice( 1 );
+ }
+
+ // If the filter matches, we need to honour include
+ if ( fullName.indexOf( filter ) !== -1 ) {
+ return include;
+ }
+
+ // Otherwise, do the opposite
+ return !include;
+ }
+
+};
+
+// Resets the test setup. Useful for tests that modify the DOM.
+/*
+DEPRECATED: Use multiple tests instead of resetting inside a test.
+Use testStart or testDone for custom cleanup.
+This method will throw an error in 2.0, and will be removed in 2.1
+*/
+QUnit.reset = function() {
+
+ // Return on non-browser environments
+ // This is necessary to not break on node tests
+ if ( typeof window === "undefined" ) {
+ return;
+ }
+
+ var fixture = defined.document && document.getElementById &&
+ document.getElementById( "qunit-fixture" );
+
+ if ( fixture ) {
+ fixture.innerHTML = config.fixture;
+ }
+};
+
+QUnit.pushFailure = function() {
+ if ( !QUnit.config.current ) {
+ throw new Error( "pushFailure() assertion outside test context, in " +
+ sourceFromStacktrace( 2 ) );
+ }
+
+ // Gets current test obj
+ var currentTest = QUnit.config.current;
+
+ return currentTest.pushFailure.apply( currentTest, arguments );
+};
+
+// Based on Java's String.hashCode, a simple but not
+// rigorously collision resistant hashing function
+function generateHash( module, testName ) {
+ var hex,
+ i = 0,
+ hash = 0,
+ str = module + "\x1C" + testName,
+ len = str.length;
+
+ for ( ; i < len; i++ ) {
+ hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
+ hash |= 0;
+ }
+
+ // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
+ // strictly necessary but increases user understanding that the id is a SHA-like hash
+ hex = ( 0x100000000 + hash ).toString( 16 );
+ if ( hex.length < 8 ) {
+ hex = "0000000" + hex;
+ }
+
+ return hex.slice( -8 );
}
+function Assert( testContext ) {
+ this.test = testContext;
+}
+
+// Assert helpers
+QUnit.assert = Assert.prototype = {
+
+ // Specify the number of expected assertions to guarantee that failed test
+ // (no assertions are run at all) don't slip through.
+ expect: function( asserts ) {
+ if ( arguments.length === 1 ) {
+ this.test.expected = asserts;
+ } else {
+ return this.test.expected;
+ }
+ },
+
+ // Increment this Test's semaphore counter, then return a single-use function that
+ // decrements that counter a maximum of once.
+ async: function() {
+ var test = this.test,
+ popped = false;
+
+ test.semaphore += 1;
+ test.usedAsync = true;
+ pauseProcessing();
+
+ return function done() {
+ if ( !popped ) {
+ test.semaphore -= 1;
+ popped = true;
+ resumeProcessing();
+ } else {
+ test.pushFailure( "Called the callback returned from `assert.async` more than once",
+ sourceFromStacktrace( 2 ) );
+ }
+ };
+ },
+
+ // Exports test.push() to the user API
+ push: function( /* result, actual, expected, message */ ) {
+ var assert = this,
+ currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
+
+ // Backwards compatibility fix.
+ // Allows the direct use of global exported assertions and QUnit.assert.*
+ // Although, it's use is not recommended as it can leak assertions
+ // to other tests from async tests, because we only get a reference to the current test,
+ // not exactly the test where assertion were intended to be called.
+ if ( !currentTest ) {
+ throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
+ }
+
+ if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
+ currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
+ sourceFromStacktrace( 2 ) );
+
+ // Allow this assertion to continue running anyway...
+ }
+
+ if ( !( assert instanceof Assert ) ) {
+ assert = currentTest.assert;
+ }
+ return assert.test.push.apply( assert.test, arguments );
+ },
+
+ ok: function( result, message ) {
+ message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
+ QUnit.dump.parse( result ) );
+ this.push( !!result, result, true, message );
+ },
+
+ notOk: function( result, message ) {
+ message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
+ QUnit.dump.parse( result ) );
+ this.push( !result, result, false, message );
+ },
+
+ equal: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ this.push( expected == actual, actual, expected, message );
+ },
+
+ notEqual: function( actual, expected, message ) {
+ /*jshint eqeqeq:false */
+ this.push( expected != actual, actual, expected, message );
+ },
+
+ propEqual: function( actual, expected, message ) {
+ actual = objectValues( actual );
+ expected = objectValues( expected );
+ this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ notPropEqual: function( actual, expected, message ) {
+ actual = objectValues( actual );
+ expected = objectValues( expected );
+ this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ deepEqual: function( actual, expected, message ) {
+ this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ notDeepEqual: function( actual, expected, message ) {
+ this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+ },
+
+ strictEqual: function( actual, expected, message ) {
+ this.push( expected === actual, actual, expected, message );
+ },
+
+ notStrictEqual: function( actual, expected, message ) {
+ this.push( expected !== actual, actual, expected, message );
+ },
+
+ "throws": function( block, expected, message ) {
+ var actual, expectedType,
+ expectedOutput = expected,
+ ok = false,
+ currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
+
+ // 'expected' is optional unless doing string comparison
+ if ( message == null && typeof expected === "string" ) {
+ message = expected;
+ expected = null;
+ }
+
+ currentTest.ignoreGlobalErrors = true;
+ try {
+ block.call( currentTest.testEnvironment );
+ } catch (e) {
+ actual = e;
+ }
+ currentTest.ignoreGlobalErrors = false;
+
+ if ( actual ) {
+ expectedType = QUnit.objectType( expected );
+
+ // we don't want to validate thrown error
+ if ( !expected ) {
+ ok = true;
+ expectedOutput = null;
+
+ // expected is a regexp
+ } else if ( expectedType === "regexp" ) {
+ ok = expected.test( errorString( actual ) );
+
+ // expected is a string
+ } else if ( expectedType === "string" ) {
+ ok = expected === errorString( actual );
+
+ // expected is a constructor, maybe an Error constructor
+ } else if ( expectedType === "function" && actual instanceof expected ) {
+ ok = true;
+
+ // expected is an Error object
+ } else if ( expectedType === "object" ) {
+ ok = actual instanceof expected.constructor &&
+ actual.name === expected.name &&
+ actual.message === expected.message;
+
+ // expected is a validation function which returns true if validation passed
+ } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
+ expectedOutput = null;
+ ok = true;
+ }
+ }
+
+ currentTest.assert.push( ok, actual, expectedOutput, message );
+ }
+};
+
+// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
+// Known to us are: Closure Compiler, Narwhal
+(function() {
+ /*jshint sub:true */
+ Assert.prototype.raises = Assert.prototype[ "throws" ];
+}());
+
// Test for equality any JavaScript type.
-// Discussions and reference: http://philrathe.com/articles/equiv
-// Test suites: http://philrathe.com/tests/equiv
// Author: Philippe Rathé <prathe@gmail.com>
-QUnit.equiv = function () {
-
- var innerEquiv; // the real equiv function
- var callers = []; // stack to decide between skip/abort functions
- var parents = []; // stack to avoiding loops from circular referencing
-
-
- // Determine what is o.
- function hoozit(o) {
- if (QUnit.is("String", o)) {
- return "string";
-
- } else if (QUnit.is("Boolean", o)) {
- return "boolean";
-
- } else if (QUnit.is("Number", o)) {
-
- if (isNaN(o)) {
- return "nan";
+QUnit.equiv = (function() {
+
+ // Call the o related callback with the given arguments.
+ function bindCallbacks( o, callbacks, args ) {
+ var prop = QUnit.objectType( o );
+ if ( prop ) {
+ if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
+ return callbacks[ prop ].apply( callbacks, args );
+ } else {
+ return callbacks[ prop ]; // or undefined
+ }
+ }
+ }
+
+ // the real equiv function
+ var innerEquiv,
+
+ // stack to decide between skip/abort functions
+ callers = [],
+
+ // stack to avoiding loops from circular referencing
+ parents = [],
+ parentsB = [],
+
+ getProto = Object.getPrototypeOf || function( obj ) {
+ /* jshint camelcase: false, proto: true */
+ return obj.__proto__;
+ },
+ callbacks = (function() {
+
+ // for string, boolean, number and null
+ function useStrictEquality( b, a ) {
+
+ /*jshint eqeqeq:false */
+ if ( b instanceof a.constructor || a instanceof b.constructor ) {
+
+ // to catch short annotation VS 'new' annotation of a
+ // declaration
+ // e.g. var i = 1;
+ // var j = new Number(1);
+ return a == b;
+ } else {
+ return a === b;
+ }
+ }
+
+ return {
+ "string": useStrictEquality,
+ "boolean": useStrictEquality,
+ "number": useStrictEquality,
+ "null": useStrictEquality,
+ "undefined": useStrictEquality,
+
+ "nan": function( b ) {
+ return isNaN( b );
+ },
+
+ "date": function( b, a ) {
+ return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
+ },
+
+ "regexp": function( b, a ) {
+ return QUnit.objectType( b ) === "regexp" &&
+
+ // the regex itself
+ a.source === b.source &&
+
+ // and its modifiers
+ a.global === b.global &&
+
+ // (gmi) ...
+ a.ignoreCase === b.ignoreCase &&
+ a.multiline === b.multiline &&
+ a.sticky === b.sticky;
+ },
+
+ // - skip when the property is a method of an instance (OOP)
+ // - abort otherwise,
+ // initial === would have catch identical references anyway
+ "function": function() {
+ var caller = callers[ callers.length - 1 ];
+ return caller !== Object && typeof caller !== "undefined";
+ },
+
+ "array": function( b, a ) {
+ var i, j, len, loop, aCircular, bCircular;
+
+ // b could be an object literal here
+ if ( QUnit.objectType( b ) !== "array" ) {
+ return false;
+ }
+
+ len = a.length;
+ if ( len !== b.length ) {
+ // safe and faster
+ return false;
+ }
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+ for ( i = 0; i < len; i++ ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[ j ] === a[ i ];
+ bCircular = parentsB[ j ] === b[ i ];
+ if ( aCircular || bCircular ) {
+ if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ }
+ if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+ parents.pop();
+ parentsB.pop();
+ return false;
+ }
+ }
+ parents.pop();
+ parentsB.pop();
+ return true;
+ },
+
+ "object": function( b, a ) {
+
+ /*jshint forin:false */
+ var i, j, loop, aCircular, bCircular,
+ // Default to true
+ eq = true,
+ aProperties = [],
+ bProperties = [];
+
+ // comparing constructors is more strict than using
+ // instanceof
+ if ( a.constructor !== b.constructor ) {
+
+ // Allow objects with no prototype to be equivalent to
+ // objects with Object as their constructor.
+ if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) ||
+ ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) {
+ return false;
+ }
+ }
+
+ // stack constructor before traversing properties
+ callers.push( a.constructor );
+
+ // track reference to avoid circular references
+ parents.push( a );
+ parentsB.push( b );
+
+ // be strict: don't ensure hasOwnProperty and go deep
+ for ( i in a ) {
+ loop = false;
+ for ( j = 0; j < parents.length; j++ ) {
+ aCircular = parents[ j ] === a[ i ];
+ bCircular = parentsB[ j ] === b[ i ];
+ if ( aCircular || bCircular ) {
+ if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
+ loop = true;
+ } else {
+ eq = false;
+ break;
+ }
+ }
+ }
+ aProperties.push( i );
+ if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
+ eq = false;
+ break;
+ }
+ }
+
+ parents.pop();
+ parentsB.pop();
+ callers.pop(); // unstack, we are done
+
+ for ( i in b ) {
+ bProperties.push( i ); // collect b's properties
+ }
+
+ // Ensures identical properties name
+ return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
+ }
+ };
+ }());
+
+ innerEquiv = function() { // can take multiple arguments
+ var args = [].slice.apply( arguments );
+ if ( args.length < 2 ) {
+ return true; // end transition
+ }
+
+ return ( (function( a, b ) {
+ if ( a === b ) {
+ return true; // catch the most you can
+ } else if ( a === null || b === null || typeof a === "undefined" ||
+ typeof b === "undefined" ||
+ QUnit.objectType( a ) !== QUnit.objectType( b ) ) {
+
+ // don't lose time with error prone cases
+ return false;
+ } else {
+ return bindCallbacks( a, callbacks, [ b, a ] );
+ }
+
+ // apply transition with (1..n) arguments
+ }( args[ 0 ], args[ 1 ] ) ) &&
+ innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
+ };
+
+ return innerEquiv;
+}());
+
+// Based on jsDump by Ariel Flesler
+// http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
+QUnit.dump = (function() {
+ function quote( str ) {
+ return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+ }
+ function literal( o ) {
+ return o + "";
+ }
+ function join( pre, arr, post ) {
+ var s = dump.separator(),
+ base = dump.indent(),
+ inner = dump.indent( 1 );
+ if ( arr.join ) {
+ arr = arr.join( "," + s + inner );
+ }
+ if ( !arr ) {
+ return pre + post;
+ }
+ return [ pre, inner + arr, base + post ].join( s );
+ }
+ function array( arr, stack ) {
+ var i = arr.length,
+ ret = new Array( i );
+
+ if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+ return "[object Array]";
+ }
+
+ this.up();
+ while ( i-- ) {
+ ret[ i ] = this.parse( arr[ i ], undefined, stack );
+ }
+ this.down();
+ return join( "[", ret, "]" );
+ }
+
+ var reName = /^function (\w+)/,
+ dump = {
+
+ // objType is used mostly internally, you can fix a (custom) type in advance
+ parse: function( obj, objType, stack ) {
+ stack = stack || [];
+ var res, parser, parserType,
+ inStack = inArray( obj, stack );
+
+ if ( inStack !== -1 ) {
+ return "recursion(" + ( inStack - stack.length ) + ")";
+ }
+
+ objType = objType || this.typeOf( obj );
+ parser = this.parsers[ objType ];
+ parserType = typeof parser;
+
+ if ( parserType === "function" ) {
+ stack.push( obj );
+ res = parser.call( this, obj, stack );
+ stack.pop();
+ return res;
+ }
+ return ( parserType === "string" ) ? parser : this.parsers.error;
+ },
+ typeOf: function( obj ) {
+ var type;
+ if ( obj === null ) {
+ type = "null";
+ } else if ( typeof obj === "undefined" ) {
+ type = "undefined";
+ } else if ( QUnit.is( "regexp", obj ) ) {
+ type = "regexp";
+ } else if ( QUnit.is( "date", obj ) ) {
+ type = "date";
+ } else if ( QUnit.is( "function", obj ) ) {
+ type = "function";
+ } else if ( obj.setInterval !== undefined &&
+ obj.document !== undefined &&
+ obj.nodeType === undefined ) {
+ type = "window";
+ } else if ( obj.nodeType === 9 ) {
+ type = "document";
+ } else if ( obj.nodeType ) {
+ type = "node";
+ } else if (
+
+ // native arrays
+ toString.call( obj ) === "[object Array]" ||
+
+ // NodeList objects
+ ( typeof obj.length === "number" && obj.item !== undefined &&
+ ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
+ obj[ 0 ] === undefined ) ) )
+ ) {
+ type = "array";
+ } else if ( obj.constructor === Error.prototype.constructor ) {
+ type = "error";
+ } else {
+ type = typeof obj;
+ }
+ return type;
+ },
+ separator: function() {
+ return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " ";
+ },
+ // extra can be a number, shortcut for increasing-calling-decreasing
+ indent: function( extra ) {
+ if ( !this.multiline ) {
+ return "";
+ }
+ var chr = this.indentChar;
+ if ( this.HTML ) {
+ chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
+ }
+ return new Array( this.depth + ( extra || 0 ) ).join( chr );
+ },
+ up: function( a ) {
+ this.depth += a || 1;
+ },
+ down: function( a ) {
+ this.depth -= a || 1;
+ },
+ setParser: function( name, parser ) {
+ this.parsers[ name ] = parser;
+ },
+ // The next 3 are exposed so you can use them
+ quote: quote,
+ literal: literal,
+ join: join,
+ //
+ depth: 1,
+ maxDepth: QUnit.config.maxDepth,
+
+ // This is the list of parsers, to modify them, use dump.setParser
+ parsers: {
+ window: "[Window]",
+ document: "[Document]",
+ error: function( error ) {
+ return "Error(\"" + error.message + "\")";
+ },
+ unknown: "[Unknown]",
+ "null": "null",
+ "undefined": "undefined",
+ "function": function( fn ) {
+ var ret = "function",
+
+ // functions never have name in IE
+ name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
+
+ if ( name ) {
+ ret += " " + name;
+ }
+ ret += "( ";
+
+ ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
+ return join( ret, dump.parse( fn, "functionCode" ), "}" );
+ },
+ array: array,
+ nodelist: array,
+ "arguments": array,
+ object: function( map, stack ) {
+ var keys, key, val, i, nonEnumerableProperties,
+ ret = [];
+
+ if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
+ return "[object Object]";
+ }
+
+ dump.up();
+ keys = [];
+ for ( key in map ) {
+ keys.push( key );
+ }
+
+ // Some properties are not always enumerable on Error objects.
+ nonEnumerableProperties = [ "message", "name" ];
+ for ( i in nonEnumerableProperties ) {
+ key = nonEnumerableProperties[ i ];
+ if ( key in map && inArray( key, keys ) < 0 ) {
+ keys.push( key );
+ }
+ }
+ keys.sort();
+ for ( i = 0; i < keys.length; i++ ) {
+ key = keys[ i ];
+ val = map[ key ];
+ ret.push( dump.parse( key, "key" ) + ": " +
+ dump.parse( val, undefined, stack ) );
+ }
+ dump.down();
+ return join( "{", ret, "}" );
+ },
+ node: function( node ) {
+ var len, i, val,
+ open = dump.HTML ? "<" : "<",
+ close = dump.HTML ? ">" : ">",
+ tag = node.nodeName.toLowerCase(),
+ ret = open + tag,
+ attrs = node.attributes;
+
+ if ( attrs ) {
+ for ( i = 0, len = attrs.length; i < len; i++ ) {
+ val = attrs[ i ].nodeValue;
+
+ // IE6 includes all attributes in .attributes, even ones not explicitly
+ // set. Those have values like undefined, null, 0, false, "" or
+ // "inherit".
+ if ( val && val !== "inherit" ) {
+ ret += " " + attrs[ i ].nodeName + "=" +
+ dump.parse( val, "attribute" );
+ }
+ }
+ }
+ ret += close;
+
+ // Show content of TextNode or CDATASection
+ if ( node.nodeType === 3 || node.nodeType === 4 ) {
+ ret += node.nodeValue;
+ }
+
+ return ret + open + "/" + tag + close;
+ },
+
+ // function calls it internally, it's the arguments part of the function
+ functionArgs: function( fn ) {
+ var args,
+ l = fn.length;
+
+ if ( !l ) {
+ return "";
+ }
+
+ args = new Array( l );
+ while ( l-- ) {
+
+ // 97 is 'a'
+ args[ l ] = String.fromCharCode( 97 + l );
+ }
+ return " " + args.join( ", " ) + " ";
+ },
+ // object calls it internally, the key part of an item in a map
+ key: quote,
+ // function calls it internally, it's the content of the function
+ functionCode: "[code]",
+ // node calls it internally, it's an html attribute value
+ attribute: quote,
+ string: quote,
+ date: quote,
+ regexp: literal,
+ number: literal,
+ "boolean": literal
+ },
+ // if true, entities are escaped ( <, >, \t, space and \n )
+ HTML: false,
+ // indentation unit
+ indentChar: " ",
+ // if true, items in a collection, are separated by a \n, else just a space.
+ multiline: true
+ };
+
+ return dump;
+}());
+
+// back compat
+QUnit.jsDump = QUnit.dump;
+
+// For browser, export only select globals
+if ( typeof window !== "undefined" ) {
+
+ // Deprecated
+ // Extend assert methods to QUnit and Global scope through Backwards compatibility
+ (function() {
+ var i,
+ assertions = Assert.prototype;
+
+ function applyCurrent( current ) {
+ return function() {
+ var assert = new Assert( QUnit.config.current );
+ current.apply( assert, arguments );
+ };
+ }
+
+ for ( i in assertions ) {
+ QUnit[ i ] = applyCurrent( assertions[ i ] );
+ }
+ })();
+
+ (function() {
+ var i, l,
+ keys = [
+ "test",
+ "module",
+ "expect",
+ "asyncTest",
+ "start",
+ "stop",
+ "ok",
+ "notOk",
+ "equal",
+ "notEqual",
+ "propEqual",
+ "notPropEqual",
+ "deepEqual",
+ "notDeepEqual",
+ "strictEqual",
+ "notStrictEqual",
+ "throws"
+ ];
+
+ for ( i = 0, l = keys.length; i < l; i++ ) {
+ window[ keys[ i ] ] = QUnit[ keys[ i ] ];
+ }
+ })();
+
+ window.QUnit = QUnit;
+}
+
+// For nodejs
+if ( typeof module !== "undefined" && module && module.exports ) {
+ module.exports = QUnit;
+
+ // For consistency with CommonJS environments' exports
+ module.exports.QUnit = QUnit;
+}
+
+// For CommonJS with exports, but without module.exports, like Rhino
+if ( typeof exports !== "undefined" && exports ) {
+ exports.QUnit = QUnit;
+}
+
+if ( typeof define === "function" && define.amd ) {
+ define( function() {
+ return QUnit;
+ } );
+ QUnit.config.autostart = false;
+}
+
+// Get a reference to the global object, like window in browsers
+}( (function() {
+ return this;
+})() ));
+
+/*istanbul ignore next */
+// jscs:disable maximumLineLength
+/*
+ * This file is a modified version of google-diff-match-patch's JavaScript implementation
+ * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
+ * modifications are licensed as more fully set forth in LICENSE.txt.
+ *
+ * The original source of google-diff-match-patch is attributable and licensed as follows:
+ *
+ * Copyright 2006 Google Inc.
+ * http://code.google.com/p/google-diff-match-patch/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * More Info:
+ * https://code.google.com/p/google-diff-match-patch/
+ *
+ * Usage: QUnit.diff(expected, actual)
+ *
+ * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick <del>brown </del> fox jump<ins>s</ins><del>ed</del over"
+ */
+QUnit.diff = (function() {
+
+ function DiffMatchPatch() {
+
+ // Defaults.
+ // Redefine these in your program to override the defaults.
+
+ // Number of seconds to map a diff before giving up (0 for infinity).
+ this.DiffTimeout = 1.0;
+ // Cost of an empty edit operation in terms of edit characters.
+ this.DiffEditCost = 4;
+ }
+
+ // DIFF FUNCTIONS
+
+ /**
+ * The data structure representing a diff is an array of tuples:
+ * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
+ * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
+ */
+ var DIFF_DELETE = -1,
+ DIFF_INSERT = 1,
+ DIFF_EQUAL = 0;
+
+ /**
+ * Find the differences between two texts. Simplifies the problem by stripping
+ * any common prefix or suffix off the texts before diffing.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {boolean=} optChecklines Optional speedup flag. If present and false,
+ * then don't run a line-level diff first to identify the changed areas.
+ * Defaults to true, which does a faster, slightly less optimal diff.
+ * @param {number} optDeadline Optional time when the diff should be complete
+ * by. Used internally for recursive calls. Users should set DiffTimeout
+ * instead.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) {
+ var deadline, checklines, commonlength,
+ commonprefix, commonsuffix, diffs;
+ // Set a deadline by which time the diff must be complete.
+ if ( typeof optDeadline === "undefined" ) {
+ if ( this.DiffTimeout <= 0 ) {
+ optDeadline = Number.MAX_VALUE;
} else {
- return "number";
- }
-
- } else if (typeof o === "undefined") {
- return "undefined";
-
- // consider: typeof null === object
- } else if (o === null) {
- return "null";
-
- // consider: typeof [] === object
- } else if (QUnit.is( "Array", o)) {
- return "array";
-
- // consider: typeof new Date() === object
- } else if (QUnit.is( "Date", o)) {
- return "date";
-
- // consider: /./ instanceof Object;
- // /./ instanceof RegExp;
- // typeof /./ === "function"; // => false in IE and Opera,
- // true in FF and Safari
- } else if (QUnit.is( "RegExp", o)) {
- return "regexp";
-
- } else if (typeof o === "object") {
- return "object";
-
- } else if (QUnit.is( "Function", o)) {
- return "function";
- } else {
- return undefined;
- }
- }
-
- // Call the o related callback with the given arguments.
- function bindCallbacks(o, callbacks, args) {
- var prop = hoozit(o);
- if (prop) {
- if (hoozit(callbacks[prop]) === "function") {
- return callbacks[prop].apply(callbacks, args);
- } else {
- return callbacks[prop]; // or undefined
+ optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000;
}
}
- }
-
- var callbacks = function () {
-
- // for string, boolean, number and null
- function useStrictEquality(b, a) {
- if (b instanceof a.constructor || a instanceof b.constructor) {
- // to catch short annotaion VS 'new' annotation of a declaration
- // e.g. var i = 1;
- // var j = new Number(1);
- return a == b;
- } else {
- return a === b;
+ deadline = optDeadline;
+
+ // Check for null inputs.
+ if ( text1 === null || text2 === null ) {
+ throw new Error( "Null input. (DiffMain)" );
+ }
+
+ // Check for equality (speedup).
+ if ( text1 === text2 ) {
+ if ( text1 ) {
+ return [
+ [ DIFF_EQUAL, text1 ]
+ ];
+ }
+ return [];
+ }
+
+ if ( typeof optChecklines === "undefined" ) {
+ optChecklines = true;
+ }
+
+ checklines = optChecklines;
+
+ // Trim off common prefix (speedup).
+ commonlength = this.diffCommonPrefix( text1, text2 );
+ commonprefix = text1.substring( 0, commonlength );
+ text1 = text1.substring( commonlength );
+ text2 = text2.substring( commonlength );
+
+ // Trim off common suffix (speedup).
+ /////////
+ commonlength = this.diffCommonSuffix( text1, text2 );
+ commonsuffix = text1.substring( text1.length - commonlength );
+ text1 = text1.substring( 0, text1.length - commonlength );
+ text2 = text2.substring( 0, text2.length - commonlength );
+
+ // Compute the diff on the middle block.
+ diffs = this.diffCompute( text1, text2, checklines, deadline );
+
+ // Restore the prefix and suffix.
+ if ( commonprefix ) {
+ diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
+ }
+ if ( commonsuffix ) {
+ diffs.push( [ DIFF_EQUAL, commonsuffix ] );
+ }
+ this.diffCleanupMerge( diffs );
+ return diffs;
+ };
+
+ /**
+ * Reduce the number of edits by eliminating operationally trivial equalities.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
+ var changes, equalities, equalitiesLength, lastequality,
+ pointer, preIns, preDel, postIns, postDel;
+ changes = false;
+ equalities = []; // Stack of indices where equalities are found.
+ equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ pointer = 0; // Index of current position.
+ // Is there an insertion operation before the last equality.
+ preIns = false;
+ // Is there a deletion operation before the last equality.
+ preDel = false;
+ // Is there an insertion operation after the last equality.
+ postIns = false;
+ // Is there a deletion operation after the last equality.
+ postDel = false;
+ while ( pointer < diffs.length ) {
+ if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
+ if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) {
+ // Candidate found.
+ equalities[ equalitiesLength++ ] = pointer;
+ preIns = postIns;
+ preDel = postDel;
+ lastequality = diffs[ pointer ][ 1 ];
+ } else {
+ // Not a candidate, and can never become one.
+ equalitiesLength = 0;
+ lastequality = null;
+ }
+ postIns = postDel = false;
+ } else { // An insertion or deletion.
+ if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
+ postDel = true;
+ } else {
+ postIns = true;
+ }
+ /*
+ * Five types to be split:
+ * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+ * <ins>A</ins>X<ins>C</ins><del>D</del>
+ * <ins>A</ins><del>B</del>X<ins>C</ins>
+ * <ins>A</del>X<ins>C</ins><del>D</del>
+ * <ins>A</ins><del>B</del>X<del>C</del>
+ */
+ if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
+ ( ( lastequality.length < this.DiffEditCost / 2 ) &&
+ ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
+ // Duplicate record.
+ diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] );
+ // Change second copy to insert.
+ diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
+ equalitiesLength--; // Throw away the equality we just deleted;
+ lastequality = null;
+ if (preIns && preDel) {
+ // No changes made which could affect previous entry, keep going.
+ postIns = postDel = true;
+ equalitiesLength = 0;
+ } else {
+ equalitiesLength--; // Throw away the previous equality.
+ pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
+ postIns = postDel = false;
+ }
+ changes = true;
+ }
+ }
+ pointer++;
+ }
+
+ if ( changes ) {
+ this.diffCleanupMerge( diffs );
+ }
+ };
+
+ /**
+ * Convert a diff array into a pretty HTML report.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ * @param {integer} string to be beautified.
+ * @return {string} HTML representation.
+ */
+ DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
+ var op, data, x, html = [];
+ for ( x = 0; x < diffs.length; x++ ) {
+ op = diffs[x][0]; // Operation (insert, delete, equal)
+ data = diffs[x][1]; // Text of change.
+ switch ( op ) {
+ case DIFF_INSERT:
+ html[x] = "<ins>" + data + "</ins>";
+ break;
+ case DIFF_DELETE:
+ html[x] = "<del>" + data + "</del>";
+ break;
+ case DIFF_EQUAL:
+ html[x] = "<span>" + data + "</span>";
+ break;
}
}
-
- return {
- "string": useStrictEquality,
- "boolean": useStrictEquality,
- "number": useStrictEquality,
- "null": useStrictEquality,
- "undefined": useStrictEquality,
-
- "nan": function (b) {
- return isNaN(b);
- },
-
- "date": function (b, a) {
- return hoozit(b) === "date" && a.valueOf() === b.valueOf();
- },
-
- "regexp": function (b, a) {
- return hoozit(b) === "regexp" &&
- a.source === b.source && // the regex itself
- a.global === b.global && // and its modifers (gmi) ...
- a.ignoreCase === b.ignoreCase &&
- a.multiline === b.multiline;
- },
-
- // - skip when the property is a method of an instance (OOP)
- // - abort otherwise,
- // initial === would have catch identical references anyway
- "function": function () {
- var caller = callers[callers.length - 1];
- return caller !== Object &&
- typeof caller !== "undefined";
- },
-
- "array": function (b, a) {
- var i, j, loop;
- var len;
-
- // b could be an object literal here
- if ( ! (hoozit(b) === "array")) {
- return false;
- }
-
- len = a.length;
- if (len !== b.length) { // safe and faster
- return false;
+ return html.join("");
+ };
+
+ /**
+ * Determine the common prefix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the start of each
+ * string.
+ */
+ DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
+ var pointermid, pointermax, pointermin, pointerstart;
+ // Quick check for common null cases.
+ if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ pointermin = 0;
+ pointermax = Math.min( text1.length, text2.length );
+ pointermid = pointermax;
+ pointerstart = 0;
+ while ( pointermin < pointermid ) {
+ if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) {
+ pointermin = pointermid;
+ pointerstart = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+ }
+ return pointermid;
+ };
+
+ /**
+ * Determine the common suffix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of each string.
+ */
+ DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
+ var pointermid, pointermax, pointermin, pointerend;
+ // Quick check for common null cases.
+ if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ pointermin = 0;
+ pointermax = Math.min(text1.length, text2.length);
+ pointermid = pointermax;
+ pointerend = 0;
+ while ( pointermin < pointermid ) {
+ if (text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
+ text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
+ pointermin = pointermid;
+ pointerend = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
+ }
+ return pointermid;
+ };
+
+ /**
+ * Find the differences between two texts. Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {boolean} checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster, slightly less optimal diff.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
+ var diffs, longtext, shorttext, i, hm,
+ text1A, text2A, text1B, text2B,
+ midCommon, diffsA, diffsB;
+
+ if ( !text1 ) {
+ // Just add some text (speedup).
+ return [
+ [ DIFF_INSERT, text2 ]
+ ];
+ }
+
+ if (!text2) {
+ // Just delete some text (speedup).
+ return [
+ [ DIFF_DELETE, text1 ]
+ ];
+ }
+
+ longtext = text1.length > text2.length ? text1 : text2;
+ shorttext = text1.length > text2.length ? text2 : text1;
+ i = longtext.indexOf( shorttext );
+ if ( i !== -1 ) {
+ // Shorter text is inside the longer text (speedup).
+ diffs = [
+ [ DIFF_INSERT, longtext.substring( 0, i ) ],
+ [ DIFF_EQUAL, shorttext ],
+ [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
+ ];
+ // Swap insertions for deletions if diff is reversed.
+ if ( text1.length > text2.length ) {
+ diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+ }
+ return diffs;
+ }
+
+ if ( shorttext.length === 1 ) {
+ // Single character string.
+ // After the previous speedup, the character can't be an equality.
+ return [
+ [ DIFF_DELETE, text1 ],
+ [ DIFF_INSERT, text2 ]
+ ];
+ }
+
+ // Check to see if the problem can be split in two.
+ hm = this.diffHalfMatch(text1, text2);
+ if (hm) {
+ // A half-match was found, sort out the return data.
+ text1A = hm[0];
+ text1B = hm[1];
+ text2A = hm[2];
+ text2B = hm[3];
+ midCommon = hm[4];
+ // Send both pairs off for separate processing.
+ diffsA = this.DiffMain(text1A, text2A, checklines, deadline);
+ diffsB = this.DiffMain(text1B, text2B, checklines, deadline);
+ // Merge the results.
+ return diffsA.concat([
+ [ DIFF_EQUAL, midCommon ]
+ ], diffsB);
+ }
+
+ if (checklines && text1.length > 100 && text2.length > 100) {
+ return this.diffLineMode(text1, text2, deadline);
+ }
+
+ return this.diffBisect(text1, text2, deadline);
+ };
+
+ /**
+ * Do the two texts share a substring which is at least half the length of the
+ * longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ * text1, the suffix of text1, the prefix of text2, the suffix of
+ * text2 and the common middle. Or null if there was no match.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) {
+ var longtext, shorttext, dmp,
+ text1A, text2B, text2A, text1B, midCommon,
+ hm1, hm2, hm;
+ if (this.DiffTimeout <= 0) {
+ // Don't risk returning a non-optimal diff if we have unlimited time.
+ return null;
+ }
+ longtext = text1.length > text2.length ? text1 : text2;
+ shorttext = text1.length > text2.length ? text2 : text1;
+ if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+ return null; // Pointless.
+ }
+ dmp = this; // 'this' becomes 'window' in a closure.
+
+ /**
+ * Does a substring of shorttext exist within longtext such that the substring
+ * is at least half the length of longtext?
+ * Closure, but does not reference any external variables.
+ * @param {string} longtext Longer string.
+ * @param {string} shorttext Shorter string.
+ * @param {number} i Start index of quarter length substring within longtext.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ * longtext, the suffix of longtext, the prefix of shorttext, the suffix
+ * of shorttext and the common middle. Or null if there was no match.
+ * @private
+ */
+ function diffHalfMatchI(longtext, shorttext, i) {
+ var seed, j, bestCommon, prefixLength, suffixLength,
+ bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
+ // Start with a 1/4 length substring at position i as a seed.
+ seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+ j = -1;
+ bestCommon = "";
+ while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
+ prefixLength = dmp.diffCommonPrefix(longtext.substring(i),
+ shorttext.substring(j));
+ suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i),
+ shorttext.substring(0, j));
+ if (bestCommon.length < suffixLength + prefixLength) {
+ bestCommon = shorttext.substring(j - suffixLength, j) +
+ shorttext.substring(j, j + prefixLength);
+ bestLongtextA = longtext.substring(0, i - suffixLength);
+ bestLongtextB = longtext.substring(i + prefixLength);
+ bestShorttextA = shorttext.substring(0, j - suffixLength);
+ bestShorttextB = shorttext.substring(j + prefixLength);
}
-
- //track reference to avoid circular references
- parents.push(a);
- for (i = 0; i < len; i++) {
- loop = false;
- for(j=0;j<parents.length;j++){
- if(parents[j] === a[i]){
- loop = true;//dont rewalk array
+ }
+ if (bestCommon.length * 2 >= longtext.length) {
+ return [ bestLongtextA, bestLongtextB,
+ bestShorttextA, bestShorttextB, bestCommon
+ ];
+ } else {
+ return null;
+ }
+ }
+
+ // First check if the second quarter is the seed for a half-match.
+ hm1 = diffHalfMatchI(longtext, shorttext,
+ Math.ceil(longtext.length / 4));
+ // Check again based on the third quarter.
+ hm2 = diffHalfMatchI(longtext, shorttext,
+ Math.ceil(longtext.length / 2));
+ if (!hm1 && !hm2) {
+ return null;
+ } else if (!hm2) {
+ hm = hm1;
+ } else if (!hm1) {
+ hm = hm2;
+ } else {
+ // Both matched. Select the longest.
+ hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+ }
+
+ // A half-match was found, sort out the return data.
+ text1A, text1B, text2A, text2B;
+ if (text1.length > text2.length) {
+ text1A = hm[0];
+ text1B = hm[1];
+ text2A = hm[2];
+ text2B = hm[3];
+ } else {
+ text2A = hm[0];
+ text2B = hm[1];
+ text1A = hm[2];
+ text1B = hm[3];
+ }
+ midCommon = hm[4];
+ return [ text1A, text1B, text2A, text2B, midCommon ];
+ };
+
+ /**
+ * Do a quick line-level diff on both strings, then rediff the parts for
+ * greater accuracy.
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) {
+ var a, diffs, linearray, pointer, countInsert,
+ countDelete, textInsert, textDelete, j;
+ // Scan the text on a line-by-line basis first.
+ a = this.diffLinesToChars(text1, text2);
+ text1 = a.chars1;
+ text2 = a.chars2;
+ linearray = a.lineArray;
+
+ diffs = this.DiffMain(text1, text2, false, deadline);
+
+ // Convert the diff back to original text.
+ this.diffCharsToLines(diffs, linearray);
+ // Eliminate freak matches (e.g. blank lines)
+ this.diffCleanupSemantic(diffs);
+
+ // Rediff any replacement blocks, this time character-by-character.
+ // Add a dummy entry at the end.
+ diffs.push( [ DIFF_EQUAL, "" ] );
+ pointer = 0;
+ countDelete = 0;
+ countInsert = 0;
+ textDelete = "";
+ textInsert = "";
+ while (pointer < diffs.length) {
+ switch ( diffs[pointer][0] ) {
+ case DIFF_INSERT:
+ countInsert++;
+ textInsert += diffs[pointer][1];
+ break;
+ case DIFF_DELETE:
+ countDelete++;
+ textDelete += diffs[pointer][1];
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (countDelete >= 1 && countInsert >= 1) {
+ // Delete the offending records and add the merged ones.
+ diffs.splice(pointer - countDelete - countInsert,
+ countDelete + countInsert);
+ pointer = pointer - countDelete - countInsert;
+ a = this.DiffMain(textDelete, textInsert, false, deadline);
+ for (j = a.length - 1; j >= 0; j--) {
+ diffs.splice( pointer, 0, a[j] );
}
+ pointer = pointer + a.length;
}
- if (!loop && ! innerEquiv(a[i], b[i])) {
- parents.pop();
- return false;
- }
+ countInsert = 0;
+ countDelete = 0;
+ textDelete = "";
+ textInsert = "";
+ break;
+ }
+ pointer++;
+ }
+ diffs.pop(); // Remove the dummy entry at the end.
+
+ return diffs;
+ };
+
+ /**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) {
+ var text1Length, text2Length, maxD, vOffset, vLength,
+ v1, v2, x, delta, front, k1start, k1end, k2start,
+ k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
+ // Cache the text lengths to prevent multiple calls.
+ text1Length = text1.length;
+ text2Length = text2.length;
+ maxD = Math.ceil((text1Length + text2Length) / 2);
+ vOffset = maxD;
+ vLength = 2 * maxD;
+ v1 = new Array(vLength);
+ v2 = new Array(vLength);
+ // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+ // integers and undefined.
+ for (x = 0; x < vLength; x++) {
+ v1[x] = -1;
+ v2[x] = -1;
+ }
+ v1[vOffset + 1] = 0;
+ v2[vOffset + 1] = 0;
+ delta = text1Length - text2Length;
+ // If the total number of characters is odd, then the front path will collide
+ // with the reverse path.
+ front = (delta % 2 !== 0);
+ // Offsets for start and end of k loop.
+ // Prevents mapping of space beyond the grid.
+ k1start = 0;
+ k1end = 0;
+ k2start = 0;
+ k2end = 0;
+ for (d = 0; d < maxD; d++) {
+ // Bail out if deadline is reached.
+ if ((new Date()).getTime() > deadline) {
+ break;
+ }
+
+ // Walk the front path one step.
+ for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+ k1Offset = vOffset + k1;
+ if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
+ x1 = v1[k1Offset + 1];
+ } else {
+ x1 = v1[k1Offset - 1] + 1;
}
- parents.pop();
- return true;
- },
-
- "object": function (b, a) {
- var i, j, loop;
- var eq = true; // unless we can proove it
- var aProperties = [], bProperties = []; // collection of strings
-
- // comparing constructors is more strict than using instanceof
- if ( a.constructor !== b.constructor) {
- return false;
+ y1 = x1 - k1;
+ while (x1 < text1Length && y1 < text2Length &&
+ text1.charAt(x1) === text2.charAt(y1)) {
+ x1++;
+ y1++;
}
-
- // stack constructor before traversing properties
- callers.push(a.constructor);
- //track reference to avoid circular references
- parents.push(a);
-
- for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
- loop = false;
- for(j=0;j<parents.length;j++){
- if(parents[j] === a[i])
- loop = true; //don't go down the same path twice
- }
- aProperties.push(i); // collect a's properties
-
- if (!loop && ! innerEquiv(a[i], b[i])) {
- eq = false;
- break;
+ v1[k1Offset] = x1;
+ if (x1 > text1Length) {
+ // Ran off the right of the graph.
+ k1end += 2;
+ } else if (y1 > text2Length) {
+ // Ran off the bottom of the graph.
+ k1start += 2;
+ } else if (front) {
+ k2Offset = vOffset + delta - k1;
+ if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1Length - v2[k2Offset];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+ }
}
}
-
- callers.pop(); // unstack, we are done
- parents.pop();
-
- for (i in b) {
- bProperties.push(i); // collect b's properties
+ }
+
+ // Walk the reverse path one step.
+ for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+ k2Offset = vOffset + k2;
+ if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
+ x2 = v2[k2Offset + 1];
+ } else {
+ x2 = v2[k2Offset - 1] + 1;
+ }
+ y2 = x2 - k2;
+ while (x2 < text1Length && y2 < text2Length &&
+ text1.charAt(text1Length - x2 - 1) ===
+ text2.charAt(text2Length - y2 - 1)) {
+ x2++;
+ y2++;
+ }
+ v2[k2Offset] = x2;
+ if (x2 > text1Length) {
+ // Ran off the left of the graph.
+ k2end += 2;
+ } else if (y2 > text2Length) {
+ // Ran off the top of the graph.
+ k2start += 2;
+ } else if (!front) {
+ k1Offset = vOffset + delta - k2;
+ if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {
+ x1 = v1[k1Offset];
+ y1 = vOffset + x1 - k1Offset;
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1Length - x2;
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diffBisectSplit(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+ }
+ // Diff took too long and hit the deadline or
+ // number of diffs equals number of characters, no commonality at all.
+ return [
+ [ DIFF_DELETE, text1 ],
+ [ DIFF_INSERT, text2 ]
+ ];
+ };
+
+ /**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} x Index of split point in text1.
+ * @param {number} y Index of split point in text2.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
+ var text1a, text1b, text2a, text2b, diffs, diffsb;
+ text1a = text1.substring(0, x);
+ text2a = text2.substring(0, y);
+ text1b = text1.substring(x);
+ text2b = text2.substring(y);
+
+ // Compute both diffs serially.
+ diffs = this.DiffMain(text1a, text2a, false, deadline);
+ diffsb = this.DiffMain(text1b, text2b, false, deadline);
+
+ return diffs.concat(diffsb);
+ };
+
+ /**
+ * Reduce the number of edits by eliminating semantically trivial equalities.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) {
+ var changes, equalities, equalitiesLength, lastequality,
+ pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
+ lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
+ changes = false;
+ equalities = []; // Stack of indices where equalities are found.
+ equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ pointer = 0; // Index of current position.
+ // Number of characters that changed prior to the equality.
+ lengthInsertions1 = 0;
+ lengthDeletions1 = 0;
+ // Number of characters that changed after the equality.
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ while (pointer < diffs.length) {
+ if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found.
+ equalities[equalitiesLength++] = pointer;
+ lengthInsertions1 = lengthInsertions2;
+ lengthDeletions1 = lengthDeletions2;
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ lastequality = diffs[pointer][1];
+ } else { // An insertion or deletion.
+ if (diffs[pointer][0] === DIFF_INSERT) {
+ lengthInsertions2 += diffs[pointer][1].length;
+ } else {
+ lengthDeletions2 += diffs[pointer][1].length;
+ }
+ // Eliminate an equality that is smaller or equal to the edits on both
+ // sides of it.
+ if (lastequality && (lastequality.length <=
+ Math.max(lengthInsertions1, lengthDeletions1)) &&
+ (lastequality.length <= Math.max(lengthInsertions2,
+ lengthDeletions2))) {
+ // Duplicate record.
+ diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] );
+ // Change second copy to insert.
+ diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
+ // Throw away the equality we just deleted.
+ equalitiesLength--;
+ // Throw away the previous equality (it needs to be reevaluated).
+ equalitiesLength--;
+ pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
+ lengthInsertions1 = 0; // Reset the counters.
+ lengthDeletions1 = 0;
+ lengthInsertions2 = 0;
+ lengthDeletions2 = 0;
+ lastequality = null;
+ changes = true;
}
-
- // Ensures identical properties name
- return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+ }
+ pointer++;
+ }
+
+ // Normalize the diff.
+ if (changes) {
+ this.diffCleanupMerge(diffs);
+ }
+
+ // Find any overlaps between deletions and insertions.
+ // e.g: <del>abcxxx</del><ins>xxxdef</ins>
+ // -> <del>abc</del>xxx<ins>def</ins>
+ // e.g: <del>xxxabc</del><ins>defxxx</ins>
+ // -> <ins>def</ins>xxx<del>abc</del>
+ // Only extract an overlap if it is as big as the edit ahead or behind it.
+ pointer = 1;
+ while (pointer < diffs.length) {
+ if (diffs[pointer - 1][0] === DIFF_DELETE &&
+ diffs[pointer][0] === DIFF_INSERT) {
+ deletion = diffs[pointer - 1][1];
+ insertion = diffs[pointer][1];
+ overlapLength1 = this.diffCommonOverlap(deletion, insertion);
+ overlapLength2 = this.diffCommonOverlap(insertion, deletion);
+ if (overlapLength1 >= overlapLength2) {
+ if (overlapLength1 >= deletion.length / 2 ||
+ overlapLength1 >= insertion.length / 2) {
+ // Overlap found. Insert an equality and trim the surrounding edits.
+ diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] );
+ diffs[pointer - 1][1] =
+ deletion.substring(0, deletion.length - overlapLength1);
+ diffs[pointer + 1][1] = insertion.substring(overlapLength1);
+ pointer++;
+ }
+ } else {
+ if (overlapLength2 >= deletion.length / 2 ||
+ overlapLength2 >= insertion.length / 2) {
+ // Reverse overlap found.
+ // Insert an equality and swap and trim the surrounding edits.
+ diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] );
+ diffs[pointer - 1][0] = DIFF_INSERT;
+ diffs[pointer - 1][1] =
+ insertion.substring(0, insertion.length - overlapLength2);
+ diffs[pointer + 1][0] = DIFF_DELETE;
+ diffs[pointer + 1][1] =
+ deletion.substring(overlapLength2);
+ pointer++;
+ }
+ }
+ pointer++;
}
+ pointer++;
+ }
+ };
+
+ /**
+ * Determine if the suffix of one string is the prefix of another.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of the first
+ * string and the start of the second string.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) {
+ var text1Length, text2Length, textLength,
+ best, length, pattern, found;
+ // Cache the text lengths to prevent multiple calls.
+ text1Length = text1.length;
+ text2Length = text2.length;
+ // Eliminate the null case.
+ if (text1Length === 0 || text2Length === 0) {
+ return 0;
+ }
+ // Truncate the longer string.
+ if (text1Length > text2Length) {
+ text1 = text1.substring(text1Length - text2Length);
+ } else if (text1Length < text2Length) {
+ text2 = text2.substring(0, text1Length);
+ }
+ textLength = Math.min(text1Length, text2Length);
+ // Quick check for the worst case.
+ if (text1 === text2) {
+ return textLength;
+ }
+
+ // Start by looking for a single character match
+ // and increase length until no match is found.
+ // Performance analysis: http://neil.fraser.name/news/2010/11/04/
+ best = 0;
+ length = 1;
+ while (true) {
+ pattern = text1.substring(textLength - length);
+ found = text2.indexOf(pattern);
+ if (found === -1) {
+ return best;
+ }
+ length += found;
+ if (found === 0 || text1.substring(textLength - length) ===
+ text2.substring(0, length)) {
+ best = length;
+ length++;
+ }
+ }
+ };
+
+ /**
+ * Split two texts into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
+ * An object containing the encoded text1, the encoded text2 and
+ * the array of unique strings.
+ * The zeroth element of the array of unique strings is intentionally blank.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) {
+ var lineArray, lineHash, chars1, chars2;
+ lineArray = []; // e.g. lineArray[4] === 'Hello\n'
+ lineHash = {}; // e.g. lineHash['Hello\n'] === 4
+
+ // '\x00' is a valid character, but various debuggers don't like it.
+ // So we'll insert a junk entry to avoid generating a null character.
+ lineArray[0] = "";
+
+ /**
+ * Split a text into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * Modifies linearray and linehash through being a closure.
+ * @param {string} text String to encode.
+ * @return {string} Encoded string.
+ * @private
+ */
+ function diffLinesToCharsMunge(text) {
+ var chars, lineStart, lineEnd, lineArrayLength, line;
+ chars = "";
+ // Walk the text, pulling out a substring for each line.
+ // text.split('\n') would would temporarily double our memory footprint.
+ // Modifying text would create many large strings to garbage collect.
+ lineStart = 0;
+ lineEnd = -1;
+ // Keeping our own length variable is faster than looking it up.
+ lineArrayLength = lineArray.length;
+ while (lineEnd < text.length - 1) {
+ lineEnd = text.indexOf("\n", lineStart);
+ if (lineEnd === -1) {
+ lineEnd = text.length - 1;
+ }
+ line = text.substring(lineStart, lineEnd + 1);
+ lineStart = lineEnd + 1;
+
+ if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
+ (lineHash[line] !== undefined)) {
+ chars += String.fromCharCode( lineHash[ line ] );
+ } else {
+ chars += String.fromCharCode(lineArrayLength);
+ lineHash[line] = lineArrayLength;
+ lineArray[lineArrayLength++] = line;
+ }
+ }
+ return chars;
+ }
+
+ chars1 = diffLinesToCharsMunge(text1);
+ chars2 = diffLinesToCharsMunge(text2);
+ return {
+ chars1: chars1,
+ chars2: chars2,
+ lineArray: lineArray
};
- }();
-
- innerEquiv = function () { // can take multiple arguments
- var args = Array.prototype.slice.apply(arguments);
- if (args.length < 2) {
- return true; // end transition
+ };
+
+ /**
+ * Rehydrate the text in a diff from a string of line hashes to real lines of
+ * text.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ * @param {!Array.<string>} lineArray Array of unique strings.
+ * @private
+ */
+ DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
+ var x, chars, text, y;
+ for ( x = 0; x < diffs.length; x++ ) {
+ chars = diffs[x][1];
+ text = [];
+ for ( y = 0; y < chars.length; y++ ) {
+ text[y] = lineArray[chars.charCodeAt(y)];
+ }
+ diffs[x][1] = text.join("");
+ }
+ };
+
+ /**
+ * Reorder and merge like edit sections. Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
+ */
+ DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) {
+ var pointer, countDelete, countInsert, textInsert, textDelete,
+ commonlength, changes;
+ diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
+ pointer = 0;
+ countDelete = 0;
+ countInsert = 0;
+ textDelete = "";
+ textInsert = "";
+ commonlength;
+ while (pointer < diffs.length) {
+ switch ( diffs[ pointer ][ 0 ] ) {
+ case DIFF_INSERT:
+ countInsert++;
+ textInsert += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_DELETE:
+ countDelete++;
+ textDelete += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (countDelete + countInsert > 1) {
+ if (countDelete !== 0 && countInsert !== 0) {
+ // Factor out any common prefixies.
+ commonlength = this.diffCommonPrefix(textInsert, textDelete);
+ if (commonlength !== 0) {
+ if ((pointer - countDelete - countInsert) > 0 &&
+ diffs[pointer - countDelete - countInsert - 1][0] ===
+ DIFF_EQUAL) {
+ diffs[pointer - countDelete - countInsert - 1][1] +=
+ textInsert.substring(0, commonlength);
+ } else {
+ diffs.splice( 0, 0, [ DIFF_EQUAL,
+ textInsert.substring( 0, commonlength )
+ ] );
+ pointer++;
+ }
+ textInsert = textInsert.substring(commonlength);
+ textDelete = textDelete.substring(commonlength);
+ }
+ // Factor out any common suffixies.
+ commonlength = this.diffCommonSuffix(textInsert, textDelete);
+ if (commonlength !== 0) {
+ diffs[pointer][1] = textInsert.substring(textInsert.length -
+ commonlength) + diffs[pointer][1];
+ textInsert = textInsert.substring(0, textInsert.length -
+ commonlength);
+ textDelete = textDelete.substring(0, textDelete.length -
+ commonlength);
+ }
+ }
+ // Delete the offending records and add the merged ones.
+ if (countDelete === 0) {
+ diffs.splice( pointer - countInsert,
+ countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
+ } else if (countInsert === 0) {
+ diffs.splice( pointer - countDelete,
+ countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
+ } else {
+ diffs.splice( pointer - countDelete - countInsert,
+ countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] );
+ }
+ pointer = pointer - countDelete - countInsert +
+ (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1;
+ } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
+ // Merge this equality with the previous one.
+ diffs[pointer - 1][1] += diffs[pointer][1];
+ diffs.splice(pointer, 1);
+ } else {
+ pointer++;
+ }
+ countInsert = 0;
+ countDelete = 0;
+ textDelete = "";
+ textInsert = "";
+ break;
+ }
+ }
+ if (diffs[diffs.length - 1][1] === "") {
+ diffs.pop(); // Remove the dummy entry at the end.
+ }
+
+ // Second pass: look for single edits surrounded on both sides by equalities
+ // which can be shifted sideways to eliminate an equality.
+ // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+ changes = false;
+ pointer = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < diffs.length - 1) {
+ if (diffs[pointer - 1][0] === DIFF_EQUAL &&
+ diffs[pointer + 1][0] === DIFF_EQUAL) {
+ // This is a single edit surrounded by equalities.
+ if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length -
+ diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) {
+ // Shift the edit over the previous equality.
+ diffs[pointer][1] = diffs[pointer - 1][1] +
+ diffs[pointer][1].substring(0, diffs[pointer][1].length -
+ diffs[pointer - 1][1].length);
+ diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+ diffs.splice(pointer - 1, 1);
+ changes = true;
+ } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
+ diffs[ pointer + 1 ][ 1 ] ) {
+ // Shift the edit over the next equality.
+ diffs[pointer - 1][1] += diffs[pointer + 1][1];
+ diffs[pointer][1] =
+ diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+ diffs[pointer + 1][1];
+ diffs.splice(pointer + 1, 1);
+ changes = true;
+ }
+ }
+ pointer++;
}
-
- return (function (a, b) {
- if (a === b) {
- return true; // catch the most you can
- } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
- return false; // don't lose time with error prone cases
- } else {
- return bindCallbacks(a, callbacks, [b, a]);
- }
-
- // apply transition with (1..n) arguments
- })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+ // If shifts were made, the diff needs reordering and another shift sweep.
+ if (changes) {
+ this.diffCleanupMerge(diffs);
+ }
+ };
+
+ return function(o, n) {
+ var diff, output, text;
+ diff = new DiffMatchPatch();
+ output = diff.DiffMain(o, n);
+ //console.log(output);
+ diff.diffCleanupEfficiency(output);
+ text = diff.diffPrettyHtml(output);
+
+ return text;
};
-
- return innerEquiv;
-
-}();
+}());
+// jscs:enable
+
+(function() {
+
+// Deprecated QUnit.init - Ref #530
+// Re-initialize the configuration options
+QUnit.init = function() {
+ var tests, banner, result, qunit,
+ config = QUnit.config;
+
+ config.stats = { all: 0, bad: 0 };
+ config.moduleStats = { all: 0, bad: 0 };
+ config.started = 0;
+ config.updateRate = 1000;
+ config.blocking = false;
+ config.autostart = true;
+ config.autorun = false;
+ config.filter = "";
+ config.queue = [];
+
+ // Return on non-browser environments
+ // This is necessary to not break on node tests
+ if ( typeof window === "undefined" ) {
+ return;
+ }
+
+ qunit = id( "qunit" );
+ if ( qunit ) {
+ qunit.innerHTML =
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+ "<h2 id='qunit-banner'></h2>" +
+ "<div id='qunit-testrunner-toolbar'></div>" +
+ "<h2 id='qunit-userAgent'></h2>" +
+ "<ol id='qunit-tests'></ol>";
+ }
+
+ tests = id( "qunit-tests" );
+ banner = id( "qunit-banner" );
+ result = id( "qunit-testresult" );
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ }
+
+ if ( banner ) {
+ banner.className = "";
+ }
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running...<br /> ";
+ }
+};
+
+// Don't load the HTML Reporter on non-Browser environments
+if ( typeof window === "undefined" ) {
+ return;
+}
+
+var config = QUnit.config,
+ hasOwn = Object.prototype.hasOwnProperty,
+ defined = {
+ document: window.document !== undefined,
+ sessionStorage: (function() {
+ var x = "qunit-test-string";
+ try {
+ sessionStorage.setItem( x, x );
+ sessionStorage.removeItem( x );
+ return true;
+ } catch ( e ) {
+ return false;
+ }
+ }())
+ },
+ modulesList = [];
+
+/**
+* Escape text for attribute or text content.
+*/
+function escapeText( s ) {
+ if ( !s ) {
+ return "";
+ }
+ s = s + "";
+
+ // Both single quotes and double quotes (for attributes)
+ return s.replace( /['"<>&]/g, function( s ) {
+ switch ( s ) {
+ case "'":
+ return "'";
+ case "\"":
+ return """;
+ case "<":
+ return "<";
+ case ">":
+ return ">";
+ case "&":
+ return "&";
+ }
+ });
+}
+
+/**
+ * @param {HTMLElement} elem
+ * @param {string} type
+ * @param {Function} fn
+ */
+function addEvent( elem, type, fn ) {
+ if ( elem.addEventListener ) {
+
+ // Standards-based browsers
+ elem.addEventListener( type, fn, false );
+ } else if ( elem.attachEvent ) {
+
+ // support: IE <9
+ elem.attachEvent( "on" + type, function() {
+ var event = window.event;
+ if ( !event.target ) {
+ event.target = event.srcElement || document;
+ }
+
+ fn.call( elem, event );
+ });
+ }
+}
/**
- * jsDump
- * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
- * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
- * Date: 5/15/2008
- * @projectDescription Advanced and extensible data dumping for Javascript.
- * @version 1.0.0
- * @author Ariel Flesler
- * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ * @param {Array|NodeList} elems
+ * @param {string} type
+ * @param {Function} fn
*/
-QUnit.jsDump = (function() {
- function quote( str ) {
- return '"' + str.toString().replace(/"/g, '\\"') + '"';
- };
- function literal( o ) {
- return o + '';
- };
- function join( pre, arr, post ) {
- var s = jsDump.separator(),
- base = jsDump.indent(),
- inner = jsDump.indent(1);
- if ( arr.join )
- arr = arr.join( ',' + s + inner );
- if ( !arr )
- return pre + post;
- return [ pre, inner + arr, base + post ].join(s);
- };
- function array( arr ) {
- var i = arr.length, ret = Array(i);
- this.up();
- while ( i-- )
- ret[i] = this.parse( arr[i] );
- this.down();
- return join( '[', ret, ']' );
- };
-
- var reName = /^function (\w+)/;
-
- var jsDump = {
- parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
- var parser = this.parsers[ type || this.typeOf(obj) ];
- type = typeof parser;
-
- return type == 'function' ? parser.call( this, obj ) :
- type == 'string' ? parser :
- this.parsers.error;
- },
- typeOf:function( obj ) {
- var type;
- if ( obj === null ) {
- type = "null";
- } else if (typeof obj === "undefined") {
- type = "undefined";
- } else if (QUnit.is("RegExp", obj)) {
- type = "regexp";
- } else if (QUnit.is("Date", obj)) {
- type = "date";
- } else if (QUnit.is("Function", obj)) {
- type = "function";
- } else if (obj.setInterval && obj.document && !obj.nodeType) {
- type = "window";
- } else if (obj.nodeType === 9) {
- type = "document";
- } else if (obj.nodeType) {
- type = "node";
- } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
- type = "array";
+function addEvents( elems, type, fn ) {
+ var i = elems.length;
+ while ( i-- ) {
+ addEvent( elems[ i ], type, fn );
+ }
+}
+
+function hasClass( elem, name ) {
+ return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
+}
+
+function addClass( elem, name ) {
+ if ( !hasClass( elem, name ) ) {
+ elem.className += ( elem.className ? " " : "" ) + name;
+ }
+}
+
+function toggleClass( elem, name ) {
+ if ( hasClass( elem, name ) ) {
+ removeClass( elem, name );
+ } else {
+ addClass( elem, name );
+ }
+}
+
+function removeClass( elem, name ) {
+ var set = " " + elem.className + " ";
+
+ // Class name may appear multiple times
+ while ( set.indexOf( " " + name + " " ) >= 0 ) {
+ set = set.replace( " " + name + " ", " " );
+ }
+
+ // trim for prettiness
+ elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
+}
+
+function id( name ) {
+ return defined.document && document.getElementById && document.getElementById( name );
+}
+
+function getUrlConfigHtml() {
+ var i, j, val,
+ escaped, escapedTooltip,
+ selection = false,
+ len = config.urlConfig.length,
+ urlConfigHtml = "";
+
+ for ( i = 0; i < len; i++ ) {
+ val = config.urlConfig[ i ];
+ if ( typeof val === "string" ) {
+ val = {
+ id: val,
+ label: val
+ };
+ }
+
+ escaped = escapeText( val.id );
+ escapedTooltip = escapeText( val.tooltip );
+
+ if ( config[ val.id ] === undefined ) {
+ config[ val.id ] = QUnit.urlParams[ val.id ];
+ }
+
+ if ( !val.value || typeof val.value === "string" ) {
+ urlConfigHtml += "<input id='qunit-urlconfig-" + escaped +
+ "' name='" + escaped + "' type='checkbox'" +
+ ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
+ ( config[ val.id ] ? " checked='checked'" : "" ) +
+ " title='" + escapedTooltip + "' /><label for='qunit-urlconfig-" + escaped +
+ "' title='" + escapedTooltip + "'>" + val.label + "</label>";
+ } else {
+ urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
+ "' title='" + escapedTooltip + "'>" + val.label +
+ ": </label><select id='qunit-urlconfig-" + escaped +
+ "' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>";
+
+ if ( QUnit.is( "array", val.value ) ) {
+ for ( j = 0; j < val.value.length; j++ ) {
+ escaped = escapeText( val.value[ j ] );
+ urlConfigHtml += "<option value='" + escaped + "'" +
+ ( config[ val.id ] === val.value[ j ] ?
+ ( selection = true ) && " selected='selected'" : "" ) +
+ ">" + escaped + "</option>";
+ }
} else {
- type = typeof obj;
+ for ( j in val.value ) {
+ if ( hasOwn.call( val.value, j ) ) {
+ urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
+ ( config[ val.id ] === j ?
+ ( selection = true ) && " selected='selected'" : "" ) +
+ ">" + escapeText( val.value[ j ] ) + "</option>";
+ }
+ }
+ }
+ if ( config[ val.id ] && !selection ) {
+ escaped = escapeText( config[ val.id ] );
+ urlConfigHtml += "<option value='" + escaped +
+ "' selected='selected' disabled='disabled'>" + escaped + "</option>";
+ }
+ urlConfigHtml += "</select>";
+ }
+ }
+
+ return urlConfigHtml;
+}
+
+// Handle "click" events on toolbar checkboxes and "change" for select menus.
+// Updates the URL with the new state of `config.urlConfig` values.
+function toolbarChanged() {
+ var updatedUrl, value,
+ field = this,
+ params = {};
+
+ // Detect if field is a select menu or a checkbox
+ if ( "selectedIndex" in field ) {
+ value = field.options[ field.selectedIndex ].value || undefined;
+ } else {
+ value = field.checked ? ( field.defaultValue || true ) : undefined;
+ }
+
+ params[ field.name ] = value;
+ updatedUrl = setUrl( params );
+
+ if ( "hidepassed" === field.name && "replaceState" in window.history ) {
+ config[ field.name ] = value || false;
+ if ( value ) {
+ addClass( id( "qunit-tests" ), "hidepass" );
+ } else {
+ removeClass( id( "qunit-tests" ), "hidepass" );
+ }
+
+ // It is not necessary to refresh the whole page
+ window.history.replaceState( null, "", updatedUrl );
+ } else {
+ window.location = updatedUrl;
+ }
+}
+
+function setUrl( params ) {
+ var key,
+ querystring = "?";
+
+ params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
+
+ for ( key in params ) {
+ if ( hasOwn.call( params, key ) ) {
+ if ( params[ key ] === undefined ) {
+ continue;
+ }
+ querystring += encodeURIComponent( key );
+ if ( params[ key ] !== true ) {
+ querystring += "=" + encodeURIComponent( params[ key ] );
}
- return type;
- },
- separator:function() {
- return this.multiline ? this.HTML ? '<br />' : '\n' : this.HTML ? ' ' : ' ';
- },
- indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
- if ( !this.multiline )
- return '';
- var chr = this.indentChar;
- if ( this.HTML )
- chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
- return Array( this._depth_ + (extra||0) ).join(chr);
- },
- up:function( a ) {
- this._depth_ += a || 1;
- },
- down:function( a ) {
- this._depth_ -= a || 1;
- },
- setParser:function( name, parser ) {
- this.parsers[name] = parser;
- },
- // The next 3 are exposed so you can use them
- quote:quote,
- literal:literal,
- join:join,
- //
- _depth_: 1,
- // This is the list of parsers, to modify them, use jsDump.setParser
- parsers:{
- window: '[Window]',
- document: '[Document]',
- error:'[ERROR]', //when no parser is found, shouldn't happen
- unknown: '[Unknown]',
- 'null':'null',
- undefined:'undefined',
- 'function':function( fn ) {
- var ret = 'function',
- name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
- if ( name )
- ret += ' ' + name;
- ret += '(';
-
- ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
- return join( ret, this.parse(fn,'functionCode'), '}' );
- },
- array: array,
- nodelist: array,
- arguments: array,
- object:function( map ) {
- var ret = [ ];
- this.up();
- for ( var key in map )
- ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
- this.down();
- return join( '{', ret, '}' );
- },
- node:function( node ) {
- var open = this.HTML ? '<' : '<',
- close = this.HTML ? '>' : '>';
-
- var tag = node.nodeName.toLowerCase(),
- ret = open + tag;
-
- for ( var a in this.DOMAttrs ) {
- var val = node[this.DOMAttrs[a]];
- if ( val )
- ret += ' ' + a + '=' + this.parse( val, 'attribute' );
- }
- return ret + close + open + '/' + tag + close;
- },
- functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
- var l = fn.length;
- if ( !l ) return '';
-
- var args = Array(l);
- while ( l-- )
- args[l] = String.fromCharCode(97+l);//97 is 'a'
- return ' ' + args.join(', ') + ' ';
- },
- key:quote, //object calls it internally, the key part of an item in a map
- functionCode:'[code]', //function calls it internally, it's the content of the function
- attribute:quote, //node calls it internally, it's an html attribute value
- string:quote,
- date:quote,
- regexp:literal, //regex
- number:literal,
- 'boolean':literal
- },
- DOMAttrs:{//attributes to dump from nodes, name=>realName
- id:'id',
- name:'name',
- 'class':'className'
- },
- HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
- indentChar:' ',//indentation unit
- multiline:false //if true, items in a collection, are separated by a \n, else just a space.
- };
-
- return jsDump;
+ querystring += "&";
+ }
+ }
+ return location.protocol + "//" + location.host +
+ location.pathname + querystring.slice( 0, -1 );
+}
+
+function applyUrlParams() {
+ var selectedModule,
+ modulesList = id( "qunit-modulefilter" ),
+ filter = id( "qunit-filter-input" ).value;
+
+ selectedModule = modulesList ?
+ decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) :
+ undefined;
+
+ window.location = setUrl({
+ module: ( selectedModule === "" ) ? undefined : selectedModule,
+ filter: ( filter === "" ) ? undefined : filter,
+
+ // Remove testId filter
+ testId: undefined
+ });
+}
+
+function toolbarUrlConfigContainer() {
+ var urlConfigContainer = document.createElement( "span" );
+
+ urlConfigContainer.innerHTML = getUrlConfigHtml();
+ addClass( urlConfigContainer, "qunit-url-config" );
+
+ // For oldIE support:
+ // * Add handlers to the individual elements instead of the container
+ // * Use "click" instead of "change" for checkboxes
+ addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged );
+ addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
+
+ return urlConfigContainer;
+}
+
+function toolbarLooseFilter() {
+ var filter = document.createElement( "form" ),
+ label = document.createElement( "label" ),
+ input = document.createElement( "input" ),
+ button = document.createElement( "button" );
+
+ addClass( filter, "qunit-filter" );
+
+ label.innerHTML = "Filter: ";
+
+ input.type = "text";
+ input.value = config.filter || "";
+ input.name = "filter";
+ input.id = "qunit-filter-input";
+
+ button.innerHTML = "Go";
+
+ label.appendChild( input );
+
+ filter.appendChild( label );
+ filter.appendChild( button );
+ addEvent( filter, "submit", function( ev ) {
+ applyUrlParams();
+
+ if ( ev && ev.preventDefault ) {
+ ev.preventDefault();
+ }
+
+ return false;
+ });
+
+ return filter;
+}
+
+function toolbarModuleFilterHtml() {
+ var i,
+ moduleFilterHtml = "";
+
+ if ( !modulesList.length ) {
+ return false;
+ }
+
+ modulesList.sort(function( a, b ) {
+ return a.localeCompare( b );
+ });
+
+ moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label>" +
+ "<select id='qunit-modulefilter' name='modulefilter'><option value='' " +
+ ( QUnit.urlParams.module === undefined ? "selected='selected'" : "" ) +
+ ">< All Modules ></option>";
+
+ for ( i = 0; i < modulesList.length; i++ ) {
+ moduleFilterHtml += "<option value='" +
+ escapeText( encodeURIComponent( modulesList[ i ] ) ) + "' " +
+ ( QUnit.urlParams.module === modulesList[ i ] ? "selected='selected'" : "" ) +
+ ">" + escapeText( modulesList[ i ] ) + "</option>";
+ }
+ moduleFilterHtml += "</select>";
+
+ return moduleFilterHtml;
+}
+
+function toolbarModuleFilter() {
+ var toolbar = id( "qunit-testrunner-toolbar" ),
+ moduleFilter = document.createElement( "span" ),
+ moduleFilterHtml = toolbarModuleFilterHtml();
+
+ if ( !toolbar || !moduleFilterHtml ) {
+ return false;
+ }
+
+ moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
+ moduleFilter.innerHTML = moduleFilterHtml;
+
+ addEvent( moduleFilter.lastChild, "change", applyUrlParams );
+
+ toolbar.appendChild( moduleFilter );
+}
+
+function appendToolbar() {
+ var toolbar = id( "qunit-testrunner-toolbar" );
+
+ if ( toolbar ) {
+ toolbar.appendChild( toolbarUrlConfigContainer() );
+ toolbar.appendChild( toolbarLooseFilter() );
+ }
+}
+
+function appendHeader() {
+ var header = id( "qunit-header" );
+
+ if ( header ) {
+ header.innerHTML = "<a href='" +
+ setUrl({ filter: undefined, module: undefined, testId: undefined }) +
+ "'>" + header.innerHTML + "</a> ";
+ }
+}
+
+function appendBanner() {
+ var banner = id( "qunit-banner" );
+
+ if ( banner ) {
+ banner.className = "";
+ }
+}
+
+function appendTestResults() {
+ var tests = id( "qunit-tests" ),
+ result = id( "qunit-testresult" );
+
+ if ( result ) {
+ result.parentNode.removeChild( result );
+ }
+
+ if ( tests ) {
+ tests.innerHTML = "";
+ result = document.createElement( "p" );
+ result.id = "qunit-testresult";
+ result.className = "result";
+ tests.parentNode.insertBefore( result, tests );
+ result.innerHTML = "Running...<br /> ";
+ }
+}
+
+function storeFixture() {
+ var fixture = id( "qunit-fixture" );
+ if ( fixture ) {
+ config.fixture = fixture.innerHTML;
+ }
+}
+
+function appendUserAgent() {
+ var userAgent = id( "qunit-userAgent" );
+
+ if ( userAgent ) {
+ userAgent.innerHTML = "";
+ userAgent.appendChild(
+ document.createTextNode(
+ "QUnit " + QUnit.version + "; " + navigator.userAgent
+ )
+ );
+ }
+}
+
+function appendTestsList( modules ) {
+ var i, l, x, z, test, moduleObj;
+
+ for ( i = 0, l = modules.length; i < l; i++ ) {
+ moduleObj = modules[ i ];
+
+ if ( moduleObj.name ) {
+ modulesList.push( moduleObj.name );
+ }
+
+ for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
+ test = moduleObj.tests[ x ];
+
+ appendTest( test.name, test.testId, moduleObj.name );
+ }
+ }
+}
+
+function appendTest( name, testId, moduleName ) {
+ var title, rerunTrigger, testBlock, assertList,
+ tests = id( "qunit-tests" );
+
+ if ( !tests ) {
+ return;
+ }
+
+ title = document.createElement( "strong" );
+ title.innerHTML = getNameHtml( name, moduleName );
+
+ rerunTrigger = document.createElement( "a" );
+ rerunTrigger.innerHTML = "Rerun";
+ rerunTrigger.href = setUrl({ testId: testId });
+
+ testBlock = document.createElement( "li" );
+ testBlock.appendChild( title );
+ testBlock.appendChild( rerunTrigger );
+ testBlock.id = "qunit-test-output-" + testId;
+
+ assertList = document.createElement( "ol" );
+ assertList.className = "qunit-assert-list";
+
+ testBlock.appendChild( assertList );
+
+ tests.appendChild( testBlock );
+}
+
+// HTML Reporter initialization and load
+QUnit.begin(function( details ) {
+ var qunit = id( "qunit" );
+
+ // Fixture is the only one necessary to run without the #qunit element
+ storeFixture();
+
+ if ( qunit ) {
+ qunit.innerHTML =
+ "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
+ "<h2 id='qunit-banner'></h2>" +
+ "<div id='qunit-testrunner-toolbar'></div>" +
+ "<h2 id='qunit-userAgent'></h2>" +
+ "<ol id='qunit-tests'></ol>";
+ }
+
+ appendHeader();
+ appendBanner();
+ appendTestResults();
+ appendUserAgent();
+ appendToolbar();
+ appendTestsList( details.modules );
+ toolbarModuleFilter();
+
+ if ( qunit && config.hidepassed ) {
+ addClass( qunit.lastChild, "hidepass" );
+ }
+});
+
+QUnit.done(function( details ) {
+ var i, key,
+ banner = id( "qunit-banner" ),
+ tests = id( "qunit-tests" ),
+ html = [
+ "Tests completed in ",
+ details.runtime,
+ " milliseconds.<br />",
+ "<span class='passed'>",
+ details.passed,
+ "</span> assertions of <span class='total'>",
+ details.total,
+ "</span> passed, <span class='failed'>",
+ details.failed,
+ "</span> failed."
+ ].join( "" );
+
+ if ( banner ) {
+ banner.className = details.failed ? "qunit-fail" : "qunit-pass";
+ }
+
+ if ( tests ) {
+ id( "qunit-testresult" ).innerHTML = html;
+ }
+
+ if ( config.altertitle && defined.document && document.title ) {
+
+ // show ✖ for good, ✔ for bad suite result in title
+ // use escape sequences in case file gets loaded with non-utf-8-charset
+ document.title = [
+ ( details.failed ? "\u2716" : "\u2714" ),
+ document.title.replace( /^[\u2714\u2716] /i, "" )
+ ].join( " " );
+ }
+
+ // clear own sessionStorage items if all tests passed
+ if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
+ for ( i = 0; i < sessionStorage.length; i++ ) {
+ key = sessionStorage.key( i++ );
+ if ( key.indexOf( "qunit-test-" ) === 0 ) {
+ sessionStorage.removeItem( key );
+ }
+ }
+ }
+
+ // scroll back to top to show results
+ if ( config.scrolltop && window.scrollTo ) {
+ window.scrollTo( 0, 0 );
+ }
+});
+
+function getNameHtml( name, module ) {
+ var nameHtml = "";
+
+ if ( module ) {
+ nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: ";
+ }
+
+ nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>";
+
+ return nameHtml;
+}
+
+QUnit.testStart(function( details ) {
+ var running, testBlock, bad;
+
+ testBlock = id( "qunit-test-output-" + details.testId );
+ if ( testBlock ) {
+ testBlock.className = "running";
+ } else {
+
+ // Report later registered tests
+ appendTest( details.name, details.testId, details.module );
+ }
+
+ running = id( "qunit-testresult" );
+ if ( running ) {
+ bad = QUnit.config.reorder && defined.sessionStorage &&
+ +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name );
+
+ running.innerHTML = ( bad ?
+ "Rerunning previously failed test: <br />" :
+ "Running: <br />" ) +
+ getNameHtml( details.name, details.module );
+ }
+
+});
+
+QUnit.log(function( details ) {
+ var assertList, assertLi,
+ message, expected, actual,
+ testItem = id( "qunit-test-output-" + details.testId );
+
+ if ( !testItem ) {
+ return;
+ }
+
+ message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
+ message = "<span class='test-message'>" + message + "</span>";
+ message += "<span class='runtime'>@ " + details.runtime + " ms</span>";
+
+ // pushFailure doesn't provide details.expected
+ // when it calls, it's implicit to also not show expected and diff stuff
+ // Also, we need to check details.expected existence, as it can exist and be undefined
+ if ( !details.result && hasOwn.call( details, "expected" ) ) {
+ expected = escapeText( QUnit.dump.parse( details.expected ) );
+ actual = escapeText( QUnit.dump.parse( details.actual ) );
+ message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" +
+ expected +
+ "</pre></td></tr>";
+
+ if ( actual !== expected ) {
+ message += "<tr class='test-actual'><th>Result: </th><td><pre>" +
+ actual + "</pre></td></tr>" +
+ "<tr class='test-diff'><th>Diff: </th><td><pre>" +
+ QUnit.diff( expected, actual ) + "</pre></td></tr>";
+ } else {
+ if ( expected.indexOf( "[object Array]" ) !== -1 ||
+ expected.indexOf( "[object Object]" ) !== -1 ) {
+ message += "<tr class='test-message'><th>Message: </th><td>" +
+ "Diff suppressed as the depth of object is more than current max depth (" +
+ QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " +
+ " run with a higher max depth or <a href='" + setUrl({ maxDepth: -1 }) + "'>" +
+ "Rerun</a> without max depth.</p></td></tr>";
+ }
+ }
+
+ if ( details.source ) {
+ message += "<tr class='test-source'><th>Source: </th><td><pre>" +
+ escapeText( details.source ) + "</pre></td></tr>";
+ }
+
+ message += "</table>";
+
+ // this occours when pushFailure is set and we have an extracted stack trace
+ } else if ( !details.result && details.source ) {
+ message += "<table>" +
+ "<tr class='test-source'><th>Source: </th><td><pre>" +
+ escapeText( details.source ) + "</pre></td></tr>" +
+ "</table>";
+ }
+
+ assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+ assertLi = document.createElement( "li" );
+ assertLi.className = details.result ? "pass" : "fail";
+ assertLi.innerHTML = message;
+ assertList.appendChild( assertLi );
+});
+
+QUnit.testDone(function( details ) {
+ var testTitle, time, testItem, assertList,
+ good, bad, testCounts, skipped,
+ tests = id( "qunit-tests" );
+
+ if ( !tests ) {
+ return;
+ }
+
+ testItem = id( "qunit-test-output-" + details.testId );
+
+ assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
+
+ good = details.passed;
+ bad = details.failed;
+
+ // store result when possible
+ if ( config.reorder && defined.sessionStorage ) {
+ if ( bad ) {
+ sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad );
+ } else {
+ sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name );
+ }
+ }
+
+ if ( bad === 0 ) {
+ addClass( assertList, "qunit-collapsed" );
+ }
+
+ // testItem.firstChild is the test name
+ testTitle = testItem.firstChild;
+
+ testCounts = bad ?
+ "<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " :
+ "";
+
+ testTitle.innerHTML += " <b class='counts'>(" + testCounts +
+ details.assertions.length + ")</b>";
+
+ if ( details.skipped ) {
+ testItem.className = "skipped";
+ skipped = document.createElement( "em" );
+ skipped.className = "qunit-skipped-label";
+ skipped.innerHTML = "skipped";
+ testItem.insertBefore( skipped, testTitle );
+ } else {
+ addEvent( testTitle, "click", function() {
+ toggleClass( assertList, "qunit-collapsed" );
+ });
+
+ testItem.className = bad ? "fail" : "pass";
+
+ time = document.createElement( "span" );
+ time.className = "runtime";
+ time.innerHTML = details.runtime + " ms";
+ testItem.insertBefore( time, assertList );
+ }
+});
+
+if ( defined.document ) {
+ if ( document.readyState === "complete" ) {
+ QUnit.load();
+ } else {
+ addEvent( window, "load", QUnit.load );
+ }
+} else {
+ config.pageLoaded = true;
+ config.autorun = true;
+}
+
})();
-
-})(this);