Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Fixes

- `[expect-utils]` Fix `toStrictEqual` failing on `structuredClone` results due to cross-realm constructor mismatch ([#14011](https://github.com/jestjs/jest/issues/14011))
- `[jest-runtime]` Improve CJS-from-ESM interop: `__esModule`/Babel default unwrap, broader named-export coverage, and shared CJS singleton across importers ([#16050](https://github.com/jestjs/jest/pull/16050))
- `[jest-runtime]` Load `.js` files with ESM syntax but no `"type":"module"` marker as native ESM ([#16050](https://github.com/jestjs/jest/pull/16050))
- `[jest-runtime]` Fix deadlocks and double-evaluation in concurrent ESM and wasm imports ([#16050](https://github.com/jestjs/jest/pull/16050))
Expand Down
3 changes: 3 additions & 0 deletions packages/expect-utils/src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": "../../../../tsconfig.test.json",
"include": ["./**/*"],
"compilerOptions": {
"types": ["node", "@jest/test-globals"]
},
"references": [{"path": "../../"}]
}
82 changes: 82 additions & 0 deletions packages/expect-utils/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {List, OrderedMap, OrderedSet, Record} from 'immutable';
import {stringify} from 'jest-matcher-utils';
import {createContext, runInContext} from 'vm';
import {equals} from '../jasmineUtils';
import {
arrayBufferEquality,
Expand Down Expand Up @@ -658,6 +659,87 @@ describe('typeEquality', () => {
test('returns undefined if given mock.calls and []', () => {
expect(typeEquality(jest.fn().mock.calls, [])).toBeUndefined();
});

test('returns undefined for cross-realm plain objects', () => {
const context = createContext({});
const otherRealmObject = runInContext('({key: "test"})', context);
expect(typeEquality(otherRealmObject, {key: 'test'})).toBeUndefined();
});

test('returns undefined for cross-realm Maps', () => {
const context = createContext({});
const otherRealmMap = runInContext('new Map([["key", "value"]])', context);
expect(
typeEquality(otherRealmMap, new Map([['key', 'value']])),
).toBeUndefined();
});

test('returns undefined for cross-realm Sets', () => {
const context = createContext({});
const otherRealmSet = runInContext('new Set(["test"])', context);
expect(typeEquality(otherRealmSet, new Set(['test']))).toBeUndefined();
});

test('returns undefined for cross-realm Dates', () => {
const context = createContext({});
const otherRealmDate = runInContext('new Date("2023-01-01")', context);
expect(
typeEquality(otherRealmDate, new Date('2023-01-01')),
).toBeUndefined();
});

test('returns undefined for cross-realm RegExps', () => {
const context = createContext({});
const otherRealmRegExp = runInContext('/test/gi', context);
expect(typeEquality(otherRealmRegExp, /test/gi)).toBeUndefined();
});

test('returns false for different user-defined classes with same name', () => {
class Child {
a: number;
constructor(a: number) {
this.a = a;
}
}
const OtherChild = class Child {
a: number;
constructor(a: number) {
this.a = a;
}
};
expect(typeEquality(new Child(1), new OtherChild(1))).toBe(false);
});
});

describe('iterableEquality (cross-realm)', () => {
test('returns true for cross-realm Sets with same values', () => {
const context = createContext({});
const otherRealmSet = runInContext('new Set([1, 2, 3])', context);
expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(true);
});

test('returns true for cross-realm Maps with same entries', () => {
const context = createContext({});
const otherRealmMap = runInContext(
'new Map([["a", 1], ["b", 2]])',
context,
);
expect(
iterableEquality(
otherRealmMap,
new Map([
['a', 1],
['b', 2],
]),
),
).toBe(true);
});

test('returns false for cross-realm Sets with different values', () => {
const context = createContext({});
const otherRealmSet = runInContext('new Set([1, 2])', context);
expect(iterableEquality(otherRealmSet, new Set([1, 2, 3]))).toBe(false);
});
});

describe('arrayBufferEquality', () => {
Expand Down
35 changes: 34 additions & 1 deletion packages/expect-utils/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,17 @@ export const iterableEquality = (
return undefined;
}
if (a.constructor !== b.constructor) {
return false;
// Same cross-realm constructor check as typeEquality — see #14011.
// https://github.com/jestjs/jest/issues/14011
if (
a.constructor == null ||
b.constructor == null ||
a.constructor.name !== b.constructor.name ||
!isNativeFunction(a.constructor) ||
!isNativeFunction(b.constructor)
) {
return false;
}
}
let length = aStack.length;
while (length--) {
Expand Down Expand Up @@ -392,6 +402,14 @@ export const subsetEquality = (
return subsetEqualityWithContext()(object, subset);
};

// Returns true if `fn` is a native function (its toString contains "[native code]").
function isNativeFunction(fn: unknown): boolean {
return (
typeof fn === 'function' &&
Function.prototype.toString.call(fn).includes('[native code]')
);
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const typeEquality = (a: any, b: any): boolean | undefined => {
if (
Expand All @@ -407,6 +425,21 @@ export const typeEquality = (a: any, b: any): boolean | undefined => {
return undefined;
}

// structuredClone (and other cross-realm calls) return objects whose
// constructors come from a different VM context, so identity checks fail.
// Fall back to comparing constructor names for native built-ins only —
// user-defined classes still need identity equality.
// https://github.com/jestjs/jest/issues/14011
if (
a.constructor != null &&
b.constructor != null &&
a.constructor.name === b.constructor.name &&
isNativeFunction(a.constructor) &&
isNativeFunction(b.constructor)
) {
return undefined;
}

return false;
};

Expand Down
Loading