Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1307f20

Browse files
committed
fix(expect-utils): handle cross-realm constructors in toStrictEqual
structuredClone returns objects whose constructors come from the outer VM context. typeEquality and iterableEquality compared constructors by identity (===), which always failed across realms — even for plain objects, Maps, Sets, Dates, and RegExps. When constructors don't match by identity, fall back to comparing constructor names, but only when both are native built-in functions. User-defined classes still require identity equality, so toStrictEqual keeps distinguishing e.g. two different classes named Child. Fixes #14011
1 parent f459dd6 commit 1307f20

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
### Fixes
99

10+
- `[expect-utils]` Fix `toStrictEqual` failing on `structuredClone` results due to cross-realm constructor mismatch ([#14011](https://github.com/jestjs/jest/issues/14011))
1011
- `[jest-mock]` Use `Symbol` from test environment ([#15858](https://github.com/jestjs/jest/pull/15858))
1112
- `[jest-reporters]` Fix issue where console output not displayed for GHA reporter even with `silent: false` option ([#15864](https://github.com/jestjs/jest/pull/15864))
1213
- `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842))

packages/expect-utils/src/__tests__/utils.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,98 @@ describe('typeEquality', () => {
658658
test('returns undefined if given mock.calls and []', () => {
659659
expect(typeEquality(jest.fn().mock.calls, [])).toBeUndefined();
660660
});
661+
662+
test('returns undefined for cross-realm plain objects', () => {
663+
const vm = require('vm');
664+
const context = vm.createContext({});
665+
const otherRealmObject = vm.runInContext('({key: "test"})', context);
666+
expect(typeEquality(otherRealmObject, {key: 'test'})).toBeUndefined();
667+
});
668+
669+
test('returns undefined for cross-realm Maps', () => {
670+
const vm = require('vm');
671+
const context = vm.createContext({});
672+
const otherRealmMap = vm.runInContext(
673+
'new Map([["key", "value"]])',
674+
context,
675+
);
676+
expect(
677+
typeEquality(otherRealmMap, new Map([['key', 'value']])),
678+
).toBeUndefined();
679+
});
680+
681+
test('returns undefined for cross-realm Sets', () => {
682+
const vm = require('vm');
683+
const context = vm.createContext({});
684+
const otherRealmSet = vm.runInContext('new Set(["test"])', context);
685+
expect(typeEquality(otherRealmSet, new Set(['test']))).toBeUndefined();
686+
});
687+
688+
test('returns undefined for cross-realm Dates', () => {
689+
const vm = require('vm');
690+
const context = vm.createContext({});
691+
const otherRealmDate = vm.runInContext('new Date("2023-01-01")', context);
692+
expect(
693+
typeEquality(otherRealmDate, new Date('2023-01-01')),
694+
).toBeUndefined();
695+
});
696+
697+
test('returns undefined for cross-realm RegExps', () => {
698+
const vm = require('vm');
699+
const context = vm.createContext({});
700+
const otherRealmRegExp = vm.runInContext('/test/gi', context);
701+
expect(typeEquality(otherRealmRegExp, /test/gi)).toBeUndefined();
702+
});
703+
704+
test('returns false for different user-defined classes with same name', () => {
705+
class Child {
706+
a: number;
707+
constructor(a: number) {
708+
this.a = a;
709+
}
710+
}
711+
const OtherChild = class Child {
712+
a: number;
713+
constructor(a: number) {
714+
this.a = a;
715+
}
716+
};
717+
expect(typeEquality(new Child(1), new OtherChild(1))).toBe(false);
718+
});
719+
});
720+
721+
describe('iterableEquality (cross-realm)', () => {
722+
test('returns true for cross-realm Sets with same values', () => {
723+
const vm = require('vm');
724+
const context = vm.createContext({});
725+
const otherRealmSet = vm.runInContext('new Set([1, 2, 3])', context);
726+
expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(true);
727+
});
728+
729+
test('returns true for cross-realm Maps with same entries', () => {
730+
const vm = require('vm');
731+
const context = vm.createContext({});
732+
const otherRealmMap = vm.runInContext(
733+
'new Map([["a", 1], ["b", 2]])',
734+
context,
735+
);
736+
expect(
737+
iterableEquality(
738+
otherRealmMap,
739+
new Map([
740+
['a', 1],
741+
['b', 2],
742+
]),
743+
),
744+
).toBe(true);
745+
});
746+
747+
test('returns false for cross-realm Sets with different values', () => {
748+
const vm = require('vm');
749+
const context = vm.createContext({});
750+
const otherRealmSet = vm.runInContext('new Set([1, 2])', context);
751+
expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(false);
752+
});
661753
});
662754

663755
describe('arrayBufferEquality', () => {

packages/expect-utils/src/utils.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,17 @@ export const iterableEquality = (
193193
return undefined;
194194
}
195195
if (a.constructor !== b.constructor) {
196-
return false;
196+
// Same cross-realm constructor check as typeEquality — see #14011.
197+
// https://github.com/jestjs/jest/issues/14011
198+
if (
199+
a.constructor == null ||
200+
b.constructor == null ||
201+
a.constructor.name !== b.constructor.name ||
202+
!isNativeFunction(a.constructor) ||
203+
!isNativeFunction(b.constructor)
204+
) {
205+
return false;
206+
}
197207
}
198208
let length = aStack.length;
199209
while (length--) {
@@ -392,6 +402,14 @@ export const subsetEquality = (
392402
return subsetEqualityWithContext()(object, subset);
393403
};
394404

405+
// Returns true if `fn` is a native function (its toString contains "[native code]").
406+
function isNativeFunction(fn: unknown): boolean {
407+
return (
408+
typeof fn === 'function' &&
409+
Function.prototype.toString.call(fn).includes('[native code]')
410+
);
411+
}
412+
395413
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
396414
export const typeEquality = (a: any, b: any): boolean | undefined => {
397415
if (
@@ -407,6 +425,21 @@ export const typeEquality = (a: any, b: any): boolean | undefined => {
407425
return undefined;
408426
}
409427

428+
// structuredClone (and other cross-realm calls) return objects whose
429+
// constructors come from a different VM context, so identity checks fail.
430+
// Fall back to comparing constructor names for native built-ins only —
431+
// user-defined classes still need identity equality.
432+
// https://github.com/jestjs/jest/issues/14011
433+
if (
434+
a.constructor != null &&
435+
b.constructor != null &&
436+
a.constructor.name === b.constructor.name &&
437+
isNativeFunction(a.constructor) &&
438+
isNativeFunction(b.constructor)
439+
) {
440+
return undefined;
441+
}
442+
410443
return false;
411444
};
412445

0 commit comments

Comments
 (0)