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: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "unstable" michael@0: }; michael@0: michael@0: const { isFunction, isNull, isObject, isString, michael@0: isRegExp, isArray, isDate, isPrimitive, michael@0: isUndefined, instanceOf, source } = require("../lang/type"); michael@0: michael@0: /** michael@0: * The `AssertionError` is defined in assert. michael@0: * @extends Error michael@0: * @example michael@0: * new assert.AssertionError({ michael@0: * message: message, michael@0: * actual: actual, michael@0: * expected: expected michael@0: * }) michael@0: */ michael@0: function AssertionError(options) { michael@0: let assertionError = Object.create(AssertionError.prototype); michael@0: michael@0: if (isString(options)) michael@0: options = { message: options }; michael@0: if ("actual" in options) michael@0: assertionError.actual = options.actual; michael@0: if ("expected" in options) michael@0: assertionError.expected = options.expected; michael@0: if ("operator" in options) michael@0: assertionError.operator = options.operator; michael@0: michael@0: assertionError.message = options.message; michael@0: assertionError.stack = new Error().stack; michael@0: return assertionError; michael@0: } michael@0: AssertionError.prototype = Object.create(Error.prototype, { michael@0: constructor: { value: AssertionError }, michael@0: name: { value: "AssertionError", enumerable: true }, michael@0: toString: { value: function toString() { michael@0: let value; michael@0: if (this.message) { michael@0: value = this.name + " : " + this.message; michael@0: } michael@0: else { michael@0: value = [ michael@0: this.name + " : ", michael@0: source(this.expected), michael@0: this.operator, michael@0: source(this.actual) michael@0: ].join(" "); michael@0: } michael@0: return value; michael@0: }} michael@0: }); michael@0: exports.AssertionError = AssertionError; michael@0: michael@0: function Assert(logger) { michael@0: let assert = Object.create(Assert.prototype, { _log: { value: logger }}); michael@0: michael@0: assert.fail = assert.fail.bind(assert); michael@0: assert.pass = assert.pass.bind(assert); michael@0: michael@0: return assert; michael@0: } michael@0: michael@0: Assert.prototype = { michael@0: fail: function fail(e) { michael@0: if (!e || typeof(e) !== 'object') { michael@0: this._log.fail(e); michael@0: return; michael@0: } michael@0: let message = e.message; michael@0: try { michael@0: if ('operator' in e) { michael@0: message += [ michael@0: " -", michael@0: source(e.expected), michael@0: e.operator, michael@0: source(e.actual) michael@0: ].join(" "); michael@0: } michael@0: } michael@0: catch(e) {} michael@0: this._log.fail(message); michael@0: }, michael@0: pass: function pass(message) { michael@0: this._log.pass(message); michael@0: }, michael@0: error: function error(e) { michael@0: this._log.exception(e); michael@0: }, michael@0: ok: function ok(value, message) { michael@0: if (!!!value) { michael@0: this.fail({ michael@0: actual: value, michael@0: expected: true, michael@0: message: message, michael@0: operator: "==" michael@0: }); michael@0: } michael@0: else { michael@0: this.pass(message); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The equality assertion tests shallow, coercive equality with `==`. michael@0: * @example michael@0: * assert.equal(1, 1, "one is one"); michael@0: */ michael@0: equal: function equal(actual, expected, message) { michael@0: if (actual == expected) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "==" michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The non-equality assertion tests for whether two objects are not equal michael@0: * with `!=`. michael@0: * @example michael@0: * assert.notEqual(1, 2, "one is not two"); michael@0: */ michael@0: notEqual: function notEqual(actual, expected, message) { michael@0: if (actual != expected) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "!=", michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The equivalence assertion tests a deep (with `===`) equality relation. michael@0: * @example michael@0: * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects") michael@0: */ michael@0: deepEqual: function deepEqual(actual, expected, message) { michael@0: if (isDeepEqual(actual, expected)) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "deepEqual" michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The non-equivalence assertion tests for any deep (with `===`) inequality. michael@0: * @example michael@0: * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }), michael@0: * "object's inherit from different prototypes"); michael@0: */ michael@0: notDeepEqual: function notDeepEqual(actual, expected, message) { michael@0: if (!isDeepEqual(actual, expected)) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "notDeepEqual" michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The strict equality assertion tests strict equality, as determined by michael@0: * `===`. michael@0: * @example michael@0: * assert.strictEqual(null, null, "`null` is `null`") michael@0: */ michael@0: strictEqual: function strictEqual(actual, expected, message) { michael@0: if (actual === expected) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "===" michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The strict non-equality assertion tests for strict inequality, as michael@0: * determined by `!==`. michael@0: * @example michael@0: * assert.notStrictEqual(null, undefined, "`null` is not `undefined`"); michael@0: */ michael@0: notStrictEqual: function notStrictEqual(actual, expected, message) { michael@0: if (actual !== expected) { michael@0: this.pass(message); michael@0: } michael@0: else { michael@0: this.fail({ michael@0: actual: actual, michael@0: expected: expected, michael@0: message: message, michael@0: operator: "!==" michael@0: }) michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The assertion whether or not given `block` throws an exception. If optional michael@0: * `Error` argument is provided and it's type of function thrown error is michael@0: * asserted to be an instance of it, if type of `Error` is string then message michael@0: * of throw exception is asserted to contain it. michael@0: * @param {Function} block michael@0: * Function that is expected to throw. michael@0: * @param {Error|RegExp} [Error] michael@0: * Error constructor that is expected to be thrown or a string that michael@0: * must be contained by a message of the thrown exception, or a RegExp michael@0: * matching a message of the thrown exception. michael@0: * @param {String} message michael@0: * Description message michael@0: * michael@0: * @examples michael@0: * michael@0: * assert.throws(function block() { michael@0: * doSomething(4) michael@0: * }, "Object is expected", "Incorrect argument is passed"); michael@0: * michael@0: * assert.throws(function block() { michael@0: * Object.create(5) michael@0: * }, TypeError, "TypeError is thrown"); michael@0: */ michael@0: throws: function throws(block, Error, message) { michael@0: let threw = false; michael@0: let exception = null; michael@0: michael@0: // If third argument is not provided and second argument is a string it michael@0: // means that optional `Error` argument was not passed, so we shift michael@0: // arguments. michael@0: if (isString(Error) && isUndefined(message)) { michael@0: message = Error; michael@0: Error = undefined; michael@0: } michael@0: michael@0: // Executing given `block`. michael@0: try { michael@0: block(); michael@0: } michael@0: catch (e) { michael@0: threw = true; michael@0: exception = e; michael@0: } michael@0: michael@0: // If exception was thrown and `Error` argument was not passed assert is michael@0: // passed. michael@0: if (threw && (isUndefined(Error) || michael@0: // If passed `Error` is RegExp using it's test method to michael@0: // assert thrown exception message. michael@0: (isRegExp(Error) && Error.test(exception.message)) || michael@0: // If passed `Error` is a constructor function testing if michael@0: // thrown exception is an instance of it. michael@0: (isFunction(Error) && instanceOf(exception, Error)))) michael@0: { michael@0: this.pass(message); michael@0: } michael@0: michael@0: // Otherwise we report assertion failure. michael@0: else { michael@0: let failure = { michael@0: message: message, michael@0: operator: "throws" michael@0: }; michael@0: michael@0: if (exception) michael@0: failure.actual = exception; michael@0: michael@0: if (Error) michael@0: failure.expected = Error; michael@0: michael@0: this.fail(failure); michael@0: } michael@0: } michael@0: }; michael@0: exports.Assert = Assert; michael@0: michael@0: function isDeepEqual(actual, expected) { michael@0: michael@0: // 7.1. All identical values are equivalent, as determined by ===. michael@0: if (actual === expected) { michael@0: return true; michael@0: } michael@0: 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 (isDate(actual) && isDate(expected)) { michael@0: return actual.getTime() === expected.getTime(); michael@0: } michael@0: michael@0: // XXX specification bug: this should be specified michael@0: else if (isPrimitive(actual) || isPrimitive(expected)) { michael@0: return expected === actual; michael@0: } michael@0: michael@0: // 7.3. Other pairs that do not both pass typeof value == "object", michael@0: // equivalence is determined by ==. michael@0: else if (!isObject(actual) && !isObject(expected)) { michael@0: return actual == expected; michael@0: } michael@0: michael@0: // 7.4. 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 actual.prototype === expected.prototype && michael@0: isEquivalent(actual, expected); michael@0: } michael@0: } michael@0: michael@0: function isEquivalent(a, b, stack) { michael@0: let aKeys = Object.keys(a); michael@0: let bKeys = Object.keys(b); michael@0: michael@0: return aKeys.length === bKeys.length && michael@0: isArrayEquivalent(aKeys.sort(), bKeys.sort()) && michael@0: aKeys.every(function(key) { michael@0: return isDeepEqual(a[key], b[key], stack) michael@0: }); michael@0: } michael@0: michael@0: function isArrayEquivalent(a, b, stack) { michael@0: return isArray(a) && isArray(b) && michael@0: a.every(function(value, index) { michael@0: return isDeepEqual(value, b[index]); michael@0: }); michael@0: }