Files
lodash/test/isEqual.spec.js
2023-09-16 22:59:56 -07:00

822 lines
24 KiB
JavaScript

import lodashStable from 'lodash';
import {
noop,
create,
args,
realm,
arrayViews,
map,
promise,
set,
defineProperty,
document,
stubFalse,
} from './utils';
import isEqual from '../src/isEqual';
describe('isEqual', () => {
const symbol1 = Symbol ? Symbol('a') : true;
const symbol2 = Symbol ? Symbol('b') : false;
it('should compare primitives', () => {
const pairs = [
[1, 1, true],
[1, Object(1), true],
[1, '1', false],
[1, 2, false],
[-0, -0, true],
[0, 0, true],
[0, Object(0), true],
[Object(0), Object(0), true],
[-0, 0, true],
[0, '0', false],
[0, null, false],
[NaN, NaN, true],
[NaN, Object(NaN), true],
[Object(NaN), Object(NaN), true],
[NaN, 'a', false],
[NaN, Infinity, false],
['a', 'a', true],
['a', Object('a'), true],
[Object('a'), Object('a'), true],
['a', 'b', false],
['a', ['a'], false],
[true, true, true],
[true, Object(true), true],
[Object(true), Object(true), true],
[true, 1, false],
[true, 'a', false],
[false, false, true],
[false, Object(false), true],
[Object(false), Object(false), true],
[false, 0, false],
[false, '', false],
[symbol1, symbol1, true],
[symbol1, Object(symbol1), true],
[Object(symbol1), Object(symbol1), true],
[symbol1, symbol2, false],
[null, null, true],
[null, undefined, false],
[null, {}, false],
[null, '', false],
[undefined, undefined, true],
[undefined, null, false],
[undefined, '', false],
];
const expected = lodashStable.map(pairs, (pair) => pair[2]);
const actual = lodashStable.map(pairs, (pair) => isEqual(pair[0], pair[1]));
expect(actual).toEqual(expected);
});
it('should compare arrays', () => {
let array1 = [true, null, 1, 'a', undefined];
let array2 = [true, null, 1, 'a', undefined];
expect(isEqual(array1, array2)).toBe(true);
array1 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }];
array2 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }];
expect(isEqual(array1, array2)).toBe(true);
array1 = [1];
array1[2] = 3;
array2 = [1];
array2[1] = undefined;
array2[2] = 3;
expect(isEqual(array1, array2)).toBe(true);
array1 = [
Object(1),
false,
Object('a'),
/x/,
new Date(2012, 4, 23),
['a', 'b', [Object('c')]],
{ a: 1 },
];
array2 = [
1,
Object(false),
'a',
/x/,
new Date(2012, 4, 23),
['a', Object('b'), ['c']],
{ a: 1 },
];
expect(isEqual(array1, array2)).toBe(true);
array1 = [1, 2, 3];
array2 = [3, 2, 1];
expect(isEqual(array1, array2)).toBe(false);
array1 = [1, 2];
array2 = [1, 2, 3];
expect(isEqual(array1, array2)).toBe(false);
});
it('should treat arrays with identical values but different non-index properties as equal', () => {
let array1 = [1, 2, 3];
let array2 = [1, 2, 3];
array1.every =
array1.filter =
array1.forEach =
array1.indexOf =
array1.lastIndexOf =
array1.map =
array1.some =
array1.reduce =
array1.reduceRight =
null;
array2.concat =
array2.join =
array2.pop =
array2.reverse =
array2.shift =
array2.slice =
array2.sort =
array2.splice =
array2.unshift =
null;
expect(isEqual(array1, array2)).toBe(true);
array1 = [1, 2, 3];
array1.a = 1;
array2 = [1, 2, 3];
array2.b = 1;
expect(isEqual(array1, array2)).toBe(true);
array1 = /c/.exec('abcde');
array2 = ['c'];
expect(isEqual(array1, array2)).toBe(true);
});
it('should compare sparse arrays', () => {
const array = Array(1);
expect(isEqual(array, Array(1))).toBe(true);
expect(isEqual(array, [undefined])).toBe(true);
expect(isEqual(array, Array(2))).toBe(false);
});
it('should compare plain objects', () => {
let object1 = { a: true, b: null, c: 1, d: 'a', e: undefined };
let object2 = { a: true, b: null, c: 1, d: 'a', e: undefined };
expect(isEqual(object1, object2)).toBe(true);
object1 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } };
object2 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } };
expect(isEqual(object1, object2)).toBe(true);
object1 = { a: 1, b: 2, c: 3 };
object2 = { a: 3, b: 2, c: 1 };
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2, c: 3 };
object2 = { d: 1, e: 2, f: 3 };
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2 };
object2 = { a: 1, b: 2, c: 3 };
expect(isEqual(object1, object2)).toBe(false);
});
it('should compare objects regardless of key order', () => {
const object1 = { a: 1, b: 2, c: 3 };
const object2 = { c: 3, a: 1, b: 2 };
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare nested objects', () => {
const object1 = {
a: [1, 2, 3],
b: true,
c: Object(1),
d: 'a',
e: {
f: ['a', Object('b'), 'c'],
g: Object(false),
h: new Date(2012, 4, 23),
i: noop,
j: 'a',
},
};
const object2 = {
a: [1, Object(2), 3],
b: Object(true),
c: 1,
d: Object('a'),
e: {
f: ['a', 'b', 'c'],
g: false,
h: new Date(2012, 4, 23),
i: noop,
j: 'a',
},
};
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare object instances', () => {
function Foo() {
this.a = 1;
}
Foo.prototype.a = 1;
function Bar() {
this.a = 1;
}
Bar.prototype.a = 2;
expect(isEqual(new Foo(), new Foo())).toBe(true);
expect(isEqual(new Foo(), new Bar())).toBe(false);
expect(isEqual({ a: 1 }, new Foo())).toBe(false);
expect(isEqual({ a: 2 }, new Bar())).toBe(false);
});
it('should compare objects with constructor properties', () => {
expect(isEqual({ constructor: 1 }, { constructor: 1 })).toBe(true);
expect(isEqual({ constructor: 1 }, { constructor: '1' })).toBe(false);
expect(isEqual({ constructor: [1] }, { constructor: [1] })).toBe(true);
expect(isEqual({ constructor: [1] }, { constructor: ['1'] })).toBe(false);
expect(isEqual({ constructor: Object }, {})).toBe(false);
});
it('should compare arrays with circular references', () => {
let array1 = [];
let array2 = [];
array1.push(array1);
array2.push(array2);
expect(isEqual(array1, array2)).toBe(true);
array1.push('b');
array2.push('b');
expect(isEqual(array1, array2)).toBe(true);
array1.push('c');
array2.push('d');
expect(isEqual(array1, array2)).toBe(false);
array1 = ['a', 'b', 'c'];
array1[1] = array1;
array2 = ['a', ['a', 'b', 'c'], 'c'];
expect(isEqual(array1, array2)).toBe(false);
});
it('should have transitive equivalence for circular references of arrays', () => {
const array1 = [];
const array2 = [array1];
const array3 = [array2];
array1[0] = array1;
expect(isEqual(array1, array2)).toBe(true);
expect(isEqual(array2, array3)).toBe(true);
expect(isEqual(array1, array3)).toBe(true);
});
it('should compare objects with circular references', () => {
let object1 = {};
let object2 = {};
object1.a = object1;
object2.a = object2;
expect(isEqual(object1, object2)).toBe(true);
object1.b = 0;
object2.b = Object(0);
expect(isEqual(object1, object2)).toBe(true);
object1.c = Object(1);
object2.c = Object(2);
expect(isEqual(object1, object2)).toBe(false);
object1 = { a: 1, b: 2, c: 3 };
object1.b = object1;
object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 };
expect(isEqual(object1, object2)).toBe(false);
});
it('should have transitive equivalence for circular references of objects', () => {
const object1 = {};
const object2 = { a: object1 };
const object3 = { a: object2 };
object1.a = object1;
expect(isEqual(object1, object2)).toBe(true);
expect(isEqual(object2, object3)).toBe(true);
expect(isEqual(object1, object3)).toBe(true);
});
it('should compare objects with multiple circular references', () => {
const array1 = [{}];
const array2 = [{}];
(array1[0].a = array1).push(array1);
(array2[0].a = array2).push(array2);
expect(isEqual(array1, array2)).toBe(true);
array1[0].b = 0;
array2[0].b = Object(0);
expect(isEqual(array1, array2)).toBe(true);
array1[0].c = Object(1);
array2[0].c = Object(2);
expect(isEqual(array1, array2)).toBe(false);
});
it('should compare objects with complex circular references', () => {
const object1 = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
const object2 = {
foo: { b: { c: { d: {} } } },
bar: { a: 2 },
};
object1.foo.b.c.d = object1;
object1.bar.b = object1.foo.b;
object2.foo.b.c.d = object2;
object2.bar.b = object2.foo.b;
expect(isEqual(object1, object2)).toBe(true);
});
it('should compare objects with shared property values', () => {
const object1 = {
a: [1, 2],
};
const object2 = {
a: [1, 2],
b: [1, 2],
};
object1.b = object1.a;
expect(isEqual(object1, object2)).toBe(true);
});
it('should treat objects created by `Object.create(null)` like plain objects', () => {
function Foo() {
this.a = 1;
}
Foo.prototype.constructor = null;
const object1 = create(null);
object1.a = 1;
const object2 = { a: 1 };
expect(isEqual(object1, object2)).toBe(true);
expect(isEqual(new Foo(), object2)).toBe(false);
});
it('should avoid common type coercions', () => {
expect(isEqual(true, Object(false))).toBe(false);
expect(isEqual(Object(false), Object(0))).toBe(false);
expect(isEqual(false, Object(''))).toBe(false);
expect(isEqual(Object(36), Object('36'))).toBe(false);
expect(isEqual(0, '')).toBe(false);
expect(isEqual(1, true)).toBe(false);
expect(isEqual(1337756400000, new Date(2012, 4, 23))).toBe(false);
expect(isEqual('36', 36)).toBe(false);
expect(isEqual(36, '36')).toBe(false);
});
it('should compare `arguments` objects', () => {
const args1 = (function () {
return arguments;
})();
const args2 = (function () {
return arguments;
})();
const args3 = (function () {
return arguments;
})(1, 2);
expect(isEqual(args1, args2)).toBe(true);
expect(isEqual(args1, args3)).toBe(false);
});
it('should treat `arguments` objects like `Object` objects', () => {
const object = { 0: 1, 1: 2, 2: 3 };
function Foo() {}
Foo.prototype = object;
expect(isEqual(args, object)).toBe(true);
expect(isEqual(object, args)).toBe(true);
expect(isEqual(args, new Foo())).toBe(false);
expect(isEqual(new Foo(), args)).toBe(false);
});
it('should compare array buffers', () => {
if (ArrayBuffer) {
const buffer = new Int8Array([-1]).buffer;
expect(isEqual(buffer, new Uint8Array([255]).buffer)).toBe(true);
expect(isEqual(buffer, new ArrayBuffer(1))).toBe(false);
}
});
it('should compare array views', () => {
lodashStable.times(2, (index) => {
const ns = index ? realm : root;
const pairs = lodashStable.map(arrayViews, (type, viewIndex) => {
const otherType = arrayViews[(viewIndex + 1) % arrayViews.length];
const CtorA =
ns[type] ||
function (n) {
this.n = n;
};
const CtorB =
ns[otherType] ||
function (n) {
this.n = n;
};
const bufferA = ns[type] ? new ns.ArrayBuffer(8) : 8;
const bufferB = ns[otherType] ? new ns.ArrayBuffer(8) : 8;
const bufferC = ns[otherType] ? new ns.ArrayBuffer(16) : 16;
return [
new CtorA(bufferA),
new CtorA(bufferA),
new CtorB(bufferB),
new CtorB(bufferC),
];
});
const expected = lodashStable.map(pairs, lodashStable.constant([true, false, false]));
const actual = lodashStable.map(pairs, (pair) => [
isEqual(pair[0], pair[1]),
isEqual(pair[0], pair[2]),
isEqual(pair[2], pair[3]),
]);
expect(actual).toEqual(expected);
});
});
it('should compare buffers', () => {
if (Buffer) {
const buffer = Buffer.alloc([1]);
expect(isEqual(buffer, Buffer.alloc([1]))).toBe(true);
expect(isEqual(buffer, Buffer.alloc([2]))).toBe(false);
expect(isEqual(buffer, new Uint8Array([1]))).toBe(false);
}
});
it('should compare date objects', () => {
const date = new Date(2012, 4, 23);
expect(isEqual(date, new Date(2012, 4, 23))).toBe(true);
expect(isEqual(new Date('a'), new Date('b'))).toBe(true);
expect(isEqual(date, new Date(2013, 3, 25))).toBe(false);
expect(isEqual(date, { getTime: lodashStable.constant(+date) })).toBe(false);
});
it('should compare error objects', () => {
const pairs = lodashStable.map(
[
'Error',
'EvalError',
'RangeError',
'ReferenceError',
'SyntaxError',
'TypeError',
'URIError',
],
(type, index, errorTypes) => {
const otherType = errorTypes[++index % errorTypes.length];
const CtorA = root[type];
const CtorB = root[otherType];
return [new CtorA('a'), new CtorA('a'), new CtorB('a'), new CtorB('b')];
},
);
const expected = lodashStable.map(pairs, lodashStable.constant([true, false, false]));
const actual = lodashStable.map(pairs, (pair) => [
isEqual(pair[0], pair[1]),
isEqual(pair[0], pair[2]),
isEqual(pair[2], pair[3]),
]);
expect(actual).toEqual(expected);
});
it('should compare functions', () => {
function a() {
return 1 + 2;
}
function b() {
return 1 + 2;
}
expect(isEqual(a, a)).toBe(true);
expect(isEqual(a, b)).toBe(false);
});
it('should compare maps', () => {
if (Map) {
lodashStable.each(
[
[map, new Map()],
[map, realm.map],
],
(maps) => {
const map1 = maps[0];
const map2 = maps[1];
map1.set('a', 1);
map2.set('b', 2);
expect(isEqual(map1, map2)).toBe(false);
map1.set('b', 2);
map2.set('a', 1);
expect(isEqual(map1, map2)).toBe(true);
map1.delete('a');
map1.set('a', 1);
expect(isEqual(map1, map2)).toBe(true);
map2.delete('a');
expect(isEqual(map1, map2)).toBe(false);
map1.clear();
map2.clear();
},
);
}
});
it('should compare maps with circular references', () => {
if (Map) {
const map1 = new Map();
const map2 = new Map();
map1.set('a', map1);
map2.set('a', map2);
expect(isEqual(map1, map2)).toBe(true);
map1.set('b', 1);
map2.set('b', 2);
expect(isEqual(map1, map2)).toBe(false);
}
});
it('should compare promises by reference', () => {
if (promise) {
lodashStable.each(
[
[promise, Promise.resolve(1)],
[promise, realm.promise],
],
(promises) => {
const promise1 = promises[0];
const promise2 = promises[1];
expect(isEqual(promise1, promise2)).toBe(false);
expect(isEqual(promise1, promise1)).toBe(true);
},
);
}
});
it('should compare regexes', () => {
expect(isEqual(/x/gim, /x/gim)).toBe(true);
expect(isEqual(/x/gim, /x/gim)).toBe(true);
expect(isEqual(/x/gi, /x/g)).toBe(false);
expect(isEqual(/x/, /y/)).toBe(false);
assert.strictEqual(
isEqual(/x/g, { global: true, ignoreCase: false, multiline: false, source: 'x' }),
false,
);
});
it('should compare sets', () => {
if (Set) {
lodashStable.each(
[
[set, new Set()],
[set, realm.set],
],
(sets) => {
const set1 = sets[0];
const set2 = sets[1];
set1.add(1);
set2.add(2);
expect(isEqual(set1, set2)).toBe(false);
set1.add(2);
set2.add(1);
expect(isEqual(set1, set2)).toBe(true);
set1.delete(1);
set1.add(1);
expect(isEqual(set1, set2)).toBe(true);
set2.delete(1);
expect(isEqual(set1, set2)).toBe(false);
set1.clear();
set2.clear();
},
);
}
});
it('should compare sets with circular references', () => {
if (Set) {
const set1 = new Set();
const set2 = new Set();
set1.add(set1);
set2.add(set2);
expect(isEqual(set1, set2)).toBe(true);
set1.add(1);
set2.add(2);
expect(isEqual(set1, set2)).toBe(false);
}
});
it('should compare symbol properties', () => {
if (Symbol) {
const object1 = { a: 1 };
const object2 = { a: 1 };
object1[symbol1] = { a: { b: 2 } };
object2[symbol1] = { a: { b: 2 } };
defineProperty(object2, symbol2, {
configurable: true,
enumerable: false,
writable: true,
value: 2,
});
expect(isEqual(object1, object2)).toBe(true);
object2[symbol1] = { a: 1 };
expect(isEqual(object1, object2)).toBe(false);
delete object2[symbol1];
object2[Symbol('a')] = { a: { b: 2 } };
expect(isEqual(object1, object2)).toBe(false);
}
});
it('should compare wrapped values', () => {
const stamp = +new Date();
const values = [
[
[1, 2],
[1, 2],
[1, 2, 3],
],
[true, true, false],
[new Date(stamp), new Date(stamp), new Date(stamp - 100)],
[
{ a: 1, b: 2 },
{ a: 1, b: 2 },
{ a: 1, b: 1 },
],
[1, 1, 2],
[NaN, NaN, Infinity],
[/x/, /x/, /x/i],
['a', 'a', 'A'],
];
lodashStable.each(values, (vals) => {
let wrapped1 = _(vals[0]);
let wrapped2 = _(vals[1]);
let actual = wrapped1.isEqual(wrapped2);
expect(actual).toBe(true);
expect(isEqual(_(actual), _(true))).toBe(true);
wrapped1 = _(vals[0]);
wrapped2 = _(vals[2]);
actual = wrapped1.isEqual(wrapped2);
expect(actual).toBe(false);
expect(isEqual(_(actual), _(false))).toBe(true);
});
});
it('should compare wrapped and non-wrapped values', () => {
let object1 = _({ a: 1, b: 2 });
let object2 = { a: 1, b: 2 };
expect(object1.isEqual(object2)).toBe(true);
expect(isEqual(object1, object2)).toBe(true);
object1 = _({ a: 1, b: 2 });
object2 = { a: 1, b: 1 };
expect(object1.isEqual(object2)).toBe(false);
expect(isEqual(object1, object2)).toBe(false);
});
it('should work as an iteratee for `_.every`', () => {
const actual = lodashStable.every([1, 1, 1], lodashStable.partial(isEqual, 1));
expect(actual);
});
it('should not error on DOM elements', () => {
if (document) {
const element1 = document.createElement('div');
const element2 = element1.cloneNode(true);
try {
expect(isEqual(element1, element2)).toBe(false);
} catch (e) {
expect(false, e.message);
}
}
});
it('should return `true` for like-objects from different documents', () => {
if (realm.object) {
expect(isEqual([1], realm.array)).toBe(true);
expect(isEqual([2], realm.array)).toBe(false);
expect(isEqual({ a: 1 }, realm.object)).toBe(true);
expect(isEqual({ a: 2 }, realm.object)).toBe(false);
}
});
it('should return `false` for objects with custom `toString` methods', () => {
let primitive;
const object = {
toString: function () {
return primitive;
},
};
const values = [true, null, 1, 'a', undefined];
const expected = lodashStable.map(values, stubFalse);
const actual = lodashStable.map(values, (value) => {
primitive = value;
return isEqual(object, value);
});
expect(actual).toEqual(expected);
});
it('should return an unwrapped value when implicitly chaining', () => {
expect(_('a').isEqual('a')).toBe(true);
});
it('should return a wrapped value when explicitly chaining', () => {
expect(_('a').chain().isEqual('a') instanceof _);
});
});