From 5e8ca745a62dbeb111e9d5cdabfd147fd1ed0a78 Mon Sep 17 00:00:00 2001 From: Jacob Erdman Date: Mon, 11 Nov 2024 15:32:37 -0800 Subject: [PATCH 1/4] feat(expect): add `toHaveBeenCalledOnceWith` expect matcher --- packages/expect/src/jest-expect.ts | 21 ++++++++++ packages/expect/src/types.ts | 9 +++++ test/core/test/jest-expect.test.ts | 64 ++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c8cb304ce193..e52fb62c8851 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -595,6 +595,27 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { throw new AssertionError(formatCalls(spy, msg, args)) } }) + def('toHaveBeenCalledOnceWith', function (...args) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const callCount = spy.mock.calls.length + const hasCallWithArgs = spy.mock.calls.some(callArg => + jestEquals(callArg, args, [...customTesters, iterableEquality]), + ) + const pass = hasCallWithArgs && callCount === 1 + const isNot = utils.flag(this, 'negate') as boolean + + const msg = utils.getMessage(this, [ + pass, + `expected "${spyName}" to be called once with arguments: #{exp}`, + `expected "${spyName}" to not be called once with arguments: #{exp}`, + args, + ]) + + if ((pass && isNot) || (!pass && !isNot)) { + throw new AssertionError(formatCalls(spy, msg, args)) + } + }) def( ['toHaveBeenNthCalledWith', 'nthCalledWith'], function (times: number, ...args: any[]) { diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index c1f033437980..5856de8d4696 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -635,6 +635,15 @@ export interface Assertion */ toHaveBeenCalledOnce: () => void + /** + * Ensure that a mock function is called with specific arguments and called + * exactly once. + * + * @example + * expect(mockFunc).toHaveBeenCalledOnceWith('arg1', 42); + */ + toHaveBeenCalledOnceWith: (...args: E) => void + /** * Checks that a value satisfies a custom matcher function. * diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 4d6bb3a0e8e7..d94e150ca5ed 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -634,6 +634,70 @@ describe('toHaveBeenCalledWith', () => { }) }) +describe('toHaveBeenCalledOnceWith', () => { + describe('negated', () => { + it('fails if called', () => { + const mock = vi.fn() + mock(3) + + expect(() => { + expect(mock).not.toHaveBeenCalledOnceWith(3) + }).toThrow(/^expected "spy" to not be called once with arguments: \[ 3 \][^e]/) + }) + + it('passes if called multiple times with args', () => { + const mock = vi.fn() + mock(3) + mock(3) + + expect(mock).not.toHaveBeenCalledOnceWith(3) + }) + + it('passes if not called', () => { + const mock = vi.fn() + expect(mock).not.toHaveBeenCalledOnceWith(3) + }) + + it('passes if called with a different argument', () => { + const mock = vi.fn() + mock(4) + + expect(mock).not.toHaveBeenCalledOnceWith(3) + }) + }) + + it('fails if not called or called too many times', () => { + const mock = vi.fn() + + expect(() => { + expect(mock).toHaveBeenCalledOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + + mock(3) + mock(3) + + expect(() => { + expect(mock).toHaveBeenCalledOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + }) + + it('fails if called with wrong args', () => { + const mock = vi.fn() + mock(4) + + expect(() => { + expect(mock).toHaveBeenCalledOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + }) + + it('passes if called exactly once with args', () => { + const mock = vi.fn() + mock(3) + + expect(mock).toHaveBeenCalledOnceWith(3) + }) +}) + describe('async expect', () => { it('resolves', async () => { await expect((async () => 'true')()).resolves.toBe('true') From 5fb770e0a2686eddb792286be42b51271620cd4b Mon Sep 17 00:00:00 2001 From: Jacob Erdman Date: Mon, 11 Nov 2024 15:53:31 -0800 Subject: [PATCH 2/4] docs: add toHaveBeenCalledOnceWith to expect docs --- docs/api/expect.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/api/expect.md b/docs/api/expect.md index 1103ee6a15cb..9c369ea504d6 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,6 +876,30 @@ test('spy function', () => { }) ``` +## toHaveBeenCalledOnceWith + +- **Type**: `(...args: any[]) => Awaitable` + +This assertion checks if a function was called exactly once and with certain parameters. Requires a spy function to be passed to `expect`. + +```ts +import { expect, test, vi } from 'vitest' + +const market = { + buy(subject: string, amount: number) { + // ... + }, +} + +test('spy function', () => { + const buySpy = vi.spyOn(market, 'buy') + + market.buy('apples', 10) + + expect(buySpy).toHaveBeenCalledOnceWith('apples', 10) +}) +``` + ## toHaveBeenLastCalledWith - **Type**: `(...args: any[]) => Awaitable` From bbaf9b7d93290ccc128a7ac07e4dc9eb72bdd85a Mon Sep 17 00:00:00 2001 From: Jacob Erdman Date: Tue, 12 Nov 2024 12:29:44 -0800 Subject: [PATCH 3/4] fix: rename toHaveBeenCalledOnceWith to toHaveBeenCalledExactlyOnceWith to better fit in with the current ecosystem --- docs/api/expect.md | 4 ++-- packages/expect/src/jest-expect.ts | 2 +- packages/expect/src/types.ts | 4 ++-- test/core/test/jest-expect.test.ts | 18 +++++++++--------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index 9c369ea504d6..0e6b3f2b6770 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,7 +876,7 @@ test('spy function', () => { }) ``` -## toHaveBeenCalledOnceWith +## toHaveBeenCalledExactlyOnceWith - **Type**: `(...args: any[]) => Awaitable` @@ -896,7 +896,7 @@ test('spy function', () => { market.buy('apples', 10) - expect(buySpy).toHaveBeenCalledOnceWith('apples', 10) + expect(buySpy).toHaveBeenCalledExactlyOnceWith('apples', 10) }) ``` diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index e52fb62c8851..dc16c62dafa1 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -595,7 +595,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { throw new AssertionError(formatCalls(spy, msg, args)) } }) - def('toHaveBeenCalledOnceWith', function (...args) { + def('toHaveBeenCalledExactlyOnceWith', function (...args) { const spy = getSpy(this) const spyName = spy.getMockName() const callCount = spy.mock.calls.length diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 5856de8d4696..e53b059e555e 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -640,9 +640,9 @@ export interface Assertion * exactly once. * * @example - * expect(mockFunc).toHaveBeenCalledOnceWith('arg1', 42); + * expect(mockFunc).toHaveBeenCalledExactlyOnceWith('arg1', 42); */ - toHaveBeenCalledOnceWith: (...args: E) => void + toHaveBeenCalledExactlyOnceWith: (...args: E) => void /** * Checks that a value satisfies a custom matcher function. diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index d94e150ca5ed..79ffc4874b39 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -634,14 +634,14 @@ describe('toHaveBeenCalledWith', () => { }) }) -describe('toHaveBeenCalledOnceWith', () => { +describe('toHaveBeenCalledExactlyOnceWith', () => { describe('negated', () => { it('fails if called', () => { const mock = vi.fn() mock(3) expect(() => { - expect(mock).not.toHaveBeenCalledOnceWith(3) + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) }).toThrow(/^expected "spy" to not be called once with arguments: \[ 3 \][^e]/) }) @@ -650,19 +650,19 @@ describe('toHaveBeenCalledOnceWith', () => { mock(3) mock(3) - expect(mock).not.toHaveBeenCalledOnceWith(3) + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) }) it('passes if not called', () => { const mock = vi.fn() - expect(mock).not.toHaveBeenCalledOnceWith(3) + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) }) it('passes if called with a different argument', () => { const mock = vi.fn() mock(4) - expect(mock).not.toHaveBeenCalledOnceWith(3) + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) }) }) @@ -670,14 +670,14 @@ describe('toHaveBeenCalledOnceWith', () => { const mock = vi.fn() expect(() => { - expect(mock).toHaveBeenCalledOnceWith(3) + expect(mock).toHaveBeenCalledExactlyOnceWith(3) }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) mock(3) mock(3) expect(() => { - expect(mock).toHaveBeenCalledOnceWith(3) + expect(mock).toHaveBeenCalledExactlyOnceWith(3) }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) }) @@ -686,7 +686,7 @@ describe('toHaveBeenCalledOnceWith', () => { mock(4) expect(() => { - expect(mock).toHaveBeenCalledOnceWith(3) + expect(mock).toHaveBeenCalledExactlyOnceWith(3) }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) }) @@ -694,7 +694,7 @@ describe('toHaveBeenCalledOnceWith', () => { const mock = vi.fn() mock(3) - expect(mock).toHaveBeenCalledOnceWith(3) + expect(mock).toHaveBeenCalledExactlyOnceWith(3) }) }) From f137bbe1cdf6ebfd6947b42e9e0641a0be23f71c Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 13 Nov 2024 17:00:33 +0100 Subject: [PATCH 4/4] Apply suggestions from code review --- docs/api/expect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index 0e6b3f2b6770..282e65984587 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,7 +876,7 @@ test('spy function', () => { }) ``` -## toHaveBeenCalledExactlyOnceWith +## toHaveBeenCalledExactlyOnceWith 2.2.0 {#tohavebeencalledexactlyoncewith} - **Type**: `(...args: any[]) => Awaitable`