Skip to content

Commit

Permalink
feat(expect): add toHaveBeenCalledExactlyOnceWith expect matcher (#…
Browse files Browse the repository at this point in the history
…6894)

Co-authored-by: Vladimir <[email protected]>
  • Loading branch information
jacoberdman2147 and sheremet-va authored Nov 13, 2024
1 parent 391860f commit ff66206
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 0 deletions.
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

0 comments on commit ff66206

Please sign in to comment.