Upgrade QUnit to v1.22.0
authorTimo Tijhof <krinklemail@gmail.com>
Sat, 12 Mar 2016 02:08:19 +0000 (02:08 +0000)
committerTimo Tijhof <krinklemail@gmail.com>
Sat, 12 Mar 2016 02:12:51 +0000 (02:12 +0000)
The v1.19.0 release fixed a major bug in QUnit.test Promise handling

> Test: Don't pass Promise fulfillment value to QUnit.start

Without this fix, passing tests that involve an async Promise
can wrongly be marked as failure when they have a falsey resolution
value due to the resolution value being passed to QUnit.start()
by accident.

Change-Id: Id4b52085529f4cf8996f8053648680026ed62941

RELEASE-NOTES-1.27
resources/lib/qunitjs/qunit.css
resources/lib/qunitjs/qunit.js

index a1614d0..82d8103 100644 (file)
@@ -172,7 +172,8 @@ HHVM 3.1.
 ==== Upgraded external libraries ====
 * Updated oojs/oojs-ui from v0.12.12 to v0.13.3.
 * Updated composer/semver from v1.0.0 to v1.2.0.
-* Update liuggio/statsd-php-client to 1.0.18.
+* Updated liuggio/statsd-php-client to 1.0.18.
+* Updated QUnit from v1.18.0 to v1.22.0.
 
 ==== New external libraries ====
 * Added wikimedia/base-convert v1.0.1.
index f1dcd4e..8c78b67 100644 (file)
@@ -1,27 +1,27 @@
 /*!
- * QUnit 1.18.0
- * http://qunitjs.com/
+ * QUnit 1.22.0
+ * https://qunitjs.com/
  *
  * Copyright jQuery Foundation and other contributors
  * Released under the MIT license
- * http://jquery.org/license
+ * https://jquery.org/license
  *
- * Date: 2015-04-03T10:23Z
+ * Date: 2016-02-23T15:57Z
  */
 
 /** Font Family and Sizes */
 
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult {
        font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
 }
 
-#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
+#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
 #qunit-tests { font-size: smaller; }
 
 
 /** Resets */
 
-#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
+#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
        margin: 0;
        padding: 0;
 }
        overflow: hidden;
 }
 
+#qunit-filteredTest {
+       padding: 0.5em 1em 0.5em 1em;
+       background-color: #F4FF77;
+       color: #366097;
+}
+
 #qunit-userAgent {
        padding: 0.5em 1em 0.5em 1em;
        background-color: #2B81AF;
        display: list-item;
 }
 
+#qunit-tests.hidepass {
+       position: relative;
+}
+
 #qunit-tests.hidepass li.running,
 #qunit-tests.hidepass li.pass {
        visibility: hidden;
        position: absolute;
-       width:   0px;
-       height:  0px;
+       width:   0;
+       height:  0;
        padding: 0;
        border:  0;
        margin:  0;
        border-radius: 5px;
 }
 
+.qunit-source {
+       margin: 0.6em 0 0.3em;
+}
+
 .qunit-collapsed {
        display: none;
 }
index f3542ca..84873ae 100644 (file)
 /*!
- * QUnit 1.18.0
- * http://qunitjs.com/
+ * QUnit 1.22.0
+ * https://qunitjs.com/
  *
  * Copyright jQuery Foundation and other contributors
  * Released under the MIT license
- * http://jquery.org/license
+ * https://jquery.org/license
  *
- * Date: 2015-04-03T10:23Z
+ * Date: 2016-02-23T15:57Z
  */
 
-(function( window ) {
+(function( global ) {
 
-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;
+var QUnit = {};
+
+var Date = global.Date;
+var now = Date.now || function() {
+       return new Date().getTime();
+};
+
+var setTimeout = global.setTimeout;
+var clearTimeout = global.clearTimeout;
+
+// Store a local window from the global to allow direct references.
+var window = global.window;
+
+var defined = {
+       document: window && window.document !== undefined,
+       setTimeout: setTimeout !== undefined,
+       sessionStorage: (function() {
+               var x = "qunit-test-string";
+               try {
+                       sessionStorage.setItem( x, x );
+                       sessionStorage.removeItem( x );
+                       return true;
+               } catch ( e ) {
+                       return false;
+               }
+       }() )
+};
+
+var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" );
+var globalStartCalled = false;
+var runStarted = false;
+
+var toString = Object.prototype.toString,
+       hasOwn = Object.prototype.hasOwnProperty;
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+       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;
                        }
-               }())
-       },
-       /**
-        * 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;
+               }
+       }
+       return result;
+}
+
+// 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;
+}
+
+/**
+ * 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).
+ */
+function objectValues ( 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;
+               }
+       }
+       return vals;
+}
+
+function extend( a, b, undefOnly ) {
+       for ( var prop in b ) {
+               if ( hasOwn.call( b, prop ) ) {
+
+                       // Avoid "Member not found" error in IE8 caused by messing with window.constructor
+                       // This block runs on every environment, so `global` is being used instead of `window`
+                       // to avoid errors on node.
+                       if ( prop !== "constructor" || a !== global ) {
+                               if ( b[ prop ] === undefined ) {
+                                       delete a[ prop ];
+                               } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
+                                       a[ prop ] = b[ prop ];
+                               }
+                       }
+               }
+       }
+
+       return a;
+}
+
+function objectType( 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 "Set":
+               case "Map":
+               case "Date":
+               case "RegExp":
+               case "Function":
+               case "Symbol":
+                       return type.toLowerCase();
+       }
+       if ( typeof obj === "object" ) {
+               return "object";
+       }
+}
+
+// Safe object type checking
+function is( type, obj ) {
+       return QUnit.objectType( obj ) === type;
+}
+
+var getUrlParams = function() {
+       var i, param, name, value;
+       var urlParams = {};
+       var location = window.location;
+       var params = location.search.slice( 1 ).split( "&" );
+       var length = params.length;
+
+       for ( i = 0; i < length; i++ ) {
+               if ( params[ i ] ) {
+                       param = params[ i ].split( "=" );
+                       name = decodeURIComponent( param[ 0 ] );
+
+                       // allow just a key to turn on a flag, e.g., test.html?noglobals
+                       value = param.length === 1 ||
+                               decodeURIComponent( param.slice( 1 ).join( "=" ) ) ;
+                       if ( urlParams[ name ] ) {
+                               urlParams[ name ] = [].concat( urlParams[ name ], value );
                        } else {
-                               return "Error";
+                               urlParams[ name ] = value;
                        }
-               } else {
-                       return errorString;
                }
-       },
-       /**
-        * 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;
+       }
+
+       return urlParams;
+};
+
+// 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 vals;
-       };
+               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;
+       }
+}
+
+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;
+               }
+       }
 
-QUnit = {};
+       return extractStacktrace( error, offset );
+}
 
 /**
  * Config object: Maintain internal state
  * Later exposed as QUnit.config
  * `config` initialized at top of scope
  */
-config = {
+var config = {
        // The queue of tests to run
        queue: [],
 
@@ -110,15 +257,19 @@ config = {
        // by default, modify document.title when suite is done
        altertitle: true,
 
+       // HTML Reporter: collapse every test except the first failing test
+       // If false, all failing tests will be expanded
+       collapse: 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,
 
+       // when enabled, all tests must call expect()
+       requireExpects: false,
+
        // add checkboxes that are persisted in the query-string
        // when enabled, the id is set to `true` as a `QUnit.config` property
        urlConfig: [
@@ -131,7 +282,7 @@ config = {
                        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."
+                               "global object (`window` in Browsers). Stored as query-strings."
                },
                {
                        id: "notrycatch",
@@ -144,6 +295,9 @@ config = {
        // Set of all modules.
        modules: [],
 
+       // Stack of nested modules
+       moduleStack: [],
+
        // The first unnamed module
        currentModule: {
                name: "",
@@ -153,75 +307,162 @@ config = {
        callbacks: {}
 };
 
+var urlParams = defined.document ? getUrlParams() : {};
+
 // 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 ( urlParams.filter === true ) {
+       delete urlParams.filter;
+}
 
-       if ( params[ 0 ] ) {
-               for ( i = 0; i < length; i++ ) {
-                       current = params[ i ].split( "=" );
-                       current[ 0 ] = decodeURIComponent( current[ 0 ] );
+// String search anywhere in moduleName+testName
+config.filter = urlParams.filter;
 
-                       // 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 ];
+config.testId = [];
+if ( urlParams.testId ) {
+       // Ensure that urlParams.testId is an array
+       urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
+       for (var i = 0; i < urlParams.testId.length; i++ ) {
+               config.testId.push( urlParams.testId[ i ] );
+       }
+}
+
+var loggingCallbacks = {};
+
+// Register logging callbacks
+function registerLoggingCallbacks( obj ) {
+       var i, l, key,
+               callbackNames = [ "begin", "done", "log", "testStart", "testDone",
+                       "moduleStart", "moduleDone" ];
+
+       function registerLoggingCallback( key ) {
+               var loggingCallback = function( callback ) {
+                       if ( 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;
+       }
+
+       for ( i = 0, l = callbackNames.length; i < l; i++ ) {
+               key = callbackNames[ i ];
+
+               // Initialize key collection of logging callback
+               if ( objectType( config.callbacks[ key ] ) === "undefined" ) {
+                       config.callbacks[ key ] = [];
                }
+
+               obj[ key ] = registerLoggingCallback( key );
        }
+}
+
+function runLoggingCallbacks( key, args ) {
+       var i, l, callbacks;
 
-       if ( urlParams.filter === true ) {
-               delete urlParams.filter;
+       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;
 
-       QUnit.urlParams = urlParams;
+       for ( loggingCallback in loggingCallbacks ) {
+               if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
 
-       // String search anywhere in moduleName+testName
-       config.filter = urlParams.filter;
+                       userCallback = QUnit[ loggingCallback ];
 
-       if ( urlParams.maxDepth ) {
-               config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ?
-                       Number.POSITIVE_INFINITY :
-                       urlParams.maxDepth;
-       }
+                       // Restore the callback function
+                       QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
 
-       config.testId = [];
-       if ( urlParams.testId ) {
+                       // Assign the deprecated given callback
+                       QUnit[ loggingCallback ]( userCallback );
 
-               // 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 ] );
+                       if ( global.console && global.console.warn ) {
+                               global.console.warn(
+                                       "QUnit." + loggingCallback + " was replaced with a new value.\n" +
+                                       "Please, check out the documentation on how to apply logging callbacks.\n" +
+                                       "Reference: https://api.qunitjs.com/category/callbacks/"
+                               );
+                       }
                }
        }
+}
 
-       // Figure out if we're running the tests from a server or not
-       QUnit.isLocal = location.protocol === "file:";
+( function() {
+       if ( !defined.document ) {
+               return;
+       }
 
-       // Expose the current QUnit version
-       QUnit.version = "1.18.0";
-}());
+       // `onErrorFnPrev` initialized at top of scope
+       // Preserve other handlers
+       var 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;
+                               }
+                               QUnit.pushFailure( error, filePath + ":" + linerNr );
+                       } else {
+                               QUnit.test( "global failure", extend(function() {
+                                       QUnit.pushFailure( error, filePath + ":" + linerNr );
+                               }, { validTest: true } ) );
+                       }
+                       return false;
+               }
+
+               return ret;
+       };
+} )();
+
+QUnit.urlParams = urlParams;
+
+// Figure out if we're running the tests from a server or not
+QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" );
+
+// Expose the current QUnit version
+QUnit.version = "1.22.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: []
-               };
+       module: function( name, testEnvironment, executeNow ) {
+               var module, moduleFns;
+               var currentModule = config.currentModule;
+
+               if ( arguments.length === 2 ) {
+                       if ( testEnvironment instanceof Function ) {
+                               executeNow = testEnvironment;
+                               testEnvironment = undefined;
+                       }
+               }
 
                // DEPRECATED: handles setup/teardown functions,
                // beforeEach and afterEach should be used instead
@@ -234,46 +475,61 @@ extend( QUnit, {
                        delete testEnvironment.teardown;
                }
 
-               config.modules.push( currentModule );
-               config.currentModule = currentModule;
-       },
+               module = createModule();
 
-       // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
-       asyncTest: function( testName, expected, callback ) {
-               if ( arguments.length === 2 ) {
-                       callback = expected;
-                       expected = null;
-               }
+               moduleFns = {
+                       beforeEach: setHook( module, "beforeEach" ),
+                       afterEach: setHook( module, "afterEach" )
+               };
 
-               QUnit.test( testName, expected, callback, true );
-       },
+               if ( executeNow instanceof Function ) {
+                       config.moduleStack.push( module );
+                       setCurrentModule( module );
+                       executeNow.call( module.testEnvironment, moduleFns );
+                       config.moduleStack.pop();
+                       module = module.parentModule || currentModule;
+               }
+
+               setCurrentModule( module );
+
+               function createModule() {
+                       var parentModule = config.moduleStack.length ?
+                               config.moduleStack.slice( -1 )[ 0 ] : null;
+                       var moduleName = parentModule !== null ?
+                               [ parentModule.name, name ].join( " > " ) : name;
+                       var module = {
+                               name: moduleName,
+                               parentModule: parentModule,
+                               tests: []
+                       };
 
-       test: function( testName, expected, callback, async ) {
-               var test;
+                       var env = {};
+                       if ( parentModule ) {
+                               extend( env, parentModule.testEnvironment );
+                               delete env.beforeEach;
+                               delete env.afterEach;
+                       }
+                       extend( env, testEnvironment );
+                       module.testEnvironment = env;
 
-               if ( arguments.length === 2 ) {
-                       callback = expected;
-                       expected = null;
+                       config.modules.push( module );
+                       return module;
                }
 
-               test = new Test({
-                       testName: testName,
-                       expected: expected,
-                       async: async,
-                       callback: callback
-               });
+               function setCurrentModule( module ) {
+                       config.currentModule = module;
+               }
 
-               test.queue();
        },
 
-       skip: function( testName ) {
-               var test = new Test({
-                       testName: testName,
-                       skip: true
-               });
+       // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
+       asyncTest: asyncTest,
 
-               test.queue();
-       },
+       test: test,
+
+       skip: skip,
+
+       only: only,
 
        // 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.
@@ -301,6 +557,17 @@ extend( QUnit, {
                        // If a test is running, adjust its semaphore
                        config.current.semaphore -= count || 1;
 
+                       // If semaphore is non-numeric, throw error
+                       if ( isNaN( config.current.semaphore ) ) {
+                               config.current.semaphore = 0;
+
+                               QUnit.pushFailure(
+                                       "Called start() with a non-numeric decrement.",
+                                       sourceFromStacktrace( 2 )
+                               );
+                               return;
+                       }
+
                        // Don't start until equal number of stop-calls
                        if ( config.current.semaphore > 0 ) {
                                return;
@@ -337,43 +604,9 @@ extend( QUnit, {
 
        config: config,
 
-       // Safe object type checking
-       is: function( type, obj ) {
-               return QUnit.objectType( obj ) === type;
-       },
-
-       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 ] || "";
+       is: is,
 
-               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;
-       },
+       objectType: objectType,
 
        extend: extend,
 
@@ -395,174 +628,50 @@ extend( QUnit, {
                if ( config.autostart ) {
                        resumeProcessing();
                }
+       },
+
+       stack: function( offset ) {
+               offset = ( offset || 0 ) + 2;
+               return sourceFromStacktrace( offset );
        }
 });
 
-// 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."
-                               );
-                       }
+registerLoggingCallbacks( QUnit );
 
-                       config.callbacks[ key ].push( callback );
-               };
+function begin() {
+       var i, l,
+               modulesLog = [];
 
-               // DEPRECATED: This will be removed on QUnit 2.0.0+
-               // Stores the registered functions allowing restoring
-               // at verifyLoggingCallbacks() if modified
-               loggingCallbacks[ key ] = loggingCallback;
+       // If the test run hasn't officially begun yet
+       if ( !config.started ) {
 
-               return loggingCallback;
-       }
+               // Record the time of the test run's beginning
+               config.started = now();
 
-       for ( i = 0, l = callbacks.length; i < l; i++ ) {
-               key = callbacks[ i ];
+               verifyLoggingCallbacks();
 
-               // Initialize key collection of logging callback
-               if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
-                       config.callbacks[ key ] = [];
+               // Delete the loose unnamed module if unused.
+               if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
+                       config.modules.shift();
                }
 
-               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;
-                       }
-                       QUnit.pushFailure( error, filePath + ":" + linerNr );
-               } else {
-                       QUnit.test( "global failure", extend(function() {
-                               QUnit.pushFailure( error, filePath + ":" + linerNr );
-                       }, { validTest: true } ) );
+               // 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
+                       });
                }
-               return false;
-       }
-
-       return ret;
-};
-
-function done() {
-       var runtime, passed;
-
-       config.autorun = true;
 
-       // Log the last module results
-       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
+               // The test run is officially beginning now
+               runLoggingCallbacks( "begin", {
+                       totalTests: Test.count,
+                       modules: modulesLog
                });
        }
-       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
-       });
-}
-
-// 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;
-       }
-}
-
-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 extractStacktrace( error, offset );
-}
-
-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( last );
-       }
+       config.blocking = false;
+       process( true );
 }
 
 function process( last ) {
@@ -592,40 +701,21 @@ function process( last ) {
        }
 }
 
-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
-                       });
-               }
+function pauseProcessing() {
+       config.blocking = true;
 
-               // The test run is officially beginning now
-               runLoggingCallbacks( "begin", {
-                       totalTests: Test.count,
-                       modules: modulesLog
-               });
+       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 );
        }
-
-       config.blocking = false;
-       process( true );
 }
 
 function resumeProcessing() {
@@ -648,143 +738,47 @@ function resumeProcessing() {
        }
 }
 
-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 done() {
+       var runtime, passed;
 
-function saveGlobal() {
-       config.pollution = [];
+       config.autorun = true;
 
-       if ( config.noglobals ) {
-               for ( var key in window ) {
-                       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() {
-       var newGlobals,
-               deletedGlobals,
-               old = config.pollution;
-
-       saveGlobal();
-
-       newGlobals = diff( config.pollution, old );
-       if ( newGlobals.length > 0 ) {
-               QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
-       }
-
-       deletedGlobals = diff( old, config.pollution );
-       if ( deletedGlobals.length > 0 ) {
-               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 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;
-                       }
-               }
+       // Log the last module results
+       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
+               });
        }
-       return result;
-}
-
-function extend( a, b, undefOnly ) {
-       for ( var prop in b ) {
-               if ( hasOwn.call( b, prop ) ) {
+       delete config.previousModule;
 
-                       // 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 ];
-                               }
-                       }
-               }
-       }
+       runtime = now() - config.started;
+       passed = config.stats.all - config.stats.bad;
 
-       return a;
+       runLoggingCallbacks( "done", {
+               failed: config.stats.bad,
+               passed: passed,
+               total: config.stats.all,
+               runtime: runtime
+       });
 }
 
-function runLoggingCallbacks( key, args ) {
-       var i, l, callbacks;
-
-       callbacks = config.callbacks[ key ];
-       for ( i = 0, l = callbacks.length; i < l; i++ ) {
-               callbacks[ i ]( args );
+function setHook( module, hookName ) {
+       if ( module.testEnvironment === undefined ) {
+               module.testEnvironment = {};
        }
-}
-
-// 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/"
-                               );
-                       }
-               }
-       }
+       return function( callback ) {
+               module.testEnvironment[ hookName ] = callback;
+       };
 }
 
-// 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;
-}
+var focused = false;
+var priorityCount = 0;
 
 function Test( settings ) {
        var i, l;
@@ -858,9 +852,11 @@ Test.prototype = {
 
                config.current = this;
 
+               if ( this.module.testEnvironment ) {
+                       delete this.module.testEnvironment.beforeEach;
+                       delete this.module.testEnvironment.afterEach;
+               }
                this.testEnvironment = extend( {}, this.module.testEnvironment );
-               delete this.testEnvironment.beforeEach;
-               delete this.testEnvironment.afterEach;
 
                this.started = now();
                runLoggingCallbacks( "testStart", {
@@ -886,14 +882,12 @@ Test.prototype = {
                this.callbackStarted = now();
 
                if ( config.notrycatch ) {
-                       promise = this.callback.call( this.testEnvironment, this.assert );
-                       this.resolvePromise( promise );
+                       runTest( this );
                        return;
                }
 
                try {
-                       promise = this.callback.call( this.testEnvironment, this.assert );
-                       this.resolvePromise( promise );
+                       runTest( this );
                } catch ( e ) {
                        this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
                                this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
@@ -906,6 +900,11 @@ Test.prototype = {
                                QUnit.start();
                        }
                }
+
+               function runTest( test ) {
+                       promise = test.callback.call( test.testEnvironment, test.assert );
+                       test.resolvePromise( promise );
+               }
        },
 
        after: function() {
@@ -918,16 +917,19 @@ Test.prototype = {
                return function runHook() {
                        config.current = test;
                        if ( config.notrycatch ) {
-                               promise = hook.call( test.testEnvironment, test.assert );
-                               test.resolvePromise( promise, hookName );
+                               callHook();
                                return;
                        }
                        try {
-                               promise = hook.call( test.testEnvironment, test.assert );
-                               test.resolvePromise( promise, hookName );
+                               callHook();
                        } catch ( error ) {
                                test.pushFailure( hookName + " failed on " + test.testName + ": " +
-                                       ( error.message || error ), extractStacktrace( error, 0 ) );
+                               ( error.message || error ), extractStacktrace( error, 0 ) );
+                       }
+
+                       function callHook() {
+                               promise = hook.call( test.testEnvironment, test.assert );
+                               test.resolvePromise( promise, hookName );
                        }
                };
        },
@@ -936,16 +938,20 @@ Test.prototype = {
        hooks: function( handler ) {
                var hooks = [];
 
-               // Hooks are ignored on skipped tests
-               if ( this.skip ) {
-                       return hooks;
+               function processHooks( test, module ) {
+                       if ( module.parentModule ) {
+                               processHooks( test, module.parentModule );
+                       }
+                       if ( module.testEnvironment &&
+                               QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) {
+                               hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) );
+                       }
                }
 
-               if ( this.module.testEnvironment &&
-                               QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
-                       hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
+               // Hooks are ignored on skipped tests
+               if ( !this.skip ) {
+                       processHooks( this, this.module );
                }
-
                return hooks;
        },
 
@@ -990,6 +996,9 @@ Test.prototype = {
                        assertions: this.assertions,
                        testId: this.testId,
 
+                       // Source of Test
+                       source: this.stack,
+
                        // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
                        duration: this.runtime
                });
@@ -1003,7 +1012,7 @@ Test.prototype = {
        },
 
        queue: function() {
-               var bad,
+               var priority,
                        test = this;
 
                if ( !this.valid() ) {
@@ -1019,7 +1028,6 @@ Test.prototype = {
                                },
 
                                test.hooks( "beforeEach" ),
-
                                function() {
                                        test.run();
                                },
@@ -1035,32 +1043,30 @@ Test.prototype = {
                        ]);
                }
 
-               // `bad` initialized at top of scope
-               // defer when previous test run passed, if storage is available
-               bad = QUnit.config.reorder && defined.sessionStorage &&
+               // Prioritize previously failed tests, detected from sessionStorage
+               priority = QUnit.config.reorder && defined.sessionStorage &&
                                +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
 
-               if ( bad ) {
-                       run();
-               } else {
-                       synchronize( run, true );
-               }
+               return synchronize( run, priority );
        },
 
-       push: function( result, actual, expected, message ) {
+       pushResult: function( resultInfo ) {
+
+               // resultInfo = { result, actual, expected, message, negative }
                var source,
                        details = {
                                module: this.module.name,
                                name: this.testName,
-                               result: result,
-                               message: message,
-                               actual: actual,
-                               expected: expected,
+                               result: resultInfo.result,
+                               message: resultInfo.message,
+                               actual: resultInfo.actual,
+                               expected: resultInfo.expected,
                                testId: this.testId,
+                               negative: resultInfo.negative || false,
                                runtime: now() - this.started
                        };
 
-               if ( !result ) {
+               if ( !resultInfo.result ) {
                        source = sourceFromStacktrace();
 
                        if ( source ) {
@@ -1071,13 +1077,13 @@ Test.prototype = {
                runLoggingCallbacks( "log", details );
 
                this.assertions.push({
-                       result: !!result,
-                       message: message
+                       result: !!resultInfo.result,
+                       message: resultInfo.message
                });
        },
 
        pushFailure: function( message, source, actual ) {
-               if ( !this instanceof Test ) {
+               if ( !( this instanceof Test ) ) {
                        throw new Error( "pushFailure() assertion outside test context, was " +
                                sourceFromStacktrace( 2 ) );
                }
@@ -1113,7 +1119,7 @@ Test.prototype = {
                                QUnit.stop();
                                then.call(
                                        promise,
-                                       QUnit.start,
+                                       function() { QUnit.start(); },
                                        function( error ) {
                                                message = "Promise rejected " +
                                                        ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
@@ -1132,10 +1138,21 @@ Test.prototype = {
        },
 
        valid: function() {
-               var include,
-                       filter = config.filter && config.filter.toLowerCase(),
+               var filter = config.filter,
+                       regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ),
                        module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
-                       fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
+                       fullName = ( this.module.name + ": " + this.testName );
+
+               function testInModuleChain( testModule ) {
+                       var testModuleName = testModule.name ? testModule.name.toLowerCase() : null;
+                       if ( testModuleName === module ) {
+                               return true;
+                       } else if ( testModule.parentModule ) {
+                               return testInModuleChain( testModule.parentModule );
+                       } else {
+                               return false;
+                       }
+               }
 
                // Internally-generated tests are always valid
                if ( this.callback && this.callback.validTest ) {
@@ -1146,7 +1163,7 @@ Test.prototype = {
                        return false;
                }
 
-               if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
+               if ( module && !testInModuleChain( this.module ) ) {
                        return false;
                }
 
@@ -1154,7 +1171,23 @@ Test.prototype = {
                        return true;
                }
 
-               include = filter.charAt( 0 ) !== "!";
+               return regexFilter ?
+                       this.regexFilter( !!regexFilter[1], regexFilter[2], regexFilter[3], fullName ) :
+                       this.stringFilter( filter, fullName );
+       },
+
+       regexFilter: function( exclude, pattern, flags, fullName ) {
+               var regex = new RegExp( pattern, flags );
+               var match = regex.test( fullName );
+
+               return match !== exclude;
+       },
+
+       stringFilter: function( filter, fullName ) {
+               filter = filter.toLowerCase();
+               fullName = fullName.toLowerCase();
+
+               var include = filter.charAt( 0 ) !== "!";
                if ( !include ) {
                        filter = filter.slice( 1 );
                }
@@ -1167,7 +1200,6 @@ Test.prototype = {
                // Otherwise, do the opposite
                return !include;
        }
-
 };
 
 // Resets the test setup. Useful for tests that modify the DOM.
@@ -1180,7 +1212,7 @@ QUnit.reset = function() {
 
        // Return on non-browser environments
        // This is necessary to not break on node tests
-       if ( typeof window === "undefined" ) {
+       if ( !defined.document ) {
                return;
        }
 
@@ -1228,6 +1260,129 @@ function generateHash( module, testName ) {
        return hex.slice( -8 );
 }
 
+function synchronize( callback, priority ) {
+       var last = !priority;
+
+       if ( QUnit.objectType( callback ) === "array" ) {
+               while ( callback.length ) {
+                       synchronize( callback.shift() );
+               }
+               return;
+       }
+
+       if ( priority ) {
+               config.queue.splice( priorityCount++, 0, callback );
+       } else {
+               config.queue.push( callback );
+       }
+
+       if ( config.autorun && !config.blocking ) {
+               process( last );
+       }
+}
+
+function saveGlobal() {
+       config.pollution = [];
+
+       if ( config.noglobals ) {
+               for ( var key in global ) {
+                       if ( hasOwn.call( global, 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() {
+       var newGlobals,
+               deletedGlobals,
+               old = config.pollution;
+
+       saveGlobal();
+
+       newGlobals = diff( config.pollution, old );
+       if ( newGlobals.length > 0 ) {
+               QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
+       }
+
+       deletedGlobals = diff( old, config.pollution );
+       if ( deletedGlobals.length > 0 ) {
+               QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
+       }
+}
+
+// Will be exposed as QUnit.asyncTest
+function asyncTest( testName, expected, callback ) {
+       if ( arguments.length === 2 ) {
+               callback = expected;
+               expected = null;
+       }
+
+       QUnit.test( testName, expected, callback, true );
+}
+
+// Will be exposed as QUnit.test
+function test( testName, expected, callback, async ) {
+       if ( focused )  { return; }
+
+       var newTest;
+
+       if ( arguments.length === 2 ) {
+               callback = expected;
+               expected = null;
+       }
+
+       newTest = new Test({
+               testName: testName,
+               expected: expected,
+               async: async,
+               callback: callback
+       });
+
+       newTest.queue();
+}
+
+// Will be exposed as QUnit.skip
+function skip( testName ) {
+       if ( focused )  { return; }
+
+       var test = new Test({
+               testName: testName,
+               skip: true
+       });
+
+       test.queue();
+}
+
+// Will be exposed as QUnit.only
+function only( testName, expected, callback, async ) {
+       var newTest;
+
+       if ( focused )  { return; }
+
+       QUnit.config.queue.length = 0;
+       focused = true;
+
+       if ( arguments.length === 2 ) {
+               callback = expected;
+               expected = null;
+       }
+
+       newTest = new Test({
+               testName: testName,
+               expected: expected,
+               async: async,
+               callback: callback
+       });
+
+       newTest.queue();
+}
+
 function Assert( testContext ) {
        this.test = testContext;
 }
@@ -1245,30 +1400,55 @@ QUnit.assert = Assert.prototype = {
                }
        },
 
-       // Increment this Test's semaphore counter, then return a single-use function that
+       // Increment this Test's semaphore counter, then return a function that
        // decrements that counter a maximum of once.
-       async: function() {
+       async: function( count ) {
                var test = this.test,
-                       popped = false;
+                       popped = false,
+                       acceptCallCount = count;
+
+               if ( typeof acceptCallCount === "undefined" ) {
+                       acceptCallCount = 1;
+               }
 
                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",
+
+                       if ( popped ) {
+                               test.pushFailure( "Too many calls to the `assert.async` callback",
                                        sourceFromStacktrace( 2 ) );
+                               return;
+                       }
+                       acceptCallCount -= 1;
+                       if ( acceptCallCount > 0 ) {
+                               return;
                        }
+
+                       test.semaphore -= 1;
+                       popped = true;
+                       resumeProcessing();
                };
        },
 
        // Exports test.push() to the user API
-       push: function( /* result, actual, expected, message */ ) {
+       // Alias of pushResult.
+       push: function( result, actual, expected, message, negative ) {
+               var currentAssert = this instanceof Assert ? this : QUnit.config.current.assert;
+               return currentAssert.pushResult( {
+                       result: result,
+                       actual: actual,
+                       expected: expected,
+                       message: message,
+                       negative: negative
+               } );
+       },
+
+       pushResult: function( resultInfo ) {
+
+               // resultInfo = { result, actual, expected, message, negative }
                var assert = this,
                        currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
 
@@ -1291,57 +1471,112 @@ QUnit.assert = Assert.prototype = {
                if ( !( assert instanceof Assert ) ) {
                        assert = currentTest.assert;
                }
-               return assert.test.push.apply( assert.test, arguments );
+
+               return assert.test.pushResult( resultInfo );
        },
 
        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 );
+               this.pushResult( {
+                       result: !!result,
+                       actual: result,
+                       expected: true,
+                       message: 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 );
+               this.pushResult( {
+                       result: !result,
+                       actual: result,
+                       expected: false,
+                       message: message
+               } );
        },
 
        equal: function( actual, expected, message ) {
                /*jshint eqeqeq:false */
-               this.push( expected == actual, actual, expected, message );
+               this.pushResult( {
+                       result: expected == actual,
+                       actual: actual,
+                       expected: expected,
+                       message: message
+               } );
        },
 
        notEqual: function( actual, expected, message ) {
                /*jshint eqeqeq:false */
-               this.push( expected != actual, actual, expected, message );
+               this.pushResult( {
+                       result: expected != actual,
+                       actual: actual,
+                       expected: expected,
+                       message: message,
+                       negative: true
+               } );
        },
 
        propEqual: function( actual, expected, message ) {
                actual = objectValues( actual );
                expected = objectValues( expected );
-               this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+               this.pushResult( {
+                       result: QUnit.equiv( actual, expected ),
+                       actual: actual,
+                       expected: expected,
+                       message: message
+               } );
        },
 
        notPropEqual: function( actual, expected, message ) {
                actual = objectValues( actual );
                expected = objectValues( expected );
-               this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+               this.pushResult( {
+                       result: !QUnit.equiv( actual, expected ),
+                       actual: actual,
+                       expected: expected,
+                       message: message,
+                       negative: true
+               } );
        },
 
        deepEqual: function( actual, expected, message ) {
-               this.push( QUnit.equiv( actual, expected ), actual, expected, message );
+               this.pushResult( {
+                       result: QUnit.equiv( actual, expected ),
+                       actual: actual,
+                       expected: expected,
+                       message: message
+               } );
        },
 
        notDeepEqual: function( actual, expected, message ) {
-               this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
+               this.pushResult( {
+                       result: !QUnit.equiv( actual, expected ),
+                       actual: actual,
+                       expected: expected,
+                       message: message,
+                       negative: true
+               } );
        },
 
        strictEqual: function( actual, expected, message ) {
-               this.push( expected === actual, actual, expected, message );
+               this.pushResult( {
+                       result: expected === actual,
+                       actual: actual,
+                       expected: expected,
+                       message: message
+               } );
        },
 
        notStrictEqual: function( actual, expected, message ) {
-               this.push( expected !== actual, actual, expected, message );
+               this.pushResult( {
+                       result: expected !== actual,
+                       actual: actual,
+                       expected: expected,
+                       message: message,
+                       negative: true
+               } );
        },
 
        "throws": function( block, expected, message ) {
@@ -1397,233 +1632,280 @@ QUnit.assert = Assert.prototype = {
                        }
                }
 
-               currentTest.assert.push( ok, actual, expectedOutput, message );
+               currentTest.assert.pushResult( {
+                       result: ok,
+                       actual: actual,
+                       expected: expectedOutput,
+                       message: message
+               } );
        }
 };
 
-// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
+// Provide an alternative to assert.throws(), for environments that consider throws a reserved word
 // Known to us are: Closure Compiler, Narwhal
 (function() {
        /*jshint sub:true */
        Assert.prototype.raises = Assert.prototype[ "throws" ];
 }());
 
+function errorString( error ) {
+       var name, message,
+               resultErrorString = error.toString();
+       if ( resultErrorString.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 resultErrorString;
+       }
+}
+
 // Test for equality any JavaScript type.
 // Author: Philippe Rathé <prathe@gmail.com>
 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
-                       }
-               }
-       }
+       // Stack to decide between skip/abort functions
+       var callers = [];
 
-       // the real equiv function
-       var innerEquiv,
+       // Stack to avoiding loops from circular referencing
+       var parents = [];
+       var parentsB = [];
 
-               // stack to decide between skip/abort functions
-               callers = [],
+       var getProto = Object.getPrototypeOf || function( obj ) {
 
-               // stack to avoiding loops from circular referencing
-               parents = [],
-               parentsB = [],
+               /*jshint proto: true */
+               return obj.__proto__;
+       };
 
-               getProto = Object.getPrototypeOf || function( obj ) {
-                       /* jshint camelcase: false, proto: true */
-                       return obj.__proto__;
-               },
-               callbacks = (function() {
+       function useStrictEquality( b, a ) {
 
-                       // for string, boolean, number and null
-                       function useStrictEquality( b, a ) {
+               // To catch short annotation VS 'new' annotation of a declaration. e.g.:
+               // `var i = 1;`
+               // `var j = new Number(1);`
+               if ( typeof a === "object" ) {
+                       a = a.valueOf();
+               }
+               if ( typeof b === "object" ) {
+                       b = b.valueOf();
+               }
 
-                               /*jshint eqeqeq:false */
-                               if ( b instanceof a.constructor || a instanceof b.constructor ) {
+               return a === b;
+       }
 
-                                       // 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;
-                               }
-                       }
+       function compareConstructors( a, b ) {
+               var protoA = getProto( a );
+               var protoB = getProto( b );
+
+               // Comparing constructors is more strict than using `instanceof`
+               if ( a.constructor === b.constructor ) {
+                       return true;
+               }
 
-                       return {
-                               "string": useStrictEquality,
-                               "boolean": useStrictEquality,
-                               "number": useStrictEquality,
-                               "null": useStrictEquality,
-                               "undefined": useStrictEquality,
+               // Ref #851
+               // If the obj prototype descends from a null constructor, treat it
+               // as a null prototype.
+               if ( protoA && protoA.constructor === null ) {
+                       protoA = null;
+               }
+               if ( protoB && protoB.constructor === null ) {
+                       protoB = null;
+               }
 
-                               "nan": function( b ) {
-                                       return isNaN( b );
-                               },
+               // Allow objects with no prototype to be equivalent to
+               // objects with Object as their constructor.
+               if ( ( protoA === null && protoB === Object.prototype ) ||
+                               ( protoB === null && protoA === Object.prototype ) ) {
+                       return true;
+               }
 
-                               "date": function( b, a ) {
-                                       return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
-                               },
+               return false;
+       }
 
-                               "regexp": function( b, a ) {
-                                       return QUnit.objectType( b ) === "regexp" &&
+       function getRegExpFlags( regexp ) {
+               return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ];
+       }
 
-                                               // the regex itself
-                                               a.source === b.source &&
+       var callbacks = {
+               "string": useStrictEquality,
+               "boolean": useStrictEquality,
+               "number": useStrictEquality,
+               "null": useStrictEquality,
+               "undefined": useStrictEquality,
+               "symbol": useStrictEquality,
+               "date": useStrictEquality,
 
-                                               // and its modifiers
-                                               a.global === b.global &&
+               "nan": function() {
+                       return true;
+               },
 
-                                               // (gmi) ...
-                                               a.ignoreCase === b.ignoreCase &&
-                                               a.multiline === b.multiline &&
-                                               a.sticky === b.sticky;
-                               },
+               "regexp": function( b, a ) {
+                       return a.source === b.source &&
 
-                               // - 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";
-                               },
+                               // Include flags in the comparison
+                               getRegExpFlags( a ) === getRegExpFlags( b );
+               },
 
-                               "array": function( b, a ) {
-                                       var i, j, len, loop, aCircular, bCircular;
+               // - 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";
+               },
 
-                                       // b could be an object literal here
-                                       if ( QUnit.objectType( b ) !== "array" ) {
-                                               return false;
-                                       }
+               "array": function( b, a ) {
+                       var i, j, len, loop, aCircular, bCircular;
 
-                                       len = a.length;
-                                       if ( len !== b.length ) {
-                                               // safe and faster
-                                               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 ] ) ) {
+                       // 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 true;
-                               },
+                                       return false;
+                               }
+                       }
+                       parents.pop();
+                       parentsB.pop();
+                       return true;
+               },
 
-                               "object": function( b, a ) {
+               "set": function( b, a ) {
+                       var aArray, bArray;
 
-                                       /*jshint forin:false */
-                                       var i, j, loop, aCircular, bCircular,
-                                               // Default to true
-                                               eq = true,
-                                               aProperties = [],
-                                               bProperties = [];
+                       aArray = [];
+                       a.forEach( function( v ) {
+                               aArray.push( v );
+                       });
+                       bArray = [];
+                       b.forEach( function( v ) {
+                               bArray.push( v );
+                       });
 
-                                       // comparing constructors is more strict than using
-                                       // instanceof
-                                       if ( a.constructor !== b.constructor ) {
+                       return innerEquiv( bArray, aArray );
+               },
 
-                                               // 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;
-                                               }
-                                       }
+               "map": function( b, a ) {
+                       var aArray, bArray;
 
-                                       // 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 ] ) ) {
+                       aArray = [];
+                       a.forEach( function( v, k ) {
+                               aArray.push( [ k, v ] );
+                       });
+                       bArray = [];
+                       b.forEach( function( v, k ) {
+                               bArray.push( [ k, v ] );
+                       });
+
+                       return innerEquiv( bArray, aArray );
+               },
+
+               "object": function( b, a ) {
+                       var i, j, loop, aCircular, bCircular;
+
+                       // Default to true
+                       var eq = true;
+                       var aProperties = [];
+                       var bProperties = [];
+
+                       if ( compareConstructors( a, b ) === false ) {
+                               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
+                       parents.pop();
+                       parentsB.pop();
 
-                                       for ( i in b ) {
-                                               bProperties.push( i ); // collect b's properties
-                                       }
+                       // Unstack, we are done
+                       callers.pop();
 
-                                       // Ensures identical properties name
-                                       return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
-                               }
-                       };
-               }());
+                       for ( i in b ) {
 
-       innerEquiv = function() { // can take multiple arguments
-               var args = [].slice.apply( arguments );
-               if ( args.length < 2 ) {
-                       return true; // end transition
+                               // Collect b's properties
+                               bProperties.push( i );
+                       }
+
+                       // Ensures identical properties name
+                       return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
                }
+       };
 
-               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 ) ) {
+       function typeEquiv( a, b ) {
+               var type = QUnit.objectType( a );
+               return QUnit.objectType( b ) === type && callbacks[ type ]( b, a );
+       }
 
-                               // don't lose time with error prone cases
-                               return false;
-                       } else {
-                               return bindCallbacks( a, callbacks, [ b, a ] );
-                       }
+       // The real equiv function
+       function innerEquiv( a, b ) {
 
-                       // apply transition with (1..n) arguments
-               }( args[ 0 ], args[ 1 ] ) ) &&
-                       innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
-       };
+               // We're done when there's nothing more to compare
+               if ( arguments.length < 2 ) {
+                       return true;
+               }
+
+               // Require type-specific equality
+               return ( a === b || typeEquiv( a, b ) ) &&
+
+                       // ...across all consecutive argument pairs
+                       ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) );
+       }
 
        return innerEquiv;
 }());
@@ -1632,7 +1914,7 @@ QUnit.equiv = (function() {
 // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
 QUnit.dump = (function() {
        function quote( str ) {
-               return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
+               return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\"";
        }
        function literal( o ) {
                return o + "";
@@ -1839,1225 +2121,1255 @@ QUnit.dump = (function() {
                                                        }
                                                }
                                        }
-                                       ret += close;
+                                       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 a 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;
+
+// Deprecated
+// Extend assert methods to QUnit for 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 ] );
+       }
+})();
+
+// For browser, export only select globals
+if ( defined.document ) {
+
+       (function() {
+               var i, l,
+                       keys = [
+                               "test",
+                               "module",
+                               "expect",
+                               "asyncTest",
+                               "start",
+                               "stop",
+                               "ok",
+                               "notOk",
+                               "equal",
+                               "notEqual",
+                               "propEqual",
+                               "notPropEqual",
+                               "deepEqual",
+                               "notDeepEqual",
+                               "strictEqual",
+                               "notStrictEqual",
+                               "throws",
+                               "raises"
+                       ];
+
+               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;
+}
+
+/*
+ * 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.
+ * https://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
+ *
+ * https://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 = ( function() {
+       function DiffMatchPatch() {
+       }
+
+       //  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.
+        * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
+        */
+       DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) {
+               var deadline, checklines, commonlength,
+                       commonprefix, commonsuffix, diffs;
+
+               // The diff must be complete in up to 1 second.
+               deadline = ( new Date() ).getTime() + 1000;
+
+               // 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 ) {
+
+                       // Equality found.
+                       if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) {
+                               if ( diffs[ pointer ][ 1 ].length < 4 && ( 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;
+
+                       // An insertion or deletion.
+                       } else {
+
+                               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 < 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 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: https://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: https://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;
+
+               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 );
+                               }
+                       }
+                       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;
+                               }
+                               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;
+                               }
+                               y1 = x1 - k1;
+                               while ( x1 < text1Length && y1 < text2Length &&
+                                       text1.charAt( x1 ) === text2.charAt( y1 ) ) {
+                                       x1++;
+                                       y1++;
+                               }
+                               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 );
+                                               }
+                                       }
+                               }
+                       }
 
-                                       // Show content of TextNode or CDATASection
-                                       if ( node.nodeType === 3 || node.nodeType === 4 ) {
-                                               ret += node.nodeValue;
+                       // 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 ]
+               ];
+       };
 
-                                       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;
+       /**
+        * 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 );
+       };
 
-                                       if ( !l ) {
-                                               return "";
-                                       }
+       /**
+        * 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;
+
+                                       // Reset the counters.
+                                       lengthInsertions1 = 0;
+                                       lengthDeletions1 = 0;
+                                       lengthInsertions2 = 0;
+                                       lengthDeletions2 = 0;
+                                       lastequality = null;
+                                       changes = true;
+                               }
+                       }
+                       pointer++;
+               }
 
-                                       args = new Array( l );
-                                       while ( l-- ) {
+               // Normalize the diff.
+               if ( changes ) {
+                       this.diffCleanupMerge( diffs );
+               }
 
-                                               // 97 is 'a'
-                                               args[ l ] = String.fromCharCode( 97 + l );
+               // 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++;
                                        }
-                                       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;
-}());
+                               } 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++;
+               }
+       };
 
-// back compat
-QUnit.jsDump = QUnit.dump;
+       /**
+        * 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;
+               }
 
-// For browser, export only select globals
-if ( typeof window !== "undefined" ) {
+               // Start by looking for a single character match
+               // and increase length until no match is found.
+               // Performance analysis: https://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++;
+                       }
+               }
+       };
 
-       // Deprecated
-       // Extend assert methods to QUnit and Global scope through Backwards compatibility
-       (function() {
-               var i,
-                       assertions = Assert.prototype;
+       /**
+        * 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;
 
-               function applyCurrent( current ) {
-                       return function() {
-                               var assert = new Assert( QUnit.config.current );
-                               current.apply( assert, arguments );
-                       };
+                               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;
                }
 
-               for ( i in assertions ) {
-                       QUnit[ i ] = applyCurrent( assertions[ i ] );
+               chars1 = diffLinesToCharsMunge( text1 );
+               chars2 = diffLinesToCharsMunge( text2 );
+               return {
+                       chars1: chars1,
+                       chars2: chars2,
+                       lineArray: lineArray
+               };
+       };
+
+       /**
+        * 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( "" );
                }
-       })();
+       };
 
-       (function() {
-               var i, l,
-                       keys = [
-                               "test",
-                               "module",
-                               "expect",
-                               "asyncTest",
-                               "start",
-                               "stop",
-                               "ok",
-                               "notOk",
-                               "equal",
-                               "notEqual",
-                               "propEqual",
-                               "notPropEqual",
-                               "deepEqual",
-                               "notDeepEqual",
-                               "strictEqual",
-                               "notStrictEqual",
-                               "throws"
-                       ];
+       /**
+        * 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, diffPointer, position;
+               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 prefixes.
+                                               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 ) {
 
-               for ( i = 0, l = keys.length; i < l; i++ ) {
-                       window[ keys[ i ] ] = QUnit[ keys[ i ] ];
+                                       // 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.
                }
-       })();
 
-       window.QUnit = QUnit;
-}
+               // 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;
 
-// For nodejs
-if ( typeof module !== "undefined" && module && module.exports ) {
-       module.exports = QUnit;
+               // 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 ) {
 
-       // For consistency with CommonJS environments' exports
-       module.exports.QUnit = QUnit;
-}
+                               diffPointer = diffs[ pointer ][ 1 ];
+                               position = diffPointer.substring(
+                                       diffPointer.length - diffs[ pointer - 1 ][ 1 ].length
+                               );
 
-// For CommonJS with exports, but without module.exports, like Rhino
-if ( typeof exports !== "undefined" && exports ) {
-       exports.QUnit = QUnit;
-}
+                               // This is a single edit surrounded by equalities.
+                               if ( position === 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 ( diffPointer.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++;
+               }
+               // If shifts were made, the diff needs reordering and another shift sweep.
+               if ( changes ) {
+                       this.diffCleanupMerge( diffs );
+               }
+       };
 
-if ( typeof define === "function" && define.amd ) {
-       define( function() {
-               return QUnit;
-       } );
-       QUnit.config.autostart = false;
-}
+       return function( o, n ) {
+               var diff, output, text;
+               diff = new DiffMatchPatch();
+               output = diff.DiffMain( o, n );
+               diff.diffCleanupEfficiency( output );
+               text = diff.diffPrettyHtml( output );
+
+               return text;
+       };
+}() );
 
 // 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 {
-                optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000;
-            }
-        }
-        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 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);
-                }
-            }
-            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;
-                    }
-                    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;
-                }
-                y1 = x1 - k1;
-                while (x1 < text1Length && y1 < text2Length &&
-                    text1.charAt(x1) === text2.charAt(y1)) {
-                    x1++;
-                    y1++;
-                }
-                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);
-                        }
-                    }
-                }
-            }
-
-            // 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;
-                }
-            }
-            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
-        };
-    };
-
-    /**
-     * 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++;
-        }
-        // 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;
-    };
-}());
-// jscs:enable
-
 (function() {
 
+// Don't load the HTML Reporter on non-Browser environments
+if ( typeof window === "undefined" || !window.document ) {
+       return;
+}
+
 // Deprecated QUnit.init - Ref #530
 // Re-initialize the configuration options
 QUnit.init = function() {
@@ -3115,12 +3427,8 @@ QUnit.init = function() {
        }
 };
 
-// Don't load the HTML Reporter on non-Browser environments
-if ( typeof window === "undefined" ) {
-       return;
-}
-
 var config = QUnit.config,
+       collapseNext = false,
        hasOwn = Object.prototype.hasOwnProperty,
        defined = {
                document: window.document !== undefined,
@@ -3479,7 +3787,7 @@ function appendHeader() {
 
        if ( header ) {
                header.innerHTML = "<a href='" +
-                       setUrl({ filter: undefined, module: undefined, testId: undefined }) +
+                       escapeText( setUrl( { filter: undefined, module: undefined, testId: undefined } ) ) +
                        "'>" + header.innerHTML + "</a> ";
        }
 }
@@ -3517,6 +3825,18 @@ function storeFixture() {
        }
 }
 
+function appendFilteredTest() {
+       var testId = QUnit.config.testId;
+       if ( !testId || testId.length <= 0 ) {
+               return "";
+       }
+       return "<div id='qunit-filteredTest'>Rerunning selected tests: " +
+               escapeText( testId.join(", ") ) +
+               " <a id='qunit-clearFilter' href='" +
+               escapeText( setUrl( { filter: undefined, module: undefined, testId: undefined } ) ) +
+               "'>" + "Run all tests" + "</a></div>";
+}
+
 function appendUserAgent() {
        var userAgent = id( "qunit-userAgent" );
 
@@ -3524,7 +3844,7 @@ function appendUserAgent() {
                userAgent.innerHTML = "";
                userAgent.appendChild(
                        document.createTextNode(
-                               "QUnit " + QUnit.version  + "; " + navigator.userAgent
+                               "QUnit " + QUnit.version + "; " + navigator.userAgent
                        )
                );
        }
@@ -3588,6 +3908,7 @@ QUnit.begin(function( details ) {
                        "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
                        "<h2 id='qunit-banner'></h2>" +
                        "<div id='qunit-testrunner-toolbar'></div>" +
+                       appendFilteredTest() +
                        "<h2 id='qunit-userAgent'></h2>" +
                        "<ol id='qunit-tests'></ol>";
        }
@@ -3693,9 +4014,15 @@ QUnit.testStart(function( details ) {
 
 });
 
+function stripHtml( string ) {
+       // strip tags, html entity and whitespaces
+       return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\&quot;/g, "").replace(/\s+/g, "");
+}
+
 QUnit.log(function( details ) {
        var assertList, assertLi,
-               message, expected, actual,
+               message, expected, actual, diff,
+               showDiff = false,
                testItem = id( "qunit-test-output-" + details.testId );
 
        if ( !testItem ) {
@@ -3710,26 +4037,44 @@ QUnit.log(function( details ) {
        // 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 ) );
+               if ( details.negative ) {
+                       expected = escapeText( "NOT " + QUnit.dump.parse( details.expected ) );
+               } else {
+                       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>";
+                               actual + "</pre></td></tr>";
+
+                       // Don't show diff if actual or expected are booleans
+                       if ( !( /^(true|false)$/.test( actual ) ) &&
+                                       !( /^(true|false)$/.test( expected ) ) ) {
+                               diff = QUnit.diff( expected, actual );
+                               showDiff = stripHtml( diff ).length !==
+                                       stripHtml( expected ).length +
+                                       stripHtml( actual ).length;
+                       }
+
+                       // Don't show diff if expected and actual are totally different
+                       if ( showDiff ) {
+                               message += "<tr class='test-diff'><th>Diff: </th><td><pre>" +
+                                       diff + "</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='" +
+                               escapeText( setUrl( { maxDepth: -1 } ) ) + "'>" +
+                               "Rerun</a> without max depth.</p></td></tr>";
                }
 
                if ( details.source ) {
@@ -3739,7 +4084,7 @@ QUnit.log(function( details ) {
 
                message += "</table>";
 
-       // this occours when pushFailure is set and we have an extracted stack trace
+       // this occurs 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>" +
@@ -3757,7 +4102,7 @@ QUnit.log(function( details ) {
 
 QUnit.testDone(function( details ) {
        var testTitle, time, testItem, assertList,
-               good, bad, testCounts, skipped,
+               good, bad, testCounts, skipped, sourceName,
                tests = id( "qunit-tests" );
 
        if ( !tests ) {
@@ -3781,6 +4126,16 @@ QUnit.testDone(function( details ) {
        }
 
        if ( bad === 0 ) {
+
+               // Collapse the passing tests
+               addClass( assertList, "qunit-collapsed" );
+       } else if ( bad && config.collapse && !collapseNext ) {
+
+               // Skip collapsing the first failing test
+               collapseNext = true;
+       } else {
+
+               // Collapse remaining tests
                addClass( assertList, "qunit-collapsed" );
        }
 
@@ -3812,10 +4167,31 @@ QUnit.testDone(function( details ) {
                time.innerHTML = details.runtime + " ms";
                testItem.insertBefore( time, assertList );
        }
+
+       // Show the source of the test when showing assertions
+       if ( details.source ) {
+               sourceName = document.createElement( "p" );
+               sourceName.innerHTML = "<strong>Source: </strong>" + details.source;
+               addClass( sourceName, "qunit-source" );
+               if ( bad === 0 ) {
+                       addClass( sourceName, "qunit-collapsed" );
+               }
+               addEvent( testTitle, "click", function() {
+                       toggleClass( sourceName, "qunit-collapsed" );
+               });
+               testItem.appendChild( sourceName );
+       }
 });
 
 if ( defined.document ) {
-       if ( document.readyState === "complete" ) {
+
+       // Avoid readyState issue with phantomjs
+       // Ref: #818
+       var notPhantom = ( function( p ) {
+               return !( p && p.version && p.version.major > 0 );
+       } )( window.phantom );
+
+       if ( notPhantom && document.readyState === "complete" ) {
                QUnit.load();
        } else {
                addEvent( window, "load", QUnit.load );