Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expect): add toHaveBeenCalledExactlyOnceWith expect matcher #6894

Merged
Merged
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
24 changes: 24 additions & 0 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,30 @@ test('spy function', () => {
})
```

## toHaveBeenCalledExactlyOnceWith <Version>2.2.0</Version> {#tohavebeencalledexactlyoncewith}

- **Type**: `(...args: any[]) => Awaitable<void>`

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<void>`
Expand Down
21 changes: 21 additions & 0 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down
9 changes: 9 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,15 @@ export interface Assertion<T = any>
*/
toHaveBeenCalledOnce: () => void

/**
* Ensure that a mock function is called with specific arguments and called
* exactly once.
*
* @example
* expect(mockFunc).toHaveBeenCalledExactlyOnceWith('arg1', 42);
*/
toHaveBeenCalledExactlyOnceWith: <E extends any[]>(...args: E) => void

/**
* Checks that a value satisfies a custom matcher function.
*
Expand Down
64 changes: 64 additions & 0 deletions test/core/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down