Skip to content

Commit

Permalink
Fix calledWith(objectContaining) when there are multiple calls
Browse files Browse the repository at this point in the history
  • Loading branch information
ecraig12345 committed Feb 18, 2025
1 parent 93ca9d3 commit 6375e33
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Features

- `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164))
- `[expect]` Revert [#15038](https://github.com/jestjs/jest/pull/15038) to fix `expect(fn).toHaveBeenCalledWith(expect.objectContaining(...))` when there are multiple calls ([#15508](https://github.com/jestjs/jest/pull/15508))
- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))
- `[jest-circus]` Add a `retryImmediately` option to `jest.retryTimes` ([#14696](https://github.com/jestjs/jest/pull/14696))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2176,13 +2176,13 @@ exports[`.toEqual() {pass: false} expect({"a": 1, "b": 2}).toEqual(ObjectContain
<d>expect(</><r>received</><d>).</>toEqual<d>(</><g>expected</><d>) // deep equality</>

<g>- Expected - 2</>
<r>+ Received + 2</>
<r>+ Received + 3</>

<g>- ObjectContaining {</>
<g>- "a": 2,</>
<r>+ Object {</>
<r>+ "a": 1,</>
<d> "b": 2,</>
<r>+ "b": 2,</>
<d> }</>
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,38 @@ Received
Number of calls: <r>3</>
`;

exports[`toHaveBeenCalledWith works with objectContaining 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenCalledWith<d>(</><g>...expected</><d>)</>

Expected: <g>ObjectContaining {"b": 3}</>
Received
1: <r>{"a": 1, "b": 2, "c": 4}</>
2: <r>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenCalledWith works with objectContaining 2`] = `
<d>expect(</><r>jest.fn()</><d>).</>not<d>.</>toHaveBeenCalledWith<d>(</><g>...expected</><d>)</>

Expected: not <g>ObjectContaining {"b": 7}</>
Received
2: <d>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenCalledWith works with objectContaining 3`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenCalledWith<d>(</><g>...expected</><d>)</>

Expected: <g>ObjectNotContaining {"c": 4}</>
Received
1: <r>{"a": 1, "b": 2, "c": 4}</>
2: <r>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenCalledWith works with trailing undefined arguments 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenCalledWith<d>(</><g>...expected</><d>)</>

Expand Down Expand Up @@ -552,6 +584,28 @@ Received
Number of calls: <r>3</>
`;

exports[`toHaveBeenLastCalledWith works with objectContaining 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenLastCalledWith<d>(</><g>...expected</><d>)</>

Expected: <g>ObjectContaining {"b": 3}</>
Received
1: <r>{"a": 1, "b": 2, "c": 4}</>
-> 2: <r>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenLastCalledWith works with objectContaining 2`] = `
<d>expect(</><r>jest.fn()</><d>).</>not<d>.</>toHaveBeenLastCalledWith<d>(</><g>...expected</><d>)</>

Expected: not <g>ObjectContaining {"b": 7}</>
Received
1: <r>{"a": 1, "b": 2, "c": 4}</>
-> 2: <d>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenLastCalledWith works with trailing undefined arguments 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenLastCalledWith<d>(</><g>...expected</><d>)</>

Expand Down Expand Up @@ -762,6 +816,42 @@ Received: <r>0</>, <r>["foo", "bar"]</>
Number of calls: <r>1</>
`;

exports[`toHaveBeenNthCalledWith works with objectContaining 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenNthCalledWith<d>(</>n<d>, </><g>...expected</><d>)</>

n: 1
Expected: <g>ObjectContaining {"b": 7}</>
Received
-> 1: <r>{"a": 1, "b": 2, "c": 4}</>
2: <d>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenNthCalledWith works with objectContaining 2`] = `
<d>expect(</><r>jest.fn()</><d>).</>not<d>.</>toHaveBeenNthCalledWith<d>(</>n<d>, </><g>...expected</><d>)</>

n: 1
Expected: not <g>ObjectContaining {"b": 2}</>
Received
-> 1: <d>{"a": 1, "b": 2, "c": 4}</>
2: <r>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenNthCalledWith works with objectContaining 3`] = `
<d>expect(</><r>jest.fn()</><d>).</>toHaveBeenNthCalledWith<d>(</>n<d>, </><g>...expected</><d>)</>

n: 1
Expected: <g>ObjectNotContaining {"b": 2}</>
Received
-> 1: <r>{"a": 1, "b": 2, "c": 4}</>
2: <d>{"a": 3, "b": 7, "c": 4}</>

Number of calls: <r>2</>
`;

exports[`toHaveBeenNthCalledWith works with three calls 1`] = `
<d>expect(</><r>jest.fn()</><d>).</>not<d>.</>toHaveBeenNthCalledWith<d>(</>n<d>, </><g>...expected</><d>)</>

Expand Down
14 changes: 14 additions & 0 deletions packages/expect/src/__tests__/matchers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,20 @@ describe('.toEqual()', () => {
expect(actual).toEqual({x: 3});
});

test('objectContaining sample can be used multiple times', () => {
// This mimics what happens when there are multiple calls to a function:
// expect(mockFn).toHaveBeenCalledWith(expect.objectContaining(...))
const expected = expect.objectContaining({b: 7});
expect({a: 1, b: 2}).not.toEqual(expected);
expect({a: 3, b: 7}).toEqual(expected);
});

test('inverse objectContaining sample can be used multiple times', () => {
const expected = expect.not.objectContaining({b: 7});
expect({a: 1, b: 2}).toEqual(expected);
expect({a: 3, b: 7}).not.toEqual(expected);
});

describe('cyclic object equality', () => {
test('properties with the same circularity are equal', () => {
const a = {};
Expand Down
51 changes: 51 additions & 0 deletions packages/expect/src/__tests__/spyMatchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,57 @@ describe.each([
).toThrowErrorMatchingSnapshot();
}
});

test('works with objectContaining', () => {
const fn = jest.fn();
// Call the function twice with different objects and verify that the
// correct comparison sample is still used (original sample isn't mutated)
fn({a: 1, b: 2, c: 4});
fn({a: 3, b: 7, c: 4});

if (isToHaveNth(calledWith)) {
jestExpect(fn)[calledWith](1, jestExpect.objectContaining({b: 2}));
jestExpect(fn)[calledWith](2, jestExpect.objectContaining({b: 7}));
jestExpect(fn)[calledWith](2, jestExpect.not.objectContaining({b: 2}));

expect(() =>
jestExpect(fn)[calledWith](1, jestExpect.objectContaining({b: 7})),
).toThrowErrorMatchingSnapshot();

expect(() =>
jestExpect(fn).not[calledWith](1, jestExpect.objectContaining({b: 2})),
).toThrowErrorMatchingSnapshot();

expect(() =>
jestExpect(fn)[calledWith](1, jestExpect.not.objectContaining({b: 2})),
).toThrowErrorMatchingSnapshot();
} else {
jestExpect(fn)[calledWith](jestExpect.objectContaining({b: 7}));
jestExpect(fn)[calledWith](jestExpect.not.objectContaining({b: 3}));

// The function was never called with this value.
// Only {"b": 3} should be shown as the expected value in the snapshot
// (no extra properties in the expected value).
expect(() =>
jestExpect(fn)[calledWith](jestExpect.objectContaining({b: 3})),
).toThrowErrorMatchingSnapshot();

// Only {"b": 7} should be shown in the snapshot.
expect(() =>
jestExpect(fn).not[calledWith](jestExpect.objectContaining({b: 7})),
).toThrowErrorMatchingSnapshot();
}

if (calledWith === 'toHaveBeenCalledWith') {
// The first call had {b: 2}, so this passes.
jestExpect(fn)[calledWith](jestExpect.not.objectContaining({b: 7}));

// Only {"c": 4} should be shown in the snapshot.
expect(() =>
jestExpect(fn)[calledWith](jestExpect.not.objectContaining({c: 4})),
).toThrowErrorMatchingSnapshot();
}
});
});

describe('toHaveReturned', () => {
Expand Down
8 changes: 0 additions & 8 deletions packages/expect/src/asymmetricMatchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,19 +239,11 @@ class ObjectContaining extends AsymmetricMatcher<
const matcherContext = this.getMatcherContext();
const objectKeys = getObjectKeys(this.sample);

const otherKeys = other ? getObjectKeys(other) : [];

for (const key of objectKeys) {
if (
!hasProperty(other, key) ||
!equals(this.sample[key], other[key], matcherContext.customTesters)
) {
// Result has already been determined, mutation only affects diff output
for (const key of otherKeys) {
if (!hasProperty(this.sample, key)) {
this.sample[key] = other[key];
}
}
result = false;
break;
}
Expand Down

0 comments on commit 6375e33

Please sign in to comment.