diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c462425b36..c1092a426b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index c0e737dc4685..31c26a85f106 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -2176,13 +2176,13 @@ exports[`.toEqual() {pass: false} expect({"a": 1, "b": 2}).toEqual(ObjectContain expect(received).toEqual(expected) // deep equality - Expected - 2 -+ Received + 2 ++ Received + 3 - ObjectContaining { - "a": 2, + Object { + "a": 1, - "b": 2, ++ "b": 2, } `; diff --git a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.ts.snap b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.ts.snap index e8b6a57a9d3c..b0cec05db543 100644 --- a/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.ts.snap +++ b/packages/expect/src/__tests__/__snapshots__/spyMatchers.test.ts.snap @@ -361,6 +361,38 @@ Received Number of calls: 3 `; +exports[`toHaveBeenCalledWith works with objectContaining 1`] = ` +expect(jest.fn()).toHaveBeenCalledWith(...expected) + +Expected: ObjectContaining {"b": 3} +Received + 1: {"a": 1, "b": 2, "c": 4} + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + +exports[`toHaveBeenCalledWith works with objectContaining 2`] = ` +expect(jest.fn()).not.toHaveBeenCalledWith(...expected) + +Expected: not ObjectContaining {"b": 7} +Received + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + +exports[`toHaveBeenCalledWith works with objectContaining 3`] = ` +expect(jest.fn()).toHaveBeenCalledWith(...expected) + +Expected: ObjectNotContaining {"c": 4} +Received + 1: {"a": 1, "b": 2, "c": 4} + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + exports[`toHaveBeenCalledWith works with trailing undefined arguments 1`] = ` expect(jest.fn()).toHaveBeenCalledWith(...expected) @@ -552,6 +584,28 @@ Received Number of calls: 3 `; +exports[`toHaveBeenLastCalledWith works with objectContaining 1`] = ` +expect(jest.fn()).toHaveBeenLastCalledWith(...expected) + +Expected: ObjectContaining {"b": 3} +Received + 1: {"a": 1, "b": 2, "c": 4} +-> 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + +exports[`toHaveBeenLastCalledWith works with objectContaining 2`] = ` +expect(jest.fn()).not.toHaveBeenLastCalledWith(...expected) + +Expected: not ObjectContaining {"b": 7} +Received + 1: {"a": 1, "b": 2, "c": 4} +-> 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + exports[`toHaveBeenLastCalledWith works with trailing undefined arguments 1`] = ` expect(jest.fn()).toHaveBeenLastCalledWith(...expected) @@ -762,6 +816,42 @@ Received: 0, ["foo", "bar"] Number of calls: 1 `; +exports[`toHaveBeenNthCalledWith works with objectContaining 1`] = ` +expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: ObjectContaining {"b": 7} +Received +-> 1: {"a": 1, "b": 2, "c": 4} + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + +exports[`toHaveBeenNthCalledWith works with objectContaining 2`] = ` +expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: not ObjectContaining {"b": 2} +Received +-> 1: {"a": 1, "b": 2, "c": 4} + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + +exports[`toHaveBeenNthCalledWith works with objectContaining 3`] = ` +expect(jest.fn()).toHaveBeenNthCalledWith(n, ...expected) + +n: 1 +Expected: ObjectNotContaining {"b": 2} +Received +-> 1: {"a": 1, "b": 2, "c": 4} + 2: {"a": 3, "b": 7, "c": 4} + +Number of calls: 2 +`; + exports[`toHaveBeenNthCalledWith works with three calls 1`] = ` expect(jest.fn()).not.toHaveBeenNthCalledWith(n, ...expected) diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index 6f636471a494..b3024bb134ab 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -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 = {}; diff --git a/packages/expect/src/__tests__/spyMatchers.test.ts b/packages/expect/src/__tests__/spyMatchers.test.ts index fd8d40d87f83..62ee5440eae7 100644 --- a/packages/expect/src/__tests__/spyMatchers.test.ts +++ b/packages/expect/src/__tests__/spyMatchers.test.ts @@ -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', () => { diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index 8eaa17d85bef..1e4c49776cb7 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -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; }