GeezerVison is showing github.com — lib: update isDeepStrictEqual to support options · nodejs/node@fed1dac
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/assert.md +38-1Lines changed: 38 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -227,11 +227,20 @@ The `Assert` class allows creating independent assertion instances with custom o | 227 | 227 |
| 228 | 228 | ### `new assert.Assert([options])` | 229 | 229 |
| | 230 | +<!-- YAML | | 231 | +changes: | | 232 | + - version: REPLACEME | | 233 | + pr-url: https://github.com/nodejs/node/pull/59762 | | 234 | + description: Added `skipPrototype` option. | | 235 | +--> | | 236 | + | 230 | 237 | * `options` {Object} | 231 | 238 | * `diff` {string} If set to `'full'`, shows the full diff in assertion errors. Defaults to `'simple'`. | 232 | 239 | Accepted values: `'simple'`, `'full'`. | 233 | 240 | * `strict` {boolean} If set to `true`, non-strict methods behave like their | 234 | 241 | corresponding strict methods. Defaults to `true`. | | 242 | + * `skipPrototype` {boolean} If set to `true`, skips prototype and constructor | | 243 | + comparison in deep equality checks. Defaults to `false`. | 235 | 244 |
| 236 | 245 | Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages. | 237 | 246 |
| @@ -243,7 +252,8 @@ assertInstance.deepStrictEqual({ a: 1 }, { a: 2 }); | 243 | 252 | ``` | 244 | 253 |
| 245 | 254 | **Important**: When destructuring assertion methods from an `Assert` instance, | 246 | | -the methods lose their connection to the instance's configuration options (such as `diff` and `strict` settings). | | 255 | +the methods lose their connection to the instance's configuration options (such | | 256 | +as `diff`, `strict`, and `skipPrototype` settings). | 247 | 257 | The destructured methods will fall back to default behavior instead. | 248 | 258 |
| 249 | 259 | ```js | @@ -257,6 +267,33 @@ const { strictEqual } = myAssert; | 257 | 267 | strictEqual({ a: 1 }, { b: { c: 1 } }); | 258 | 268 | ``` | 259 | 269 |
| | 270 | +The `skipPrototype` option affects all deep equality methods: | | 271 | + | | 272 | +```js | | 273 | +class Foo { | | 274 | + constructor(a) { | | 275 | + this.a = a; | | 276 | + } | | 277 | +} | | 278 | + | | 279 | +class Bar { | | 280 | + constructor(a) { | | 281 | + this.a = a; | | 282 | + } | | 283 | +} | | 284 | + | | 285 | +const foo = new Foo(1); | | 286 | +const bar = new Bar(1); | | 287 | + | | 288 | +// Default behavior - fails due to different constructors | | 289 | +const assert1 = new Assert(); | | 290 | +assert1.deepStrictEqual(foo, bar); // AssertionError | | 291 | + | | 292 | +// Skip prototype comparison - passes if properties are equal | | 293 | +const assert2 = new Assert({ skipPrototype: true }); | | 294 | +assert2.deepStrictEqual(foo, bar); // OK | | 295 | +``` | | 296 | + | 260 | 297 | When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior | 261 | 298 | (diff: 'simple', non-strict mode). | 262 | 299 | To maintain custom options when using destructured methods, avoid | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/util.md +38-1Lines changed: 38 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1551,19 +1551,56 @@ inspect.defaultOptions.maxArrayLength = null; | 1551 | 1551 | console.log(arr); // logs the full array | 1552 | 1552 | ``` | 1553 | 1553 |
| 1554 | | -## `util.isDeepStrictEqual(val1, val2)` | | 1554 | +## `util.isDeepStrictEqual(val1, val2[, options])` | 1555 | 1555 |
| 1556 | 1556 | <!-- YAML | 1557 | 1557 | added: v9.0.0 | | 1558 | +changes: | | 1559 | + - version: REPLACEME | | 1560 | + pr-url: https://github.com/nodejs/node/pull/59762 | | 1561 | + description: Added `options` parameter to allow skipping prototype comparison. | 1558 | 1562 | --> | 1559 | 1563 |
| 1560 | 1564 | * `val1` {any} | 1561 | 1565 | * `val2` {any} | | 1566 | +* `skipPrototype` {boolean} If `true`, prototype and constructor | | 1567 | + comparison is skipped during deep strict equality check. **Default:** `false`. | 1562 | 1568 | * Returns: {boolean} | 1563 | 1569 |
| 1564 | 1570 | Returns `true` if there is deep strict equality between `val1` and `val2`. | 1565 | 1571 | Otherwise, returns `false`. | 1566 | 1572 |
| | 1573 | +By default, deep strict equality includes comparison of object prototypes and | | 1574 | +constructors. When `skipPrototype` is `true`, objects with | | 1575 | +different prototypes or constructors can still be considered equal if their | | 1576 | +enumerable properties are deeply strictly equal. | | 1577 | + | | 1578 | +```js | | 1579 | +const util = require('node:util'); | | 1580 | + | | 1581 | +class Foo { | | 1582 | + constructor(a) { | | 1583 | + this.a = a; | | 1584 | + } | | 1585 | +} | | 1586 | + | | 1587 | +class Bar { | | 1588 | + constructor(a) { | | 1589 | + this.a = a; | | 1590 | + } | | 1591 | +} | | 1592 | + | | 1593 | +const foo = new Foo(1); | | 1594 | +const bar = new Bar(1); | | 1595 | + | | 1596 | +// Different constructors, same properties | | 1597 | +console.log(util.isDeepStrictEqual(foo, bar)); | | 1598 | +// false | | 1599 | + | | 1600 | +console.log(util.isDeepStrictEqual(foo, bar, true)); | | 1601 | +// true | | 1602 | +``` | | 1603 | + | 1567 | 1604 | See [`assert.deepStrictEqual()`][] for more information about deep strict | 1568 | 1605 | equality. | 1569 | 1606 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/assert.js +5-3Lines changed: 5 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -96,6 +96,8 @@ const NO_EXCEPTION_SENTINEL = {}; | 96 | 96 | * @property {'full'|'simple'} [diff='simple'] - If set to 'full', shows the full diff in assertion errors. | 97 | 97 | * @property {boolean} [strict=true] - If set to true, non-strict methods behave like their corresponding | 98 | 98 | * strict methods. | | 99 | + * @property {boolean} [skipPrototype=false] - If set to true, skips comparing prototypes | | 100 | + * in deep equality checks. | 99 | 101 | */ | 100 | 102 |
| 101 | 103 | /** | @@ -108,7 +110,7 @@ function Assert(options) { | 108 | 110 | throw new ERR_CONSTRUCT_CALL_REQUIRED('Assert'); | 109 | 111 | } | 110 | 112 |
| 111 | | - options = ObjectAssign({ __proto__: null, strict: true }, options); | | 113 | + options = ObjectAssign({ __proto__: null, strict: true, skipPrototype: false }, options); | 112 | 114 |
| 113 | 115 | const allowedDiffs = ['simple', 'full']; | 114 | 116 | if (options.diff !== undefined) { | @@ -336,7 +338,7 @@ Assert.prototype.deepStrictEqual = function deepStrictEqual(actual, expected, me | 336 | 338 | throw new ERR_MISSING_ARGS('actual', 'expected'); | 337 | 339 | } | 338 | 340 | if (isDeepEqual === undefined) lazyLoadComparison(); | 339 | | - if (!isDeepStrictEqual(actual, expected)) { | | 341 | + if (!isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) { | 340 | 342 | innerFail({ | 341 | 343 | actual, | 342 | 344 | expected, | @@ -362,7 +364,7 @@ function notDeepStrictEqual(actual, expected, message) { | 362 | 364 | throw new ERR_MISSING_ARGS('actual', 'expected'); | 363 | 365 | } | 364 | 366 | if (isDeepEqual === undefined) lazyLoadComparison(); | 365 | | - if (isDeepStrictEqual(actual, expected)) { | | 367 | + if (isDeepStrictEqual(actual, expected, this?.[kOptions]?.skipPrototype)) { | 366 | 368 | innerFail({ | 367 | 369 | actual, | 368 | 370 | expected, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/util/comparisons.js +8-4Lines changed: 8 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -126,9 +126,10 @@ const { | 126 | 126 | getOwnNonIndexProperties, | 127 | 127 | } = internalBinding('util'); | 128 | 128 |
| 129 | | -const kStrict = 1; | | 129 | +const kStrict = 2; | | 130 | +const kStrictWithoutPrototypes = 3; | 130 | 131 | const kLoose = 0; | 131 | | -const kPartial = 2; | | 132 | +const kPartial = 1; | 132 | 133 |
| 133 | 134 | const kNoIterator = 0; | 134 | 135 | const kIsArray = 1; | @@ -452,7 +453,7 @@ function keyCheck(val1, val2, mode, memos, iterationType, keys2) { | 452 | 453 | } | 453 | 454 | } else if (keys2.length !== (keys1 = ObjectKeys(val1)).length) { | 454 | 455 | return false; | 455 | | - } else if (mode === kStrict) { | | 456 | + } else if (mode === kStrict || mode === kStrictWithoutPrototypes) { | 456 | 457 | const symbolKeysA = getOwnSymbols(val1); | 457 | 458 | if (symbolKeysA.length !== 0) { | 458 | 459 | let count = 0; | @@ -1021,7 +1022,10 @@ module.exports = { | 1021 | 1022 | isDeepEqual(val1, val2) { | 1022 | 1023 | return detectCycles(val1, val2, kLoose); | 1023 | 1024 | }, | 1024 | | - isDeepStrictEqual(val1, val2) { | | 1025 | + isDeepStrictEqual(val1, val2, skipPrototype) { | | 1026 | + if (skipPrototype) { | | 1027 | + return detectCycles(val1, val2, kStrictWithoutPrototypes); | | 1028 | + } | 1025 | 1029 | return detectCycles(val1, val2, kStrict); | 1026 | 1030 | }, | 1027 | 1031 | isPartialStrictEqual(val1, val2) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/util.js +3-4Lines changed: 3 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -487,12 +487,11 @@ module.exports = { | 487 | 487 | isArray: deprecate(ArrayIsArray, | 488 | 488 | 'The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.', | 489 | 489 | 'DEP0044'), | 490 | | - isDeepStrictEqual(a, b) { | | 490 | + isDeepStrictEqual(a, b, skipPrototype) { | 491 | 491 | if (internalDeepEqual === undefined) { | 492 | | - internalDeepEqual = require('internal/util/comparisons') | 493 | | - .isDeepStrictEqual; | | 492 | + internalDeepEqual = require('internal/util/comparisons').isDeepStrictEqual; | 494 | 493 | } | 495 | | - return internalDeepEqual(a, b); | | 494 | + return internalDeepEqual(a, b, skipPrototype); | 496 | 495 | }, | 497 | 496 | promisify, | 498 | 497 | stripVTControlCharacters, | |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-assert-class.js +160Lines changed: 160 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -478,3 +478,163 @@ test('Assert class non strict with simple diff', () => { | 478 | 478 | ); | 479 | 479 | } | 480 | 480 | }); | | 481 | + | | 482 | +// Shared setup for skipPrototype tests | | 483 | +{ | | 484 | + const message = 'Expected values to be strictly deep-equal:\n' + | | 485 | + '+ actual - expected\n' + | | 486 | + '\n' + | | 487 | + ' [\n' + | | 488 | + ' 1,\n' + | | 489 | + ' 2,\n' + | | 490 | + ' 3,\n' + | | 491 | + ' 4,\n' + | | 492 | + ' 5,\n' + | | 493 | + '+ 6,\n' + | | 494 | + '- 9,\n' + | | 495 | + ' 7\n' + | | 496 | + ' ]\n'; | | 497 | + | | 498 | + function CoolClass(name) { this.name = name; } | | 499 | + | | 500 | + function AwesomeClass(name) { this.name = name; } | | 501 | + | | 502 | + class Modern { constructor(value) { this.value = value; } } | | 503 | + class Legacy { constructor(value) { this.value = value; } } | | 504 | + | | 505 | + const cool = new CoolClass('Assert is inspiring'); | | 506 | + const awesome = new AwesomeClass('Assert is inspiring'); | | 507 | + const modern = new Modern(42); | | 508 | + const legacy = new Legacy(42); | | 509 | + | | 510 | + test('Assert class strict with skipPrototype', () => { | | 511 | + const assertInstance = new Assert({ skipPrototype: true }); | | 512 | + | | 513 | + assert.throws( | | 514 | + () => assertInstance.deepEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]), | | 515 | + { message } | | 516 | + ); | | 517 | + | | 518 | + assertInstance.deepEqual(cool, awesome); | | 519 | + assertInstance.deepStrictEqual(cool, awesome); | | 520 | + assertInstance.deepEqual(modern, legacy); | | 521 | + assertInstance.deepStrictEqual(modern, legacy); | | 522 | + | | 523 | + const cool2 = new CoolClass('Soooo coooool'); | | 524 | + assert.throws( | | 525 | + () => assertInstance.deepStrictEqual(cool, cool2), | | 526 | + { code: 'ERR_ASSERTION' } | | 527 | + ); | | 528 | + | | 529 | + const nested1 = { obj: new CoolClass('test'), arr: [1, 2, 3] }; | | 530 | + const nested2 = { obj: new AwesomeClass('test'), arr: [1, 2, 3] }; | | 531 | + assertInstance.deepStrictEqual(nested1, nested2); | | 532 | + | | 533 | + const arr = new Uint8Array([1, 2, 3]); | | 534 | + const buf = Buffer.from([1, 2, 3]); | | 535 | + assertInstance.deepStrictEqual(arr, buf); | | 536 | + }); | | 537 | + | | 538 | + test('Assert class non strict with skipPrototype', () => { | | 539 | + const assertInstance = new Assert({ strict: false, skipPrototype: true }); | | 540 | + | | 541 | + assert.throws( | | 542 | + () => assertInstance.deepStrictEqual([1, 2, 3, 4, 5, 6, 7], [1, 2, 3, 4, 5, 9, 7]), | | 543 | + { message } | | 544 | + ); | | 545 | + | | 546 | + assertInstance.deepStrictEqual(cool, awesome); | | 547 | + assertInstance.deepStrictEqual(modern, legacy); | | 548 | + }); | | 549 | + | | 550 | + test('Assert class skipPrototype with complex objects', () => { | | 551 | + const assertInstance = new Assert({ skipPrototype: true }); | | 552 | + | | 553 | + function ComplexAwesomeClass(name, age) { | | 554 | + this.name = name; | | 555 | + this.age = age; | | 556 | + this.settings = { | | 557 | + theme: 'dark', | | 558 | + lang: 'en' | | 559 | + }; | | 560 | + } | | 561 | + | | 562 | + function ComplexCoolClass(name, age) { | | 563 | + this.name = name; | | 564 | + this.age = age; | | 565 | + this.settings = { | | 566 | + theme: 'dark', | | 567 | + lang: 'en' | | 568 | + }; | | 569 | + } | | 570 | + | | 571 | + const awesome1 = new ComplexAwesomeClass('Foo', 30); | | 572 | + const cool1 = new ComplexCoolClass('Foo', 30); | | 573 | + | | 574 | + assertInstance.deepStrictEqual(awesome1, cool1); | | 575 | + | | 576 | + const cool2 = new ComplexCoolClass('Foo', 30); | | 577 | + cool2.settings.theme = 'light'; | | 578 | + | | 579 | + assert.throws( | | 580 | + () => assertInstance.deepStrictEqual(awesome1, cool2), | | 581 | + { code: 'ERR_ASSERTION' } | | 582 | + ); | | 583 | + }); | | 584 | + | | 585 | + test('Assert class skipPrototype with arrays and special objects', () => { | | 586 | + const assertInstance = new Assert({ skipPrototype: true }); | | 587 | + | | 588 | + const arr1 = [1, 2, 3]; | | 589 | + const arr2 = new Array(1, 2, 3); | | 590 | + assertInstance.deepStrictEqual(arr1, arr2); | | 591 | + | | 592 | + const date1 = new Date('2023-01-01'); | | 593 | + const date2 = new Date('2023-01-01'); | | 594 | + assertInstance.deepStrictEqual(date1, date2); | | 595 | + | | 596 | + const regex1 = /test/g; | | 597 | + const regex2 = new RegExp('test', 'g'); | | 598 | + assertInstance.deepStrictEqual(regex1, regex2); | | 599 | + | | 600 | + const date3 = new Date('2023-01-02'); | | 601 | + assert.throws( | | 602 | + () => assertInstance.deepStrictEqual(date1, date3), | | 603 | + { code: 'ERR_ASSERTION' } | | 604 | + ); | | 605 | + }); | | 606 | + | | 607 | + test('Assert class skipPrototype with notDeepStrictEqual', () => { | | 608 | + const assertInstance = new Assert({ skipPrototype: true }); | | 609 | + | | 610 | + assert.throws( | | 611 | + () => assertInstance.notDeepStrictEqual(cool, awesome), | | 612 | + { code: 'ERR_ASSERTION' } | | 613 | + ); | | 614 | + | | 615 | + const notAwesome = new AwesomeClass('Not so awesome'); | | 616 | + assertInstance.notDeepStrictEqual(cool, notAwesome); | | 617 | + | | 618 | + const defaultAssertInstance = new Assert({ skipPrototype: false }); | | 619 | + defaultAssertInstance.notDeepStrictEqual(cool, awesome); | | 620 | + }); | | 621 | + | | 622 | + test('Assert class skipPrototype with mixed types', () => { | | 623 | + const assertInstance = new Assert({ skipPrototype: true }); | | 624 | + | | 625 | + const obj1 = { value: 42, nested: { prop: 'test' } }; | | 626 | + | | 627 | + function CustomObj(value, nested) { | | 628 | + this.value = value; | | 629 | + this.nested = nested; | | 630 | + } | | 631 | + | | 632 | + const obj2 = new CustomObj(42, { prop: 'test' }); | | 633 | + assertInstance.deepStrictEqual(obj1, obj2); | | 634 | + | | 635 | + assert.throws( | | 636 | + () => assertInstance.deepStrictEqual({ num: 42 }, { num: '42' }), | | 637 | + { code: 'ERR_ASSERTION' } | | 638 | + ); | | 639 | + }); | | 640 | +} |
|