michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: // http://wiki.commonjs.org/wiki/Unit_Testing/1.0 michael@0: // When you see a javadoc comment that contains a number, it's a reference to a michael@0: // specific section of the CommonJS spec. michael@0: // michael@0: // Originally from narwhal.js (http://narwhaljs.org) michael@0: // Copyright (c) 2009 Thomas Robinson <280north.com> michael@0: // MIT license: http://opensource.org/licenses/MIT michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "Assert" michael@0: ]; michael@0: michael@0: /** michael@0: * 1. The assert module provides functions that throw AssertionError's when michael@0: * particular conditions are not met. michael@0: * michael@0: * To use the module you'll need to instantiate it first, which allows consumers michael@0: * to override certain behavior on the newly obtained instance. For examples, michael@0: * see the javadoc comments for the `report` member function. michael@0: */ michael@0: let Assert = this.Assert = function(reporterFunc) { michael@0: if (reporterFunc) michael@0: this.setReporter(reporterFunc); michael@0: }; michael@0: michael@0: function instanceOf(object, type) { michael@0: return Object.prototype.toString.call(object) == "[object " + type + "]"; michael@0: } michael@0: michael@0: function replacer(key, value) { michael@0: if (value === undefined) { michael@0: return "" + value; michael@0: } michael@0: if (typeof value === "number" && (isNaN(value) || !isFinite(value))) { michael@0: return value.toString(); michael@0: } michael@0: if (typeof value === "function" || instanceOf(value, "RegExp")) { michael@0: return value.toString(); michael@0: } michael@0: return value; michael@0: } michael@0: michael@0: const kTruncateLength = 128; michael@0: michael@0: function truncate(text, newLength = kTruncateLength) { michael@0: if (typeof text == "string") { michael@0: return text.length < newLength ? text : text.slice(0, newLength); michael@0: } else { michael@0: return text; michael@0: } michael@0: } michael@0: michael@0: function getMessage(error, prefix = "") { michael@0: let actual, expected; michael@0: // Wrap calls to JSON.stringify in try...catch blocks, as they may throw. If michael@0: // so, fall back to toString(). michael@0: try { michael@0: actual = JSON.stringify(error.actual, replacer); michael@0: } catch (ex) { michael@0: actual = Object.prototype.toString.call(error.actual); michael@0: } michael@0: try { michael@0: expected = JSON.stringify(error.expected, replacer); michael@0: } catch (ex) { michael@0: expected = Object.prototype.toString.call(error.expected); michael@0: } michael@0: let message = prefix; michael@0: if (error.operator) { michael@0: message += (prefix ? " - " : "") + truncate(actual) + " " + error.operator + michael@0: " " + truncate(expected); michael@0: } michael@0: return message; michael@0: } michael@0: michael@0: /** michael@0: * 2. The AssertionError is defined in assert. michael@0: * michael@0: * Example: michael@0: * new assert.AssertionError({ michael@0: * message: message, michael@0: * actual: actual, michael@0: * expected: expected, michael@0: * operator: operator michael@0: * }); michael@0: * michael@0: * At present only the four keys mentioned above are used and michael@0: * understood by the spec. Implementations or sub modules can pass michael@0: * other keys to the AssertionError's constructor - they will be michael@0: * ignored. michael@0: */ michael@0: Assert.AssertionError = function(options) { michael@0: this.name = "AssertionError"; michael@0: this.actual = options.actual; michael@0: this.expected = options.expected; michael@0: this.operator = options.operator; michael@0: this.message = getMessage(this, options.message); michael@0: // The part of the stack that comes from this module is not interesting. michael@0: let stack = Components.stack; michael@0: do { michael@0: stack = stack.caller; michael@0: } while(stack.filename && stack.filename.contains("Assert.jsm")) michael@0: this.stack = stack; michael@0: }; michael@0: michael@0: // assert.AssertionError instanceof Error michael@0: Assert.AssertionError.prototype = Object.create(Error.prototype, { michael@0: constructor: { michael@0: value: Assert.AssertionError, michael@0: enumerable: false, michael@0: writable: true, michael@0: configurable: true michael@0: } michael@0: }); michael@0: michael@0: let proto = Assert.prototype; michael@0: michael@0: proto._reporter = null; michael@0: /** michael@0: * Set a custom assertion report handler function. Arguments passed in to this michael@0: * function are: michael@0: * err (AssertionError|null) An error object when the assertion failed or null michael@0: * when it passed michael@0: * message (string) Message describing the assertion michael@0: * stack (stack) Stack trace of the assertion function michael@0: * michael@0: * Example: michael@0: * ```js michael@0: * Assert.setReporter(function customReporter(err, message, stack) { michael@0: * if (err) { michael@0: * do_report_result(false, err.message, err.stack); michael@0: * } else { michael@0: * do_report_result(true, message, stack); michael@0: * } michael@0: * }); michael@0: * ``` michael@0: * michael@0: * @param reporterFunc michael@0: * (function) Report handler function michael@0: */ michael@0: proto.setReporter = function(reporterFunc) { michael@0: this._reporter = reporterFunc; michael@0: }; michael@0: michael@0: /** michael@0: * 3. All of the following functions must throw an AssertionError when a michael@0: * corresponding condition is not met, with a message that may be undefined if michael@0: * not provided. All assertion methods provide both the actual and expected michael@0: * values to the assertion error for display purposes. michael@0: * michael@0: * This report method only throws errors on assertion failures, as per spec, michael@0: * but consumers of this module (think: xpcshell-test, mochitest) may want to michael@0: * override this default implementation. michael@0: * michael@0: * Example: michael@0: * ```js michael@0: * // The following will report an assertion failure. michael@0: * this.report(1 != 2, 1, 2, "testing JS number math!", "=="); michael@0: * ``` michael@0: * michael@0: * @param failed michael@0: * (boolean) Indicates if the assertion failed or not michael@0: * @param actual michael@0: * (mixed) The result of evaluating the assertion michael@0: * @param expected (optional) michael@0: * (mixed) Expected result from the test author michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: * @param operator (optional) michael@0: * (string) Operation qualifier used by the assertion method (ex: '==') michael@0: */ michael@0: proto.report = function(failed, actual, expected, message, operator) { michael@0: let err = new Assert.AssertionError({ michael@0: message: message, michael@0: actual: actual, michael@0: expected: expected, michael@0: operator: operator michael@0: }); michael@0: if (!this._reporter) { michael@0: // If no custom reporter is set, throw the error. michael@0: if (failed) { michael@0: throw err; michael@0: } michael@0: } else { michael@0: this._reporter(failed ? err : null, message, err.stack); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * 4. Pure assertion tests whether a value is truthy, as determined by !!guard. michael@0: * assert.ok(guard, message_opt); michael@0: * This statement is equivalent to assert.equal(true, !!guard, message_opt);. michael@0: * To test strictly for the value true, use assert.strictEqual(true, guard, michael@0: * message_opt);. michael@0: * michael@0: * @param value michael@0: * (mixed) Test subject to be evaluated as truthy michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.ok = function(value, message) { michael@0: this.report(!value, value, true, message, "=="); michael@0: }; michael@0: michael@0: /** michael@0: * 5. The equality assertion tests shallow, coercive equality with ==. michael@0: * assert.equal(actual, expected, message_opt); michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as equivalent to `expected` michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.equal = function equal(actual, expected, message) { michael@0: this.report(actual != expected, actual, expected, message, "=="); michael@0: }; michael@0: michael@0: /** michael@0: * 6. The non-equality assertion tests for whether two objects are not equal michael@0: * with != assert.notEqual(actual, expected, message_opt); michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as NOT equivalent to `expected` michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.notEqual = function notEqual(actual, expected, message) { michael@0: this.report(actual == expected, actual, expected, message, "!="); michael@0: }; michael@0: michael@0: /** michael@0: * 7. The equivalence assertion tests a deep equality relation. michael@0: * assert.deepEqual(actual, expected, message_opt); michael@0: * michael@0: * We check using the most exact approximation of equality between two objects michael@0: * to keep the chance of false positives to a minimum. michael@0: * `JSON.stringify` is not designed to be used for this purpose; objects may michael@0: * have ambiguous `toJSON()` implementations that would influence the test. michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as equivalent to `expected`, including nested properties michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.deepEqual = function deepEqual(actual, expected, message) { michael@0: this.report(!_deepEqual(actual, expected), actual, expected, message, "deepEqual"); michael@0: }; michael@0: michael@0: function _deepEqual(actual, expected) { michael@0: // 7.1. All identical values are equivalent, as determined by ===. michael@0: if (actual === expected) { michael@0: return true; michael@0: // 7.2. If the expected value is a Date object, the actual value is michael@0: // equivalent if it is also a Date object that refers to the same time. michael@0: } else if (instanceOf(actual, "Date") && instanceOf(expected, "Date")) { michael@0: return actual.getTime() === expected.getTime(); michael@0: // 7.3 If the expected value is a RegExp object, the actual value is michael@0: // equivalent if it is also a RegExp object with the same source and michael@0: // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). michael@0: } else if (instanceOf(actual, "RegExp") && instanceOf(expected, "RegExp")) { michael@0: return actual.source === expected.source && michael@0: actual.global === expected.global && michael@0: actual.multiline === expected.multiline && michael@0: actual.lastIndex === expected.lastIndex && michael@0: actual.ignoreCase === expected.ignoreCase; michael@0: // 7.4. Other pairs that do not both pass typeof value == "object", michael@0: // equivalence is determined by ==. michael@0: } else if (typeof actual != "object" && typeof expected != "object") { michael@0: return actual == expected; michael@0: // 7.5 For all other Object pairs, including Array objects, equivalence is michael@0: // determined by having the same number of owned properties (as verified michael@0: // with Object.prototype.hasOwnProperty.call), the same set of keys michael@0: // (although not necessarily the same order), equivalent values for every michael@0: // corresponding key, and an identical 'prototype' property. Note: this michael@0: // accounts for both named and indexed properties on Arrays. michael@0: } else { michael@0: return objEquiv(actual, expected); michael@0: } michael@0: } michael@0: michael@0: function isUndefinedOrNull(value) { michael@0: return value === null || value === undefined; michael@0: } michael@0: michael@0: function isArguments(object) { michael@0: return instanceOf(object, "Arguments"); michael@0: } michael@0: michael@0: function objEquiv(a, b) { michael@0: if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { michael@0: return false; michael@0: } michael@0: // An identical 'prototype' property. michael@0: if (a.prototype !== b.prototype) { michael@0: return false; michael@0: } michael@0: // Object.keys may be broken through screwy arguments passing. Converting to michael@0: // an array solves the problem. michael@0: if (isArguments(a)) { michael@0: if (!isArguments(b)) { michael@0: return false; michael@0: } michael@0: a = pSlice.call(a); michael@0: b = pSlice.call(b); michael@0: return _deepEqual(a, b); michael@0: } michael@0: let ka, kb, key, i; michael@0: try { michael@0: ka = Object.keys(a); michael@0: kb = Object.keys(b); michael@0: } catch (e) { michael@0: // Happens when one is a string literal and the other isn't michael@0: return false; michael@0: } michael@0: // Having the same number of owned properties (keys incorporates michael@0: // hasOwnProperty) michael@0: if (ka.length != kb.length) michael@0: return false; michael@0: // The same set of keys (although not necessarily the same order), michael@0: ka.sort(); michael@0: kb.sort(); michael@0: // Equivalent values for every corresponding key, and possibly expensive deep michael@0: // test michael@0: for (i = ka.length - 1; i >= 0; i--) { michael@0: key = ka[i]; michael@0: if (!_deepEqual(a[key], b[key])) { michael@0: return false; michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: /** michael@0: * 8. The non-equivalence assertion tests for any deep inequality. michael@0: * assert.notDeepEqual(actual, expected, message_opt); michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as NOT equivalent to `expected`, including nested properties michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.notDeepEqual = function notDeepEqual(actual, expected, message) { michael@0: this.report(_deepEqual(actual, expected), actual, expected, message, "notDeepEqual"); michael@0: }; michael@0: michael@0: /** michael@0: * 9. The strict equality assertion tests strict equality, as determined by ===. michael@0: * assert.strictEqual(actual, expected, message_opt); michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as strictly equivalent to `expected` michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.strictEqual = function strictEqual(actual, expected, message) { michael@0: this.report(actual !== expected, actual, expected, message, "==="); michael@0: }; michael@0: michael@0: /** michael@0: * 10. The strict non-equality assertion tests for strict inequality, as michael@0: * determined by !==. assert.notStrictEqual(actual, expected, message_opt); michael@0: * michael@0: * @param actual michael@0: * (mixed) Test subject to be evaluated as NOT strictly equivalent to `expected` michael@0: * @param expected michael@0: * (mixed) Test reference to evaluate against `actual` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.notStrictEqual = function notStrictEqual(actual, expected, message) { michael@0: this.report(actual === expected, actual, expected, message, "!=="); michael@0: }; michael@0: michael@0: function expectedException(actual, expected) { michael@0: if (!actual || !expected) { michael@0: return false; michael@0: } michael@0: michael@0: if (instanceOf(expected, "RegExp")) { michael@0: return expected.test(actual); michael@0: } else if (actual instanceof expected) { michael@0: return true; michael@0: } else if (expected.call({}, actual) === true) { michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: /** michael@0: * 11. Expected to throw an error: michael@0: * assert.throws(block, Error_opt, message_opt); michael@0: * michael@0: * @param block michael@0: * (function) Function block to evaluate and catch eventual thrown errors michael@0: * @param expected (optional) michael@0: * (mixed) Test reference to evaluate against the thrown result from `block` michael@0: * @param message (optional) michael@0: * (string) Short explanation of the expected result michael@0: */ michael@0: proto.throws = function(block, expected, message) { michael@0: let actual; michael@0: michael@0: if (typeof expected === "string") { michael@0: message = expected; michael@0: expected = null; michael@0: } michael@0: michael@0: try { michael@0: block(); michael@0: } catch (e) { michael@0: actual = e; michael@0: } michael@0: michael@0: message = (expected && expected.name ? " (" + expected.name + ")." : ".") + michael@0: (message ? " " + message : "."); michael@0: michael@0: if (!actual) { michael@0: this.report(true, actual, expected, "Missing expected exception" + message); michael@0: } michael@0: michael@0: if ((actual && expected && !expectedException(actual, expected))) { michael@0: throw actual; michael@0: } michael@0: michael@0: this.report(false, expected, expected, message); michael@0: };