Skip to content

[Breaking] bring package in line with node's assert.deepEqual #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"files": ["example/**", "test/**"],
"rules": {
"array-bracket-newline": 0,
"max-lines": 0,
"max-params": 0,
"max-statements": 0,
"no-console": 0,
Expand Down
39 changes: 35 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ var isArguments = require('is-arguments');
var is = require('object-is');
var isRegex = require('is-regex');
var flags = require('regexp.prototype.flags');
var isArray = require('isarray');
var isDate = require('is-date-object');
var isBoxedPrimitive = require('is-boxed-primitive');
var toPrimitive = require('es-to-primitive/es2015'); // TODO: replace this with ES2020 once updated

var getTime = Date.prototype.getTime;
var gPO = Object.getPrototypeOf;
var objToString = Object.prototype.toString;

function deepEqual(actual, expected, options) {
var opts = options || {};
Expand All @@ -15,6 +20,12 @@ function deepEqual(actual, expected, options) {
return true;
}

var actualBoxed = isBoxedPrimitive(actual);
var expectedBoxed = isBoxedPrimitive(expected);
if (actualBoxed || expectedBoxed) {
return deepEqual(toPrimitive(actual), toPrimitive(expected), opts);
}

// 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==.
if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) {
return opts.strict ? is(actual, expected) : actual == expected;
Expand Down Expand Up @@ -50,26 +61,45 @@ function isBuffer(x) {
}

function objEquiv(a, b, opts) {
/* eslint max-statements: [2, 50] */
/* eslint max-statements: [2, 70], max-lines-per-function: [2, 80] */
var i, key;

if (typeof a !== typeof b) { return false; }
if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { return false; }

// an identical 'prototype' property.
if (a.prototype !== b.prototype) { return false; }

if (objToString.call(a) !== objToString.call(b)) { return false; }

if (isArguments(a) !== isArguments(b)) { return false; }

var aIsArray = isArray(a);
var bIsArray = isArray(b);
if (aIsArray !== bIsArray) { return false; }

// TODO: replace when a cross-realm brand check is available
var aIsError = a instanceof Error;
var bIsError = b instanceof Error;
if (aIsError !== bIsError) { return false; }
if (aIsError || bIsError) {
if (a.name !== b.name || a.message !== b.message) { return false; }
}

var aIsRegex = isRegex(a);
var bIsRegex = isRegex(b);
if (aIsRegex !== bIsRegex) { return false; }
if (aIsRegex || bIsRegex) {
return a.source === b.source && flags(a) === flags(b);
}

if (isDate(a) && isDate(b)) {
return getTime.call(a) === getTime.call(b);
}
var aIsDate = isDate(a);
var bIsDate = isDate(b);
if (aIsDate !== bIsDate) { return false; }
if (aIsDate || bIsDate) { // && would work too, because both are true or both false here
if (getTime.call(a) !== getTime.call(b)) { return false; }
if (opts.strict && gPO && gPO(a) !== gPO(b)) { return false; }
} else if (gPO && gPO(a) !== gPO(b)) { return false; } // non-Dates always compare [[Prototype]]s

var aIsBuffer = isBuffer(a);
var bIsBuffer = isBuffer(b);
Expand Down Expand Up @@ -100,6 +130,7 @@ function objEquiv(a, b, opts) {
for (i = ka.length - 1; i >= 0; i--) {
if (ka[i] != kb[i]) { return false; }
}

// equivalent values for every corresponding key, and ~~~possibly expensive deep test
for (i = ka.length - 1; i >= 0; i--) {
key = ka[i];
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@
"test": "npm run tests-only"
},
"dependencies": {
"es-to-primitive": "^1.2.0",
"is-arguments": "^1.0.4",
"is-boxed-primitive": "^1.0.0",
"is-date-object": "^1.0.1",
"is-regex": "^1.0.4",
"isarray": "^2.0.5",
"object-is": "^1.0.1",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.2.0"
},
"devDependencies": {
"@ljharb/eslint-config": "^13.1.1",
"eslint": "^5.16.0",
"has-symbols": "^1.0.0",
"object.assign": "^4.1.0",
"tape": "^4.11.0"
},
"repository": {
Expand Down
107 changes: 104 additions & 3 deletions test/cmp.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var test = require('tape');
require('./_tape');
var assign = require('object.assign');
var hasSymbols = require('has-symbols')();

var equal = require('../');

Expand Down Expand Up @@ -90,7 +92,13 @@ test('arguments class', function (t) {
test('dates', function (t) {
var d0 = new Date(1387585278000);
var d1 = new Date('Fri Dec 20 2013 16:21:18 GMT-0800 (PST)');
t.deepEqualTest(d0, d1, 'equivalent Dates', true, true);

t.deepEqualTest(d0, d1, 'two Dates with the same timestamp', true, true);

d1.a = true;

t.deepEqualTest(d0, d1, 'two Dates with the same timestamp but different own properties', false, false);

t.end();
});

Expand Down Expand Up @@ -327,9 +335,9 @@ test('regexen', function (t) {
});

test('arrays and objects', function (t) {
t.deepEqualTest([], {}, 'empty array and empty object', true, true);
t.deepEqualTest([], {}, 'empty array and empty object', false, false);
t.deepEqualTest([], { length: 0 }, 'empty array and empty arraylike object', false, false);
t.deepEqualTest([1], { 0: 1 }, 'array and similar object', true, true);
t.deepEqualTest([1], { 0: 1 }, 'array and similar object', false, false);

t.end();
});
Expand All @@ -342,3 +350,96 @@ test('functions', function (t) {

t.end();
});

test('Errors', function (t) {
t.deepEqualTest(new Error('xyz'), new Error('xyz'), 'two errors of the same type with the same message', true, true, false);
t.deepEqualTest(new Error('xyz'), new TypeError('xyz'), 'two errors of different types with the same message', false, false);
t.deepEqualTest(new Error('xyz'), new Error('zyx'), 'two errors of the same type with a different message', false, false);

t.deepEqualTest(
new Error('a'),
assign(new Error('a'), { code: 10 }),
'two otherwise equal errors with different own properties',
false,
false
);

t.end();
});

test('errors', function (t) {

t.end();
});

test('error = Object', function (t) {
t.notOk(equal(new Error('a'), { message: 'a' }));
t.end();
});

test('[[Prototypes]]', { skip: !Object.getPrototypeOf }, function (t) {
function C() {}
var instance = new C();
delete instance.constructor;

t.deepEqualTest({}, instance, 'two identical objects with different [[Prototypes]]', false, false);

t.test('Dates with different prototypes', { skip: !Object.setPrototypeOf }, function (st) {
var d1 = new Date(0);
var d2 = new Date(0);

t.deepEqualTest(d1, d2, 'two dates with the same timestamp', true, true);

var newProto = {};
Object.setPrototypeOf(newProto, Date.prototype);
Object.setPrototypeOf(d2, newProto);
st.ok(d2 instanceof Date, 'd2 is still a Date instance after tweaking [[Prototype]]');

t.deepEqualTest(d1, d2, 'two dates with the same timestamp and different [[Prototype]]', true, false);

st.end();
});

t.end();
});

test('toStringTag', { skip: !hasSymbols || !Symbol.toStringTag }, function (t) {
var o1 = {};
t.equal(Object.prototype.toString.call(o1), '[object Object]', 'o1: Symbol.toStringTag works');

var o2 = {};
t.equal(Object.prototype.toString.call(o2), '[object Object]', 'o2: original Symbol.toStringTag works');

t.deepEqualTest(o1, o2, 'two normal empty objects', true, true);

o2[Symbol.toStringTag] = 'jifasnif';
t.equal(Object.prototype.toString.call(o2), '[object jifasnif]', 'o2: modified Symbol.toStringTag works');

t.deepEqualTest(o1, o2, 'two normal empty objects with different toStringTags', false, false);

t.end();
});

test('boxed primitives', function (t) {
t.deepEqualTest(Object(false), false, 'boxed and primitive `false`', true, true);
t.deepEqualTest(Object(true), true, 'boxed and primitive `true`', true, true);
t.deepEqualTest(Object(3), 3, 'boxed and primitive `3`', true, true);
t.deepEqualTest(Object(NaN), NaN, 'boxed and primitive `NaN`', false, true);
t.deepEqualTest(Object(''), '', 'boxed and primitive `""`', true, true);
t.deepEqualTest(Object('str'), 'str', 'boxed and primitive `"str"`', true, true);

t.test('symbol', { skip: !hasSymbols }, function (st) {
var s = Symbol('');
st.deepEqualTest(Object(s), s, 'boxed and primitive `Symbol()`', true, true);
st.end();
});

/* globals BigInt: false */
t.test('bigint', { skip: typeof BigInt !== 'function' }, function (st) {
var hhgtg = BigInt(42);
st.deepEqualTest(Object(hhgtg), hhgtg, 'boxed and primitive `BigInt(42)`', true, true);
st.end();
});

t.end();
});