assert: add partialDeepStrictEqual · nodejs/node@05d6227

@@ -21,22 +21,35 @@

2121

'use strict';

22222323

const {

24+

ArrayFrom,

25+

ArrayIsArray,

2426

ArrayPrototypeIndexOf,

2527

ArrayPrototypeJoin,

2628

ArrayPrototypePush,

2729

ArrayPrototypeSlice,

2830

Error,

31+

FunctionPrototypeCall,

32+

MapPrototypeDelete,

33+

MapPrototypeGet,

34+

MapPrototypeHas,

35+

MapPrototypeSet,

2936

NumberIsNaN,

3037

ObjectAssign,

3138

ObjectIs,

3239

ObjectKeys,

3340

ObjectPrototypeIsPrototypeOf,

3441

ReflectApply,

42+

ReflectHas,

43+

ReflectOwnKeys,

3544

RegExpPrototypeExec,

45+

SafeMap,

46+

SafeSet,

47+

SafeWeakSet,

3648

String,

3749

StringPrototypeIndexOf,

3850

StringPrototypeSlice,

3951

StringPrototypeSplit,

52+

SymbolIterator,

4053

} = primordials;

41544255

const {

@@ -50,8 +63,18 @@ const {

5063

} = require('internal/errors');

5164

const AssertionError = require('internal/assert/assertion_error');

5265

const { inspect } = require('internal/util/inspect');

53-

const { isPromise, isRegExp } = require('internal/util/types');

54-

const { isError, deprecate } = require('internal/util');

66+

const { Buffer } = require('buffer');

67+

const {

68+

isKeyObject,

69+

isPromise,

70+

isRegExp,

71+

isMap,

72+

isSet,

73+

isDate,

74+

isWeakSet,

75+

isWeakMap,

76+

} = require('internal/util/types');

77+

const { isError, deprecate, emitExperimentalWarning } = require('internal/util');

5578

const { innerOk } = require('internal/assert/utils');

56795780

const CallTracker = require('internal/assert/calltracker');

@@ -341,6 +364,191 @@ assert.notStrictEqual = function notStrictEqual(actual, expected, message) {

341364

}

342365

};

343366367+

function isSpecial(obj) {

368+

return obj == null || typeof obj !== 'object' || isError(obj) || isRegExp(obj) || isDate(obj);

369+

}

370+371+

const typesToCallDeepStrictEqualWith = [

372+

isKeyObject, isWeakSet, isWeakMap, Buffer.isBuffer,

373+

];

374+375+

/**

376+

* Compares two objects or values recursively to check if they are equal.

377+

* @param {any} actual - The actual value to compare.

378+

* @param {any} expected - The expected value to compare.

379+

* @param {Set} [comparedObjects=new Set()] - Set to track compared objects for handling circular references.

380+

* @returns {boolean} - Returns `true` if the actual value matches the expected value, otherwise `false`.

381+

* @example

382+

* compareBranch({a: 1, b: 2, c: 3}, {a: 1, b: 2}); // true

383+

*/

384+

function compareBranch(

385+

actual,

386+

expected,

387+

comparedObjects,

388+

) {

389+

// Check for Map object equality

390+

if (isMap(actual) && isMap(expected)) {

391+

if (actual.size !== expected.size) {

392+

return false;

393+

}

394+

const safeIterator = FunctionPrototypeCall(SafeMap.prototype[SymbolIterator], actual);

395+396+

comparedObjects ??= new SafeWeakSet();

397+398+

for (const { 0: key, 1: val } of safeIterator) {

399+

if (!MapPrototypeHas(expected, key)) {

400+

return false;

401+

}

402+

if (!compareBranch(val, MapPrototypeGet(expected, key), comparedObjects)) {

403+

return false;

404+

}

405+

}

406+

return true;

407+

}

408+409+

for (const type of typesToCallDeepStrictEqualWith) {

410+

if (type(actual) || type(expected)) {

411+

if (isDeepStrictEqual === undefined) lazyLoadComparison();

412+

return isDeepStrictEqual(actual, expected);

413+

}

414+

}

415+416+

// Check for Set object equality

417+

// TODO(aduh95): switch to `SetPrototypeIsSubsetOf` when it's available

418+

if (isSet(actual) && isSet(expected)) {

419+

if (expected.size > actual.size) {

420+

return false; // `expected` can't be a subset if it has more elements

421+

}

422+423+

if (isDeepEqual === undefined) lazyLoadComparison();

424+425+

const actualArray = ArrayFrom(actual);

426+

const expectedArray = ArrayFrom(expected);

427+

const usedIndices = new SafeSet();

428+429+

for (let expectedIdx = 0; expectedIdx < expectedArray.length; expectedIdx++) {

430+

const expectedItem = expectedArray[expectedIdx];

431+

let found = false;

432+433+

for (let actualIdx = 0; actualIdx < actualArray.length; actualIdx++) {

434+

if (!usedIndices.has(actualIdx) && isDeepStrictEqual(actualArray[actualIdx], expectedItem)) {

435+

usedIndices.add(actualIdx);

436+

found = true;

437+

break;

438+

}

439+

}

440+441+

if (!found) {

442+

return false;

443+

}

444+

}

445+446+

return true;

447+

}

448+449+

// Check if expected array is a subset of actual array

450+

if (ArrayIsArray(actual) && ArrayIsArray(expected)) {

451+

if (expected.length > actual.length) {

452+

return false;

453+

}

454+455+

if (isDeepEqual === undefined) lazyLoadComparison();

456+457+

// Create a map to count occurrences of each element in the expected array

458+

const expectedCounts = new SafeMap();

459+

for (const expectedItem of expected) {

460+

let found = false;

461+

for (const { 0: key, 1: count } of expectedCounts) {

462+

if (isDeepStrictEqual(key, expectedItem)) {

463+

MapPrototypeSet(expectedCounts, key, count + 1);

464+

found = true;

465+

break;

466+

}

467+

}

468+

if (!found) {

469+

MapPrototypeSet(expectedCounts, expectedItem, 1);

470+

}

471+

}

472+473+

// Create a map to count occurrences of relevant elements in the actual array

474+

for (const actualItem of actual) {

475+

for (const { 0: key, 1: count } of expectedCounts) {

476+

if (isDeepStrictEqual(key, actualItem)) {

477+

if (count === 1) {

478+

MapPrototypeDelete(expectedCounts, key);

479+

} else {

480+

MapPrototypeSet(expectedCounts, key, count - 1);

481+

}

482+

break;

483+

}

484+

}

485+

}

486+487+

return !expectedCounts.size;

488+

}

489+490+

// Comparison done when at least one of the values is not an object

491+

if (isSpecial(actual) || isSpecial(expected)) {

492+

if (isDeepEqual === undefined) {

493+

lazyLoadComparison();

494+

}

495+

return isDeepStrictEqual(actual, expected);

496+

}

497+498+

// Use Reflect.ownKeys() instead of Object.keys() to include symbol properties

499+

const keysExpected = ReflectOwnKeys(expected);

500+501+

comparedObjects ??= new SafeWeakSet();

502+503+

// Handle circular references

504+

if (comparedObjects.has(actual)) {

505+

return true;

506+

}

507+

comparedObjects.add(actual);

508+509+

// Check if all expected keys and values match

510+

for (let i = 0; i < keysExpected.length; i++) {

511+

const key = keysExpected[i];

512+

assert(

513+

ReflectHas(actual, key),

514+

new AssertionError({ message: `Expected key ${String(key)} not found in actual object` }),

515+

);

516+

if (!compareBranch(actual[key], expected[key], comparedObjects)) {

517+

return false;

518+

}

519+

}

520+521+

return true;

522+

}

523+524+

/**

525+

* The strict equivalence assertion test between two objects

526+

* @param {any} actual

527+

* @param {any} expected

528+

* @param {string | Error} [message]

529+

* @returns {void}

530+

*/

531+

assert.partialDeepStrictEqual = function partialDeepStrictEqual(

532+

actual,

533+

expected,

534+

message,

535+

) {

536+

emitExperimentalWarning('assert.partialDeepStrictEqual');

537+

if (arguments.length < 2) {

538+

throw new ERR_MISSING_ARGS('actual', 'expected');

539+

}

540+541+

if (!compareBranch(actual, expected)) {

542+

innerFail({

543+

actual,

544+

expected,

545+

message,

546+

operator: 'partialDeepStrictEqual',

547+

stackStartFn: partialDeepStrictEqual,

548+

});

549+

}

550+

};

551+344552

class Comparison {

345553

constructor(obj, keys, actual) {

346554

for (const key of keys) {