qunit
Version:
The powerful, easy-to-use testing framework.
7,299 lines • 266 kB
JavaScript
/*!
* QUnit 3.0.0
* https://qunitjs.com/
*
* Copyright OpenJS Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*/
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) {
if (Object(t) !== t) return;
f = !1;
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
// We don't use global-this-polyfill [1], because it modifies
// the globals scope by default. QUnit must not affect the host context
// as developers may test their project may be such a polyfill, and/or
// they may intentionally test their project with and without certain
// polyfills and we must not affect that. It also uses an obscure
// mechanism that seems to sometimes causes a runtime error in older
// browsers (specifically Safari and IE versions that support
// Object.defineProperty but then report _T_ as undefined).
// [1] https://github.com/ungap/global-this/blob/v0.4.4/esm/index.js
//
// Another way is `Function('return this')()`, but doing so relies
// on eval which will cause a CSP error on some servers.
//
// Instead, simply check the four options that already exist
// in all supported environments.
function getGlobalThis() {
if (typeof globalThis !== 'undefined') {
// For SpiderMonkey, modern browsers, and recent Node.js
// eslint-disable-next-line no-undef
return globalThis;
}
if (typeof self !== 'undefined') {
// For web workers
// eslint-disable-next-line no-undef
return self;
}
if (typeof window$1 !== 'undefined') {
// For document context in browsers
return window$1;
}
if (typeof global !== 'undefined') {
// For Node.js
// eslint-disable-next-line no-undef
return global;
}
throw new Error('Unable to locate global object');
}
// This avoids a simple `export const` assignment as that would lead Rollup
// to change getGlobalThis and use the same (generated) variable name there.
var g = getGlobalThis();
// These optional globals are undefined in one or more environments:
// modern browser, old browser, Node.js, SpiderMonkey.
// Calling code must check these for truthy-ness before use.
var console$1 = g.console;
var setTimeout$1 = g.setTimeout;
var clearTimeout = g.clearTimeout;
var process$1 = g.process;
var window$1 = g.window;
var document = window$1 && window$1.document;
var navigator = window$1 && window$1.navigator;
var sessionStorage = function () {
var x = 'qunit-test-string';
try {
g.sessionStorage.setItem(x, x);
g.sessionStorage.removeItem(x);
return g.sessionStorage;
} catch (e) {
return undefined;
}
}();
// Fallback for ES6 Map, `new Map(iterable)`, and ES7 Map#keys
//
// Support: IE 9-11, iOS 7; Map is undefined
// Support: iOS 8; `new Map(iterable)` is not supported
// Support: IE 11; Map#keys is undefined
var StringMap = typeof g.Map === 'function' && typeof g.Map.prototype.keys === 'function' && typeof g.Symbol === 'function' && typeof g.Symbol.iterator === 'symbol' ? g.Map : function StringMap(input) {
var store = Object.create(null);
var hasOwn = Object.prototype.hasOwnProperty;
this.has = function (strKey) {
return hasOwn.call(store, strKey);
};
this.get = function (strKey) {
return store[strKey];
};
this.set = function (strKey, val) {
if (!hasOwn.call(store, strKey)) {
this.size++;
}
store[strKey] = val;
return this;
};
this.delete = function (strKey) {
if (hasOwn.call(store, strKey)) {
delete store[strKey];
this.size--;
}
};
this.forEach = function (callback) {
for (var strKey in store) {
callback(store[strKey], strKey);
}
};
this.keys = function () {
return Object.keys(store);
};
this.clear = function () {
store = Object.create(null);
this.size = 0;
};
this.size = 0;
if (input) {
input.forEach((val, strKey) => {
this.set(strKey, val);
});
}
};
// Basic fallback for ES6 Set
// Support: IE 11, `new Set(iterable)` parameter not yet implemented
// Test for Set#values() which came after Set(iterable).
var StringSet = typeof g.Set === 'function' && typeof g.Set.prototype.values === 'function' ? g.Set : function (input) {
var set = Object.create(null);
if (Array.isArray(input)) {
input.forEach(item => {
set[item] = true;
});
}
return {
add(value) {
set[value] = true;
},
has(value) {
return value in set;
},
get size() {
return Object.keys(set).length;
}
};
};
var toString = Object.prototype.toString;
var hasOwn$3 = Object.prototype.hasOwnProperty;
var performance = {
// eslint-disable-next-line compat/compat -- Checked
now: window$1 && window$1.performance && window$1.performance.now ? window$1.performance.now.bind(window$1.performance) : Date.now
};
// Returns a new Array with the elements that are in a but not in b
function diff$1(a, b) {
return a.filter(a => b.indexOf(a) === -1);
}
/**
* Determines whether an element exists in a given array or not.
*
* @method inArray
* @param {any} elem
* @param {Array} array
* @return {boolean}
*/
var inArray = Array.prototype.includes
// eslint-disable-next-line compat/compat -- Checked
? (elem, array) => array.includes(elem) : (elem, array) => array.indexOf(elem) !== -1;
/**
* Recursively clone an object into a plain array or object, taking only the
* own enumerable properties.
*
* @param {any} obj
* @param {bool} [allowArray=true]
* @return {Object|Array}
*/
function objectValues(obj) {
var allowArray = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
var vals = allowArray && is('array', obj) ? [] : {};
for (var key in obj) {
if (hasOwn$3.call(obj, key)) {
var val = obj[key];
vals[key] = val === Object(val) ? objectValues(val, allowArray) : val;
}
}
return vals;
}
/**
* Recursively clone an object into a plain object, taking only the
* subset of own enumerable properties that exist a given model.
*
* @param {any} obj
* @param {any} model
* @return {Object}
*/
function objectValuesSubset(obj, model) {
// Return primitive values unchanged to avoid false positives or confusing
// results from assert.propContains().
// E.g. an actual null or false wrongly equaling an empty object,
// or an actual string being reported as object not matching a partial object.
if (obj !== Object(obj)) {
return obj;
}
// Unlike objectValues(), subset arrays to a plain objects as well.
// This enables subsetting [20, 30] with {1: 30}.
var subset = {};
for (var key in model) {
if (hasOwn$3.call(model, key) && hasOwn$3.call(obj, key)) {
subset[key] = objectValuesSubset(obj[key], model[key]);
}
}
return subset;
}
// Support: IE 11, iOS 7-8
function extend(a, b, undefOnly, allProperties) {
for (var prop in b) {
if (hasOwn$3.call(b, prop) || allProperties) {
if (b[prop] === undefined) {
delete a[prop];
} else if (!(undefOnly && typeof a[prop] !== 'undefined')) {
a[prop] = b[prop];
}
}
}
return a;
}
function objectType(obj) {
if (typeof obj === 'undefined') {
return 'undefined';
}
// Consider: typeof null === object
if (obj === null) {
return 'null';
}
var match = toString.call(obj).match(/^\[object\s(.*)\]$/);
var type = match && match[1];
switch (type) {
case 'Number':
if (isNaN(obj)) {
return 'nan';
}
return 'number';
case 'String':
case 'Boolean':
case 'Array':
case 'Set':
case 'Map':
case 'Date':
case 'RegExp':
case 'Function':
case 'Symbol':
return type.toLowerCase();
default:
return typeof obj;
}
}
// Safe object type checking
function is(type, obj) {
return objectType(obj) === type;
}
// Based on Java's String.hashCode, a simple but not
// rigorously collision resistant hashing function
function generateHash(module, testName) {
var str = module + '\x1C' + testName;
var hash = 0;
for (var i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
// Convert the possibly negative integer hash code into an 8 character hex string, which isn't
// strictly necessary but increases user understanding that the id is a SHA-like hash
var hex = (0x100000000 + hash).toString(16);
if (hex.length < 8) {
hex = '0000000' + hex;
}
return hex.slice(-8);
}
/**
* Converts an error into a simple string for comparisons.
*
* @param {Error|any} error
* @return {string}
*/
function errorString(error) {
// Use String() instead of toString() to handle non-object values like undefined or null.
var resultErrorString = String(error);
// If the error wasn't a subclass of Error but something like
// an object literal with name and message properties...
if (resultErrorString.slice(0, 7) === '[object') {
// Based on https://es5.github.io/#x15.11.4.4
return (error.name || 'Error') + (error.message ? ": ".concat(error.message) : '');
} else {
return resultErrorString;
}
}
function escapeText(str) {
if (!str) {
return '';
}
// Both single quotes and double quotes (for attributes)
return ('' + str).replace(/['"<>&]/g, function (s) {
switch (s) {
case "'":
return ''';
case '"':
return '"';
case '<':
return '<';
case '>':
return '>';
case '&':
return '&';
}
});
}
function isAsyncFunction(fn) {
return typeof fn === 'function' && Object.prototype.toString.call(fn) === '[object AsyncFunction]';
}
function getUrlParams() {
var urlParams = Object.create(null);
// Only interact with URLs via window.location
var location = typeof window$1 !== 'undefined' && window$1.location;
// Silently skip in non-browser environment
if (location) {
var params = location.search.slice(1).split('&');
var length = params.length;
for (var i = 0; i < length; i++) {
if (params[i]) {
var param = params[i].split('=');
var name = decodeQueryParam(param[0]);
// Allow just a key to turn on a flag, e.g., test.html?noglobals
var value = param.length === 1 || decodeQueryParam(param.slice(1).join('='));
if (name in urlParams) {
urlParams[name] = [].concat(urlParams[name], value);
} else {
urlParams[name] = value;
}
}
}
}
return urlParams;
}
function decodeQueryParam(param) {
return decodeURIComponent(param.replace(/\+/g, '%20'));
}
var urlParams = getUrlParams();
/**
* Config object: Maintain internal state
* Later exposed as QUnit.config
* `config` initialized at top of scope
*/
var config = {
// HTML Reporter: Modify document.title when suite is done
altertitle: true,
autostart: true,
// HTML Reporter: collapse every test except the first failing test
// If false, all failing tests will be expanded
collapse: true,
current: undefined,
// whether or not to fail when there are zero tests
failOnZeroTests: true,
// Select by pattern or case-insensitive substring match against "moduleName: testName"
filter: '',
ignoreUnhandledRejections: false,
testFilter: null,
fixture: undefined,
// HTML Reporter: Hide results of passed tests.
hidepassed: false,
// Depth up-to which object will be dumped
maxDepth: 5,
// Select case-insensitive match of the module name
module: undefined,
// HTML Reporter: Select module/test by array of internal IDs
moduleId: undefined,
noglobals: false,
notrycatch: false,
// By default, run previously failed tests first
// very useful in combination with "Hide passed tests" checked
reorder: true,
reporters: {},
// When enabled, all tests must call expect()
requireExpects: false,
// By default, scroll to top of the page when suite is done
scrolltop: true,
// Enable randomized ordering of tests.
seed: undefined,
// The storage module to use for reordering tests
storage: sessionStorage,
testId: undefined,
testTimeout: 3000,
// The updateRate controls how often QUnit will yield the main thread
// between tests. This is mainly for the benefit of the HTML Reporter,
// so that the browser can visually paint DOM changes with test results.
// This also helps avoid causing browsers to prompt a warning about
// long-running scripts.
updateRate: 1000,
// HTML Reporter: List of URL parameters that are given visual controls.
// These are given an `<input type=checkbox/>` or `<select/>` by the HTML Reporter.
// Values can be read from QUnit.urlParams.
urlConfig: [{
id: 'hidepassed',
label: 'Hide passed tests',
tooltip: 'Only show tests and assertions that fail. Stored as query string.'
}, {
id: 'noglobals',
label: 'Check for globals',
tooltip: 'Enabling this will test if any test introduces new properties on the ' + 'global object (e.g. `window` in browsers). Stored as query string.'
}, {
id: 'notrycatch',
label: 'No try-catch',
tooltip: 'Enabling this will run tests outside of a try-catch block. Stored as query string.'
}],
// List of defined modules (read-only).
modules: [],
// Semi-internal state.
//
// These are undocumented but defacto stable for certain limited use cases,
// in order to maintain ecosystem compat with popular QUnit 2.x plugins and integrations.
//
// - currentModule: This object represents the most recent `QUnit.module()` call,
// and is used by functions like `QUnit.test()` to determine their module parent.
// It is also referred to from `config.modules` and `config._moduleStack`.
//
// This starts out with an unnamed placeholder module to hold any "global" tests.
// The unnamed module was introduced in QUnit 1.16. When we reach doStart() in start.js,
// if no global tests exist, the unnamed module will be removed `config.modules`, as if
// it never existed, and thus never exposed to the events and callbacks API.
//
// Note that this unnamed initial module is not a "root" module, it is not an ancestor
// to any other modules. Doing so would negatively impact developer experience by adding
// needless indentation, indirection, and other visible noise in test results (or require
// workarounds to prevent that). Since the unnamed module is a regular module, it will
// "end" after the last global test (i.e. before the first named module), and not e.g.
// at the end of the test run.
// To set global hooks, use `QUnit.hooks` instead.
// To listen for the end of the run, handle the "runEnd" event from `QUnit.on()`.
//
// - blocking: Whether new tests will be defined and queued, or executed immediately.
// In other words, whether QUnit.start() has been called yet.
//
// - started: Used to measure runtime duration from `QUnit.on('runStart')`.
//
// - queue: List of internal objects. The only supported operation is checking
// the length of the array, or emptying the array as a way to halt execution.
//
// - stats: Internal assertion counts. Use `QUnit.on('runEnd')` instead.
// These are discouraged per the notice at https://qunitjs.com/api/callbacks/QUnit.done/.
// https://qunitjs.com/api/callbacks/QUnit.on/#the-runend-event
//
currentModule: null,
// initial unnamed module for "global tests", assigned in core.js.
blocking: true,
started: 0,
callbacks: {},
queue: [],
stats: {
all: 0,
bad: 0,
testCount: 0
},
// Internal state, exposed to ease in-process resets
// Ref https://github.com/qunitjs/qunit/pull/1598
_moduleStack: [],
_globalHooks: {},
_pq: null,
// ProcessingQueue singleton, assigned in core.js
_QUnit: null,
// Self-reference to the exported QUnit API, for start.js
_runStarted: false,
_event_listeners: Object.create(null),
_event_memory: {}
};
function readFlatPreconfigBoolean(val, dest) {
if (typeof val === 'boolean' || typeof val === 'string' && val !== '') {
config[dest] = val === true || val === 'true' || val === '1';
}
}
function readFlatPreconfigNumber(val, dest) {
if (typeof val === 'number' || typeof val === 'string' && /^[0-9]+$/.test(val)) {
config[dest] = +val;
}
}
function readFlatPreconfigString(val, dest) {
if (typeof val === 'string' && val !== '') {
config[dest] = val;
}
}
function readFlatPreconfigStringOrBoolean(val, dest) {
if (typeof val === 'boolean' || typeof val === 'string' && val !== '') {
config[dest] = val;
}
}
function readFlatPreconfigStringArray(val, dest) {
if (typeof val === 'string' && val !== '') {
config[dest] = [val];
}
}
function readFlatPreconfig(obj) {
readFlatPreconfigBoolean(obj.qunit_config_altertitle, 'altertitle');
readFlatPreconfigBoolean(obj.qunit_config_autostart, 'autostart');
readFlatPreconfigBoolean(obj.qunit_config_collapse, 'collapse');
readFlatPreconfigBoolean(obj.qunit_config_failonzerotests, 'failOnZeroTests');
readFlatPreconfigString(obj.qunit_config_filter, 'filter');
readFlatPreconfigString(obj.qunit_config_fixture, 'fixture');
readFlatPreconfigBoolean(obj.qunit_config_hidepassed, 'hidepassed');
readFlatPreconfigNumber(obj.qunit_config_maxdepth, 'maxDepth');
readFlatPreconfigString(obj.qunit_config_module, 'module');
readFlatPreconfigStringArray(obj.qunit_config_moduleid, 'moduleId');
readFlatPreconfigBoolean(obj.qunit_config_noglobals, 'noglobals');
readFlatPreconfigBoolean(obj.qunit_config_notrycatch, 'notrycatch');
readFlatPreconfigBoolean(obj.qunit_config_reorder, 'reorder');
readFlatPreconfigBoolean(obj.qunit_config_requireexpects, 'requireExpects');
readFlatPreconfigBoolean(obj.qunit_config_scrolltop, 'scrolltop');
readFlatPreconfigStringOrBoolean(obj.qunit_config_seed, 'seed');
readFlatPreconfigStringArray(obj.qunit_config_testid, 'testId');
readFlatPreconfigNumber(obj.qunit_config_testtimeout, 'testTimeout');
var reporterKeys = {
qunit_config_reporters_console: 'console',
qunit_config_reporters_perf: 'perf',
qunit_config_reporters_tap: 'tap',
qunit_config_reporters_html: 'html'
};
for (var key in reporterKeys) {
var val = obj[key];
// Based on readFlatPreconfigBoolean
if (typeof val === 'boolean' || typeof val === 'string' && val !== '') {
var dest = reporterKeys[key];
config.reporters[dest] = val === true || val === 'true' || val === '1';
}
}
}
if (process$1 && 'env' in process$1) {
readFlatPreconfig(process$1.env);
}
readFlatPreconfig(g);
// Apply a predefined QUnit.config object
//
// Ignore QUnit.config if it is a QUnit distribution instead of preconfig.
// That means QUnit was loaded twice! (Use the same approach as export.js)
var preConfig = g && g.QUnit && !g.QUnit.version && g.QUnit.config;
if (preConfig) {
extend(config, preConfig);
}
// Apply QUnit.urlParams
// in accordance with /docs/api/config.index.md#order
readFlatPreconfigString(urlParams.filter, 'filter');
readFlatPreconfigNumber(urlParams.maxDepth, 'maxDepth');
readFlatPreconfigString(urlParams.module, 'module');
if (urlParams.moduleId) {
config.moduleId = [].concat(urlParams.moduleId);
}
if (urlParams.testId) {
config.testId = [].concat(urlParams.testId);
}
readFlatPreconfigBoolean(urlParams.hidepassed, 'hidepassed');
readFlatPreconfigBoolean(urlParams.noglobals, 'noglobals');
readFlatPreconfigBoolean(urlParams.notrycatch, 'notrycatch');
readFlatPreconfigStringOrBoolean(urlParams.seed, 'seed');
if (config.seed === 'true' || config.seed === true) {
// Generate a random seed
// Length of `Math.random()` fraction, in base 36, may vary from 6-14.
// Pad and take slice to a consistent 10-digit value.
// TODO: Present this in HtmlReporter
config.seed = (Math.random().toString(36) + '0000000000').slice(2, 12);
}
// QUnit.dump is based on jsDump 1.0.0 by Ariel Flesler
// From <https://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html>
// Licensed under BSD <https://opensource.org/licenses/BSD-2-Clause>
//
// -------
// Copyright 2008 Ariel Flesler - aflesler(at)gmail(dot)com
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
// -------
var dump = (function () {
function quote(str) {
return '"' + str.toString().replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
function literal(o) {
return o + '';
}
function join(pre, arr, post) {
var s = dump.separator();
var inner = dump.indent(1);
if (arr.join) {
arr = arr.join(',' + s + inner);
}
if (!arr) {
return pre + post;
}
var base = dump.indent();
return [pre, inner + arr, base + post].join(s);
}
function array(arr, stack) {
if (config.maxDepth && dump.depth > config.maxDepth) {
return '[object Array]';
}
this.up();
var i = arr.length;
var ret = new Array(i);
while (i--) {
ret[i] = this.parse(arr[i], undefined, stack);
}
this.down();
return join('[', ret, ']');
}
function isArray(obj) {
return (
// Native Arrays
Array.isArray(obj)
// NodeList objects
|| typeof obj.length === 'number' && obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined)
);
}
var reName = /^function (\w+)/;
var dump = {
// The objType is used mostly internally, you can fix a (custom) type in advance
parse: function (obj, objType, stack) {
stack = stack || [];
var objIndex = stack.indexOf(obj);
if (objIndex !== -1) {
return "recursion(".concat(objIndex - stack.length, ")");
}
objType = objType || this.typeOf(obj);
var parser = this.parsers[objType];
var parserType = typeof parser;
if (parserType === 'function') {
stack.push(obj);
var res = parser.call(this, obj, stack);
stack.pop();
return res;
}
if (parserType === 'string') {
return parser;
}
return '[ERROR: Missing QUnit.dump formatter for type ' + objType + ']';
},
typeOf: function (obj) {
if (obj === null) {
return 'null';
}
if (typeof obj === 'undefined') {
return 'undefined';
} else if (is('regexp', obj)) {
return 'regexp';
} else if (is('date', obj)) {
return 'date';
} else if (is('function', obj)) {
return 'function';
} else if (obj.setInterval !== undefined && obj.document !== undefined && obj.nodeType === undefined) {
return 'window';
} else if (obj.nodeType === 9) {
return 'document';
} else if (obj.nodeType) {
return 'node';
} else if (isArray(obj)) {
return 'array';
} else if (obj.constructor === Error.prototype.constructor) {
return 'error';
}
return typeof obj;
},
separator: function () {
return '\n';
},
// Extra can be a number, shortcut for increasing-calling-decreasing
indent: function (extra) {
var chr = this.indentChar;
return new Array(this.depth + (extra || 0)).join(chr);
},
up: function (a) {
this.depth += a || 1;
},
down: function (a) {
this.depth -= a || 1;
},
setParser: function (name, parser) {
this.parsers[name] = parser;
},
// The next 3 are exposed so you can use them
quote: quote,
literal: literal,
join: join,
depth: 1,
get maxDepth() {
return config.maxDepth;
},
set maxDepth(value) {
config.maxDepth = value;
},
// This is the list of parsers, to modify them, use dump.setParser
parsers: {
window: '[Window]',
document: '[Document]',
error: function (error) {
return 'Error("' + error.message + '")';
},
// This has been unused since QUnit 1.0.0.
// @todo Deprecate and remove.
unknown: '[Unknown]',
null: 'null',
undefined: 'undefined',
function: function (fn) {
var ret = 'function';
// Functions never have name in IE
var name = 'name' in fn ? fn.name : (reName.exec(fn) || [])[1];
if (name) {
ret += ' ' + name;
}
ret += '(';
ret = [ret, dump.parse(fn, 'functionArgs'), '){'].join('');
return join(ret, dump.parse(fn, 'functionCode'), '}');
},
array: array,
nodelist: array,
arguments: array,
object: function (map, stack) {
var ret = [];
if (config.maxDepth && dump.depth > config.maxDepth) {
return '[object Object]';
}
dump.up();
var keys = [];
for (var key in map) {
keys.push(key);
}
// Some properties are not always enumerable on Error objects.
var nonEnumerableProperties = ['message', 'name'];
for (var i in nonEnumerableProperties) {
var _key = nonEnumerableProperties[i];
if (_key in map && !inArray(_key, keys)) {
keys.push(_key);
}
}
keys.sort();
for (var _i = 0; _i < keys.length; _i++) {
var _key2 = keys[_i];
// eslint-disable-next-line compat/compat -- https://github.com/amilajack/eslint-plugin-compat/issues/688
var val = map[_key2];
ret.push(dump.parse(_key2, 'key') + ': ' + dump.parse(val, undefined, stack));
}
dump.down();
return join('{', ret, '}');
},
node: function (node) {
var open = '<';
var close = '>';
var tag = node.nodeName.toLowerCase();
var ret = open + tag;
var attrs = node.attributes;
if (attrs) {
for (var i = 0; i < attrs.length; i++) {
var val = attrs[i].nodeValue;
// IE6 includes all attributes in .attributes, even ones not explicitly
// set. Those have values like undefined, null, 0, false, "" or
// "inherit".
if (val && val !== 'inherit') {
ret += ' ' + attrs[i].nodeName + '=' + dump.parse(val, 'attribute');
}
}
}
ret += close;
// Show content of TextNode or CDATASection
if (node.nodeType === 3 || node.nodeType === 4) {
ret += node.nodeValue;
}
return ret + open + '/' + tag + close;
},
// Function calls it internally, it's the arguments part of the function
functionArgs: function (fn) {
var l = fn.length;
if (!l) {
return '';
}
var args = new Array(l);
while (l--) {
// 97 is 'a'
args[l] = String.fromCharCode(97 + l);
}
return ' ' + args.join(', ') + ' ';
},
// Object calls it internally, the key part of an item in a map
key: quote,
// Function calls it internally, it's the content of the function
functionCode: '[code]',
// Node calls it internally, it's a html attribute value
attribute: quote,
string: quote,
date: quote,
regexp: literal,
number: literal,
boolean: literal,
symbol: function (sym) {
return sym.toString();
}
},
// Indentation unit
indentChar: ' '
};
return dump;
})();
var BOXABLE_TYPES = new StringSet(['boolean', 'number', 'string']);
// Memory for previously seen containers (object, array, map, set).
// Used for recursion detection, and to avoid repeated comparison.
//
// Elements are { a: val, b: val }.
var memory = [];
function useStrictEquality(a, b) {
return a === b;
}
function useObjectValueEquality(a, b) {
return a === b || a.valueOf() === b.valueOf();
}
function compareConstructors(a, b) {
// Comparing constructors is more strict than using `instanceof`
return getConstructor(a) === getConstructor(b);
}
function getConstructor(obj) {
var proto = Object.getPrototypeOf(obj);
// If the obj prototype descends from a null constructor, treat it
// as a null prototype.
// Ref https://github.com/qunitjs/qunit/issues/851
//
// Allow objects with no prototype, from Object.create(null), to be equivalent to
// plain objects that have Object as their constructor.
return !proto || proto.constructor === null ? Object : obj.constructor;
}
function getRegExpFlags(regexp) {
return 'flags' in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0];
}
// Specialised comparisons after entryTypeCallbacks.object, based on `objectType()`
var objTypeCallbacks = {
undefined: useStrictEquality,
null: useStrictEquality,
// Handle boxed boolean
boolean: useObjectValueEquality,
number(a, b) {
// Handle NaN and boxed number
return a === b || a.valueOf() === b.valueOf() || isNaN(a.valueOf()) && isNaN(b.valueOf());
},
// Handle boxed string
string: useObjectValueEquality,
symbol: useStrictEquality,
date: useObjectValueEquality,
nan() {
return true;
},
regexp(a, b) {
return a.source === b.source
// Include flags in the comparison
&& getRegExpFlags(a) === getRegExpFlags(b);
},
// identical reference only
function: useStrictEquality,
array(a, b) {
if (a.length !== b.length) {
// Safe and faster
return false;
}
for (var i = 0; i < a.length; i++) {
if (!typeEquiv(a[i], b[i])) {
return false;
}
}
return true;
},
// Define sets a and b to be equivalent if for each element aVal in a, there
// is some element bVal in b such that aVal and bVal are equivalent. Element
// repetitions are not counted, so these are equivalent:
// a = new Set( [ X={}, Y=[], Y ] );
// b = new Set( [ Y, X, X ] );
set(a, b) {
if (a.size !== b.size) {
// This optimization has certain quirks because of the lack of
// repetition counting. For instance, adding the same
// (reference-identical) element to two equivalent sets can
// make them non-equivalent.
return false;
}
var outerEq = true;
a.forEach(aVal => {
// Short-circuit if the result is already known. (Using for...of
// with a break clause would be cleaner here, but it would cause
// a syntax error on older JavaScript implementations even if
// Set is unused)
if (!outerEq) {
return;
}
var innerEq = false;
b.forEach(bVal => {
// Likewise, short-circuit if the result is already known
if (innerEq) {
return;
}
// Swap out the global memory, as nested typeEquiv() would clobber it
var originalMemory = memory;
memory = [];
if (typeEquiv(bVal, aVal)) {
innerEq = true;
}
// Restore
memory = originalMemory;
});
if (!innerEq) {
outerEq = false;
}
});
return outerEq;
},
// Define maps a and b to be equivalent if for each key-value pair (aKey, aVal)
// in a, there is some key-value pair (bKey, bVal) in b such that
// [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not
// counted, so these are equivalent:
// a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] );
// b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] );
map(a, b) {
if (a.size !== b.size) {
// This optimization has certain quirks because of the lack of
// repetition counting. For instance, adding the same
// (reference-identical) key-value pair to two equivalent maps
// can make them non-equivalent.
return false;
}
var outerEq = true;
a.forEach((aVal, aKey) => {
// Short-circuit if the result is already known. (Using for...of
// with a break clause would be cleaner here, but it would cause
// a syntax error on older JavaScript implementations even if
// Map is unused)
if (!outerEq) {
return;
}
var innerEq = false;
b.forEach((bVal, bKey) => {
// Likewise, short-circuit if the result is already known
if (innerEq) {
return;
}
// Swap out the global memory, as nested typeEquiv() would clobber it
var originalMemory = memory;
memory = [];
if (objTypeCallbacks.array([bVal, bKey], [aVal, aKey])) {
innerEq = true;
}
// Restore
memory = originalMemory;
});
if (!innerEq) {
outerEq = false;
}
});
return outerEq;
}
};
// Entry points from typeEquiv, based on `typeof`
var entryTypeCallbacks = {
undefined: useStrictEquality,
null: useStrictEquality,
boolean: useStrictEquality,
number(a, b) {
// Handle NaN
return a === b || isNaN(a) && isNaN(b);
},
string: useStrictEquality,
symbol: useStrictEquality,
function: useStrictEquality,
object(a, b) {
// Handle memory (skip recursion)
if (memory.some(pair => pair.a === a && pair.b === b)) {
return true;
}
memory.push({
a,
b
});
var aObjType = objectType(a);
var bObjType = objectType(b);
if (aObjType !== 'object' || bObjType !== 'object') {
// Handle literal `null`
// Handle: Array, Map/Set, Date, RegExp/Function, boxed primitives
return aObjType === bObjType && objTypeCallbacks[aObjType](a, b);
}
// NOTE: Literal null must not make it here as it would throw
if (compareConstructors(a, b) === false) {
return false;
}
var aProperties = [];
var bProperties = [];
// Be strict and go deep, no filtering with hasOwnProperty.
for (var i in a) {
// Collect a's properties
aProperties.push(i);
// Skip OOP methods that look the same
if (a.constructor !== Object && typeof a.constructor !== 'undefined' && typeof a[i] === 'function' && typeof b[i] === 'function' && a[i].toString() === b[i].toString()) {
continue;
}
if (!typeEquiv(a[i], b[i])) {
return false;
}
}
for (var _i in b) {
// Collect b's properties
bProperties.push(_i);
}
return objTypeCallbacks.array(aProperties.sort(), bProperties.sort());
}
};
function typeEquiv(a, b) {
// Optimization: Only perform type-specific comparison when pairs are not strictly equal.
if (a === b) {
return true;
}
var aType = typeof a;
var bType = typeof b;
if (aType !== bType) {
// Support comparing primitive to boxed primitives
// Try again after possibly unwrapping one
return (aType === 'object' && BOXABLE_TYPES.has(objectType(a)) ? a.valueOf() : a) === (bType === 'object' && BOXABLE_TYPES.has(objectType(b)) ? b.valueOf() : b);
}
return entryTypeCallbacks[aType](a, b);
}
function innerEquiv(a, b) {
var res = typeEquiv(a, b);
// Release any retained objects and reset recursion detection for next call
memory = [];
return res;
}
/**
* Test any two types of JavaScript values for equality.
*
* @author Philippe Rathé <prathe@gmail.com>
* @author David Chan <david@troi.org>
*/
function equiv(a, b) {
if (arguments.length === 2) {
return a === b || innerEquiv(a, b);
}
// Given 0 or 1 arguments, just return true (nothing to compare).
// Given (A,B,C,D) compare C,D then B,C then A,B.
var i = arguments.length - 1;
while (i > 0) {
if (!innerEquiv(arguments[i - 1], arguments[i])) {
return false;
}
i--;
}
return true;
}
// Stacktrace cleaner to focus on the path from error source to test suite.
//
// This should reduce a raw stack trace like this:
//
// > foo.broken()@/example/foo.js
// > Bar@/example/bar.js
// > @/test/bar.test.js
// > @/lib/qunit.js:500:12
// > @/lib/qunit.js:100:28
// > @/lib/qunit.js:200:56
// > setTimeout@
// > @/dist/vendor.js
//
// and shorten it to show up until the end of the user's bar.test.js code.
//
// > foo.broken()@/example/foo.js
// > Bar@/example/bar.js
// > @/test/bar.test.js
//
// QUnit will obtain one example trace (once per process/pageload suffices),
// strip off any :<line> and :<line>:<column>, and use that as match needle,
// to the first QUnit-internal frames, and then stop at that point.
// Any later frames, including those that are outside QUnit again, will be omitted
// as being uninteresting to the test, since QUnit will have either started or
// resumed the test. This we also clean away browser built-ins, or other
// vendor/bundler that may be higher up the stack.
//
// Stripping :<line>:<column> is not for prettiness, it is essential for the
// match needle to work, since this sample trace will by definition not be the
// same line as e.g. the QUnit.test() call we're trying to identify.
//
// See also:
// - https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
function qunitFileName() {
var error = new Error();
if (!error.stack) {
// Copy of sourceFromStacktrace() to avoid circular dependency
// Support: IE 9-11
try {
throw error;
} catch (err) {
error = err;
}
}
return (error.stack || ''
// Copy of extractStacktrace() to avoid circular dependency
// Support: V8/Chrome
).replace(/^error$\n/im, '').split('\n')[0]
// Global replace, because a frame like localhost:4000/lib/qunit.js:1234:50,
// would otherwise (harmlessly, but uselessly) remove only the port (first match).
// https://github.com/qunitjs/qunit/issues/1769
.replace(/(:\d+)+\)?/g, '')
// Remove anything prior to the last slash (Unix/Windows) from the last frame,
// leaving only "qunit.js".
.replace(/.+[/\\]/, '');
}
var fileName = qunitFileName();
/**
* Responsibilities:
* - For internal errors from QUnit itself, remove the first qunit.js frames.
* - For errors in Node.js, format any remaining qunit.js and node:internal
* frames as internal (i.e. grey out).
*
* @param {string} stack Error#stack
* @param {Function} formatInternal Format a string in an "internal" color
* @param {string|null} [eToString] Error#toString() to help remove
* noise from Node.js/V8 stack traces.
*/
function annotateStacktrace(stack, formatInternal) {
var eToString = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
var frames = stack.split('\n');
var annotated = [];
if (eToString && eToString.indexOf(frames[0]) !== -1) {
// In Firefox and Safari e.stack starts with frame 0, but in V8 (Chrome/Node.js),
// e.stack starts first stringified message. Preserve this separately,
// so that, below, we can distinguish between internal frames on top
// (to remove) vs later internal frames (to format differently).
annotated.push(frames.shift());
}
var initialInternal = true;
for (var i = 0; i < frames.length; i++) {
var frame = frames[i];
// Example of internal stack trace (Node 16+)
// "at wrapModuleLoad (node:internal/modules/cjs/loader:234:24)"
var isInternal = fileName && frame.indexOf(fileName) !== -1 || frame.indexOf('node:internal/') !== -1;
if (!isInternal) {
initialInternal = false;
}
// Remove initial internal frames entirely.
if (!initialInternal) {
annotated.push(isInternal ? formatInternal(frame) : frame);
}
}
return annotated.join('\n');
}
function extractStacktrace(e, offset) {
offset = offset === undefined ? 4 : offset;
// Support: IE9, e.stack is not supported, we will return undefined
if (e && e.stack) {
var _stack = e.stack.split('\n');
// In Firefox and Safari, e.stack starts immediately with the first frame.
//
// In V8 (Chrome/Node.js), the stack starts first with a stringified error message,
// and the real stack starting on line 2.
if (/^error$/i.test(_stack[0])) {
_stack.shift();
}
if (fileName) {
var include = [];
for (var i = offset; i < _stack.length; i++) {
if (_stack[i].indexOf(fileName) !== -1) {
break;
}
include.push(_stack[i]);
}
if (include.length) {
return include.join('\n');
}
}
return _stack[offset];
}
}
function sourceFromStacktrace(offset) {
var error = new Error();
// Support: IE 9-11, iOS 7
// Not all browsers generate the `stack` property for `new Error()`
// See also https://github.com/qunitjs/qunit/issues/636
if (!error.stack) {
try {
throw error;
} catch (err) {
error = err;
}
}
return extractStacktrace(error, offset);
}
function stack(offset) {
offset = (offset || 0) + 2;
// Support Safari: Use temp variable to avoid triggering ES6 Proper Tail Calls,
// which ensures a consistent cross-browser result.
// https://bugs.webkit.org/show_bug.cgi?id=276187
var source = sourceFromStacktrace(offset);
return source;
}
class Assert {
constructor(testContext) {
this.test = testContext;
}
timeout(duration) {
if (typeof duration !== 'number') {
throw new Error('You must pass a number as the duration to assert.timeout');
}
this.test.timeout = duration;
// If a timeout has been set, clear it and reset with the new duration
if (config.timeout) {
clearTimeout(config.timeout);
config.timeout = null;
if (config.timeoutHandler && this.test.timeout > 0) {
this.test.internalResetTimeout(this.test.timeout);
}
}
}
// Documents a "step", which is a string value, in a test as a passing assertion
step(value) {
if (typeof value === 'undefined' || value === '') {
throw new TypeError('You must provide a value to assert.step');
}
if (typeof value !== 'string') {
throw new TypeError('You must provide a string value to assert.step');
}
this.test.steps.push(value);
}
// Verifies the steps in a test match a given array of string values
verifySteps(steps, message) {
// Since the steps array is just string values, we can clone with slice
var actualStepsClone = this.test.steps.slice();
this.deepEqual(actualStepsClone, steps, message);
this.test.stepsCount += this.test.steps.length;
this.test.steps.length = 0;
}
expect(asserts) {
if (arguments.length === 1) {
this.test.expected = asserts;
} else {
return this.test.expected;
}
}
// Create a new async pause and return a new function that can release the pause.
async(count) {
if (count === undefined) {
count = 1;
} else if (typeof count !== 'number') {
throw new TypeError('async takes number as an input');
}
var requiredCalls = count;
return this.test.internalStop(requiredCalls);
}
closeTo(actual, expected, delta, message) {
if (typeof delta !== 'number') {
throw new TypeError('closeTo() requires a delta argument');
}
this.pushResult({
result: Math.abs(actual - expected) <= delta,
actual,
expected,
message: message || "value should be within ".concat(delta, " inclusive")
});
}
push(result, actual, expected, message, negative) {
var currentTest = this instanceof Assert && this.test || config.current;
return currentTest.pushResult({
result,
actual,
expected,
message,
negative
});
}
// Public API to internal test.pushResult()
pushResult(resultInfo) {
// Prefer context object when possible, so that test.pushResult() can provide
// useful error when producing assertions on a closed test.
var currentTest = this instanceof Assert && this.test || config.current;
// Backwards compatibility for direct use of global QUnit.assert.* methods.
// It's use is not recommended as it can leak assertions to other tests from async
// tests, because we only get a reference to the current test.
if (!currentTest) {
throw new Error('assertion outside test context, in ' + sourceFromStacktrace(2));
}
return currentTest.pushResult(resultInfo);
}
ok(result, message) {
if (!message) {
message = result ? 'okay' : "failed, expected argument to be truthy, was: ".concat(dump.parse(result));
}
this.pushResult({
result: !!result,
actual: result,
expected: true,
message
});
}
notOk(result, message) {
if (!message) {
message = !result ? 'okay' : "failed, expected argument to be falsy, was: ".concat(dump.parse(result));
}
this.pushResult({
result: !result,
actual: result,
expected: false,
message
});
}
true(result, message) {
this.pushResult({
result: result === true,
actual: result,
expected: true,
message
});
}
false(result, message) {
this.pushResult({
result: result === false,
actual: result,
expected: false,
message
});
}
equal(actual, expected, message) {
this.pushResult({
// eslint-disable-next-line eqeqeq
result: expected == actual,
actual,
expected,
message
});
}
notEqual(actual, expected, message) {
this.pushResult({
// eslint-disable-next-line eqeqeq
result: expected != actual,
actual,
expected,
message,
negative: true
});
}
propEqual(actual, expected, message) {
actual = objectValues(actual);
expected = objectValues(expected);
this.pushResult({
result: equiv(actual, expected),
actual,
expected,
message
});
}
notPropEqual(actual, expected, message) {
actual = objectValues(actual);
expected = objectValues(expected);
this.pushResult({
result: !equiv(actual, expected),
actual,
expected,
message,
negative: true
});
}
propContains(actual, expected, message) {
actual = objectValuesSubset(actual, expected);
// The expected parameter is usually a plain object, but clone it for
// consistency with propEqual(), and to make it easy to explain that
// inheritance is not considered (on either side), and to support
// recursively checking subsets of nested objects.
expected = objectValues(expected, false);
this.pushResult({
result: equiv(actual, expected),
actual,
expected,
message
});
}
notPropContains(actual, expected, message) {
actual = objectValuesSubset(actual, expected);
expected = objectValues(expected);
this.pushResult({
result: !equiv(actual, expected),
actual,
expected,
message,
negative: true
});
}
deepEqual(actual, expected, message) {
this.pushResult({
result: equiv(actual, expected),
actual,
expected,
message
});
}
notDeepEqual(actual, expected, message) {
this.pushResult({
result: !equiv(actual, expected),
actual,
expected,
message,
negative: true
});
}
strictEqual(actual, expected, message) {
this.pushResult({
result: expected === actual,
actual,
expected,
message
});
}
notStrictEqual(actual, expected, message) {
this.pushResult({
result: expected !== actual,
actual,
expected,
message,
negative: true
});
}
['throws'](block, expected, message) {
var _validateExpectedExce = validateExpectedExceptionArgs(expected, message, 'throws');
var _validateExpectedExce2 = _slicedToArray(_validateExpectedExce, 2);
expected = _validateExpectedExce2[0];
message = _validateExpectedExce2[1];
var currentTest = this instanceof Assert && this.test || config.current;
if (typeof block !== 'function') {
currentTest.assert.pushResult({
result: false,
actual: block,
message: 'The value provided to `assert.throws` in ' + '"' + currentTest.testName + '" was not a function.'
});
return;
}
var actual;
var result = false;
currentTest.ignoreGlobalErrors = true;
try {
block.call(currentTest.testEnvironment);
} catch (e) {
actual = e;
}
currentTest.ignoreGlobalErrors = false;
if (actual) {
var _validateException = validateException(actual, expected, message);
var _validateException2 = _slicedToArray(_validateException, 3);
result = _validateException2[0];
expected = _validateException2[1];
message = _validateException2[2];
}
currentTest.assert.pushResult({
result,
// undefined if it didn't throw
actual: actual && errorString(actual),
expected,
message
});
return actual;
}
rejects(promise, expected, message) {
var _validateExpectedExce3 = validateExpectedExceptionArgs(expected, message, 'rejects');
var _validateExpectedExce4 = _slicedToArray(_validateExpectedExce3, 2);
expected = _validateExpectedExce4[0];
message = _validateExpectedExce4[1];
var currentTest = this instanceof Assert && this.test || config.current;
var then = promise && promise.then;
if (typeof then !== 'function') {
currentTest.assert.pushResult({
result: false,
message: 'The value provided to `assert.rejects` in ' + '"' + currentTest.testName + '" was not a promise.',
actual: promise
});
return;
}
var done = this.async();
return then.call(promise, function handleFulfillment() {
currentTest.assert.pushResult({
result: false,
message: 'The promise returned by the `assert.rejects` callback in ' + '"' + currentTest.testName + '" did not reject.',
actual: promise
});
done();
}, function handleRejection(actual) {
var result;
var _validateException3 = validateException(actual, expected, message);
var _validateException4 = _slicedToArray(_validateException3, 3);
result = _validateException4[0];
expected = _validateException4[1];
message = _validateException4[2];
currentTest.assert.pushResult({
result,
// leave rejection value of undefined as-is
actual: actual && errorString(actual),
expected,
message
});
done();
return actual;
});
}
}
function validateExpectedExceptionArgs(expected, message, assertionMethod) {
var expectedType = objectType(expected);
// 'expected' is optional unless doing string comparison
if (expectedType === 'string') {
if (message === undefined) {
message = expected;
expected = undefined;
return [expected, message];
} else {
// To avoid ambiguity between the `message` and the `expected` argument,
// we cannot support a string as expected value in assert.throws() and assert.rejects().
throw new Error('assert.' + assertionMethod + ' does not accept a string value for the expected argument.\n' + 'Use a non-string object value (e.g. RegExp or validator function) instead if necessary.');
}
}
var valid = !expected // TODO: be more explicit here
|| expectedType === 'regexp' || expectedType === 'function' || expectedType === 'object';
if (!valid) {
throw new Error('Invalid expected value type (' + expectedType + ') ' + 'provided to assert.' + assertionMethod + '.');
}
return [expected, message];
}
function validateException(actual, expected, message) {
var result = false;
var expectedType = objectType(expected);
// These branches should be exhaustive, based on validation done in validateExpectedException
// We don't want to validate
if (!expected) {
result = true;
// Expected is a regexp
} else if (expectedType === 'regexp') {
result = expected.test(errorString(actual));
// Log the string form of the regexp
expected = String(expected);
// Expected is a constructor, maybe an Error constructor.
// Note the extra check on its prototype - this is an implicit
// requirement of "instanceof", else it will throw a TypeError.
} else if (expectedType === 'function' && expected.prototype !== undefined && actual instanceof expected) {
result = true;
// Expected is an Error object
} else if (expectedType === 'object') {
result = actual instanceof expected.constructor && actual.name === expected.name && actual.message === expected.message;
// Log the string form of the Error object
expected = errorString(expected);
// Expected is a validation function which returns true if validation passed
} else if (expectedType === 'function') {
// protect against accidental semantics which could hard error in the test
try {
result = expected.call({}, actual) === true;
expected = null;
} catch (e) {
// assign the "expected" to a nice error string to communicate the local failure to the user
expected = errorString(e);
}
}
return [result, expected, message];
}
// Provide an alternative to assert.throws(), for environments that consider throws a reserved word
// Known to us are: Closure Compiler, Narwhal
// eslint-disable-next-line dot-notation
Assert.prototype.raises = Assert.prototype['throws'];
var SUPPORTED_EVENTS = ['error', 'runStart', 'suiteStart', 'testStart', 'assertion', 'testEnd', 'suiteEnd', 'runEnd'];
var MEMORY_EVENTS = ['error', 'runEnd'];
/**
* Emits an event with the specified data to all currently registered listeners.
* Callbacks will fire in the order in which they are registered (FIFO). This
* function is not exposed publicly; it is used by QUnit internals to emit
* logging events.
*
* @private
* @method emit
* @param {string} eventName
* @param {Object} data
* @return {void}
*/
function emit(eventName, data) {
/* istanbul ignore if: private function */
if (typeof eventName !== 'string') {
throw new TypeError('eventName must be a string when emitting an event');
}
// Clone the callbacks in case one of them registers a new callback
var originalCallbacks = config._event_listeners[eventName];
var callbacks = originalCallbacks ? [...originalCallbacks] : [];
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
if (inArray(eventName, MEMORY_EVENTS)) {
config._event_memory[eventName] = data;
}
}
var prioritySymbol = {};
/**
* Registers a callback as a listener to the specified event.
*
* @public
* @method on
* @param {string} eventName
* @param {Function} callback
* @param {Object} [priority] Internal parameter for PerfReporter
* @return {void}
*/
function on(eventName, callback) {
var priority = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
if (typeof eventName !== 'string') {
throw new TypeError('eventName must be a string when registering a listener');
}
if (!inArray(eventName, SUPPORTED_EVENTS)) {
var events = SUPPORTED_EVENTS.join(', ');
throw new Error("\"".concat(eventName, "\" is not a valid event; must be one of: ").concat(events, "."));
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function when registering a listener');
}
/* istanbul ignore if: internal argument */
if (priority && priority !== prioritySymbol) {
throw new TypeError('invalid priority parameter');
}
var listeners = config._event_listeners[eventName] || (config._event_listeners[eventName] = []);
// Don't register the same callback more than once
if (!inArray(callback, listeners)) {
if (priority === prioritySymbol) {
listeners.unshift(callback);
} else {
listeners.push(callback);
}
if (config._event_memory[eventName] !== undefined) {
callback(config._event_memory[eventName]);
}
}
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var promisePolyfill = {exports: {}};
/*
https://github.com/taylorhakes/promise-polyfill/tree/8.2.0
Copyright 2014 Taylor Hakes
Copyright 2014 Forbes Lindesay
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-------
Patches for use in QUnit:
- 2021-01-09: Export as module only, don't change global scope as QUnit must not
affect the host context (e.g. people may test their application intentionally
with different or no polyfills and we must not affect that).
- 2021-01-10: Avoid unconditional reference to setTimeout, which isn't supported
on SpiderMonkey (mozjs 68). Done by re-arranging the code so that we return early
(it has native support for Promise), instead of building an unused polyfill.
- 2021-01-10: Add 'globalThis' to globalNS implementation to support SpiderMonkey.
- 2024-02-07: Save unmodified setImmediate to avoid conflicts with Sinon fake timers.
*/
var hasRequiredPromisePolyfill;
function requirePromisePolyfill() {
if (hasRequiredPromisePolyfill) return promisePolyfill.exports;
hasRequiredPromisePolyfill = 1;
(function () {
/** @suppress {undefinedVars} */
var globalNS = function () {
// the only reliable means to get the global object is
// `Function('return this')()`
// However, this causes CSP violations in Chrome apps.
if (typeof globalThis !== 'undefined') {
return globalThis;
}
if (typeof self !== 'undefined') {
return self;
}
if (typeof window !== 'undefined') {
return window;
}
if (typeof commonjsGlobal !== 'undefined') {
return commonjsGlobal;
}
throw new Error('unable to locate global object');
}();
// Expose the polyfill if Promise is undefined or set to a
// non-function value. The latter can be due to a named HTMLElement
// being exposed by browsers for legacy reasons.
// https://github.com/taylorhakes/promise-polyfill/issues/114
if (typeof globalNS['Promise'] === 'function') {
promisePolyfill.exports = globalNS['Promise'];
return;
}
/**
* @this {Promise}
*/
function finallyConstructor(callback) {
var constructor = this.constructor;
return this.then(function (value) {
// @ts-ignore
return constructor.resolve(callback()).then(function () {
return value;
});
}, function (reason) {
// @ts-ignore
return constructor.resolve(callback()).then(function () {
// @ts-ignore
return constructor.reject(reason);
});
});
}
function allSettled(arr) {
var P = this;
return new P(function (resolve, reject) {
if (!(arr && typeof arr.length !== 'undefined')) {
return reject(new TypeError(typeof arr + ' ' + arr + ' is not iterable(cannot read property Symbol(Symbol.iterator))'));
}
var args = Array.prototype.slice.call(arr);
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
if (val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if (typeof then === 'function') {
then.call(val, function (val) {
res(i, val);
}, function (e) {
args[i] = {
status: 'rejected',
reason: e
};
if (--remaining === 0) {
resolve(args);
}
});
return;
}
}
args[i] = {
status: 'fulfilled',
value: val
};
if (--remaining === 0) {
resolve(args);
}
}
for (var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
}
// Store setTimeout reference so promise-polyfill will be unaffected by
// other code modifying setTimeout (like sinon.useFakeTimers())
var setTimeoutFunc = setTimeout;
function isArray(x) {
return Boolean(x && typeof x.length !== 'undefined');
}
function noop() {}
// Polyfill for Function.prototype.bind
function bind(fn, thisArg) {
return function () {
fn.apply(thisArg, arguments);
};
}
/**
* @constructor
* @param {Function} fn
*/
function Promise(fn) {
if (!(this instanceof Promise)) throw new TypeError('Promises must be constructed via new');
if (typeof fn !== 'function') throw new TypeError('not a function');
/** @type {!number} */
this._state = 0;
/** @type {!boolean} */
this._handled = false;
/** @type {Promise|undefined} */
this._value = undefined;
/** @type {!Array<!Function>} */
this._deferreds = [];
doResolve(fn, this);
}
function handle(self, deferred) {
while (self._state === 3) {
self = self._value;
}
if (self._state === 0) {
self._deferreds.push(deferred);
return;
}
self._handled = true;
Promise._immediateFn(function () {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
if (cb === null) {
(self._state === 1 ? resolve : reject)(deferred.promise, self._value);
return;
}
var ret;
try {
ret = cb(self._value);
} catch (e) {
reject(deferred.promise, e);
return;
}
resolve(deferred.promise, ret);
});
}
function resolve(self, newValue) {
try {
// Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.');
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (newValue instanceof Promise) {
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
doResolve(bind(then, newValue), self);
return;
}
}
self._state = 1;
self._value = newValue;
finale(self);
} catch (e) {
reject(self, e);
}
}
function reject(self, newValue) {
self._state = 2;
self._value = newValue;
finale(self);
}
function finale(self) {
if (self._state === 2 && self._deferreds.length === 0) {
Promise._immediateFn(function () {
if (!self._handled) {
Promise._unhandledRejectionFn(self._value);
}
});
}
for (var i = 0, len = self._deferreds.length; i < len; i++) {
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
/**
* @constructor
*/
function Handler(onFulfilled, onRejected, promise) {
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
this.promise = promise;
}
/**
* Take a potentially misbehaving resolver function and make sure
* onFulfilled and onRejected are only called once.
*
* Makes no guarantees about asynchrony.
*/
function doResolve(fn, self) {
var done = false;
try {
fn(function (value) {
if (done) return;
done = true;
resolve(self, value);
}, function (reason) {
if (done) return;
done = true;
reject(self, reason);
});
} catch (ex) {
if (done) return;
done = true;
reject(self, ex);
}
}
Promise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
Promise.prototype.then = function (onFulfilled, onRejected) {
// @ts-ignore
var prom = new this.constructor(noop);
handle(this, new Handler(onFulfilled, onRejected, prom));
return prom;
};
Promise.prototype['finally'] = finallyConstructor;
Promise.all = function (arr) {
return new Promise(function (resolve, reject) {
if (!isArray(arr)) {
return reject(new TypeError('Promise.all accepts an array'));
}
var args = Array.prototype.slice.call(arr);
if (args.length === 0) return resolve([]);
var remaining = args.length;
function res(i, val) {
try {
if (val && (typeof val === 'object' || typeof val === 'function')) {
var then = val.then;
if (typeof then === 'function') {
then.call(val, function (val) {
res(i, val);
}, reject);
return;
}
}
args[i] = val;
if (--remaining === 0) {
resolve(args);
}
} catch (ex) {
reject(ex);
}
}
for (var i = 0; i < args.length; i++) {
res(i, args[i]);
}
});
};
Promise.allSettled = allSettled;
Promise.resolve = function (value) {
if (value && typeof value === 'object' && value.constructor === Promise) {
return value;
}
return new Promise(function (resolve) {
resolve(value);
});
};
Promise.reject = function (value) {
return new Promise(function (resolve, reject) {
reject(value);
});
};
Promise.race = function (arr) {
return new Promise(function (resolve, reject) {
if (!isArray(arr)) {
return reject(new TypeError('Promise.race accepts an array'));
}
for (var i = 0, len = arr.length; i < len; i++) {
Promise.resolve(arr[i]).then(resolve, reject);
}
});
};
// Use polyfill for setImmediate for performance gains
// @ts-ignore
if (typeof setImmediate === 'function') {
// @ts-ignore
var setImmediateFunc = setImmediate;
Promise._immediateFn = function (fn) {
setImmediateFunc(fn);
};
} else {
Promise._immediateFn = function (fn) {
setTimeoutFunc(fn, 0);
};
}
Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) {
if (typeof console !== 'undefined' && console) {
console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console
}
};
promisePolyfill.exports = Promise;
})();
return promisePolyfill.exports;
}
var promisePolyfillExports = requirePromisePolyfill();
var _Promise = /*@__PURE__*/getDefaultExportFromCjs(promisePolyfillExports);
function createRegisterCallbackFunction(key) {
// Initialize key collection of callback
if (typeof config.callbacks[key] === 'undefined') {
config.callbacks[key] = [];
}
return function registerCallback(callback) {
var priority = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (typeof callback !== 'function') {
throw new TypeError('Callback parameter must be a function');
}
/* istanbul ignore if: internal argument */
if (priority && priority !== prioritySymbol) {
throw new TypeError('invalid priority parameter');
}
if (priority === prioritySymbol) {
config.callbacks[key].unshift(callback);
} else {
config.callbacks[key].push(callback);
}
};
}
function runLoggingCallbacks(key, arg) {
var callbacks = config.callbacks[key];
// Handling 'log' callbacks separately. Unlike the other callbacks,
// the log callback is not controlled by the processing queue,
// but rather used by asserts. Hence to promisify the 'log' callback
// would mean promisifying each step of a test
if (key === 'log') {
callbacks.map(callback => callback(arg));
return;
}
// ensure that each callback is executed serially
var promiseChain = _Promise.resolve();
callbacks.forEach(callback => {
promiseChain = promiseChain.then(() => {
return _Promise.resolve(callback(arg));
});
});
return promiseChain;
}
/* eslint-disable indent */
/*
* This file is a modified version of google-diff-match-patch's JavaScript implementation
* (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
* modifications are licensed as more fully set forth in LICENSE.txt.
*
* The original source of google-diff-match-patch is attributable and licensed as follows:
*
* Copyright 2006 Google Inc.
* https://code.google.com/p/google-diff-match-patch/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* More Info:
* https://code.google.com/p/google-diff-match-patch/
*
* Usage: QUnit.diff(expected, actual)
*
*/
function DiffMatchPatch() {}
// DIFF FUNCTIONS
/**
* The data structure representing a diff is an array of tuples:
* [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
* which means: delete 'Hello', add 'Goodbye' and keep ' world.'
*/
var DIFF_DELETE = -1;
var DIFF_INSERT = 1;
var DIFF_EQUAL = 0;
var hasOwn$2 = Object.prototype.hasOwnProperty;
/**
* Find the differences between two texts. Simplifies the problem by stripping
* any common prefix or suffix off the texts before diffing.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {boolean=} optChecklines Optional speedup flag. If present and false,
* then don't run a line-level diff first to identify the changed areas.
* Defaults to true, which does a faster, slightly less optimal diff.
* @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
*/
DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) {
// The diff must be complete in up to 1 second.
var deadline = Date.now() + 1000;
// Check for null inputs.
if (text1 === null || text2 === null) {
throw new Error('Cannot diff null input.');
}
// Check for equality (speedup).
if (text1 === text2) {
if (text1) {
return [[DIFF_EQUAL, text1]];
}
return [];
}
if (typeof optChecklines === 'undefined') {
optChecklines = true;
}
// Trim off common prefix (speedup).
var commonlength = this.diffCommonPrefix(text1, text2);
var commonprefix = text1.substring(0, commonlength);
text1 = text1.substring(commonlength);
text2 = text2.substring(commonlength);
// Trim off common suffix (speedup).
commonlength = this.diffCommonSuffix(text1, text2);
var commonsuffix = text1.substring(text1.length - commonlength);
text1 = text1.substring(0, text1.length - commonlength);
text2 = text2.substring(0, text2.length - commonlength);
// Compute the diff on the middle block.
var diffs = this.diffCompute(text1, text2, optChecklines, deadline);
// Restore the prefix and suffix.
if (commonprefix) {
diffs.unshift([DIFF_EQUAL, commonprefix]);
}
if (commonsuffix) {
diffs.push([DIFF_EQUAL, commonsuffix]);
}
this.diffCleanupMerge(diffs);
return diffs;
};
/**
* Reduce the number of edits by eliminating operationally trivial equalities.
* @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
*/
DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) {
var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel;
changes = false;
equalities = []; // Stack of indices where equalities are found.
equalitiesLength = 0; // Keeping our own length var is faster in JS.
/** @type {?string} */
lastequality = null;
// Always equal to diffs[equalities[equalitiesLength - 1]][1]
pointer = 0; // Index of current position.
// Is there an insertion operation before the last equality.
preIns = false;
// Is there a deletion operation before the last equality.
preDel = false;
// Is there an insertion operation after the last equality.
postIns = false;
// Is there a deletion operation after the last equality.
postDel = false;
while (pointer < diffs.length) {
// Equality found.
if (diffs[pointer][0] === DIFF_EQUAL) {
if (diffs[pointer][1].length < 4 && (postIns || postDel)) {
// Candidate found.
equalities[equalitiesLength++] = pointer;
preIns = postIns;
preDel = postDel;
lastequality = diffs[pointer][1];
} else {
// Not a candidate, and can never become one.
equalitiesLength = 0;
lastequality = null;
}
postIns = postDel = false;
// An insertion or deletion.
} else {
if (diffs[pointer][0] === DIFF_DELETE) {
postDel = true;
} else {
postIns = true;
}
/*
* Five types to be split:
* <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
* <ins>A</ins>X<ins>C</ins><del>D</del>
* <ins>A</ins><del>B</del>X<ins>C</ins>
* <ins>A</del>X<ins>C</ins><del>D</del>
* <ins>A</ins><del>B</del>X<del>C</del>
*/
if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) {
// Duplicate record.
diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]);
// Change second copy to insert.
diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
equalitiesLength--; // Throw away the equality we just deleted;
lastequality = null;
if (preIns && preDel) {
// No changes made which could affect previous entry, keep going.
postIns = postDel = true;
equalitiesLength = 0;
} else {
equalitiesLength--; // Throw away the previous equality.
pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
postIns = postDel = false;
}
changes = true;
}
}
pointer++;
}
if (changes) {
this.diffCleanupMerge(diffs);
}
};
/**
* Convert a diff array into a pretty HTML report.
* @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
* @param {integer} string to be beautified.
* @return {string} HTML representation.
*/
DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) {
var html = [];
for (var x = 0; x < diffs.length; x++) {
var op = diffs[x][0]; // Operation (insert, delete, equal)
var data = diffs[x][1]; // Text of change.
switch (op) {
case DIFF_INSERT:
html[x] = '<ins>' + escapeText(data) + '</ins>';
break;
case DIFF_DELETE:
html[x] = '<del>' + escapeText(data) + '</del>';
break;
case DIFF_EQUAL:
html[x] = '<span>' + escapeText(data) + '</span>';
break;
}
}
return html.join('');
};
/**
* Determine the common prefix of two strings.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the start of each
* string.
*/
DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) {
var pointermid, pointermax, pointermin, pointerstart;
// Quick check for common null cases.
if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) {
return 0;
}
// Binary search.
// Performance analysis: https://neil.fraser.name/news/2007/10/09/
pointermin = 0;
pointermax = Math.min(text1.length, text2.length);
pointermid = pointermax;
pointerstart = 0;
while (pointermin < pointermid) {
if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) {
pointermin = pointermid;
pointerstart = pointermin;
} else {
pointermax = pointermid;
}
pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
}
return pointermid;
};
/**
* Determine the common suffix of two strings.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the end of each string.
*/
DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) {
var pointermid, pointermax, pointermin, pointerend;
// Quick check for common null cases.
if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) {
return 0;
}
// Binary search.
// Performance analysis: https://neil.fraser.name/news/2007/10/09/
pointermin = 0;
pointermax = Math.min(text1.length, text2.length);
pointermid = pointermax;
pointerend = 0;
while (pointermin < pointermid) {
if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) {
pointermin = pointermid;
pointerend = pointermin;
} else {
pointermax = pointermid;
}
pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
}
return pointermid;
};
/**
* Find the differences between two texts. Assumes that the texts do not
* have any common prefix or suffix.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {boolean} checklines Speedup flag. If false, then don't run a
* line-level diff first to identify the changed areas.
* If true, then run a faster, slightly less optimal diff.
* @param {number} deadline Time when the diff should be complete by.
* @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
* @private
*/
DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) {
var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB;
if (!text1) {
// Just add some text (speedup).
return [[DIFF_INSERT, text2]];
}
if (!text2) {
// Just delete some text (speedup).
return [[DIFF_DELETE, text1]];
}
longtext = text1.length > text2.length ? text1 : text2;
shorttext = text1.length > text2.length ? text2 : text1;
i = longtext.indexOf(shorttext);
if (i !== -1) {
// Shorter text is inside the longer text (speedup).
diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
// Swap insertions for deletions if diff is reversed.
if (text1.length > text2.length) {
diffs[0][0] = diffs[2][0] = DIFF_DELETE;
}
return diffs;
}
if (shorttext.length === 1) {
// Single character string.
// After the previous speedup, the character can't be an equality.
return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
}
// Check to see if the problem can be split in two.
hm = this.diffHalfMatch(text1, text2);
if (hm) {
// A half-match was found, sort out the return data.
text1A = hm[0];
text1B = hm[1];
text2A = hm[2];
text2B = hm[3];
midCommon = hm[4];
// Send both pairs off for separate processing.
diffsA = this.DiffMain(text1A, text2A, checklines, deadline);
diffsB = this.DiffMain(text1B, text2B, checklines, deadline);
// Merge the results.
return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB);
}
if (checklines && text1.length > 100 && text2.length > 100) {
return this.diffLineMode(text1, text2, deadline);
}
return this.diffBisect(text1, text2, deadline);
};
/**
* Do the two texts share a substring which is at least half the length of the
* longer text?
* This speedup can produce non-minimal diffs.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {Array.<string>} Five element Array, containing the prefix of
* text1, the suffix of text1, the prefix of text2, the suffix of
* text2 and the common middle. Or null if there was no match.
* @private
*/
DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) {
var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm;
longtext = text1.length > text2.length ? text1 : text2;
shorttext = text1.length > text2.length ? text2 : text1;
if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
return null; // Pointless.
}
dmp = this; // 'this' becomes 'window' in a closure.
/**
* Does a substring of shorttext exist within longtext such that the substring
* is at least half the length of longtext?
* Closure, but does not reference any external variables.
* @param {string} longtext Longer string.
* @param {string} shorttext Shorter string.
* @param {number} i Start index of quarter length substring within longtext.
* @return {Array.<string>} Five element Array, containing the prefix of
* longtext, the suffix of longtext, the prefix of shorttext, the suffix
* of shorttext and the common middle. Or null if there was no match.
* @private
*/
function diffHalfMatchI(longtext, shorttext, i) {
var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
// Start with a 1/4 length substring at position i as a seed.
seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
j = -1;
bestCommon = '';
while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j));
suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j));
if (bestCommon.length < suffixLength + prefixLength) {
bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength);
bestLongtextA = longtext.substring(0, i - suffixLength);
bestLongtextB = longtext.substring(i + prefixLength);
bestShorttextA = shorttext.substring(0, j - suffixLength);
bestShorttextB = shorttext.substring(j + prefixLength);
}
}
if (bestCommon.length * 2 >= longtext.length) {
return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon];
} else {
return null;
}
}
// First check if the second quarter is the seed for a half-match.
hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4));
// Check again based on the third quarter.
hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2));
if (!hm1 && !hm2) {
return null;
} else if (!hm2) {
hm = hm1;
} else if (!hm1) {
hm = hm2;
} else {
// Both matched. Select the longest.
hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
}
// A half-match was found, sort out the return data.
if (text1.length > text2.length) {
text1A = hm[0];
text1B = hm[1];
text2A = hm[2];
text2B = hm[3];
} else {
text2A = hm[0];
text2B = hm[1];
text1A = hm[2];
text1B = hm[3];
}
midCommon = hm[4];
return [text1A, text1B, text2A, text2B, midCommon];
};
/**
* Do a quick line-level diff on both strings, then rediff the parts for
* greater accuracy.
* This speedup can produce non-minimal diffs.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {number} deadline Time when the diff should be complete by.
* @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
* @private
*/
DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) {
var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j;
// Scan the text on a line-by-line basis first.
a = this.diffLinesToChars(text1, text2);
text1 = a.chars1;
text2 = a.chars2;
linearray = a.lineArray;
diffs = this.DiffMain(text1, text2, false, deadline);
// Convert the diff back to original text.
this.diffCharsToLines(diffs, linearray);
// Eliminate freak matches (e.g. blank lines)
this.diffCleanupSemantic(diffs);
// Rediff any replacement blocks, this time character-by-character.
// Add a dummy entry at the end.
diffs.push([DIFF_EQUAL, '']);
pointer = 0;
countDelete = 0;
countInsert = 0;
textDelete = '';
textInsert = '';
while (pointer < diffs.length) {
switch (diffs[pointer][0]) {
case DIFF_INSERT:
countInsert++;
textInsert += diffs[pointer][1];
break;
case DIFF_DELETE:
countDelete++;
textDelete += diffs[pointer][1];
break;
case DIFF_EQUAL:
// Upon reaching an equality, check for prior redundancies.
if (countDelete >= 1 && countInsert >= 1) {
// Delete the offending records and add the merged ones.
diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert);
pointer = pointer - countDelete - countInsert;
a = this.DiffMain(textDelete, textInsert, false, deadline);
for (j = a.length - 1; j >= 0; j--) {
diffs.splice(pointer, 0, a[j]);
}
pointer = pointer + a.length;
}
countInsert = 0;
countDelete = 0;
textDelete = '';
textInsert = '';
break;
}
pointer++;
}
diffs.pop(); // Remove the dummy entry at the end.
return diffs;
};
/**
* Find the 'middle snake' of a diff, split the problem in two
* and return the recursively constructed diff.
* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {number} deadline Time at which to bail if not yet complete.
* @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
* @private
*/
DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) {
var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
// Cache the text lengths to prevent multiple calls.
text1Length = text1.length;
text2Length = text2.length;
maxD = Math.ceil((text1Length + text2Length) / 2);
vOffset = maxD;
vLength = 2 * maxD;
v1 = new Array(vLength);
v2 = new Array(vLength);
// Setting all elements to -1 is faster in Chrome & Firefox than mixing
// integers and undefined.
for (x = 0; x < vLength; x++) {
v1[x] = -1;
v2[x] = -1;
}
v1[vOffset + 1] = 0;
v2[vOffset + 1] = 0;
delta = text1Length - text2Length;
// If the total number of characters is odd, then the front path will collide
// with the reverse path.
front = delta % 2 !== 0;
// Offsets for start and end of k loop.
// Prevents mapping of space beyond the grid.
k1start = 0;
k1end = 0;
k2start = 0;
k2end = 0;
for (d = 0; d < maxD; d++) {
// Bail out if deadline is reached.
if (Date.now() > deadline) {
break;
}
// Walk the front path one step.
for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
k1Offset = vOffset + k1;
if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) {
x1 = v1[k1Offset + 1];
} else {
x1 = v1[k1Offset - 1] + 1;
}
y1 = x1 - k1;
while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) {
x1++;
y1++;
}
v1[k1Offset] = x1;
if (x1 > text1Length) {
// Ran off the right of the graph.
k1end += 2;
} else if (y1 > text2Length) {
// Ran off the bottom of the graph.
k1start += 2;
} else if (front) {
k2Offset = vOffset + delta - k1;
if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) {
// Mirror x2 onto top-left coordinate system.
x2 = text1Length - v2[k2Offset];
if (x1 >= x2) {
// Overlap detected.
return this.diffBisectSplit(text1, text2, x1, y1, deadline);
}
}
}
}
// Walk the reverse path one step.
for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
k2Offset = vOffset + k2;
if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) {
x2 = v2[k2Offset + 1];
} else {
x2 = v2[k2Offset - 1] + 1;
}
y2 = x2 - k2;
while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) {
x2++;
y2++;
}
v2[k2Offset] = x2;
if (x2 > text1Length) {
// Ran off the left of the graph.
k2end += 2;
} else if (y2 > text2Length) {
// Ran off the top of the graph.
k2start += 2;
} else if (!front) {
k1Offset = vOffset + delta - k2;
if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) {
x1 = v1[k1Offset];
y1 = vOffset + x1 - k1Offset;
// Mirror x2 onto top-left coordinate system.
x2 = text1Length - x2;
if (x1 >= x2) {
// Overlap detected.
return this.diffBisectSplit(text1, text2, x1, y1, deadline);
}
}
}
}
}
// Diff took too long and hit the deadline or
// number of diffs equals number of characters, no commonality at all.
return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
};
/**
* Given the location of the 'middle snake', split the diff in two parts
* and recurse.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {number} x Index of split point in text1.
* @param {number} y Index of split point in text2.
* @param {number} deadline Time at which to bail if not yet complete.
* @return {!Array.<!DiffMatchPatch.Diff>} Array of diff tuples.
* @private
*/
DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) {
var text1a, text1b, text2a, text2b, diffs, diffsb;
text1a = text1.substring(0, x);
text2a = text2.substring(0, y);
text1b = text1.substring(x);
text2b = text2.substring(y);
// Compute both diffs serially.
diffs = this.DiffMain(text1a, text2a, false, deadline);
diffsb = this.DiffMain(text1b, text2b, false, deadline);
return diffs.concat(diffsb);
};
/**
* Reduce the number of edits by eliminating semantically trivial equalities.
* @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
*/
DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) {
var changes = false;
var equalities = []; // Stack of indices where equalities are found.
var equalitiesLength = 0; // Keeping our own length var is faster in JS.
/** @type {?string} */
var lastequality = null;
// Always equal to diffs[equalities[equalitiesLength - 1]][1]
var pointer = 0; // Index of current position.
// Number of characters that changed prior to the equality.
var lengthInsertions1 = 0;
var lengthDeletions1 = 0;
// Number of characters that changed after the equality.
var lengthInsertions2 = 0;
var lengthDeletions2 = 0;
while (pointer < diffs.length) {
if (diffs[pointer][0] === DIFF_EQUAL) {
// Equality found.
equalities[equalitiesLength++] = pointer;
lengthInsertions1 = lengthInsertions2;
lengthDeletions1 = lengthDeletions2;
lengthInsertions2 = 0;
lengthDeletions2 = 0;
lastequality = diffs[pointer][1];
} else {
// An insertion or deletion.
if (diffs[pointer][0] === DIFF_INSERT) {
lengthInsertions2 += diffs[pointer][1].length;
} else {
lengthDeletions2 += diffs[pointer][1].length;
}
// Eliminate an equality that is smaller or equal to the edits on both
// sides of it.
if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) {
// Duplicate record.
diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]);
// Change second copy to insert.
diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
// Throw away the equality we just deleted.
equalitiesLength--;
// Throw away the previous equality (it needs to be reevaluated).
equalitiesLength--;
pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
// Reset the counters.
lengthInsertions1 = 0;
lengthDeletions1 = 0;
lengthInsertions2 = 0;
lengthDeletions2 = 0;
lastequality = null;
changes = true;
}
}
pointer++;
}
// Normalize the diff.
if (changes) {
this.diffCleanupMerge(diffs);
}
var deletion, insertion, overlapLength1, overlapLength2;
// Find any overlaps between deletions and insertions.
// e.g: <del>abcxxx</del><ins>xxxdef</ins>
// -> <del>abc</del>xxx<ins>def</ins>
// e.g: <del>xxxabc</del><ins>defxxx</ins>
// -> <ins>def</ins>xxx<del>abc</del>
// Only extract an overlap if it is as big as the edit ahead or behind it.
pointer = 1;
while (pointer < diffs.length) {
if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) {
deletion = diffs[pointer - 1][1];
insertion = diffs[pointer][1];
overlapLength1 = this.diffCommonOverlap(deletion, insertion);
overlapLength2 = this.diffCommonOverlap(insertion, deletion);
if (overlapLength1 >= overlapLength2) {
if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) {
// Overlap found. Insert an equality and trim the surrounding edits.
diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]);
diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1);
diffs[pointer + 1][1] = insertion.substring(overlapLength1);
pointer++;
}
} else {
if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) {
// Reverse overlap found.
// Insert an equality and swap and trim the surrounding edits.
diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]);
diffs[pointer - 1][0] = DIFF_INSERT;
diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2);
diffs[pointer + 1][0] = DIFF_DELETE;
diffs[pointer + 1][1] = deletion.substring(overlapLength2);
pointer++;
}
}
pointer++;
}
pointer++;
}
};
/**
* Determine if the suffix of one string is the prefix of another.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the end of the first
* string and the start of the second string.
* @private
*/
DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) {
// Cache the text lengths to prevent multiple calls.
var text1Length = text1.length;
var text2Length = text2.length;
// Eliminate the null case.
if (text1Length === 0 || text2Length === 0) {
return 0;
}
// Truncate the longer string.
if (text1Length > text2Length) {
text1 = text1.substring(text1Length - text2Length);
} else if (text1Length < text2Length) {
text2 = text2.substring(0, text1Length);
}
var textLength = Math.min(text1Length, text2Length);
// Quick check for the worst case.
if (text1 === text2) {
return textLength;
}
// Start by looking for a single character match
// and increase length until no match is found.
// Performance analysis: https://neil.fraser.name/news/2010/11/04/
var best = 0;
var length = 1;
while (true) {
var pattern = text1.substring(textLength - length);
var found = text2.indexOf(pattern);
if (found === -1) {
return best;
}
length += found;
if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) {
best = length;
length++;
}
}
};
/**
* Split two texts into an array of strings. Reduce the texts to a string of
* hashes where each Unicode character represents one line.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
* An object containing the encoded text1, the encoded text2 and
* the array of unique strings.
* The zeroth element of the array of unique strings is intentionally blank.
* @private
*/
DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) {
var lineArray = []; // E.g. lineArray[4] === 'Hello\n'
var lineHash = {}; // E.g. lineHash['Hello\n'] === 4
// '\x00' is a valid character, but various debuggers don't like it.
// So we'll insert a junk entry to avoid generating a null character.
lineArray[0] = '';
/**
* Split a text into an array of strings. Reduce the texts to a string of
* hashes where each Unicode character represents one line.
* Modifies linearray and linehash through being a closure.
* @param {string} text String to encode.
* @return {string} Encoded string.
* @private
*/
function diffLinesToCharsMunge(text) {
var chars = '';
// Walk the text, pulling out a substring for each line.
// text.split('\n') would would temporarily double our memory footprint.
// Modifying text would create many large strings to garbage collect.
var lineStart = 0;
var lineEnd = -1;
// Keeping our own length variable is faster than looking it up.
var lineArrayLength = lineArray.length;
while (lineEnd < text.length - 1) {
lineEnd = text.indexOf('\n', lineStart);
if (lineEnd === -1) {
lineEnd = text.length - 1;
}
var line = text.substring(lineStart, lineEnd + 1);
lineStart = lineEnd + 1;
if (hasOwn$2.call(lineHash, line)) {
chars += String.fromCharCode(lineHash[line]);
} else {
chars += String.fromCharCode(lineArrayLength);
lineHash[line] = lineArrayLength;
lineArray[lineArrayLength++] = line;
}
}
return chars;
}
var chars1 = diffLinesToCharsMunge(text1);
var chars2 = diffLinesToCharsMunge(text2);
return {
chars1: chars1,
chars2: chars2,
lineArray: lineArray
};
};
/**
* Rehydrate the text in a diff from a string of line hashes to real lines of
* text.
* @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
* @param {!Array.<string>} lineArray Array of unique strings.
* @private
*/
DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) {
for (var x = 0; x < diffs.length; x++) {
var chars = diffs[x][1];
var text = [];
for (var y = 0; y < chars.length; y++) {
text[y] = lineArray[chars.charCodeAt(y)];
}
diffs[x][1] = text.join('');
}
};
/**
* Reorder and merge like edit sections. Merge equalities.
* Any edit section can move as long as it doesn't cross an equality.
* @param {!Array.<!DiffMatchPatch.Diff>} diffs Array of diff tuples.
*/
DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) {
diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end.
var pointer = 0;
var countDelete = 0;
var countInsert = 0;
var textDelete = '';
var textInsert = '';
while (pointer < diffs.length) {
switch (diffs[pointer][0]) {
case DIFF_INSERT:
countInsert++;
textInsert += diffs[pointer][1];
pointer++;
break;
case DIFF_DELETE:
countDelete++;
textDelete += diffs[pointer][1];
pointer++;
break;
case DIFF_EQUAL:
// Upon reaching an equality, check for prior redundancies.
if (countDelete + countInsert > 1) {
if (countDelete !== 0 && countInsert !== 0) {
// Factor out any common prefixes.
var commonlength = this.diffCommonPrefix(textInsert, textDelete);
if (commonlength !== 0) {
if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) {
diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength);
} else {
diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]);
pointer++;
}
textInsert = textInsert.substring(commonlength);
textDelete = textDelete.substring(commonlength);
}
// Factor out any common suffixes.
commonlength = this.diffCommonSuffix(textInsert, textDelete);
if (commonlength !== 0) {
diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1];
textInsert = textInsert.substring(0, textInsert.length - commonlength);
textDelete = textDelete.substring(0, textDelete.length - commonlength);
}
}
// Delete the offending records and add the merged ones.
if (countDelete === 0) {
diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]);
} else if (countInsert === 0) {
diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]);
} else {
diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]);
}
pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1;
} else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
// Merge this equality with the previous one.
diffs[pointer - 1][1] += diffs[pointer][1];
diffs.splice(pointer, 1);
} else {
pointer++;
}
countInsert = 0;
countDelete = 0;
textDelete = '';
textInsert = '';
break;
}
}
if (diffs[diffs.length - 1][1] === '') {
diffs.pop(); // Remove the dummy entry at the end.
}
// Second pass: look for single edits surrounded on both sides by equalities
// which can be shifted sideways to eliminate an equality.
// e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
var changes = false;
pointer = 1;
// Intentionally ignore the first and last element (don't need checking).
while (pointer < diffs.length - 1) {
if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) {
var diffPointer = diffs[pointer][1];
var position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length);
// This is a single edit surrounded by equalities.
if (position === diffs[pointer - 1][1]) {
// Shift the edit over the previous equality.
diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length);
diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
diffs.splice(pointer - 1, 1);
changes = true;
} else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) {
// Shift the edit over the next equality.
diffs[pointer - 1][1] += diffs[pointer + 1][1];
diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1];
diffs.splice(pointer + 1, 1);
changes = true;
}
}
pointer++;
}
// If shifts were made, the diff needs reordering and another shift sweep.
if (changes) {
this.diffCleanupMerge(diffs);
}
};
function diff(o, n) {
var diff, output, text;
diff = new DiffMatchPatch();
output = diff.DiffMain(o, n);
diff.diffCleanupEfficiency(output);
text = diff.diffPrettyHtml(output);
return text;
}
function makeAddGlobalHook(hookName) {
return function addGlobalHook(callback) {
if (!config._globalHooks[hookName]) {
config._globalHooks[hookName] = [];
}
config._globalHooks[hookName].push(callback);
};
}
var hooks = {
beforeEach: makeAddGlobalHook('beforeEach'),
afterEach: makeAddGlobalHook('afterEach')
};
class SuiteReport {
constructor(name, parentSuite) {
this.name = name;
this.fullName = parentSuite ? parentSuite.fullName.concat(name) : [];
// When an "error" event is emitted from onUncaughtException(), the
// "runEnd" event should report the status as failed. The "runEnd" event data
// is tracked through this property (via the "globalSuiteReport" instance).
this.globalFailureCount = 0;
this.tests = [];
this.childSuites = [];
if (parentSuite) {
parentSuite.pushChildSuite(this);
}
}
start(recordTime) {
if (recordTime) {
this._startTime = performance.now();
}
return {
name: this.name,
fullName: this.fullName.slice(),
tests: this.tests.map(test => test.start()),
childSuites: this.childSuites.map(suite => suite.start()),
testCounts: {
// TODO: Due to code reuse, this ends up computing getStatus()
// for tests that obviously haven't run yet. It's harmless but
// quite inefficient recursion, that we repeat many times over
// also via test.start() and suite.start() above.
total: this.getTestCounts().total
}
};
}
end(recordTime) {
if (recordTime) {
this._endTime = performance.now();
}
return {
name: this.name,
fullName: this.fullName.slice(),
tests: this.tests.map(test => test.end()),
childSuites: this.childSuites.map(suite => suite.end()),
testCounts: this.getTestCounts(),
runtime: this.getRuntime(),
status: this.getStatus()
};
}
pushChildSuite(suite) {
this.childSuites.push(suite);
}
pushTest(test) {
this.tests.push(test);
}
getRuntime() {
return Math.round(this._endTime - this._startTime);
}
getTestCounts() {
var counts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
passed: 0,
failed: 0,
skipped: 0,
todo: 0,
total: 0
};
counts.failed += this.globalFailureCount;
counts.total += this.globalFailureCount;
counts = this.tests.reduce((counts, test) => {
if (test.valid) {
counts[test.getStatus()]++;
counts.total++;
}
return counts;
}, counts);
return this.childSuites.reduce((counts, suite) => {
return suite.getTestCounts(counts);
}, counts);
}
getStatus() {
var _this$getTestCounts = this.getTestCounts(),
total = _this$getTestCounts.total,
failed = _this$getTestCounts.failed,
skipped = _this$getTestCounts.skipped,
todo = _this$getTestCounts.todo;
if (failed) {
return 'failed';
} else {
if (skipped === total) {
return 'skipped';
} else if (todo === total) {
return 'todo';
} else {
return 'passed';
}
}
}
}
var globalSuiteReport = new SuiteReport();
// This also pushes the unnamed module to config.modules.
// It is important that we not push this into the moduleStack,
// since it is not meant to be a parent to any other modules.
var unnamedModule = createModule('', {}, {}, globalSuiteReport);
function isParentModuleInQueue() {
var modulesInQueue = config.modules.filter(module => !module.ignored).map(module => module.moduleId);
return config._moduleStack.some(module => modulesInQueue.includes(module.moduleId));
}
/**
* This does:
* - Create a module object
* - Link it to and from a parent module (if one is on the stack)
* - Link it to a parent SuiteReport (if one is on the stack)
* - Push it to `config.modules`
*
* It does NOT push it to config._moduleStack. That's only relevant for
* scoped modules, and is the responsibility of processModule().
*
* @param {string} name
* @param {Object} testEnvironment
* @param {Object} modifiers
* @param {SuiteReport} suiteReport Force the report, for use by the initial unnamedModule
* @return {Object}
*/
function createModule(name, testEnvironment, modifiers, suiteReport) {
var parentModule = config._moduleStack.length ? config._moduleStack[config._moduleStack.length - 1] : null;
var moduleName = parentModule !== null ? [parentModule.name, name].join(' > ') : name;
var skip = parentModule !== null && parentModule.skip || modifiers.skip;
var todo = parentModule !== null && parentModule.todo || modifiers.todo;
var module = {
name: moduleName,
parentModule: parentModule,
hooks: {
before: [],
beforeEach: [],
afterEach: [],
after: []
},
testEnvironment: extend(
// Live inheritance as of QUnit 3. https://github.com/qunitjs/qunit/pull/1762
parentModule ? Object.create(parentModule.testEnvironment || {}) : {}, testEnvironment),
tests: [],
moduleId: generateHash(moduleName),
testsRun: 0,
testsIgnored: 0,
childModules: [],
suiteReport: suiteReport || new SuiteReport(name, parentModule ? parentModule.suiteReport : globalSuiteReport),
// Initialised by test.js when the module start executing,
// i.e. before the first test in this module (or a child).
stats: null,
// Pass along `skip` and `todo` properties from parent module, in case
// there is one, to children. And use own otherwise.
// This property will be used to mark own tests and tests of child suites
// as either `skipped` or `todo`.
skip: skip,
todo: skip ? false : todo,
ignored: modifiers.ignored || false
};
if (parentModule) {
parentModule.childModules.push(module);
}
config.modules.push(module);
return module;
}
function setHookFromEnvironment(environment, hooks, name) {
var potentialHook = environment[name];
if (typeof potentialHook === 'function') {
hooks[name].push(potentialHook);
}
delete environment[name];
}
function makeSetHook(module, hookName) {
return function setHook(callback) {
if (config.currentModule !== module) {
throw new Error("Cannot add ".concat(hookName, " hook outside the containing module. Called on \"").concat(module.name, "\", instead of expected \"").concat(config.currentModule.name, "\". https://qunitjs.com/api/QUnit/module/#E0002"));
}
module.hooks[hookName].push(callback);
};
}
function processModule(name, options, scope) {
var modifiers = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
if (typeof options === 'function') {
scope = options;
options = undefined;
}
if (isAsyncFunction(scope)) {
throw new TypeError('QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks');
}
var module = createModule(name, options, modifiers);
// Transfer any initial hooks from the options object to the 'hooks' object
var testEnvironment = module.testEnvironment;
var hooks = module.hooks;
setHookFromEnvironment(testEnvironment, hooks, 'before');
setHookFromEnvironment(testEnvironment, hooks, 'beforeEach');
setHookFromEnvironment(testEnvironment, hooks, 'afterEach');
setHookFromEnvironment(testEnvironment, hooks, 'after');
var moduleFns = {
before: makeSetHook(module, 'before'),
beforeEach: makeSetHook(module, 'beforeEach'),
afterEach: makeSetHook(module, 'afterEach'),
after: makeSetHook(module, 'after')
};
var prevModule = config.currentModule;
config.currentModule = module;
if (typeof scope === 'function') {
config._moduleStack.push(module);
try {
var cbReturnValue = scope.call(module.testEnvironment, moduleFns);
if (cbReturnValue && typeof cbReturnValue.then === 'function') {
throw new TypeError('QUnit.module() callback must not be async. For async module setup, use hooks. https://qunitjs.com/api/QUnit/module/#hooks');
}
} finally {
// If the module closure threw an uncaught error during the load phase,
// we let this bubble up to global error handlers. But, not until after
// we teardown internal state to ensure correct module nesting.
// Ref https://github.com/qunitjs/qunit/issues/1478.
config._moduleStack.pop();
config.currentModule = module.parentModule || prevModule;
}
}
}
/**
* Clear the SuiteReport tree of all tests and leave only current module as child suite
*
* This should be called before defining the first module.only() or test.only()
* because otherwise:
* - `runEnd.testCounts` is too high.
* - UI (HtmlReporter) and TAP (TapReporter) display totals too high.
* - Test runners like QTap might timeout because the TAP plan
* would be printed as "1..9" even if only 2 tests are run,
* which means tap-finished will wait for 3-9.
*/
function clearSuiteReports(currentModule) {
var childSuite = null;
var suiteReport = currentModule.suiteReport;
while (suiteReport) {
suiteReport.tests.length = 0;
var i = suiteReport.childSuites.indexOf(childSuite);
if (i === -1) {
suiteReport.childSuites.length = 0;
} else {
// Reduce in-place to just currentModule.suiteReport or its intermediary
suiteReport.childSuites.splice(0, i);
suiteReport.childSuites.splice(1);
}
if (suiteReport === globalSuiteReport) {
suiteReport = null;
} else {
childSuite = suiteReport;
currentModule = currentModule.parentModule;
suiteReport = currentModule && currentModule.suiteReport || globalSuiteReport;
}
}
}
var focused$1 = false; // indicates that the "only" filter was used
function module$1(name, options, scope) {
var ignored = focused$1 && !isParentModuleInQueue();
processModule(name, options, scope, {
ignored
});
}
module$1.only = function () {
if (!focused$1) {
// Upon the first module.only() call,
// delete any and all previously registered modules and tests.
config.modules.length = 0;
config.queue.length = 0;
clearSuiteReports(config.currentModule);
// Ignore any tests declared after this block within the same
// module parent. https://github.com/qunitjs/qunit/issues/1645
config.currentModule.ignored = true;
}
focused$1 = true;
processModule(...arguments);
};
module$1.skip = function (name, options, scope) {
if (focused$1) {
return;
}
processModule(name, options, scope, {
skip: true
});
};
module$1.if = function (name, condition, options, scope) {
if (focused$1) {
return;
}
processModule(name, options, scope, {
skip: !condition
});
};
module$1.todo = function (name, options, scope) {
if (focused$1) {
return;
}
processModule(name, options, scope, {
todo: true
});
};
/**
* Handle a global error that should result in a failed test run.
*
* Summary:
*
* - If we're strictly inside a test (or one if its module hooks), the exception
* becomes a failed assertion.
*
* This has the important side-effect that uncaught exceptions (such as
* calling an undefined function) during a "todo" test do NOT result in
* a failed test run.
*
* - If we're anywhere outside a test (be it in early event callbacks, or
* internally between tests, or somewhere after "runEnd" if the process is
* still alive for some reason), then send an "error" event to the reporters.
*
* @since 2.17.0
* @param {Error|any} error
*/
function onUncaughtException(error) {
if (config.current) {
// This omits 'actual' and 'expected' (undefined)
config.current.assert.pushResult({
result: false,
message: "global failure: ".concat(errorString(error)),
// We could let callers specify an offset to subtract a number of frames via
// sourceFromStacktrace, in case they are a wrapper further away from the error
// handler, and thus reduce some noise in the stack trace. However, we're not
// doing this right now because it would almost never be used in practice given
// the vast majority of error values will be Error objects, and thus have their
// own stack trace already.
source: error && error.stack || sourceFromStacktrace(2)
});
} else {
// The "error" event was added in QUnit 2.17.
// Increase "bad assertion" stats despite no longer pushing an assertion in this case.
// This ensures "runEnd" and "QUnit.done()" handlers behave as expected, since the "bad"
// count is typically how reporters decide on the boolean outcome of the test run.
globalSuiteReport.globalFailureCount++;
config.stats.bad++;
config.stats.all++;
emit('error', error);
}
}
/**
* Creates a seeded "sample" generator which is used for randomizing tests.
*/
function unitSamplerGenerator(seed) {
// 32-bit xorshift, requires only a nonzero seed
// https://excamera.com/sphinx/article-xorshift.html
var sample = parseInt(generateHash(seed), 16) || -1;
return function () {
sample ^= sample << 13;
sample ^= sample >>> 17;
sample ^= sample << 5;
// ECMAScript has no unsigned number type
if (sample < 0) {
sample += 0x100000000;
}
return sample / 0x100000000;
};
}
class ProcessingQueue {
constructor() {
this.priorityCount = 0;
this.unitSampler = null;
// This is a queue of functions that are tasks within a single test.
// After tests are dequeued from config.queue they are expanded into
// a set of tasks in this queue.
this.taskQueue = [];
this.finished = false;
}
/**
* Advances the taskQueue to the next task. If the taskQueue is empty,
* process the testQueue
*/
advance() {
this.advanceTaskQueue();
if (!this.taskQueue.length && !config.blocking && !config.current) {
this.advanceTestQueue();
}
}
/**
* Advances the taskQueue with an increased depth
*/
advanceTaskQueue() {
var start = performance.now();
config.depth = (config.depth || 0) + 1;
this.processTaskQueue(start);
config.depth--;
}
/**
* Process the first task on the taskQueue as a promise.
* Each task is a function added by Test#queue() in test.js
*/
processTaskQueue(start) {
if (this.taskQueue.length && !config.blocking) {
var elapsedTime = performance.now() - start;
if (!setTimeout$1 || config.updateRate <= 0 || elapsedTime < config.updateRate) {
var task = this.taskQueue.shift();
_Promise.resolve(task()).then(() => {
if (!this.taskQueue.length) {
this.advance();
} else {
this.processTaskQueue(start);
}
});
} else {
setTimeout$1(() => {
this.advance();
});
}
}
}
/**
* Advance the testQueue to the next test to process. Call done() if testQueue completes.
*/
advanceTestQueue() {
if (!config.blocking && !config.queue.length && config.depth === 0) {
this.done();
return;
}
var testTasks = config.queue.shift();
this.addToTaskQueue(testTasks());
if (this.priorityCount > 0) {
this.priorityCount--;
}
this.advance();
}
/**
* Enqueue the tasks for a test into the task queue.
* @param {Array} tasksArray
*/
addToTaskQueue(tasksArray) {
this.taskQueue.push(...tasksArray);
}
/**
* Return the number of tasks remaining in the task queue to be processed.
* @return {number}
*/
taskCount() {
return this.taskQueue.length;
}
/**
* Adds a test to the TestQueue for execution.
* @param {Function} testTasksFunc
* @param {boolean} prioritize
*/
add(testTasksFunc, prioritize) {
if (prioritize) {
config.queue.splice(this.priorityCount++, 0, testTasksFunc);
} else if (config.seed) {
if (!this.unitSampler) {
this.unitSampler = unitSamplerGenerator(config.seed);
}
// Insert into a random position after all prioritized items
var index = Math.floor(this.unitSampler() * (config.queue.length - this.priorityCount + 1));
config.queue.splice(this.priorityCount + index, 0, testTasksFunc);
} else {
config.queue.push(testTasksFunc);
}
}
abort() {
// Delete the next test and anything after it.
// Thus, leaving only the bare minimum to cleanly exit the current test.
for (var i = 0, len = config.queue.length; i < len; i++) {
if (config.queue[i].name === 'runTest') {
var nextTestIndex = i;
config.queue.splice(nextTestIndex);
return;
}
}
}
/**
* This function is called when the ProcessingQueue is done processing all
* items. It handles emitting the final run events.
*/
done() {
// We have reached the end of the processing queue and are about to emit the
// "runEnd" event after which reporters typically stop listening and exit
// the process. First, check if we need to emit an error event.
if (config.stats.testCount === 0 && config.failOnZeroTests === true) {
var error;
if (config.filter && config.filter.length) {
error = new Error("No tests matched the filter \"".concat(config.filter, "\"."));
} else if (config.module && config.module.length) {
error = new Error("No tests matched the module \"".concat(config.module, "\"."));
} else if (config.moduleId && config.moduleId.length) {
error = new Error("No tests matched the moduleId \"".concat(config.moduleId, "\"."));
} else if (config.testId && config.testId.length) {
error = new Error("No tests matched the testId \"".concat(config.testId, "\"."));
} else {
error = new Error('No tests were run.');
}
onUncaughtException(error);
}
var storage = config.storage;
this.finished = true;
var runEnd = globalSuiteReport.end(true);
emit('runEnd', runEnd);
runLoggingCallbacks('done', {
// Use of "details" parameter to QUnit.done() is discouraged
// since QUnit 2.19 to solve the "assertion counting" gotchas
// documented at https://qunitjs.com/api/callbacks/QUnit.done/.
// Use QUnit.on('runEnd') instead.
//
// Kept for compatibility with ecosystem integrations
// such as Testem, which safely use the "runtime" field only.
passed: config.stats.all - config.stats.bad,
failed: config.stats.bad,
total: config.stats.all,
runtime: runEnd.runtime
}).then(() => {
// Clear our storage keys if run passed and there were no "todo" tests left.
//
// The check for "todo" is important, because after a passing run,
// it is useful for re-runs to quickly start with known "todo" tests.
// In test.js we inform storage about "todo" failures the same as
// any other failure. Clearing here after merely "passed" would defeat
// that logic.
if (storage && runEnd.status === 'passed' && !runEnd.testCounts.todo) {
for (var i = storage.length - 1; i >= 0; i--) {
var key = storage.key(i);
if (key.indexOf('qunit-test-') === 0) {
storage.removeItem(key);
}
}
}
});
}
}
class ConsoleReporter {
constructor(runner) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Cache references to console methods to ensure we can report failures
// from tests tests that mock the console object itself.
// https://github.com/qunitjs/qunit/issues/1340
// Support IE 9: Function#bind is supported, but no console.log.bind().
this.log = options.log || Function.prototype.bind.call(console$1.log, console$1);
runner.on('error', this.onError.bind(this));
runner.on('runStart', this.onRunStart.bind(this));
runner.on('testStart', this.onTestStart.bind(this));
runner.on('testEnd', this.onTestEnd.bind(this));
runner.on('runEnd', this.onRunEnd.bind(this));
}
static init(runner, options) {
return new ConsoleReporter(runner, options);
}
onError(error) {
this.log('error', error);
}
onRunStart(runStart) {
this.log('runStart', runStart);
}
onTestStart(test) {
this.log('testStart', test);
}
onTestEnd(test) {
this.log('testEnd', test);
}
onRunEnd(runEnd) {
this.log('runEnd', runEnd);
}
}
// Support IE 9: No console object, unless the developer tools are not open.
// Support IE 9: Function#bind is supported, but no console.log.bind().
// Support: SpiderMonkey (mozjs 68+)
// The console object has a log method, but no warn method.
var Logger = {
warn: console$1 ? Function.prototype.bind.call(console$1.warn || console$1.log, console$1) : function () {}
};
var nativePerf = typeof g.performance !== 'undefined'
// eslint-disable-next-line compat/compat -- Checked
&& typeof g.performance.mark === 'function'
// eslint-disable-next-line compat/compat -- Checked
&& typeof g.performance.measure === 'function' ? g.performance : undefined;
var perf = {
measure: nativePerf ? function (comment, startMark, endMark) {
// `performance.measure` may fail if the mark could not be found.
// reasons a specific mark could not be found include: outside code invoking `performance.clearMarks()`
try {
nativePerf.measure(comment, startMark, endMark);
} catch (ex) {
Logger.warn('performance.measure could not be executed because of ', ex.message);
}
} : function () {},
mark: nativePerf ? nativePerf.mark.bind(nativePerf) : function () {}
};
class PerfReporter {
constructor(runner) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.perf = options.perf || perf;
runner.on('runStart', this.onRunStart.bind(this), prioritySymbol);
runner.on('runEnd', this.onRunEnd.bind(this));
runner.on('suiteStart', this.onSuiteStart.bind(this), prioritySymbol);
runner.on('suiteEnd', this.onSuiteEnd.bind(this));
runner.on('testStart', this.onTestStart.bind(this), prioritySymbol);
runner.on('testEnd', this.onTestEnd.bind(this));
}
static init(runner, options) {
return new PerfReporter(runner, options);
}
onRunStart() {
this.perf.mark('qunit_suite_0_start');
}
onSuiteStart(suiteStart) {
var suiteLevel = suiteStart.fullName.length;
this.perf.mark("qunit_suite_".concat(suiteLevel, "_start"));
}
onSuiteEnd(suiteEnd) {
var suiteLevel = suiteEnd.fullName.length;
var suiteName = suiteEnd.fullName.join(' – ');
this.perf.mark("qunit_suite_".concat(suiteLevel, "_end"));
this.perf.measure("QUnit Module: ".concat(suiteName), "qunit_suite_".concat(suiteLevel, "_start"), "qunit_suite_".concat(suiteLevel, "_end"));
}
onTestStart() {
this.perf.mark('qunit_test_start');
}
onTestEnd(testEnd) {
this.perf.mark('qunit_test_end');
var testName = testEnd.fullName.join(' – ');
this.perf.measure("QUnit Test: ".concat(testName), 'qunit_test_start', 'qunit_test_end');
}
onRunEnd() {
this.perf.mark('qunit_suite_0_end');
this.perf.measure('QUnit Run', 'qunit_suite_0_start', 'qunit_suite_0_end');
}
}
var FORCE_COLOR,
NODE_DISABLE_COLORS,
NO_COLOR,
TERM,
isTTY = true;
if (typeof process !== 'undefined') {
var _ref = process.env || {};
FORCE_COLOR = _ref.FORCE_COLOR;
NODE_DISABLE_COLORS = _ref.NODE_DISABLE_COLORS;
NO_COLOR = _ref.NO_COLOR;
TERM = _ref.TERM;
isTTY = process.stdout && process.stdout.isTTY;
}
var $ = {
enabled: !NODE_DISABLE_COLORS && NO_COLOR == null && TERM !== 'dumb' && (FORCE_COLOR != null && FORCE_COLOR !== '0' || isTTY),
// modifiers
reset: init(0, 0),
bold: init(1, 22),
dim: init(2, 22),
italic: init(3, 23),
underline: init(4, 24),
inverse: init(7, 27),
hidden: init(8, 28),
strikethrough: init(9, 29),
// colors
black: init(30, 39),
red: init(31, 39),
green: init(32, 39),
yellow: init(33, 39),
blue: init(34, 39),
magenta: init(35, 39),
cyan: init(36, 39),
white: init(37, 39),
gray: init(90, 39),
grey: init(90, 39),
// background colors
bgBlack: init(40, 49),
bgRed: init(41, 49),
bgGreen: init(42, 49),
bgYellow: init(43, 49),
bgBlue: init(44, 49),
bgMagenta: init(45, 49),
bgCyan: init(46, 49),
bgWhite: init(47, 49)
};
function run(arr, str) {
var i = 0,
tmp,
beg = '',
end = '';
for (; i < arr.length; i++) {
tmp = arr[i];
beg += tmp.open;
end += tmp.close;
if (!!~str.indexOf(tmp.close)) {
str = str.replace(tmp.rgx, tmp.close + tmp.open);
}
}
return beg + str + end;
}
function chain(has, keys) {
var ctx = {
has,
keys
};
ctx.reset = $.reset.bind(ctx);
ctx.bold = $.bold.bind(ctx);
ctx.dim = $.dim.bind(ctx);
ctx.italic = $.italic.bind(ctx);
ctx.underline = $.underline.bind(ctx);
ctx.inverse = $.inverse.bind(ctx);
ctx.hidden = $.hidden.bind(ctx);
ctx.strikethrough = $.strikethrough.bind(ctx);
ctx.black = $.black.bind(ctx);
ctx.red = $.red.bind(ctx);
ctx.green = $.green.bind(ctx);
ctx.yellow = $.yellow.bind(ctx);
ctx.blue = $.blue.bind(ctx);
ctx.magenta = $.magenta.bind(ctx);
ctx.cyan = $.cyan.bind(ctx);
ctx.white = $.white.bind(ctx);
ctx.gray = $.gray.bind(ctx);
ctx.grey = $.grey.bind(ctx);
ctx.bgBlack = $.bgBlack.bind(ctx);
ctx.bgRed = $.bgRed.bind(ctx);
ctx.bgGreen = $.bgGreen.bind(ctx);
ctx.bgYellow = $.bgYellow.bind(ctx);
ctx.bgBlue = $.bgBlue.bind(ctx);
ctx.bgMagenta = $.bgMagenta.bind(ctx);
ctx.bgCyan = $.bgCyan.bind(ctx);
ctx.bgWhite = $.bgWhite.bind(ctx);
return ctx;
}
function init(open, close) {
var blk = {
open: "\x1B[".concat(open, "m"),
close: "\x1B[".concat(close, "m"),
rgx: new RegExp("\\x1b\\[".concat(close, "m"), 'g')
};
return function (txt) {
if (this !== void 0 && this.has !== void 0) {
!!~this.has.indexOf(open) || (this.has.push(open), this.keys.push(blk));
return txt === void 0 ? this : $.enabled ? run(this.keys, txt + '') : txt + '';
}
return txt === void 0 ? chain([open], [blk]) : $.enabled ? run([blk], txt + '') : txt + '';
};
}
/**
* Format a given value into YAML.
*
* YAML is a superset of JSON that supports all the same data
* types and syntax, and more. As such, it is always possible
* to fallback to JSON.stringify, but we generally avoid
* that to make output easier to read for humans.
*
* Supported data types:
*
* - null
* - boolean
* - number
* - string
* - array
* - object
*
* Anything else (including NaN, Infinity, and undefined)
* must be described in strings, for display purposes.
*
* Note that quotes are optional in YAML strings if the
* strings are "simple", and as such we generally prefer
* that for improved readability. We output strings in
* one of three ways:
*
* - bare unquoted text, for simple one-line strings.
* - JSON (quoted text), for complex one-line strings.
* - YAML Block, for complex multi-line strings.
*
* Objects with cyclical references will be stringified as
* "[Circular]" as they cannot otherwise be represented.
*/
function prettyYamlValue(value) {
var indent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
if (value === undefined) {
// Not supported in JSON/YAML, turn into string
// and let the below output it as bare string.
value = String(value);
}
// Support IE 9-11: Use isFinite instead of ES6 Number.isFinite
if (typeof value === 'number' && !isFinite(value)) {
// Turn NaN and Infinity into simple strings.
// Paranoia: Don't return directly just in case there's
// a way to add special characters here.
value = String(value);
}
if (typeof value === 'number') {
// Simple numbers
return JSON.stringify(value);
}
if (typeof value === 'string') {
// If any of these match, then we can't output it
// as bare unquoted text, because that would either
// cause data loss or invalid YAML syntax.
//
// - Quotes, escapes, line breaks, or JSON-like stuff.
// - Not allowed in YAML unquoted strings per https://yaml.org/spec/1.2.2/#733-plain-style
// * ": " (colon followed by space)
// * " #" (space followed by hash)
var rSpecialJson = /['"\\/[{}\]\r\n|:#]/;
// - Characters that are special at the start of a YAML value
var rSpecialYaml = /[-?:,[\]{}#&*!|=>'"%@`]/;
// - Leading or trailing whitespace.
var rUntrimmed = /(^\s|\s$)/;
// - Ambiguous as YAML number, e.g. '2', '-1.2', '.2', or '2_000'
var rNumerical = /^[\d._-]+$/;
// - Ambiguous as YAML bool.
// Use case-insensitive match, although technically only
// fully-lower, fully-upper, or uppercase-first would be ambiguous.
// e.g. true/True/TRUE, but not tRUe.
var rBool = /^(true|false|y|n|yes|no|on|off)$/i;
// Is this a complex string?
if (value === '' || rSpecialJson.test(value) || rSpecialYaml.test(value[0]) || rUntrimmed.test(value) || rNumerical.test(value) || rBool.test(value)) {
if (!/\n/.test(value)) {
// Complex one-line string, use JSON (quoted string)
return JSON.stringify(value);
}
// See also <https://yaml-multiline.info/>
// Support IE 9-11: Avoid ES6 String#repeat
var _prefix = new Array(indent * 2 + 1).join(' ');
var trailingLinebreakMatch = value.match(/\n+$/);
var trailingLinebreaks = trailingLinebreakMatch ? trailingLinebreakMatch[0].length : 0;
if (trailingLinebreaks === 1) {
// Use the most straight-forward "Block" string in YAML
// without any "Chomping" indicators.
var lines = value
// Ignore the last new line, since we'll get that one for free
// with the straight-forward Block syntax.
.replace(/\n$/, '').split('\n').map(line => _prefix + line);
return '|\n' + lines.join('\n');
} else {
// This has either no trailing new lines, or more than 1.
// Use |+ so that YAML parsers will preserve it exactly.
var _lines = value.split('\n').map(line => _prefix + line);
return '|+\n' + _lines.join('\n');
}
} else {
// Simple string, use bare unquoted text
return value;
}
}
var prefix = new Array(indent + 1).join(' ');
// Handle null, boolean, array, and object
return JSON.stringify(decycledShallowClone(value), null, 2).split('\n').map((line, i) => i === 0 ? line : prefix + line).join('\n');
}
/**
* Creates a shallow clone of an object where cycles have
* been replaced with "[Circular]".
*/
function decycledShallowClone(object) {
var ancestors = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
if (ancestors.indexOf(object) !== -1) {
return '[Circular]';
}
var type = Object.prototype.toString.call(object).replace(/^\[.+\s(.+?)]$/, '$1').toLowerCase();
var clone;
switch (type) {
case 'array':
ancestors.push(object);
clone = object.map(function (element) {
return decycledShallowClone(element, ancestors);
});
ancestors.pop();
break;
case 'object':
ancestors.push(object);
clone = {};
Object.keys(object).forEach(function (key) {
clone[key] = decycledShallowClone(object[key], ancestors);
});
ancestors.pop();
break;
default:
clone = object;
}
return clone;
}
class TapReporter {
constructor(runner) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Cache references to console methods to ensure we can report failures
// from tests tests that mock the console object itself.
// https://github.com/qunitjs/qunit/issues/1340
// Support IE 9: Function#bind is supported, but no console.log.bind().
this.log = options.log || Function.prototype.bind.call(console$1.log, console$1);
this.testCount = 0;
this.started = false;
this.ended = false;
this.bailed = false;
runner.on('error', this.onError.bind(this));
runner.on('runStart', this.onRunStart.bind(this));
runner.on('testEnd', this.onTestEnd.bind(this));
runner.on('runEnd', this.onRunEnd.bind(this));
}
static init(runner, options) {
return new TapReporter(runner, options);
}
onRunStart() {
if (!this.started) {
this.log('TAP version 13');
this.started = true;
}
}
onError(error) {
if (this.bailed) {
return;
}
this.bailed = true;
// Imitate onTestEnd
// Skip this if we're past "runEnd" as it would look odd
if (!this.ended) {
this.onRunStart();
this.testCount = this.testCount + 1;
this.log("not ok ".concat(this.testCount, " ").concat($.red('global failure')));
this.logError(error);
}
this.log('Bail out! ' + errorString(error).split('\n')[0]);
if (this.ended) {
this.logError(error);
}
}
onTestEnd(test) {
this.testCount = this.testCount + 1;
if (test.status === 'passed') {
this.log("ok ".concat(this.testCount, " ").concat(test.fullName.join(' > ')));
} else if (test.status === 'skipped') {
this.log("ok ".concat(this.testCount, " ").concat($.yellow(test.fullName.join(' > ')), " # SKIP"));
} else if (test.status === 'todo') {
this.log("not ok ".concat(this.testCount, " ").concat($.cyan(test.fullName.join(' > ')), " # TODO"));
test.errors.forEach(error => this.logAssertion(error, 'todo'));
} else {
this.log("not ok ".concat(this.testCount, " ").concat($.red(test.fullName.join(' > '))));
test.errors.forEach(error => this.logAssertion(error));
}
}
onRunEnd(runEnd) {
this.ended = true;
this.log("1..".concat(runEnd.testCounts.total));
this.log("# pass ".concat(runEnd.testCounts.passed));
this.log("# ".concat($.yellow("skip ".concat(runEnd.testCounts.skipped))));
this.log("# ".concat($.cyan("todo ".concat(runEnd.testCounts.todo))));
this.log("# ".concat($.red("fail ".concat(runEnd.testCounts.failed))));
}
logAssertion(error, severity) {
var out = ' ---';
out += "\n message: ".concat(prettyYamlValue(error.message || 'failed'));
out += "\n severity: ".concat(prettyYamlValue(severity || 'failed'));
// When pushFailure() is used, actual/expected are initially unset but
// eventually in Test#logAssertion, for testReport#pushAssertion, these are
// forged into existence as undefined.
var hasAny = error.expected !== undefined || error.actual !== undefined;
if (hasAny) {
out += "\n actual : ".concat(prettyYamlValue(error.actual));
out += "\n expected: ".concat(prettyYamlValue(error.expected));
}
if (error.stack) {
// Since stacks aren't user generated, take a bit of liberty by
// adding a trailing new line to allow a straight-forward YAML Blocks.
var fmtStack = annotateStacktrace(error.stack, $.grey);
if (fmtStack.length) {
out += "\n stack: ".concat(prettyYamlValue(fmtStack + '\n'));
}
}
out += '\n ...';
this.log(out);
}
logError(error) {
var out = ' ---';
out += "\n message: ".concat(prettyYamlValue(errorString(error)));
out += "\n severity: ".concat(prettyYamlValue('failed'));
if (error && error.stack) {
var fmtStack = annotateStacktrace(error.stack, $.grey, error.toString());
if (fmtStack.length) {
out += "\n stack: ".concat(prettyYamlValue(fmtStack + '\n'));
}
}
out += '\n ...';
this.log(out);
}
}
// Expose the current QUnit version
// Replaced by /rollup.config.js using /build/dist-replace.js
var version = '3.0.0';
function commonjsRequire(path) {
throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
}
var fuzzysort$2 = {exports: {}};
/*
fuzzysort.js https://github.com/farzher/fuzzysort
SublimeText-like Fuzzy Search
fuzzysort.single('fs', 'Fuzzy Search') // {score: -16}
fuzzysort.single('test', 'test') // {score: 0}
fuzzysort.single('doesnt exist', 'target') // null
fuzzysort.go('mr', [{file:'Monitor.cpp'}, {file:'MeshRenderer.cpp'}], {key:'file'})
// [{score:-18, obj:{file:'MeshRenderer.cpp'}}, {score:-6009, obj:{file:'Monitor.cpp'}}]
fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp'])
// [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}]
fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '<b>', '</b>')
// <b>F</b>uzzy <b>S</b>earch
*/
var fuzzysort$1 = fuzzysort$2.exports;
var hasRequiredFuzzysort;
function requireFuzzysort() {
if (hasRequiredFuzzysort) return fuzzysort$2.exports;
hasRequiredFuzzysort = 1;
(function (module) {
(function (root, UMD) {
if (module.exports) module.exports = UMD();else root.fuzzysort = UMD();
})(fuzzysort$1, function UMD() {
function fuzzysortNew(instanceOptions) {
var fuzzysort = {
single: function (search, target, options) {
if (search == 'farzher') return {
target: "farzher was here (^-^*)/",
score: 0,
indexes: [0, 1, 2, 3, 4, 5, 6]
};
if (!search) return null;
if (!isObj(search)) search = fuzzysort.getPreparedSearch(search);
if (!target) return null;
if (!isObj(target)) target = fuzzysort.getPrepared(target);
var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true;
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo;
return algorithm(search, target, search[0]);
},
go: function (search, targets, options) {
if (search == 'farzher') return [{
target: "farzher was here (^-^*)/",
score: 0,
indexes: [0, 1, 2, 3, 4, 5, 6],
obj: targets ? targets[0] : null
}];
if (!search) return noResults;
search = fuzzysort.prepareSearch(search);
var searchLowerCode = search[0];
var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991;
var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991;
var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true;
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo;
var resultsLen = 0;
var limitedCount = 0;
var targetsLen = targets.length;
// This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
// options.keys
if (options && options.keys) {
var scoreFn = options.scoreFn || defaultScoreFn;
var keys = options.keys;
var keysLen = keys.length;
for (var i = targetsLen - 1; i >= 0; --i) {
var obj = targets[i];
var objResults = new Array(keysLen);
for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
var key = keys[keyI];
var target = getValue(obj, key);
if (!target) {
objResults[keyI] = null;
continue;
}
if (!isObj(target)) target = fuzzysort.getPrepared(target);
objResults[keyI] = algorithm(search, target, searchLowerCode);
}
objResults.obj = obj; // before scoreFn so scoreFn can use it
var score = scoreFn(objResults);
if (score === null) continue;
if (score < threshold) continue;
objResults.score = score;
if (resultsLen < limit) {
q.add(objResults);
++resultsLen;
} else {
++limitedCount;
if (score > q.peek().score) q.replaceTop(objResults);
}
}
// options.key
} else if (options && options.key) {
var key = options.key;
for (var i = targetsLen - 1; i >= 0; --i) {
var obj = targets[i];
var target = getValue(obj, key);
if (!target) continue;
if (!isObj(target)) target = fuzzysort.getPrepared(target);
var result = algorithm(search, target, searchLowerCode);
if (result === null) continue;
if (result.score < threshold) continue;
// have to clone result so duplicate targets from different obj can each reference the correct obj
result = {
target: result.target,
_targetLowerCodes: null,
_nextBeginningIndexes: null,
score: result.score,
indexes: result.indexes,
obj: obj
}; // hidden
if (resultsLen < limit) {
q.add(result);
++resultsLen;
} else {
++limitedCount;
if (result.score > q.peek().score) q.replaceTop(result);
}
}
// no keys
} else {
for (var i = targetsLen - 1; i >= 0; --i) {
var target = targets[i];
if (!target) continue;
if (!isObj(target)) target = fuzzysort.getPrepared(target);
var result = algorithm(search, target, searchLowerCode);
if (result === null) continue;
if (result.score < threshold) continue;
if (resultsLen < limit) {
q.add(result);
++resultsLen;
} else {
++limitedCount;
if (result.score > q.peek().score) q.replaceTop(result);
}
}
}
if (resultsLen === 0) return noResults;
var results = new Array(resultsLen);
for (var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll();
results.total = resultsLen + limitedCount;
return results;
},
goAsync: function (search, targets, options) {
var canceled = false;
var p = new Promise(function (resolve, reject) {
if (search == 'farzher') return resolve([{
target: "farzher was here (^-^*)/",
score: 0,
indexes: [0, 1, 2, 3, 4, 5, 6],
obj: targets ? targets[0] : null
}]);
if (!search) return resolve(noResults);
search = fuzzysort.prepareSearch(search);
var searchLowerCode = search[0];
var q = fastpriorityqueue();
var iCurrent = targets.length - 1;
var threshold = options && options.threshold || instanceOptions && instanceOptions.threshold || -9007199254740991;
var limit = options && options.limit || instanceOptions && instanceOptions.limit || 9007199254740991;
var allowTypo = options && options.allowTypo !== undefined ? options.allowTypo : instanceOptions && instanceOptions.allowTypo !== undefined ? instanceOptions.allowTypo : true;
var algorithm = allowTypo ? fuzzysort.algorithm : fuzzysort.algorithmNoTypo;
var resultsLen = 0;
var limitedCount = 0;
function step() {
if (canceled) return reject('canceled');
var startMs = Date.now();
// This code is copy/pasted 3 times for performance reasons [options.keys, options.key, no keys]
// options.keys
if (options && options.keys) {
var scoreFn = options.scoreFn || defaultScoreFn;
var keys = options.keys;
var keysLen = keys.length;
for (; iCurrent >= 0; --iCurrent) {
if (iCurrent % 1000 /*itemsPerCheck*/ === 0) {
if (Date.now() - startMs >= 10 /*asyncInterval*/) {
isNode ? setImmediate(step) : setTimeout(step);
return;
}
}
var obj = targets[iCurrent];
var objResults = new Array(keysLen);
for (var keyI = keysLen - 1; keyI >= 0; --keyI) {
var key = keys[keyI];
var target = getValue(obj, key);
if (!target) {
objResults[keyI] = null;
continue;
}
if (!isObj(target)) target = fuzzysort.getPrepared(target);
objResults[keyI] = algorithm(search, target, searchLowerCode);
}
objResults.obj = obj; // before scoreFn so scoreFn can use it
var score = scoreFn(objResults);
if (score === null) continue;
if (score < threshold) continue;
objResults.score = score;
if (resultsLen < limit) {
q.add(objResults);
++resultsLen;
} else {
++limitedCount;
if (score > q.peek().score) q.replaceTop(objResults);
}
}
// options.key
} else if (options && options.key) {
var key = options.key;
for (; iCurrent >= 0; --iCurrent) {
if (iCurrent % 1000 /*itemsPerCheck*/ === 0) {
if (Date.now() - startMs >= 10 /*asyncInterval*/) {
isNode ? setImmediate(step) : setTimeout(step);
return;
}
}
var obj = targets[iCurrent];
var target = getValue(obj, key);
if (!target) continue;
if (!isObj(target)) target = fuzzysort.getPrepared(target);
var result = algorithm(search, target, searchLowerCode);
if (result === null) continue;
if (result.score < threshold) continue;
// have to clone result so duplicate targets from different obj can each reference the correct obj
result = {
target: result.target,
_targetLowerCodes: null,
_nextBeginningIndexes: null,
score: result.score,
indexes: result.indexes,
obj: obj
}; // hidden
if (resultsLen < limit) {
q.add(result);
++resultsLen;
} else {
++limitedCount;
if (result.score > q.peek().score) q.replaceTop(result);
}
}
// no keys
} else {
for (; iCurrent >= 0; --iCurrent) {
if (iCurrent % 1000 /*itemsPerCheck*/ === 0) {
if (Date.now() - startMs >= 10 /*asyncInterval*/) {
isNode ? setImmediate(step) : setTimeout(step);
return;
}
}
var target = targets[iCurrent];
if (!target) continue;
if (!isObj(target)) target = fuzzysort.getPrepared(target);
var result = algorithm(search, target, searchLowerCode);
if (result === null) continue;
if (result.score < threshold) continue;
if (resultsLen < limit) {
q.add(result);
++resultsLen;
} else {
++limitedCount;
if (result.score > q.peek().score) q.replaceTop(result);
}
}
}
if (resultsLen === 0) return resolve(noResults);
var results = new Array(resultsLen);
for (var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll();
results.total = resultsLen + limitedCount;
resolve(results);
}
isNode ? setImmediate(step) : step(); //setTimeout here is too slow
});
p.cancel = function () {
canceled = true;
};
return p;
},
highlight: function (result, hOpen, hClose) {
if (typeof hOpen == 'function') return fuzzysort.highlightCallback(result, hOpen);
if (result === null) return null;
if (hOpen === undefined) hOpen = '<b>';
if (hClose === undefined) hClose = '</b>';
var highlighted = '';
var matchesIndex = 0;
var opened = false;
var target = result.target;
var targetLen = target.length;
var matchesBest = result.indexes;
for (var i = 0; i < targetLen; ++i) {
var char = target[i];
if (matchesBest[matchesIndex] === i) {
++matchesIndex;
if (!opened) {
opened = true;
highlighted += hOpen;
}
if (matchesIndex === matchesBest.length) {
highlighted += char + hClose + target.substr(i + 1);
break;
}
} else {
if (opened) {
opened = false;
highlighted += hClose;
}
}
highlighted += char;
}
return highlighted;
},
highlightCallback: function (result, cb) {
if (result === null) return null;
var target = result.target;
var targetLen = target.length;
var indexes = result.indexes;
var highlighted = '';
var matchI = 0;
var indexesI = 0;
var opened = false;
var result = [];
for (var i = 0; i < targetLen; ++i) {
var char = target[i];
if (indexes[indexesI] === i) {
++indexesI;
if (!opened) {
opened = true;
result.push(highlighted);
highlighted = '';
}
if (indexesI === indexes.length) {
highlighted += char;
result.push(cb(highlighted, matchI++));
highlighted = '';
result.push(target.substr(i + 1));
break;
}
} else {
if (opened) {
opened = false;
result.push(cb(highlighted, matchI++));
highlighted = '';
}
}
highlighted += char;
}
return result;
},
prepare: function (target) {
if (!target) return {
target: '',
_targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/],
_nextBeginningIndexes: null,
score: null,
indexes: null,
obj: null
}; // hidden
return {
target: target,
_targetLowerCodes: fuzzysort.prepareLowerCodes(target),
_nextBeginningIndexes: null,
score: null,
indexes: null,
obj: null
}; // hidden
},
prepareSlow: function (target) {
if (!target) return {
target: '',
_targetLowerCodes: [0 /*this 0 doesn't make sense. here because an empty array causes the algorithm to deoptimize and run 50% slower!*/],
_nextBeginningIndexes: null,
score: null,
indexes: null,
obj: null
}; // hidden
return {
target: target,
_targetLowerCodes: fuzzysort.prepareLowerCodes(target),
_nextBeginningIndexes: fuzzysort.prepareNextBeginningIndexes(target),
score: null,
indexes: null,
obj: null
}; // hidden
},
prepareSearch: function (search) {
if (!search) search = '';
return fuzzysort.prepareLowerCodes(search);
},
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
// Below this point is only internal code
getPrepared: function (target) {
if (target.length > 999) return fuzzysort.prepare(target); // don't cache huge targets
var targetPrepared = preparedCache.get(target);
if (targetPrepared !== undefined) return targetPrepared;
targetPrepared = fuzzysort.prepare(target);
preparedCache.set(target, targetPrepared);
return targetPrepared;
},
getPreparedSearch: function (search) {
if (search.length > 999) return fuzzysort.prepareSearch(search); // don't cache huge searches
var searchPrepared = preparedSearchCache.get(search);
if (searchPrepared !== undefined) return searchPrepared;
searchPrepared = fuzzysort.prepareSearch(search);
preparedSearchCache.set(search, searchPrepared);
return searchPrepared;
},
algorithm: function (searchLowerCodes, prepared, searchLowerCode) {
var targetLowerCodes = prepared._targetLowerCodes;
var searchLen = searchLowerCodes.length;
var targetLen = targetLowerCodes.length;
var searchI = 0; // where we at
var targetI = 0; // where you at
var typoSimpleI = 0;
var matchesSimpleLen = 0;
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for (;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI];
if (isMatch) {
matchesSimple[matchesSimpleLen++] = targetI;
++searchI;
if (searchI === searchLen) break;
searchLowerCode = searchLowerCodes[typoSimpleI === 0 ? searchI : typoSimpleI === searchI ? searchI + 1 : typoSimpleI === searchI - 1 ? searchI - 1 : searchI];
}
++targetI;
if (targetI >= targetLen) {
// Failed to find searchI
// Check for typo or exit
// we go as far as possible before trying to transpose
// then we transpose backwards until we reach the beginning
for (;;) {
if (searchI <= 1) return null; // not allowed to transpose first char
if (typoSimpleI === 0) {
// we haven't tried to transpose yet
--searchI;
var searchLowerCodeNew = searchLowerCodes[searchI];
if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char
typoSimpleI = searchI;
} else {
if (typoSimpleI === 1) return null; // reached the end of the line for transposing
--typoSimpleI;
searchI = typoSimpleI;
searchLowerCode = searchLowerCodes[searchI + 1];
var searchLowerCodeNew = searchLowerCodes[searchI];
if (searchLowerCode === searchLowerCodeNew) continue; // doesn't make sense to transpose a repeat char
}
matchesSimpleLen = searchI;
targetI = matchesSimple[matchesSimpleLen - 1] + 1;
break;
}
}
}
var searchI = 0;
var typoStrictI = 0;
var successStrict = false;
var matchesStrictLen = 0;
var nextBeginningIndexes = prepared._nextBeginningIndexes;
if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target);
var firstPossibleI = targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1];
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
if (targetI !== targetLen) for (;;) {
if (targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if (searchI <= 0) {
// We failed to push chars forward for a better match
// transpose, starting from the beginning
++typoStrictI;
if (typoStrictI > searchLen - 2) break;
if (searchLowerCodes[typoStrictI] === searchLowerCodes[typoStrictI + 1]) continue; // doesn't make sense to transpose a repeat char
targetI = firstPossibleI;
continue;
}
--searchI;
var lastMatch = matchesStrict[--matchesStrictLen];
targetI = nextBeginningIndexes[lastMatch];
} else {
var isMatch = searchLowerCodes[typoStrictI === 0 ? searchI : typoStrictI === searchI ? searchI + 1 : typoStrictI === searchI - 1 ? searchI - 1 : searchI] === targetLowerCodes[targetI];
if (isMatch) {
matchesStrict[matchesStrictLen++] = targetI;
++searchI;
if (searchI === searchLen) {
successStrict = true;
break;
}
++targetI;
} else {
targetI = nextBeginningIndexes[targetI];
}
}
}
{
// tally up the score & keep track of matches for highlighting later
if (successStrict) {
var matchesBest = matchesStrict;
var matchesBestLen = matchesStrictLen;
} else {
var matchesBest = matchesSimple;
var matchesBestLen = matchesSimpleLen;
}
var score = 0;
var lastTargetI = -1;
for (var i = 0; i < searchLen; ++i) {
var targetI = matchesBest[i];
// score only goes down if they're not consecutive
if (lastTargetI !== targetI - 1) score -= targetI;
lastTargetI = targetI;
}
if (!successStrict) {
score *= 1000;
if (typoSimpleI !== 0) score += -20; /*typoPenalty*/
} else {
if (typoStrictI !== 0) score += -20; /*typoPenalty*/
}
score -= targetLen - searchLen;
prepared.score = score;
prepared.indexes = new Array(matchesBestLen);
for (var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i];
return prepared;
}
},
algorithmNoTypo: function (searchLowerCodes, prepared, searchLowerCode) {
var targetLowerCodes = prepared._targetLowerCodes;
var searchLen = searchLowerCodes.length;
var targetLen = targetLowerCodes.length;
var searchI = 0; // where we at
var targetI = 0; // where you at
var matchesSimpleLen = 0;
// very basic fuzzy match; to remove non-matching targets ASAP!
// walk through target. find sequential matches.
// if all chars aren't found then exit
for (;;) {
var isMatch = searchLowerCode === targetLowerCodes[targetI];
if (isMatch) {
matchesSimple[matchesSimpleLen++] = targetI;
++searchI;
if (searchI === searchLen) break;
searchLowerCode = searchLowerCodes[searchI];
}
++targetI;
if (targetI >= targetLen) return null; // Failed to find searchI
}
var searchI = 0;
var successStrict = false;
var matchesStrictLen = 0;
var nextBeginningIndexes = prepared._nextBeginningIndexes;
if (nextBeginningIndexes === null) nextBeginningIndexes = prepared._nextBeginningIndexes = fuzzysort.prepareNextBeginningIndexes(prepared.target);
targetI = matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1];
// Our target string successfully matched all characters in sequence!
// Let's try a more advanced and strict test to improve the score
// only count it as a match if it's consecutive or a beginning character!
if (targetI !== targetLen) for (;;) {
if (targetI >= targetLen) {
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
if (searchI <= 0) break; // We failed to push chars forward for a better match
--searchI;
var lastMatch = matchesStrict[--matchesStrictLen];
targetI = nextBeginningIndexes[lastMatch];
} else {
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI];
if (isMatch) {
matchesStrict[matchesStrictLen++] = targetI;
++searchI;
if (searchI === searchLen) {
successStrict = true;
break;
}
++targetI;
} else {
targetI = nextBeginningIndexes[targetI];
}
}
}
{
// tally up the score & keep track of matches for highlighting later
if (successStrict) {
var matchesBest = matchesStrict;
var matchesBestLen = matchesStrictLen;
} else {
var matchesBest = matchesSimple;
var matchesBestLen = matchesSimpleLen;
}
var score = 0;
var lastTargetI = -1;
for (var i = 0; i < searchLen; ++i) {
var targetI = matchesBest[i];
// score only goes down if they're not consecutive
if (lastTargetI !== targetI - 1) score -= targetI;
lastTargetI = targetI;
}
if (!successStrict) score *= 1000;
score -= targetLen - searchLen;
prepared.score = score;
prepared.indexes = new Array(matchesBestLen);
for (var i = matchesBestLen - 1; i >= 0; --i) prepared.indexes[i] = matchesBest[i];
return prepared;
}
},
prepareLowerCodes: function (str) {
var strLen = str.length;
var lowerCodes = []; // new Array(strLen) sparse array is too slow
var lower = str.toLowerCase();
for (var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i);
return lowerCodes;
},
prepareBeginningIndexes: function (target) {
var targetLen = target.length;
var beginningIndexes = [];
var beginningIndexesLen = 0;
var wasUpper = false;
var wasAlphanum = false;
for (var i = 0; i < targetLen; ++i) {
var targetCode = target.charCodeAt(i);
var isUpper = targetCode >= 65 && targetCode <= 90;
var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57;
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum;
wasUpper = isUpper;
wasAlphanum = isAlphanum;
if (isBeginning) beginningIndexes[beginningIndexesLen++] = i;
}
return beginningIndexes;
},
prepareNextBeginningIndexes: function (target) {
var targetLen = target.length;
var beginningIndexes = fuzzysort.prepareBeginningIndexes(target);
var nextBeginningIndexes = []; // new Array(targetLen) sparse array is too slow
var lastIsBeginning = beginningIndexes[0];
var lastIsBeginningI = 0;
for (var i = 0; i < targetLen; ++i) {
if (lastIsBeginning > i) {
nextBeginningIndexes[i] = lastIsBeginning;
} else {
lastIsBeginning = beginningIndexes[++lastIsBeginningI];
nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning;
}
}
return nextBeginningIndexes;
},
cleanup: cleanup,
new: fuzzysortNew
};
return fuzzysort;
} // fuzzysortNew
// This stuff is outside fuzzysortNew, because it's shared with instances of fuzzysort.new()
var isNode = typeof commonjsRequire !== 'undefined' && typeof window === 'undefined';
var MyMap = typeof Map === 'function' ? Map : function () {
var s = Object.create(null);
this.get = function (k) {
return s[k];
};
this.set = function (k, val) {
s[k] = val;
return this;
};
this.clear = function () {
s = Object.create(null);
};
};
var preparedCache = new MyMap();
var preparedSearchCache = new MyMap();
var noResults = [];
noResults.total = 0;
var matchesSimple = [];
var matchesStrict = [];
function cleanup() {
preparedCache.clear();
preparedSearchCache.clear();
matchesSimple = [];
matchesStrict = [];
}
function defaultScoreFn(a) {
var max = -9007199254740991;
for (var i = a.length - 1; i >= 0; --i) {
var result = a[i];
if (result === null) continue;
var score = result.score;
if (score > max) max = score;
}
if (max === -9007199254740991) return null;
return max;
}
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
// prop = 'key1.key2' 10ms
// prop = ['key1', 'key2'] 27ms
function getValue(obj, prop) {
var tmp = obj[prop];
if (tmp !== undefined) return tmp;
var segs = prop;
if (!Array.isArray(prop)) segs = prop.split('.');
var len = segs.length;
var i = -1;
while (obj && ++i < len) obj = obj[segs[i]];
return obj;
}
function isObj(x) {
return typeof x === 'object';
} // faster as a function
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
var fastpriorityqueue = function () {
var r = [],
o = 0,
e = {};
function n() {
for (var e = 0, n = r[e], c = 1; c < o;) {
var f = c + 1;
e = c, f < o && r[f].score < r[c].score && (e = f), r[e - 1 >> 1] = r[e], c = 1 + (e << 1);
}
for (var a = e - 1 >> 1; e > 0 && n.score < r[a].score; a = (e = a) - 1 >> 1) r[e] = r[a];
r[e] = n;
}
return e.add = function (e) {
var n = o;
r[o++] = e;
for (var c = n - 1 >> 1; n > 0 && e.score < r[c].score; c = (n = c) - 1 >> 1) r[n] = r[c];
r[n] = e;
}, e.poll = function () {
if (0 !== o) {
var e = r[0];
return r[0] = r[--o], n(), e;
}
}, e.peek = function (e) {
if (0 !== o) return r[0];
}, e.replaceTop = function (o) {
r[0] = o, n();
}, e;
};
var q = fastpriorityqueue(); // reuse this, except for async, it needs to make its own
return fuzzysortNew();
}); // UMD
// TODO: (performance) wasm version!?
// TODO: (performance) threads?
// TODO: (performance) avoid cache misses
// TODO: (performance) preparedCache is a memory leak
// TODO: (like sublime) backslash === forwardslash
// TODO: (like sublime) spaces: "a b" should do 2 searches 1 for a and 1 for b
// TODO: (scoring) garbage in targets that allows most searches to strict match need a penality
// TODO: (performance) idk if allowTypo is optimized
})(fuzzysort$2);
return fuzzysort$2.exports;
}
var fuzzysortExports = requireFuzzysort();
var fuzzysort = /*@__PURE__*/getDefaultExportFromCjs(fuzzysortExports);
var hasOwn$1 = Object.prototype.hasOwnProperty;
var DOM = {
on(elem, type, fn) {
elem.addEventListener(type, fn, false);
},
off(elem, type, fn) {
elem.removeEventListener(type, fn, false);
},
onEach(elems, type, fn) {
var i = elems.length;
while (i--) {
DOM.on(elems[i], type, fn);
}
},
// TODO: Use HTMLElement.classList. IE11+, except toggle(x,y), add(x,y), or remove(x,y).
// TODO: Verify that eslint-plugin-compat catches those exceptions.
hasClass(elem, name) {
return (' ' + elem.className + ' ').indexOf(' ' + name + ' ') >= 0;
},
addClass(elem, name) {
if (!DOM.hasClass(elem, name)) {
elem.className += (elem.className ? ' ' : '') + name;
}
},
toggleClass(elem, name, force) {
if (force || typeof force === 'undefined' && !DOM.hasClass(elem, name)) {
DOM.addClass(elem, name);
} else {
DOM.removeClass(elem, name);
}
},
removeClass(elem, name) {
var set = ' ' + elem.className + ' ';
// Class name may appear multiple times
while (set.indexOf(' ' + name + ' ') >= 0) {
set = set.replace(' ' + name + ' ', ' ');
}
// Trim for prettiness
elem.className = trim(set);
}
};
function trim(string) {
if (typeof string.trim === 'function') {
return string.trim();
} else {
return string.replace(/^\s+|\s+$/g, '');
}
}
function stripHtml(string) {
// Strip tags, html entity and whitespaces
return string.replace(/<\/?[^>]+(>|$)/g, '').replace(/"/g, '').replace(/\s+/g, '');
}
class HtmlReporter {
/**
* @param {QUnit} QUnit
* @param {Object} [options] For internal usage
*/
static init(QUnit) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return new HtmlReporter(QUnit, extend(options, {
urlParams: QUnit.urlParams,
// This must use a live reference (i.e. not store a copy), because
// users may apply their settings to QUnit.config anywhere between
// loading qunit.js and the last QUnit.begin() listener finishing.
config: QUnit.config,
abort: function () {
QUnit.config._pq.abort();
}
}));
}
/**
* @internal Use QUnit.reporters.html.init() instead.
*
* @param {QUnit} QUnit
* @param {Object} options
* @param {Object} options.urlParams
* @param {Object} options.config
* - `boolean` options.config.hidepassed
* - `boolean` options.config.collapse For test result
* - `string` options.config.filter
* - `?string` options.config.moduleId For module selector
* - `?string` options.config.testId For test result, rerun link
* - `number` options.config.maxDepth For test result, error message
* @param {Function} options.abort
* @param {HTMLElement|undefined|null} options.element Output element
* If set to HTMLElement, the report will be written to this element.
* If set to undefined, then at `QUnit.on('runStart')` we look for `#qunit`
* and use that if it exists.
* If set to null, the reporter is disabled.
*/
constructor(QUnit, options) {
// Don't init the HTML Reporter in non-browser environments
if (!window$1 || !document || !document.querySelector || options.element === null) {
return;
}
this.stats = {
failedTests: [],
defined: 0,
completed: 0
};
this.urlParams = options.urlParams;
this.config = options.config;
this.abort = options.abort;
this.hiddenTests = [];
// Keep state for our hidepassed toggle, which can change without a reload.
// null indicates we use the config/urlParams. Otherwise this will be
// true/undefined based on checkbox changes. We don't use false here since
// that would serialize in makeUrl() as "false".
this.hidepassed = null;
this.collapseNext = false;
this.unfilteredUrl = this.makeUrl({
filter: undefined,
module: undefined,
moduleId: undefined,
testId: undefined
});
this.dropdownData = null;
// We must not fallback to creating `<div id="qunit">` ourselves if it
// does not exist, because not having `<div id="qunit">` is how projects
// indicate that they wish to run QUnit headless, and do their own reporter.
//
// If options.element was set to HTMLElement, or if `<div id="qunit">` already
// exists (i.e. qunit.js script is placed at end of `<body>`), then we render
// an initial layout now (which is mostly inert).
// Otherwise, we wait until QUnit.start and the onRunStart event, which by
// default is called from window.onload.
//
// If no element is found now, leave this.element as undefined (unchanged).
// We will try again at onRunStart().
this.element = options.element || document.querySelector('#qunit') || undefined;
this.elementBanner = null;
this.elementDisplay = null;
this.elementTests = null;
if (this.element) {
this.appendInterface();
}
// NOTE: Only listen for "runStart" now.
// Other event handlers are added via listen() from onRunStart,
// after we know that the element exists. This reduces overhead and avoids
// potential internal errors when the HTML Reporter is disabled.
this.listen = function () {
this.listen = null;
// It's important that we're in the callback queue before any user-defined
// "QUnit.begin()", because, if those may throw, ours wouldn't run and
// the UI would remain blank or incomplete.
// https://github.com/qunitjs/qunit/issues/1792
QUnit.begin(this.onBegin.bind(this), prioritySymbol);
// Use prioritySignal for testStart() to increase availability
// of the HTML API for TESTID elements toward other event listeners.
QUnit.testStart(this.onTestStart.bind(this), prioritySymbol);
QUnit.log(this.onLog.bind(this), prioritySymbol);
QUnit.testDone(this.onTestDone.bind(this));
QUnit.on('runEnd', this.onRunEnd.bind(this));
// It's important that we don't listen for onError until after
// this.element is found and populated by appendInterface(), as
// otherwise it will fail in appendTest() to display error details.
// We've given on() a memory for "error" events to accommodate
// late listening.
QUnit.on('error', this.onError.bind(this), prioritySymbol);
};
QUnit.on('runStart', this.onRunStart.bind(this), prioritySymbol);
}
// Handle "submit" event from "filter" or "moduleFilter" field.
onFilterSubmit(ev) {
// Trim potential accidental whitespace so that QUnit doesn't throw an error about no tests matching the filter.
var filterInputElem = this.element.querySelector('#qunit-filter-input');
filterInputElem.value = trim(filterInputElem.value);
this.applyUrlParams();
if (ev) {
ev.preventDefault();
}
return false;
}
// Handle "click" events on toolbar checkboxes and "change" for select menus.
// Updates the URL with the new state of `config.urlConfig` values.
onToolbarChanged(ev) {
var field = ev.currentTarget;
// Detect if field is a select menu or a checkbox
var value;
if ('selectedIndex' in field) {
value = field.options[field.selectedIndex].value || undefined;
} else {
value = field.checked ? field.defaultValue || true : undefined;
}
var updatedUrl = this.makeUrl({
[field.name]: value
});
// Check if we can apply the change without a page refresh
if (field.name === 'hidepassed' && 'replaceState' in window$1.history) {
// Set either true or undefined, which will now take precedence over
// the original QUnit.urlParams in makeUrl()
this.hidepassed = value;
var tests = this.elementTests;
if (field.checked) {
var length = tests.children.length;
var children = tests.children;
for (var i = 0; i < length; i++) {
var test = children[i];
var className = test ? test.className : '';
var classNameHasPass = className.indexOf('pass') > -1;
var classNameHasSkipped = className.indexOf('skipped') > -1;
if (classNameHasPass || classNameHasSkipped) {
this.hiddenTests.push(test);
}
}
// Optimization: Avoid for-of iterator overhead.
for (var _i = 0; _i < this.hiddenTests.length; _i++) {
tests.removeChild(this.hiddenTests[_i]);
}
} else {
// Optimization: Avoid Array.shift() which would mutate the array many times.
// As of Chrome 126, HTMLElement.append(...hiddenTests) is still slower than
// calling appendChild in a loop.
for (var _i2 = 0; _i2 < this.hiddenTests.length; _i2++) {
tests.appendChild(this.hiddenTests[_i2]);
}
this.hiddenTests.length = 0;
}
window$1.history.replaceState(null, '', updatedUrl);
} else {
window$1.location = updatedUrl;
}
}
/**
* @param {Object.<string,boolean|string|string[]>} linkParams
* @return string
*/
makeUrl(linkParams) {
var params = extend({}, this.urlParams);
if (this.hidepassed !== null) {
params.hidepassed = this.hidepassed;
}
extend(params, linkParams);
var querystring = '?';
for (var key in params) {
// Skip inherited or undefined properties
if (hasOwn$1.call(params, key) && params[key] !== undefined) {
// Output a parameter for each value of this key
// (but usually just one)
var arrValue = [].concat(params[key]);
for (var i = 0; i < arrValue.length; i++) {
querystring += encodeURIComponent(key);
if (arrValue[i] !== true) {
querystring += '=' + encodeURIComponent(arrValue[i]);
}
querystring += '&';
}
}
}
return window$1.location.pathname + querystring.slice(0, -1);
}
applyUrlParams() {
var filter = this.element.querySelector('#qunit-filter-input').value;
window$1.location = this.makeUrl({
filter: filter === '' ? undefined : filter,
moduleId: [...this.dropdownData.selectedMap.keys()],
// Remove module and testId filter
module: undefined,
testId: undefined
});
}
abortTestsButton() {
var button = document.createElement('button');
button.id = 'qunit-abort-tests-button';
button.textContent = 'Abort';
DOM.on(button, 'click', () => {
button.disabled = true;
button.textContent = 'Aborting...';
this.abort();
return false;
});
return button;
}
toolbarLooseFilter() {
var filter = document.createElement('form');
filter.className = 'qunit-filter';
var label = document.createElement('label');
label.textContent = 'Filter: ';
var input = document.createElement('input');
input.type = 'text';
input.value = this.config.filter || '';
input.name = 'filter';
input.id = 'qunit-filter-input';
label.appendChild(input);
var button = document.createElement('button');
button.textContent = 'Go';
filter.appendChild(label);
filter.appendChild(document.createTextNode(' '));
filter.appendChild(button);
DOM.on(filter, 'submit', this.onFilterSubmit.bind(this));
return filter;
}
toolbarModuleFilter(beginDetails) {
var initialSelected = null;
var dropdownData = this.dropdownData = {
options: beginDetails.modules.slice(),
selectedMap: new StringMap(),
isDirty: function () {
return [...dropdownData.selectedMap.keys()].sort().join(',') !== [...initialSelected.keys()].sort().join(',');
}
};
if (this.config.moduleId && this.config.moduleId.length) {
// The module dropdown is seeded with the runtime configuration of the last run.
//
// We don't reference `config.moduleId` directly after this and keep our own
// copy because:
// 1. This naturally filters out unknown moduleIds.
// 2. Gives us a place to manage and remember unsubmitted checkbox changes.
// 3. Gives us an efficient way to map a selected moduleId to module name
// during rendering.
for (var i = 0; i < beginDetails.modules.length; i++) {
var mod = beginDetails.modules[i];
if (this.config.moduleId.indexOf(mod.moduleId) !== -1) {
dropdownData.selectedMap.set(mod.moduleId, mod.name);
}
}
}
initialSelected = new StringMap(dropdownData.selectedMap);
function createModuleListItem(moduleId, name, checked) {
return '<li><label class="clickable' + (checked ? ' checked' : '') + '"><input type="checkbox" ' + 'value="' + escapeText(moduleId) + '"' + (checked ? ' checked="checked"' : '') + ' />' + escapeText(name) + '</label></li>';
}
/**
* @param {Array} Results from fuzzysort
* @return {string} HTML
*/
function moduleListHtml(results) {
var html = '';
// Hoist the already selected items, and show them always
// even if not matched by the current search.
dropdownData.selectedMap.forEach((name, moduleId) => {
html += createModuleListItem(moduleId, name, true);
});
for (var _i3 = 0; _i3 < results.length; _i3++) {
var _mod = results[_i3].obj;
if (!dropdownData.selectedMap.has(_mod.moduleId)) {
html += createModuleListItem(_mod.moduleId, _mod.name, false);
}
}
return html;
}
var label = document.createElement('label');
label.htmlFor = 'qunit-modulefilter-search';
label.textContent = 'Module:\u00A0 ';
var searchContainer = document.createElement('span');
searchContainer.id = 'qunit-modulefilter-search-container';
label.appendChild(searchContainer);
var moduleSearch = document.createElement('input');
// Set type=text explicitly for ease of styling
moduleSearch.setAttribute('type', 'text');
moduleSearch.id = 'qunit-modulefilter-search';
moduleSearch.autocomplete = 'off';
DOM.on(moduleSearch, 'input', searchInput);
DOM.on(moduleSearch, 'input', searchFocus);
DOM.on(moduleSearch, 'focus', searchFocus);
DOM.on(moduleSearch, 'click', searchFocus);
searchContainer.appendChild(moduleSearch);
var applyButton = document.createElement('button');
applyButton.textContent = 'Apply';
applyButton.title = 'Re-run the selected test modules';
DOM.on(applyButton, 'click', this.applyUrlParams.bind(this));
var resetButton = document.createElement('button');
resetButton.textContent = 'Reset';
resetButton.type = 'reset';
resetButton.title = 'Restore the previous module selection';
var clearButton = document.createElement('button');
clearButton.textContent = 'Select none';
clearButton.type = 'button';
clearButton.title = 'Clear the current module selection';
DOM.on(clearButton, 'click', function () {
dropdownData.selectedMap.clear();
selectionChange();
searchInput();
});
var actions = document.createElement('span');
actions.id = 'qunit-modulefilter-actions';
actions.appendChild(applyButton);
actions.appendChild(resetButton);
if (initialSelected.size) {
// Only show clear button if functionally different from reset
actions.appendChild(clearButton);
}
var dropDownList = document.createElement('ul');
dropDownList.id = 'qunit-modulefilter-dropdown-list';
var dropDown = document.createElement('div');
dropDown.id = 'qunit-modulefilter-dropdown';
dropDown.style.display = 'none';
dropDown.appendChild(actions);
dropDown.appendChild(dropDownList);
DOM.on(dropDown, 'change', selectionChange);
searchContainer.appendChild(dropDown);
// Set initial moduleSearch.placeholder and clearButton/resetButton.
selectionChange();
var moduleFilter = document.createElement('form');
moduleFilter.id = 'qunit-modulefilter';
moduleFilter.appendChild(label);
DOM.on(moduleFilter, 'submit', this.onFilterSubmit.bind(this));
DOM.on(moduleFilter, 'reset', function () {
dropdownData.selectedMap = new StringMap(initialSelected);
// Set moduleSearch.placeholder and reflect non-dirty state
selectionChange();
searchInput();
});
// Enables show/hide for the dropdown
function searchFocus() {
if (dropDown.style.display !== 'none') {
return;
}
// Optimization: Defer rendering options until focussed.
// https://github.com/qunitjs/qunit/issues/1664
// To test or benchmark, use /demos/q4000-qunit.html.
searchInput();
dropDown.style.display = 'block';
// Hide on Escape keydown or on click outside the container
DOM.on(document, 'click', hideHandler);
DOM.on(document, 'keydown', hideHandler);
function hideHandler(e) {
var inContainer = moduleFilter.contains(e.target);
if (e.keyCode === 27 || !inContainer) {
if (e.keyCode === 27 && inContainer) {
moduleSearch.focus();
}
dropDown.style.display = 'none';
DOM.off(document, 'click', hideHandler);
DOM.off(document, 'keydown', hideHandler);
moduleSearch.value = '';
searchInput();
}
}
}
/**
* @param {string} searchText
* @return {string} HTML
*/
function filterModules(searchText) {
var results;
// Optimization: Limit number of results to ensure UI is responsive.
// To benchmark typeahead responsiveness, use /demos/q4000-qunit.html.
if (searchText === '') {
// Improve on-boarding experience by having an immediate display of
// module names, indicating how the interface works. This also makes
// for a quicker interaction in the common case of small projects.
// Don't mandate typing just to get the menu.
results = dropdownData.options.slice(0, 100).map(obj => {
// Fake empty results. https://github.com/farzher/fuzzysort/issues/41
return {
obj: obj
};
});
} else {
results = fuzzysort.go(searchText, dropdownData.options, {
limit: 100,
key: 'name',
allowTypo: true
});
}
return moduleListHtml(results);
}
// Processes module search box input
var searchInputTimeout;
function searchInput() {
// Use a debounce with a ~0ms timeout. This is effectively instantaneous,
// but is better than undebounced because it avoids an ever-growing
// backlog of unprocessed now-outdated input events if fuzzysearch or
// dropdown DOM is slow (e.g. very large test suite).
window$1.clearTimeout(searchInputTimeout);
searchInputTimeout = window$1.setTimeout(() => {
dropDownList.innerHTML = filterModules(moduleSearch.value);
});
}
// Processes checkbox change, or a generic render (initial render, or after reset event)
// Avoid any dropdown rendering here as this is used by toolbarModuleFilter()
// during the initial render, which should not delay test execution.
function selectionChange(evt) {
var checkbox = evt && evt.target || null;
if (checkbox) {
// Update internal state
if (checkbox.checked) {
dropdownData.selectedMap.set(checkbox.value, checkbox.parentNode.textContent);
} else {
dropdownData.selectedMap.delete(checkbox.value);
}
// Update UI state
DOM.toggleClass(checkbox.parentNode, 'checked', checkbox.checked);
}
var textForm = dropdownData.selectedMap.size ? dropdownData.selectedMap.size + ' ' + (dropdownData.selectedMap.size === 1 ? 'module' : 'modules') : 'All modules';
moduleSearch.placeholder = textForm;
moduleSearch.title = 'Type to search through and reduce the list.';
resetButton.disabled = !dropdownData.isDirty();
clearButton.style.display = dropdownData.selectedMap.size ? '' : 'none';
}
return moduleFilter;
}
getUrlConfigHtml() {
var urlConfig = this.config.urlConfig;
var urlConfigHtml = '';
for (var i = 0; i < urlConfig.length; i++) {
// Options can be either strings or objects with nonempty "id" properties
var val = urlConfig[i];
if (typeof val === 'string') {
val = {
id: val,
label: val
};
}
// https://github.com/qunitjs/qunit/issues/1792
var currentVal = this.config[val.id] !== undefined ? this.config[val.id] : this.urlParams[val.id];
var escaped = escapeText(val.id);
var escapedTooltip = escapeText(val.tooltip);
if (!val.value || typeof val.value === 'string') {
urlConfigHtml += '<label for="qunit-urlconfig-' + escaped + '" title="' + escapedTooltip + '"><input id="qunit-urlconfig-' + escaped + '" name="' + escaped + '" type="checkbox"' + (val.value ? ' value="' + escapeText(val.value) + '"' : '') + (currentVal ? ' checked="checked"' : '') + ' title="' + escapedTooltip + '" />' + escapeText(val.label) + '</label>';
} else {
var selection = false;
urlConfigHtml += '<label for="qunit-urlconfig-' + escaped + '" title="' + escapedTooltip + '">' + escapeText(val.label) + ': <select id="qunit-urlconfig-' + escaped + '" name="' + escaped + '" title="' + escapedTooltip + '"><option></option>';
if (Array.isArray(val.value)) {
for (var j = 0; j < val.value.length; j++) {
escaped = escapeText(val.value[j]);
urlConfigHtml += '<option value="' + escaped + '"' + (currentVal === val.value[j] ? (selection = true) && ' selected="selected"' : '') + '>' + escaped + '</option>';
}
} else {
for (var _j in val.value) {
if (hasOwn$1.call(val.value, _j)) {
urlConfigHtml += '<option value="' + escapeText(_j) + '"' + (currentVal === _j ? (selection = true) && ' selected="selected"' : '') + '>' + escapeText(val.value[_j]) + '</option>';
}
}
}
if (currentVal && !selection) {
escaped = escapeText(currentVal);
urlConfigHtml += '<option value="' + escaped + '" selected="selected" disabled="disabled">' + escaped + '</option>';
}
urlConfigHtml += '</select></label>';
}
}
return urlConfigHtml;
}
appendToolbarControls(beginDetails) {
var toolbarControls = this.element.querySelector('#qunit-testrunner-toolbar');
if (toolbarControls) {
var urlConfigContainer = document.createElement('span');
urlConfigContainer.id = 'qunit-toolbar-urlconfig';
urlConfigContainer.className = 'qunit-url-config';
urlConfigContainer.innerHTML = this.getUrlConfigHtml();
DOM.onEach(urlConfigContainer.getElementsByTagName('input'), 'change', this.onToolbarChanged.bind(this));
DOM.onEach(urlConfigContainer.getElementsByTagName('select'), 'change', this.onToolbarChanged.bind(this));
var toolbarFilters = document.createElement('span');
toolbarFilters.id = 'qunit-toolbar-filters';
toolbarFilters.appendChild(this.toolbarLooseFilter());
toolbarFilters.appendChild(this.toolbarModuleFilter(beginDetails));
toolbarControls.appendChild(urlConfigContainer);
toolbarControls.appendChild(toolbarFilters);
}
}
appendTestResultControls() {
var controls = this.element.querySelector('#qunit-testresult-controls');
if (controls) {
controls.appendChild(this.abortTestsButton());
}
}
appendFilteredTest() {
var testId = this.config.testId;
if (!testId || testId.length <= 0) {
return '';
}
return '<div id="qunit-filteredTest">Rerunning selected tests: ' + escapeText(testId.join(', ')) + ' <a id="qunit-clearFilter" href="' + escapeText(this.unfilteredUrl) + '">Run all tests</a></div>';
}
appendInterface() {
// Since QUnit 1.3, these are created automatically.
this.element.setAttribute('role', 'main');
this.element.innerHTML = '<div id="qunit-header">' + '<h1><a href="' + escapeText(this.unfilteredUrl) + '">' + escapeText(document.title) + '</a></h1> ' + '<div id="qunit-userAgent" data-version="' + escapeText(version) + '" data-useragent="' + escapeText(navigator.userAgent) + '">' + escapeText('QUnit ' + version) + '</div>' + '</div>' + '<div id="qunit-toolbar" role="navigation">' + '<div id="qunit-banner"></div>' + '<div id="qunit-testrunner-toolbar"></div>' + '<div id="qunit-testresult" class="result">' + '<div id="qunit-testresult-controls"></div>' + '<div id="qunit-testresult-display">Running...<br /> </div>' + '</div>' + this.appendFilteredTest() + '</div>' + '<ol id="qunit-tests"></ol>';
this.elementBanner = this.element.querySelector('#qunit-banner');
this.elementDisplay = this.element.querySelector('#qunit-testresult-display');
this.elementTests = this.element.querySelector('#qunit-tests');
}
appendTest(name, testId, moduleName) {
var title = document.createElement('strong');
title.className = 'qunit-test-name';
title.innerHTML = getNameHtml(name, moduleName);
var testBlock = document.createElement('li');
testBlock.className = 'qunit-test running';
testBlock.appendChild(title);
// No ID or rerun link for "global failure" blocks
if (testId !== undefined) {
var rerunTrigger = document.createElement('a');
rerunTrigger.textContent = 'Rerun';
rerunTrigger.href = this.makeUrl({
testId: testId
});
testBlock.id = 'qunit-test-output-' + testId;
testBlock.appendChild(rerunTrigger);
}
var assertList = document.createElement('ol');
assertList.className = 'qunit-assert-list';
testBlock.appendChild(assertList);
this.elementTests.appendChild(testBlock);
return testBlock;
}
// HTML Reporter initialization and load
onRunStart(runStart) {
if (this.config.reporters && this.config.reporters.html === false) {
// If QUnit.config.reporters.html was set to false after loading QUnit,
// then undo the initial layout (created from browser-runnner.js)
if (this.element) {
this.element.innerHTML = '';
this.element = null;
}
return;
}
if (!this.element) {
this.element = document.querySelector('#qunit') || null;
if (!this.element) {
return;
}
this.appendInterface();
}
this.stats.defined = runStart.testCounts.total;
// Element exists, now it is safe to attach other event listeners
this.listen();
}
// Display the HTML Reporter interface
//
// This is done from begin() instead of runStart, because urlparams.js uses begin(),
// which we need to wait for. urlparams.js in turn uses begin() to allow plugins to
// add entries to QUnit.config.urlConfig, which may be done asynchronously.
// https://github.com/qunitjs/qunit/issues/1657
onBegin(beginDetails) {
this.appendToolbarControls(beginDetails);
this.appendTestResultControls();
this.elementDisplay.className = 'running';
}
getRerunFailedHtml(failedTests) {
if (failedTests.length === 0) {
return '';
}
var href = this.makeUrl({
testId: failedTests
});
return ['<br /><a href="' + escapeText(href) + '">', failedTests.length === 1 ? 'Rerun 1 failed test' : 'Rerun ' + failedTests.length + ' failed tests', '</a>'].join('');
}
onRunEnd(runEnd) {
function msToSec(milliseconds) {
// Will return a whole number of seconds,
// or e.g. "0.2", "0.03" or "0.004".
var sec = milliseconds > 1000 ? '' + Math.round(milliseconds / 1000) : (milliseconds / 1000).toPrecision(1);
return sec + (sec === '1' ? ' second' : ' seconds');
}
var abortButton = this.element.querySelector('#qunit-abort-tests-button');
var html = ['<span class="total">', runEnd.testCounts.total, '</span> tests completed in ', msToSec(runEnd.runtime), '.<br/>', '<span class="passed">', runEnd.testCounts.passed, '</span> passed, ', '<span class="skipped">', runEnd.testCounts.skipped, '</span> skipped, ', '<span class="failed">', runEnd.testCounts.failed, '</span> failed, ', 'and <span class="todo">', runEnd.testCounts.todo, '</span> todo.', this.getRerunFailedHtml(this.stats.failedTests)].join('');
// Update remaining tests to aborted
if (abortButton && abortButton.disabled) {
html = 'Tests aborted after ' + msToSec(runEnd.runtime) + '.';
for (var i = 0; i < this.elementTests.children.length; i++) {
var test = this.elementTests.children[i];
if (DOM.hasClass(test.className, 'running')) {
test.className = 'aborted';
var assertList = test.getElementsByTagName('ol')[0];
var assertLi = document.createElement('li');
assertLi.className = 'fail';
assertLi.textContent = 'Test aborted.';
assertList.appendChild(assertLi);
}
}
}
if (!abortButton || abortButton.disabled === false) {
this.elementBanner.className = runEnd.status === 'failed' ? 'qunit-fail' : 'qunit-pass';
}
if (abortButton) {
abortButton.parentNode.removeChild(abortButton);
}
this.elementDisplay.innerHTML = html;
}
onTestStart(details) {
this.appendTest(details.name, details.testId, details.module);
this.elementDisplay.innerHTML = [details.previousFailure ? 'Rerunning previously failed test: <br />' : "Running test ".concat(this.stats.completed + 1, " of ").concat(this.stats.defined, ": <br />"), getNameHtml(details.name, details.module), this.getRerunFailedHtml(this.stats.failedTests)].join('');
if (this.elementBanner.style.setProperty) {
this.elementBanner.style.setProperty('--qunit-progress', Math.ceil((this.stats.completed + 1) / this.stats.defined * 100) + '%');
}
}
onLog(details) {
var testItem = this.element.querySelector('#qunit-test-output-' + details.testId);
if (!testItem) {
return;
}
var message = escapeText(details.message) || (details.result ? 'okay' : 'failed');
message = '<span class="test-message">' + message + '</span>';
message += '<span class="runtime">@ ' + details.runtime + ' ms</span>';
var expected;
var actual;
// When pushFailure() is called, it is implied that no expected value
// or diff should be shown, because both expected and actual as undefined.
//
// This must check details.expected existence. If it exists as undefined,
// that's a regular assertion for which to render actual/expected and a diff.
var showAnyValues = !details.result && (details.expected !== undefined || details.actual !== undefined);
if (showAnyValues) {
if (details.negative) {
expected = 'NOT ' + dump.parse(details.expected);
} else {
expected = dump.parse(details.expected);
}
actual = dump.parse(details.actual);
message += '<table><tr class="test-expected"><th>Expected: </th><td><pre>' + escapeText(expected) + '</pre></td></tr>';
if (actual !== expected) {
message += '<tr class="test-actual"><th>Result: </th><td><pre>' + escapeText(actual) + '</pre></td></tr>';
var showDiff = false;
var diffHtml;
if (typeof details.actual === 'number' && typeof details.expected === 'number') {
if (!isNaN(details.actual) && !isNaN(details.expected)) {
showDiff = true;
var numDiff = details.actual - details.expected;
diffHtml = (numDiff > 0 ? '+' : '') + numDiff;
}
} else if (typeof details.actual !== 'boolean' && typeof details.expected !== 'boolean') {
diffHtml = diff(expected, actual);
// don't show diff if there is zero overlap
showDiff = stripHtml(diffHtml).length !== stripHtml(expected).length + stripHtml(actual).length;
}
if (showDiff) {
message += '<tr class="test-diff"><th>Diff: </th><td><pre>' + diffHtml + '</pre></td></tr>';
}
} else if (expected.indexOf('[object Array]') !== -1 || expected.indexOf('[object Object]') !== -1) {
// To test this interactively, use /demos/qunit-config-maxDepth.html
message += '<tr class="test-message"><th>Message: </th><td>' + 'Diff suppressed because the object is more than ' + this.config.maxDepth + ' levels deep.<p>Hint: Use <code>QUnit.config.maxDepth</code> to ' + 'set a higher limit, or <a href="' + escapeText(this.makeUrl({
maxDepth: 0
})) + '">Rerun without a depth limit</a>' + '.</p><br/></td></tr>';
} else {
message += '<tr class="test-message"><th>Message: </th><td>' + 'Diff suppressed as the expected and actual results have an equivalent' + ' serialization</td></tr>';
}
if (details.source) {
message += '<tr class="test-source"><th>Source: </th><td><pre>' + escapeText(details.source) + '</pre></td></tr>';
}
message += '</table>';
// This occurs when pushFailure is called and we have an extracted stack trace
} else if (!details.result && details.source) {
message += '<table>' + '<tr class="test-source"><th>Source: </th><td><pre>' + escapeText(details.source) + '</pre></td></tr>' + '</table>';
}
var assertList = testItem.getElementsByTagName('ol')[0];
var assertLi = document.createElement('li');
assertLi.className = details.result ? 'pass' : 'fail';
assertLi.innerHTML = message;
assertList.appendChild(assertLi);
}
onTestDone(details) {
var testItem = this.elementTests.querySelector('#qunit-test-output-' + details.testId);
if (!testItem) {
return;
}
// This test passed if it has no unexpected failed assertions
// TODO: Add "status" from TestReport#getStatus() to testDone() and use that.
var status;
if (details.skipped) {
status = 'skipped';
} else {
var passed = details.failed > 0 ? details.todo : !details.todo;
status = !passed ? 'failed' : details.todo ? 'todo' : 'passed';
}
var testPassed = status !== 'failed';
this.stats.completed++;
if (!testPassed) {
this.stats.failedTests.push(details.testId);
}
// The testItem.firstChild is the test name
var testTitle = testItem.firstChild;
var assertList = testItem.getElementsByTagName('ol')[0];
// Collapse passing tests by default
if (testPassed) {
DOM.addClass(assertList, 'qunit-collapsed');
} else {
if (this.config.collapse) {
if (!this.collapseNext) {
// Skip collapsing the first failing test
this.collapseNext = true;
} else {
// Collapse subsequent failing tests
DOM.addClass(assertList, 'qunit-collapsed');
}
}
}
var good = details.passed;
var bad = details.failed;
var badGoodCounts = bad ? '<span class="failed">' + bad + '</span>, ' + '<span class="passed">' + good + '</span>, ' : '';
testTitle.innerHTML += ' <span class="counts">(' + badGoodCounts + details.total + ')</span>';
DOM.removeClass(testItem, 'running');
if (status === 'skipped') {
DOM.addClass(testItem, 'skipped');
var skippedLabel = document.createElement('em');
skippedLabel.className = 'qunit-skipped-label';
skippedLabel.textContent = 'skipped';
testItem.insertBefore(skippedLabel, testTitle);
testItem.insertBefore(document.createTextNode(' '), testTitle);
} else {
DOM.addClass(testItem, testPassed ? 'pass' : 'fail');
DOM.on(testTitle, 'click', function () {
DOM.toggleClass(assertList, 'qunit-collapsed');
});
if (details.todo) {
// Add label both for status=todo (passing) and for status=failed on a todo test.
DOM.addClass(testItem, 'todo');
var todoLabel = document.createElement('em');
todoLabel.className = 'qunit-todo-label';
todoLabel.textContent = 'todo';
testItem.insertBefore(todoLabel, testTitle);
testItem.insertBefore(document.createTextNode(' '), testTitle);
}
var time = document.createElement('span');
time.className = 'runtime';
time.textContent = details.runtime + ' ms';
testItem.insertBefore(time, assertList);
}
// Show the source of the test when showing assertions
if (details.source) {
var sourceName = document.createElement('p');
sourceName.innerHTML = '<strong>Source: </strong>' + escapeText(details.source);
DOM.addClass(sourceName, 'qunit-source');
if (testPassed) {
DOM.addClass(sourceName, 'qunit-collapsed');
}
DOM.on(testTitle, 'click', function () {
DOM.toggleClass(sourceName, 'qunit-collapsed');
});
testItem.appendChild(sourceName);
}
var hidepassed = this.hidepassed !== null ? this.hidepassed : this.config.hidepassed;
if (hidepassed && (status === 'passed' || details.skipped)) {
this.hiddenTests.push(testItem);
// use removeChild() instead of remove() for wider browser support
this.elementTests.removeChild(testItem);
}
}
onError(error) {
var testItem = this.appendTest('global failure');
// Render similar to a failed assertion (see above QUnit.log callback)
var message = escapeText(errorString(error));
message = '<span class="test-message">' + message + '</span>';
if (error && error.stack) {
message += '<table>' + '<tr class="test-source"><th>Source: </th><td><pre>' + escapeText(error.stack) + '</pre></td></tr>' + '</table>';
}
var assertList = testItem.getElementsByTagName('ol')[0];
var assertLi = document.createElement('li');
assertLi.className = 'fail';
assertLi.innerHTML = message;
assertList.appendChild(assertLi);
// Make it visible
DOM.removeClass(testItem, 'running');
DOM.addClass(testItem, 'fail');
}
}
function getNameHtml(name, module) {
var nameHtml = '';
if (module) {
nameHtml = '<span class="module-name">' + escapeText(module) + '</span>: ';
}
nameHtml += '<span class="test-name">' + escapeText(name) + '</span>';
return nameHtml;
}
var reporters = {
console: ConsoleReporter,
perf: PerfReporter,
tap: TapReporter,
html: HtmlReporter
};
class TestReport {
constructor(name, suite, options) {
this.name = name;
this.suiteName = suite.name;
this.fullName = suite.fullName.concat(name);
this.runtime = 0;
this.assertions = [];
this.skipped = !!options.skip;
this.todo = !!options.todo;
this.valid = options.valid;
this._startTime = 0;
this._endTime = 0;
suite.pushTest(this);
}
start(recordTime) {
if (recordTime) {
this._startTime = performance.now();
}
return {
name: this.name,
suiteName: this.suiteName,
fullName: this.fullName.slice()
};
}
end(recordTime) {
if (recordTime) {
this._endTime = performance.now();
}
return extend(this.start(), {
runtime: this.getRuntime(),
status: this.getStatus(),
errors: this.getFailedAssertions(),
assertions: this.getAssertions()
});
}
pushAssertion(assertion) {
this.assertions.push(assertion);
}
getRuntime() {
return Math.round(this._endTime - this._startTime);
}
getStatus() {
if (this.skipped) {
return 'skipped';
}
var testPassed = this.getFailedAssertions().length > 0 ? this.todo : !this.todo;
if (!testPassed) {
return 'failed';
} else if (this.todo) {
return 'todo';
} else {
return 'passed';
}
}
getFailedAssertions() {
return this.assertions.filter(assertion => !assertion.passed);
}
getAssertions() {
return this.assertions.slice();
}
// Remove actual and expected values from assertions. This is to prevent
// leaking memory throughout a test suite.
slimAssertions() {
this.assertions = this.assertions.map(assertion => {
delete assertion.actual;
delete assertion.expected;
return assertion;
});
}
}
function Test(settings) {
this.expected = null;
this.assertions = [];
this.module = config.currentModule;
this.steps = [];
// Track count in order to detect likely issue when upgrading.
// https://github.com/qunitjs/qunit/pull/1775
this.stepsCount = 0;
this.timeout = undefined;
this.data = undefined;
this.withData = false;
this.pauses = new StringMap();
this.nextPauseId = 1;
// For the most common case, we have:
// - 0: new Test
// - 1: addTest
// - 2: QUnit.test
// - 3: user file
//
// A notable exception is test.each(), which overrides this.
this.stackOffset = 3;
extend(this, settings);
// If a module is skipped, all its tests and the tests of the child suites
// should be treated as skipped even if they are defined as `only` or `todo`.
// As for `todo` module, all its tests will be treated as `todo` except for
// tests defined as `skip` which will be left intact.
//
// So, if a test is defined as `todo` and is inside a skipped module, we should
// then treat that test as if was defined as `skip`.
if (this.module.skip) {
this.skip = true;
this.todo = false;
// Skipped tests should be left intact
} else if (this.module.todo && !this.skip) {
this.todo = true;
}
// Queuing a late test after the run has ended is not allowed.
// This was once supported for internal use by QUnit.onUncaughtException(),
// to render a "global error" if the uncaught error happened outside a test
// and after the runEnd event. This was unstable and could be missed by CI.
// (Meaning the CI would pass despite the late-failing test).
// Ref https://github.com/qunitjs/qunit/issues/1377
if (config._pq.finished) {
throw new Error('Unexpected test after runEnd. https://qunitjs.com/api/QUnit/module/#E0001');
}
if (!this.skip && typeof this.callback !== 'function') {
var method = this.todo ? 'QUnit.todo' : 'QUnit.test';
throw new TypeError("You must provide a callback to ".concat(method, "(\"").concat(this.testName, "\")"));
}
// Register unique strings
for (var i = 0, l = this.module.tests; i < l.length; i++) {
if (this.module.tests[i].name === this.testName) {
this.testName += ' ';
}
}
this.testId = generateHash(this.module.name, this.testName);
// No validation after this. Beyond this point, failures must be recorded as
// a completed test with errors, instead of early bail out.
// Otherwise, internals may be left in an inconsistent state.
// Ref https://github.com/qunitjs/qunit/issues/1514
++Test.count;
this.errorForStack = new Error();
this.testReport = new TestReport(this.testName, this.module.suiteReport, {
todo: this.todo,
skip: this.skip,
valid: this.valid()
});
this.module.tests.push({
name: this.testName,
testId: this.testId,
skip: !!this.skip
});
if (this.skip) {
// Skipped tests will fully ignore (and dereference for garbage collect) any sent callback
this.callback = function () {};
this.async = false;
this.expected = 0;
} else {
this.assert = new Assert(this);
}
}
Test.count = 0;
function getModulesForStartEvent(startingModule) {
var module = startingModule;
var modules = [];
while (module && module.testsRun === 0) {
modules.push(module);
module = module.parentModule;
}
// The above starts from the child and moves up to the parent.
// Return this in reversed order, such that we start with top-most parent.
return modules.reverse();
}
Test.prototype = {
// Use a getter to avoid computing a stack trace (which can be expensive),
// This is displayed by the HTML Reporter, but most other integrations do
// not access it.
get stack() {
return extractStacktrace(this.errorForStack, this.stackOffset);
},
before: function () {
var module = this.module;
var notStartedModules = getModulesForStartEvent(module);
// ensure the callbacks are executed serially for each module
var moduleStartChain = _Promise.resolve();
notStartedModules.forEach(startModule => {
moduleStartChain = moduleStartChain.then(() => {
startModule.stats = {
all: 0,
bad: 0,
started: performance.now()
};
emit('suiteStart', startModule.suiteReport.start(true));
return runLoggingCallbacks('moduleStart', {
name: startModule.name,
tests: startModule.tests
});
});
});
return moduleStartChain.then(() => {
config.current = this;
this.started = performance.now();
emit('testStart', this.testReport.start(true));
return runLoggingCallbacks('testStart', {
name: this.testName,
module: module.name,
testId: this.testId,
previousFailure: this.previousFailure
}).then(() => {
if (!config.pollution) {
saveGlobal();
}
});
});
},
run: function () {
config.current = this;
var promise;
if (config.notrycatch) {
promise = this.withData ? this.callback.call(this.testEnvironment, this.assert, this.data) : this.callback.call(this.testEnvironment, this.assert);
} else {
try {
promise = this.withData ? this.callback.call(this.testEnvironment, this.assert, this.data) : this.callback.call(this.testEnvironment, this.assert);
} catch (e) {
this.pushFailure('Died on test #' + (this.assertions.length + 1) + ': ' + (e.message || e) + '\n' + this.stack, extractStacktrace(e, 0));
// Else next test will carry the responsibility
saveGlobal();
// Restart the tests if they're blocking
if (config.blocking) {
internalRecover(this);
}
}
}
this.resolvePromise(promise);
// If the test has an async "pause" on it, but the timeout is 0, then we push a
// failure as the test should be synchronous.
if (this.timeout === 0 && this.pauses.size > 0) {
pushFailure('Test did not finish synchronously even though assert.timeout( 0 ) was used.', sourceFromStacktrace(2));
}
},
after: function () {
checkPollution();
},
queueGlobalHook(hook, hookName) {
var runGlobalHook = () => {
config.current = this;
var promise;
if (config.notrycatch) {
promise = hook.call(this.testEnvironment, this.assert);
} else {
try {
promise = hook.call(this.testEnvironment, this.assert);
} catch (error) {
this.pushFailure('Global ' + hookName + ' failed on ' + this.testName + ': ' + errorString(error), extractStacktrace(error, 0));
return;
}
}
this.resolvePromise(promise, hookName);
};
return runGlobalHook;
},
queueHook(hook, hookName, hookOwner) {
var runHook = () => {
if (hookName === 'before' && hookOwner.testsRun !== 0) {
return;
}
// The 'after' hook should only execute when there are no tests left and
// when the 'after' and 'finish' tasks are the only tasks left to process
if (hookName === 'after' && !lastTestWithinModuleExecuted(hookOwner) && (config.queue.length > 0 || config._pq.taskCount() > 2)) {
return;
}
config.current = this;
// before and after hooks are called with the owning module's testEnvironment
var testEnvironment = hookName === 'before' || hookName === 'after' ? hookOwner.testEnvironment : this.testEnvironment;
var promise;
if (config.notrycatch) {
promise = hook.call(testEnvironment, this.assert);
} else {
try {
promise = hook.call(testEnvironment, this.assert);
} catch (error) {
this.pushFailure(hookName + ' failed on ' + this.testName + ': ' + (error.message || error), extractStacktrace(error, 0));
return;
}
}
this.resolvePromise(promise, hookName);
};
return runHook;
},
// Currently only used for module level hooks, can be used to add global level ones
hooks(handler) {
var hooks = [];
function processGlobalhooks(test) {
if ((handler === 'beforeEach' || handler === 'afterEach') && config._globalHooks[handler]) {
for (var i = 0; i < config._globalHooks[handler].length; i++) {
hooks.push(test.queueGlobalHook(config._globalHooks[handler][i], handler));
}
}
}
function processHooks(test, module) {
if (module.parentModule) {
processHooks(test, module.parentModule);
}
if (module.hooks[handler].length) {
for (var i = 0; i < module.hooks[handler].length; i++) {
hooks.push(test.queueHook(module.hooks[handler][i], handler, module));
}
}
}
// Hooks are ignored on skipped tests
if (!this.skip) {
processGlobalhooks(this);
processHooks(this, this.module);
}
return hooks;
},
finish: function () {
config.current = this;
// Release the timeout and timeout callback references to be garbage collected.
// https://github.com/qunitjs/qunit/pull/1708
if (setTimeout$1) {
clearTimeout(this.timeout);
config.timeoutHandler = null;
}
// Release the test callback to ensure that anything referenced has been
// released to be garbage collected.
this.callback = undefined;
if (this.steps.length) {
var stepsList = this.steps.join(', ');
this.pushFailure("Expected assert.verifySteps() to be called before end of test after using assert.step(). Unverified steps: ".concat(stepsList), this.stack);
}
if (config.requireExpects && this.expected === null) {
this.pushFailure('Expected number of assertions to be defined, but expect() was not called.', this.stack);
} else if (this.expected !== null && this.stepsCount && this.expected === this.assertions.length + this.stepsCount) {
this.pushFailure('Expected ' + this.expected + ' assertions, but ' + this.assertions.length + ' were run.\nIt looks like you are upgrading from QUnit 2. Steps no longer count as separate assertions. https://qunitjs.com/api/assert/expect/', this.stack);
} else if (this.expected !== null && this.expected !== this.assertions.length) {
this.pushFailure('Expected ' + this.expected + ' assertions, but ' + this.assertions.length + ' were run', this.stack);
} else if (this.expected === null && !this.assertions.length) {
this.pushFailure('Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.', this.stack);
}
var module = this.module;
var moduleName = module.name;
var testName = this.testName;
var skipped = !!this.skip;
var todo = !!this.todo;
var bad = 0;
var storage = config.storage;
this.runtime = Math.round(performance.now() - this.started);
config.stats.all += this.assertions.length;
config.stats.testCount += 1;
module.stats.all += this.assertions.length;
for (var i = 0; i < this.assertions.length; i++) {
// For legacy reasons, `config.stats` reflects raw assertion counts.
// This means all failures add to the "bad" count, even an expected
// failure inside a passing "todo" test.
// See also https://qunitjs.com/api/callbacks/QUnit.done/
if (!this.assertions[i].result) {
bad++;
config.stats.bad++;
module.stats.bad++;
}
}
if (skipped) {
incrementTestsIgnored(module);
} else {
incrementTestsRun(module);
}
// Store result when possible.
// Note that this also marks todo tests as bad, thus they get hoisted,
// and always run first on refresh.
if (storage) {
if (bad) {
storage.setItem('qunit-test-' + moduleName + '-' + testName, bad);
} else {
storage.removeItem('qunit-test-' + moduleName + '-' + testName);
}
}
// After emitting the event, we trim the assertion data to
// avoid leaking it. It is not used by the legacy testDone callbacks.
emit('testEnd', this.testReport.end(true));
this.testReport.slimAssertions();
var test = this;
return runLoggingCallbacks('testDone', {
name: testName,
module: moduleName,
skipped: skipped,
todo: todo,
failed: bad,
passed: this.assertions.length - bad,
total: this.assertions.length,
runtime: skipped ? 0 : this.runtime,
assertions: this.assertions,
testId: this.testId,
// Source of Test
// generating stack trace is expensive, so using a getter will help defer this until we need it
get source() {
return test.stack;
}
}).then(function () {
// Emit the `suiteEnd` event and `moduleDone` callbacks for modules
// that are completed as of now.
var completedModules = [];
var parent = module;
while (parent && allTestsExecuted(parent)) {
completedModules.push(parent);
parent = parent.parentModule;
}
var moduleDoneChain = _Promise.resolve();
completedModules.forEach(completedModule => {
moduleDoneChain = moduleDoneChain.then(() => {
return logSuiteEnd(completedModule);
});
});
return moduleDoneChain;
}).then(function () {
config.current = undefined;
});
function logSuiteEnd(module) {
// Empty `module.hooks` to ensure that anything referenced in these hooks
// has been released to be garbage collected. Descendant modules that were
// entirely skipped, e.g. due to filtering, will never have this method
// called for them, but might have hooks with references that hold data in
// memory (even if the hooks were never executed), so we empty the
// hooks on all descendant modules here as well. This is safe because we
// will never call this as long as any descendant modules still have tests
// to run. This also means that for deeply nested modules, we might empty
// the hooks on completed child modules multiple times. That's harmless.
var modules = [module];
while (modules.length) {
var nextModule = modules.shift();
nextModule.hooks = {};
modules.push(...nextModule.childModules);
}
emit('suiteEnd', module.suiteReport.end(true));
return runLoggingCallbacks('moduleDone', {
name: module.name,
tests: module.tests,
failed: module.stats.bad,
passed: module.stats.all - module.stats.bad,
total: module.stats.all,
runtime: Math.round(performance.now() - module.stats.started)
});
}
},
queue() {
var test = this;
if (!this.valid()) {
incrementTestsIgnored(this.module);
return;
}
function runTest() {
return [function () {
return test.before();
}, ...test.hooks('before'), function () {
test.testEnvironment = extend({}, test.module.testEnvironment, false, true);
}, ...test.hooks('beforeEach'), function () {
test.run();
}, ...test.hooks('afterEach').reverse(), ...test.hooks('after').reverse(), function () {
test.after();
}, function () {
return test.finish();
}];
}
var previousFailCount = config.storage && +config.storage.getItem('qunit-test-' + this.module.name + '-' + this.testName);
// Prioritize previously failed tests, detected from storage
var prioritize = config.reorder && !!previousFailCount;
this.previousFailure = !!previousFailCount;
config._pq.add(runTest, prioritize);
},
pushResult: function (resultInfo) {
if (this !== config.current) {
var message = resultInfo && resultInfo.message || '';
var testName = this && this.testName || '';
var error = 'Assertion occurred after test finished.\n' + '> Test: ' + testName + '\n' + '> Message: ' + message + '\n';
throw new Error(error);
}
// Destructure of resultInfo = { result, actual, expected, message, negative }
var details = {
module: this.module.name,
name: this.testName,
result: resultInfo.result,
message: resultInfo.message,
actual: resultInfo.actual,
testId: this.testId,
negative: resultInfo.negative || false,
runtime: Math.round(performance.now() - this.started),
todo: !!this.todo
};
if (hasOwn$3.call(resultInfo, 'expected')) {
details.expected = resultInfo.expected;
}
if (!resultInfo.result) {
var source = resultInfo.source || sourceFromStacktrace();
if (source) {
details.source = source;
}
}
this.logAssertion(details);
this.assertions.push({
result: !!resultInfo.result,
message: resultInfo.message
});
},
pushFailure: function (message, source) {
if (!(this instanceof Test)) {
throw new Error('pushFailure() assertion outside test context, was ' + sourceFromStacktrace(2));
}
this.pushResult({
result: false,
message: message || 'error',
source
});
},
/**
* Log assertion details using both the old QUnit.log interface and
* QUnit.on( "assertion" ) interface.
*
* @private
*/
logAssertion(details) {
runLoggingCallbacks('log', details);
var assertion = {
passed: details.result,
actual: details.actual,
expected: details.expected,
message: details.message,
stack: details.source,
todo: details.todo
};
this.testReport.pushAssertion(assertion);
emit('assertion', assertion);
},
/**
* Reset config.timeout with a new timeout duration.
*
* @param {number} timeoutDuration
*/
internalResetTimeout(timeoutDuration) {
clearTimeout(config.timeout);
config.timeout = setTimeout$1(config.timeoutHandler(timeoutDuration), timeoutDuration);
},
/**
* Create a new async pause and return a new function that can release the pause.
*
* This mechanism is internally used by:
*
* - explicit async pauses, created by calling `assert.async()`,
* - implicit async pauses, created when `QUnit.test()` or module hook callbacks
* use async-await or otherwise return a Promise.
*
* Happy scenario:
*
* - Pause is created by calling internalStop().
*
* Pause is released normally by invoking release() during the same test.
*
* The release() callback lets internal processing resume.
*
* Failure scenarios:
*
* - The test fails due to an uncaught exception.
*
* In this case, Test.run() will call internalRecover() which empties the clears all
* async pauses and sets the cancelled flag, which means we silently ignore any
* late calls to the resume() callback, as we will have moved on to a different
* test by then, and we don't want to cause an extra "release during a different test"
* errors that the developer isn't really responsible for. This can happen when a test
* correctly schedules a call to release(), but also causes an uncaught error. The
* uncaught error means we will no longer wait for the release (as it might not arrive).
*
* - Pause is never released, or called an insufficient number of times.
*
* Our timeout handler will kill the pause and resume test processing, basically
* like internalRecover(), but for one pause instead of any/all.
*
* Here, too, any late calls to resume() will be silently ignored to avoid
* extra errors. We tolerate this since the original test will have already been
* marked as failure.
*
* - Pause is spontaneously released during a different test,
* or when no test is currently running.
*
* This is close to impossible because this error only happens if the original test
* successfully finished first (since other failure scenarios kill pauses and ignore
* late calls). It can happen if a test ended exactly as expected, but has some
* external or shared state continuing to hold a reference to the release callback,
* and either the same test scheduled another call to it in the future, or a later test
* causes it to be called through some shared state.
*
* - Pause release() is called too often, during the same test.
*
* This simply throws an error, after which uncaught error handling picks it up
* and processing resumes.
*
* @param {number} [requiredCalls=1]
*/
internalStop() {
var requiredCalls = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
config.blocking = true;
var test = this;
var pauseId = this.nextPauseId++;
var pause = {
cancelled: false,
remaining: requiredCalls
};
test.pauses.set(pauseId, pause);
function release() {
if (pause.cancelled) {
return;
}
if (config.current === undefined) {
throw new Error('Unexpected release of async pause after tests finished.\n' + "> Test: ".concat(test.testName, " [async #").concat(pauseId, "]"));
}
if (config.current !== test) {
throw new Error('Unexpected release of async pause during a different test.\n' + "> Test: ".concat(test.testName, " [async #").concat(pauseId, "]"));
}
if (pause.remaining <= 0) {
throw new Error('Tried to release async pause that was already released.\n' + "> Test: ".concat(test.testName, " [async #").concat(pauseId, "]"));
}
// The `requiredCalls` parameter exists to support `assert.async(count)`
pause.remaining--;
if (pause.remaining === 0) {
test.pauses.delete(pauseId);
}
internalStart(test);
}
// Set a recovery timeout, if so configured.
if (setTimeout$1) {
var timeoutDuration;
if (typeof test.timeout === 'number') {
timeoutDuration = test.timeout;
} else if (typeof config.testTimeout === 'number') {
timeoutDuration = config.testTimeout;
} else {
Logger.warn("QUnit.config.testTimeout was set to an an invalid value (".concat(dump.typeOf(config.testTimeout), "). Using default. https://qunitjs.com/api/config/testTimeout/"));
timeoutDuration = 3000;
}
if (timeoutDuration > 0) {
config.timeoutHandler = function (timeout) {
return function () {
config.timeout = null;
pause.cancelled = true;
test.pauses.delete(pauseId);
test.pushFailure("Test took longer than ".concat(timeout, "ms; test timed out."), sourceFromStacktrace(2));
internalRecover(test);
};
};
clearTimeout(config.timeout);
config.timeout = setTimeout$1(config.timeoutHandler(timeoutDuration), timeoutDuration);
}
}
return release;
},
resolvePromise: function (promise, phase) {
if (promise !== undefined && promise !== null) {
var _test = this;
if (typeof promise.then === 'function') {
var resume = _test.internalStop();
var resolve = function () {
resume();
};
if (config.notrycatch) {
promise.then(resolve);
} else {
var reject = function (error) {
var message = 'Promise rejected ' + (!phase ? 'during' : phase.replace(/Each$/, '')) + ' "' + _test.testName + '": ' + (error && error.message || error);
_test.pushFailure(message, extractStacktrace(error, 0));
// Else next test will carry the responsibility
saveGlobal();
// Unblock
internalRecover(_test);
};
// Note that `promise` is a user-supplied thenable, not per-se a standard Promise.
// This means it can be a "bad thenable" where calling then() can also throw.
// This is not about a rejected Promise or throwing async function.
// In those cases, `ret.then(, cb)` will not throw, but inform you via cb(err).
// Instead, this is testing a bad Thenable implementation, where then() itself
// throws an error. This is not possible with native Promise, but is possible with
// custom Promise-compatible libraries.
// We must catch this to avoid breaking the ProcessingQueue.
// Promise.resolve() normalizes and catches even these internal errors and
// sends them to reject.
_Promise.resolve(promise).then(resolve, reject);
}
}
}
},
valid: function () {
function moduleChainIdMatch(testModule, selectedId) {
return (
// undefined or empty array
!selectedId || !selectedId.length || inArray(testModule.moduleId, selectedId) || testModule.parentModule && moduleChainIdMatch(testModule.parentModule, selectedId)
);
}
if (!moduleChainIdMatch(this.module, config.moduleId)) {
return false;
}
if (config.testId && config.testId.length && !inArray(this.testId, config.testId)) {
return false;
}
function moduleChainNameMatch(testModule, selectedModule) {
if (!selectedModule) {
// undefined or empty string
return true;
}
var testModuleName = testModule.name ? testModule.name.toLowerCase() : null;
if (testModuleName === selectedModule) {
return true;
} else if (testModule.parentModule) {
return moduleChainNameMatch(testModule.parentModule, selectedModule);
} else {
return false;
}
}
var selectedModule = config.module && config.module.toLowerCase();
if (!moduleChainNameMatch(this.module, selectedModule)) {
return false;
}
var filter = config.filter;
if (filter) {
var regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter);
var fullName = this.module.name + ': ' + this.testName;
if (regexFilter) {
if (!this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName)) {
return false;
}
} else if (!this.stringFilter(filter, fullName)) {
return false;
}
}
if (typeof config.testFilter === 'function') {
var testInfo = {
testId: this.testId,
testName: this.testName,
module: this.module.name,
skip: !!this.skip
};
try {
return !!config.testFilter(testInfo);
} catch (error) {
Logger.warn('Error in QUnit.config.testFilter callback: ', error);
return false;
}
}
return true;
},
regexFilter: function (exclude, pattern, flags, fullName) {
var regex = new RegExp(pattern, flags);
var match = regex.test(fullName);
return match !== exclude;
},
stringFilter: function (filter, fullName) {
filter = filter.toLowerCase();
fullName = fullName.toLowerCase();
var include = filter.charAt(0) !== '!';
if (!include) {
filter = filter.slice(1);
}
// If the filter matches, we need to honour include
if (fullName.indexOf(filter) !== -1) {
return include;
}
// Otherwise, do the opposite
return !include;
}
};
function pushFailure() {
if (!config.current) {
throw new Error('pushFailure() assertion outside test context, in ' + sourceFromStacktrace(2));
}
// Gets current test obj
var currentTest = config.current;
return currentTest.pushFailure.apply(currentTest, arguments);
}
function saveGlobal() {
config.pollution = [];
if (config.noglobals) {
for (var key in g) {
if (hasOwn$3.call(g, key)) {
// In Opera sometimes DOM element ids show up here, ignore them
if (/^qunit-test-output/.test(key)) {
continue;
}
config.pollution.push(key);
}
}
}
}
function checkPollution() {
var old = config.pollution;
saveGlobal();
var newGlobals = diff$1(config.pollution, old);
if (newGlobals.length > 0) {
pushFailure('Introduced global variable(s): ' + newGlobals.join(', '));
}
var deletedGlobals = diff$1(old, config.pollution);
if (deletedGlobals.length > 0) {
pushFailure('Deleted global variable(s): ' + deletedGlobals.join(', '));
}
}
var focused = false; // indicates that the "only" filter was used
function addTest(settings) {
if (focused || config.currentModule.ignored) {
return;
}
var newTest = new Test(settings);
newTest.queue();
}
function addOnlyTest(settings) {
if (config.currentModule.ignored) {
return;
}
if (!focused) {
config.queue.length = 0;
clearSuiteReports(config.currentModule);
focused = true;
}
var newTest = new Test(settings);
newTest.queue();
}
// Will be exposed as QUnit.test
function test(testName, callback) {
addTest({
testName,
callback
});
}
function makeEachTestName(testName, argument) {
return "".concat(testName, " [").concat(argument, "]");
}
// Characters to avoid in test names especially CLI/AP output:
// * x00-1F: e.g. NULL, backspace (\b), line breaks (\r\n), ESC.
// * x74: DEL.
// * xA0: non-breaking space.
//
// See https://en.wikipedia.org/wiki/ASCII#Character_order
//
// eslint-disable-next-line no-control-regex
var rNonObviousStr = /[\x00-\x1F\x7F\xA0]/;
function runEach(data, eachFn) {
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
var value = data[i];
// Create automatic labels for primitive data in arrays passed to test.each().
// We want to avoid the default "example [0], example [1]" where possible since
// these are not self-explanatory in results, and are also tedious to locate
// the source of since the numerical key of an array isn't literally in the
// code (you have to count).
//
// Design requirements:
// * Unique. Each label must be unique and correspond 1:1 with a data value.
// This way each test name will hash to a unique testId with Rerun link,
// without having to rely on Test class enforcing uniqueness with invisible
// space hack.
// * Unambiguous. While technical uniqueness is a hard requirement above,
// we also want the labels to be obvious and unambiguous to humans.
// For example, abbreviating "foobar" and "foobaz" to "f" and "fo" is
// technically unique, but ambiguous to humans which one is which.
// * Short and readable. Where possible we omit the array index numbers
// so that in most cases, the value is simply shown as-is.
// We prefer "example [foo], example [bar]"
// over "example [0: foo], example [2: bar]".
// This also has the benefit of being stable and robust against e.g.
// re-ordering data or adding new items during development, without
// invalidating a previous filter or rerun link immediately.
var valueType = typeof value;
var testKey = i;
if (valueType === 'string' && value.length <= 40 && !rNonObviousStr.test(value) && !/\s*\d+: /.test(value)) {
testKey = value;
} else if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'undefined' || value === null) {
var valueForName = String(value);
if (!rNonObviousStr.test(valueForName)) {
testKey = i + ': ' + (valueForName.length <= 30 ? valueForName : valueForName.slice(0, 29) + '…');
}
}
eachFn(value, testKey);
}
} else if (typeof data === 'object' && data !== null) {
for (var key in data) {
eachFn(data[key], key);
}
} else {
throw new TypeError("test.each() expects an array or object as input, but\nfound ".concat(typeof data, " instead."));
}
}
extend(test, {
todo: function (testName, callback) {
addTest({
testName,
callback,
todo: true
});
},
skip: function (testName) {
addTest({
testName,
skip: true
});
},
if: function (testName, condition, callback) {
addTest({
testName,
callback,
skip: !condition
});
},
only: function (testName, callback) {
addOnlyTest({
testName,
callback
});
},
each: function (testName, dataset, callback) {
runEach(dataset, (data, testKey) => {
addTest({
testName: makeEachTestName(testName, testKey),
callback,
withData: true,
stackOffset: 5,
data
});
});
}
});
test.todo.each = function (testName, dataset, callback) {
runEach(dataset, (data, testKey) => {
addTest({
testName: makeEachTestName(testName, testKey),
callback,
todo: true,
withData: true,
stackOffset: 5,
data
});
});
};
test.skip.each = function (testName, dataset) {
runEach(dataset, (_, testKey) => {
addTest({
testName: makeEachTestName(testName, testKey),
stackOffset: 5,
skip: true
});
});
};
test.if.each = function (testName, condition, dataset, callback) {
runEach(dataset, (data, testKey) => {
addTest({
testName: makeEachTestName(testName, testKey),
callback,
withData: true,
stackOffset: 5,
skip: !condition,
data: condition ? data : undefined
});
});
};
test.only.each = function (testName, dataset, callback) {
runEach(dataset, (data, testKey) => {
addOnlyTest({
testName: makeEachTestName(testName, testKey),
callback,
withData: true,
stackOffset: 5,
data
});
});
};
// Forcefully release all processing holds.
function internalRecover(test) {
test.pauses.forEach(pause => {
pause.cancelled = true;
});
test.pauses.clear();
internalStart(test);
}
// Release a processing hold, scheduling a resumption attempt if no holds remain.
function internalStart(test) {
// Ignore if other async pauses still exist.
if (test.pauses.size > 0) {
return;
}
// Add a slight delay to allow more assertions etc.
if (setTimeout$1) {
clearTimeout(config.timeout);
config.timeout = setTimeout$1(function () {
if (test.pauses.size > 0) {
return;
}
clearTimeout(config.timeout);
config.timeout = null;
config.blocking = false;
config._pq.advance();
});
} else {
config.blocking = false;
config._pq.advance();
}
}
function collectTests(module) {
var tests = [].concat(module.tests);
var modules = [...module.childModules];
// Do a breadth-first traversal of the child modules
while (modules.length) {
var nextModule = modules.shift();
tests.push.apply(tests, nextModule.tests);
modules.push(...nextModule.childModules);
}
return tests;
}
// This returns true after all executable and skippable tests
// in a module have been processed, and informs 'suiteEnd'
// and moduleDone().
function allTestsExecuted(module) {
return module.testsRun + module.testsIgnored === collectTests(module).length;
}
// This returns true during the last executable non-skipped test
// within a module, and informs the running of the 'after' hook
// for a given module. This runs only once for a given module,
// but must run during the last non-skipped test. When it runs,
// there may be non-zero skipped tests left.
function lastTestWithinModuleExecuted(module) {
return module.testsRun === collectTests(module).filter(test => !test.skip).length - 1;
}
function incrementTestsRun(module) {
module.testsRun++;
while (module = module.parentModule) {
module.testsRun++;
}
}
function incrementTestsIgnored(module) {
module.testsIgnored++;
while (module = module.parentModule) {
module.testsIgnored++;
}
}
function unblockAndAdvanceQueue() {
config.blocking = false;
config._pq.advance();
}
function doStart() {
if (config.started) {
unblockAndAdvanceQueue();
return;
}
// QUnit.config.reporters is considered writable between qunit.js and QUnit.start().
// Now that QUnit.start() has been called, it is time to decide which built-in reporters
// to load.
// For config.reporters.html, refer to browser-runner.js and HtmlReporter#onRunStart.
/* istanbul ignore if: internal guard */
if (!config._QUnit) {
throw new ReferenceError('QUnit is undefined. Cannot call start() before qunit.js exports QUnit.');
}
if (config.reporters.console) {
reporters.console.init(config._QUnit);
}
if (config.reporters.perf || config.reporters.perf === undefined && window$1 && document) {
reporters.perf.init(config._QUnit);
}
if (config.reporters.tap) {
reporters.tap.init(config._QUnit);
}
// The test run hasn't officially begun yet
// Record the time of the test run's beginning
config.started = performance.now();
// Delete the unnamed module if no global tests were defined (see config.js)
if (config.modules[0].name === '' && config.modules[0].tests.length === 0) {
config.modules.shift();
}
// Create a list of simplified and independent module descriptor objects for
// the QUnit.begin callbacks. This prevents plugins from relying on reading
// from (or writing!) to internal state.
var modulesLog = [];
for (var i = 0; i < config.modules.length; i++) {
// Always omit the unnamed module from the list of module names
// for UI plugins, even if there were global tests defined.
if (config.modules[i].name !== '') {
modulesLog.push({
name: config.modules[i].name,
moduleId: config.modules[i].moduleId
});
}
}
// The test run is officially beginning now
emit('runStart', globalSuiteReport.start(true));
runLoggingCallbacks('begin', {
totalTests: Test.count,
modules: modulesLog
}).then(unblockAndAdvanceQueue);
}
function start() {
if (config.current) {
throw new Error('QUnit.start cannot be called inside a test.');
}
if (config._runStarted) {
if (document && config.autostart) {
throw new Error('QUnit.start() called too many times. Did you call QUnit.start() in browser context when autostart is also enabled? https://qunitjs.com/api/QUnit/start/');
}
throw new Error('QUnit.start() called too many times.');
}
config._runStarted = true;
// Add a slight delay to allow definition of more modules and tests.
if (document && document.readyState !== 'complete' && setTimeout$1) {
// In browser environments, if QUnit.start() is called very early,
// still wait for DOM ready to ensure reliable integration of reporters.
window$1.addEventListener('load', function () {
setTimeout$1(function () {
doStart();
});
});
} else if (setTimeout$1) {
setTimeout$1(function () {
doStart();
});
} else {
doStart();
}
}
function initFixture(QUnit, document) {
var config = QUnit.config;
// Stores fixture HTML for resetting later
function storeFixture() {
// Avoid overwriting user-defined values
if (config.fixture !== undefined) {
// If set to null, do nothing as the fixture feature is disabled.
//
// If set to string or HTMLElement, also do nothing because in that case
// we will restore the fixture to the configured value after each test,
// instead of using the initial DOM value of #qunit-fixture as the
// configured value to restore (default behaviour).
return;
}
var fixture = document.getElementById('qunit-fixture');
if (fixture) {
config.fixture = fixture.cloneNode(true);
}
}
QUnit.begin(storeFixture);
// Resets the fixture DOM element if available.
function resetFixture() {
if (config.fixture === undefined || config.fixture === null) {
return;
}
var fixture = document.getElementById('qunit-fixture');
var resetFixtureType = typeof config.fixture;
if (resetFixtureType === 'string') {
// support user defined values for `config.fixture`
var newFixture = document.createElement('div');
newFixture.setAttribute('id', 'qunit-fixture');
newFixture.innerHTML = config.fixture;
fixture.parentNode.replaceChild(newFixture, fixture);
} else {
var clonedFixture = config.fixture.cloneNode(true);
fixture.parentNode.replaceChild(clonedFixture, fixture);
}
}
QUnit.testStart(resetFixture);
}
var hasOwn = Object.prototype.hasOwnProperty;
function initUrlConfig(QUnit) {
// Wait until QUnit.begin() so that users can add their keys to urlConfig
// any time during test loading, including during `QUnit.on('runStart')`.
QUnit.begin(function () {
var urlConfig = QUnit.config.urlConfig;
for (var i = 0; i < urlConfig.length; i++) {
// Options can be either strings or objects with nonempty "id" properties
var option = QUnit.config.urlConfig[i];
if (typeof option !== 'string') {
option = option.id;
}
// only create new property for user-defined QUnit.config.urlConfig keys
// that don't conflict with a built-in QUnit.config option or are otherwise
// already set. This prevents internal TypeError from bad urls where keys
// could otherwise unexpectedly be set to type string or array.
//
// Given that HTML Reporter sets checkbox state based on QUnit.config,
// instead of QUnit.urlParams, this also helps make sure that checkboxes
// for built-in keys are correctly shown as off if a urlParams value exists
// but was invalid and discarded by config.js.
if (!hasOwn.call(QUnit.config, option)) {
QUnit.config[option] = QUnit.urlParams[option];
}
}
});
}
function initBrowser(QUnit, window, document) {
// Report uncaught exceptions to QUnit.
//
// Wrap and preserve any pre-existing window.onerror.
// An existing handler can "accepts" the error by returning true.
//
// Returning true from window.onerror suppresses the browser's default error
// reporting. Likewise, we will also not report it in that case.
var originalWindowOnError = window.onerror;
window.onerror = function (message, fileName, lineNumber, columnNumber, errorObj) {
var ret = false;
if (originalWindowOnError) {
for (var _len = arguments.length, args = new Array(_len > 5 ? _len - 5 : 0), _key = 5; _key < _len; _key++) {
args[_key - 5] = arguments[_key];
}
ret = originalWindowOnError.call(this, message, fileName, lineNumber, columnNumber, errorObj, ...args);
}
// Treat return value as window.onerror itself does,
// Only do our handling if not suppressed.
if (ret !== true) {
// If there is a current test that sets the internal `ignoreGlobalErrors` field
// (such as during `assert.throws()`), then the error is ignored and native
// error reporting is suppressed as well. This is because in browsers, an error
// can sometimes end up in `window.onerror` instead of in the local try/catch.
// This ignoring of errors does not apply to our general onUncaughtException
// method, nor to our `unhandledRejection` handlers, as those are not meant
// to receive an "expected" error during `assert.throws()`.
if (QUnit.config.current && QUnit.config.current.ignoreGlobalErrors) {
return true;
}
// According to
// https://blog.sentry.io/2016/01/04/client-javascript-reporting-window-onerror,
// most modern browsers support an errorObj argument; use that to
// get a full stack trace if it's available.
var error = errorObj || new Error(message);
if (!error.stack && fileName && lineNumber) {
error.stack = "".concat(fileName, ":").concat(lineNumber);
}
QUnit.onUncaughtException(error);
}
return ret;
};
window.addEventListener('unhandledrejection', function (event) {
if (!QUnit.config.ignoreUnhandledRejections) {
QUnit.onUncaughtException(event.reason);
}
});
QUnit.on('runEnd', function (runEnd) {
if (QUnit.config.altertitle && document.title) {
// Show ✖ for good, ✔ for bad suite result in title
// use escape sequences in case file gets loaded with non-utf-8
// charset
document.title = [runEnd.status === 'failed' ? '\u2716' : '\u2714', document.title.replace(/^[\u2714\u2716] /i, '')].join(' ');
}
// Scroll back to top to show results
if (QUnit.config.scrolltop && window.scrollTo) {
window.scrollTo(0, 0);
}
});
initFixture(QUnit, document);
initUrlConfig(QUnit);
// Unless explicitly disabled via preconfig, initialize HtmlReporter now
// (i.e. QUnit.config.reporters.html is undefined or true).
//
// This allows the UI to render instantly (since QUnit 3.0) for cases where the
// qunit.js script is after `<div id="qunit">`, which is recommended.
//
// Otherwise, we'll fallback to waiting with a blank page until window.onload,
// which is how it's always been in QUnit 1 and QUnit 2.
//
// Note that HtmlReporter constructor will only render an initial layout and
// listen to 1 event. The final decision on whether to attach event handlers
// and render the interactive UI is made from HtmlReporter#onRunStart, which
// is also where it will honor QUnit.config.reporters.html if it was set to
// false between qunit.js (here) and onRunStart.
//
// If someone explicitly sets QUnit.config.reporters.html to false via preconfig,
// but then changes it at runtime to true, that is unsupported and the reporter
// will remain disabled.
if (QUnit.config.reporters.html !== false) {
QUnit.reporters.html.init(QUnit);
}
// NOTE:
// * It is important to attach error handlers (above) before setting up reporters,
// to ensure reliable reporting of error events.
// * Is is important to set up HTML Reporter (if enabled) before calling QUnit.start(),
// as otherwise it will miss the first few or even all synchronous events.
//
// Prior to QUnit 3.0, the reporter was initialised here, between error handler (above),
// and start (below). As of QUnit 3.0, reporters are initialized by doStart() within
// QUnit.start(), which is logically the same place, but decoupled from initBrowser().
function autostart() {
// Check as late as possible because if projects set autostart=false,
// they generally do so in their own scripts, after qunit.js.
if (QUnit.config.autostart) {
QUnit.start();
}
}
if (document.readyState === 'complete') {
autostart();
} else {
window.addEventListener('load', autostart, false);
}
}
// Imports that define the QUnit API
// Finalise internal state and exports before we export the API
config.currentModule = unnamedModule;
config._pq = new ProcessingQueue();
var assert = Assert.prototype;
var isLocal = window$1 && window$1.location && window$1.location.protocol === 'file:';
var begin = createRegisterCallbackFunction('begin');
var done = createRegisterCallbackFunction('done');
var log = createRegisterCallbackFunction('log');
var moduleDone = createRegisterCallbackFunction('moduleDone');
var moduleStart = createRegisterCallbackFunction('moduleStart');
var testDone = createRegisterCallbackFunction('testDone');
var testStart = createRegisterCallbackFunction('testStart');
var only = test.only;
var skip = test.skip;
var todo = test.todo;
var QUnit = {
assert,
begin,
config,
diff,
done,
dump,
equiv,
extend,
hooks,
is,
isLocal,
log,
module: module$1,
moduleDone,
moduleStart,
objectType,
on,
only,
onUncaughtException,
pushFailure,
reporters,
skip,
stack,
start,
test,
testDone,
testStart,
todo,
urlParams,
version
};
// Inject the exported QUnit API for use by reporters in start()
config._QUnit = QUnit;
// Support: require('qunit').QUnit
//
// For interop and consistency between Node.js `module.exports = QUnit`
// and CommonJS environments `exports.QUnit = QUnit`, the below will
// effectively assign `module.exports.QUnit = QUnit` as well.
QUnit.QUnit = QUnit;
if (window$1 && document) {
// In browsers, throw if QUnit is loaded a second time.
// This must not throw if a global called "QUnit" exists for preconfiguring,
// in that case we simply upgrade/replace it with the proper export.
// Such preconfig global would only have QUnit.config set, not e.g. QUnit.version.
if (g.QUnit && g.QUnit.version) {
throw new Error('QUnit has already been defined.');
}
}
// Ensure the global is available in all environments.
//
// For backward compatibility, we only enforce load-once in browsers above.
// In other environments QUnit is accessible via import/require() and may
// load multiple times, including different versions from different sources.
// Callers decide whether to make their secondary instance global or not.
if (!g.QUnit || !g.QUnit.version) {
g.QUnit = QUnit;
}
if (window$1 && document) {
initBrowser(QUnit, window$1, document);
}
export { QUnit, assert, begin, config, QUnit as default, diff, done, dump, equiv, extend, hooks, is, isLocal, log, module$1 as module, moduleDone, moduleStart, objectType, on, onUncaughtException, only, pushFailure, reporters, skip, stack, start, test, testDone, testStart, todo, urlParams, version };