Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
39 changes: 38 additions & 1 deletion doc/api/assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,20 @@

### `new assert.Assert([options])`

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/00000

Check warning on line 235 in doc/api/assert.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Added `skipPrototypeComparison` option.
-->

* `options` {Object}
* `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`.
Accepted values: `'simple'`, `'full'`.
* `strict` {boolean} If set to `true`, non-strict methods behave like their
corresponding strict methods. Defaults to `true`.
* `skipPrototypeComparison` {boolean} If set to `true`, skips prototype and constructor
comparison in deep equality checks. Defaults to `false`.

Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages.

Expand All @@ -245,7 +254,8 @@
```

**Important**: When destructuring assertion methods from an `Assert` instance,
the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings).
the methods lose their connection to the instance's configuration options (such
as `diff`, `strict`, and `skipPrototypeComparison` settings).
The destructured methods will fall back to default behavior instead.

```js
Expand All @@ -259,6 +269,33 @@
strictEqual({ a: 1 }, { b: { c: 1 } });
```

The `skipPrototypeComparison` option affects all deep equality methods:

```js
class Foo {
constructor(a) {
this.a = a;
}
}

class Bar {
constructor(a) {
this.a = a;
}
}

const foo = new Foo(1);
const bar = new Bar(1);

// Default behavior - fails due to different constructors
const assert1 = new Assert();
assert1.deepStrictEqual(foo, bar); // AssertionError

// Skip prototype comparison - passes if properties are equal
const assert2 = new Assert({ skipPrototypeComparison: true });
assert2.deepStrictEqual(foo, bar); // OK
```

When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
(diff: 'simple', non-strict mode).
To maintain custom options when using destructured methods, avoid
Expand Down
40 changes: 39 additions & 1 deletion doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1553,19 +1553,57 @@
console.log(arr); // logs the full array
```

## `util.isDeepStrictEqual(val1, val2)`
## `util.isDeepStrictEqual(val1, val2[, options])`

<!-- YAML
added: v9.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/00000
description: Added `options` parameter to allow skipping prototype comparison.
-->

* `val1` {any}
* `val2` {any}
* `options` {Object}
* `skipPrototypeComparison` {boolean} If `true`, prototype and constructor
comparison is skipped during deep strict equality check. **Default:** `false`.
* Returns: {boolean}

Returns `true` if there is deep strict equality between `val1` and `val2`.
Otherwise, returns `false`.

By default, deep strict equality includes comparison of object prototypes and
constructors. When `options.skipPrototypeComparison` is `true`, objects with
different prototypes or constructors can still be considered equal if their
enumerable properties are deeply strictly equal.

```js
const util = require('node:util');

Check warning on line 1583 in doc/api/util.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
class Foo {
constructor(a) {
this.a = a;
}
}

class Bar {
constructor(a) {
this.a = a;
}
}

const foo = new Foo(1);
const bar = new Bar(1);

// Different constructors, same properties
console.log(util.isDeepStrictEqual(foo, bar));
// false

console.log(util.isDeepStrictEqual(foo, bar, { skipPrototypeComparison: true }));
// true
```

See [`assert.deepStrictEqual()`][] for more information about deep strict
equality.

Expand Down
14 changes: 11 additions & 3 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ const NO_EXCEPTION_SENTINEL = {};
* @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors.
* @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding
* strict methods.
* @property {boolean} [skipPrototypeComparison=false] - If set to true, skips comparing prototypes
* in deep equality checks.
*/

/**
Expand All @@ -105,7 +107,7 @@ function Assert(options) {
throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert');
}

options = ObjectAssign({ __proto__: null, strict: true }, options);
options = ObjectAssign({ __proto__: null, strict: true, skipPrototypeComparison: false }, options);

const allowedDiffs = ['simple', 'full'];
if (options.diff !== undefined) {
Expand Down Expand Up @@ -311,7 +313,10 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me
throw new ERR_MISSING_ARGS('actual', 'expected');
}
if (isDeepEqual === undefined) lazyLoadComparison();
if (!isDeepStrictEqual(actual, expected)) {
if (!isDeepStrictEqual(actual, expected, {
__proto__: null,
skipPrototypeComparison: this?.[kOptions]?.skipPrototypeComparison,
})) {
innerFail({
actual,
expected,
Expand All @@ -337,7 +342,10 @@ function notDeepStrictEqual(actual, expected, message) {
throw new ERR_MISSING_ARGS('actual', 'expected');
}
if (isDeepEqual === undefined) lazyLoadComparison();
if (isDeepStrictEqual(actual, expected)) {
if (isDeepStrictEqual(actual, expected, {
__proto__: null,
skipPrototypeComparison: this?.[kOptions]?.skipPrototypeComparison,
})) {
innerFail({
actual,
expected,
Expand Down
12 changes: 8 additions & 4 deletions lib/internal/util/comparisons.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ const {
getOwnNonIndexProperties,
} = internalBinding('util');

const kStrict = 1;
const kStrict = 2;
const kStrictWithoutPrototypes = 3;
const kLoose = 0;
const kPartial = 2;
const kPartial = 1;

const kNoIterator = 0;
const kIsArray = 1;
Expand Down Expand Up @@ -458,7 +459,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) {
}
} else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) {
return false;
} else if (mode === kStrict) {
} else if (mode === kStrict || mode === kStrictWithoutPrototypes) {
const symbolKeysA = getOwnSymbols(val1);
if (symbolKeysA.length !== 0) {
let count = 0;
Expand Down Expand Up @@ -1027,7 +1028,10 @@ module.exports = {
isDeepEqual(val1, val2) {
return detectCycles(val1, val2, kLoose);
},
isDeepStrictEqual(val1, val2) {
isDeepStrictEqual(val1, val2, options) {
if (options?.skipPrototypeComparison) {
return detectCycles(val1, val2, kStrictWithoutPrototypes);
}
return detectCycles(val1, val2, kStrict);
},
isPartialStrictEqual(val1, val2) {
Expand Down
7 changes: 3 additions & 4 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,11 @@ module.exports = {
isArray: deprecate(ArrayIsArray,
'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.',
'DEP0044'),
isDeepStrictEqual(a, b) {
isDeepStrictEqual(a, b, options) {
if (internalDeepEqual === undefined) {
internalDeepEqual = require('internal/util/comparisons')
.isDeepStrictEqual;
internalDeepEqual = require('internal/util/comparisons').isDeepStrictEqual;
}
return internalDeepEqual(a, b);
return internalDeepEqual(a, b, options);
},
promisify,
stripVTControlCharacters,
Expand Down
160 changes: 160 additions & 0 deletions test/parallel/test-assert-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,163 @@ test('Assert class non strict with simple diff', () => {
);
}
});

// Shared setup for skipPrototypeComparison tests
{
const message = 'Expected values to be strictly deep-equal:\n' +
'+ actual - expected\n' +
'\n' +
' [\n' +
' 1,\n' +
' 2,\n' +
' 3,\n' +
' 4,\n' +
' 5,\n' +
'+ 6,\n' +
'- 9,\n' +
' 7\n' +
' ]\n';

function CoolClass(name) { this.name = name; }

function AwesomeClass(name) { this.name = name; }

class Modern { constructor(value) { this.value = value; } }
class Legacy { constructor(value) { this.value = value; } }

const cool = new CoolClass('Assert is inspiring');
const awesome = new AwesomeClass('Assert is inspiring');
const modern = new Modern(42);
const legacy = new Legacy(42);

test('Assert class strict with skipPrototypeComparison', () => {
const assertInstance = new Assert({ skipPrototypeComparison: true });

assert.throws(
() => assertInstance.deepEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]),
{ message }
);

assertInstance.deepEqual(cool, awesome);
assertInstance.deepStrictEqual(cool, awesome);
assertInstance.deepEqual(modern, legacy);
assertInstance.deepStrictEqual(modern, legacy);

const cool2 = new CoolClass('Soooo coooool');
assert.throws(
() => assertInstance.deepStrictEqual(cool, cool2),
{ code: 'ERR_ASSERTION' }
);

const nested1 = { obj: new CoolClass('test'), arr: [1, 2, 3] };
const nested2 = { obj: new AwesomeClass('test'), arr: [1, 2, 3] };
assertInstance.deepStrictEqual(nested1, nested2);

const arr = new Uint8Array([1, 2, 3]);
const buf = Buffer.from([1, 2, 3]);
assertInstance.deepStrictEqual(arr, buf);
});

test('Assert class non strict with skipPrototypeComparison', () => {
const assertInstance = new Assert({ strict: false, skipPrototypeComparison: true });

assert.throws(
() => assertInstance.deepStrictEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]),
{ message }
);

assertInstance.deepStrictEqual(cool, awesome);
assertInstance.deepStrictEqual(modern, legacy);
});

test('Assert class skipPrototypeComparison with complex objects', () => {
const assertInstance = new Assert({ skipPrototypeComparison: true });

function ComplexAwesomeClass(name, age) {
this.name = name;
this.age = age;
this.settings = {
theme: 'dark',
lang: 'en'
};
}

function ComplexCoolClass(name, age) {
this.name = name;
this.age = age;
this.settings = {
theme: 'dark',
lang: 'en'
};
}

const awesome1 = new ComplexAwesomeClass('Foo', 30);
const cool1 = new ComplexCoolClass('Foo', 30);

assertInstance.deepStrictEqual(awesome1, cool1);

const cool2 = new ComplexCoolClass('Foo', 30);
cool2.settings.theme = 'light';

assert.throws(
() => assertInstance.deepStrictEqual(awesome1, cool2),
{ code: 'ERR_ASSERTION' }
);
});

test('Assert class skipPrototypeComparison with arrays and special objects', () => {
const assertInstance = new Assert({ skipPrototypeComparison: true });

const arr1 = [1, 2, 3];
const arr2 = new Array(1, 2, 3);
assertInstance.deepStrictEqual(arr1, arr2);

const date1 = new Date('2023-01-01');
const date2 = new Date('2023-01-01');
assertInstance.deepStrictEqual(date1, date2);

const regex1 = /test/g;
const regex2 = new RegExp('test', 'g');
assertInstance.deepStrictEqual(regex1, regex2);

const date3 = new Date('2023-01-02');
assert.throws(
() => assertInstance.deepStrictEqual(date1, date3),
{ code: 'ERR_ASSERTION' }
);
});

test('Assert class skipPrototypeComparison with notDeepStrictEqual', () => {
const assertInstance = new Assert({ skipPrototypeComparison: true });

assert.throws(
() => assertInstance.notDeepStrictEqual(cool, awesome),
{ code: 'ERR_ASSERTION' }
);

const notAwesome = new AwesomeClass('Not so awesome');
assertInstance.notDeepStrictEqual(cool, notAwesome);

const defaultAssertInstance = new Assert({ skipPrototypeComparison: false });
defaultAssertInstance.notDeepStrictEqual(cool, awesome);
});

test('Assert class skipPrototypeComparison with mixed types', () => {
const assertInstance = new Assert({ skipPrototypeComparison: true });

const obj1 = { value: 42, nested: { prop: 'test' } };

function CustomObj(value, nested) {
this.value = value;
this.nested = nested;
}

const obj2 = new CustomObj(42, { prop: 'test' });
assertInstance.deepStrictEqual(obj1, obj2);

assert.throws(
() => assertInstance.deepStrictEqual({ num: 42 }, { num: '42' }),
{ code: 'ERR_ASSERTION' }
);
});
}
Loading
Loading