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