Skip to content
Open
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 @@ -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))
Expand Down
9 changes: 9 additions & 0 deletions packages/expect-utils/src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
168 changes: 84 additions & 84 deletions packages/expect-utils/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tester> = [
...customTesters.filter(t => t !== iterableEquality),
iterableEqualityWithStack,
];

if (a.size !== undefined) {
if (a.size !== b.size) {
return false;
} else if (isA<Set<unknown>>('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<unknown>>('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<unknown, unknown>>('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<unknown, unknown>>('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) => {
Expand Down
Loading