5 * Copyright jQuery Foundation and other contributors
6 * Released under the MIT license
7 * http://jquery.org/license
9 * Date: 2015-04-03T10:23Z
17 loggingCallbacks = {},
18 fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ),
19 toString = Object.prototype.toString,
20 hasOwn = Object.prototype.hasOwnProperty,
21 // Keep a local reference to Date (GH-283)
23 now = Date.now || function() {
24 return new Date().getTime();
26 globalStartCalled = false,
28 setTimeout = window.setTimeout,
29 clearTimeout = window.clearTimeout,
31 document: window.document !== undefined,
32 setTimeout: window.setTimeout !== undefined,
33 sessionStorage: (function() {
34 var x = "qunit-test-string";
36 sessionStorage.setItem( x, x );
37 sessionStorage.removeItem( x );
45 * Provides a normalized error string, correcting an issue
46 * with IE 7 (and prior) where Error.prototype.toString is
47 * not properly implemented
49 * Based on http://es5.github.com/#x15.11.4.4
51 * @param {String|Error} error
52 * @return {String} error message
54 errorString = function( error ) {
56 errorString = error.toString();
57 if ( errorString.substring( 0, 7 ) === "[object" ) {
58 name = error.name ? error.name.toString() : "Error";
59 message = error.message ? error.message.toString() : "";
60 if ( name && message ) {
61 return name + ": " + message;
64 } else if ( message ) {
74 * Makes a clone of an object using only Array or Object as base,
75 * and copies over the own enumerable properties.
78 * @return {Object} New object with only the own properties (recursively).
80 objectValues = function( obj ) {
82 vals = QUnit.is( "array", obj ) ? [] : {};
84 if ( hasOwn.call( obj, key ) ) {
86 vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
95 * Config object: Maintain internal state
96 * Later exposed as QUnit.config
97 * `config` initialized at top of scope
100 // The queue of tests to run
103 // block until document ready
106 // by default, run previously failed tests first
107 // very useful in combination with "Hide passed tests" checked
110 // by default, modify document.title when suite is done
113 // by default, scroll to top of the page when suite is done
116 // when enabled, all tests must call expect()
117 requireExpects: false,
119 // depth up-to which object will be dumped
122 // add checkboxes that are persisted in the query-string
123 // when enabled, the id is set to `true` as a `QUnit.config` property
127 label: "Hide passed tests",
128 tooltip: "Only show tests and assertions that fail. Stored as query-strings."
132 label: "Check for Globals",
133 tooltip: "Enabling this will test if any test introduces new properties on the " +
134 "`window` object. Stored as query-strings."
138 label: "No try-catch",
139 tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
140 "exceptions in IE reasonable. Stored as query-strings."
144 // Set of all modules.
147 // The first unnamed module
156 // Push a loose unnamed module to the modules collection
157 config.modules.push( config.currentModule );
159 // Initialize more QUnit.config and QUnit.urlParams
162 location = window.location || { search: "", protocol: "file:" },
163 params = location.search.slice( 1 ).split( "&" ),
164 length = params.length,
168 for ( i = 0; i < length; i++ ) {
169 current = params[ i ].split( "=" );
170 current[ 0 ] = decodeURIComponent( current[ 0 ] );
172 // allow just a key to turn on a flag, e.g., test.html?noglobals
173 current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
174 if ( urlParams[ current[ 0 ] ] ) {
175 urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
177 urlParams[ current[ 0 ] ] = current[ 1 ];
182 if ( urlParams.filter === true ) {
183 delete urlParams.filter;
186 QUnit.urlParams = urlParams;
188 // String search anywhere in moduleName+testName
189 config.filter = urlParams.filter;
191 if ( urlParams.maxDepth ) {
192 config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ?
193 Number.POSITIVE_INFINITY :
198 if ( urlParams.testId ) {
200 // Ensure that urlParams.testId is an array
201 urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
202 for ( i = 0; i < urlParams.testId.length; i++ ) {
203 config.testId.push( urlParams.testId[ i ] );
207 // Figure out if we're running the tests from a server or not
208 QUnit.isLocal = location.protocol === "file:";
210 // Expose the current QUnit version
211 QUnit.version = "1.18.0";
214 // Root QUnit object.
215 // `QUnit` initialized at top of scope
218 // call on start of module test to prepend name to all tests
219 module: function( name, testEnvironment ) {
220 var currentModule = {
222 testEnvironment: testEnvironment,
226 // DEPRECATED: handles setup/teardown functions,
227 // beforeEach and afterEach should be used instead
228 if ( testEnvironment && testEnvironment.setup ) {
229 testEnvironment.beforeEach = testEnvironment.setup;
230 delete testEnvironment.setup;
232 if ( testEnvironment && testEnvironment.teardown ) {
233 testEnvironment.afterEach = testEnvironment.teardown;
234 delete testEnvironment.teardown;
237 config.modules.push( currentModule );
238 config.currentModule = currentModule;
241 // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
242 asyncTest: function( testName, expected, callback ) {
243 if ( arguments.length === 2 ) {
248 QUnit.test( testName, expected, callback, true );
251 test: function( testName, expected, callback, async ) {
254 if ( arguments.length === 2 ) {
269 skip: function( testName ) {
270 var test = new Test({
278 // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
279 // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
280 start: function( count ) {
281 var globalStartAlreadyCalled = globalStartCalled;
283 if ( !config.current ) {
284 globalStartCalled = true;
287 throw new Error( "Called start() outside of a test context while already started" );
288 } else if ( globalStartAlreadyCalled || count > 1 ) {
289 throw new Error( "Called start() outside of a test context too many times" );
290 } else if ( config.autostart ) {
291 throw new Error( "Called start() outside of a test context when " +
292 "QUnit.config.autostart was true" );
293 } else if ( !config.pageLoaded ) {
295 // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
296 config.autostart = true;
301 // If a test is running, adjust its semaphore
302 config.current.semaphore -= count || 1;
304 // Don't start until equal number of stop-calls
305 if ( config.current.semaphore > 0 ) {
309 // throw an Error if start is called more often than stop
310 if ( config.current.semaphore < 0 ) {
311 config.current.semaphore = 0;
314 "Called start() while already started (test's semaphore was 0 already)",
315 sourceFromStacktrace( 2 )
324 // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
325 stop: function( count ) {
327 // If there isn't a test running, don't allow QUnit.stop() to be called
328 if ( !config.current ) {
329 throw new Error( "Called stop() outside of a test context" );
332 // If a test is running, adjust its semaphore
333 config.current.semaphore += count || 1;
340 // Safe object type checking
341 is: function( type, obj ) {
342 return QUnit.objectType( obj ) === type;
345 objectType: function( obj ) {
346 if ( typeof obj === "undefined" ) {
350 // Consider: typeof null === object
351 if ( obj === null ) {
355 var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
356 type = match && match[ 1 ] || "";
360 if ( isNaN( obj ) ) {
370 return type.toLowerCase();
372 if ( typeof obj === "object" ) {
381 config.pageLoaded = true;
383 // Initialize the configuration options
385 stats: { all: 0, bad: 0 },
386 moduleStats: { all: 0, bad: 0 },
393 config.blocking = false;
395 if ( config.autostart ) {
401 // Register logging callbacks
404 callbacks = [ "begin", "done", "log", "testStart", "testDone",
405 "moduleStart", "moduleDone" ];
407 function registerLoggingCallback( key ) {
408 var loggingCallback = function( callback ) {
409 if ( QUnit.objectType( callback ) !== "function" ) {
411 "QUnit logging methods require a callback function as their first parameters."
415 config.callbacks[ key ].push( callback );
418 // DEPRECATED: This will be removed on QUnit 2.0.0+
419 // Stores the registered functions allowing restoring
420 // at verifyLoggingCallbacks() if modified
421 loggingCallbacks[ key ] = loggingCallback;
423 return loggingCallback;
426 for ( i = 0, l = callbacks.length; i < l; i++ ) {
427 key = callbacks[ i ];
429 // Initialize key collection of logging callback
430 if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) {
431 config.callbacks[ key ] = [];
434 QUnit[ key ] = registerLoggingCallback( key );
438 // `onErrorFnPrev` initialized at top of scope
439 // Preserve other handlers
440 onErrorFnPrev = window.onerror;
442 // Cover uncaught exceptions
443 // Returning true will suppress the default browser handler,
444 // returning false will let it run.
445 window.onerror = function( error, filePath, linerNr ) {
447 if ( onErrorFnPrev ) {
448 ret = onErrorFnPrev( error, filePath, linerNr );
451 // Treat return value as window.onerror itself does,
452 // Only do our handling if not suppressed.
453 if ( ret !== true ) {
454 if ( QUnit.config.current ) {
455 if ( QUnit.config.current.ignoreGlobalErrors ) {
458 QUnit.pushFailure( error, filePath + ":" + linerNr );
460 QUnit.test( "global failure", extend(function() {
461 QUnit.pushFailure( error, filePath + ":" + linerNr );
462 }, { validTest: true } ) );
473 config.autorun = true;
475 // Log the last module results
476 if ( config.previousModule ) {
477 runLoggingCallbacks( "moduleDone", {
478 name: config.previousModule.name,
479 tests: config.previousModule.tests,
480 failed: config.moduleStats.bad,
481 passed: config.moduleStats.all - config.moduleStats.bad,
482 total: config.moduleStats.all,
483 runtime: now() - config.moduleStats.started
486 delete config.previousModule;
488 runtime = now() - config.started;
489 passed = config.stats.all - config.stats.bad;
491 runLoggingCallbacks( "done", {
492 failed: config.stats.bad,
494 total: config.stats.all,
499 // Doesn't support IE6 to IE9, it will return undefined on these browsers
500 // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
501 function extractStacktrace( e, offset ) {
502 offset = offset === undefined ? 4 : offset;
504 var stack, include, i;
507 stack = e.stack.split( "\n" );
508 if ( /^error$/i.test( stack[ 0 ] ) ) {
513 for ( i = offset; i < stack.length; i++ ) {
514 if ( stack[ i ].indexOf( fileName ) !== -1 ) {
517 include.push( stack[ i ] );
519 if ( include.length ) {
520 return include.join( "\n" );
523 return stack[ offset ];
525 // Support: Safari <=6 only
526 } else if ( e.sourceURL ) {
528 // exclude useless self-reference for generated Error objects
529 if ( /qunit.js$/.test( e.sourceURL ) ) {
533 // for actual exceptions, this is useful
534 return e.sourceURL + ":" + e.line;
538 function sourceFromStacktrace( offset ) {
539 var error = new Error();
541 // Support: Safari <=7 only, IE <=10 - 11 only
542 // Not all browsers generate the `stack` property for `new Error()`, see also #636
543 if ( !error.stack ) {
551 return extractStacktrace( error, offset );
554 function synchronize( callback, last ) {
555 if ( QUnit.objectType( callback ) === "array" ) {
556 while ( callback.length ) {
557 synchronize( callback.shift() );
561 config.queue.push( callback );
563 if ( config.autorun && !config.blocking ) {
568 function process( last ) {
573 config.depth = ( config.depth || 0 ) + 1;
575 while ( config.queue.length && !config.blocking ) {
576 if ( !defined.setTimeout || config.updateRate <= 0 ||
577 ( ( now() - start ) < config.updateRate ) ) {
578 if ( config.current ) {
580 // Reset async tracking for each phase of the Test lifecycle
581 config.current.usedAsync = false;
583 config.queue.shift()();
585 setTimeout( next, 13 );
590 if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
599 // If the test run hasn't officially begun yet
600 if ( !config.started ) {
602 // Record the time of the test run's beginning
603 config.started = now();
605 verifyLoggingCallbacks();
607 // Delete the loose unnamed module if unused.
608 if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
609 config.modules.shift();
612 // Avoid unnecessary information by not logging modules' test environments
613 for ( i = 0, l = config.modules.length; i < l; i++ ) {
615 name: config.modules[ i ].name,
616 tests: config.modules[ i ].tests
620 // The test run is officially beginning now
621 runLoggingCallbacks( "begin", {
622 totalTests: Test.count,
627 config.blocking = false;
631 function resumeProcessing() {
634 // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
635 if ( defined.setTimeout ) {
636 setTimeout(function() {
637 if ( config.current && config.current.semaphore > 0 ) {
640 if ( config.timeout ) {
641 clearTimeout( config.timeout );
651 function pauseProcessing() {
652 config.blocking = true;
654 if ( config.testTimeout && defined.setTimeout ) {
655 clearTimeout( config.timeout );
656 config.timeout = setTimeout(function() {
657 if ( config.current ) {
658 config.current.semaphore = 0;
659 QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
661 throw new Error( "Test timed out" );
664 }, config.testTimeout );
668 function saveGlobal() {
669 config.pollution = [];
671 if ( config.noglobals ) {
672 for ( var key in window ) {
673 if ( hasOwn.call( window, key ) ) {
674 // in Opera sometimes DOM element ids show up here, ignore them
675 if ( /^qunit-test-output/.test( key ) ) {
678 config.pollution.push( key );
684 function checkPollution() {
687 old = config.pollution;
691 newGlobals = diff( config.pollution, old );
692 if ( newGlobals.length > 0 ) {
693 QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
696 deletedGlobals = diff( old, config.pollution );
697 if ( deletedGlobals.length > 0 ) {
698 QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
702 // returns a new Array with the elements that are in a but not in b
703 function diff( a, b ) {
707 for ( i = 0; i < result.length; i++ ) {
708 for ( j = 0; j < b.length; j++ ) {
709 if ( result[ i ] === b[ j ] ) {
710 result.splice( i, 1 );
719 function extend( a, b, undefOnly ) {
720 for ( var prop in b ) {
721 if ( hasOwn.call( b, prop ) ) {
723 // Avoid "Member not found" error in IE8 caused by messing with window.constructor
724 if ( !( prop === "constructor" && a === window ) ) {
725 if ( b[ prop ] === undefined ) {
727 } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
728 a[ prop ] = b[ prop ];
737 function runLoggingCallbacks( key, args ) {
740 callbacks = config.callbacks[ key ];
741 for ( i = 0, l = callbacks.length; i < l; i++ ) {
742 callbacks[ i ]( args );
746 // DEPRECATED: This will be removed on 2.0.0+
747 // This function verifies if the loggingCallbacks were modified by the user
748 // If so, it will restore it, assign the given callback and print a console warning
749 function verifyLoggingCallbacks() {
750 var loggingCallback, userCallback;
752 for ( loggingCallback in loggingCallbacks ) {
753 if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
755 userCallback = QUnit[ loggingCallback ];
757 // Restore the callback function
758 QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
760 // Assign the deprecated given callback
761 QUnit[ loggingCallback ]( userCallback );
763 if ( window.console && window.console.warn ) {
765 "QUnit." + loggingCallback + " was replaced with a new value.\n" +
766 "Please, check out the documentation on how to apply logging callbacks.\n" +
767 "Reference: http://api.qunitjs.com/category/callbacks/"
775 function inArray( elem, array ) {
776 if ( array.indexOf ) {
777 return array.indexOf( elem );
780 for ( var i = 0, length = array.length; i < length; i++ ) {
781 if ( array[ i ] === elem ) {
789 function Test( settings ) {
794 extend( this, settings );
795 this.assertions = [];
797 this.usedAsync = false;
798 this.module = config.currentModule;
799 this.stack = sourceFromStacktrace( 3 );
801 // Register unique strings
802 for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
803 if ( this.module.tests[ i ].name === this.testName ) {
804 this.testName += " ";
808 this.testId = generateHash( this.module.name, this.testName );
810 this.module.tests.push({
815 if ( settings.skip ) {
817 // Skipped tests will fully ignore any sent callback
818 this.callback = function() {};
822 this.assert = new Assert( this );
832 // Emit moduleStart when we're switching from one module to another
833 this.module !== config.previousModule ||
835 // They could be equal (both undefined) but if the previousModule property doesn't
836 // yet exist it means this is the first test in a suite that isn't wrapped in a
837 // module, in which case we'll just emit a moduleStart event for 'undefined'.
838 // Without this, reporters can get testStart before moduleStart which is a problem.
839 !hasOwn.call( config, "previousModule" )
841 if ( hasOwn.call( config, "previousModule" ) ) {
842 runLoggingCallbacks( "moduleDone", {
843 name: config.previousModule.name,
844 tests: config.previousModule.tests,
845 failed: config.moduleStats.bad,
846 passed: config.moduleStats.all - config.moduleStats.bad,
847 total: config.moduleStats.all,
848 runtime: now() - config.moduleStats.started
851 config.previousModule = this.module;
852 config.moduleStats = { all: 0, bad: 0, started: now() };
853 runLoggingCallbacks( "moduleStart", {
854 name: this.module.name,
855 tests: this.module.tests
859 config.current = this;
861 this.testEnvironment = extend( {}, this.module.testEnvironment );
862 delete this.testEnvironment.beforeEach;
863 delete this.testEnvironment.afterEach;
865 this.started = now();
866 runLoggingCallbacks( "testStart", {
868 module: this.module.name,
872 if ( !config.pollution ) {
880 config.current = this;
886 this.callbackStarted = now();
888 if ( config.notrycatch ) {
889 promise = this.callback.call( this.testEnvironment, this.assert );
890 this.resolvePromise( promise );
895 promise = this.callback.call( this.testEnvironment, this.assert );
896 this.resolvePromise( promise );
898 this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
899 this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
901 // else next test will carry the responsibility
904 // Restart the tests if they're blocking
905 if ( config.blocking ) {
915 queueHook: function( hook, hookName ) {
918 return function runHook() {
919 config.current = test;
920 if ( config.notrycatch ) {
921 promise = hook.call( test.testEnvironment, test.assert );
922 test.resolvePromise( promise, hookName );
926 promise = hook.call( test.testEnvironment, test.assert );
927 test.resolvePromise( promise, hookName );
929 test.pushFailure( hookName + " failed on " + test.testName + ": " +
930 ( error.message || error ), extractStacktrace( error, 0 ) );
935 // Currently only used for module level hooks, can be used to add global level ones
936 hooks: function( handler ) {
939 // Hooks are ignored on skipped tests
944 if ( this.module.testEnvironment &&
945 QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) {
946 hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) );
953 config.current = this;
954 if ( config.requireExpects && this.expected === null ) {
955 this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
956 "not called.", this.stack );
957 } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
958 this.pushFailure( "Expected " + this.expected + " assertions, but " +
959 this.assertions.length + " were run", this.stack );
960 } else if ( this.expected === null && !this.assertions.length ) {
961 this.pushFailure( "Expected at least one assertion, but none were run - call " +
962 "expect(0) to accept zero assertions.", this.stack );
968 this.runtime = now() - this.started;
969 config.stats.all += this.assertions.length;
970 config.moduleStats.all += this.assertions.length;
972 for ( i = 0; i < this.assertions.length; i++ ) {
973 if ( !this.assertions[ i ].result ) {
976 config.moduleStats.bad++;
980 runLoggingCallbacks( "testDone", {
982 module: this.module.name,
983 skipped: !!this.skip,
985 passed: this.assertions.length - bad,
986 total: this.assertions.length,
987 runtime: this.runtime,
990 assertions: this.assertions,
993 // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
994 duration: this.runtime
997 // QUnit.reset() is deprecated and will be replaced for a new
998 // fixture reset function on QUnit 2.0/2.1.
999 // It's still called here for backwards compatibility handling
1002 config.current = undefined;
1009 if ( !this.valid() ) {
1015 // each of these can by async
1021 test.hooks( "beforeEach" ),
1027 test.hooks( "afterEach" ).reverse(),
1038 // `bad` initialized at top of scope
1039 // defer when previous test run passed, if storage is available
1040 bad = QUnit.config.reorder && defined.sessionStorage &&
1041 +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
1046 synchronize( run, true );
1050 push: function( result, actual, expected, message ) {
1053 module: this.module.name,
1054 name: this.testName,
1059 testId: this.testId,
1060 runtime: now() - this.started
1064 source = sourceFromStacktrace();
1067 details.source = source;
1071 runLoggingCallbacks( "log", details );
1073 this.assertions.push({
1079 pushFailure: function( message, source, actual ) {
1080 if ( !this instanceof Test ) {
1081 throw new Error( "pushFailure() assertion outside test context, was " +
1082 sourceFromStacktrace( 2 ) );
1086 module: this.module.name,
1087 name: this.testName,
1089 message: message || "error",
1090 actual: actual || null,
1091 testId: this.testId,
1092 runtime: now() - this.started
1096 details.source = source;
1099 runLoggingCallbacks( "log", details );
1101 this.assertions.push({
1107 resolvePromise: function( promise, phase ) {
1110 if ( promise != null ) {
1111 then = promise.then;
1112 if ( QUnit.objectType( then ) === "function" ) {
1118 message = "Promise rejected " +
1119 ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
1120 " " + test.testName + ": " + ( error.message || error );
1121 test.pushFailure( message, extractStacktrace( error, 0 ) );
1123 // else next test will carry the responsibility
1136 filter = config.filter && config.filter.toLowerCase(),
1137 module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
1138 fullName = ( this.module.name + ": " + this.testName ).toLowerCase();
1140 // Internally-generated tests are always valid
1141 if ( this.callback && this.callback.validTest ) {
1145 if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
1149 if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) {
1157 include = filter.charAt( 0 ) !== "!";
1159 filter = filter.slice( 1 );
1162 // If the filter matches, we need to honour include
1163 if ( fullName.indexOf( filter ) !== -1 ) {
1167 // Otherwise, do the opposite
1173 // Resets the test setup. Useful for tests that modify the DOM.
1175 DEPRECATED: Use multiple tests instead of resetting inside a test.
1176 Use testStart or testDone for custom cleanup.
1177 This method will throw an error in 2.0, and will be removed in 2.1
1179 QUnit.reset = function() {
1181 // Return on non-browser environments
1182 // This is necessary to not break on node tests
1183 if ( typeof window === "undefined" ) {
1187 var fixture = defined.document && document.getElementById &&
1188 document.getElementById( "qunit-fixture" );
1191 fixture.innerHTML = config.fixture;
1195 QUnit.pushFailure = function() {
1196 if ( !QUnit.config.current ) {
1197 throw new Error( "pushFailure() assertion outside test context, in " +
1198 sourceFromStacktrace( 2 ) );
1201 // Gets current test obj
1202 var currentTest = QUnit.config.current;
1204 return currentTest.pushFailure.apply( currentTest, arguments );
1207 // Based on Java's String.hashCode, a simple but not
1208 // rigorously collision resistant hashing function
1209 function generateHash( module, testName ) {
1213 str = module + "\x1C" + testName,
1216 for ( ; i < len; i++ ) {
1217 hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
1221 // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
1222 // strictly necessary but increases user understanding that the id is a SHA-like hash
1223 hex = ( 0x100000000 + hash ).toString( 16 );
1224 if ( hex.length < 8 ) {
1225 hex = "0000000" + hex;
1228 return hex.slice( -8 );
1231 function Assert( testContext ) {
1232 this.test = testContext;
1236 QUnit.assert = Assert.prototype = {
1238 // Specify the number of expected assertions to guarantee that failed test
1239 // (no assertions are run at all) don't slip through.
1240 expect: function( asserts ) {
1241 if ( arguments.length === 1 ) {
1242 this.test.expected = asserts;
1244 return this.test.expected;
1248 // Increment this Test's semaphore counter, then return a single-use function that
1249 // decrements that counter a maximum of once.
1251 var test = this.test,
1254 test.semaphore += 1;
1255 test.usedAsync = true;
1258 return function done() {
1260 test.semaphore -= 1;
1264 test.pushFailure( "Called the callback returned from `assert.async` more than once",
1265 sourceFromStacktrace( 2 ) );
1270 // Exports test.push() to the user API
1271 push: function( /* result, actual, expected, message */ ) {
1273 currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
1275 // Backwards compatibility fix.
1276 // Allows the direct use of global exported assertions and QUnit.assert.*
1277 // Although, it's use is not recommended as it can leak assertions
1278 // to other tests from async tests, because we only get a reference to the current test,
1279 // not exactly the test where assertion were intended to be called.
1280 if ( !currentTest ) {
1281 throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
1284 if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
1285 currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
1286 sourceFromStacktrace( 2 ) );
1288 // Allow this assertion to continue running anyway...
1291 if ( !( assert instanceof Assert ) ) {
1292 assert = currentTest.assert;
1294 return assert.test.push.apply( assert.test, arguments );
1297 ok: function( result, message ) {
1298 message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
1299 QUnit.dump.parse( result ) );
1300 this.push( !!result, result, true, message );
1303 notOk: function( result, message ) {
1304 message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
1305 QUnit.dump.parse( result ) );
1306 this.push( !result, result, false, message );
1309 equal: function( actual, expected, message ) {
1310 /*jshint eqeqeq:false */
1311 this.push( expected == actual, actual, expected, message );
1314 notEqual: function( actual, expected, message ) {
1315 /*jshint eqeqeq:false */
1316 this.push( expected != actual, actual, expected, message );
1319 propEqual: function( actual, expected, message ) {
1320 actual = objectValues( actual );
1321 expected = objectValues( expected );
1322 this.push( QUnit.equiv( actual, expected ), actual, expected, message );
1325 notPropEqual: function( actual, expected, message ) {
1326 actual = objectValues( actual );
1327 expected = objectValues( expected );
1328 this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
1331 deepEqual: function( actual, expected, message ) {
1332 this.push( QUnit.equiv( actual, expected ), actual, expected, message );
1335 notDeepEqual: function( actual, expected, message ) {
1336 this.push( !QUnit.equiv( actual, expected ), actual, expected, message );
1339 strictEqual: function( actual, expected, message ) {
1340 this.push( expected === actual, actual, expected, message );
1343 notStrictEqual: function( actual, expected, message ) {
1344 this.push( expected !== actual, actual, expected, message );
1347 "throws": function( block, expected, message ) {
1348 var actual, expectedType,
1349 expectedOutput = expected,
1351 currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
1353 // 'expected' is optional unless doing string comparison
1354 if ( message == null && typeof expected === "string" ) {
1359 currentTest.ignoreGlobalErrors = true;
1361 block.call( currentTest.testEnvironment );
1365 currentTest.ignoreGlobalErrors = false;
1368 expectedType = QUnit.objectType( expected );
1370 // we don't want to validate thrown error
1373 expectedOutput = null;
1375 // expected is a regexp
1376 } else if ( expectedType === "regexp" ) {
1377 ok = expected.test( errorString( actual ) );
1379 // expected is a string
1380 } else if ( expectedType === "string" ) {
1381 ok = expected === errorString( actual );
1383 // expected is a constructor, maybe an Error constructor
1384 } else if ( expectedType === "function" && actual instanceof expected ) {
1387 // expected is an Error object
1388 } else if ( expectedType === "object" ) {
1389 ok = actual instanceof expected.constructor &&
1390 actual.name === expected.name &&
1391 actual.message === expected.message;
1393 // expected is a validation function which returns true if validation passed
1394 } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
1395 expectedOutput = null;
1400 currentTest.assert.push( ok, actual, expectedOutput, message );
1404 // Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word
1405 // Known to us are: Closure Compiler, Narwhal
1407 /*jshint sub:true */
1408 Assert.prototype.raises = Assert.prototype[ "throws" ];
1411 // Test for equality any JavaScript type.
1412 // Author: Philippe Rathé <prathe@gmail.com>
1413 QUnit.equiv = (function() {
1415 // Call the o related callback with the given arguments.
1416 function bindCallbacks( o, callbacks, args ) {
1417 var prop = QUnit.objectType( o );
1419 if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
1420 return callbacks[ prop ].apply( callbacks, args );
1422 return callbacks[ prop ]; // or undefined
1427 // the real equiv function
1430 // stack to decide between skip/abort functions
1433 // stack to avoiding loops from circular referencing
1437 getProto = Object.getPrototypeOf || function( obj ) {
1438 /* jshint camelcase: false, proto: true */
1439 return obj.__proto__;
1441 callbacks = (function() {
1443 // for string, boolean, number and null
1444 function useStrictEquality( b, a ) {
1446 /*jshint eqeqeq:false */
1447 if ( b instanceof a.constructor || a instanceof b.constructor ) {
1449 // to catch short annotation VS 'new' annotation of a
1452 // var j = new Number(1);
1460 "string": useStrictEquality,
1461 "boolean": useStrictEquality,
1462 "number": useStrictEquality,
1463 "null": useStrictEquality,
1464 "undefined": useStrictEquality,
1466 "nan": function( b ) {
1470 "date": function( b, a ) {
1471 return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
1474 "regexp": function( b, a ) {
1475 return QUnit.objectType( b ) === "regexp" &&
1478 a.source === b.source &&
1480 // and its modifiers
1481 a.global === b.global &&
1484 a.ignoreCase === b.ignoreCase &&
1485 a.multiline === b.multiline &&
1486 a.sticky === b.sticky;
1489 // - skip when the property is a method of an instance (OOP)
1490 // - abort otherwise,
1491 // initial === would have catch identical references anyway
1492 "function": function() {
1493 var caller = callers[ callers.length - 1 ];
1494 return caller !== Object && typeof caller !== "undefined";
1497 "array": function( b, a ) {
1498 var i, j, len, loop, aCircular, bCircular;
1500 // b could be an object literal here
1501 if ( QUnit.objectType( b ) !== "array" ) {
1506 if ( len !== b.length ) {
1511 // track reference to avoid circular references
1514 for ( i = 0; i < len; i++ ) {
1516 for ( j = 0; j < parents.length; j++ ) {
1517 aCircular = parents[ j ] === a[ i ];
1518 bCircular = parentsB[ j ] === b[ i ];
1519 if ( aCircular || bCircular ) {
1520 if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1529 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1540 "object": function( b, a ) {
1542 /*jshint forin:false */
1543 var i, j, loop, aCircular, bCircular,
1549 // comparing constructors is more strict than using
1551 if ( a.constructor !== b.constructor ) {
1553 // Allow objects with no prototype to be equivalent to
1554 // objects with Object as their constructor.
1555 if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) ||
1556 ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) {
1561 // stack constructor before traversing properties
1562 callers.push( a.constructor );
1564 // track reference to avoid circular references
1568 // be strict: don't ensure hasOwnProperty and go deep
1571 for ( j = 0; j < parents.length; j++ ) {
1572 aCircular = parents[ j ] === a[ i ];
1573 bCircular = parentsB[ j ] === b[ i ];
1574 if ( aCircular || bCircular ) {
1575 if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1583 aProperties.push( i );
1584 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1592 callers.pop(); // unstack, we are done
1595 bProperties.push( i ); // collect b's properties
1598 // Ensures identical properties name
1599 return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1604 innerEquiv = function() { // can take multiple arguments
1605 var args = [].slice.apply( arguments );
1606 if ( args.length < 2 ) {
1607 return true; // end transition
1610 return ( (function( a, b ) {
1612 return true; // catch the most you can
1613 } else if ( a === null || b === null || typeof a === "undefined" ||
1614 typeof b === "undefined" ||
1615 QUnit.objectType( a ) !== QUnit.objectType( b ) ) {
1617 // don't lose time with error prone cases
1620 return bindCallbacks( a, callbacks, [ b, a ] );
1623 // apply transition with (1..n) arguments
1624 }( args[ 0 ], args[ 1 ] ) ) &&
1625 innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) );
1631 // Based on jsDump by Ariel Flesler
1632 // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
1633 QUnit.dump = (function() {
1634 function quote( str ) {
1635 return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
1637 function literal( o ) {
1640 function join( pre, arr, post ) {
1641 var s = dump.separator(),
1642 base = dump.indent(),
1643 inner = dump.indent( 1 );
1645 arr = arr.join( "," + s + inner );
1650 return [ pre, inner + arr, base + post ].join( s );
1652 function array( arr, stack ) {
1654 ret = new Array( i );
1656 if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
1657 return "[object Array]";
1662 ret[ i ] = this.parse( arr[ i ], undefined, stack );
1665 return join( "[", ret, "]" );
1668 var reName = /^function (\w+)/,
1671 // objType is used mostly internally, you can fix a (custom) type in advance
1672 parse: function( obj, objType, stack ) {
1673 stack = stack || [];
1674 var res, parser, parserType,
1675 inStack = inArray( obj, stack );
1677 if ( inStack !== -1 ) {
1678 return "recursion(" + ( inStack - stack.length ) + ")";
1681 objType = objType || this.typeOf( obj );
1682 parser = this.parsers[ objType ];
1683 parserType = typeof parser;
1685 if ( parserType === "function" ) {
1687 res = parser.call( this, obj, stack );
1691 return ( parserType === "string" ) ? parser : this.parsers.error;
1693 typeOf: function( obj ) {
1695 if ( obj === null ) {
1697 } else if ( typeof obj === "undefined" ) {
1699 } else if ( QUnit.is( "regexp", obj ) ) {
1701 } else if ( QUnit.is( "date", obj ) ) {
1703 } else if ( QUnit.is( "function", obj ) ) {
1705 } else if ( obj.setInterval !== undefined &&
1706 obj.document !== undefined &&
1707 obj.nodeType === undefined ) {
1709 } else if ( obj.nodeType === 9 ) {
1711 } else if ( obj.nodeType ) {
1716 toString.call( obj ) === "[object Array]" ||
1719 ( typeof obj.length === "number" && obj.item !== undefined &&
1720 ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
1721 obj[ 0 ] === undefined ) ) )
1724 } else if ( obj.constructor === Error.prototype.constructor ) {
1731 separator: function() {
1732 return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " ";
1734 // extra can be a number, shortcut for increasing-calling-decreasing
1735 indent: function( extra ) {
1736 if ( !this.multiline ) {
1739 var chr = this.indentChar;
1741 chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
1743 return new Array( this.depth + ( extra || 0 ) ).join( chr );
1746 this.depth += a || 1;
1748 down: function( a ) {
1749 this.depth -= a || 1;
1751 setParser: function( name, parser ) {
1752 this.parsers[ name ] = parser;
1754 // The next 3 are exposed so you can use them
1760 maxDepth: QUnit.config.maxDepth,
1762 // This is the list of parsers, to modify them, use dump.setParser
1765 document: "[Document]",
1766 error: function( error ) {
1767 return "Error(\"" + error.message + "\")";
1769 unknown: "[Unknown]",
1771 "undefined": "undefined",
1772 "function": function( fn ) {
1773 var ret = "function",
1775 // functions never have name in IE
1776 name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
1783 ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
1784 return join( ret, dump.parse( fn, "functionCode" ), "}" );
1789 object: function( map, stack ) {
1790 var keys, key, val, i, nonEnumerableProperties,
1793 if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
1794 return "[object Object]";
1799 for ( key in map ) {
1803 // Some properties are not always enumerable on Error objects.
1804 nonEnumerableProperties = [ "message", "name" ];
1805 for ( i in nonEnumerableProperties ) {
1806 key = nonEnumerableProperties[ i ];
1807 if ( key in map && inArray( key, keys ) < 0 ) {
1812 for ( i = 0; i < keys.length; i++ ) {
1815 ret.push( dump.parse( key, "key" ) + ": " +
1816 dump.parse( val, undefined, stack ) );
1819 return join( "{", ret, "}" );
1821 node: function( node ) {
1823 open = dump.HTML ? "<" : "<",
1824 close = dump.HTML ? ">" : ">",
1825 tag = node.nodeName.toLowerCase(),
1827 attrs = node.attributes;
1830 for ( i = 0, len = attrs.length; i < len; i++ ) {
1831 val = attrs[ i ].nodeValue;
1833 // IE6 includes all attributes in .attributes, even ones not explicitly
1834 // set. Those have values like undefined, null, 0, false, "" or
1836 if ( val && val !== "inherit" ) {
1837 ret += " " + attrs[ i ].nodeName + "=" +
1838 dump.parse( val, "attribute" );
1844 // Show content of TextNode or CDATASection
1845 if ( node.nodeType === 3 || node.nodeType === 4 ) {
1846 ret += node.nodeValue;
1849 return ret + open + "/" + tag + close;
1852 // function calls it internally, it's the arguments part of the function
1853 functionArgs: function( fn ) {
1861 args = new Array( l );
1865 args[ l ] = String.fromCharCode( 97 + l );
1867 return " " + args.join( ", " ) + " ";
1869 // object calls it internally, the key part of an item in a map
1871 // function calls it internally, it's the content of the function
1872 functionCode: "[code]",
1873 // node calls it internally, it's an html attribute value
1881 // if true, entities are escaped ( <, >, \t, space and \n )
1885 // if true, items in a collection, are separated by a \n, else just a space.
1893 QUnit.jsDump = QUnit.dump;
1895 // For browser, export only select globals
1896 if ( typeof window !== "undefined" ) {
1899 // Extend assert methods to QUnit and Global scope through Backwards compatibility
1902 assertions = Assert.prototype;
1904 function applyCurrent( current ) {
1906 var assert = new Assert( QUnit.config.current );
1907 current.apply( assert, arguments );
1911 for ( i in assertions ) {
1912 QUnit[ i ] = applyCurrent( assertions[ i ] );
1938 for ( i = 0, l = keys.length; i < l; i++ ) {
1939 window[ keys[ i ] ] = QUnit[ keys[ i ] ];
1943 window.QUnit = QUnit;
1947 if ( typeof module !== "undefined" && module && module.exports ) {
1948 module.exports = QUnit;
1950 // For consistency with CommonJS environments' exports
1951 module.exports.QUnit = QUnit;
1954 // For CommonJS with exports, but without module.exports, like Rhino
1955 if ( typeof exports !== "undefined" && exports ) {
1956 exports.QUnit = QUnit;
1959 if ( typeof define === "function" && define.amd ) {
1960 define( function() {
1963 QUnit.config.autostart = false;
1966 // Get a reference to the global object, like window in browsers
1971 /*istanbul ignore next */
1972 // jscs:disable maximumLineLength
1974 * This file is a modified version of google-diff-match-patch's JavaScript implementation
1975 * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
1976 * modifications are licensed as more fully set forth in LICENSE.txt.
1978 * The original source of google-diff-match-patch is attributable and licensed as follows:
1980 * Copyright 2006 Google Inc.
1981 * http://code.google.com/p/google-diff-match-patch/
1983 * Licensed under the Apache License, Version 2.0 (the "License");
1984 * you may not use this file except in compliance with the License.
1985 * You may obtain a copy of the License at
1987 * http://www.apache.org/licenses/LICENSE-2.0
1989 * Unless required by applicable law or agreed to in writing, software
1990 * distributed under the License is distributed on an "AS IS" BASIS,
1991 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1992 * See the License for the specific language governing permissions and
1993 * limitations under the License.
1996 * https://code.google.com/p/google-diff-match-patch/
1998 * Usage: QUnit.diff(expected, actual)
2000 * 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"
2002 QUnit.diff = (function() {
2004 function DiffMatchPatch() {
2007 // Redefine these in your program to override the defaults.
2009 // Number of seconds to map a diff before giving up (0 for infinity).
2010 this.DiffTimeout = 1.0;
2011 // Cost of an empty edit operation in terms of edit characters.
2012 this.DiffEditCost = 4;
2018 * The data structure representing a diff is an array of tuples:
2019 * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
2020 * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
2022 var DIFF_DELETE = -1,
2027 * Find the differences between two texts. Simplifies the problem by stripping
2028 * any common prefix or suffix off the texts before diffing.
2029 * @param {string} text1 Old string to be diffed.
2030 * @param {string} text2 New string to be diffed.
2031 * @param {boolean=} optChecklines Optional speedup flag. If present and false,
2032 * then don't run a line-level diff first to identify the changed areas.
2033 * Defaults to true, which does a faster, slightly less optimal diff.
2034 * @param {number} optDeadline Optional time when the diff should be complete
2035 * by. Used internally for recursive calls. Users should set DiffTimeout
2037 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
2039 DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) {
2040 var deadline, checklines, commonlength,
2041 commonprefix, commonsuffix, diffs;
2042 // Set a deadline by which time the diff must be complete.
2043 if ( typeof optDeadline === "undefined" ) {
2044 if ( this.DiffTimeout <= 0 ) {
2045 optDeadline = Number.MAX_VALUE;
2047 optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000;
2050 deadline = optDeadline;
2052 // Check for null inputs.
2053 if ( text1 === null || text2 === null ) {
2054 throw new Error( "Null input. (DiffMain)" );
2057 // Check for equality (speedup).
2058 if ( text1 === text2 ) {
2061 [ DIFF_EQUAL, text1 ]
2067 if ( typeof optChecklines === "undefined" ) {
2068 optChecklines = true;
2071 checklines = optChecklines;
2073 // Trim off common prefix (speedup).
2074 commonlength = this.diffCommonPrefix( text1, text2 );
2075 commonprefix = text1.substring( 0, commonlength );
2076 text1 = text1.substring( commonlength );
2077 text2 = text2.substring( commonlength );
2079 // Trim off common suffix (speedup).
2081 commonlength = this.diffCommonSuffix( text1, text2 );
2082 commonsuffix = text1.substring( text1.length - commonlength );
2083 text1 = text1.substring( 0, text1.length - commonlength );
2084 text2 = text2.substring( 0, text2.length - commonlength );
2086 // Compute the diff on the middle block.
2087 diffs = this.diffCompute( text1, text2, checklines, deadline );
2089 // Restore the prefix and suffix.
2090 if ( commonprefix ) {
2091 diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
2093 if ( commonsuffix ) {
2094 diffs.push( [ DIFF_EQUAL, commonsuffix ] );
2096 this.diffCleanupMerge( diffs );
2101 * Reduce the number of edits by eliminating operationally trivial equalities.
2102 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
2104 DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
2105 var changes, equalities, equalitiesLength, lastequality,
2106 pointer, preIns, preDel, postIns, postDel;
2108 equalities = []; // Stack of indices where equalities are found.
2109 equalitiesLength = 0; // Keeping our own length var is faster in JS.
2110 /** @type {?string} */
2111 lastequality = null;
2112 // Always equal to diffs[equalities[equalitiesLength - 1]][1]
2113 pointer = 0; // Index of current position.
2114 // Is there an insertion operation before the last equality.
2116 // Is there a deletion operation before the last equality.
2118 // Is there an insertion operation after the last equality.
2120 // Is there a deletion operation after the last equality.
2122 while ( pointer < diffs.length ) {
2123 if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
2124 if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) {
2126 equalities[ equalitiesLength++ ] = pointer;
2129 lastequality = diffs[ pointer ][ 1 ];
2131 // Not a candidate, and can never become one.
2132 equalitiesLength = 0;
2133 lastequality = null;
2135 postIns = postDel = false;
2136 } else { // An insertion or deletion.
2137 if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
2143 * Five types to be split:
2144 * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
2145 * <ins>A</ins>X<ins>C</ins><del>D</del>
2146 * <ins>A</ins><del>B</del>X<ins>C</ins>
2147 * <ins>A</del>X<ins>C</ins><del>D</del>
2148 * <ins>A</ins><del>B</del>X<del>C</del>
2150 if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
2151 ( ( lastequality.length < this.DiffEditCost / 2 ) &&
2152 ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
2153 // Duplicate record.
2154 diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] );
2155 // Change second copy to insert.
2156 diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
2157 equalitiesLength--; // Throw away the equality we just deleted;
2158 lastequality = null;
2159 if (preIns && preDel) {
2160 // No changes made which could affect previous entry, keep going.
2161 postIns = postDel = true;
2162 equalitiesLength = 0;
2164 equalitiesLength--; // Throw away the previous equality.
2165 pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
2166 postIns = postDel = false;
2175 this.diffCleanupMerge( diffs );
2180 * Convert a diff array into a pretty HTML report.
2181 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
2182 * @param {integer} string to be beautified.
2183 * @return {string} HTML representation.
2185 DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
2186 var op, data, x, html = [];
2187 for ( x = 0; x < diffs.length; x++ ) {
2188 op = diffs[x][0]; // Operation (insert, delete, equal)
2189 data = diffs[x][1]; // Text of change.
2192 html[x] = "<ins>" + data + "</ins>";
2195 html[x] = "<del>" + data + "</del>";
2198 html[x] = "<span>" + data + "</span>";
2202 return html.join("");
2206 * Determine the common prefix of two strings.
2207 * @param {string} text1 First string.
2208 * @param {string} text2 Second string.
2209 * @return {number} The number of characters common to the start of each
2212 DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
2213 var pointermid, pointermax, pointermin, pointerstart;
2214 // Quick check for common null cases.
2215 if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) {
2219 // Performance analysis: http://neil.fraser.name/news/2007/10/09/
2221 pointermax = Math.min( text1.length, text2.length );
2222 pointermid = pointermax;
2224 while ( pointermin < pointermid ) {
2225 if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) {
2226 pointermin = pointermid;
2227 pointerstart = pointermin;
2229 pointermax = pointermid;
2231 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
2237 * Determine the common suffix of two strings.
2238 * @param {string} text1 First string.
2239 * @param {string} text2 Second string.
2240 * @return {number} The number of characters common to the end of each string.
2242 DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
2243 var pointermid, pointermax, pointermin, pointerend;
2244 // Quick check for common null cases.
2245 if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) {
2249 // Performance analysis: http://neil.fraser.name/news/2007/10/09/
2251 pointermax = Math.min(text1.length, text2.length);
2252 pointermid = pointermax;
2254 while ( pointermin < pointermid ) {
2255 if (text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
2256 text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
2257 pointermin = pointermid;
2258 pointerend = pointermin;
2260 pointermax = pointermid;
2262 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
2268 * Find the differences between two texts. Assumes that the texts do not
2269 * have any common prefix or suffix.
2270 * @param {string} text1 Old string to be diffed.
2271 * @param {string} text2 New string to be diffed.
2272 * @param {boolean} checklines Speedup flag. If false, then don't run a
2273 * line-level diff first to identify the changed areas.
2274 * If true, then run a faster, slightly less optimal diff.
2275 * @param {number} deadline Time when the diff should be complete by.
2276 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
2279 DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
2280 var diffs, longtext, shorttext, i, hm,
2281 text1A, text2A, text1B, text2B,
2282 midCommon, diffsA, diffsB;
2285 // Just add some text (speedup).
2287 [ DIFF_INSERT, text2 ]
2292 // Just delete some text (speedup).
2294 [ DIFF_DELETE, text1 ]
2298 longtext = text1.length > text2.length ? text1 : text2;
2299 shorttext = text1.length > text2.length ? text2 : text1;
2300 i = longtext.indexOf( shorttext );
2302 // Shorter text is inside the longer text (speedup).
2304 [ DIFF_INSERT, longtext.substring( 0, i ) ],
2305 [ DIFF_EQUAL, shorttext ],
2306 [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
2308 // Swap insertions for deletions if diff is reversed.
2309 if ( text1.length > text2.length ) {
2310 diffs[0][0] = diffs[2][0] = DIFF_DELETE;
2315 if ( shorttext.length === 1 ) {
2316 // Single character string.
2317 // After the previous speedup, the character can't be an equality.
2319 [ DIFF_DELETE, text1 ],
2320 [ DIFF_INSERT, text2 ]
2324 // Check to see if the problem can be split in two.
2325 hm = this.diffHalfMatch(text1, text2);
2327 // A half-match was found, sort out the return data.
2333 // Send both pairs off for separate processing.
2334 diffsA = this.DiffMain(text1A, text2A, checklines, deadline);
2335 diffsB = this.DiffMain(text1B, text2B, checklines, deadline);
2336 // Merge the results.
2337 return diffsA.concat([
2338 [ DIFF_EQUAL, midCommon ]
2342 if (checklines && text1.length > 100 && text2.length > 100) {
2343 return this.diffLineMode(text1, text2, deadline);
2346 return this.diffBisect(text1, text2, deadline);
2350 * Do the two texts share a substring which is at least half the length of the
2352 * This speedup can produce non-minimal diffs.
2353 * @param {string} text1 First string.
2354 * @param {string} text2 Second string.
2355 * @return {Array.<string>} Five element Array, containing the prefix of
2356 * text1, the suffix of text1, the prefix of text2, the suffix of
2357 * text2 and the common middle. Or null if there was no match.
2360 DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) {
2361 var longtext, shorttext, dmp,
2362 text1A, text2B, text2A, text1B, midCommon,
2364 if (this.DiffTimeout <= 0) {
2365 // Don't risk returning a non-optimal diff if we have unlimited time.
2368 longtext = text1.length > text2.length ? text1 : text2;
2369 shorttext = text1.length > text2.length ? text2 : text1;
2370 if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
2371 return null; // Pointless.
2373 dmp = this; // 'this' becomes 'window' in a closure.
2376 * Does a substring of shorttext exist within longtext such that the substring
2377 * is at least half the length of longtext?
2378 * Closure, but does not reference any external variables.
2379 * @param {string} longtext Longer string.
2380 * @param {string} shorttext Shorter string.
2381 * @param {number} i Start index of quarter length substring within longtext.
2382 * @return {Array.<string>} Five element Array, containing the prefix of
2383 * longtext, the suffix of longtext, the prefix of shorttext, the suffix
2384 * of shorttext and the common middle. Or null if there was no match.
2387 function diffHalfMatchI(longtext, shorttext, i) {
2388 var seed, j, bestCommon, prefixLength, suffixLength,
2389 bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
2390 // Start with a 1/4 length substring at position i as a seed.
2391 seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
2394 while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
2395 prefixLength = dmp.diffCommonPrefix(longtext.substring(i),
2396 shorttext.substring(j));
2397 suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i),
2398 shorttext.substring(0, j));
2399 if (bestCommon.length < suffixLength + prefixLength) {
2400 bestCommon = shorttext.substring(j - suffixLength, j) +
2401 shorttext.substring(j, j + prefixLength);
2402 bestLongtextA = longtext.substring(0, i - suffixLength);
2403 bestLongtextB = longtext.substring(i + prefixLength);
2404 bestShorttextA = shorttext.substring(0, j - suffixLength);
2405 bestShorttextB = shorttext.substring(j + prefixLength);
2408 if (bestCommon.length * 2 >= longtext.length) {
2409 return [ bestLongtextA, bestLongtextB,
2410 bestShorttextA, bestShorttextB, bestCommon
2417 // First check if the second quarter is the seed for a half-match.
2418 hm1 = diffHalfMatchI(longtext, shorttext,
2419 Math.ceil(longtext.length / 4));
2420 // Check again based on the third quarter.
2421 hm2 = diffHalfMatchI(longtext, shorttext,
2422 Math.ceil(longtext.length / 2));
2430 // Both matched. Select the longest.
2431 hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
2434 // A half-match was found, sort out the return data.
2435 text1A, text1B, text2A, text2B;
2436 if (text1.length > text2.length) {
2448 return [ text1A, text1B, text2A, text2B, midCommon ];
2452 * Do a quick line-level diff on both strings, then rediff the parts for
2454 * This speedup can produce non-minimal diffs.
2455 * @param {string} text1 Old string to be diffed.
2456 * @param {string} text2 New string to be diffed.
2457 * @param {number} deadline Time when the diff should be complete by.
2458 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
2461 DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) {
2462 var a, diffs, linearray, pointer, countInsert,
2463 countDelete, textInsert, textDelete, j;
2464 // Scan the text on a line-by-line basis first.
2465 a = this.diffLinesToChars(text1, text2);
2468 linearray = a.lineArray;
2470 diffs = this.DiffMain(text1, text2, false, deadline);
2472 // Convert the diff back to original text.
2473 this.diffCharsToLines(diffs, linearray);
2474 // Eliminate freak matches (e.g. blank lines)
2475 this.diffCleanupSemantic(diffs);
2477 // Rediff any replacement blocks, this time character-by-character.
2478 // Add a dummy entry at the end.
2479 diffs.push( [ DIFF_EQUAL, "" ] );
2485 while (pointer < diffs.length) {
2486 switch ( diffs[pointer][0] ) {
2489 textInsert += diffs[pointer][1];
2493 textDelete += diffs[pointer][1];
2496 // Upon reaching an equality, check for prior redundancies.
2497 if (countDelete >= 1 && countInsert >= 1) {
2498 // Delete the offending records and add the merged ones.
2499 diffs.splice(pointer - countDelete - countInsert,
2500 countDelete + countInsert);
2501 pointer = pointer - countDelete - countInsert;
2502 a = this.DiffMain(textDelete, textInsert, false, deadline);
2503 for (j = a.length - 1; j >= 0; j--) {
2504 diffs.splice( pointer, 0, a[j] );
2506 pointer = pointer + a.length;
2516 diffs.pop(); // Remove the dummy entry at the end.
2522 * Find the 'middle snake' of a diff, split the problem in two
2523 * and return the recursively constructed diff.
2524 * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
2525 * @param {string} text1 Old string to be diffed.
2526 * @param {string} text2 New string to be diffed.
2527 * @param {number} deadline Time at which to bail if not yet complete.
2528 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
2531 DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) {
2532 var text1Length, text2Length, maxD, vOffset, vLength,
2533 v1, v2, x, delta, front, k1start, k1end, k2start,
2534 k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
2535 // Cache the text lengths to prevent multiple calls.
2536 text1Length = text1.length;
2537 text2Length = text2.length;
2538 maxD = Math.ceil((text1Length + text2Length) / 2);
2541 v1 = new Array(vLength);
2542 v2 = new Array(vLength);
2543 // Setting all elements to -1 is faster in Chrome & Firefox than mixing
2544 // integers and undefined.
2545 for (x = 0; x < vLength; x++) {
2549 v1[vOffset + 1] = 0;
2550 v2[vOffset + 1] = 0;
2551 delta = text1Length - text2Length;
2552 // If the total number of characters is odd, then the front path will collide
2553 // with the reverse path.
2554 front = (delta % 2 !== 0);
2555 // Offsets for start and end of k loop.
2556 // Prevents mapping of space beyond the grid.
2561 for (d = 0; d < maxD; d++) {
2562 // Bail out if deadline is reached.
2563 if ((new Date()).getTime() > deadline) {
2567 // Walk the front path one step.
2568 for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
2569 k1Offset = vOffset + k1;
2570 if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
2571 x1 = v1[k1Offset + 1];
2573 x1 = v1[k1Offset - 1] + 1;
2576 while (x1 < text1Length && y1 < text2Length &&
2577 text1.charAt(x1) === text2.charAt(y1)) {
2582 if (x1 > text1Length) {
2583 // Ran off the right of the graph.
2585 } else if (y1 > text2Length) {
2586 // Ran off the bottom of the graph.
2589 k2Offset = vOffset + delta - k1;
2590 if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {
2591 // Mirror x2 onto top-left coordinate system.
2592 x2 = text1Length - v2[k2Offset];
2594 // Overlap detected.
2595 return this.diffBisectSplit(text1, text2, x1, y1, deadline);
2601 // Walk the reverse path one step.
2602 for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
2603 k2Offset = vOffset + k2;
2604 if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
2605 x2 = v2[k2Offset + 1];
2607 x2 = v2[k2Offset - 1] + 1;
2610 while (x2 < text1Length && y2 < text2Length &&
2611 text1.charAt(text1Length - x2 - 1) ===
2612 text2.charAt(text2Length - y2 - 1)) {
2617 if (x2 > text1Length) {
2618 // Ran off the left of the graph.
2620 } else if (y2 > text2Length) {
2621 // Ran off the top of the graph.
2623 } else if (!front) {
2624 k1Offset = vOffset + delta - k2;
2625 if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {
2627 y1 = vOffset + x1 - k1Offset;
2628 // Mirror x2 onto top-left coordinate system.
2629 x2 = text1Length - x2;
2631 // Overlap detected.
2632 return this.diffBisectSplit(text1, text2, x1, y1, deadline);
2638 // Diff took too long and hit the deadline or
2639 // number of diffs equals number of characters, no commonality at all.
2641 [ DIFF_DELETE, text1 ],
2642 [ DIFF_INSERT, text2 ]
2647 * Given the location of the 'middle snake', split the diff in two parts
2649 * @param {string} text1 Old string to be diffed.
2650 * @param {string} text2 New string to be diffed.
2651 * @param {number} x Index of split point in text1.
2652 * @param {number} y Index of split point in text2.
2653 * @param {number} deadline Time at which to bail if not yet complete.
2654 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
2657 DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
2658 var text1a, text1b, text2a, text2b, diffs, diffsb;
2659 text1a = text1.substring(0, x);
2660 text2a = text2.substring(0, y);
2661 text1b = text1.substring(x);
2662 text2b = text2.substring(y);
2664 // Compute both diffs serially.
2665 diffs = this.DiffMain(text1a, text2a, false, deadline);
2666 diffsb = this.DiffMain(text1b, text2b, false, deadline);
2668 return diffs.concat(diffsb);
2672 * Reduce the number of edits by eliminating semantically trivial equalities.
2673 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
2675 DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) {
2676 var changes, equalities, equalitiesLength, lastequality,
2677 pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
2678 lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
2680 equalities = []; // Stack of indices where equalities are found.
2681 equalitiesLength = 0; // Keeping our own length var is faster in JS.
2682 /** @type {?string} */
2683 lastequality = null;
2684 // Always equal to diffs[equalities[equalitiesLength - 1]][1]
2685 pointer = 0; // Index of current position.
2686 // Number of characters that changed prior to the equality.
2687 lengthInsertions1 = 0;
2688 lengthDeletions1 = 0;
2689 // Number of characters that changed after the equality.
2690 lengthInsertions2 = 0;
2691 lengthDeletions2 = 0;
2692 while (pointer < diffs.length) {
2693 if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found.
2694 equalities[equalitiesLength++] = pointer;
2695 lengthInsertions1 = lengthInsertions2;
2696 lengthDeletions1 = lengthDeletions2;
2697 lengthInsertions2 = 0;
2698 lengthDeletions2 = 0;
2699 lastequality = diffs[pointer][1];
2700 } else { // An insertion or deletion.
2701 if (diffs[pointer][0] === DIFF_INSERT) {
2702 lengthInsertions2 += diffs[pointer][1].length;
2704 lengthDeletions2 += diffs[pointer][1].length;
2706 // Eliminate an equality that is smaller or equal to the edits on both
2708 if (lastequality && (lastequality.length <=
2709 Math.max(lengthInsertions1, lengthDeletions1)) &&
2710 (lastequality.length <= Math.max(lengthInsertions2,
2711 lengthDeletions2))) {
2712 // Duplicate record.
2713 diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] );
2714 // Change second copy to insert.
2715 diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
2716 // Throw away the equality we just deleted.
2718 // Throw away the previous equality (it needs to be reevaluated).
2720 pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
2721 lengthInsertions1 = 0; // Reset the counters.
2722 lengthDeletions1 = 0;
2723 lengthInsertions2 = 0;
2724 lengthDeletions2 = 0;
2725 lastequality = null;
2732 // Normalize the diff.
2734 this.diffCleanupMerge(diffs);
2737 // Find any overlaps between deletions and insertions.
2738 // e.g: <del>abcxxx</del><ins>xxxdef</ins>
2739 // -> <del>abc</del>xxx<ins>def</ins>
2740 // e.g: <del>xxxabc</del><ins>defxxx</ins>
2741 // -> <ins>def</ins>xxx<del>abc</del>
2742 // Only extract an overlap if it is as big as the edit ahead or behind it.
2744 while (pointer < diffs.length) {
2745 if (diffs[pointer - 1][0] === DIFF_DELETE &&
2746 diffs[pointer][0] === DIFF_INSERT) {
2747 deletion = diffs[pointer - 1][1];
2748 insertion = diffs[pointer][1];
2749 overlapLength1 = this.diffCommonOverlap(deletion, insertion);
2750 overlapLength2 = this.diffCommonOverlap(insertion, deletion);
2751 if (overlapLength1 >= overlapLength2) {
2752 if (overlapLength1 >= deletion.length / 2 ||
2753 overlapLength1 >= insertion.length / 2) {
2754 // Overlap found. Insert an equality and trim the surrounding edits.
2755 diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] );
2756 diffs[pointer - 1][1] =
2757 deletion.substring(0, deletion.length - overlapLength1);
2758 diffs[pointer + 1][1] = insertion.substring(overlapLength1);
2762 if (overlapLength2 >= deletion.length / 2 ||
2763 overlapLength2 >= insertion.length / 2) {
2764 // Reverse overlap found.
2765 // Insert an equality and swap and trim the surrounding edits.
2766 diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] );
2767 diffs[pointer - 1][0] = DIFF_INSERT;
2768 diffs[pointer - 1][1] =
2769 insertion.substring(0, insertion.length - overlapLength2);
2770 diffs[pointer + 1][0] = DIFF_DELETE;
2771 diffs[pointer + 1][1] =
2772 deletion.substring(overlapLength2);
2783 * Determine if the suffix of one string is the prefix of another.
2784 * @param {string} text1 First string.
2785 * @param {string} text2 Second string.
2786 * @return {number} The number of characters common to the end of the first
2787 * string and the start of the second string.
2790 DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) {
2791 var text1Length, text2Length, textLength,
2792 best, length, pattern, found;
2793 // Cache the text lengths to prevent multiple calls.
2794 text1Length = text1.length;
2795 text2Length = text2.length;
2796 // Eliminate the null case.
2797 if (text1Length === 0 || text2Length === 0) {
2800 // Truncate the longer string.
2801 if (text1Length > text2Length) {
2802 text1 = text1.substring(text1Length - text2Length);
2803 } else if (text1Length < text2Length) {
2804 text2 = text2.substring(0, text1Length);
2806 textLength = Math.min(text1Length, text2Length);
2807 // Quick check for the worst case.
2808 if (text1 === text2) {
2812 // Start by looking for a single character match
2813 // and increase length until no match is found.
2814 // Performance analysis: http://neil.fraser.name/news/2010/11/04/
2818 pattern = text1.substring(textLength - length);
2819 found = text2.indexOf(pattern);
2824 if (found === 0 || text1.substring(textLength - length) ===
2825 text2.substring(0, length)) {
2833 * Split two texts into an array of strings. Reduce the texts to a string of
2834 * hashes where each Unicode character represents one line.
2835 * @param {string} text1 First string.
2836 * @param {string} text2 Second string.
2837 * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
2838 * An object containing the encoded text1, the encoded text2 and
2839 * the array of unique strings.
2840 * The zeroth element of the array of unique strings is intentionally blank.
2843 DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) {
2844 var lineArray, lineHash, chars1, chars2;
2845 lineArray = []; // e.g. lineArray[4] === 'Hello\n'
2846 lineHash = {}; // e.g. lineHash['Hello\n'] === 4
2848 // '\x00' is a valid character, but various debuggers don't like it.
2849 // So we'll insert a junk entry to avoid generating a null character.
2853 * Split a text into an array of strings. Reduce the texts to a string of
2854 * hashes where each Unicode character represents one line.
2855 * Modifies linearray and linehash through being a closure.
2856 * @param {string} text String to encode.
2857 * @return {string} Encoded string.
2860 function diffLinesToCharsMunge(text) {
2861 var chars, lineStart, lineEnd, lineArrayLength, line;
2863 // Walk the text, pulling out a substring for each line.
2864 // text.split('\n') would would temporarily double our memory footprint.
2865 // Modifying text would create many large strings to garbage collect.
2868 // Keeping our own length variable is faster than looking it up.
2869 lineArrayLength = lineArray.length;
2870 while (lineEnd < text.length - 1) {
2871 lineEnd = text.indexOf("\n", lineStart);
2872 if (lineEnd === -1) {
2873 lineEnd = text.length - 1;
2875 line = text.substring(lineStart, lineEnd + 1);
2876 lineStart = lineEnd + 1;
2878 if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
2879 (lineHash[line] !== undefined)) {
2880 chars += String.fromCharCode( lineHash[ line ] );
2882 chars += String.fromCharCode(lineArrayLength);
2883 lineHash[line] = lineArrayLength;
2884 lineArray[lineArrayLength++] = line;
2890 chars1 = diffLinesToCharsMunge(text1);
2891 chars2 = diffLinesToCharsMunge(text2);
2895 lineArray: lineArray
2900 * Rehydrate the text in a diff from a string of line hashes to real lines of
2902 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
2903 * @param {!Array.<string>} lineArray Array of unique strings.
2906 DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
2907 var x, chars, text, y;
2908 for ( x = 0; x < diffs.length; x++ ) {
2909 chars = diffs[x][1];
2911 for ( y = 0; y < chars.length; y++ ) {
2912 text[y] = lineArray[chars.charCodeAt(y)];
2914 diffs[x][1] = text.join("");
2919 * Reorder and merge like edit sections. Merge equalities.
2920 * Any edit section can move as long as it doesn't cross an equality.
2921 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
2923 DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) {
2924 var pointer, countDelete, countInsert, textInsert, textDelete,
2925 commonlength, changes;
2926 diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
2933 while (pointer < diffs.length) {
2934 switch ( diffs[ pointer ][ 0 ] ) {
2937 textInsert += diffs[pointer][1];
2942 textDelete += diffs[pointer][1];
2946 // Upon reaching an equality, check for prior redundancies.
2947 if (countDelete + countInsert > 1) {
2948 if (countDelete !== 0 && countInsert !== 0) {
2949 // Factor out any common prefixies.
2950 commonlength = this.diffCommonPrefix(textInsert, textDelete);
2951 if (commonlength !== 0) {
2952 if ((pointer - countDelete - countInsert) > 0 &&
2953 diffs[pointer - countDelete - countInsert - 1][0] ===
2955 diffs[pointer - countDelete - countInsert - 1][1] +=
2956 textInsert.substring(0, commonlength);
2958 diffs.splice( 0, 0, [ DIFF_EQUAL,
2959 textInsert.substring( 0, commonlength )
2963 textInsert = textInsert.substring(commonlength);
2964 textDelete = textDelete.substring(commonlength);
2966 // Factor out any common suffixies.
2967 commonlength = this.diffCommonSuffix(textInsert, textDelete);
2968 if (commonlength !== 0) {
2969 diffs[pointer][1] = textInsert.substring(textInsert.length -
2970 commonlength) + diffs[pointer][1];
2971 textInsert = textInsert.substring(0, textInsert.length -
2973 textDelete = textDelete.substring(0, textDelete.length -
2977 // Delete the offending records and add the merged ones.
2978 if (countDelete === 0) {
2979 diffs.splice( pointer - countInsert,
2980 countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
2981 } else if (countInsert === 0) {
2982 diffs.splice( pointer - countDelete,
2983 countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
2985 diffs.splice( pointer - countDelete - countInsert,
2986 countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] );
2988 pointer = pointer - countDelete - countInsert +
2989 (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1;
2990 } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
2991 // Merge this equality with the previous one.
2992 diffs[pointer - 1][1] += diffs[pointer][1];
2993 diffs.splice(pointer, 1);
3004 if (diffs[diffs.length - 1][1] === "") {
3005 diffs.pop(); // Remove the dummy entry at the end.
3008 // Second pass: look for single edits surrounded on both sides by equalities
3009 // which can be shifted sideways to eliminate an equality.
3010 // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
3013 // Intentionally ignore the first and last element (don't need checking).
3014 while (pointer < diffs.length - 1) {
3015 if (diffs[pointer - 1][0] === DIFF_EQUAL &&
3016 diffs[pointer + 1][0] === DIFF_EQUAL) {
3017 // This is a single edit surrounded by equalities.
3018 if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length -
3019 diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) {
3020 // Shift the edit over the previous equality.
3021 diffs[pointer][1] = diffs[pointer - 1][1] +
3022 diffs[pointer][1].substring(0, diffs[pointer][1].length -
3023 diffs[pointer - 1][1].length);
3024 diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
3025 diffs.splice(pointer - 1, 1);
3027 } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
3028 diffs[ pointer + 1 ][ 1 ] ) {
3029 // Shift the edit over the next equality.
3030 diffs[pointer - 1][1] += diffs[pointer + 1][1];
3032 diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
3033 diffs[pointer + 1][1];
3034 diffs.splice(pointer + 1, 1);
3040 // If shifts were made, the diff needs reordering and another shift sweep.
3042 this.diffCleanupMerge(diffs);
3046 return function(o, n) {
3047 var diff, output, text;
3048 diff = new DiffMatchPatch();
3049 output = diff.DiffMain(o, n);
3050 //console.log(output);
3051 diff.diffCleanupEfficiency(output);
3052 text = diff.diffPrettyHtml(output);
3061 // Deprecated QUnit.init - Ref #530
3062 // Re-initialize the configuration options
3063 QUnit.init = function() {
3064 var tests, banner, result, qunit,
3065 config = QUnit.config;
3067 config.stats = { all: 0, bad: 0 };
3068 config.moduleStats = { all: 0, bad: 0 };
3070 config.updateRate = 1000;
3071 config.blocking = false;
3072 config.autostart = true;
3073 config.autorun = false;
3077 // Return on non-browser environments
3078 // This is necessary to not break on node tests
3079 if ( typeof window === "undefined" ) {
3083 qunit = id( "qunit" );
3086 "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
3087 "<h2 id='qunit-banner'></h2>" +
3088 "<div id='qunit-testrunner-toolbar'></div>" +
3089 "<h2 id='qunit-userAgent'></h2>" +
3090 "<ol id='qunit-tests'></ol>";
3093 tests = id( "qunit-tests" );
3094 banner = id( "qunit-banner" );
3095 result = id( "qunit-testresult" );
3098 tests.innerHTML = "";
3102 banner.className = "";
3106 result.parentNode.removeChild( result );
3110 result = document.createElement( "p" );
3111 result.id = "qunit-testresult";
3112 result.className = "result";
3113 tests.parentNode.insertBefore( result, tests );
3114 result.innerHTML = "Running...<br /> ";
3118 // Don't load the HTML Reporter on non-Browser environments
3119 if ( typeof window === "undefined" ) {
3123 var config = QUnit.config,
3124 hasOwn = Object.prototype.hasOwnProperty,
3126 document: window.document !== undefined,
3127 sessionStorage: (function() {
3128 var x = "qunit-test-string";
3130 sessionStorage.setItem( x, x );
3131 sessionStorage.removeItem( x );
3141 * Escape text for attribute or text content.
3143 function escapeText( s ) {
3149 // Both single quotes and double quotes (for attributes)
3150 return s.replace( /['"<>&]/g, function( s ) {
3167 * @param {HTMLElement} elem
3168 * @param {string} type
3169 * @param {Function} fn
3171 function addEvent( elem, type, fn ) {
3172 if ( elem.addEventListener ) {
3174 // Standards-based browsers
3175 elem.addEventListener( type, fn, false );
3176 } else if ( elem.attachEvent ) {
3179 elem.attachEvent( "on" + type, function() {
3180 var event = window.event;
3181 if ( !event.target ) {
3182 event.target = event.srcElement || document;
3185 fn.call( elem, event );
3191 * @param {Array|NodeList} elems
3192 * @param {string} type
3193 * @param {Function} fn
3195 function addEvents( elems, type, fn ) {
3196 var i = elems.length;
3198 addEvent( elems[ i ], type, fn );
3202 function hasClass( elem, name ) {
3203 return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
3206 function addClass( elem, name ) {
3207 if ( !hasClass( elem, name ) ) {
3208 elem.className += ( elem.className ? " " : "" ) + name;
3212 function toggleClass( elem, name ) {
3213 if ( hasClass( elem, name ) ) {
3214 removeClass( elem, name );
3216 addClass( elem, name );
3220 function removeClass( elem, name ) {
3221 var set = " " + elem.className + " ";
3223 // Class name may appear multiple times
3224 while ( set.indexOf( " " + name + " " ) >= 0 ) {
3225 set = set.replace( " " + name + " ", " " );
3228 // trim for prettiness
3229 elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
3232 function id( name ) {
3233 return defined.document && document.getElementById && document.getElementById( name );
3236 function getUrlConfigHtml() {
3238 escaped, escapedTooltip,
3240 len = config.urlConfig.length,
3243 for ( i = 0; i < len; i++ ) {
3244 val = config.urlConfig[ i ];
3245 if ( typeof val === "string" ) {
3252 escaped = escapeText( val.id );
3253 escapedTooltip = escapeText( val.tooltip );
3255 if ( config[ val.id ] === undefined ) {
3256 config[ val.id ] = QUnit.urlParams[ val.id ];
3259 if ( !val.value || typeof val.value === "string" ) {
3260 urlConfigHtml += "<input id='qunit-urlconfig-" + escaped +
3261 "' name='" + escaped + "' type='checkbox'" +
3262 ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
3263 ( config[ val.id ] ? " checked='checked'" : "" ) +
3264 " title='" + escapedTooltip + "' /><label for='qunit-urlconfig-" + escaped +
3265 "' title='" + escapedTooltip + "'>" + val.label + "</label>";
3267 urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
3268 "' title='" + escapedTooltip + "'>" + val.label +
3269 ": </label><select id='qunit-urlconfig-" + escaped +
3270 "' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>";
3272 if ( QUnit.is( "array", val.value ) ) {
3273 for ( j = 0; j < val.value.length; j++ ) {
3274 escaped = escapeText( val.value[ j ] );
3275 urlConfigHtml += "<option value='" + escaped + "'" +
3276 ( config[ val.id ] === val.value[ j ] ?
3277 ( selection = true ) && " selected='selected'" : "" ) +
3278 ">" + escaped + "</option>";
3281 for ( j in val.value ) {
3282 if ( hasOwn.call( val.value, j ) ) {
3283 urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
3284 ( config[ val.id ] === j ?
3285 ( selection = true ) && " selected='selected'" : "" ) +
3286 ">" + escapeText( val.value[ j ] ) + "</option>";
3290 if ( config[ val.id ] && !selection ) {
3291 escaped = escapeText( config[ val.id ] );
3292 urlConfigHtml += "<option value='" + escaped +
3293 "' selected='selected' disabled='disabled'>" + escaped + "</option>";
3295 urlConfigHtml += "</select>";
3299 return urlConfigHtml;
3302 // Handle "click" events on toolbar checkboxes and "change" for select menus.
3303 // Updates the URL with the new state of `config.urlConfig` values.
3304 function toolbarChanged() {
3305 var updatedUrl, value,
3309 // Detect if field is a select menu or a checkbox
3310 if ( "selectedIndex" in field ) {
3311 value = field.options[ field.selectedIndex ].value || undefined;
3313 value = field.checked ? ( field.defaultValue || true ) : undefined;
3316 params[ field.name ] = value;
3317 updatedUrl = setUrl( params );
3319 if ( "hidepassed" === field.name && "replaceState" in window.history ) {
3320 config[ field.name ] = value || false;
3322 addClass( id( "qunit-tests" ), "hidepass" );
3324 removeClass( id( "qunit-tests" ), "hidepass" );
3327 // It is not necessary to refresh the whole page
3328 window.history.replaceState( null, "", updatedUrl );
3330 window.location = updatedUrl;
3334 function setUrl( params ) {
3338 params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
3340 for ( key in params ) {
3341 if ( hasOwn.call( params, key ) ) {
3342 if ( params[ key ] === undefined ) {
3345 querystring += encodeURIComponent( key );
3346 if ( params[ key ] !== true ) {
3347 querystring += "=" + encodeURIComponent( params[ key ] );
3352 return location.protocol + "//" + location.host +
3353 location.pathname + querystring.slice( 0, -1 );
3356 function applyUrlParams() {
3358 modulesList = id( "qunit-modulefilter" ),
3359 filter = id( "qunit-filter-input" ).value;
3361 selectedModule = modulesList ?
3362 decodeURIComponent( modulesList.options[ modulesList.selectedIndex ].value ) :
3365 window.location = setUrl({
3366 module: ( selectedModule === "" ) ? undefined : selectedModule,
3367 filter: ( filter === "" ) ? undefined : filter,
3369 // Remove testId filter
3374 function toolbarUrlConfigContainer() {
3375 var urlConfigContainer = document.createElement( "span" );
3377 urlConfigContainer.innerHTML = getUrlConfigHtml();
3378 addClass( urlConfigContainer, "qunit-url-config" );
3380 // For oldIE support:
3381 // * Add handlers to the individual elements instead of the container
3382 // * Use "click" instead of "change" for checkboxes
3383 addEvents( urlConfigContainer.getElementsByTagName( "input" ), "click", toolbarChanged );
3384 addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
3386 return urlConfigContainer;
3389 function toolbarLooseFilter() {
3390 var filter = document.createElement( "form" ),
3391 label = document.createElement( "label" ),
3392 input = document.createElement( "input" ),
3393 button = document.createElement( "button" );
3395 addClass( filter, "qunit-filter" );
3397 label.innerHTML = "Filter: ";
3399 input.type = "text";
3400 input.value = config.filter || "";
3401 input.name = "filter";
3402 input.id = "qunit-filter-input";
3404 button.innerHTML = "Go";
3406 label.appendChild( input );
3408 filter.appendChild( label );
3409 filter.appendChild( button );
3410 addEvent( filter, "submit", function( ev ) {
3413 if ( ev && ev.preventDefault ) {
3414 ev.preventDefault();
3423 function toolbarModuleFilterHtml() {
3425 moduleFilterHtml = "";
3427 if ( !modulesList.length ) {
3431 modulesList.sort(function( a, b ) {
3432 return a.localeCompare( b );
3435 moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label>" +
3436 "<select id='qunit-modulefilter' name='modulefilter'><option value='' " +
3437 ( QUnit.urlParams.module === undefined ? "selected='selected'" : "" ) +
3438 ">< All Modules ></option>";
3440 for ( i = 0; i < modulesList.length; i++ ) {
3441 moduleFilterHtml += "<option value='" +
3442 escapeText( encodeURIComponent( modulesList[ i ] ) ) + "' " +
3443 ( QUnit.urlParams.module === modulesList[ i ] ? "selected='selected'" : "" ) +
3444 ">" + escapeText( modulesList[ i ] ) + "</option>";
3446 moduleFilterHtml += "</select>";
3448 return moduleFilterHtml;
3451 function toolbarModuleFilter() {
3452 var toolbar = id( "qunit-testrunner-toolbar" ),
3453 moduleFilter = document.createElement( "span" ),
3454 moduleFilterHtml = toolbarModuleFilterHtml();
3456 if ( !toolbar || !moduleFilterHtml ) {
3460 moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
3461 moduleFilter.innerHTML = moduleFilterHtml;
3463 addEvent( moduleFilter.lastChild, "change", applyUrlParams );
3465 toolbar.appendChild( moduleFilter );
3468 function appendToolbar() {
3469 var toolbar = id( "qunit-testrunner-toolbar" );
3472 toolbar.appendChild( toolbarUrlConfigContainer() );
3473 toolbar.appendChild( toolbarLooseFilter() );
3477 function appendHeader() {
3478 var header = id( "qunit-header" );
3481 header.innerHTML = "<a href='" +
3482 setUrl({ filter: undefined, module: undefined, testId: undefined }) +
3483 "'>" + header.innerHTML + "</a> ";
3487 function appendBanner() {
3488 var banner = id( "qunit-banner" );
3491 banner.className = "";
3495 function appendTestResults() {
3496 var tests = id( "qunit-tests" ),
3497 result = id( "qunit-testresult" );
3500 result.parentNode.removeChild( result );
3504 tests.innerHTML = "";
3505 result = document.createElement( "p" );
3506 result.id = "qunit-testresult";
3507 result.className = "result";
3508 tests.parentNode.insertBefore( result, tests );
3509 result.innerHTML = "Running...<br /> ";
3513 function storeFixture() {
3514 var fixture = id( "qunit-fixture" );
3516 config.fixture = fixture.innerHTML;
3520 function appendUserAgent() {
3521 var userAgent = id( "qunit-userAgent" );
3524 userAgent.innerHTML = "";
3525 userAgent.appendChild(
3526 document.createTextNode(
3527 "QUnit " + QUnit.version + "; " + navigator.userAgent
3533 function appendTestsList( modules ) {
3534 var i, l, x, z, test, moduleObj;
3536 for ( i = 0, l = modules.length; i < l; i++ ) {
3537 moduleObj = modules[ i ];
3539 if ( moduleObj.name ) {
3540 modulesList.push( moduleObj.name );
3543 for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
3544 test = moduleObj.tests[ x ];
3546 appendTest( test.name, test.testId, moduleObj.name );
3551 function appendTest( name, testId, moduleName ) {
3552 var title, rerunTrigger, testBlock, assertList,
3553 tests = id( "qunit-tests" );
3559 title = document.createElement( "strong" );
3560 title.innerHTML = getNameHtml( name, moduleName );
3562 rerunTrigger = document.createElement( "a" );
3563 rerunTrigger.innerHTML = "Rerun";
3564 rerunTrigger.href = setUrl({ testId: testId });
3566 testBlock = document.createElement( "li" );
3567 testBlock.appendChild( title );
3568 testBlock.appendChild( rerunTrigger );
3569 testBlock.id = "qunit-test-output-" + testId;
3571 assertList = document.createElement( "ol" );
3572 assertList.className = "qunit-assert-list";
3574 testBlock.appendChild( assertList );
3576 tests.appendChild( testBlock );
3579 // HTML Reporter initialization and load
3580 QUnit.begin(function( details ) {
3581 var qunit = id( "qunit" );
3583 // Fixture is the only one necessary to run without the #qunit element
3588 "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
3589 "<h2 id='qunit-banner'></h2>" +
3590 "<div id='qunit-testrunner-toolbar'></div>" +
3591 "<h2 id='qunit-userAgent'></h2>" +
3592 "<ol id='qunit-tests'></ol>";
3597 appendTestResults();
3600 appendTestsList( details.modules );
3601 toolbarModuleFilter();
3603 if ( qunit && config.hidepassed ) {
3604 addClass( qunit.lastChild, "hidepass" );
3608 QUnit.done(function( details ) {
3610 banner = id( "qunit-banner" ),
3611 tests = id( "qunit-tests" ),
3613 "Tests completed in ",
3615 " milliseconds.<br />",
3616 "<span class='passed'>",
3618 "</span> assertions of <span class='total'>",
3620 "</span> passed, <span class='failed'>",
3626 banner.className = details.failed ? "qunit-fail" : "qunit-pass";
3630 id( "qunit-testresult" ).innerHTML = html;
3633 if ( config.altertitle && defined.document && document.title ) {
3635 // show ✖ for good, ✔ for bad suite result in title
3636 // use escape sequences in case file gets loaded with non-utf-8-charset
3638 ( details.failed ? "\u2716" : "\u2714" ),
3639 document.title.replace( /^[\u2714\u2716] /i, "" )
3643 // clear own sessionStorage items if all tests passed
3644 if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
3645 for ( i = 0; i < sessionStorage.length; i++ ) {
3646 key = sessionStorage.key( i++ );
3647 if ( key.indexOf( "qunit-test-" ) === 0 ) {
3648 sessionStorage.removeItem( key );
3653 // scroll back to top to show results
3654 if ( config.scrolltop && window.scrollTo ) {
3655 window.scrollTo( 0, 0 );
3659 function getNameHtml( name, module ) {
3663 nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: ";
3666 nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>";
3671 QUnit.testStart(function( details ) {
3672 var running, testBlock, bad;
3674 testBlock = id( "qunit-test-output-" + details.testId );
3676 testBlock.className = "running";
3679 // Report later registered tests
3680 appendTest( details.name, details.testId, details.module );
3683 running = id( "qunit-testresult" );
3685 bad = QUnit.config.reorder && defined.sessionStorage &&
3686 +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name );
3688 running.innerHTML = ( bad ?
3689 "Rerunning previously failed test: <br />" :
3690 "Running: <br />" ) +
3691 getNameHtml( details.name, details.module );
3696 QUnit.log(function( details ) {
3697 var assertList, assertLi,
3698 message, expected, actual,
3699 testItem = id( "qunit-test-output-" + details.testId );
3705 message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
3706 message = "<span class='test-message'>" + message + "</span>";
3707 message += "<span class='runtime'>@ " + details.runtime + " ms</span>";
3709 // pushFailure doesn't provide details.expected
3710 // when it calls, it's implicit to also not show expected and diff stuff
3711 // Also, we need to check details.expected existence, as it can exist and be undefined
3712 if ( !details.result && hasOwn.call( details, "expected" ) ) {
3713 expected = escapeText( QUnit.dump.parse( details.expected ) );
3714 actual = escapeText( QUnit.dump.parse( details.actual ) );
3715 message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" +
3719 if ( actual !== expected ) {
3720 message += "<tr class='test-actual'><th>Result: </th><td><pre>" +
3721 actual + "</pre></td></tr>" +
3722 "<tr class='test-diff'><th>Diff: </th><td><pre>" +
3723 QUnit.diff( expected, actual ) + "</pre></td></tr>";
3725 if ( expected.indexOf( "[object Array]" ) !== -1 ||
3726 expected.indexOf( "[object Object]" ) !== -1 ) {
3727 message += "<tr class='test-message'><th>Message: </th><td>" +
3728 "Diff suppressed as the depth of object is more than current max depth (" +
3729 QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " +
3730 " run with a higher max depth or <a href='" + setUrl({ maxDepth: -1 }) + "'>" +
3731 "Rerun</a> without max depth.</p></td></tr>";
3735 if ( details.source ) {
3736 message += "<tr class='test-source'><th>Source: </th><td><pre>" +
3737 escapeText( details.source ) + "</pre></td></tr>";
3740 message += "</table>";
3742 // this occours when pushFailure is set and we have an extracted stack trace
3743 } else if ( !details.result && details.source ) {
3744 message += "<table>" +
3745 "<tr class='test-source'><th>Source: </th><td><pre>" +
3746 escapeText( details.source ) + "</pre></td></tr>" +
3750 assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
3752 assertLi = document.createElement( "li" );
3753 assertLi.className = details.result ? "pass" : "fail";
3754 assertLi.innerHTML = message;
3755 assertList.appendChild( assertLi );
3758 QUnit.testDone(function( details ) {
3759 var testTitle, time, testItem, assertList,
3760 good, bad, testCounts, skipped,
3761 tests = id( "qunit-tests" );
3767 testItem = id( "qunit-test-output-" + details.testId );
3769 assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
3771 good = details.passed;
3772 bad = details.failed;
3774 // store result when possible
3775 if ( config.reorder && defined.sessionStorage ) {
3777 sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad );
3779 sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name );
3784 addClass( assertList, "qunit-collapsed" );
3787 // testItem.firstChild is the test name
3788 testTitle = testItem.firstChild;
3791 "<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " :
3794 testTitle.innerHTML += " <b class='counts'>(" + testCounts +
3795 details.assertions.length + ")</b>";
3797 if ( details.skipped ) {
3798 testItem.className = "skipped";
3799 skipped = document.createElement( "em" );
3800 skipped.className = "qunit-skipped-label";
3801 skipped.innerHTML = "skipped";
3802 testItem.insertBefore( skipped, testTitle );
3804 addEvent( testTitle, "click", function() {
3805 toggleClass( assertList, "qunit-collapsed" );
3808 testItem.className = bad ? "fail" : "pass";
3810 time = document.createElement( "span" );
3811 time.className = "runtime";
3812 time.innerHTML = details.runtime + " ms";
3813 testItem.insertBefore( time, assertList );
3817 if ( defined.document ) {
3818 if ( document.readyState === "complete" ) {
3821 addEvent( window, "load", QUnit.load );
3824 config.pageLoaded = true;
3825 config.autorun = true;