diff --git a/docs/api/expect.md b/docs/api/expect.md index 1103ee6a15cb..282e65984587 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -876,6 +876,30 @@ test('spy function', () => { }) ``` +## toHaveBeenCalledExactlyOnceWith 2.2.0 {#tohavebeencalledexactlyoncewith} + +- **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).toHaveBeenCalledExactlyOnceWith('apples', 10) +}) +``` + ## toHaveBeenLastCalledWith - **Type**: `(...args: any[]) => Awaitable` diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index c8cb304ce193..dc16c62dafa1 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('toHaveBeenCalledExactlyOnceWith', 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..e53b059e555e 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).toHaveBeenCalledExactlyOnceWith('arg1', 42); + */ + 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 4d6bb3a0e8e7..79ffc4874b39 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -634,6 +634,70 @@ describe('toHaveBeenCalledWith', () => { }) }) +describe('toHaveBeenCalledExactlyOnceWith', () => { + describe('negated', () => { + it('fails if called', () => { + const mock = vi.fn() + mock(3) + + expect(() => { + expect(mock).not.toHaveBeenCalledExactlyOnceWith(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.toHaveBeenCalledExactlyOnceWith(3) + }) + + it('passes if not called', () => { + const mock = vi.fn() + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }) + + it('passes if called with a different argument', () => { + const mock = vi.fn() + mock(4) + + expect(mock).not.toHaveBeenCalledExactlyOnceWith(3) + }) + }) + + it('fails if not called or called too many times', () => { + const mock = vi.fn() + + expect(() => { + expect(mock).toHaveBeenCalledExactlyOnceWith(3) + }).toThrow(/^expected "spy" to be called once with arguments: \[ 3 \][^e]/) + + mock(3) + mock(3) + + expect(() => { + expect(mock).toHaveBeenCalledExactlyOnceWith(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).toHaveBeenCalledExactlyOnceWith(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).toHaveBeenCalledExactlyOnceWith(3) + }) +}) + describe('async expect', () => { it('resolves', async () => { await expect((async () => 'true')()).resolves.toBe('true')