diff --git a/package.json b/package.json index ca04ab40..c1cfcd6e 100644 --- a/package.json +++ b/package.json @@ -57,8 +57,8 @@ "grunt-contrib-copy": "^1.0.0", "grunt-contrib-cssmin": "^2.0.0", "grunt-contrib-jshint": "^1.1.0", - "grunt-contrib-qunit": "^1.3.0", - "grunt-contrib-uglify": "^2.2.1", + "grunt-contrib-qunit": "^2.0.0", + "grunt-contrib-uglify": "^2.3.0", "grunt-contrib-watch": "^1.0.0", "grunt-htmlhint": "^0.9.13", "grunt-jscs": "^3.0.1", diff --git a/test.html b/test.html index 4bfd9339..c7b67eab 100644 --- a/test.html +++ b/test.html @@ -3,10 +3,10 @@ Tablesorter Testing (WIP) - + - + diff --git a/testing/qunit-2.1.1.css b/testing/qunit-2.3.1.css similarity index 97% rename from testing/qunit-2.1.1.css rename to testing/qunit-2.3.1.css index 50fe5c29..35a2be1f 100644 --- a/testing/qunit-2.1.1.css +++ b/testing/qunit-2.3.1.css @@ -1,12 +1,12 @@ /*! - * QUnit 2.1.1 + * QUnit 2.3.1 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * - * Date: 2017-01-06T01:52Z + * Date: 2017-04-10T19:56Z */ /** Font Family and Sizes */ @@ -236,7 +236,7 @@ } #qunit-tests.hidepass li.running, -#qunit-tests.hidepass li.pass { +#qunit-tests.hidepass li.pass:not(.todo) { visibility: hidden; position: absolute; width: 0; @@ -384,6 +384,7 @@ background-color: #EBECE9; } +#qunit-tests .qunit-todo-label, #qunit-tests .qunit-skipped-label { background-color: #F4FF77; display: inline-block; @@ -394,6 +395,10 @@ margin: -0.4em 0.4em -0.4em 0; } +#qunit-tests .qunit-todo-label { + background-color: #EEE; +} + /** Result */ #qunit-testresult { diff --git a/testing/qunit-2.1.1.js b/testing/qunit-2.3.1.js similarity index 81% rename from testing/qunit-2.1.1.js rename to testing/qunit-2.3.1.js index e90caadf..c0de6c29 100644 --- a/testing/qunit-2.1.1.js +++ b/testing/qunit-2.3.1.js @@ -1,12 +1,12 @@ /*! - * QUnit 2.1.1 + * QUnit 2.3.1 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * - * Date: 2017-01-06T01:52Z + * Date: 2017-04-10T19:56Z */ (function (global$1) { 'use strict'; @@ -20,7 +20,17 @@ var document = window && window.document; var navigator = window && window.navigator; - var sessionStorage = window && window.sessionStorage; + + var localSessionStorage = function () { + var x = "qunit-test-string"; + try { + global$1.sessionStorage.setItem(x, x); + global$1.sessionStorage.removeItem(x); + return global$1.sessionStorage; + } catch (e) { + return undefined; + } + }(); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; @@ -141,19 +151,16 @@ return result; } - // From jquery.js + /** + * Determines whether an element exists in a given array or not. + * + * @method inArray + * @param {Any} elem + * @param {Array} array + * @return {Boolean} + */ function inArray(elem, array) { - if (array.indexOf) { - return array.indexOf(elem); - } - - for (var i = 0, length = array.length; i < length; i++) { - if (array[i] === elem) { - return i; - } - } - - return -1; + return array.indexOf(elem) !== -1; } /** @@ -231,26 +238,47 @@ 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); + } + // Test for equality any JavaScript type. - // Author: Philippe Rathé + // Authors: Philippe Rathé , David Chan var equiv = (function () { - // Stack to decide between skip/abort functions - var callers = []; - - // Stack to avoiding loops from circular referencing - var parents = []; - var parentsB = []; + // Value pairs queued for comparison. Used for breadth-first processing order, recursion + // detection and avoiding repeated comparison (see below for details). + // Elements are { a: val, b: val }. + var pairs = []; var getProto = Object.getPrototypeOf || function (obj) { return obj.__proto__; }; - function useStrictEquality(b, a) { + function useStrictEquality(a, b) { - // To catch short annotation VS 'new' annotation of a declaration. e.g.: + // This only gets called if a and b are not strict equal, and is used to compare on + // the primitive values inside object wrappers. For example: // `var i = 1;` // `var j = new Number(1);` + // Neither a nor b can be null, as a !== b and they have the same type. if ((typeof a === "undefined" ? "undefined" : _typeof(a)) === "object") { a = a.valueOf(); } @@ -293,6 +321,31 @@ return "flags" in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; } + function isContainer(val) { + return ["object", "array", "map", "set"].indexOf(objectType(val)) !== -1; + } + + function breadthFirstCompareChild(a, b) { + + // If a is a container not reference-equal to b, postpone the comparison to the + // end of the pairs queue -- unless (a, b) has been seen before, in which case skip + // over the pair. + if (a === b) { + return true; + } + if (!isContainer(a)) { + return typeEquiv(a, b); + } + if (pairs.every(function (pair) { + return pair.a !== a || pair.b !== b; + })) { + + // Not yet started comparing this pair + pairs.push({ a: a, b: b }); + } + return true; + } + var callbacks = { "string": useStrictEquality, "boolean": useStrictEquality, @@ -306,24 +359,20 @@ return true; }, - "regexp": function regexp(b, a) { + "regexp": function regexp(a, b) { return a.source === b.source && // Include flags in the comparison getRegExpFlags(a) === getRegExpFlags(b); }, - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function _function(b, a) { - - var caller = callers[callers.length - 1]; - return caller !== Object && typeof caller !== "undefined" && a.toString() === b.toString(); + // abort (identical references / instance methods were skipped earlier) + "function": function _function() { + return false; }, - "array": function array(b, a) { - var i, j, len, loop, aCircular, bCircular; + "array": function array(a, b) { + var i, len; len = a.length; if (len !== b.length) { @@ -332,50 +381,63 @@ return false; } - // Track reference to avoid circular references - parents.push(a); - parentsB.push(b); for (i = 0; i < len; i++) { - loop = false; - for (j = 0; j < parents.length; j++) { - aCircular = parents[j] === a[i]; - bCircular = parentsB[j] === b[i]; - if (aCircular || bCircular) { - if (a[i] === b[i] || aCircular && bCircular) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if (!loop && !innerEquiv(a[i], b[i])) { - parents.pop(); - parentsB.pop(); + + // Compare non-containers; queue non-reference-equal containers + if (!breadthFirstCompareChild(a[i], b[i])) { return false; } } - parents.pop(); - parentsB.pop(); return true; }, - "set": function set$$1(b, a) { + // 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( [ {}, [], [] ] ); + // b = new Set( [ {}, {}, [] ] ); + "set": function set$$1(a, b) { var innerEq, outerEq = true; 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; } a.forEach(function (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; + } + innerEq = false; b.forEach(function (bVal) { + var parentPairs; + + // Likewise, short-circuit if the result is already known + if (innerEq) { + return; + } + + // Swap out the global pairs list, as the nested call to + // innerEquiv will clobber its contents + parentPairs = pairs; if (innerEquiv(bVal, aVal)) { innerEq = true; } + + // Replace the global pairs list + pairs = parentPairs; }); if (!innerEq) { @@ -386,21 +448,54 @@ return outerEq; }, - "map": function map(b, a) { + // 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": function map(a, b) { var innerEq, outerEq = true; 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; } a.forEach(function (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; + } + innerEq = false; b.forEach(function (bVal, bKey) { + var parentPairs; + + // Likewise, short-circuit if the result is already known + if (innerEq) { + return; + } + + // Swap out the global pairs list, as the nested call to + // innerEquiv will clobber its contents + parentPairs = pairs; if (innerEquiv([bVal, bKey], [aVal, aKey])) { innerEq = true; } + + // Replace the global pairs list + pairs = parentPairs; }); if (!innerEq) { @@ -411,53 +506,32 @@ return outerEq; }, - "object": function object(b, a) { - var i, j, loop, aCircular, bCircular; - - // Default to true - var eq = true; - var aProperties = []; - var bProperties = []; + "object": function object(a, b) { + var i, + aProperties = [], + bProperties = []; if (compareConstructors(a, b) === false) { return false; } - // Stack constructor before traversing properties - callers.push(a.constructor); - - // Track reference to avoid circular references - parents.push(a); - parentsB.push(b); - // Be strict: don't ensure hasOwnProperty and go deep for (i in a) { - loop = false; - for (j = 0; j < parents.length; j++) { - aCircular = parents[j] === a[i]; - bCircular = parentsB[j] === b[i]; - if (aCircular || bCircular) { - if (a[i] === b[i] || aCircular && bCircular) { - loop = true; - } else { - eq = false; - break; - } - } - } + + // Collect a's properties aProperties.push(i); - if (!loop && !innerEquiv(a[i], b[i])) { - eq = false; - break; + + // 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; + } + + // Compare non-containers; queue non-reference-equal containers + if (!breadthFirstCompareChild(a[i], b[i])) { + return false; } } - parents.pop(); - parentsB.pop(); - - // Unstack, we are done - callers.pop(); - for (i in b) { // Collect b's properties @@ -465,28 +539,52 @@ } // Ensures identical properties name - return eq && innerEquiv(aProperties.sort(), bProperties.sort()); + return typeEquiv(aProperties.sort(), bProperties.sort()); } }; function typeEquiv(a, b) { var type = objectType(a); - return objectType(b) === type && callbacks[type](b, a); + + // Callbacks for containers will append to the pairs queue to achieve breadth-first + // search order. The pairs queue is also used to avoid reprocessing any pair of + // containers that are reference-equal to a previously visited pair (a special case + // this being recursion detection). + // + // Because of this approach, once typeEquiv returns a false value, it should not be + // called again without clearing the pair queue else it may wrongly report a visited + // pair as being equivalent. + return objectType(b) === type && callbacks[type](a, b); } - // The real equiv function function innerEquiv(a, b) { + var i, pair; // We're done when there's nothing more to compare if (arguments.length < 2) { return true; } - // Require type-specific equality - return (a === b || typeEquiv(a, b)) && ( + // Clear the global pair queue and add the top-level values being compared + pairs = [{ a: a, b: b }]; + + for (i = 0; i < pairs.length; i++) { + pair = pairs[i]; + + // Perform type-specific comparison on any pairs that are not strictly + // equal. For container types, that comparison will postpone comparison + // of any sub-container pair to the end of the pair queue. This gives + // breadth-first search order. It also avoids the reprocessing of + // reference-equal siblings, cousins etc, which can have a significant speed + // impact when comparing a container of small objects each of which has a + // reference to the same (singleton) large object. + if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { + return false; + } + } // ...across all consecutive argument pairs - arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1))); + return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); } return innerEquiv; @@ -531,9 +629,6 @@ // Set of all modules. modules: [], - // Stack of nested modules - moduleStack: [], - // The first unnamed module currentModule: { name: "", @@ -545,7 +640,7 @@ callbacks: {}, // The storage module to use for reordering tests - storage: sessionStorage + storage: localSessionStorage }; // take a predefined QUnit.config and extend the defaults @@ -616,10 +711,10 @@ var res, parser, parserType, - inStack = inArray(obj, stack); + objIndex = stack.indexOf(obj); - if (inStack !== -1) { - return "recursion(" + (inStack - stack.length) + ")"; + if (objIndex !== -1) { + return "recursion(" + (objIndex - stack.length) + ")"; } objType = objType || this.typeOf(obj); @@ -749,7 +844,7 @@ nonEnumerableProperties = ["message", "name"]; for (i in nonEnumerableProperties) { key = nonEnumerableProperties[i]; - if (key in map && inArray(key, keys) < 0) { + if (key in map && !inArray(key, keys)) { keys.push(key); } } @@ -843,6 +938,64 @@ return dump; })(); + var LISTENERS = Object.create(null); + var SUPPORTED_EVENTS = ["runStart", "suiteStart", "testStart", "assertion", "testEnd", "suiteEnd", "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) { + if (objectType(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 = LISTENERS[eventName]; + var callbacks = originalCallbacks ? [].concat(toConsumableArray(originalCallbacks)) : []; + + for (var i = 0; i < callbacks.length; i++) { + callbacks[i](data); + } + } + + /** + * Registers a callback as a listener to the specified event. + * + * @public + * @method on + * @param {String} eventName + * @param {Function} callback + * @return {Void} + */ + function on(eventName, callback) { + if (objectType(eventName) !== "string") { + throw new TypeError("eventName must be a string when registering a listener"); + } else if (!inArray(eventName, SUPPORTED_EVENTS)) { + var events = SUPPORTED_EVENTS.join(", "); + throw new Error("\"" + eventName + "\" is not a valid event; must be one of: " + events + "."); + } else if (objectType(callback) !== "function") { + throw new TypeError("callback must be a function when registering a listener"); + } + + if (!LISTENERS[eventName]) { + LISTENERS[eventName] = []; + } + + // Don't register the same callback more than once + if (!inArray(callback, LISTENERS[eventName])) { + LISTENERS[eventName].push(callback); + } + } + // Register logging callbacks function registerLoggingCallbacks(obj) { var i, @@ -929,9 +1082,240 @@ return extractStacktrace(error, offset); } - var unitSampler; - var focused = false; var priorityCount = 0; + var unitSampler = void 0; + + /** + * Advances the ProcessingQueue to the next item if it is ready. + * @param {Boolean} last + */ + function advance() { + var start = now(); + config.depth = (config.depth || 0) + 1; + + while (config.queue.length && !config.blocking) { + var elapsedTime = now() - start; + + if (!defined.setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) { + if (priorityCount > 0) { + priorityCount--; + } + + config.queue.shift()(); + } else { + setTimeout(advance, 13); + break; + } + } + + config.depth--; + + if (!config.blocking && !config.queue.length && config.depth === 0) { + done(); + } + } + + function addToQueueImmediate(callback) { + if (objectType(callback) === "array") { + while (callback.length) { + addToQueueImmediate(callback.pop()); + } + + return; + } + + config.queue.unshift(callback); + priorityCount++; + } + + /** + * Adds a function to the ProcessingQueue for execution. + * @param {Function|Array} callback + * @param {Boolean} priority + * @param {String} seed + */ + function addToQueue(callback, prioritize, seed) { + if (prioritize) { + config.queue.splice(priorityCount++, 0, callback); + } else if (seed) { + if (!unitSampler) { + unitSampler = unitSamplerGenerator(seed); + } + + // Insert into a random position after all prioritized items + var index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); + config.queue.splice(priorityCount + index, 0, callback); + } else { + config.queue.push(callback); + } + } + + /** + * Creates a seeded "sample" generator which is used for randomizing tests. + */ + function unitSamplerGenerator(seed) { + + // 32-bit xorshift, requires only a nonzero seed + // http://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; + }; + } + + /** + * This function is called when the ProcessingQueue is done processing all + * items. It handles emitting the final run events. + */ + function done() { + var storage = config.storage; + + ProcessingQueue.finished = true; + + var runtime = now() - config.started; + var passed = config.stats.all - config.stats.bad; + + emit("runEnd", globalSuite.end(true)); + runLoggingCallbacks("done", { + passed: passed, + failed: config.stats.bad, + total: config.stats.all, + runtime: runtime + }); + + // Clear own storage items if all tests passed + if (storage && config.stats.bad === 0) { + for (var i = storage.length - 1; i >= 0; i--) { + var key = storage.key(i); + + if (key.indexOf("qunit-test-") === 0) { + storage.removeItem(key); + } + } + } + } + + var ProcessingQueue = { + finished: false, + add: addToQueue, + addImmediate: addToQueueImmediate, + advance: advance + }; + + var TestReport = function () { + function TestReport(name, suite, options) { + classCallCheck(this, TestReport); + + 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); + } + + createClass(TestReport, [{ + key: "start", + value: function start(recordTime) { + if (recordTime) { + this._startTime = Date.now(); + } + + return { + name: this.name, + suiteName: this.suiteName, + fullName: this.fullName.slice() + }; + } + }, { + key: "end", + value: function end(recordTime) { + if (recordTime) { + this._endTime = Date.now(); + } + + return extend(this.start(), { + runtime: this.getRuntime(), + status: this.getStatus(), + errors: this.getFailedAssertions(), + assertions: this.getAssertions() + }); + } + }, { + key: "pushAssertion", + value: function pushAssertion(assertion) { + this.assertions.push(assertion); + } + }, { + key: "getRuntime", + value: function getRuntime() { + return this._endTime - this._startTime; + } + }, { + key: "getStatus", + value: function 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"; + } + } + }, { + key: "getFailedAssertions", + value: function getFailedAssertions() { + return this.assertions.filter(function (assertion) { + return !assertion.passed; + }); + } + }, { + key: "getAssertions", + value: function getAssertions() { + return this.assertions.slice(); + } + + // Remove actual and expected values from assertions. This is to prevent + // leaking memory throughout a test suite. + + }, { + key: "slimAssertions", + value: function slimAssertions() { + this.assertions = this.assertions.map(function (assertion) { + delete assertion.actual; + delete assertion.expected; + return assertion; + }); + } + }]); + return TestReport; + }(); + + var focused = false; function Test(settings) { var i, l; @@ -942,9 +1326,15 @@ extend(this, settings); this.assertions = []; this.semaphore = 0; - this.usedAsync = false; this.module = config.currentModule; this.stack = sourceFromStacktrace(3); + this.steps = []; + + this.testReport = new TestReport(settings.testName, this.module.suiteReport, { + todo: settings.todo, + skip: settings.skip, + valid: this.valid() + }); // Register unique strings for (i = 0, l = this.module.tests; i < l.length; i++) { @@ -995,6 +1385,7 @@ for (i = notStartedModules.length - 1; i >= 0; i--) { startModule = notStartedModules[i]; startModule.stats = { all: 0, bad: 0, started: now() }; + emit("suiteStart", startModule.suiteReport.start(true)); runLoggingCallbacks("moduleStart", { name: startModule.name, tests: startModule.tests @@ -1003,15 +1394,10 @@ config.current = this; - if (module.testEnvironment) { - delete module.testEnvironment.before; - delete module.testEnvironment.beforeEach; - delete module.testEnvironment.afterEach; - delete module.testEnvironment.after; - } this.testEnvironment = extend({}, module.testEnvironment); this.started = now(); + emit("testStart", this.testReport.start(true)); runLoggingCallbacks("testStart", { name: this.testName, module: module.name, @@ -1072,7 +1458,7 @@ test.preserveEnvironment = true; } - if (hookName === "after" && hookOwner.testsRun !== numberOfTests(hookOwner) - 1) { + if (hookName === "after" && hookOwner.testsRun !== numberOfTests(hookOwner) - 1 && config.queue.length > 2) { return; } @@ -1102,8 +1488,8 @@ if (module.parentModule) { processHooks(test, module.parentModule); } - if (module.testEnvironment && objectType(module.testEnvironment[handler]) === "function") { - hooks.push(test.queueHook(module.testEnvironment[handler], handler, module)); + if (module.hooks && objectType(module.hooks[handler]) === "function") { + hooks.push(test.queueHook(module.hooks[handler], handler, module)); } } @@ -1129,6 +1515,7 @@ moduleName = module.name, testName = this.testName, skipped = !!this.skip, + todo = !!this.todo, bad = 0, storage = config.storage; @@ -1156,10 +1543,16 @@ } } + // After emitting the js-reporters event we cleanup 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(); + runLoggingCallbacks("testDone", { name: testName, module: moduleName, skipped: skipped, + todo: todo, failed: bad, passed: this.assertions.length - bad, total: this.assertions.length, @@ -1174,6 +1567,21 @@ }); if (module.testsRun === numberOfTests(module)) { + logSuiteEnd(module); + + // Check if the parent modules, iteratively, are done. If that the case, + // we emit the `suiteEnd` event and trigger `moduleDone` callback. + var parent = module.parentModule; + while (parent && parent.testsRun === numberOfTests(parent)) { + logSuiteEnd(parent); + parent = parent.parentModule; + } + } + + config.current = undefined; + + function logSuiteEnd(module) { + emit("suiteEnd", module.suiteReport.end(true)); runLoggingCallbacks("moduleDone", { name: module.name, tests: module.tests, @@ -1183,8 +1591,6 @@ runtime: now() - module.stats.started }); } - - config.current = undefined; }, preserveTestEnvironment: function preserveTestEnvironment() { @@ -1195,18 +1601,16 @@ }, queue: function queue() { - var priority, - previousFailCount, - test = this; + var test = this; if (!this.valid()) { return; } - function run() { + function runTest() { // Each of these can by async - synchronize([function () { + ProcessingQueue.addImmediate([function () { test.before(); }, test.hooks("before"), function () { test.preserveTestEnvironment(); @@ -1219,17 +1623,26 @@ }]); } - previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName); + var previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName); // Prioritize previously failed tests, detected from storage - priority = config.reorder && previousFailCount; + var prioritize = config.reorder && !!previousFailCount; this.previousFailure = !!previousFailCount; - return synchronize(run, priority, config.seed); + ProcessingQueue.add(runTest, prioritize, config.seed); + + // If the queue has already finished, we manually process the new test + if (ProcessingQueue.finished) { + ProcessingQueue.advance(); + } }, + pushResult: function pushResult(resultInfo) { + if (this !== config.current) { + throw new Error("Assertion occured after test had finished."); + } // Destructure of resultInfo = { result, actual, expected, message, negative } var source, @@ -1242,18 +1655,19 @@ expected: resultInfo.expected, testId: this.testId, negative: resultInfo.negative || false, - runtime: now() - this.started + runtime: now() - this.started, + todo: !!this.todo }; if (!resultInfo.result) { - source = sourceFromStacktrace(); + source = resultInfo.source || sourceFromStacktrace(); if (source) { details.source = source; } } - runLoggingCallbacks("log", details); + this.logAssertion(details); this.assertions.push({ result: !!resultInfo.result, @@ -1266,28 +1680,37 @@ throw new Error("pushFailure() assertion outside test context, was " + sourceFromStacktrace(2)); } - var details = { - module: this.module.name, - name: this.testName, + this.pushResult({ result: false, message: message || "error", actual: actual || null, - testId: this.testId, - runtime: now() - this.started - }; - - if (source) { - details.source = source; - } - - runLoggingCallbacks("log", details); - - this.assertions.push({ - result: false, - message: message + expected: null, + source: source }); }, + /** + * Log assertion details using both the old QUnit.log interface and + * QUnit.on( "assertion" ) interface. + * + * @private + */ + logAssertion: function 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); + }, + + resolvePromise: function resolvePromise(promise, phase) { var then, resume, @@ -1331,7 +1754,7 @@ } function moduleChainIdMatch(testModule) { - return inArray(testModule.moduleId, config.moduleId) > -1 || testModule.parentModule && moduleChainIdMatch(testModule.parentModule); + return inArray(testModule.moduleId, config.moduleId) || testModule.parentModule && moduleChainIdMatch(testModule.parentModule); } // Internally-generated tests are always valid @@ -1344,7 +1767,7 @@ return false; } - if (config.testId && config.testId.length > 0 && inArray(this.testId, config.testId) < 0) { + if (config.testId && config.testId.length > 0 && !inArray(this.testId, config.testId)) { return false; } @@ -1397,79 +1820,6 @@ return currentTest.pushFailure.apply(currentTest, arguments); } - // Based on Java's String.hashCode, a simple but not - // rigorously collision resistant hashing function - function generateHash(module, testName) { - var hex, - i = 0, - hash = 0, - str = module + "\x1C" + testName, - len = str.length; - - for (; i < len; 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 - hex = (0x100000000 + hash).toString(16); - if (hex.length < 8) { - hex = "0000000" + hex; - } - - return hex.slice(-8); - } - - function synchronize(callback, priority, seed) { - var last = !priority, - index; - - if (objectType(callback) === "array") { - while (callback.length) { - synchronize(callback.shift()); - } - return; - } - - if (priority) { - config.queue.splice(priorityCount++, 0, callback); - } else if (seed) { - if (!unitSampler) { - unitSampler = unitSamplerGenerator(seed); - } - - // Insert into a random position after all priority items - index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); - config.queue.splice(priorityCount + index, 0, callback); - } else { - config.queue.push(callback); - } - - if (internalState.autorun && !config.blocking) { - process(last); - } - } - - function unitSamplerGenerator(seed) { - - // 32-bit xorshift, requires only a nonzero seed - // http://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; - }; - } - function saveGlobal() { config.pollution = []; @@ -1511,9 +1861,7 @@ return; } - var newTest; - - newTest = new Test({ + var newTest = new Test({ testName: testName, callback: callback }); @@ -1521,6 +1869,20 @@ newTest.queue(); } + function todo(testName, callback) { + if (focused) { + return; + } + + var newTest = new Test({ + testName: testName, + callback: callback, + todo: true + }); + + newTest.queue(); + } + // Will be exposed as QUnit.skip function skip(testName) { if (focused) { @@ -1537,8 +1899,6 @@ // Will be exposed as QUnit.only function only(testName, callback) { - var newTest; - if (focused) { return; } @@ -1546,7 +1906,7 @@ config.queue.length = 0; focused = true; - newTest = new Test({ + var newTest = new Test({ testName: testName, callback: callback }); @@ -1633,8 +1993,8 @@ } function numberOfTests(module) { - var count = module.tests.length, - modules = [].concat(toConsumableArray(module.childModules)); + var count = module.tests.length; + var modules = [].concat(toConsumableArray(module.childModules)); // Do a breadth-first traversal of the child modules while (modules.length) { @@ -1653,6 +2013,24 @@ } } + /** + * Returns a function that proxies to the given method name on the globals + * console object. The proxy will also detect if the console doesn't exist and + * will appropriately no-op. This allows support for IE9, which doesn't have a + * console if the developer tools are not open. + */ + function consoleProxy(method) { + return function () { + if (console) { + console[method].apply(console, arguments); + } + }; + } + + var Logger = { + warn: consoleProxy("warn") + }; + var Assert = function () { function Assert(testContext) { classCallCheck(this, Assert); @@ -1662,11 +2040,34 @@ // Assert helpers - // Specify the number of expected assertions to guarantee that failed test - // (no assertions are run at all) don't slip through. + // Documents a "step", which is a string value, in a test as a passing assertion createClass(Assert, [{ + key: "step", + value: function step(message) { + var result = !!message; + + this.test.steps.push(message); + + return this.pushResult({ + result: result, + message: message || "You must provide a message to assert.step" + }); + } + + // Verifies the steps in a test match a given array of string values + + }, { + key: "verifySteps", + value: function verifySteps(steps, message) { + this.deepEqual(this.test.steps, steps, message); + } + + // Specify the number of expected assertions to guarantee that failed test + // (no assertions are run at all) don't slip through. + + }, { key: "expect", value: function expect(asserts) { if (arguments.length === 1) { @@ -1681,18 +2082,22 @@ }, { key: "async", value: function async(count) { - var test$$1 = this.test, - popped = false, + var test$$1 = this.test; + + var popped = false, acceptCallCount = count; if (typeof acceptCallCount === "undefined") { acceptCallCount = 1; } - test$$1.usedAsync = true; var resume = internalStop(test$$1); return function done() { + if (config.current !== test$$1) { + throw Error("assert.async callback called after test finished."); + } + if (popped) { test$$1.pushFailure("Too many calls to the `assert.async` callback", sourceFromStacktrace(2)); return; @@ -1714,7 +2119,7 @@ }, { key: "push", value: function push(result, actual, expected, message, negative) { - console.warn("assert.push is deprecated and will be removed in QUnit 3.0." + " Please use assert.pushResult instead (http://api.qunitjs.com/pushResult/)."); + Logger.warn("assert.push is deprecated and will be removed in QUnit 3.0." + " Please use assert.pushResult instead (http://api.qunitjs.com/pushResult/)."); var currentAssert = this instanceof Assert ? this : config.current.assert; return currentAssert.pushResult({ @@ -1730,8 +2135,8 @@ value: function pushResult(resultInfo) { // Destructure of resultInfo = { result, actual, expected, message, negative } - var assert = this, - currentTest = assert instanceof Assert && assert.test || config.current; + var assert = this; + var currentTest = assert instanceof Assert && assert.test || config.current; // Backwards compatibility fix. // Allows the direct use of global exported assertions and QUnit.assert.* @@ -1742,12 +2147,6 @@ throw new Error("assertion outside test context, in " + sourceFromStacktrace(2)); } - if (currentTest.usedAsync === true && currentTest.semaphore === 0) { - currentTest.pushFailure("Assertion after the final `assert.async` was resolved", sourceFromStacktrace(2)); - - // Allow this assertion to continue running anyway... - } - if (!(assert instanceof Assert)) { assert = currentTest.assert; } @@ -1884,8 +2283,9 @@ key: "throws", value: function throws(block, expected, message) { var actual = void 0, - result = false, - currentTest = this instanceof Assert && this.test || config.current; + result = false; + + var currentTest = this instanceof Assert && this.test || config.current; // 'expected' is optional unless doing string comparison if (objectType(expected) === "string") { @@ -2011,66 +2411,194 @@ } } - (function () { - if (!defined.document) { - return; + var SuiteReport = function () { + function SuiteReport(name, parentSuite) { + classCallCheck(this, SuiteReport); + + this.name = name; + this.fullName = parentSuite ? parentSuite.fullName.concat(name) : []; + + this.tests = []; + this.childSuites = []; + + if (parentSuite) { + parentSuite.pushChildSuite(this); + } } - // `onErrorFnPrev` initialized at top of scope - // Preserve other handlers - var onErrorFnPrev = window.onerror; - - // Cover uncaught exceptions - // Returning true will suppress the default browser handler, - // returning false will let it run. - window.onerror = function (error, filePath, linerNr) { - var ret = false; - if (onErrorFnPrev) { - ret = onErrorFnPrev(error, filePath, linerNr); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if (ret !== true) { - if (config.current) { - if (config.current.ignoreGlobalErrors) { - return true; - } - pushFailure(error, filePath + ":" + linerNr); - } else { - test("global failure", extend(function () { - pushFailure(error, filePath + ":" + linerNr); - }, { validTest: true })); + createClass(SuiteReport, [{ + key: "start", + value: function start(recordTime) { + if (recordTime) { + this._startTime = Date.now(); } - return false; - } - return ret; - }; - })(); + return { + name: this.name, + fullName: this.fullName.slice(), + tests: this.tests.map(function (test) { + return test.start(); + }), + childSuites: this.childSuites.map(function (suite) { + return suite.start(); + }), + testCounts: { + total: this.getTestCounts().total + } + }; + } + }, { + key: "end", + value: function end(recordTime) { + if (recordTime) { + this._endTime = Date.now(); + } + + return { + name: this.name, + fullName: this.fullName.slice(), + tests: this.tests.map(function (test) { + return test.end(); + }), + childSuites: this.childSuites.map(function (suite) { + return suite.end(); + }), + testCounts: this.getTestCounts(), + runtime: this.getRuntime(), + status: this.getStatus() + }; + } + }, { + key: "pushChildSuite", + value: function pushChildSuite(suite) { + this.childSuites.push(suite); + } + }, { + key: "pushTest", + value: function pushTest(test) { + this.tests.push(test); + } + }, { + key: "getRuntime", + value: function getRuntime() { + return this._endTime - this._startTime; + } + }, { + key: "getTestCounts", + value: function getTestCounts() { + var counts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 }; + + counts = this.tests.reduce(function (counts, test) { + if (test.valid) { + counts[test.getStatus()]++; + counts.total++; + } + + return counts; + }, counts); + + return this.childSuites.reduce(function (counts, suite) { + return suite.getTestCounts(counts); + }, counts); + } + }, { + key: "getStatus", + value: function getStatus() { + var _getTestCounts = this.getTestCounts(), + total = _getTestCounts.total, + failed = _getTestCounts.failed, + skipped = _getTestCounts.skipped, + todo = _getTestCounts.todo; + + if (failed) { + return "failed"; + } else { + if (skipped === total) { + return "skipped"; + } else if (todo === total) { + return "todo"; + } else { + return "passed"; + } + } + } + }]); + return SuiteReport; + }(); + + // Handle an unhandled exception. By convention, returns true if further + // error handling should be suppressed and false otherwise. + // In this case, we will only suppress further error handling if the + // "ignoreGlobalErrors" configuration option is enabled. + function onError(error) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + if (config.current) { + if (config.current.ignoreGlobalErrors) { + return true; + } + pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); + } else { + test("global failure", extend(function () { + pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); + }, { validTest: true })); + } + + return false; + } var QUnit = {}; + var globalSuite = new SuiteReport(); + // The initial "currentModule" represents the global (or top-level) module that + // is not explicitly defined by the user, therefore we add the "globalSuite" to + // it since each module has a suiteReport associated with it. + config.currentModule.suiteReport = globalSuite; + + var moduleStack = []; var globalStartCalled = false; var runStarted = false; - var internalState = { - autorun: false - }; - // Figure out if we're running the tests from a server or not QUnit.isLocal = !(defined.document && window.location.protocol !== "file:"); // Expose the current QUnit version - QUnit.version = "2.1.1"; + QUnit.version = "2.3.1"; + + function createModule(name, testEnvironment) { + var parentModule = moduleStack.length ? moduleStack.slice(-1)[0] : null; + var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name; + var parentSuite = parentModule ? parentModule.suiteReport : globalSuite; + + var module = { + name: moduleName, + parentModule: parentModule, + tests: [], + moduleId: generateHash(moduleName), + testsRun: 0, + childModules: [], + suiteReport: new SuiteReport(name, parentSuite) + }; + + var env = {}; + if (parentModule) { + parentModule.childModules.push(module); + extend(env, parentModule.testEnvironment); + } + extend(env, testEnvironment); + module.testEnvironment = env; + + config.modules.push(module); + return module; + } extend(QUnit, { + on: on, // Call on start of module test to prepend name to all tests module: function module(name, testEnvironment, executeNow) { - var module, moduleFns; - var currentModule = config.currentModule; - if (arguments.length === 2) { if (objectType(testEnvironment) === "function") { executeNow = testEnvironment; @@ -2078,58 +2606,46 @@ } } - module = createModule(); + var module = createModule(name, testEnvironment); - moduleFns = { + // Move any hooks to a 'hooks' object + if (module.testEnvironment) { + module.hooks = { + before: module.testEnvironment.before, + beforeEach: module.testEnvironment.beforeEach, + afterEach: module.testEnvironment.afterEach, + after: module.testEnvironment.after + }; + + delete module.testEnvironment.before; + delete module.testEnvironment.beforeEach; + delete module.testEnvironment.afterEach; + delete module.testEnvironment.after; + } + + var moduleFns = { before: setHook(module, "before"), beforeEach: setHook(module, "beforeEach"), afterEach: setHook(module, "afterEach"), after: setHook(module, "after") }; + var currentModule = config.currentModule; if (objectType(executeNow) === "function") { - config.moduleStack.push(module); - setCurrentModule(module); + moduleStack.push(module); + config.currentModule = module; executeNow.call(module.testEnvironment, moduleFns); - config.moduleStack.pop(); + moduleStack.pop(); module = module.parentModule || currentModule; } - setCurrentModule(module); - - function createModule() { - var parentModule = config.moduleStack.length ? config.moduleStack.slice(-1)[0] : null; - var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name; - var module = { - name: moduleName, - parentModule: parentModule, - tests: [], - moduleId: generateHash(moduleName), - testsRun: 0, - childModules: [] - }; - - var env = {}; - if (parentModule) { - parentModule.childModules.push(module); - extend(env, parentModule.testEnvironment); - delete env.beforeEach; - delete env.afterEach; - } - extend(env, testEnvironment); - module.testEnvironment = env; - - config.modules.push(module); - return module; - } - - function setCurrentModule(module) { - config.currentModule = module; - } + config.currentModule = module; }, test: test, + todo: todo, + skip: skip, only: only, @@ -2146,14 +2662,18 @@ throw new Error("Called start() outside of a test context too many times"); } else if (config.autostart) { throw new Error("Called start() outside of a test context when " + "QUnit.config.autostart was true"); - } else if (!defined.document && !config.pageLoaded) { - - // Starts from Node even if .load was not previously called - QUnit.load(); } else if (!config.pageLoaded) { - // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it + // The page isn't completely loaded yet, so we set autostart and then + // load if we're in Node or wait for the browser's load event. config.autostart = true; + + // Starts from Node even if .load was not previously called. We still return + // early otherwise we'll wind up "beginning" twice. + if (!defined.document) { + QUnit.load(); + } + return; } } else { @@ -2195,7 +2715,9 @@ stack: function stack(offset) { offset = (offset || 0) + 2; return sourceFromStacktrace(offset); - } + }, + + onError: onError }); QUnit.pushFailure = pushFailure; @@ -2244,6 +2766,7 @@ } // The test run is officially beginning now + emit("runStart", globalSuite.start(true)); runLoggingCallbacks("begin", { totalTests: Test.count, modules: modulesLog @@ -2251,72 +2774,16 @@ } config.blocking = false; - process(true); - } - - function process(last) { - function next() { - process(last); - } - var start = now(); - config.depth = (config.depth || 0) + 1; - - while (config.queue.length && !config.blocking) { - if (!defined.setTimeout || config.updateRate <= 0 || now() - start < config.updateRate) { - if (config.current) { - - // Reset async tracking for each phase of the Test lifecycle - config.current.usedAsync = false; - } - config.queue.shift()(); - } else { - setTimeout(next, 13); - break; - } - } - config.depth--; - if (last && !config.blocking && !config.queue.length && config.depth === 0) { - done(); - } - } - - function done() { - var runtime, - passed, - i, - key, - storage = config.storage; - - internalState.autorun = true; - - runtime = now() - config.started; - passed = config.stats.all - config.stats.bad; - - runLoggingCallbacks("done", { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - }); - - // Clear own storage items if all tests passed - if (storage && config.stats.bad === 0) { - for (i = storage.length - 1; i >= 0; i--) { - key = storage.key(i); - if (key.indexOf("qunit-test-") === 0) { - storage.removeItem(key); - } - } - } + ProcessingQueue.advance(); } function setHook(module, hookName) { - if (module.testEnvironment === undefined) { - module.testEnvironment = {}; + if (!module.hooks) { + module.hooks = {}; } return function (callback) { - module.testEnvironment[hookName] = callback; + module.hooks[hookName] = callback; }; } @@ -2456,6 +2923,13 @@ } })(); + var stats = { + passedTests: 0, + failedTests: 0, + skippedTests: 0, + todoTests: 0 + }; + // Escape text for attribute or text content. function escapeText(s) { if (!s) { @@ -3046,7 +3520,8 @@ var banner = id("qunit-banner"), tests = id("qunit-tests"), abortButton = id("qunit-abort-tests-button"), - html = ["Tests completed in ", details.runtime, " milliseconds.
", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed."].join(""), + totalTests = stats.passedTests + stats.skippedTests + stats.todoTests + stats.failedTests, + html = [totalTests, " tests completed in ", details.runtime, " milliseconds, with ", stats.failedTests, " failed, ", stats.skippedTests, " skipped, and ", stats.todoTests, " todo.
", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed."].join(""), test, assertLi, assertList; @@ -3069,7 +3544,7 @@ } if (banner && (!abortButton || abortButton.disabled === false)) { - banner.className = details.failed ? "qunit-fail" : "qunit-pass"; + banner.className = stats.failedTests ? "qunit-fail" : "qunit-pass"; } if (abortButton) { @@ -3084,7 +3559,7 @@ // Show ✖ for good, ✔ for bad suite result in title // use escape sequences in case file gets loaded with non-utf-8-charset - document$$1.title = [details.failed ? "\u2716" : "\u2714", document$$1.title.replace(/^[\u2714\u2716] /i, "")].join(" "); + document$$1.title = [stats.failedTests ? "\u2716" : "\u2714", document$$1.title.replace(/^[\u2714\u2716] /i, "")].join(" "); } // Scroll back to top to show results @@ -3224,7 +3699,10 @@ good = details.passed; bad = details.failed; - if (bad === 0) { + // This test passed if it has no unexpected failed assertions + var testPassed = details.failed > 0 ? details.todo : !details.todo; + + if (testPassed) { // Collapse the passing tests addClass(assertList, "qunit-collapsed"); @@ -3248,6 +3726,8 @@ testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; if (details.skipped) { + stats.skippedTests++; + testItem.className = "skipped"; skipped = document$$1.createElement("em"); skipped.className = "qunit-skipped-label"; @@ -3258,12 +3738,28 @@ toggleClass(assertList, "qunit-collapsed"); }); - testItem.className = bad ? "fail" : "pass"; + testItem.className = testPassed ? "pass" : "fail"; + + if (details.todo) { + var todoLabel = document$$1.createElement("em"); + todoLabel.className = "qunit-todo-label"; + todoLabel.innerHTML = "todo"; + testItem.className += " todo"; + testItem.insertBefore(todoLabel, testTitle); + } time = document$$1.createElement("span"); time.className = "runtime"; time.innerHTML = details.runtime + " ms"; testItem.insertBefore(time, assertList); + + if (!testPassed) { + stats.failedTests++; + } else if (details.todo) { + stats.todoTests++; + } else { + stats.passedTests++; + } } // Show the source of the test when showing assertions @@ -3271,7 +3767,7 @@ sourceName = document$$1.createElement("p"); sourceName.innerHTML = "Source: " + details.source; addClass(sourceName, "qunit-source"); - if (bad === 0) { + if (testPassed) { addClass(sourceName, "qunit-collapsed"); } addEvent(testTitle, "click", function () { @@ -3292,6 +3788,39 @@ } else { addEvent(window, "load", QUnit.load); } + + // Wrap window.onerror. We will call the original window.onerror to see if + // the existing handler fully handles the error; if not, we will call the + // QUnit.onError function. + var originalWindowOnError = window.onerror; + + // Cover uncaught exceptions + // Returning true will suppress the default browser handler, + // returning false will let it run. + window.onerror = function (message, fileName, lineNumber) { + var ret = false; + if (originalWindowOnError) { + for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { + args[_key - 3] = arguments[_key]; + } + + ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber].concat(args)); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if (ret !== true) { + var error = { + message: message, + fileName: fileName, + lineNumber: lineNumber + }; + + ret = QUnit.onError(error); + } + + return ret; + }; })(); /*