deep-assert
Version:
Better deep-equals object expectations, supporting dynamic bottom-up assertions using any() and satisfies().
215 lines (214 loc) • 8.75 kB
JavaScript
;
// Based on code from <https://github.com/substack/node-deep-equal>
Object.defineProperty(exports, "__esModule", { value: true });
var formatter_1 = require("./formatter");
var symbols_1 = require("./symbols");
var supportsArgumentsClass = (function () {
return Object.prototype.toString.call(arguments);
})() === '[object Arguments]';
function supported(object) {
return Object.prototype.toString.call(object) === '[object Arguments]';
}
;
function unsupported(object) {
return object &&
typeof object === 'object' &&
typeof object.length === 'number' &&
Object.prototype.hasOwnProperty.call(object, 'callee') &&
!Object.prototype.propertyIsEnumerable.call(object, 'callee') ||
false;
}
;
function createMessage(message, actual, expected) {
return message + ":\n Actual: " + actual + "\n Expected: " + expected;
}
function isUndefinedOrNull(value) {
return value === null || value === undefined;
}
function isBuffer(x) {
if (!x || typeof x !== 'object' || typeof x.length !== 'number') {
return false;
}
if (typeof x.copy !== 'function' || typeof x.slice !== 'function') {
return false;
}
if (x.length > 0 && typeof x[0] !== 'number') {
return false;
}
return true;
}
function stripFirstLine(message) {
var indexOfFirstLinebreak = message.indexOf("\n");
return message.substr(indexOfFirstLinebreak + 1);
}
var isArguments = supportsArgumentsClass ? supported : unsupported;
function any() {
var _a;
return _a = {},
_a[symbols_1.$any] = true,
_a;
}
exports.any = any;
function satisfies(compare) {
var _a;
return _a = {},
_a[symbols_1.$compare] = function (actual) {
try {
var result = compare(actual);
return result === true
? true
: Error(createMessage("Custom value matcher failed", actual, "<custom logic>"));
}
catch (error) {
return error;
}
},
_a;
}
exports.satisfies = satisfies;
function deepEquals(actual, expected, path, opts) {
if (opts === void 0) { opts = {}; }
var result;
var allowAdditionalProps = opts.allowAdditionalProps === true;
if (expected && expected[symbols_1.$any]) {
if (typeof expected === "object" && Object.keys(expected).length > 0) {
// $any got in the object because of an object spread
allowAdditionalProps = true;
}
else {
return true;
}
}
if (expected && expected[symbols_1.$compare]) {
return expected[symbols_1.$compare](actual);
}
if (actual === expected) {
result = true;
}
else if (actual instanceof Date && expected instanceof Date) {
result = (actual.getTime() === expected.getTime()) ||
Error(createMessage("Dates do not match", actual, expected));
}
else if (!actual || !expected || typeof actual !== 'object' && typeof expected !== 'object') {
// tslint:disable-next-line
result = (opts.strict ? actual === expected : actual == expected) ||
Error(createMessage("Values do not match", actual, expected));
}
else if (isUndefinedOrNull(actual) || isUndefinedOrNull(expected)) {
return actual === expected || Error(createMessage("Values do not match", actual, expected));
}
else if (["bigint", "boolean", "number", "string", "symbol"].indexOf(typeof actual) > -1) {
return actual === expected || Error(createMessage("Values do not match", actual, expected));
}
else {
result = objEquiv(actual, expected, path, allowAdditionalProps, opts);
}
if (result instanceof Error && result.path) {
return result;
}
else if (result instanceof Error) {
var error = path.length > 0
? Error("Expectation failed: " + formatter_1.describePath(["$root"].concat(path)) + " does not match.\n" + stripFirstLine(result.message))
: result;
// tslint:disable-next-line
return Object.assign(error, { path: path });
}
return true;
}
exports.deepEquals = deepEquals;
function diffArrayElements(actual, expected, path, allowAdditionalProps, opts) {
var propMatches = [];
for (var index = 0; index < expected.length; index++) {
var result = deepEquals(actual[index], expected[index], path.concat([index]), opts);
propMatches.push([index, !(result instanceof Error)]);
}
if (!allowAdditionalProps && actual.length > expected.length) {
for (var index = expected.length; index < actual.length; index++) {
propMatches.push([index, false]);
}
}
propMatches.sort(function (pa, pb) { return pa > pb ? 1 : -1; });
return propMatches;
}
function diffObjectProps(actual, expected, path, allowAdditionalProps, opts) {
var actualKeys;
var expectedKeys;
try {
actualKeys = Object.keys(actual);
expectedKeys = Object.keys(expected);
}
catch (e) {
// happens when one is a string literal and the other isn't
return Error(createMessage("Actual value or expectation is a string literal, the other one is not", actual, expected));
}
var propMatches = [];
for (var _i = 0, expectedKeys_1 = expectedKeys; _i < expectedKeys_1.length; _i++) {
var key = expectedKeys_1[_i];
var result = deepEquals(actual[key], expected[key], path.concat([key]), opts);
propMatches.push([key, !(result instanceof Error)]);
}
if (!allowAdditionalProps) {
var unexpectedKeys = actualKeys.filter(function (key) { return expectedKeys.indexOf(key) === -1; });
propMatches.push.apply(propMatches, unexpectedKeys.map(function (key) { return [key, false]; }));
}
propMatches.sort(function (pa, pb) { return pa > pb ? 1 : -1; });
return propMatches;
}
function objEquiv(actual, expected, path, allowAdditionalProps, opts) {
// an identical 'prototype' property.
if (actual.prototype !== expected.prototype) {
return Error(createMessage("Object prototypes do not match", actual, expected));
}
// ~~~I've managed to break Object.keys through screwy arguments passing. (@substack)
// Converting to array solves the problem.
if (isArguments(actual)) {
if (!isArguments(expected)) {
return Error(createMessage("Got arguments pseudo-array, but did not expect it", actual, expected));
}
return deepEquals(actual.slice(), expected.slice(), path, opts);
}
if (isBuffer(expected)) {
if (!isBuffer(actual)) {
return Error(createMessage("Expected a buffer", actual, expected));
}
if (actual.length !== expected.length) {
return Error(createMessage("Buffer size does not match", actual.length, expected.length));
}
for (var i = 0; i < actual.length; i++) {
if (actual[i] !== expected[i]) {
return Error(createMessage("Buffer contents do not match at offset " + i, actual[i], expected[i]));
}
}
return true;
}
if (!Array.isArray(actual) && Array.isArray(expected)) {
return Error(createMessage("Expected an array", actual, expected));
}
else if (Array.isArray(actual) && !Array.isArray(expected)) {
return Error(createMessage("Got an array, but did not expect one", actual, expected));
}
var propMatches = Array.isArray(expected)
? diffArrayElements(actual, expected, path, allowAdditionalProps, opts)
: diffObjectProps(actual, expected, path, allowAdditionalProps, opts);
if (propMatches instanceof Error) {
return propMatches;
}
if (propMatches.some(function (_a) {
var matches = _a[1];
return !matches;
})) {
var mismatchingSubObjectsProp = propMatches.find(function (_a) {
var prop = _a[0], matches = _a[1];
return !matches && actual[prop] && expected[prop] && typeof actual[prop] === "object" && typeof expected[prop] === "object";
});
if (mismatchingSubObjectsProp) {
var prop = mismatchingSubObjectsProp[0];
return deepEquals(actual[prop], expected[prop], path.concat([prop]), opts);
}
else {
var message = (Array.isArray(actual) ? "Arrays" : "Objects") + " do not match:";
return Object.assign(Error(formatter_1.createObjectDiffMessage(message, actual, expected, path, propMatches)), { path: path });
}
}
return typeof actual === typeof expected || Error(createMessage("Types of values do not match", typeof actual, typeof expected));
}