]> git.ipfire.org Git - ipfire.org.git/blob - src/scss/bootstrap-4.0.0-alpha.6/js/tests/vendor/qunit.js
Introduce autotools
[ipfire.org.git] / src / scss / bootstrap-4.0.0-alpha.6 / js / tests / vendor / qunit.js
1 /*!
2 * QUnit 2.0.1
3 * https://qunitjs.com/
4 *
5 * Copyright jQuery Foundation and other contributors
6 * Released under the MIT license
7 * https://jquery.org/license
8 *
9 * Date: 2016-07-23T19:39Z
10 */
11
12 ( function( global ) {
13
14 var QUnit = {};
15
16 var Date = global.Date;
17 var now = Date.now || function() {
18 return new Date().getTime();
19 };
20
21 var setTimeout = global.setTimeout;
22 var clearTimeout = global.clearTimeout;
23
24 // Store a local window from the global to allow direct references.
25 var window = global.window;
26
27 var defined = {
28 document: window && window.document !== undefined,
29 setTimeout: setTimeout !== undefined,
30 sessionStorage: ( function() {
31 var x = "qunit-test-string";
32 try {
33 sessionStorage.setItem( x, x );
34 sessionStorage.removeItem( x );
35 return true;
36 } catch ( e ) {
37 return false;
38 }
39 }() )
40 };
41
42 var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" );
43 var globalStartCalled = false;
44 var runStarted = false;
45
46 var autorun = false;
47
48 var toString = Object.prototype.toString,
49 hasOwn = Object.prototype.hasOwnProperty;
50
51 // Returns a new Array with the elements that are in a but not in b
52 function diff( a, b ) {
53 var i, j,
54 result = a.slice();
55
56 for ( i = 0; i < result.length; i++ ) {
57 for ( j = 0; j < b.length; j++ ) {
58 if ( result[ i ] === b[ j ] ) {
59 result.splice( i, 1 );
60 i--;
61 break;
62 }
63 }
64 }
65 return result;
66 }
67
68 // From jquery.js
69 function inArray( elem, array ) {
70 if ( array.indexOf ) {
71 return array.indexOf( elem );
72 }
73
74 for ( var i = 0, length = array.length; i < length; i++ ) {
75 if ( array[ i ] === elem ) {
76 return i;
77 }
78 }
79
80 return -1;
81 }
82
83 /**
84 * Makes a clone of an object using only Array or Object as base,
85 * and copies over the own enumerable properties.
86 *
87 * @param {Object} obj
88 * @return {Object} New object with only the own properties (recursively).
89 */
90 function objectValues ( obj ) {
91 var key, val,
92 vals = QUnit.is( "array", obj ) ? [] : {};
93 for ( key in obj ) {
94 if ( hasOwn.call( obj, key ) ) {
95 val = obj[ key ];
96 vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
97 }
98 }
99 return vals;
100 }
101
102 function extend( a, b, undefOnly ) {
103 for ( var prop in b ) {
104 if ( hasOwn.call( b, prop ) ) {
105 if ( b[ prop ] === undefined ) {
106 delete a[ prop ];
107 } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
108 a[ prop ] = b[ prop ];
109 }
110 }
111 }
112
113 return a;
114 }
115
116 function objectType( obj ) {
117 if ( typeof obj === "undefined" ) {
118 return "undefined";
119 }
120
121 // Consider: typeof null === object
122 if ( obj === null ) {
123 return "null";
124 }
125
126 var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
127 type = match && match[ 1 ];
128
129 switch ( type ) {
130 case "Number":
131 if ( isNaN( obj ) ) {
132 return "nan";
133 }
134 return "number";
135 case "String":
136 case "Boolean":
137 case "Array":
138 case "Set":
139 case "Map":
140 case "Date":
141 case "RegExp":
142 case "Function":
143 case "Symbol":
144 return type.toLowerCase();
145 }
146 if ( typeof obj === "object" ) {
147 return "object";
148 }
149 }
150
151 // Safe object type checking
152 function is( type, obj ) {
153 return QUnit.objectType( obj ) === type;
154 }
155
156 // Doesn't support IE9, it will return undefined on these browsers
157 // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
158 function extractStacktrace( e, offset ) {
159 offset = offset === undefined ? 4 : offset;
160
161 var stack, include, i;
162
163 if ( e.stack ) {
164 stack = e.stack.split( "\n" );
165 if ( /^error$/i.test( stack[ 0 ] ) ) {
166 stack.shift();
167 }
168 if ( fileName ) {
169 include = [];
170 for ( i = offset; i < stack.length; i++ ) {
171 if ( stack[ i ].indexOf( fileName ) !== -1 ) {
172 break;
173 }
174 include.push( stack[ i ] );
175 }
176 if ( include.length ) {
177 return include.join( "\n" );
178 }
179 }
180 return stack[ offset ];
181 }
182 }
183
184 function sourceFromStacktrace( offset ) {
185 var error = new Error();
186
187 // Support: Safari <=7 only, IE <=10 - 11 only
188 // Not all browsers generate the `stack` property for `new Error()`, see also #636
189 if ( !error.stack ) {
190 try {
191 throw error;
192 } catch ( err ) {
193 error = err;
194 }
195 }
196
197 return extractStacktrace( error, offset );
198 }
199
200 /**
201 * Config object: Maintain internal state
202 * Later exposed as QUnit.config
203 * `config` initialized at top of scope
204 */
205 var config = {
206
207 // The queue of tests to run
208 queue: [],
209
210 // Block until document ready
211 blocking: true,
212
213 // By default, run previously failed tests first
214 // very useful in combination with "Hide passed tests" checked
215 reorder: true,
216
217 // By default, modify document.title when suite is done
218 altertitle: true,
219
220 // HTML Reporter: collapse every test except the first failing test
221 // If false, all failing tests will be expanded
222 collapse: true,
223
224 // By default, scroll to top of the page when suite is done
225 scrolltop: true,
226
227 // Depth up-to which object will be dumped
228 maxDepth: 5,
229
230 // When enabled, all tests must call expect()
231 requireExpects: false,
232
233 // Placeholder for user-configurable form-exposed URL parameters
234 urlConfig: [],
235
236 // Set of all modules.
237 modules: [],
238
239 // Stack of nested modules
240 moduleStack: [],
241
242 // The first unnamed module
243 currentModule: {
244 name: "",
245 tests: []
246 },
247
248 callbacks: {}
249 };
250
251 // Push a loose unnamed module to the modules collection
252 config.modules.push( config.currentModule );
253
254 // Register logging callbacks
255 function registerLoggingCallbacks( obj ) {
256 var i, l, key,
257 callbackNames = [ "begin", "done", "log", "testStart", "testDone",
258 "moduleStart", "moduleDone" ];
259
260 function registerLoggingCallback( key ) {
261 var loggingCallback = function( callback ) {
262 if ( objectType( callback ) !== "function" ) {
263 throw new Error(
264 "QUnit logging methods require a callback function as their first parameters."
265 );
266 }
267
268 config.callbacks[ key ].push( callback );
269 };
270
271 return loggingCallback;
272 }
273
274 for ( i = 0, l = callbackNames.length; i < l; i++ ) {
275 key = callbackNames[ i ];
276
277 // Initialize key collection of logging callback
278 if ( objectType( config.callbacks[ key ] ) === "undefined" ) {
279 config.callbacks[ key ] = [];
280 }
281
282 obj[ key ] = registerLoggingCallback( key );
283 }
284 }
285
286 function runLoggingCallbacks( key, args ) {
287 var i, l, callbacks;
288
289 callbacks = config.callbacks[ key ];
290 for ( i = 0, l = callbacks.length; i < l; i++ ) {
291 callbacks[ i ]( args );
292 }
293 }
294
295 ( function() {
296 if ( !defined.document ) {
297 return;
298 }
299
300 // `onErrorFnPrev` initialized at top of scope
301 // Preserve other handlers
302 var onErrorFnPrev = window.onerror;
303
304 // Cover uncaught exceptions
305 // Returning true will suppress the default browser handler,
306 // returning false will let it run.
307 window.onerror = function( error, filePath, linerNr ) {
308 var ret = false;
309 if ( onErrorFnPrev ) {
310 ret = onErrorFnPrev( error, filePath, linerNr );
311 }
312
313 // Treat return value as window.onerror itself does,
314 // Only do our handling if not suppressed.
315 if ( ret !== true ) {
316 if ( QUnit.config.current ) {
317 if ( QUnit.config.current.ignoreGlobalErrors ) {
318 return true;
319 }
320 QUnit.pushFailure( error, filePath + ":" + linerNr );
321 } else {
322 QUnit.test( "global failure", extend( function() {
323 QUnit.pushFailure( error, filePath + ":" + linerNr );
324 }, { validTest: true } ) );
325 }
326 return false;
327 }
328
329 return ret;
330 };
331 }() );
332
333 // Figure out if we're running the tests from a server or not
334 QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" );
335
336 // Expose the current QUnit version
337 QUnit.version = "2.0.1";
338
339 extend( QUnit, {
340
341 // Call on start of module test to prepend name to all tests
342 module: function( name, testEnvironment, executeNow ) {
343 var module, moduleFns;
344 var currentModule = config.currentModule;
345
346 if ( arguments.length === 2 ) {
347 if ( objectType( testEnvironment ) === "function" ) {
348 executeNow = testEnvironment;
349 testEnvironment = undefined;
350 }
351 }
352
353 module = createModule();
354
355 if ( testEnvironment && ( testEnvironment.setup || testEnvironment.teardown ) ) {
356 console.warn(
357 "Module's `setup` and `teardown` are not hooks anymore on QUnit 2.0, use " +
358 "`beforeEach` and `afterEach` instead\n" +
359 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
360 );
361 }
362
363 moduleFns = {
364 before: setHook( module, "before" ),
365 beforeEach: setHook( module, "beforeEach" ),
366 afterEach: setHook( module, "afterEach" ),
367 after: setHook( module, "after" )
368 };
369
370 if ( objectType( executeNow ) === "function" ) {
371 config.moduleStack.push( module );
372 setCurrentModule( module );
373 executeNow.call( module.testEnvironment, moduleFns );
374 config.moduleStack.pop();
375 module = module.parentModule || currentModule;
376 }
377
378 setCurrentModule( module );
379
380 function createModule() {
381 var parentModule = config.moduleStack.length ?
382 config.moduleStack.slice( -1 )[ 0 ] : null;
383 var moduleName = parentModule !== null ?
384 [ parentModule.name, name ].join( " > " ) : name;
385 var module = {
386 name: moduleName,
387 parentModule: parentModule,
388 tests: [],
389 moduleId: generateHash( moduleName ),
390 testsRun: 0
391 };
392
393 var env = {};
394 if ( parentModule ) {
395 parentModule.childModule = module;
396 extend( env, parentModule.testEnvironment );
397 delete env.beforeEach;
398 delete env.afterEach;
399 }
400 extend( env, testEnvironment );
401 module.testEnvironment = env;
402
403 config.modules.push( module );
404 return module;
405 }
406
407 function setCurrentModule( module ) {
408 config.currentModule = module;
409 }
410
411 },
412
413 test: test,
414
415 skip: skip,
416
417 only: only,
418
419 start: function( count ) {
420 var globalStartAlreadyCalled = globalStartCalled;
421
422 if ( !config.current ) {
423 globalStartCalled = true;
424
425 if ( runStarted ) {
426 throw new Error( "Called start() while test already started running" );
427 } else if ( globalStartAlreadyCalled || count > 1 ) {
428 throw new Error( "Called start() outside of a test context too many times" );
429 } else if ( config.autostart ) {
430 throw new Error( "Called start() outside of a test context when " +
431 "QUnit.config.autostart was true" );
432 } else if ( !config.pageLoaded ) {
433
434 // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
435 config.autostart = true;
436 return;
437 }
438 } else {
439 throw new Error(
440 "QUnit.start cannot be called inside a test context. This feature is removed in " +
441 "QUnit 2.0. For async tests, use QUnit.test() with assert.async() instead.\n" +
442 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
443 );
444 }
445
446 scheduleBegin();
447 },
448
449 config: config,
450
451 is: is,
452
453 objectType: objectType,
454
455 extend: extend,
456
457 load: function() {
458 config.pageLoaded = true;
459
460 // Initialize the configuration options
461 extend( config, {
462 stats: { all: 0, bad: 0 },
463 moduleStats: { all: 0, bad: 0 },
464 started: 0,
465 updateRate: 1000,
466 autostart: true,
467 filter: ""
468 }, true );
469
470 if ( !runStarted ) {
471 config.blocking = false;
472
473 if ( config.autostart ) {
474 scheduleBegin();
475 }
476 }
477 },
478
479 stack: function( offset ) {
480 offset = ( offset || 0 ) + 2;
481 return sourceFromStacktrace( offset );
482 }
483 } );
484
485 registerLoggingCallbacks( QUnit );
486
487 function scheduleBegin() {
488
489 runStarted = true;
490
491 // Add a slight delay to allow definition of more modules and tests.
492 if ( defined.setTimeout ) {
493 setTimeout( function() {
494 begin();
495 }, 13 );
496 } else {
497 begin();
498 }
499 }
500
501 function begin() {
502 var i, l,
503 modulesLog = [];
504
505 // If the test run hasn't officially begun yet
506 if ( !config.started ) {
507
508 // Record the time of the test run's beginning
509 config.started = now();
510
511 // Delete the loose unnamed module if unused.
512 if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
513 config.modules.shift();
514 }
515
516 // Avoid unnecessary information by not logging modules' test environments
517 for ( i = 0, l = config.modules.length; i < l; i++ ) {
518 modulesLog.push( {
519 name: config.modules[ i ].name,
520 tests: config.modules[ i ].tests
521 } );
522 }
523
524 // The test run is officially beginning now
525 runLoggingCallbacks( "begin", {
526 totalTests: Test.count,
527 modules: modulesLog
528 } );
529 }
530
531 config.blocking = false;
532 process( true );
533 }
534
535 function process( last ) {
536 function next() {
537 process( last );
538 }
539 var start = now();
540 config.depth = ( config.depth || 0 ) + 1;
541
542 while ( config.queue.length && !config.blocking ) {
543 if ( !defined.setTimeout || config.updateRate <= 0 ||
544 ( ( now() - start ) < config.updateRate ) ) {
545 if ( config.current ) {
546
547 // Reset async tracking for each phase of the Test lifecycle
548 config.current.usedAsync = false;
549 }
550 config.queue.shift()();
551 } else {
552 setTimeout( next, 13 );
553 break;
554 }
555 }
556 config.depth--;
557 if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
558 done();
559 }
560 }
561
562 function done() {
563 var runtime, passed;
564
565 autorun = true;
566
567 // Log the last module results
568 if ( config.previousModule ) {
569 runLoggingCallbacks( "moduleDone", {
570 name: config.previousModule.name,
571 tests: config.previousModule.tests,
572 failed: config.moduleStats.bad,
573 passed: config.moduleStats.all - config.moduleStats.bad,
574 total: config.moduleStats.all,
575 runtime: now() - config.moduleStats.started
576 } );
577 }
578 delete config.previousModule;
579
580 runtime = now() - config.started;
581 passed = config.stats.all - config.stats.bad;
582
583 runLoggingCallbacks( "done", {
584 failed: config.stats.bad,
585 passed: passed,
586 total: config.stats.all,
587 runtime: runtime
588 } );
589 }
590
591 function setHook( module, hookName ) {
592 if ( module.testEnvironment === undefined ) {
593 module.testEnvironment = {};
594 }
595
596 return function( callback ) {
597 module.testEnvironment[ hookName ] = callback;
598 };
599 }
600
601 var unitSampler,
602 focused = false,
603 priorityCount = 0;
604
605 function Test( settings ) {
606 var i, l;
607
608 ++Test.count;
609
610 this.expected = null;
611 extend( this, settings );
612 this.assertions = [];
613 this.semaphore = 0;
614 this.usedAsync = false;
615 this.module = config.currentModule;
616 this.stack = sourceFromStacktrace( 3 );
617
618 // Register unique strings
619 for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
620 if ( this.module.tests[ i ].name === this.testName ) {
621 this.testName += " ";
622 }
623 }
624
625 this.testId = generateHash( this.module.name, this.testName );
626
627 this.module.tests.push( {
628 name: this.testName,
629 testId: this.testId
630 } );
631
632 if ( settings.skip ) {
633
634 // Skipped tests will fully ignore any sent callback
635 this.callback = function() {};
636 this.async = false;
637 this.expected = 0;
638 } else {
639 this.assert = new Assert( this );
640 }
641 }
642
643 Test.count = 0;
644
645 Test.prototype = {
646 before: function() {
647 if (
648
649 // Emit moduleStart when we're switching from one module to another
650 this.module !== config.previousModule ||
651
652 // They could be equal (both undefined) but if the previousModule property doesn't
653 // yet exist it means this is the first test in a suite that isn't wrapped in a
654 // module, in which case we'll just emit a moduleStart event for 'undefined'.
655 // Without this, reporters can get testStart before moduleStart which is a problem.
656 !hasOwn.call( config, "previousModule" )
657 ) {
658 if ( hasOwn.call( config, "previousModule" ) ) {
659 runLoggingCallbacks( "moduleDone", {
660 name: config.previousModule.name,
661 tests: config.previousModule.tests,
662 failed: config.moduleStats.bad,
663 passed: config.moduleStats.all - config.moduleStats.bad,
664 total: config.moduleStats.all,
665 runtime: now() - config.moduleStats.started
666 } );
667 }
668 config.previousModule = this.module;
669 config.moduleStats = { all: 0, bad: 0, started: now() };
670 runLoggingCallbacks( "moduleStart", {
671 name: this.module.name,
672 tests: this.module.tests
673 } );
674 }
675
676 config.current = this;
677
678 if ( this.module.testEnvironment ) {
679 delete this.module.testEnvironment.before;
680 delete this.module.testEnvironment.beforeEach;
681 delete this.module.testEnvironment.afterEach;
682 delete this.module.testEnvironment.after;
683 }
684 this.testEnvironment = extend( {}, this.module.testEnvironment );
685
686 this.started = now();
687 runLoggingCallbacks( "testStart", {
688 name: this.testName,
689 module: this.module.name,
690 testId: this.testId
691 } );
692
693 if ( !config.pollution ) {
694 saveGlobal();
695 }
696 },
697
698 run: function() {
699 var promise;
700
701 config.current = this;
702
703 this.callbackStarted = now();
704
705 if ( config.notrycatch ) {
706 runTest( this );
707 return;
708 }
709
710 try {
711 runTest( this );
712 } catch ( e ) {
713 this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
714 this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
715
716 // Else next test will carry the responsibility
717 saveGlobal();
718
719 // Restart the tests if they're blocking
720 if ( config.blocking ) {
721 internalRecover( this );
722 }
723 }
724
725 function runTest( test ) {
726 promise = test.callback.call( test.testEnvironment, test.assert );
727 test.resolvePromise( promise );
728 }
729 },
730
731 after: function() {
732 checkPollution();
733 },
734
735 queueHook: function( hook, hookName, hookOwner ) {
736 var promise,
737 test = this;
738 return function runHook() {
739 if ( hookName === "before" ) {
740 if ( hookOwner.testsRun !== 0 ) {
741 return;
742 }
743
744 test.preserveEnvironment = true;
745 }
746
747 if ( hookName === "after" && hookOwner.testsRun !== numberOfTests( hookOwner ) - 1 ) {
748 return;
749 }
750
751 config.current = test;
752 if ( config.notrycatch ) {
753 callHook();
754 return;
755 }
756 try {
757 callHook();
758 } catch ( error ) {
759 test.pushFailure( hookName + " failed on " + test.testName + ": " +
760 ( error.message || error ), extractStacktrace( error, 0 ) );
761 }
762
763 function callHook() {
764 promise = hook.call( test.testEnvironment, test.assert );
765 test.resolvePromise( promise, hookName );
766 }
767 };
768 },
769
770 // Currently only used for module level hooks, can be used to add global level ones
771 hooks: function( handler ) {
772 var hooks = [];
773
774 function processHooks( test, module ) {
775 if ( module.parentModule ) {
776 processHooks( test, module.parentModule );
777 }
778 if ( module.testEnvironment &&
779 QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) {
780 hooks.push( test.queueHook( module.testEnvironment[ handler ], handler, module ) );
781 }
782 }
783
784 // Hooks are ignored on skipped tests
785 if ( !this.skip ) {
786 processHooks( this, this.module );
787 }
788 return hooks;
789 },
790
791 finish: function() {
792 config.current = this;
793 if ( config.requireExpects && this.expected === null ) {
794 this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
795 "not called.", this.stack );
796 } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
797 this.pushFailure( "Expected " + this.expected + " assertions, but " +
798 this.assertions.length + " were run", this.stack );
799 } else if ( this.expected === null && !this.assertions.length ) {
800 this.pushFailure( "Expected at least one assertion, but none were run - call " +
801 "expect(0) to accept zero assertions.", this.stack );
802 }
803
804 var i,
805 skipped = !!this.skip,
806 bad = 0;
807
808 this.runtime = now() - this.started;
809
810 config.stats.all += this.assertions.length;
811 config.moduleStats.all += this.assertions.length;
812
813 for ( i = 0; i < this.assertions.length; i++ ) {
814 if ( !this.assertions[ i ].result ) {
815 bad++;
816 config.stats.bad++;
817 config.moduleStats.bad++;
818 }
819 }
820
821 notifyTestsRan( this.module );
822 runLoggingCallbacks( "testDone", {
823 name: this.testName,
824 module: this.module.name,
825 skipped: skipped,
826 failed: bad,
827 passed: this.assertions.length - bad,
828 total: this.assertions.length,
829 runtime: skipped ? 0 : this.runtime,
830
831 // HTML Reporter use
832 assertions: this.assertions,
833 testId: this.testId,
834
835 // Source of Test
836 source: this.stack
837 } );
838
839 config.current = undefined;
840 },
841
842 preserveTestEnvironment: function() {
843 if ( this.preserveEnvironment ) {
844 this.module.testEnvironment = this.testEnvironment;
845 this.testEnvironment = extend( {}, this.module.testEnvironment );
846 }
847 },
848
849 queue: function() {
850 var priority,
851 test = this;
852
853 if ( !this.valid() ) {
854 return;
855 }
856
857 function run() {
858
859 // Each of these can by async
860 synchronize( [
861 function() {
862 test.before();
863 },
864
865 test.hooks( "before" ),
866
867 function() {
868 test.preserveTestEnvironment();
869 },
870
871 test.hooks( "beforeEach" ),
872
873 function() {
874 test.run();
875 },
876
877 test.hooks( "afterEach" ).reverse(),
878 test.hooks( "after" ).reverse(),
879
880 function() {
881 test.after();
882 },
883
884 function() {
885 test.finish();
886 }
887 ] );
888 }
889
890 // Prioritize previously failed tests, detected from sessionStorage
891 priority = QUnit.config.reorder && defined.sessionStorage &&
892 +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
893
894 return synchronize( run, priority, config.seed );
895 },
896
897 pushResult: function( resultInfo ) {
898
899 // Destructure of resultInfo = { result, actual, expected, message, negative }
900 var source,
901 details = {
902 module: this.module.name,
903 name: this.testName,
904 result: resultInfo.result,
905 message: resultInfo.message,
906 actual: resultInfo.actual,
907 expected: resultInfo.expected,
908 testId: this.testId,
909 negative: resultInfo.negative || false,
910 runtime: now() - this.started
911 };
912
913 if ( !resultInfo.result ) {
914 source = sourceFromStacktrace();
915
916 if ( source ) {
917 details.source = source;
918 }
919 }
920
921 runLoggingCallbacks( "log", details );
922
923 this.assertions.push( {
924 result: !!resultInfo.result,
925 message: resultInfo.message
926 } );
927 },
928
929 pushFailure: function( message, source, actual ) {
930 if ( !( this instanceof Test ) ) {
931 throw new Error( "pushFailure() assertion outside test context, was " +
932 sourceFromStacktrace( 2 ) );
933 }
934
935 var details = {
936 module: this.module.name,
937 name: this.testName,
938 result: false,
939 message: message || "error",
940 actual: actual || null,
941 testId: this.testId,
942 runtime: now() - this.started
943 };
944
945 if ( source ) {
946 details.source = source;
947 }
948
949 runLoggingCallbacks( "log", details );
950
951 this.assertions.push( {
952 result: false,
953 message: message
954 } );
955 },
956
957 resolvePromise: function( promise, phase ) {
958 var then, resume, message,
959 test = this;
960 if ( promise != null ) {
961 then = promise.then;
962 if ( QUnit.objectType( then ) === "function" ) {
963 resume = internalStop( test );
964 then.call(
965 promise,
966 function() { resume(); },
967 function( error ) {
968 message = "Promise rejected " +
969 ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
970 " " + test.testName + ": " + ( error.message || error );
971 test.pushFailure( message, extractStacktrace( error, 0 ) );
972
973 // Else next test will carry the responsibility
974 saveGlobal();
975
976 // Unblock
977 resume();
978 }
979 );
980 }
981 }
982 },
983
984 valid: function() {
985 var filter = config.filter,
986 regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ),
987 module = config.module && config.module.toLowerCase(),
988 fullName = ( this.module.name + ": " + this.testName );
989
990 function moduleChainNameMatch( testModule ) {
991 var testModuleName = testModule.name ? testModule.name.toLowerCase() : null;
992 if ( testModuleName === module ) {
993 return true;
994 } else if ( testModule.parentModule ) {
995 return moduleChainNameMatch( testModule.parentModule );
996 } else {
997 return false;
998 }
999 }
1000
1001 function moduleChainIdMatch( testModule ) {
1002 return inArray( testModule.moduleId, config.moduleId ) > -1 ||
1003 testModule.parentModule && moduleChainIdMatch( testModule.parentModule );
1004 }
1005
1006 // Internally-generated tests are always valid
1007 if ( this.callback && this.callback.validTest ) {
1008 return true;
1009 }
1010
1011 if ( config.moduleId && config.moduleId.length > 0 &&
1012 !moduleChainIdMatch( this.module ) ) {
1013
1014 return false;
1015 }
1016
1017 if ( config.testId && config.testId.length > 0 &&
1018 inArray( this.testId, config.testId ) < 0 ) {
1019
1020 return false;
1021 }
1022
1023 if ( module && !moduleChainNameMatch( this.module ) ) {
1024 return false;
1025 }
1026
1027 if ( !filter ) {
1028 return true;
1029 }
1030
1031 return regexFilter ?
1032 this.regexFilter( !!regexFilter[ 1 ], regexFilter[ 2 ], regexFilter[ 3 ], fullName ) :
1033 this.stringFilter( filter, fullName );
1034 },
1035
1036 regexFilter: function( exclude, pattern, flags, fullName ) {
1037 var regex = new RegExp( pattern, flags );
1038 var match = regex.test( fullName );
1039
1040 return match !== exclude;
1041 },
1042
1043 stringFilter: function( filter, fullName ) {
1044 filter = filter.toLowerCase();
1045 fullName = fullName.toLowerCase();
1046
1047 var include = filter.charAt( 0 ) !== "!";
1048 if ( !include ) {
1049 filter = filter.slice( 1 );
1050 }
1051
1052 // If the filter matches, we need to honour include
1053 if ( fullName.indexOf( filter ) !== -1 ) {
1054 return include;
1055 }
1056
1057 // Otherwise, do the opposite
1058 return !include;
1059 }
1060 };
1061
1062 QUnit.pushFailure = function() {
1063 if ( !QUnit.config.current ) {
1064 throw new Error( "pushFailure() assertion outside test context, in " +
1065 sourceFromStacktrace( 2 ) );
1066 }
1067
1068 // Gets current test obj
1069 var currentTest = QUnit.config.current;
1070
1071 return currentTest.pushFailure.apply( currentTest, arguments );
1072 };
1073
1074 // Based on Java's String.hashCode, a simple but not
1075 // rigorously collision resistant hashing function
1076 function generateHash( module, testName ) {
1077 var hex,
1078 i = 0,
1079 hash = 0,
1080 str = module + "\x1C" + testName,
1081 len = str.length;
1082
1083 for ( ; i < len; i++ ) {
1084 hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
1085 hash |= 0;
1086 }
1087
1088 // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
1089 // strictly necessary but increases user understanding that the id is a SHA-like hash
1090 hex = ( 0x100000000 + hash ).toString( 16 );
1091 if ( hex.length < 8 ) {
1092 hex = "0000000" + hex;
1093 }
1094
1095 return hex.slice( -8 );
1096 }
1097
1098 function synchronize( callback, priority, seed ) {
1099 var last = !priority,
1100 index;
1101
1102 if ( QUnit.objectType( callback ) === "array" ) {
1103 while ( callback.length ) {
1104 synchronize( callback.shift() );
1105 }
1106 return;
1107 }
1108
1109 if ( priority ) {
1110 config.queue.splice( priorityCount++, 0, callback );
1111 } else if ( seed ) {
1112 if ( !unitSampler ) {
1113 unitSampler = unitSamplerGenerator( seed );
1114 }
1115
1116 // Insert into a random position after all priority items
1117 index = Math.floor( unitSampler() * ( config.queue.length - priorityCount + 1 ) );
1118 config.queue.splice( priorityCount + index, 0, callback );
1119 } else {
1120 config.queue.push( callback );
1121 }
1122
1123 if ( autorun && !config.blocking ) {
1124 process( last );
1125 }
1126 }
1127
1128 function unitSamplerGenerator( seed ) {
1129
1130 // 32-bit xorshift, requires only a nonzero seed
1131 // http://excamera.com/sphinx/article-xorshift.html
1132 var sample = parseInt( generateHash( seed ), 16 ) || -1;
1133 return function() {
1134 sample ^= sample << 13;
1135 sample ^= sample >>> 17;
1136 sample ^= sample << 5;
1137
1138 // ECMAScript has no unsigned number type
1139 if ( sample < 0 ) {
1140 sample += 0x100000000;
1141 }
1142
1143 return sample / 0x100000000;
1144 };
1145 }
1146
1147 function saveGlobal() {
1148 config.pollution = [];
1149
1150 if ( config.noglobals ) {
1151 for ( var key in global ) {
1152 if ( hasOwn.call( global, key ) ) {
1153
1154 // In Opera sometimes DOM element ids show up here, ignore them
1155 if ( /^qunit-test-output/.test( key ) ) {
1156 continue;
1157 }
1158 config.pollution.push( key );
1159 }
1160 }
1161 }
1162 }
1163
1164 function checkPollution() {
1165 var newGlobals,
1166 deletedGlobals,
1167 old = config.pollution;
1168
1169 saveGlobal();
1170
1171 newGlobals = diff( config.pollution, old );
1172 if ( newGlobals.length > 0 ) {
1173 QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
1174 }
1175
1176 deletedGlobals = diff( old, config.pollution );
1177 if ( deletedGlobals.length > 0 ) {
1178 QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
1179 }
1180 }
1181
1182 // Will be exposed as QUnit.test
1183 function test( testName, callback ) {
1184 if ( focused ) { return; }
1185
1186 var newTest;
1187
1188 newTest = new Test( {
1189 testName: testName,
1190 callback: callback
1191 } );
1192
1193 newTest.queue();
1194 }
1195
1196 // Will be exposed as QUnit.skip
1197 function skip( testName ) {
1198 if ( focused ) { return; }
1199
1200 var test = new Test( {
1201 testName: testName,
1202 skip: true
1203 } );
1204
1205 test.queue();
1206 }
1207
1208 // Will be exposed as QUnit.only
1209 function only( testName, callback ) {
1210 var newTest;
1211
1212 if ( focused ) { return; }
1213
1214 QUnit.config.queue.length = 0;
1215 focused = true;
1216
1217 newTest = new Test( {
1218 testName: testName,
1219 callback: callback
1220 } );
1221
1222 newTest.queue();
1223 }
1224
1225 // Put a hold on processing and return a function that will release it.
1226 function internalStop( test ) {
1227 var released = false;
1228
1229 test.semaphore += 1;
1230 config.blocking = true;
1231
1232 // Set a recovery timeout, if so configured.
1233 if ( config.testTimeout && defined.setTimeout ) {
1234 clearTimeout( config.timeout );
1235 config.timeout = setTimeout( function() {
1236 QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
1237 internalRecover( test );
1238 }, config.testTimeout );
1239 }
1240
1241 return function resume() {
1242 if ( released ) {
1243 return;
1244 }
1245
1246 released = true;
1247 test.semaphore -= 1;
1248 internalStart( test );
1249 };
1250 }
1251
1252 // Forcefully release all processing holds.
1253 function internalRecover( test ) {
1254 test.semaphore = 0;
1255 internalStart( test );
1256 }
1257
1258 // Release a processing hold, scheduling a resumption attempt if no holds remain.
1259 function internalStart( test ) {
1260
1261 // If semaphore is non-numeric, throw error
1262 if ( isNaN( test.semaphore ) ) {
1263 test.semaphore = 0;
1264
1265 QUnit.pushFailure(
1266 "Invalid value on test.semaphore",
1267 sourceFromStacktrace( 2 )
1268 );
1269 return;
1270 }
1271
1272 // Don't start until equal number of stop-calls
1273 if ( test.semaphore > 0 ) {
1274 return;
1275 }
1276
1277 // Throw an Error if start is called more often than stop
1278 if ( test.semaphore < 0 ) {
1279 test.semaphore = 0;
1280
1281 QUnit.pushFailure(
1282 "Tried to restart test while already started (test's semaphore was 0 already)",
1283 sourceFromStacktrace( 2 )
1284 );
1285 return;
1286 }
1287
1288 // Add a slight delay to allow more assertions etc.
1289 if ( defined.setTimeout ) {
1290 if ( config.timeout ) {
1291 clearTimeout( config.timeout );
1292 }
1293 config.timeout = setTimeout( function() {
1294 if ( test.semaphore > 0 ) {
1295 return;
1296 }
1297
1298 if ( config.timeout ) {
1299 clearTimeout( config.timeout );
1300 }
1301
1302 begin();
1303 }, 13 );
1304 } else {
1305 begin();
1306 }
1307 }
1308
1309 function numberOfTests( module ) {
1310 var count = module.tests.length;
1311 while ( module = module.childModule ) {
1312 count += module.tests.length;
1313 }
1314 return count;
1315 }
1316
1317 function notifyTestsRan( module ) {
1318 module.testsRun++;
1319 while ( module = module.parentModule ) {
1320 module.testsRun++;
1321 }
1322 }
1323
1324 function Assert( testContext ) {
1325 this.test = testContext;
1326 }
1327
1328 // Assert helpers
1329 QUnit.assert = Assert.prototype = {
1330
1331 // Specify the number of expected assertions to guarantee that failed test
1332 // (no assertions are run at all) don't slip through.
1333 expect: function( asserts ) {
1334 if ( arguments.length === 1 ) {
1335 this.test.expected = asserts;
1336 } else {
1337 return this.test.expected;
1338 }
1339 },
1340
1341 // Put a hold on processing and return a function that will release it a maximum of once.
1342 async: function( count ) {
1343 var resume,
1344 test = this.test,
1345 popped = false,
1346 acceptCallCount = count;
1347
1348 if ( typeof acceptCallCount === "undefined" ) {
1349 acceptCallCount = 1;
1350 }
1351
1352 test.usedAsync = true;
1353 resume = internalStop( test );
1354
1355 return function done() {
1356
1357 if ( popped ) {
1358 test.pushFailure( "Too many calls to the `assert.async` callback",
1359 sourceFromStacktrace( 2 ) );
1360 return;
1361 }
1362 acceptCallCount -= 1;
1363 if ( acceptCallCount > 0 ) {
1364 return;
1365 }
1366
1367 popped = true;
1368 resume();
1369 };
1370 },
1371
1372 // Exports test.push() to the user API
1373 // Alias of pushResult.
1374 push: function( result, actual, expected, message, negative ) {
1375 var currentAssert = this instanceof Assert ? this : QUnit.config.current.assert;
1376 return currentAssert.pushResult( {
1377 result: result,
1378 actual: actual,
1379 expected: expected,
1380 message: message,
1381 negative: negative
1382 } );
1383 },
1384
1385 pushResult: function( resultInfo ) {
1386
1387 // Destructure of resultInfo = { result, actual, expected, message, negative }
1388 var assert = this,
1389 currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
1390
1391 // Backwards compatibility fix.
1392 // Allows the direct use of global exported assertions and QUnit.assert.*
1393 // Although, it's use is not recommended as it can leak assertions
1394 // to other tests from async tests, because we only get a reference to the current test,
1395 // not exactly the test where assertion were intended to be called.
1396 if ( !currentTest ) {
1397 throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
1398 }
1399
1400 if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
1401 currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
1402 sourceFromStacktrace( 2 ) );
1403
1404 // Allow this assertion to continue running anyway...
1405 }
1406
1407 if ( !( assert instanceof Assert ) ) {
1408 assert = currentTest.assert;
1409 }
1410
1411 return assert.test.pushResult( resultInfo );
1412 },
1413
1414 ok: function( result, message ) {
1415 message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
1416 QUnit.dump.parse( result ) );
1417 this.pushResult( {
1418 result: !!result,
1419 actual: result,
1420 expected: true,
1421 message: message
1422 } );
1423 },
1424
1425 notOk: function( result, message ) {
1426 message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
1427 QUnit.dump.parse( result ) );
1428 this.pushResult( {
1429 result: !result,
1430 actual: result,
1431 expected: false,
1432 message: message
1433 } );
1434 },
1435
1436 equal: function( actual, expected, message ) {
1437 /*jshint eqeqeq:false */
1438 this.pushResult( {
1439 result: expected == actual,
1440 actual: actual,
1441 expected: expected,
1442 message: message
1443 } );
1444 },
1445
1446 notEqual: function( actual, expected, message ) {
1447 /*jshint eqeqeq:false */
1448 this.pushResult( {
1449 result: expected != actual,
1450 actual: actual,
1451 expected: expected,
1452 message: message,
1453 negative: true
1454 } );
1455 },
1456
1457 propEqual: function( actual, expected, message ) {
1458 actual = objectValues( actual );
1459 expected = objectValues( expected );
1460 this.pushResult( {
1461 result: QUnit.equiv( actual, expected ),
1462 actual: actual,
1463 expected: expected,
1464 message: message
1465 } );
1466 },
1467
1468 notPropEqual: function( actual, expected, message ) {
1469 actual = objectValues( actual );
1470 expected = objectValues( expected );
1471 this.pushResult( {
1472 result: !QUnit.equiv( actual, expected ),
1473 actual: actual,
1474 expected: expected,
1475 message: message,
1476 negative: true
1477 } );
1478 },
1479
1480 deepEqual: function( actual, expected, message ) {
1481 this.pushResult( {
1482 result: QUnit.equiv( actual, expected ),
1483 actual: actual,
1484 expected: expected,
1485 message: message
1486 } );
1487 },
1488
1489 notDeepEqual: function( actual, expected, message ) {
1490 this.pushResult( {
1491 result: !QUnit.equiv( actual, expected ),
1492 actual: actual,
1493 expected: expected,
1494 message: message,
1495 negative: true
1496 } );
1497 },
1498
1499 strictEqual: function( actual, expected, message ) {
1500 this.pushResult( {
1501 result: expected === actual,
1502 actual: actual,
1503 expected: expected,
1504 message: message
1505 } );
1506 },
1507
1508 notStrictEqual: function( actual, expected, message ) {
1509 this.pushResult( {
1510 result: expected !== actual,
1511 actual: actual,
1512 expected: expected,
1513 message: message,
1514 negative: true
1515 } );
1516 },
1517
1518 "throws": function( block, expected, message ) {
1519 var actual, expectedType,
1520 expectedOutput = expected,
1521 ok = false,
1522 currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
1523
1524 // 'expected' is optional unless doing string comparison
1525 if ( QUnit.objectType( expected ) === "string" ) {
1526 if ( message == null ) {
1527 message = expected;
1528 expected = null;
1529 } else {
1530 throw new Error(
1531 "throws/raises does not accept a string value for the expected argument.\n" +
1532 "Use a non-string object value (e.g. regExp) instead if it's necessary." +
1533 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
1534 );
1535 }
1536 }
1537
1538 currentTest.ignoreGlobalErrors = true;
1539 try {
1540 block.call( currentTest.testEnvironment );
1541 } catch ( e ) {
1542 actual = e;
1543 }
1544 currentTest.ignoreGlobalErrors = false;
1545
1546 if ( actual ) {
1547 expectedType = QUnit.objectType( expected );
1548
1549 // We don't want to validate thrown error
1550 if ( !expected ) {
1551 ok = true;
1552 expectedOutput = null;
1553
1554 // Expected is a regexp
1555 } else if ( expectedType === "regexp" ) {
1556 ok = expected.test( errorString( actual ) );
1557
1558 // Expected is a constructor, maybe an Error constructor
1559 } else if ( expectedType === "function" && actual instanceof expected ) {
1560 ok = true;
1561
1562 // Expected is an Error object
1563 } else if ( expectedType === "object" ) {
1564 ok = actual instanceof expected.constructor &&
1565 actual.name === expected.name &&
1566 actual.message === expected.message;
1567
1568 // Expected is a validation function which returns true if validation passed
1569 } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
1570 expectedOutput = null;
1571 ok = true;
1572 }
1573 }
1574
1575 currentTest.assert.pushResult( {
1576 result: ok,
1577 actual: actual,
1578 expected: expectedOutput,
1579 message: message
1580 } );
1581 }
1582 };
1583
1584 // Provide an alternative to assert.throws(), for environments that consider throws a reserved word
1585 // Known to us are: Closure Compiler, Narwhal
1586 ( function() {
1587 /*jshint sub:true */
1588 Assert.prototype.raises = Assert.prototype [ "throws" ]; //jscs:ignore requireDotNotation
1589 }() );
1590
1591 function errorString( error ) {
1592 var name, message,
1593 resultErrorString = error.toString();
1594 if ( resultErrorString.substring( 0, 7 ) === "[object" ) {
1595 name = error.name ? error.name.toString() : "Error";
1596 message = error.message ? error.message.toString() : "";
1597 if ( name && message ) {
1598 return name + ": " + message;
1599 } else if ( name ) {
1600 return name;
1601 } else if ( message ) {
1602 return message;
1603 } else {
1604 return "Error";
1605 }
1606 } else {
1607 return resultErrorString;
1608 }
1609 }
1610
1611 // Test for equality any JavaScript type.
1612 // Author: Philippe Rathé <prathe@gmail.com>
1613 QUnit.equiv = ( function() {
1614
1615 // Stack to decide between skip/abort functions
1616 var callers = [];
1617
1618 // Stack to avoiding loops from circular referencing
1619 var parents = [];
1620 var parentsB = [];
1621
1622 var getProto = Object.getPrototypeOf || function( obj ) {
1623
1624 /*jshint proto: true */
1625 return obj.__proto__;
1626 };
1627
1628 function useStrictEquality( b, a ) {
1629
1630 // To catch short annotation VS 'new' annotation of a declaration. e.g.:
1631 // `var i = 1;`
1632 // `var j = new Number(1);`
1633 if ( typeof a === "object" ) {
1634 a = a.valueOf();
1635 }
1636 if ( typeof b === "object" ) {
1637 b = b.valueOf();
1638 }
1639
1640 return a === b;
1641 }
1642
1643 function compareConstructors( a, b ) {
1644 var protoA = getProto( a );
1645 var protoB = getProto( b );
1646
1647 // Comparing constructors is more strict than using `instanceof`
1648 if ( a.constructor === b.constructor ) {
1649 return true;
1650 }
1651
1652 // Ref #851
1653 // If the obj prototype descends from a null constructor, treat it
1654 // as a null prototype.
1655 if ( protoA && protoA.constructor === null ) {
1656 protoA = null;
1657 }
1658 if ( protoB && protoB.constructor === null ) {
1659 protoB = null;
1660 }
1661
1662 // Allow objects with no prototype to be equivalent to
1663 // objects with Object as their constructor.
1664 if ( ( protoA === null && protoB === Object.prototype ) ||
1665 ( protoB === null && protoA === Object.prototype ) ) {
1666 return true;
1667 }
1668
1669 return false;
1670 }
1671
1672 function getRegExpFlags( regexp ) {
1673 return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ];
1674 }
1675
1676 var callbacks = {
1677 "string": useStrictEquality,
1678 "boolean": useStrictEquality,
1679 "number": useStrictEquality,
1680 "null": useStrictEquality,
1681 "undefined": useStrictEquality,
1682 "symbol": useStrictEquality,
1683 "date": useStrictEquality,
1684
1685 "nan": function() {
1686 return true;
1687 },
1688
1689 "regexp": function( b, a ) {
1690 return a.source === b.source &&
1691
1692 // Include flags in the comparison
1693 getRegExpFlags( a ) === getRegExpFlags( b );
1694 },
1695
1696 // - skip when the property is a method of an instance (OOP)
1697 // - abort otherwise,
1698 // initial === would have catch identical references anyway
1699 "function": function() {
1700 var caller = callers[ callers.length - 1 ];
1701 return caller !== Object && typeof caller !== "undefined";
1702 },
1703
1704 "array": function( b, a ) {
1705 var i, j, len, loop, aCircular, bCircular;
1706
1707 len = a.length;
1708 if ( len !== b.length ) {
1709
1710 // Safe and faster
1711 return false;
1712 }
1713
1714 // Track reference to avoid circular references
1715 parents.push( a );
1716 parentsB.push( b );
1717 for ( i = 0; i < len; i++ ) {
1718 loop = false;
1719 for ( j = 0; j < parents.length; j++ ) {
1720 aCircular = parents[ j ] === a[ i ];
1721 bCircular = parentsB[ j ] === b[ i ];
1722 if ( aCircular || bCircular ) {
1723 if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1724 loop = true;
1725 } else {
1726 parents.pop();
1727 parentsB.pop();
1728 return false;
1729 }
1730 }
1731 }
1732 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1733 parents.pop();
1734 parentsB.pop();
1735 return false;
1736 }
1737 }
1738 parents.pop();
1739 parentsB.pop();
1740 return true;
1741 },
1742
1743 "set": function( b, a ) {
1744 var innerEq,
1745 outerEq = true;
1746
1747 if ( a.size !== b.size ) {
1748 return false;
1749 }
1750
1751 a.forEach( function( aVal ) {
1752 innerEq = false;
1753
1754 b.forEach( function( bVal ) {
1755 if ( innerEquiv( bVal, aVal ) ) {
1756 innerEq = true;
1757 }
1758 } );
1759
1760 if ( !innerEq ) {
1761 outerEq = false;
1762 }
1763 } );
1764
1765 return outerEq;
1766 },
1767
1768 "map": function( b, a ) {
1769 var innerEq,
1770 outerEq = true;
1771
1772 if ( a.size !== b.size ) {
1773 return false;
1774 }
1775
1776 a.forEach( function( aVal, aKey ) {
1777 innerEq = false;
1778
1779 b.forEach( function( bVal, bKey ) {
1780 if ( innerEquiv( [ bVal, bKey ], [ aVal, aKey ] ) ) {
1781 innerEq = true;
1782 }
1783 } );
1784
1785 if ( !innerEq ) {
1786 outerEq = false;
1787 }
1788 } );
1789
1790 return outerEq;
1791 },
1792
1793 "object": function( b, a ) {
1794 var i, j, loop, aCircular, bCircular;
1795
1796 // Default to true
1797 var eq = true;
1798 var aProperties = [];
1799 var bProperties = [];
1800
1801 if ( compareConstructors( a, b ) === false ) {
1802 return false;
1803 }
1804
1805 // Stack constructor before traversing properties
1806 callers.push( a.constructor );
1807
1808 // Track reference to avoid circular references
1809 parents.push( a );
1810 parentsB.push( b );
1811
1812 // Be strict: don't ensure hasOwnProperty and go deep
1813 for ( i in a ) {
1814 loop = false;
1815 for ( j = 0; j < parents.length; j++ ) {
1816 aCircular = parents[ j ] === a[ i ];
1817 bCircular = parentsB[ j ] === b[ i ];
1818 if ( aCircular || bCircular ) {
1819 if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1820 loop = true;
1821 } else {
1822 eq = false;
1823 break;
1824 }
1825 }
1826 }
1827 aProperties.push( i );
1828 if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1829 eq = false;
1830 break;
1831 }
1832 }
1833
1834 parents.pop();
1835 parentsB.pop();
1836
1837 // Unstack, we are done
1838 callers.pop();
1839
1840 for ( i in b ) {
1841
1842 // Collect b's properties
1843 bProperties.push( i );
1844 }
1845
1846 // Ensures identical properties name
1847 return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1848 }
1849 };
1850
1851 function typeEquiv( a, b ) {
1852 var type = QUnit.objectType( a );
1853 return QUnit.objectType( b ) === type && callbacks[ type ]( b, a );
1854 }
1855
1856 // The real equiv function
1857 function innerEquiv( a, b ) {
1858
1859 // We're done when there's nothing more to compare
1860 if ( arguments.length < 2 ) {
1861 return true;
1862 }
1863
1864 // Require type-specific equality
1865 return ( a === b || typeEquiv( a, b ) ) &&
1866
1867 // ...across all consecutive argument pairs
1868 ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) );
1869 }
1870
1871 return innerEquiv;
1872 }() );
1873
1874 // Based on jsDump by Ariel Flesler
1875 // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
1876 QUnit.dump = ( function() {
1877 function quote( str ) {
1878 return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\"";
1879 }
1880 function literal( o ) {
1881 return o + "";
1882 }
1883 function join( pre, arr, post ) {
1884 var s = dump.separator(),
1885 base = dump.indent(),
1886 inner = dump.indent( 1 );
1887 if ( arr.join ) {
1888 arr = arr.join( "," + s + inner );
1889 }
1890 if ( !arr ) {
1891 return pre + post;
1892 }
1893 return [ pre, inner + arr, base + post ].join( s );
1894 }
1895 function array( arr, stack ) {
1896 var i = arr.length,
1897 ret = new Array( i );
1898
1899 if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
1900 return "[object Array]";
1901 }
1902
1903 this.up();
1904 while ( i-- ) {
1905 ret[ i ] = this.parse( arr[ i ], undefined, stack );
1906 }
1907 this.down();
1908 return join( "[", ret, "]" );
1909 }
1910
1911 function isArray( obj ) {
1912 return (
1913
1914 //Native Arrays
1915 toString.call( obj ) === "[object Array]" ||
1916
1917 // NodeList objects
1918 ( typeof obj.length === "number" && obj.item !== undefined ) &&
1919 ( obj.length ?
1920 obj.item( 0 ) === obj[ 0 ] :
1921 ( obj.item( 0 ) === null && obj[ 0 ] === undefined )
1922 )
1923 );
1924 }
1925
1926 var reName = /^function (\w+)/,
1927 dump = {
1928
1929 // The objType is used mostly internally, you can fix a (custom) type in advance
1930 parse: function( obj, objType, stack ) {
1931 stack = stack || [];
1932 var res, parser, parserType,
1933 inStack = inArray( obj, stack );
1934
1935 if ( inStack !== -1 ) {
1936 return "recursion(" + ( inStack - stack.length ) + ")";
1937 }
1938
1939 objType = objType || this.typeOf( obj );
1940 parser = this.parsers[ objType ];
1941 parserType = typeof parser;
1942
1943 if ( parserType === "function" ) {
1944 stack.push( obj );
1945 res = parser.call( this, obj, stack );
1946 stack.pop();
1947 return res;
1948 }
1949 return ( parserType === "string" ) ? parser : this.parsers.error;
1950 },
1951 typeOf: function( obj ) {
1952 var type;
1953
1954 if ( obj === null ) {
1955 type = "null";
1956 } else if ( typeof obj === "undefined" ) {
1957 type = "undefined";
1958 } else if ( QUnit.is( "regexp", obj ) ) {
1959 type = "regexp";
1960 } else if ( QUnit.is( "date", obj ) ) {
1961 type = "date";
1962 } else if ( QUnit.is( "function", obj ) ) {
1963 type = "function";
1964 } else if ( obj.setInterval !== undefined &&
1965 obj.document !== undefined &&
1966 obj.nodeType === undefined ) {
1967 type = "window";
1968 } else if ( obj.nodeType === 9 ) {
1969 type = "document";
1970 } else if ( obj.nodeType ) {
1971 type = "node";
1972 } else if ( isArray( obj ) ) {
1973 type = "array";
1974 } else if ( obj.constructor === Error.prototype.constructor ) {
1975 type = "error";
1976 } else {
1977 type = typeof obj;
1978 }
1979 return type;
1980 },
1981
1982 separator: function() {
1983 return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? "&#160;" : " ";
1984 },
1985
1986 // Extra can be a number, shortcut for increasing-calling-decreasing
1987 indent: function( extra ) {
1988 if ( !this.multiline ) {
1989 return "";
1990 }
1991 var chr = this.indentChar;
1992 if ( this.HTML ) {
1993 chr = chr.replace( /\t/g, " " ).replace( / /g, "&#160;" );
1994 }
1995 return new Array( this.depth + ( extra || 0 ) ).join( chr );
1996 },
1997 up: function( a ) {
1998 this.depth += a || 1;
1999 },
2000 down: function( a ) {
2001 this.depth -= a || 1;
2002 },
2003 setParser: function( name, parser ) {
2004 this.parsers[ name ] = parser;
2005 },
2006
2007 // The next 3 are exposed so you can use them
2008 quote: quote,
2009 literal: literal,
2010 join: join,
2011 depth: 1,
2012 maxDepth: QUnit.config.maxDepth,
2013
2014 // This is the list of parsers, to modify them, use dump.setParser
2015 parsers: {
2016 window: "[Window]",
2017 document: "[Document]",
2018 error: function( error ) {
2019 return "Error(\"" + error.message + "\")";
2020 },
2021 unknown: "[Unknown]",
2022 "null": "null",
2023 "undefined": "undefined",
2024 "function": function( fn ) {
2025 var ret = "function",
2026
2027 // Functions never have name in IE
2028 name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
2029
2030 if ( name ) {
2031 ret += " " + name;
2032 }
2033 ret += "(";
2034
2035 ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
2036 return join( ret, dump.parse( fn, "functionCode" ), "}" );
2037 },
2038 array: array,
2039 nodelist: array,
2040 "arguments": array,
2041 object: function( map, stack ) {
2042 var keys, key, val, i, nonEnumerableProperties,
2043 ret = [];
2044
2045 if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
2046 return "[object Object]";
2047 }
2048
2049 dump.up();
2050 keys = [];
2051 for ( key in map ) {
2052 keys.push( key );
2053 }
2054
2055 // Some properties are not always enumerable on Error objects.
2056 nonEnumerableProperties = [ "message", "name" ];
2057 for ( i in nonEnumerableProperties ) {
2058 key = nonEnumerableProperties[ i ];
2059 if ( key in map && inArray( key, keys ) < 0 ) {
2060 keys.push( key );
2061 }
2062 }
2063 keys.sort();
2064 for ( i = 0; i < keys.length; i++ ) {
2065 key = keys[ i ];
2066 val = map[ key ];
2067 ret.push( dump.parse( key, "key" ) + ": " +
2068 dump.parse( val, undefined, stack ) );
2069 }
2070 dump.down();
2071 return join( "{", ret, "}" );
2072 },
2073 node: function( node ) {
2074 var len, i, val,
2075 open = dump.HTML ? "&lt;" : "<",
2076 close = dump.HTML ? "&gt;" : ">",
2077 tag = node.nodeName.toLowerCase(),
2078 ret = open + tag,
2079 attrs = node.attributes;
2080
2081 if ( attrs ) {
2082 for ( i = 0, len = attrs.length; i < len; i++ ) {
2083 val = attrs[ i ].nodeValue;
2084
2085 // IE6 includes all attributes in .attributes, even ones not explicitly
2086 // set. Those have values like undefined, null, 0, false, "" or
2087 // "inherit".
2088 if ( val && val !== "inherit" ) {
2089 ret += " " + attrs[ i ].nodeName + "=" +
2090 dump.parse( val, "attribute" );
2091 }
2092 }
2093 }
2094 ret += close;
2095
2096 // Show content of TextNode or CDATASection
2097 if ( node.nodeType === 3 || node.nodeType === 4 ) {
2098 ret += node.nodeValue;
2099 }
2100
2101 return ret + open + "/" + tag + close;
2102 },
2103
2104 // Function calls it internally, it's the arguments part of the function
2105 functionArgs: function( fn ) {
2106 var args,
2107 l = fn.length;
2108
2109 if ( !l ) {
2110 return "";
2111 }
2112
2113 args = new Array( l );
2114 while ( l-- ) {
2115
2116 // 97 is 'a'
2117 args[ l ] = String.fromCharCode( 97 + l );
2118 }
2119 return " " + args.join( ", " ) + " ";
2120 },
2121
2122 // Object calls it internally, the key part of an item in a map
2123 key: quote,
2124
2125 // Function calls it internally, it's the content of the function
2126 functionCode: "[code]",
2127
2128 // Node calls it internally, it's a html attribute value
2129 attribute: quote,
2130 string: quote,
2131 date: quote,
2132 regexp: literal,
2133 number: literal,
2134 "boolean": literal,
2135 symbol: function( sym ) {
2136 return sym.toString();
2137 }
2138 },
2139
2140 // If true, entities are escaped ( <, >, \t, space and \n )
2141 HTML: false,
2142
2143 // Indentation unit
2144 indentChar: " ",
2145
2146 // If true, items in a collection, are separated by a \n, else just a space.
2147 multiline: true
2148 };
2149
2150 return dump;
2151 }() );
2152
2153 // Back compat
2154 QUnit.jsDump = QUnit.dump;
2155
2156 function applyDeprecated( name ) {
2157 return function() {
2158 throw new Error(
2159 name + " is removed in QUnit 2.0.\n" +
2160 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
2161 );
2162 };
2163 }
2164
2165 Object.keys( Assert.prototype ).forEach( function( key ) {
2166 QUnit[ key ] = applyDeprecated( "`QUnit." + key + "`" );
2167 } );
2168
2169 QUnit.asyncTest = function() {
2170 throw new Error(
2171 "asyncTest is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" +
2172 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
2173 );
2174 };
2175
2176 QUnit.stop = function() {
2177 throw new Error(
2178 "QUnit.stop is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" +
2179 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
2180 );
2181 };
2182
2183 function resetThrower() {
2184 throw new Error(
2185 "QUnit.reset is removed in QUnit 2.0 without replacement.\n" +
2186 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
2187 );
2188 }
2189
2190 Object.defineProperty( QUnit, "reset", {
2191 get: function() {
2192 return resetThrower;
2193 },
2194 set: resetThrower
2195 } );
2196
2197 if ( defined.document ) {
2198 if ( window.QUnit ) {
2199 throw new Error( "QUnit has already been defined." );
2200 }
2201
2202 [
2203 "test",
2204 "module",
2205 "expect",
2206 "start",
2207 "ok",
2208 "notOk",
2209 "equal",
2210 "notEqual",
2211 "propEqual",
2212 "notPropEqual",
2213 "deepEqual",
2214 "notDeepEqual",
2215 "strictEqual",
2216 "notStrictEqual",
2217 "throws",
2218 "raises"
2219 ].forEach( function( key ) {
2220 window[ key ] = applyDeprecated( "The global `" + key + "`" );
2221 } );
2222
2223 window.QUnit = QUnit;
2224 }
2225
2226 // For nodejs
2227 if ( typeof module !== "undefined" && module && module.exports ) {
2228 module.exports = QUnit;
2229
2230 // For consistency with CommonJS environments' exports
2231 module.exports.QUnit = QUnit;
2232 }
2233
2234 // For CommonJS with exports, but without module.exports, like Rhino
2235 if ( typeof exports !== "undefined" && exports ) {
2236 exports.QUnit = QUnit;
2237 }
2238
2239 if ( typeof define === "function" && define.amd ) {
2240 define( function() {
2241 return QUnit;
2242 } );
2243 QUnit.config.autostart = false;
2244 }
2245
2246 // Get a reference to the global object, like window in browsers
2247 }( ( function() {
2248 return this;
2249 }() ) ) );
2250
2251 ( function() {
2252
2253 if ( typeof window === "undefined" || !window.document ) {
2254 return;
2255 }
2256
2257 var config = QUnit.config,
2258 hasOwn = Object.prototype.hasOwnProperty;
2259
2260 // Stores fixture HTML for resetting later
2261 function storeFixture() {
2262
2263 // Avoid overwriting user-defined values
2264 if ( hasOwn.call( config, "fixture" ) ) {
2265 return;
2266 }
2267
2268 var fixture = document.getElementById( "qunit-fixture" );
2269 if ( fixture ) {
2270 config.fixture = fixture.innerHTML;
2271 }
2272 }
2273
2274 QUnit.begin( storeFixture );
2275
2276 // Resets the fixture DOM element if available.
2277 function resetFixture() {
2278 if ( config.fixture == null ) {
2279 return;
2280 }
2281
2282 var fixture = document.getElementById( "qunit-fixture" );
2283 if ( fixture ) {
2284 fixture.innerHTML = config.fixture;
2285 }
2286 }
2287
2288 QUnit.testStart( resetFixture );
2289
2290 }() );
2291
2292 ( function() {
2293
2294 // Only interact with URLs via window.location
2295 var location = typeof window !== "undefined" && window.location;
2296 if ( !location ) {
2297 return;
2298 }
2299
2300 var urlParams = getUrlParams();
2301
2302 QUnit.urlParams = urlParams;
2303
2304 // Match module/test by inclusion in an array
2305 QUnit.config.moduleId = [].concat( urlParams.moduleId || [] );
2306 QUnit.config.testId = [].concat( urlParams.testId || [] );
2307
2308 // Exact case-insensitive match of the module name
2309 QUnit.config.module = urlParams.module;
2310
2311 // Regular expression or case-insenstive substring match against "moduleName: testName"
2312 QUnit.config.filter = urlParams.filter;
2313
2314 // Test order randomization
2315 if ( urlParams.seed === true ) {
2316
2317 // Generate a random seed if the option is specified without a value
2318 QUnit.config.seed = Math.random().toString( 36 ).slice( 2 );
2319 } else if ( urlParams.seed ) {
2320 QUnit.config.seed = urlParams.seed;
2321 }
2322
2323 // Add URL-parameter-mapped config values with UI form rendering data
2324 QUnit.config.urlConfig.push(
2325 {
2326 id: "hidepassed",
2327 label: "Hide passed tests",
2328 tooltip: "Only show tests and assertions that fail. Stored as query-strings."
2329 },
2330 {
2331 id: "noglobals",
2332 label: "Check for Globals",
2333 tooltip: "Enabling this will test if any test introduces new properties on the " +
2334 "global object (`window` in Browsers). Stored as query-strings."
2335 },
2336 {
2337 id: "notrycatch",
2338 label: "No try-catch",
2339 tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
2340 "exceptions in IE reasonable. Stored as query-strings."
2341 }
2342 );
2343
2344 QUnit.begin( function() {
2345 var i, option,
2346 urlConfig = QUnit.config.urlConfig;
2347
2348 for ( i = 0; i < urlConfig.length; i++ ) {
2349
2350 // Options can be either strings or objects with nonempty "id" properties
2351 option = QUnit.config.urlConfig[ i ];
2352 if ( typeof option !== "string" ) {
2353 option = option.id;
2354 }
2355
2356 if ( QUnit.config[ option ] === undefined ) {
2357 QUnit.config[ option ] = urlParams[ option ];
2358 }
2359 }
2360 } );
2361
2362 function getUrlParams() {
2363 var i, param, name, value;
2364 var urlParams = {};
2365 var params = location.search.slice( 1 ).split( "&" );
2366 var length = params.length;
2367
2368 for ( i = 0; i < length; i++ ) {
2369 if ( params[ i ] ) {
2370 param = params[ i ].split( "=" );
2371 name = decodeQueryParam( param[ 0 ] );
2372
2373 // Allow just a key to turn on a flag, e.g., test.html?noglobals
2374 value = param.length === 1 ||
2375 decodeQueryParam( param.slice( 1 ).join( "=" ) ) ;
2376 if ( urlParams[ name ] ) {
2377 urlParams[ name ] = [].concat( urlParams[ name ], value );
2378 } else {
2379 urlParams[ name ] = value;
2380 }
2381 }
2382 }
2383
2384 return urlParams;
2385 }
2386
2387 function decodeQueryParam( param ) {
2388 return decodeURIComponent( param.replace( /\+/g, "%20" ) );
2389 }
2390
2391 // Don't load the HTML Reporter on non-browser environments
2392 if ( typeof window === "undefined" || !window.document ) {
2393 return;
2394 }
2395
2396 QUnit.init = function() {
2397 throw new Error(
2398 "QUnit.init is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" +
2399 "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/"
2400 );
2401 };
2402
2403 var config = QUnit.config,
2404 document = window.document,
2405 collapseNext = false,
2406 hasOwn = Object.prototype.hasOwnProperty,
2407 unfilteredUrl = setUrl( { filter: undefined, module: undefined,
2408 moduleId: undefined, testId: undefined } ),
2409 defined = {
2410 sessionStorage: ( function() {
2411 var x = "qunit-test-string";
2412 try {
2413 sessionStorage.setItem( x, x );
2414 sessionStorage.removeItem( x );
2415 return true;
2416 } catch ( e ) {
2417 return false;
2418 }
2419 }() )
2420 },
2421 modulesList = [];
2422
2423 // Escape text for attribute or text content.
2424 function escapeText( s ) {
2425 if ( !s ) {
2426 return "";
2427 }
2428 s = s + "";
2429
2430 // Both single quotes and double quotes (for attributes)
2431 return s.replace( /['"<>&]/g, function( s ) {
2432 switch ( s ) {
2433 case "'":
2434 return "&#039;";
2435 case "\"":
2436 return "&quot;";
2437 case "<":
2438 return "&lt;";
2439 case ">":
2440 return "&gt;";
2441 case "&":
2442 return "&amp;";
2443 }
2444 } );
2445 }
2446
2447 function addEvent( elem, type, fn ) {
2448 elem.addEventListener( type, fn, false );
2449 }
2450
2451 function removeEvent( elem, type, fn ) {
2452 elem.removeEventListener( type, fn, false );
2453 }
2454
2455 function addEvents( elems, type, fn ) {
2456 var i = elems.length;
2457 while ( i-- ) {
2458 addEvent( elems[ i ], type, fn );
2459 }
2460 }
2461
2462 function hasClass( elem, name ) {
2463 return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0;
2464 }
2465
2466 function addClass( elem, name ) {
2467 if ( !hasClass( elem, name ) ) {
2468 elem.className += ( elem.className ? " " : "" ) + name;
2469 }
2470 }
2471
2472 function toggleClass( elem, name, force ) {
2473 if ( force || typeof force === "undefined" && !hasClass( elem, name ) ) {
2474 addClass( elem, name );
2475 } else {
2476 removeClass( elem, name );
2477 }
2478 }
2479
2480 function removeClass( elem, name ) {
2481 var set = " " + elem.className + " ";
2482
2483 // Class name may appear multiple times
2484 while ( set.indexOf( " " + name + " " ) >= 0 ) {
2485 set = set.replace( " " + name + " ", " " );
2486 }
2487
2488 // Trim for prettiness
2489 elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" );
2490 }
2491
2492 function id( name ) {
2493 return document.getElementById && document.getElementById( name );
2494 }
2495
2496 function interceptNavigation( ev ) {
2497 applyUrlParams();
2498
2499 if ( ev && ev.preventDefault ) {
2500 ev.preventDefault();
2501 }
2502
2503 return false;
2504 }
2505
2506 function getUrlConfigHtml() {
2507 var i, j, val,
2508 escaped, escapedTooltip,
2509 selection = false,
2510 urlConfig = config.urlConfig,
2511 urlConfigHtml = "";
2512
2513 for ( i = 0; i < urlConfig.length; i++ ) {
2514
2515 // Options can be either strings or objects with nonempty "id" properties
2516 val = config.urlConfig[ i ];
2517 if ( typeof val === "string" ) {
2518 val = {
2519 id: val,
2520 label: val
2521 };
2522 }
2523
2524 escaped = escapeText( val.id );
2525 escapedTooltip = escapeText( val.tooltip );
2526
2527 if ( !val.value || typeof val.value === "string" ) {
2528 urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
2529 "' title='" + escapedTooltip + "'><input id='qunit-urlconfig-" + escaped +
2530 "' name='" + escaped + "' type='checkbox'" +
2531 ( val.value ? " value='" + escapeText( val.value ) + "'" : "" ) +
2532 ( config[ val.id ] ? " checked='checked'" : "" ) +
2533 " title='" + escapedTooltip + "' />" + escapeText( val.label ) + "</label>";
2534 } else {
2535 urlConfigHtml += "<label for='qunit-urlconfig-" + escaped +
2536 "' title='" + escapedTooltip + "'>" + val.label +
2537 ": </label><select id='qunit-urlconfig-" + escaped +
2538 "' name='" + escaped + "' title='" + escapedTooltip + "'><option></option>";
2539
2540 if ( QUnit.is( "array", val.value ) ) {
2541 for ( j = 0; j < val.value.length; j++ ) {
2542 escaped = escapeText( val.value[ j ] );
2543 urlConfigHtml += "<option value='" + escaped + "'" +
2544 ( config[ val.id ] === val.value[ j ] ?
2545 ( selection = true ) && " selected='selected'" : "" ) +
2546 ">" + escaped + "</option>";
2547 }
2548 } else {
2549 for ( j in val.value ) {
2550 if ( hasOwn.call( val.value, j ) ) {
2551 urlConfigHtml += "<option value='" + escapeText( j ) + "'" +
2552 ( config[ val.id ] === j ?
2553 ( selection = true ) && " selected='selected'" : "" ) +
2554 ">" + escapeText( val.value[ j ] ) + "</option>";
2555 }
2556 }
2557 }
2558 if ( config[ val.id ] && !selection ) {
2559 escaped = escapeText( config[ val.id ] );
2560 urlConfigHtml += "<option value='" + escaped +
2561 "' selected='selected' disabled='disabled'>" + escaped + "</option>";
2562 }
2563 urlConfigHtml += "</select>";
2564 }
2565 }
2566
2567 return urlConfigHtml;
2568 }
2569
2570 // Handle "click" events on toolbar checkboxes and "change" for select menus.
2571 // Updates the URL with the new state of `config.urlConfig` values.
2572 function toolbarChanged() {
2573 var updatedUrl, value, tests,
2574 field = this,
2575 params = {};
2576
2577 // Detect if field is a select menu or a checkbox
2578 if ( "selectedIndex" in field ) {
2579 value = field.options[ field.selectedIndex ].value || undefined;
2580 } else {
2581 value = field.checked ? ( field.defaultValue || true ) : undefined;
2582 }
2583
2584 params[ field.name ] = value;
2585 updatedUrl = setUrl( params );
2586
2587 // Check if we can apply the change without a page refresh
2588 if ( "hidepassed" === field.name && "replaceState" in window.history ) {
2589 QUnit.urlParams[ field.name ] = value;
2590 config[ field.name ] = value || false;
2591 tests = id( "qunit-tests" );
2592 if ( tests ) {
2593 toggleClass( tests, "hidepass", value || false );
2594 }
2595 window.history.replaceState( null, "", updatedUrl );
2596 } else {
2597 window.location = updatedUrl;
2598 }
2599 }
2600
2601 function setUrl( params ) {
2602 var key, arrValue, i,
2603 querystring = "?",
2604 location = window.location;
2605
2606 params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params );
2607
2608 for ( key in params ) {
2609
2610 // Skip inherited or undefined properties
2611 if ( hasOwn.call( params, key ) && params[ key ] !== undefined ) {
2612
2613 // Output a parameter for each value of this key (but usually just one)
2614 arrValue = [].concat( params[ key ] );
2615 for ( i = 0; i < arrValue.length; i++ ) {
2616 querystring += encodeURIComponent( key );
2617 if ( arrValue[ i ] !== true ) {
2618 querystring += "=" + encodeURIComponent( arrValue[ i ] );
2619 }
2620 querystring += "&";
2621 }
2622 }
2623 }
2624 return location.protocol + "//" + location.host +
2625 location.pathname + querystring.slice( 0, -1 );
2626 }
2627
2628 function applyUrlParams() {
2629 var i,
2630 selectedModules = [],
2631 modulesList = id( "qunit-modulefilter-dropdown-list" ).getElementsByTagName( "input" ),
2632 filter = id( "qunit-filter-input" ).value;
2633
2634 for ( i = 0; i < modulesList.length; i++ ) {
2635 if ( modulesList[ i ].checked ) {
2636 selectedModules.push( modulesList[ i ].value );
2637 }
2638 }
2639
2640 window.location = setUrl( {
2641 filter: ( filter === "" ) ? undefined : filter,
2642 moduleId: ( selectedModules.length === 0 ) ? undefined : selectedModules,
2643
2644 // Remove module and testId filter
2645 module: undefined,
2646 testId: undefined
2647 } );
2648 }
2649
2650 function toolbarUrlConfigContainer() {
2651 var urlConfigContainer = document.createElement( "span" );
2652
2653 urlConfigContainer.innerHTML = getUrlConfigHtml();
2654 addClass( urlConfigContainer, "qunit-url-config" );
2655
2656 addEvents( urlConfigContainer.getElementsByTagName( "input" ), "change", toolbarChanged );
2657 addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged );
2658
2659 return urlConfigContainer;
2660 }
2661
2662 function toolbarLooseFilter() {
2663 var filter = document.createElement( "form" ),
2664 label = document.createElement( "label" ),
2665 input = document.createElement( "input" ),
2666 button = document.createElement( "button" );
2667
2668 addClass( filter, "qunit-filter" );
2669
2670 label.innerHTML = "Filter: ";
2671
2672 input.type = "text";
2673 input.value = config.filter || "";
2674 input.name = "filter";
2675 input.id = "qunit-filter-input";
2676
2677 button.innerHTML = "Go";
2678
2679 label.appendChild( input );
2680
2681 filter.appendChild( label );
2682 filter.appendChild( document.createTextNode( " " ) );
2683 filter.appendChild( button );
2684 addEvent( filter, "submit", interceptNavigation );
2685
2686 return filter;
2687 }
2688
2689 function moduleListHtml () {
2690 var i, checked,
2691 html = "";
2692
2693 for ( i = 0; i < config.modules.length; i++ ) {
2694 if ( config.modules[ i ].name !== "" ) {
2695 checked = config.moduleId.indexOf( config.modules[ i ].moduleId ) > -1;
2696 html += "<li><label class='clickable" + ( checked ? " checked" : "" ) +
2697 "'><input type='checkbox' " + "value='" + config.modules[ i ].moduleId + "'" +
2698 ( checked ? " checked='checked'" : "" ) + " />" +
2699 escapeText( config.modules[ i ].name ) + "</label></li>";
2700 }
2701 }
2702
2703 return html;
2704 }
2705
2706 function toolbarModuleFilter () {
2707 var allCheckbox, commit, reset,
2708 moduleFilter = document.createElement( "form" ),
2709 label = document.createElement( "label" ),
2710 moduleSearch = document.createElement( "input" ),
2711 dropDown = document.createElement( "div" ),
2712 actions = document.createElement( "span" ),
2713 dropDownList = document.createElement( "ul" ),
2714 dirty = false;
2715
2716 moduleSearch.id = "qunit-modulefilter-search";
2717 addEvent( moduleSearch, "input", searchInput );
2718 addEvent( moduleSearch, "input", searchFocus );
2719 addEvent( moduleSearch, "focus", searchFocus );
2720 addEvent( moduleSearch, "click", searchFocus );
2721
2722 label.id = "qunit-modulefilter-search-container";
2723 label.innerHTML = "Module: ";
2724 label.appendChild( moduleSearch );
2725
2726 actions.id = "qunit-modulefilter-actions";
2727 actions.innerHTML =
2728 "<button style='display:none'>Apply</button>" +
2729 "<button type='reset' style='display:none'>Reset</button>" +
2730 "<label class='clickable" +
2731 ( config.moduleId.length ? "" : " checked" ) +
2732 "'><input type='checkbox'" + ( config.moduleId.length ? "" : " checked='checked'" ) +
2733 ">All modules</label>";
2734 allCheckbox = actions.lastChild.firstChild;
2735 commit = actions.firstChild;
2736 reset = commit.nextSibling;
2737 addEvent( commit, "click", applyUrlParams );
2738
2739 dropDownList.id = "qunit-modulefilter-dropdown-list";
2740 dropDownList.innerHTML = moduleListHtml();
2741
2742 dropDown.id = "qunit-modulefilter-dropdown";
2743 dropDown.style.display = "none";
2744 dropDown.appendChild( actions );
2745 dropDown.appendChild( dropDownList );
2746 addEvent( dropDown, "change", selectionChange );
2747 selectionChange();
2748
2749 moduleFilter.id = "qunit-modulefilter";
2750 moduleFilter.appendChild( label );
2751 moduleFilter.appendChild( dropDown ) ;
2752 addEvent( moduleFilter, "submit", interceptNavigation );
2753 addEvent( moduleFilter, "reset", function() {
2754
2755 // Let the reset happen, then update styles
2756 window.setTimeout( selectionChange );
2757 } );
2758
2759 // Enables show/hide for the dropdown
2760 function searchFocus() {
2761 if ( dropDown.style.display !== "none" ) {
2762 return;
2763 }
2764
2765 dropDown.style.display = "block";
2766 addEvent( document, "click", hideHandler );
2767 addEvent( document, "keydown", hideHandler );
2768
2769 // Hide on Escape keydown or outside-container click
2770 function hideHandler( e ) {
2771 var inContainer = moduleFilter.contains( e.target );
2772
2773 if ( e.keyCode === 27 || !inContainer ) {
2774 if ( e.keyCode === 27 && inContainer ) {
2775 moduleSearch.focus();
2776 }
2777 dropDown.style.display = "none";
2778 removeEvent( document, "click", hideHandler );
2779 removeEvent( document, "keydown", hideHandler );
2780 moduleSearch.value = "";
2781 searchInput();
2782 }
2783 }
2784 }
2785
2786 // Processes module search box input
2787 function searchInput() {
2788 var i, item,
2789 searchText = moduleSearch.value.toLowerCase(),
2790 listItems = dropDownList.children;
2791
2792 for ( i = 0; i < listItems.length; i++ ) {
2793 item = listItems[ i ];
2794 if ( !searchText || item.textContent.toLowerCase().indexOf( searchText ) > -1 ) {
2795 item.style.display = "";
2796 } else {
2797 item.style.display = "none";
2798 }
2799 }
2800 }
2801
2802 // Processes selection changes
2803 function selectionChange( evt ) {
2804 var i, item,
2805 checkbox = evt && evt.target || allCheckbox,
2806 modulesList = dropDownList.getElementsByTagName( "input" ),
2807 selectedNames = [];
2808
2809 toggleClass( checkbox.parentNode, "checked", checkbox.checked );
2810
2811 dirty = false;
2812 if ( checkbox.checked && checkbox !== allCheckbox ) {
2813 allCheckbox.checked = false;
2814 removeClass( allCheckbox.parentNode, "checked" );
2815 }
2816 for ( i = 0; i < modulesList.length; i++ ) {
2817 item = modulesList[ i ];
2818 if ( !evt ) {
2819 toggleClass( item.parentNode, "checked", item.checked );
2820 } else if ( checkbox === allCheckbox && checkbox.checked ) {
2821 item.checked = false;
2822 removeClass( item.parentNode, "checked" );
2823 }
2824 dirty = dirty || ( item.checked !== item.defaultChecked );
2825 if ( item.checked ) {
2826 selectedNames.push( item.parentNode.textContent );
2827 }
2828 }
2829
2830 commit.style.display = reset.style.display = dirty ? "" : "none";
2831 moduleSearch.placeholder = selectedNames.join( ", " ) || allCheckbox.parentNode.textContent;
2832 moduleSearch.title = "Type to filter list. Current selection:\n" +
2833 ( selectedNames.join( "\n" ) || allCheckbox.parentNode.textContent );
2834 }
2835
2836 return moduleFilter;
2837 }
2838
2839 function appendToolbar() {
2840 var toolbar = id( "qunit-testrunner-toolbar" );
2841
2842 if ( toolbar ) {
2843 toolbar.appendChild( toolbarUrlConfigContainer() );
2844 toolbar.appendChild( toolbarModuleFilter() );
2845 toolbar.appendChild( toolbarLooseFilter() );
2846 toolbar.appendChild( document.createElement( "div" ) ).className = "clearfix";
2847 }
2848 }
2849
2850 function appendHeader() {
2851 var header = id( "qunit-header" );
2852
2853 if ( header ) {
2854 header.innerHTML = "<a href='" + escapeText( unfilteredUrl ) + "'>" + header.innerHTML +
2855 "</a> ";
2856 }
2857 }
2858
2859 function appendBanner() {
2860 var banner = id( "qunit-banner" );
2861
2862 if ( banner ) {
2863 banner.className = "";
2864 }
2865 }
2866
2867 function appendTestResults() {
2868 var tests = id( "qunit-tests" ),
2869 result = id( "qunit-testresult" );
2870
2871 if ( result ) {
2872 result.parentNode.removeChild( result );
2873 }
2874
2875 if ( tests ) {
2876 tests.innerHTML = "";
2877 result = document.createElement( "p" );
2878 result.id = "qunit-testresult";
2879 result.className = "result";
2880 tests.parentNode.insertBefore( result, tests );
2881 result.innerHTML = "Running...<br />&#160;";
2882 }
2883 }
2884
2885 function appendFilteredTest() {
2886 var testId = QUnit.config.testId;
2887 if ( !testId || testId.length <= 0 ) {
2888 return "";
2889 }
2890 return "<div id='qunit-filteredTest'>Rerunning selected tests: " +
2891 escapeText( testId.join( ", " ) ) +
2892 " <a id='qunit-clearFilter' href='" +
2893 escapeText( unfilteredUrl ) +
2894 "'>Run all tests</a></div>";
2895 }
2896
2897 function appendUserAgent() {
2898 var userAgent = id( "qunit-userAgent" );
2899
2900 if ( userAgent ) {
2901 userAgent.innerHTML = "";
2902 userAgent.appendChild(
2903 document.createTextNode(
2904 "QUnit " + QUnit.version + "; " + navigator.userAgent
2905 )
2906 );
2907 }
2908 }
2909
2910 function appendInterface() {
2911 var qunit = id( "qunit" );
2912
2913 if ( qunit ) {
2914 qunit.innerHTML =
2915 "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" +
2916 "<h2 id='qunit-banner'></h2>" +
2917 "<div id='qunit-testrunner-toolbar'></div>" +
2918 appendFilteredTest() +
2919 "<h2 id='qunit-userAgent'></h2>" +
2920 "<ol id='qunit-tests'></ol>";
2921 }
2922
2923 appendHeader();
2924 appendBanner();
2925 appendTestResults();
2926 appendUserAgent();
2927 appendToolbar();
2928 }
2929
2930 function appendTestsList( modules ) {
2931 var i, l, x, z, test, moduleObj;
2932
2933 for ( i = 0, l = modules.length; i < l; i++ ) {
2934 moduleObj = modules[ i ];
2935
2936 for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) {
2937 test = moduleObj.tests[ x ];
2938
2939 appendTest( test.name, test.testId, moduleObj.name );
2940 }
2941 }
2942 }
2943
2944 function appendTest( name, testId, moduleName ) {
2945 var title, rerunTrigger, testBlock, assertList,
2946 tests = id( "qunit-tests" );
2947
2948 if ( !tests ) {
2949 return;
2950 }
2951
2952 title = document.createElement( "strong" );
2953 title.innerHTML = getNameHtml( name, moduleName );
2954
2955 rerunTrigger = document.createElement( "a" );
2956 rerunTrigger.innerHTML = "Rerun";
2957 rerunTrigger.href = setUrl( { testId: testId } );
2958
2959 testBlock = document.createElement( "li" );
2960 testBlock.appendChild( title );
2961 testBlock.appendChild( rerunTrigger );
2962 testBlock.id = "qunit-test-output-" + testId;
2963
2964 assertList = document.createElement( "ol" );
2965 assertList.className = "qunit-assert-list";
2966
2967 testBlock.appendChild( assertList );
2968
2969 tests.appendChild( testBlock );
2970 }
2971
2972 // HTML Reporter initialization and load
2973 QUnit.begin( function( details ) {
2974 var i, moduleObj, tests;
2975
2976 // Sort modules by name for the picker
2977 for ( i = 0; i < details.modules.length; i++ ) {
2978 moduleObj = details.modules[ i ];
2979 if ( moduleObj.name ) {
2980 modulesList.push( moduleObj.name );
2981 }
2982 }
2983 modulesList.sort( function( a, b ) {
2984 return a.localeCompare( b );
2985 } );
2986
2987 // Initialize QUnit elements
2988 appendInterface();
2989 appendTestsList( details.modules );
2990 tests = id( "qunit-tests" );
2991 if ( tests && config.hidepassed ) {
2992 addClass( tests, "hidepass" );
2993 }
2994 } );
2995
2996 QUnit.done( function( details ) {
2997 var i, key,
2998 banner = id( "qunit-banner" ),
2999 tests = id( "qunit-tests" ),
3000 html = [
3001 "Tests completed in ",
3002 details.runtime,
3003 " milliseconds.<br />",
3004 "<span class='passed'>",
3005 details.passed,
3006 "</span> assertions of <span class='total'>",
3007 details.total,
3008 "</span> passed, <span class='failed'>",
3009 details.failed,
3010 "</span> failed."
3011 ].join( "" );
3012
3013 if ( banner ) {
3014 banner.className = details.failed ? "qunit-fail" : "qunit-pass";
3015 }
3016
3017 if ( tests ) {
3018 id( "qunit-testresult" ).innerHTML = html;
3019 }
3020
3021 if ( config.altertitle && document.title ) {
3022
3023 // Show ✖ for good, ✔ for bad suite result in title
3024 // use escape sequences in case file gets loaded with non-utf-8-charset
3025 document.title = [
3026 ( details.failed ? "\u2716" : "\u2714" ),
3027 document.title.replace( /^[\u2714\u2716] /i, "" )
3028 ].join( " " );
3029 }
3030
3031 // Clear own sessionStorage items if all tests passed
3032 if ( config.reorder && defined.sessionStorage && details.failed === 0 ) {
3033 for ( i = 0; i < sessionStorage.length; i++ ) {
3034 key = sessionStorage.key( i++ );
3035 if ( key.indexOf( "qunit-test-" ) === 0 ) {
3036 sessionStorage.removeItem( key );
3037 }
3038 }
3039 }
3040
3041 // Scroll back to top to show results
3042 if ( config.scrolltop && window.scrollTo ) {
3043 window.scrollTo( 0, 0 );
3044 }
3045 } );
3046
3047 function getNameHtml( name, module ) {
3048 var nameHtml = "";
3049
3050 if ( module ) {
3051 nameHtml = "<span class='module-name'>" + escapeText( module ) + "</span>: ";
3052 }
3053
3054 nameHtml += "<span class='test-name'>" + escapeText( name ) + "</span>";
3055
3056 return nameHtml;
3057 }
3058
3059 QUnit.testStart( function( details ) {
3060 var running, testBlock, bad;
3061
3062 testBlock = id( "qunit-test-output-" + details.testId );
3063 if ( testBlock ) {
3064 testBlock.className = "running";
3065 } else {
3066
3067 // Report later registered tests
3068 appendTest( details.name, details.testId, details.module );
3069 }
3070
3071 running = id( "qunit-testresult" );
3072 if ( running ) {
3073 bad = QUnit.config.reorder && defined.sessionStorage &&
3074 +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name );
3075
3076 running.innerHTML = ( bad ?
3077 "Rerunning previously failed test: <br />" :
3078 "Running: <br />" ) +
3079 getNameHtml( details.name, details.module );
3080 }
3081
3082 } );
3083
3084 function stripHtml( string ) {
3085
3086 // Strip tags, html entity and whitespaces
3087 return string.replace( /<\/?[^>]+(>|$)/g, "" ).replace( /\&quot;/g, "" ).replace( /\s+/g, "" );
3088 }
3089
3090 QUnit.log( function( details ) {
3091 var assertList, assertLi,
3092 message, expected, actual, diff,
3093 showDiff = false,
3094 testItem = id( "qunit-test-output-" + details.testId );
3095
3096 if ( !testItem ) {
3097 return;
3098 }
3099
3100 message = escapeText( details.message ) || ( details.result ? "okay" : "failed" );
3101 message = "<span class='test-message'>" + message + "</span>";
3102 message += "<span class='runtime'>@ " + details.runtime + " ms</span>";
3103
3104 // The pushFailure doesn't provide details.expected
3105 // when it calls, it's implicit to also not show expected and diff stuff
3106 // Also, we need to check details.expected existence, as it can exist and be undefined
3107 if ( !details.result && hasOwn.call( details, "expected" ) ) {
3108 if ( details.negative ) {
3109 expected = "NOT " + QUnit.dump.parse( details.expected );
3110 } else {
3111 expected = QUnit.dump.parse( details.expected );
3112 }
3113
3114 actual = QUnit.dump.parse( details.actual );
3115 message += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" +
3116 escapeText( expected ) +
3117 "</pre></td></tr>";
3118
3119 if ( actual !== expected ) {
3120
3121 message += "<tr class='test-actual'><th>Result: </th><td><pre>" +
3122 escapeText( actual ) + "</pre></td></tr>";
3123
3124 // Don't show diff if actual or expected are booleans
3125 if ( !( /^(true|false)$/.test( actual ) ) &&
3126 !( /^(true|false)$/.test( expected ) ) ) {
3127 diff = QUnit.diff( expected, actual );
3128 showDiff = stripHtml( diff ).length !==
3129 stripHtml( expected ).length +
3130 stripHtml( actual ).length;
3131 }
3132
3133 // Don't show diff if expected and actual are totally different
3134 if ( showDiff ) {
3135 message += "<tr class='test-diff'><th>Diff: </th><td><pre>" +
3136 diff + "</pre></td></tr>";
3137 }
3138 } else if ( expected.indexOf( "[object Array]" ) !== -1 ||
3139 expected.indexOf( "[object Object]" ) !== -1 ) {
3140 message += "<tr class='test-message'><th>Message: </th><td>" +
3141 "Diff suppressed as the depth of object is more than current max depth (" +
3142 QUnit.config.maxDepth + ").<p>Hint: Use <code>QUnit.dump.maxDepth</code> to " +
3143 " run with a higher max depth or <a href='" +
3144 escapeText( setUrl( { maxDepth: -1 } ) ) + "'>" +
3145 "Rerun</a> without max depth.</p></td></tr>";
3146 } else {
3147 message += "<tr class='test-message'><th>Message: </th><td>" +
3148 "Diff suppressed as the expected and actual results have an equivalent" +
3149 " serialization</td></tr>";
3150 }
3151
3152 if ( details.source ) {
3153 message += "<tr class='test-source'><th>Source: </th><td><pre>" +
3154 escapeText( details.source ) + "</pre></td></tr>";
3155 }
3156
3157 message += "</table>";
3158
3159 // This occurs when pushFailure is set and we have an extracted stack trace
3160 } else if ( !details.result && details.source ) {
3161 message += "<table>" +
3162 "<tr class='test-source'><th>Source: </th><td><pre>" +
3163 escapeText( details.source ) + "</pre></td></tr>" +
3164 "</table>";
3165 }
3166
3167 assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
3168
3169 assertLi = document.createElement( "li" );
3170 assertLi.className = details.result ? "pass" : "fail";
3171 assertLi.innerHTML = message;
3172 assertList.appendChild( assertLi );
3173 } );
3174
3175 QUnit.testDone( function( details ) {
3176 var testTitle, time, testItem, assertList,
3177 good, bad, testCounts, skipped, sourceName,
3178 tests = id( "qunit-tests" );
3179
3180 if ( !tests ) {
3181 return;
3182 }
3183
3184 testItem = id( "qunit-test-output-" + details.testId );
3185
3186 assertList = testItem.getElementsByTagName( "ol" )[ 0 ];
3187
3188 good = details.passed;
3189 bad = details.failed;
3190
3191 // Store result when possible
3192 if ( config.reorder && defined.sessionStorage ) {
3193 if ( bad ) {
3194 sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad );
3195 } else {
3196 sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name );
3197 }
3198 }
3199
3200 if ( bad === 0 ) {
3201
3202 // Collapse the passing tests
3203 addClass( assertList, "qunit-collapsed" );
3204 } else if ( bad && config.collapse && !collapseNext ) {
3205
3206 // Skip collapsing the first failing test
3207 collapseNext = true;
3208 } else {
3209
3210 // Collapse remaining tests
3211 addClass( assertList, "qunit-collapsed" );
3212 }
3213
3214 // The testItem.firstChild is the test name
3215 testTitle = testItem.firstChild;
3216
3217 testCounts = bad ?
3218 "<b class='failed'>" + bad + "</b>, " + "<b class='passed'>" + good + "</b>, " :
3219 "";
3220
3221 testTitle.innerHTML += " <b class='counts'>(" + testCounts +
3222 details.assertions.length + ")</b>";
3223
3224 if ( details.skipped ) {
3225 testItem.className = "skipped";
3226 skipped = document.createElement( "em" );
3227 skipped.className = "qunit-skipped-label";
3228 skipped.innerHTML = "skipped";
3229 testItem.insertBefore( skipped, testTitle );
3230 } else {
3231 addEvent( testTitle, "click", function() {
3232 toggleClass( assertList, "qunit-collapsed" );
3233 } );
3234
3235 testItem.className = bad ? "fail" : "pass";
3236
3237 time = document.createElement( "span" );
3238 time.className = "runtime";
3239 time.innerHTML = details.runtime + " ms";
3240 testItem.insertBefore( time, assertList );
3241 }
3242
3243 // Show the source of the test when showing assertions
3244 if ( details.source ) {
3245 sourceName = document.createElement( "p" );
3246 sourceName.innerHTML = "<strong>Source: </strong>" + details.source;
3247 addClass( sourceName, "qunit-source" );
3248 if ( bad === 0 ) {
3249 addClass( sourceName, "qunit-collapsed" );
3250 }
3251 addEvent( testTitle, "click", function() {
3252 toggleClass( sourceName, "qunit-collapsed" );
3253 } );
3254 testItem.appendChild( sourceName );
3255 }
3256 } );
3257
3258 // Avoid readyState issue with phantomjs
3259 // Ref: #818
3260 var notPhantom = ( function( p ) {
3261 return !( p && p.version && p.version.major > 0 );
3262 } )( window.phantom );
3263
3264 if ( notPhantom && document.readyState === "complete" ) {
3265 QUnit.load();
3266 } else {
3267 addEvent( window, "load", QUnit.load );
3268 }
3269
3270 /*
3271 * This file is a modified version of google-diff-match-patch's JavaScript implementation
3272 * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
3273 * modifications are licensed as more fully set forth in LICENSE.txt.
3274 *
3275 * The original source of google-diff-match-patch is attributable and licensed as follows:
3276 *
3277 * Copyright 2006 Google Inc.
3278 * https://code.google.com/p/google-diff-match-patch/
3279 *
3280 * Licensed under the Apache License, Version 2.0 (the "License");
3281 * you may not use this file except in compliance with the License.
3282 * You may obtain a copy of the License at
3283 *
3284 * https://www.apache.org/licenses/LICENSE-2.0
3285 *
3286 * Unless required by applicable law or agreed to in writing, software
3287 * distributed under the License is distributed on an "AS IS" BASIS,
3288 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3289 * See the License for the specific language governing permissions and
3290 * limitations under the License.
3291 *
3292 * More Info:
3293 * https://code.google.com/p/google-diff-match-patch/
3294 *
3295 * Usage: QUnit.diff(expected, actual)
3296 *
3297 */
3298 QUnit.diff = ( function() {
3299 function DiffMatchPatch() {
3300 }
3301
3302 // DIFF FUNCTIONS
3303
3304 /**
3305 * The data structure representing a diff is an array of tuples:
3306 * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
3307 * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
3308 */
3309 var DIFF_DELETE = -1,
3310 DIFF_INSERT = 1,
3311 DIFF_EQUAL = 0;
3312
3313 /**
3314 * Find the differences between two texts. Simplifies the problem by stripping
3315 * any common prefix or suffix off the texts before diffing.
3316 * @param {string} text1 Old string to be diffed.
3317 * @param {string} text2 New string to be diffed.
3318 * @param {boolean=} optChecklines Optional speedup flag. If present and false,
3319 * then don't run a line-level diff first to identify the changed areas.
3320 * Defaults to true, which does a faster, slightly less optimal diff.
3321 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
3322 */
3323 DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) {
3324 var deadline, checklines, commonlength,
3325 commonprefix, commonsuffix, diffs;
3326
3327 // The diff must be complete in up to 1 second.
3328 deadline = ( new Date() ).getTime() + 1000;
3329
3330 // Check for null inputs.
3331 if ( text1 === null || text2 === null ) {
3332 throw new Error( "Null input. (DiffMain)" );
3333 }
3334
3335 // Check for equality (speedup).
3336 if ( text1 === text2 ) {
3337 if ( text1 ) {
3338 return [
3339 [ DIFF_EQUAL, text1 ]
3340 ];
3341 }
3342 return [];
3343 }
3344
3345 if ( typeof optChecklines === "undefined" ) {
3346 optChecklines = true;
3347 }
3348
3349 checklines = optChecklines;
3350
3351 // Trim off common prefix (speedup).
3352 commonlength = this.diffCommonPrefix( text1, text2 );
3353 commonprefix = text1.substring( 0, commonlength );
3354 text1 = text1.substring( commonlength );
3355 text2 = text2.substring( commonlength );
3356
3357 // Trim off common suffix (speedup).
3358 commonlength = this.diffCommonSuffix( text1, text2 );
3359 commonsuffix = text1.substring( text1.length - commonlength );
3360 text1 = text1.substring( 0, text1.length - commonlength );
3361 text2 = text2.substring( 0, text2.length - commonlength );
3362
3363 // Compute the diff on the middle block.
3364 diffs = this.diffCompute( text1, text2, checklines, deadline );
3365
3366 // Restore the prefix and suffix.
3367 if ( commonprefix ) {
3368 diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
3369 }
3370 if ( commonsuffix ) {
3371 diffs.push( [ DIFF_EQUAL, commonsuffix ] );
3372 }
3373 this.diffCleanupMerge( diffs );
3374 return diffs;
3375 };
3376
3377 /**
3378 * Reduce the number of edits by eliminating operationally trivial equalities.
3379 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
3380 */
3381 DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
3382 var changes, equalities, equalitiesLength, lastequality,
3383 pointer, preIns, preDel, postIns, postDel;
3384 changes = false;
3385 equalities = []; // Stack of indices where equalities are found.
3386 equalitiesLength = 0; // Keeping our own length var is faster in JS.
3387 /** @type {?string} */
3388 lastequality = null;
3389
3390 // Always equal to diffs[equalities[equalitiesLength - 1]][1]
3391 pointer = 0; // Index of current position.
3392
3393 // Is there an insertion operation before the last equality.
3394 preIns = false;
3395
3396 // Is there a deletion operation before the last equality.
3397 preDel = false;
3398
3399 // Is there an insertion operation after the last equality.
3400 postIns = false;
3401
3402 // Is there a deletion operation after the last equality.
3403 postDel = false;
3404 while ( pointer < diffs.length ) {
3405
3406 // Equality found.
3407 if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) {
3408 if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) {
3409
3410 // Candidate found.
3411 equalities[ equalitiesLength++ ] = pointer;
3412 preIns = postIns;
3413 preDel = postDel;
3414 lastequality = diffs[ pointer ][ 1 ];
3415 } else {
3416
3417 // Not a candidate, and can never become one.
3418 equalitiesLength = 0;
3419 lastequality = null;
3420 }
3421 postIns = postDel = false;
3422
3423 // An insertion or deletion.
3424 } else {
3425
3426 if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
3427 postDel = true;
3428 } else {
3429 postIns = true;
3430 }
3431
3432 /*
3433 * Five types to be split:
3434 * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
3435 * <ins>A</ins>X<ins>C</ins><del>D</del>
3436 * <ins>A</ins><del>B</del>X<ins>C</ins>
3437 * <ins>A</del>X<ins>C</ins><del>D</del>
3438 * <ins>A</ins><del>B</del>X<del>C</del>
3439 */
3440 if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
3441 ( ( lastequality.length < 2 ) &&
3442 ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
3443
3444 // Duplicate record.
3445 diffs.splice(
3446 equalities[ equalitiesLength - 1 ],
3447 0,
3448 [ DIFF_DELETE, lastequality ]
3449 );
3450
3451 // Change second copy to insert.
3452 diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
3453 equalitiesLength--; // Throw away the equality we just deleted;
3454 lastequality = null;
3455 if ( preIns && preDel ) {
3456
3457 // No changes made which could affect previous entry, keep going.
3458 postIns = postDel = true;
3459 equalitiesLength = 0;
3460 } else {
3461 equalitiesLength--; // Throw away the previous equality.
3462 pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
3463 postIns = postDel = false;
3464 }
3465 changes = true;
3466 }
3467 }
3468 pointer++;
3469 }
3470
3471 if ( changes ) {
3472 this.diffCleanupMerge( diffs );
3473 }
3474 };
3475
3476 /**
3477 * Convert a diff array into a pretty HTML report.
3478 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
3479 * @param {integer} string to be beautified.
3480 * @return {string} HTML representation.
3481 */
3482 DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
3483 var op, data, x,
3484 html = [];
3485 for ( x = 0; x < diffs.length; x++ ) {
3486 op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal)
3487 data = diffs[ x ][ 1 ]; // Text of change.
3488 switch ( op ) {
3489 case DIFF_INSERT:
3490 html[ x ] = "<ins>" + escapeText( data ) + "</ins>";
3491 break;
3492 case DIFF_DELETE:
3493 html[ x ] = "<del>" + escapeText( data ) + "</del>";
3494 break;
3495 case DIFF_EQUAL:
3496 html[ x ] = "<span>" + escapeText( data ) + "</span>";
3497 break;
3498 }
3499 }
3500 return html.join( "" );
3501 };
3502
3503 /**
3504 * Determine the common prefix of two strings.
3505 * @param {string} text1 First string.
3506 * @param {string} text2 Second string.
3507 * @return {number} The number of characters common to the start of each
3508 * string.
3509 */
3510 DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
3511 var pointermid, pointermax, pointermin, pointerstart;
3512
3513 // Quick check for common null cases.
3514 if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) {
3515 return 0;
3516 }
3517
3518 // Binary search.
3519 // Performance analysis: https://neil.fraser.name/news/2007/10/09/
3520 pointermin = 0;
3521 pointermax = Math.min( text1.length, text2.length );
3522 pointermid = pointermax;
3523 pointerstart = 0;
3524 while ( pointermin < pointermid ) {
3525 if ( text1.substring( pointerstart, pointermid ) ===
3526 text2.substring( pointerstart, pointermid ) ) {
3527 pointermin = pointermid;
3528 pointerstart = pointermin;
3529 } else {
3530 pointermax = pointermid;
3531 }
3532 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
3533 }
3534 return pointermid;
3535 };
3536
3537 /**
3538 * Determine the common suffix of two strings.
3539 * @param {string} text1 First string.
3540 * @param {string} text2 Second string.
3541 * @return {number} The number of characters common to the end of each string.
3542 */
3543 DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
3544 var pointermid, pointermax, pointermin, pointerend;
3545
3546 // Quick check for common null cases.
3547 if ( !text1 ||
3548 !text2 ||
3549 text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) {
3550 return 0;
3551 }
3552
3553 // Binary search.
3554 // Performance analysis: https://neil.fraser.name/news/2007/10/09/
3555 pointermin = 0;
3556 pointermax = Math.min( text1.length, text2.length );
3557 pointermid = pointermax;
3558 pointerend = 0;
3559 while ( pointermin < pointermid ) {
3560 if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
3561 text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
3562 pointermin = pointermid;
3563 pointerend = pointermin;
3564 } else {
3565 pointermax = pointermid;
3566 }
3567 pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
3568 }
3569 return pointermid;
3570 };
3571
3572 /**
3573 * Find the differences between two texts. Assumes that the texts do not
3574 * have any common prefix or suffix.
3575 * @param {string} text1 Old string to be diffed.
3576 * @param {string} text2 New string to be diffed.
3577 * @param {boolean} checklines Speedup flag. If false, then don't run a
3578 * line-level diff first to identify the changed areas.
3579 * If true, then run a faster, slightly less optimal diff.
3580 * @param {number} deadline Time when the diff should be complete by.
3581 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
3582 * @private
3583 */
3584 DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
3585 var diffs, longtext, shorttext, i, hm,
3586 text1A, text2A, text1B, text2B,
3587 midCommon, diffsA, diffsB;
3588
3589 if ( !text1 ) {
3590
3591 // Just add some text (speedup).
3592 return [
3593 [ DIFF_INSERT, text2 ]
3594 ];
3595 }
3596
3597 if ( !text2 ) {
3598
3599 // Just delete some text (speedup).
3600 return [
3601 [ DIFF_DELETE, text1 ]
3602 ];
3603 }
3604
3605 longtext = text1.length > text2.length ? text1 : text2;
3606 shorttext = text1.length > text2.length ? text2 : text1;
3607 i = longtext.indexOf( shorttext );
3608 if ( i !== -1 ) {
3609
3610 // Shorter text is inside the longer text (speedup).
3611 diffs = [
3612 [ DIFF_INSERT, longtext.substring( 0, i ) ],
3613 [ DIFF_EQUAL, shorttext ],
3614 [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
3615 ];
3616
3617 // Swap insertions for deletions if diff is reversed.
3618 if ( text1.length > text2.length ) {
3619 diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE;
3620 }
3621 return diffs;
3622 }
3623
3624 if ( shorttext.length === 1 ) {
3625
3626 // Single character string.
3627 // After the previous speedup, the character can't be an equality.
3628 return [
3629 [ DIFF_DELETE, text1 ],
3630 [ DIFF_INSERT, text2 ]
3631 ];
3632 }
3633
3634 // Check to see if the problem can be split in two.
3635 hm = this.diffHalfMatch( text1, text2 );
3636 if ( hm ) {
3637
3638 // A half-match was found, sort out the return data.
3639 text1A = hm[ 0 ];
3640 text1B = hm[ 1 ];
3641 text2A = hm[ 2 ];
3642 text2B = hm[ 3 ];
3643 midCommon = hm[ 4 ];
3644
3645 // Send both pairs off for separate processing.
3646 diffsA = this.DiffMain( text1A, text2A, checklines, deadline );
3647 diffsB = this.DiffMain( text1B, text2B, checklines, deadline );
3648
3649 // Merge the results.
3650 return diffsA.concat( [
3651 [ DIFF_EQUAL, midCommon ]
3652 ], diffsB );
3653 }
3654
3655 if ( checklines && text1.length > 100 && text2.length > 100 ) {
3656 return this.diffLineMode( text1, text2, deadline );
3657 }
3658
3659 return this.diffBisect( text1, text2, deadline );
3660 };
3661
3662 /**
3663 * Do the two texts share a substring which is at least half the length of the
3664 * longer text?
3665 * This speedup can produce non-minimal diffs.
3666 * @param {string} text1 First string.
3667 * @param {string} text2 Second string.
3668 * @return {Array.<string>} Five element Array, containing the prefix of
3669 * text1, the suffix of text1, the prefix of text2, the suffix of
3670 * text2 and the common middle. Or null if there was no match.
3671 * @private
3672 */
3673 DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) {
3674 var longtext, shorttext, dmp,
3675 text1A, text2B, text2A, text1B, midCommon,
3676 hm1, hm2, hm;
3677
3678 longtext = text1.length > text2.length ? text1 : text2;
3679 shorttext = text1.length > text2.length ? text2 : text1;
3680 if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) {
3681 return null; // Pointless.
3682 }
3683 dmp = this; // 'this' becomes 'window' in a closure.
3684
3685 /**
3686 * Does a substring of shorttext exist within longtext such that the substring
3687 * is at least half the length of longtext?
3688 * Closure, but does not reference any external variables.
3689 * @param {string} longtext Longer string.
3690 * @param {string} shorttext Shorter string.
3691 * @param {number} i Start index of quarter length substring within longtext.
3692 * @return {Array.<string>} Five element Array, containing the prefix of
3693 * longtext, the suffix of longtext, the prefix of shorttext, the suffix
3694 * of shorttext and the common middle. Or null if there was no match.
3695 * @private
3696 */
3697 function diffHalfMatchI( longtext, shorttext, i ) {
3698 var seed, j, bestCommon, prefixLength, suffixLength,
3699 bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
3700
3701 // Start with a 1/4 length substring at position i as a seed.
3702 seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) );
3703 j = -1;
3704 bestCommon = "";
3705 while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) {
3706 prefixLength = dmp.diffCommonPrefix( longtext.substring( i ),
3707 shorttext.substring( j ) );
3708 suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ),
3709 shorttext.substring( 0, j ) );
3710 if ( bestCommon.length < suffixLength + prefixLength ) {
3711 bestCommon = shorttext.substring( j - suffixLength, j ) +
3712 shorttext.substring( j, j + prefixLength );
3713 bestLongtextA = longtext.substring( 0, i - suffixLength );
3714 bestLongtextB = longtext.substring( i + prefixLength );
3715 bestShorttextA = shorttext.substring( 0, j - suffixLength );
3716 bestShorttextB = shorttext.substring( j + prefixLength );
3717 }
3718 }
3719 if ( bestCommon.length * 2 >= longtext.length ) {
3720 return [ bestLongtextA, bestLongtextB,
3721 bestShorttextA, bestShorttextB, bestCommon
3722 ];
3723 } else {
3724 return null;
3725 }
3726 }
3727
3728 // First check if the second quarter is the seed for a half-match.
3729 hm1 = diffHalfMatchI( longtext, shorttext,
3730 Math.ceil( longtext.length / 4 ) );
3731
3732 // Check again based on the third quarter.
3733 hm2 = diffHalfMatchI( longtext, shorttext,
3734 Math.ceil( longtext.length / 2 ) );
3735 if ( !hm1 && !hm2 ) {
3736 return null;
3737 } else if ( !hm2 ) {
3738 hm = hm1;
3739 } else if ( !hm1 ) {
3740 hm = hm2;
3741 } else {
3742
3743 // Both matched. Select the longest.
3744 hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2;
3745 }
3746
3747 // A half-match was found, sort out the return data.
3748 text1A, text1B, text2A, text2B;
3749 if ( text1.length > text2.length ) {
3750 text1A = hm[ 0 ];
3751 text1B = hm[ 1 ];
3752 text2A = hm[ 2 ];
3753 text2B = hm[ 3 ];
3754 } else {
3755 text2A = hm[ 0 ];
3756 text2B = hm[ 1 ];
3757 text1A = hm[ 2 ];
3758 text1B = hm[ 3 ];
3759 }
3760 midCommon = hm[ 4 ];
3761 return [ text1A, text1B, text2A, text2B, midCommon ];
3762 };
3763
3764 /**
3765 * Do a quick line-level diff on both strings, then rediff the parts for
3766 * greater accuracy.
3767 * This speedup can produce non-minimal diffs.
3768 * @param {string} text1 Old string to be diffed.
3769 * @param {string} text2 New string to be diffed.
3770 * @param {number} deadline Time when the diff should be complete by.
3771 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
3772 * @private
3773 */
3774 DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) {
3775 var a, diffs, linearray, pointer, countInsert,
3776 countDelete, textInsert, textDelete, j;
3777
3778 // Scan the text on a line-by-line basis first.
3779 a = this.diffLinesToChars( text1, text2 );
3780 text1 = a.chars1;
3781 text2 = a.chars2;
3782 linearray = a.lineArray;
3783
3784 diffs = this.DiffMain( text1, text2, false, deadline );
3785
3786 // Convert the diff back to original text.
3787 this.diffCharsToLines( diffs, linearray );
3788
3789 // Eliminate freak matches (e.g. blank lines)
3790 this.diffCleanupSemantic( diffs );
3791
3792 // Rediff any replacement blocks, this time character-by-character.
3793 // Add a dummy entry at the end.
3794 diffs.push( [ DIFF_EQUAL, "" ] );
3795 pointer = 0;
3796 countDelete = 0;
3797 countInsert = 0;
3798 textDelete = "";
3799 textInsert = "";
3800 while ( pointer < diffs.length ) {
3801 switch ( diffs[ pointer ][ 0 ] ) {
3802 case DIFF_INSERT:
3803 countInsert++;
3804 textInsert += diffs[ pointer ][ 1 ];
3805 break;
3806 case DIFF_DELETE:
3807 countDelete++;
3808 textDelete += diffs[ pointer ][ 1 ];
3809 break;
3810 case DIFF_EQUAL:
3811
3812 // Upon reaching an equality, check for prior redundancies.
3813 if ( countDelete >= 1 && countInsert >= 1 ) {
3814
3815 // Delete the offending records and add the merged ones.
3816 diffs.splice( pointer - countDelete - countInsert,
3817 countDelete + countInsert );
3818 pointer = pointer - countDelete - countInsert;
3819 a = this.DiffMain( textDelete, textInsert, false, deadline );
3820 for ( j = a.length - 1; j >= 0; j-- ) {
3821 diffs.splice( pointer, 0, a[ j ] );
3822 }
3823 pointer = pointer + a.length;
3824 }
3825 countInsert = 0;
3826 countDelete = 0;
3827 textDelete = "";
3828 textInsert = "";
3829 break;
3830 }
3831 pointer++;
3832 }
3833 diffs.pop(); // Remove the dummy entry at the end.
3834
3835 return diffs;
3836 };
3837
3838 /**
3839 * Find the 'middle snake' of a diff, split the problem in two
3840 * and return the recursively constructed diff.
3841 * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
3842 * @param {string} text1 Old string to be diffed.
3843 * @param {string} text2 New string to be diffed.
3844 * @param {number} deadline Time at which to bail if not yet complete.
3845 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
3846 * @private
3847 */
3848 DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) {
3849 var text1Length, text2Length, maxD, vOffset, vLength,
3850 v1, v2, x, delta, front, k1start, k1end, k2start,
3851 k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
3852
3853 // Cache the text lengths to prevent multiple calls.
3854 text1Length = text1.length;
3855 text2Length = text2.length;
3856 maxD = Math.ceil( ( text1Length + text2Length ) / 2 );
3857 vOffset = maxD;
3858 vLength = 2 * maxD;
3859 v1 = new Array( vLength );
3860 v2 = new Array( vLength );
3861
3862 // Setting all elements to -1 is faster in Chrome & Firefox than mixing
3863 // integers and undefined.
3864 for ( x = 0; x < vLength; x++ ) {
3865 v1[ x ] = -1;
3866 v2[ x ] = -1;
3867 }
3868 v1[ vOffset + 1 ] = 0;
3869 v2[ vOffset + 1 ] = 0;
3870 delta = text1Length - text2Length;
3871
3872 // If the total number of characters is odd, then the front path will collide
3873 // with the reverse path.
3874 front = ( delta % 2 !== 0 );
3875
3876 // Offsets for start and end of k loop.
3877 // Prevents mapping of space beyond the grid.
3878 k1start = 0;
3879 k1end = 0;
3880 k2start = 0;
3881 k2end = 0;
3882 for ( d = 0; d < maxD; d++ ) {
3883
3884 // Bail out if deadline is reached.
3885 if ( ( new Date() ).getTime() > deadline ) {
3886 break;
3887 }
3888
3889 // Walk the front path one step.
3890 for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) {
3891 k1Offset = vOffset + k1;
3892 if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
3893 x1 = v1[ k1Offset + 1 ];
3894 } else {
3895 x1 = v1[ k1Offset - 1 ] + 1;
3896 }
3897 y1 = x1 - k1;
3898 while ( x1 < text1Length && y1 < text2Length &&
3899 text1.charAt( x1 ) === text2.charAt( y1 ) ) {
3900 x1++;
3901 y1++;
3902 }
3903 v1[ k1Offset ] = x1;
3904 if ( x1 > text1Length ) {
3905
3906 // Ran off the right of the graph.
3907 k1end += 2;
3908 } else if ( y1 > text2Length ) {
3909
3910 // Ran off the bottom of the graph.
3911 k1start += 2;
3912 } else if ( front ) {
3913 k2Offset = vOffset + delta - k1;
3914 if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) {
3915
3916 // Mirror x2 onto top-left coordinate system.
3917 x2 = text1Length - v2[ k2Offset ];
3918 if ( x1 >= x2 ) {
3919
3920 // Overlap detected.
3921 return this.diffBisectSplit( text1, text2, x1, y1, deadline );
3922 }
3923 }
3924 }
3925 }
3926
3927 // Walk the reverse path one step.
3928 for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) {
3929 k2Offset = vOffset + k2;
3930 if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
3931 x2 = v2[ k2Offset + 1 ];
3932 } else {
3933 x2 = v2[ k2Offset - 1 ] + 1;
3934 }
3935 y2 = x2 - k2;
3936 while ( x2 < text1Length && y2 < text2Length &&
3937 text1.charAt( text1Length - x2 - 1 ) ===
3938 text2.charAt( text2Length - y2 - 1 ) ) {
3939 x2++;
3940 y2++;
3941 }
3942 v2[ k2Offset ] = x2;
3943 if ( x2 > text1Length ) {
3944
3945 // Ran off the left of the graph.
3946 k2end += 2;
3947 } else if ( y2 > text2Length ) {
3948
3949 // Ran off the top of the graph.
3950 k2start += 2;
3951 } else if ( !front ) {
3952 k1Offset = vOffset + delta - k2;
3953 if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) {
3954 x1 = v1[ k1Offset ];
3955 y1 = vOffset + x1 - k1Offset;
3956
3957 // Mirror x2 onto top-left coordinate system.
3958 x2 = text1Length - x2;
3959 if ( x1 >= x2 ) {
3960
3961 // Overlap detected.
3962 return this.diffBisectSplit( text1, text2, x1, y1, deadline );
3963 }
3964 }
3965 }
3966 }
3967 }
3968
3969 // Diff took too long and hit the deadline or
3970 // number of diffs equals number of characters, no commonality at all.
3971 return [
3972 [ DIFF_DELETE, text1 ],
3973 [ DIFF_INSERT, text2 ]
3974 ];
3975 };
3976
3977 /**
3978 * Given the location of the 'middle snake', split the diff in two parts
3979 * and recurse.
3980 * @param {string} text1 Old string to be diffed.
3981 * @param {string} text2 New string to be diffed.
3982 * @param {number} x Index of split point in text1.
3983 * @param {number} y Index of split point in text2.
3984 * @param {number} deadline Time at which to bail if not yet complete.
3985 * @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
3986 * @private
3987 */
3988 DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
3989 var text1a, text1b, text2a, text2b, diffs, diffsb;
3990 text1a = text1.substring( 0, x );
3991 text2a = text2.substring( 0, y );
3992 text1b = text1.substring( x );
3993 text2b = text2.substring( y );
3994
3995 // Compute both diffs serially.
3996 diffs = this.DiffMain( text1a, text2a, false, deadline );
3997 diffsb = this.DiffMain( text1b, text2b, false, deadline );
3998
3999 return diffs.concat( diffsb );
4000 };
4001
4002 /**
4003 * Reduce the number of edits by eliminating semantically trivial equalities.
4004 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
4005 */
4006 DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) {
4007 var changes, equalities, equalitiesLength, lastequality,
4008 pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
4009 lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
4010 changes = false;
4011 equalities = []; // Stack of indices where equalities are found.
4012 equalitiesLength = 0; // Keeping our own length var is faster in JS.
4013 /** @type {?string} */
4014 lastequality = null;
4015
4016 // Always equal to diffs[equalities[equalitiesLength - 1]][1]
4017 pointer = 0; // Index of current position.
4018
4019 // Number of characters that changed prior to the equality.
4020 lengthInsertions1 = 0;
4021 lengthDeletions1 = 0;
4022
4023 // Number of characters that changed after the equality.
4024 lengthInsertions2 = 0;
4025 lengthDeletions2 = 0;
4026 while ( pointer < diffs.length ) {
4027 if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
4028 equalities[ equalitiesLength++ ] = pointer;
4029 lengthInsertions1 = lengthInsertions2;
4030 lengthDeletions1 = lengthDeletions2;
4031 lengthInsertions2 = 0;
4032 lengthDeletions2 = 0;
4033 lastequality = diffs[ pointer ][ 1 ];
4034 } else { // An insertion or deletion.
4035 if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) {
4036 lengthInsertions2 += diffs[ pointer ][ 1 ].length;
4037 } else {
4038 lengthDeletions2 += diffs[ pointer ][ 1 ].length;
4039 }
4040
4041 // Eliminate an equality that is smaller or equal to the edits on both
4042 // sides of it.
4043 if ( lastequality && ( lastequality.length <=
4044 Math.max( lengthInsertions1, lengthDeletions1 ) ) &&
4045 ( lastequality.length <= Math.max( lengthInsertions2,
4046 lengthDeletions2 ) ) ) {
4047
4048 // Duplicate record.
4049 diffs.splice(
4050 equalities[ equalitiesLength - 1 ],
4051 0,
4052 [ DIFF_DELETE, lastequality ]
4053 );
4054
4055 // Change second copy to insert.
4056 diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
4057
4058 // Throw away the equality we just deleted.
4059 equalitiesLength--;
4060
4061 // Throw away the previous equality (it needs to be reevaluated).
4062 equalitiesLength--;
4063 pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
4064
4065 // Reset the counters.
4066 lengthInsertions1 = 0;
4067 lengthDeletions1 = 0;
4068 lengthInsertions2 = 0;
4069 lengthDeletions2 = 0;
4070 lastequality = null;
4071 changes = true;
4072 }
4073 }
4074 pointer++;
4075 }
4076
4077 // Normalize the diff.
4078 if ( changes ) {
4079 this.diffCleanupMerge( diffs );
4080 }
4081
4082 // Find any overlaps between deletions and insertions.
4083 // e.g: <del>abcxxx</del><ins>xxxdef</ins>
4084 // -> <del>abc</del>xxx<ins>def</ins>
4085 // e.g: <del>xxxabc</del><ins>defxxx</ins>
4086 // -> <ins>def</ins>xxx<del>abc</del>
4087 // Only extract an overlap if it is as big as the edit ahead or behind it.
4088 pointer = 1;
4089 while ( pointer < diffs.length ) {
4090 if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE &&
4091 diffs[ pointer ][ 0 ] === DIFF_INSERT ) {
4092 deletion = diffs[ pointer - 1 ][ 1 ];
4093 insertion = diffs[ pointer ][ 1 ];
4094 overlapLength1 = this.diffCommonOverlap( deletion, insertion );
4095 overlapLength2 = this.diffCommonOverlap( insertion, deletion );
4096 if ( overlapLength1 >= overlapLength2 ) {
4097 if ( overlapLength1 >= deletion.length / 2 ||
4098 overlapLength1 >= insertion.length / 2 ) {
4099
4100 // Overlap found. Insert an equality and trim the surrounding edits.
4101 diffs.splice(
4102 pointer,
4103 0,
4104 [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ]
4105 );
4106 diffs[ pointer - 1 ][ 1 ] =
4107 deletion.substring( 0, deletion.length - overlapLength1 );
4108 diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 );
4109 pointer++;
4110 }
4111 } else {
4112 if ( overlapLength2 >= deletion.length / 2 ||
4113 overlapLength2 >= insertion.length / 2 ) {
4114
4115 // Reverse overlap found.
4116 // Insert an equality and swap and trim the surrounding edits.
4117 diffs.splice(
4118 pointer,
4119 0,
4120 [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ]
4121 );
4122
4123 diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT;
4124 diffs[ pointer - 1 ][ 1 ] =
4125 insertion.substring( 0, insertion.length - overlapLength2 );
4126 diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE;
4127 diffs[ pointer + 1 ][ 1 ] =
4128 deletion.substring( overlapLength2 );
4129 pointer++;
4130 }
4131 }
4132 pointer++;
4133 }
4134 pointer++;
4135 }
4136 };
4137
4138 /**
4139 * Determine if the suffix of one string is the prefix of another.
4140 * @param {string} text1 First string.
4141 * @param {string} text2 Second string.
4142 * @return {number} The number of characters common to the end of the first
4143 * string and the start of the second string.
4144 * @private
4145 */
4146 DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) {
4147 var text1Length, text2Length, textLength,
4148 best, length, pattern, found;
4149
4150 // Cache the text lengths to prevent multiple calls.
4151 text1Length = text1.length;
4152 text2Length = text2.length;
4153
4154 // Eliminate the null case.
4155 if ( text1Length === 0 || text2Length === 0 ) {
4156 return 0;
4157 }
4158
4159 // Truncate the longer string.
4160 if ( text1Length > text2Length ) {
4161 text1 = text1.substring( text1Length - text2Length );
4162 } else if ( text1Length < text2Length ) {
4163 text2 = text2.substring( 0, text1Length );
4164 }
4165 textLength = Math.min( text1Length, text2Length );
4166
4167 // Quick check for the worst case.
4168 if ( text1 === text2 ) {
4169 return textLength;
4170 }
4171
4172 // Start by looking for a single character match
4173 // and increase length until no match is found.
4174 // Performance analysis: https://neil.fraser.name/news/2010/11/04/
4175 best = 0;
4176 length = 1;
4177 while ( true ) {
4178 pattern = text1.substring( textLength - length );
4179 found = text2.indexOf( pattern );
4180 if ( found === -1 ) {
4181 return best;
4182 }
4183 length += found;
4184 if ( found === 0 || text1.substring( textLength - length ) ===
4185 text2.substring( 0, length ) ) {
4186 best = length;
4187 length++;
4188 }
4189 }
4190 };
4191
4192 /**
4193 * Split two texts into an array of strings. Reduce the texts to a string of
4194 * hashes where each Unicode character represents one line.
4195 * @param {string} text1 First string.
4196 * @param {string} text2 Second string.
4197 * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
4198 * An object containing the encoded text1, the encoded text2 and
4199 * the array of unique strings.
4200 * The zeroth element of the array of unique strings is intentionally blank.
4201 * @private
4202 */
4203 DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) {
4204 var lineArray, lineHash, chars1, chars2;
4205 lineArray = []; // E.g. lineArray[4] === 'Hello\n'
4206 lineHash = {}; // E.g. lineHash['Hello\n'] === 4
4207
4208 // '\x00' is a valid character, but various debuggers don't like it.
4209 // So we'll insert a junk entry to avoid generating a null character.
4210 lineArray[ 0 ] = "";
4211
4212 /**
4213 * Split a text into an array of strings. Reduce the texts to a string of
4214 * hashes where each Unicode character represents one line.
4215 * Modifies linearray and linehash through being a closure.
4216 * @param {string} text String to encode.
4217 * @return {string} Encoded string.
4218 * @private
4219 */
4220 function diffLinesToCharsMunge( text ) {
4221 var chars, lineStart, lineEnd, lineArrayLength, line;
4222 chars = "";
4223
4224 // Walk the text, pulling out a substring for each line.
4225 // text.split('\n') would would temporarily double our memory footprint.
4226 // Modifying text would create many large strings to garbage collect.
4227 lineStart = 0;
4228 lineEnd = -1;
4229
4230 // Keeping our own length variable is faster than looking it up.
4231 lineArrayLength = lineArray.length;
4232 while ( lineEnd < text.length - 1 ) {
4233 lineEnd = text.indexOf( "\n", lineStart );
4234 if ( lineEnd === -1 ) {
4235 lineEnd = text.length - 1;
4236 }
4237 line = text.substring( lineStart, lineEnd + 1 );
4238 lineStart = lineEnd + 1;
4239
4240 if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) :
4241 ( lineHash[ line ] !== undefined ) ) {
4242 chars += String.fromCharCode( lineHash[ line ] );
4243 } else {
4244 chars += String.fromCharCode( lineArrayLength );
4245 lineHash[ line ] = lineArrayLength;
4246 lineArray[ lineArrayLength++ ] = line;
4247 }
4248 }
4249 return chars;
4250 }
4251
4252 chars1 = diffLinesToCharsMunge( text1 );
4253 chars2 = diffLinesToCharsMunge( text2 );
4254 return {
4255 chars1: chars1,
4256 chars2: chars2,
4257 lineArray: lineArray
4258 };
4259 };
4260
4261 /**
4262 * Rehydrate the text in a diff from a string of line hashes to real lines of
4263 * text.
4264 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
4265 * @param {!Array.<string>} lineArray Array of unique strings.
4266 * @private
4267 */
4268 DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
4269 var x, chars, text, y;
4270 for ( x = 0; x < diffs.length; x++ ) {
4271 chars = diffs[ x ][ 1 ];
4272 text = [];
4273 for ( y = 0; y < chars.length; y++ ) {
4274 text[ y ] = lineArray[ chars.charCodeAt( y ) ];
4275 }
4276 diffs[ x ][ 1 ] = text.join( "" );
4277 }
4278 };
4279
4280 /**
4281 * Reorder and merge like edit sections. Merge equalities.
4282 * Any edit section can move as long as it doesn't cross an equality.
4283 * @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
4284 */
4285 DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) {
4286 var pointer, countDelete, countInsert, textInsert, textDelete,
4287 commonlength, changes, diffPointer, position;
4288 diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
4289 pointer = 0;
4290 countDelete = 0;
4291 countInsert = 0;
4292 textDelete = "";
4293 textInsert = "";
4294 commonlength;
4295 while ( pointer < diffs.length ) {
4296 switch ( diffs[ pointer ][ 0 ] ) {
4297 case DIFF_INSERT:
4298 countInsert++;
4299 textInsert += diffs[ pointer ][ 1 ];
4300 pointer++;
4301 break;
4302 case DIFF_DELETE:
4303 countDelete++;
4304 textDelete += diffs[ pointer ][ 1 ];
4305 pointer++;
4306 break;
4307 case DIFF_EQUAL:
4308
4309 // Upon reaching an equality, check for prior redundancies.
4310 if ( countDelete + countInsert > 1 ) {
4311 if ( countDelete !== 0 && countInsert !== 0 ) {
4312
4313 // Factor out any common prefixes.
4314 commonlength = this.diffCommonPrefix( textInsert, textDelete );
4315 if ( commonlength !== 0 ) {
4316 if ( ( pointer - countDelete - countInsert ) > 0 &&
4317 diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] ===
4318 DIFF_EQUAL ) {
4319 diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] +=
4320 textInsert.substring( 0, commonlength );
4321 } else {
4322 diffs.splice( 0, 0, [ DIFF_EQUAL,
4323 textInsert.substring( 0, commonlength )
4324 ] );
4325 pointer++;
4326 }
4327 textInsert = textInsert.substring( commonlength );
4328 textDelete = textDelete.substring( commonlength );
4329 }
4330
4331 // Factor out any common suffixies.
4332 commonlength = this.diffCommonSuffix( textInsert, textDelete );
4333 if ( commonlength !== 0 ) {
4334 diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length -
4335 commonlength ) + diffs[ pointer ][ 1 ];
4336 textInsert = textInsert.substring( 0, textInsert.length -
4337 commonlength );
4338 textDelete = textDelete.substring( 0, textDelete.length -
4339 commonlength );
4340 }
4341 }
4342
4343 // Delete the offending records and add the merged ones.
4344 if ( countDelete === 0 ) {
4345 diffs.splice( pointer - countInsert,
4346 countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
4347 } else if ( countInsert === 0 ) {
4348 diffs.splice( pointer - countDelete,
4349 countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
4350 } else {
4351 diffs.splice(
4352 pointer - countDelete - countInsert,
4353 countDelete + countInsert,
4354 [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ]
4355 );
4356 }
4357 pointer = pointer - countDelete - countInsert +
4358 ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1;
4359 } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) {
4360
4361 // Merge this equality with the previous one.
4362 diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ];
4363 diffs.splice( pointer, 1 );
4364 } else {
4365 pointer++;
4366 }
4367 countInsert = 0;
4368 countDelete = 0;
4369 textDelete = "";
4370 textInsert = "";
4371 break;
4372 }
4373 }
4374 if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) {
4375 diffs.pop(); // Remove the dummy entry at the end.
4376 }
4377
4378 // Second pass: look for single edits surrounded on both sides by equalities
4379 // which can be shifted sideways to eliminate an equality.
4380 // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
4381 changes = false;
4382 pointer = 1;
4383
4384 // Intentionally ignore the first and last element (don't need checking).
4385 while ( pointer < diffs.length - 1 ) {
4386 if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL &&
4387 diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) {
4388
4389 diffPointer = diffs[ pointer ][ 1 ];
4390 position = diffPointer.substring(
4391 diffPointer.length - diffs[ pointer - 1 ][ 1 ].length
4392 );
4393
4394 // This is a single edit surrounded by equalities.
4395 if ( position === diffs[ pointer - 1 ][ 1 ] ) {
4396
4397 // Shift the edit over the previous equality.
4398 diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] +
4399 diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length -
4400 diffs[ pointer - 1 ][ 1 ].length );
4401 diffs[ pointer + 1 ][ 1 ] =
4402 diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ];
4403 diffs.splice( pointer - 1, 1 );
4404 changes = true;
4405 } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
4406 diffs[ pointer + 1 ][ 1 ] ) {
4407
4408 // Shift the edit over the next equality.
4409 diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ];
4410 diffs[ pointer ][ 1 ] =
4411 diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) +
4412 diffs[ pointer + 1 ][ 1 ];
4413 diffs.splice( pointer + 1, 1 );
4414 changes = true;
4415 }
4416 }
4417 pointer++;
4418 }
4419
4420 // If shifts were made, the diff needs reordering and another shift sweep.
4421 if ( changes ) {
4422 this.diffCleanupMerge( diffs );
4423 }
4424 };
4425
4426 return function( o, n ) {
4427 var diff, output, text;
4428 diff = new DiffMatchPatch();
4429 output = diff.DiffMain( o, n );
4430 diff.diffCleanupEfficiency( output );
4431 text = diff.diffPrettyHtml( output );
4432
4433 return text;
4434 };
4435 }() );
4436
4437 }() );