Skip to content

Commit

Permalink
feat(expect): add toHaveBeenCalledAfter and `toHaveBeenCalledBefore…
Browse files Browse the repository at this point in the history
…` utility (#6056)

Co-authored-by: Vladimir Sheremet <[email protected]>
  • Loading branch information
Barbapapazes and sheremet-va authored Nov 13, 2024
1 parent 85c64e3 commit 85e6f99
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 12 deletions.
38 changes: 38 additions & 0 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,44 @@ test('spy function', () => {
})
```

## toHaveBeenCalledBefore <Version>2.2.0</Version> {#tohavebeencalledbefore}

- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable<void>`

This assertion checks if a `Mock` was called before another `Mock`.

```ts
test('calls mock1 before mock2', () => {
const mock1 = vi.fn()
const mock2 = vi.fn()

mock1()
mock2()
mock1()

expect(mock1).toHaveBeenCalledBefore(mock2)
})
```

## toHaveBeenCalledAfter <Version>2.2.0</Version> {#tohavebeencalledafter}

- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable<void>`

This assertion checks if a `Mock` was called after another `Mock`.

```ts
test('calls mock1 after mock2', () => {
const mock1 = vi.fn()
const mock2 = vi.fn()

mock2()
mock1()
mock2()

expect(mock1).toHaveBeenCalledAfter(mock2)
})
```

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

- **Type**: `(...args: any[]) => Awaitable<void>`
Expand Down
68 changes: 68 additions & 0 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,74 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
},
)

/**
* Used for `toHaveBeenCalledBefore` and `toHaveBeenCalledAfter` to determine if the expected spy was called before the result spy.
*/
function isSpyCalledBeforeAnotherSpy(beforeSpy: MockInstance, afterSpy: MockInstance, failIfNoFirstInvocation: number): boolean {
const beforeInvocationCallOrder = beforeSpy.mock.invocationCallOrder

const afterInvocationCallOrder = afterSpy.mock.invocationCallOrder

if (beforeInvocationCallOrder.length === 0) {
return !failIfNoFirstInvocation
}

if (afterInvocationCallOrder.length === 0) {
return false
}

return beforeInvocationCallOrder[0] < afterInvocationCallOrder[0]
}

def(
['toHaveBeenCalledBefore'],
function (resultSpy: MockInstance, failIfNoFirstInvocation = true) {
const expectSpy = getSpy(this)

if (!isMockFunction(resultSpy)) {
throw new TypeError(
`${utils.inspect(resultSpy)} is not a spy or a call to a spy`,
)
}

this.assert(
isSpyCalledBeforeAnotherSpy(
expectSpy,
resultSpy,
failIfNoFirstInvocation,
),
`expected "${expectSpy.getMockName()}" to have been called before "${resultSpy.getMockName()}"`,
`expected "${expectSpy.getMockName()}" to not have been called before "${resultSpy.getMockName()}"`,
resultSpy,
expectSpy,
)
},
)
def(
['toHaveBeenCalledAfter'],
function (resultSpy: MockInstance, failIfNoFirstInvocation = true) {
const expectSpy = getSpy(this)

if (!isMockFunction(resultSpy)) {
throw new TypeError(
`${utils.inspect(resultSpy)} is not a spy or a call to a spy`,
)
}

this.assert(
isSpyCalledBeforeAnotherSpy(
resultSpy,
expectSpy,
failIfNoFirstInvocation,
),
`expected "${expectSpy.getMockName()}" to have been called after "${resultSpy.getMockName()}"`,
`expected "${expectSpy.getMockName()}" to not have been called after "${resultSpy.getMockName()}"`,
resultSpy,
expectSpy,
)
},
)
def(
['toThrow', 'toThrowError'],
function (expected?: string | Constructable | RegExp | Error) {
Expand Down
33 changes: 33 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
*/

import type { MockInstance } from '@vitest/spy'
import type { Constructable } from '@vitest/utils'
import type { Formatter } from 'tinyrainbow'
import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils'
Expand Down Expand Up @@ -655,6 +656,38 @@ export interface Assertion<T = any>
*/
toSatisfy: <E>(matcher: (value: E) => boolean, message?: string) => void

/**
* This assertion checks if a `Mock` was called before another `Mock`.
* @param mock - A mock function created by `vi.spyOn` or `vi.fn`
* @param failIfNoFirstInvocation - Fail if the first mock was never called
* @example
* const mock1 = vi.fn()
* const mock2 = vi.fn()
*
* mock1()
* mock2()
* mock1()
*
* expect(mock1).toHaveBeenCalledBefore(mock2)
*/
toHaveBeenCalledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void

/**
* This assertion checks if a `Mock` was called after another `Mock`.
* @param mock - A mock function created by `vi.spyOn` or `vi.fn`
* @param failIfNoFirstInvocation - Fail if the first mock was never called
* @example
* const mock1 = vi.fn()
* const mock2 = vi.fn()
*
* mock2()
* mock1()
* mock2()
*
* expect(mock1).toHaveBeenCalledAfter(mock2)
*/
toHaveBeenCalledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void

/**
* Checks that a promise resolves successfully at least once.
*
Expand Down
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 85e6f99

Please sign in to comment.