/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Originally from narwhal.js (http://narwhaljs.org) // Copyright (c) 2009 Thomas Robinson <280north.com> // MIT license: http://opensource.org/licenses/MIT import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs"; /** * This module is based on the * `CommonJS spec <https://wiki.commonjs.org/wiki/Unit_Testing/1.0>`_ * * When you see a jsdoc comment that contains a number, it's a reference to a * specific section of the CommonJS spec. * * 1. The assert module provides functions that throw AssertionError's when * particular conditions are not met. * * To use the module you may instantiate it first. * * @param {reporterFunc} reporterFunc * Allows consumers to override reporting for this instance. * @param {boolean} isDefault * Used by test suites to set ``reporterFunc`` as the default * used by the global instance, which is called for example * by other test-only modules. This is false when the * reporter is set by content scripts, because they may still * run in the parent process. * * @class */ export function Assert(reporterFunc, isDefault) { if (reporterFunc) { this.setReporter(reporterFunc); } if (isDefault) { Assert.setReporter(reporterFunc); } } // This allows using the Assert object as an additional global instance. Object.setPrototypeOf(Assert, Assert.prototype); function instanceOf(object, type) { return Object.prototype.toString.call(object) == "[object " + type + "]"; } function replacer(key, value) { if (value === undefined) { return "" + value; } if (typeof value === "number" && (isNaN(value) || !isFinite(value))) { return value.toString(); } if (typeof value === "function" || instanceOf(value, "RegExp")) { return value.toString(); } if ( typeof value === "object" && value !== null && "QueryInterface" in value ) { return value.toString(); } return value; } const kTruncateLength = 128; function truncate(text, newLength = kTruncateLength) { if (typeof text == "string") { return text.length < newLength ? text : text.slice(0, newLength); } return text; } function getMessage(error, prefix = "") { let actual, expected; // Wrap calls to JSON.stringify in try...catch blocks, as they may throw. If // so, fall back to toString(). try { actual = JSON.stringify(error.actual, replacer); } catch (ex) { actual = Object.prototype.toString.call(error.actual); } try { expected = JSON.stringify(error.expected, replacer); } catch (ex) { expected = Object.prototype.toString.call(error.expected); } let message = prefix; if (error.operator) { let truncateLength = error.truncate ? kTruncateLength : Infinity; message += (prefix ? " - " : "") + truncate(actual, truncateLength) + " " + error.operator + " " + truncate(expected, truncateLength); } return message; } /** * 2. The AssertionError is defined in assert. * * At present only the four keys mentioned below are used and * understood by the spec. Implementations or sub modules can pass * other keys to the AssertionError's constructor - they will be * ignored. * * @example * * new assert.AssertionError({ * message: message, * actual: actual, * expected: expected, * operator: operator, * truncate: truncate, * stack: stack, // Optional, defaults to the current stack. * }); * */ Assert.AssertionError = function (options) { this.name = "AssertionError"; this.actual = options.actual; this.expected = options.expected; this.operator = options.operator; this.message = getMessage(this, options.message, options.truncate); // The part of the stack that comes from this module is not interesting. let stack = options.stack || Components.stack; do { stack = stack.asyncCaller || stack.caller; } while ( stack && stack.filename && stack.filename.includes("Assert.sys.mjs") ); this.stack = stack; }; // assert.AssertionError instanceof Error Assert.AssertionError.prototype = Object.create(Error.prototype, { constructor: { value: Assert.AssertionError, enumerable: false, writable: true, configurable: true, }, }); Assert.prototype._reporter = null; /** * This callback type is used for custom assertion report handling. * * @callback reporterFunc * @param {AssertionError|null} err * An error object when the assertion failed, or null when it passed. * @param {String} message * Message describing the assertion. * @param {Stack} stack * Stack trace of the assertion function. */ /** * Set a custom assertion report handler function. * * @example * * Assert.setReporter(function customReporter(err, message, stack) { * if (err) { * do_report_result(false, err.message, err.stack); * } else { * do_report_result(true, message, stack); * } * }); * * @param {reporterFunc} reporterFunc * Report handler function. */ Assert.prototype.setReporter = function (reporterFunc) { this._reporter = reporterFunc; }; /** * 3. All of the following functions must throw an AssertionError when a * corresponding condition is not met, with a message that may be undefined if * not provided. All assertion methods provide both the actual and expected * values to the assertion error for display purposes. * * This report method only throws errors on assertion failures, as per spec, * but consumers of this module (think: xpcshell-test, mochitest) may want to * override this default implementation. * * @example * * // The following will report an assertion failure. * this.report(1 != 2, 1, 2, "testing JS number math!", "=="); * * @param {boolean} failed * Indicates if the assertion failed or not. * @param {*} actual * The result of evaluating the assertion. * @param {*} [expected] * Expected result from the test author. * @param {String} [message] * Short explanation of the expected result. * @param {String} [operator] * Operation qualifier used by the assertion method (ex: '=='). * @param {boolean} [truncate=true] * Whether or not ``actual`` and ``expected`` should be truncated when printing. * @param {nsIStackFrame} [stack] * The stack trace including the caller of the assertion method, * if this cannot be inferred automatically (e.g. due to async callbacks). */ Assert.prototype.report = function ( failed, actual, expected, message, operator, truncate = true, stack = null // Defaults to Components.stack in AssertionError. ) { // Although not ideal, we allow a "null" message due to the way some of the extension tests // work. if (message !== undefined && message !== null && typeof message != "string") { this.ok( false, `Expected a string or undefined for the error message to Assert.*, got ${typeof message}` ); } let err = new Assert.AssertionError({ message, actual, expected, operator, truncate, stack, }); if (!this._reporter) { // If no custom reporter is set, throw the error. if (failed) { throw err; } } else { this._reporter(failed ? err : null, err.message, err.stack); } }; /** * 4. Pure assertion tests whether a value is truthy, as determined by !!guard. * ``assert.ok(guard, message_opt);`` * This statement is equivalent to ``assert.equal(true, !!guard, message_opt);``. * To test strictly for the value true, use ``assert.strictEqual(true, guard, * message_opt);``. * * @param {*} value * Test subject to be evaluated as truthy. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.ok = function (value, message) { if (arguments.length > 2) { this.report( true, false, true, "Too many arguments passed to `Assert.ok()`", "==" ); } else { this.report(!value, value, true, message, "=="); } }; /** * 5. The equality assertion tests shallow, coercive equality with ==. * ``assert.equal(actual, expected, message_opt);`` * * @param {*} actual * Test subject to be evaluated as equivalent to ``expected``. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.equal = function equal(actual, expected, message) { this.report(actual != expected, actual, expected, message, "=="); }; /** * 6. The non-equality assertion tests for whether two objects are not equal * with ``!=`` * * @example * assert.notEqual(actual, expected, message_opt); * * @param {*} actual * Test subject to be evaluated as NOT equivalent to ``expected``. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.notEqual = function notEqual(actual, expected, message) { this.report(actual == expected, actual, expected, message, "!="); }; /** * 7. The equivalence assertion tests a deep equality relation. * assert.deepEqual(actual, expected, message_opt); * * We check using the most exact approximation of equality between two objects * to keep the chance of false positives to a minimum. * `JSON.stringify` is not designed to be used for this purpose; objects may * have ambiguous `toJSON()` implementations that would influence the test. * * @param {*} actual * Test subject to be evaluated as equivalent to ``expected``, including nested properties. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.deepEqual = function deepEqual(actual, expected, message) { this.report( !ObjectUtils.deepEqual(actual, expected), actual, expected, message, "deepEqual", false ); }; /** * 8. The non-equivalence assertion tests for any deep inequality. * assert.notDeepEqual(actual, expected, message_opt); * * @param {*} actual * Test subject to be evaluated as NOT equivalent to ``expected``, including nested * properties. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.notDeepEqual = function notDeepEqual( actual, expected, message ) { this.report( ObjectUtils.deepEqual(actual, expected), actual, expected, message, "notDeepEqual", false ); }; /** * 9. The strict equality assertion tests strict equality, as determined by ===. * ``assert.strictEqual(actual, expected, message_opt);`` * * @param {*} actual * Test subject to be evaluated as strictly equivalent to ``expected``. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.strictEqual = function strictEqual(actual, expected, message) { this.report(actual !== expected, actual, expected, message, "==="); }; /** * 10. The strict non-equality assertion tests for strict inequality, as * determined by !==. ``assert.notStrictEqual(actual, expected, message_opt);`` * * @param {*} actual * Test subject to be evaluated as NOT strictly equivalent to ``expected``. * @param {*} expected * Test reference to evaluate against ``actual``. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.notStrictEqual = function notStrictEqual( actual, expected, message ) { this.report(actual === expected, actual, expected, message, "!=="); }; function checkExpectedArgument(instance, funcName, expected) { if (!expected) { instance.ok( false, `Error: The 'expected' argument was not supplied to Assert.${funcName}()` ); } if ( !instanceOf(expected, "RegExp") && typeof expected !== "function" && typeof expected !== "object" ) { instance.ok( false, `Error: The 'expected' argument to Assert.${funcName}() must be a RegExp, function or an object` ); } } function expectedException(actual, expected) { if (!actual || !expected) { return false; } if (instanceOf(expected, "RegExp")) { return expected.test(actual); // We need to guard against the right hand parameter of "instanceof" lacking // the "prototype" property, which is true of arrow functions in particular. } else if ( !(typeof expected === "function" && !expected.prototype) && actual instanceof expected ) { return true; } else if (expected.call({}, actual) === true) { return true; } return false; } /** * 11. Expected to throw an error: * assert.throws(block, Error_opt, message_opt); * * Example: * ```js * // The following will verify that an error of type TypeError was thrown: * Assert.throws(() => testBody(), TypeError); * // The following will verify that an error was thrown with an error message matching "hello": * Assert.throws(() => testBody(), /hello/); * ``` * * @param {Function} block * Function to evaluate and catch eventual thrown errors. * @param {RegExp|Function} expected * This parameter can be either a RegExp or a function. The function is * either the error type's constructor, or it's a method that returns * a boolean that describes the test outcome. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.throws = function (block, expected, message) { checkExpectedArgument(this, "throws", expected); // `true` if we realize that we have added an // error to `ChromeUtils.recentJSDevError` and // that we probably need to clean it up. let cleanupRecentJSDevError = false; if ("recentJSDevError" in ChromeUtils) { // Check that we're in a build of Firefox that supports // the `recentJSDevError` mechanism (i.e. Nightly build). if (ChromeUtils.recentJSDevError === undefined) { // There was no previous error, so if we throw // an error here, we may need to clean it up. cleanupRecentJSDevError = true; } } let actual; try { block(); } catch (e) { actual = e; } message = (expected.name ? " (" + expected.name + ")." : ".") + (message ? " " + message : "."); if (!actual) { this.report(true, actual, expected, "Missing expected exception" + message); } if (actual && !expectedException(actual, expected)) { throw actual; } this.report(false, expected, expected, message); // Make sure that we don't cause failures for JS Dev Errors that // were expected, typically for tests that attempt to check // that we react properly to TypeError, ReferenceError, SyntaxError. if (cleanupRecentJSDevError) { let recentJSDevError = ChromeUtils.recentJSDevError; if (recentJSDevError) { if (expectedException(recentJSDevError)) { ChromeUtils.clearRecentJSDevError(); } } } }; /** * A promise that is expected to reject: * assert.rejects(promise, expected, message); * * @param {Promise} promise * A promise that is expected to reject. * @param {?} [expected] * Test reference to evaluate against the rejection result. * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.rejects = function (promise, expected, message) { checkExpectedArgument(this, "rejects", expected); const operator = undefined; // Should we use "rejects" here? const stack = Components.stack; return new Promise((resolve, reject) => { return promise .then( () => { this.report( true, null, expected, "Missing expected exception " + message, operator, true, stack ); // this.report() above should raise an AssertionError. If _reporter // has been overridden and doesn't throw an error, just resolve. // Otherwise we'll have a never-resolving promise that got stuck. resolve(); }, err => { if (!expectedException(err, expected)) { // TODO bug 1480075: Should report error instead of rejecting. reject(err); return; } this.report(false, err, expected, message, operator, truncate, stack); resolve(); } ) .catch(reject); }); }; function compareNumbers(expression, lhs, rhs, message, operator) { let lhsIsNumber = typeof lhs == "number" && !Number.isNaN(lhs); let rhsIsNumber = typeof rhs == "number" && !Number.isNaN(rhs); if (lhsIsNumber && rhsIsNumber) { this.report(expression, lhs, rhs, message, operator); return; } let lhsIsDate = typeof lhs == "object" && lhs.constructor.name == "Date" && !isNaN(lhs); let rhsIsDate = typeof rhs == "object" && rhs.constructor.name == "Date" && !isNaN(rhs); if (lhsIsDate && rhsIsDate) { this.report(expression, lhs, rhs, message, operator); return; } let errorMessage; if (!lhsIsNumber && !rhsIsNumber && !lhsIsDate && !rhsIsDate) { errorMessage = `Neither '${lhs}' nor '${rhs}' are numbers or dates.`; } else if ((lhsIsNumber && rhsIsDate) || (lhsIsDate && rhsIsNumber)) { errorMessage = `'${lhsIsNumber ? lhs : rhs}' is a number and '${ rhsIsDate ? rhs : lhs }' is a date.`; } else { errorMessage = `'${ lhsIsNumber || lhsIsDate ? rhs : lhs }' is not a number or date.`; } this.report(true, lhs, rhs, errorMessage); } /** * The lhs must be greater than the rhs. * assert.greater(lhs, rhs, message_opt); * * @param {Number} lhs * The left-hand side value. * @param {Number} rhs * The right-hand side value. * @param {String} [message] * Short explanation of the comparison result. */ Assert.prototype.greater = function greater(lhs, rhs, message) { compareNumbers.call(this, lhs <= rhs, lhs, rhs, message, ">"); }; /** * The lhs must be greater than or equal to the rhs. * assert.greaterOrEqual(lhs, rhs, message_opt); * * @param {Number} [lhs] * The left-hand side value. * @param {Number} [rhs] * The right-hand side value. * @param {String} [message] * Short explanation of the comparison result. */ Assert.prototype.greaterOrEqual = function greaterOrEqual(lhs, rhs, message) { compareNumbers.call(this, lhs < rhs, lhs, rhs, message, ">="); }; /** * The lhs must be less than the rhs. * assert.less(lhs, rhs, message_opt); * * @param {Number} [lhs] * The left-hand side value. * @param {Number} [rhs] * The right-hand side value. * @param {String} [message] * Short explanation of the comparison result. */ Assert.prototype.less = function less(lhs, rhs, message) { compareNumbers.call(this, lhs >= rhs, lhs, rhs, message, "<"); }; /** * The lhs must be less than or equal to the rhs. * assert.lessOrEqual(lhs, rhs, message_opt); * * @param {Number} [lhs] * The left-hand side value. * @param {Number} [rhs] * The right-hand side value. * @param {String} [message] * Short explanation of the comparison result. */ Assert.prototype.lessOrEqual = function lessOrEqual(lhs, rhs, message) { compareNumbers.call(this, lhs > rhs, lhs, rhs, message, "<="); }; /** * The lhs must be a string that matches the rhs regular expression. * rhs can be specified either as a string or a RegExp object. If specified as a * string it will be interpreted as a regular expression so take care to escape * special characters such as "?" or "(" if you need the actual characters. * * @param {String} lhs * The string to be tested. * @param {String|RegExp} rhs * The regular expression that the string will be tested with. * Note that if passed as a string, this will be interpreted. * as a regular expression. * @param {String} [message] * Short explanation of the comparison result. */ Assert.prototype.stringMatches = function stringMatches(lhs, rhs, message) { if (typeof rhs != "string" && !instanceOf(rhs, "RegExp")) { this.report( true, lhs, String(rhs), `Expected a string or a RegExp for rhs, but "${rhs}" isn't a string or a RegExp object.` ); return; } if (typeof lhs != "string") { this.report( true, lhs, String(rhs), `Expected a string for lhs, but "${lhs}" isn't a string.` ); return; } if (typeof rhs == "string") { try { rhs = new RegExp(rhs); } catch { this.report( true, lhs, rhs, `Expected a valid regular expression for rhs, but "${rhs}" isn't one.` ); return; } } const isCorrect = rhs.test(lhs); this.report(!isCorrect, lhs, rhs.toString(), message, "matches"); }; /** * The lhs must be a string that contains the rhs string. * * @param {String} lhs * The string to be tested (haystack). * @param {String} rhs * The string to be found (needle). * @param {String} [message] * Short explanation of the expected result. */ Assert.prototype.stringContains = function stringContains(lhs, rhs, message) { if (typeof lhs != "string" || typeof rhs != "string") { this.report( true, lhs, rhs, `Expected a string for both lhs and rhs, but either "${lhs}" or "${rhs}" is not a string.` ); } const isCorrect = lhs.includes(rhs); this.report(!isCorrect, lhs, rhs, message, "includes"); };