diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b781a4a1b0..50ba8ee03c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixes +- `[@jest/expect-utils]` Prevent `toMatchObject`/subset matching from throwing when encountering exotic iterables (for example, objects with a TypedArray iterator) ([#14375](https://github.com/jestjs/jest/issues/14375)) - `[jest-mock]` Use `Symbol` from test environment ([#15858](https://github.com/jestjs/jest/pull/15858)) - `[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)) - `[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)) diff --git a/packages/expect-utils/src/__tests__/utils.test.ts b/packages/expect-utils/src/__tests__/utils.test.ts index 336f762f690f..6315f6e31ba0 100644 --- a/packages/expect-utils/src/__tests__/utils.test.ts +++ b/packages/expect-utils/src/__tests__/utils.test.ts @@ -652,6 +652,15 @@ describe('iterableEquality', () => { expect(iterableEquality(a, b)).toBe(false); }); + + test('does not throw when iterating an object with a TypedArray iterator', () => { + const badIterable = { + [Symbol.iterator]: Uint8Array.prototype[Symbol.iterator], + }; + + expect(() => iterableEquality(badIterable, badIterable)).not.toThrow(); + expect(iterableEquality(badIterable, badIterable)).toBe(false); + }); }); describe('typeEquality', () => { diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 5a647281f630..0c06b5eac0d8 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -208,120 +208,120 @@ export const iterableEquality = ( aStack.push(a); bStack.push(b); - const iterableEqualityWithStack = (a: any, b: any) => + // Replace any instance of iterableEquality with the new iterableEqualityWithStack + // so we can do circular detection. + const iterableEqualityWithStack = (aInner: any, bInner: any) => iterableEquality( - a, - b, + aInner, + bInner, [...filteredCustomTesters], [...aStack], [...bStack], ); - // Replace any instance of iterableEquality with the new - // iterableEqualityWithStack so we can do circular detection const filteredCustomTesters: Array = [ ...customTesters.filter(t => t !== iterableEquality), iterableEqualityWithStack, ]; - if (a.size !== undefined) { - if (a.size !== b.size) { - return false; - } else if (isA>('Set', a) || isImmutableUnorderedSet(a)) { - let allFound = true; - for (const aValue of a) { - if (!b.has(aValue)) { - let has = false; - for (const bValue of b) { - const isEqual = equals(aValue, bValue, filteredCustomTesters); - if (isEqual === true) { - has = true; + try { + if (a.size !== undefined) { + if (a.size !== b.size) { + return false; + } else if (isA>('Set', a) || isImmutableUnorderedSet(a)) { + let allFound = true; + for (const aValue of a) { + if (!b.has(aValue)) { + let has = false; + for (const bValue of b) { + const isEqual = equals(aValue, bValue, filteredCustomTesters); + if (isEqual === true) { + has = true; + } } - } - if (has === false) { - allFound = false; - break; + if (has === false) { + allFound = false; + break; + } } } - } - // Remove the first value from the stack of traversed values. - aStack.pop(); - bStack.pop(); - return allFound; - } else if ( - isA>('Map', a) || - isImmutableUnorderedKeyed(a) - ) { - let allFound = true; - for (const aEntry of a) { - if ( - !b.has(aEntry[0]) || - !equals(aEntry[1], b.get(aEntry[0]), filteredCustomTesters) - ) { - let has = false; - for (const bEntry of b) { - const matchedKey = equals( - aEntry[0], - bEntry[0], - filteredCustomTesters, - ); - - let matchedValue = false; - if (matchedKey === true) { - matchedValue = equals( - aEntry[1], - bEntry[1], + return allFound; + } else if ( + isA>('Map', a) || + isImmutableUnorderedKeyed(a) + ) { + let allFound = true; + for (const aEntry of a) { + if ( + !b.has(aEntry[0]) || + !equals(aEntry[1], b.get(aEntry[0]), filteredCustomTesters) + ) { + let has = false; + for (const bEntry of b) { + const matchedKey = equals( + aEntry[0], + bEntry[0], filteredCustomTesters, ); + + let matchedValue = false; + if (matchedKey === true) { + matchedValue = equals( + aEntry[1], + bEntry[1], + filteredCustomTesters, + ); + } + if (matchedValue === true) { + has = true; + } } - if (matchedValue === true) { - has = true; - } - } - if (has === false) { - allFound = false; - break; + if (has === false) { + allFound = false; + break; + } } } + return allFound; } - // Remove the first value from the stack of traversed values. - aStack.pop(); - bStack.pop(); - return allFound; } - } - const bIterator = b[IteratorSymbol](); + const bIterator = b[IteratorSymbol](); - for (const aValue of a) { - const nextB = bIterator.next(); - if (nextB.done || !equals(aValue, nextB.value, filteredCustomTesters)) { + for (const aValue of a) { + const nextB = bIterator.next(); + if (nextB.done || !equals(aValue, nextB.value, filteredCustomTesters)) { + return false; + } + } + if (!bIterator.next().done) { return false; } - } - if (!bIterator.next().done) { - return false; - } - if ( - !isImmutableList(a) && - !isImmutableOrderedKeyed(a) && - !isImmutableOrderedSet(a) && - !isImmutableRecord(a) - ) { - const aEntries = entries(a); - const bEntries = entries(b); - if (!equals(aEntries, bEntries)) { - return false; + if ( + !isImmutableList(a) && + !isImmutableOrderedKeyed(a) && + !isImmutableOrderedSet(a) && + !isImmutableRecord(a) + ) { + const aEntries = entries(a); + const bEntries = entries(b); + if (!equals(aEntries, bEntries)) { + return false; + } } - } - // Remove the first value from the stack of traversed values. - aStack.pop(); - bStack.pop(); - return true; + return true; + } catch { + // If an exotic iterator/getter throws (DOM objects, proxies, host objects), + // treat it as "not equal" rather than crashing the matcher. + return false; + } finally { + aStack.pop(); + bStack.pop(); + } }; const entries = (obj: any) => {