diff --git a/lib/assert.js b/lib/assert.js index 75b04d359..8d885c110 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -358,3 +358,164 @@ assert.doesNotThrow = function(block, /*optional*/error, /*optional*/message) { }; assert.ifError = function (err) { if (err) {throw err;}}; + + +/** + * Return true if every element in the expected array also exists in the the actual + * array. The actual array may contain more elements that are not in the expected + * array. This implementation is very simple and not very efficient (Order(n^2)) so + * do not call this to compare large arrays. + * + * @param {Array.} actual The actual array to test + * @param {Array.} expected The array to test against + * @returns True if every element of the expected array exists in the actual array. + */ +function isEqualIgnoringOrder(actual, expected) { + var found = false; + for (var i = 0; i < expected.length; i++) { + var found = false; + for (var j = 0; j < actual.length; j++) { + try { + if (_deepEqual(actual[j], expected[i])) { + found = true; + break; + } + } catch (ignored) { + } + } + if (!found) { + return false; + } + } + return true; +} + +function isArray(object) { + return typeof(object) === 'object' && Object.prototype.toString.call(object) === '[object Array]'; +}; + +/** + * Performs a deep comparison of arrays, ignoring the order of the + * elements of the array. Essentially, this ensure that they have the same + * contents, possibly in a different order. Each of the elements of the + * array is compared with a deep equals. If this is called with objects + * that are not arrays, the assertion fails. + * + * @example // passing assertions + * assert.equalIgnoringOrder([5, 2, 8, 3], [2, 4, 5, 9]); + * assert.equalIgnoringOrder(["apples", "bananas", "oranges"], ["oranges", "apples", "bananas"]); + * + * @param {Object} actual The actual value + * @param {Object} expected The expected value + * @throws ArgumentsError + * @throws AssertionError + */ +assert.equalIgnoringOrder = function(actual, expected, message) { + if (!isArray(expected)) { + fail("Invalid expected argument to equalIgnoringOrder."); + } else if (isArray(actual)) { + if (isEqualIgnoringOrder(actual, expected) === false) { + fail(actual, expected, message, "equalIgnoringOrder", assert.equalIgnoringOrder); + } + } else { + fail(actual, expected, message, "equalIgnoringOrder", assert.equalIgnoringOrder); + } + return; +}; + +/** + * Performs a numeric equivalence within the given tolerance. If the + * difference between the arguments is within that tolerance, the assertion + * passes. Otherwise, it fails. + * + * @example // passing assertion + * assert.roughlyEqual(5.23456789, 5.2345947, 0.001); + * + * @param {Number} actual The actual value + * @param {Number} expected The expected value + * @param {Number} tolerance The tolerance for the difference between the two + * @throws ArgumentsError + * @throws AssertionError + */ +assert.roughlyEqual = function(actual, expected, tolerance, message) { + if (typeof(actual) !== "number" || typeof(expected) !== "number" || typeof(tolerance) !== "number") { + fail("Invalid expected argument to roughlyEquals."); + } else if (Math.abs(expected - actual) >= tolerance) { + fail(actual, expected, message, "roughlyEquals", assert.roughlyEquals); + } + return; +}; + +/** + * Check the actual result to see that every property that exists in the expected + * object also exists in the actual object and that it has the same value. If the + * expected object is a string, it names the property that should exist without + * giving the value. If the expected object is an array, each item in the expected + * array should exist in the actual array. If expected is an object, then the actual + * object must have all properties that the expected object has and with the same + * value. The actual object may have more properties that do not exist in + * expected, but this function + * is used to test for only the properties that the unit test cares about. + * + * @example // passing assertion + * assert.contains({a: 2, b: 3}, "b"); + * + * @param {Object} actual The actual value to test which may be an array or an object + * @param {*} expected The name of the property that is expected to be within the object in the actual parameter + * @param {string} message message to print when the assertion fails + * @throws ArgumentsError + * @throws AssertionError + */ +assert.contains = function(actual, expected, message) { + if (isArray(actual)) { + if (typeof(expected) === "undefined") { + fail("Invalid expected argument to contains."); + } + + if (typeof(expected) === "object") { + fail(actual, expected, message + " Expected is object and actual is array.", "contains", assert.contains); + } else if (isArray(expected)) { + for (var i = 0; i < expected.length; i++) { + if (actual.indexOf(expected[i]) < 0) { + fail(actual, expected, message + " Actual array does not contain array index " + i + " of expected.", "contains", assert.contains); + } + } + } else { + // primitive type -- check to see if it is in the actual array + if (actual.indexOf(expected) < 0) { + fail(actual, expected, message + " Expected value " + expected + " is not contained in the array in actual.", "contains", assert.contains); + } + } + } else if (typeof(actual) === "object") { + if (typeof(expected) === "object") { + for (p in expected) { + if (p && expected.hasOwnProperty(p)) { + if (typeof(actual[p]) === 'undefined') { + // "actual does not contain expected properties"; + fail(actual[p], expected[p], message + " Expected contains property " + p + " and actual does not.", "contains", assert.contains); + } else if (typeof(expected[p]) === 'object') { + if (!_deepEqual(actual[p], expected[p])) { + fail(actual, expected, message, "contains", assert.notDeepEqual); + } + } else { + if (actual[p] != expected[p]) { + fail(actual, expected, message, "contains", assert.notDeepEqual); + } + } + } + } + } else if (isArray(expected)) { + fail(actual, expected, message + " Expected is array and actual is object.", "contains", assert.contains); + } else if (typeof(expected) === "string") { + if (typeof(actual[p]) === "undefined") { + fail(actual[p], expected[p], message + " Expected is looking for property " + expected + " and actual does not contain it.", "contains", assert.contains); + } + } else { + fail("Invalid expected argument to contains."); + } + } else { + fail("Invalid expected argument to contains."); + } + return; +}; + diff --git a/lib/types.js b/lib/types.js index 879edd8b3..36653d7ff 100644 --- a/lib/types.js +++ b/lib/types.js @@ -149,6 +149,9 @@ exports.test = function (name, start, options, callback) { ok: wrapAssert('ok', 'ok', 2), same: wrapAssert('same', 'deepEqual', 3), equals: wrapAssert('equals', 'equal', 3), + equalIgnoringOrder: wrapAssert('equalIgnoringOrder', 'equalIgnoringOrder', 3), + roughlyEqual: wrapAssert('roughlyEqual', 'roughlyEqual', 3), + contains: wrapAssert('contains', 'contains', 3), expect: function (num) { expecting = num; }, diff --git a/package.json b/package.json index c238fcf68..eddc05afb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodeunit", - "version": "0.11.2", + "version": "0.12.0", "description": "Easy unit testing for node.js and the browser.", "maintainers": [ {