Skip to content

Commit 85e6f99

Browse files
feat(expect): add toHaveBeenCalledAfter and toHaveBeenCalledBefore utility (#6056)
Co-authored-by: Vladimir Sheremet <[email protected]>
1 parent 85c64e3 commit 85e6f99

File tree

5 files changed

+303
-12
lines changed

5 files changed

+303
-12
lines changed

docs/api/expect.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,44 @@ test('spy function', () => {
876876
})
877877
```
878878

879+
## toHaveBeenCalledBefore <Version>2.2.0</Version> {#tohavebeencalledbefore}
880+
881+
- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable<void>`
882+
883+
This assertion checks if a `Mock` was called before another `Mock`.
884+
885+
```ts
886+
test('calls mock1 before mock2', () => {
887+
const mock1 = vi.fn()
888+
const mock2 = vi.fn()
889+
890+
mock1()
891+
mock2()
892+
mock1()
893+
894+
expect(mock1).toHaveBeenCalledBefore(mock2)
895+
})
896+
```
897+
898+
## toHaveBeenCalledAfter <Version>2.2.0</Version> {#tohavebeencalledafter}
899+
900+
- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable<void>`
901+
902+
This assertion checks if a `Mock` was called after another `Mock`.
903+
904+
```ts
905+
test('calls mock1 after mock2', () => {
906+
const mock1 = vi.fn()
907+
const mock2 = vi.fn()
908+
909+
mock2()
910+
mock1()
911+
mock2()
912+
913+
expect(mock1).toHaveBeenCalledAfter(mock2)
914+
})
915+
```
916+
879917
## toHaveBeenCalledExactlyOnceWith <Version>2.2.0</Version> {#tohavebeencalledexactlyoncewith}
880918

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

packages/expect/src/jest-expect.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,74 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
656656
)
657657
},
658658
)
659+
660+
/**
661+
* Used for `toHaveBeenCalledBefore` and `toHaveBeenCalledAfter` to determine if the expected spy was called before the result spy.
662+
*/
663+
function isSpyCalledBeforeAnotherSpy(beforeSpy: MockInstance, afterSpy: MockInstance, failIfNoFirstInvocation: number): boolean {
664+
const beforeInvocationCallOrder = beforeSpy.mock.invocationCallOrder
665+
666+
const afterInvocationCallOrder = afterSpy.mock.invocationCallOrder
667+
668+
if (beforeInvocationCallOrder.length === 0) {
669+
return !failIfNoFirstInvocation
670+
}
671+
672+
if (afterInvocationCallOrder.length === 0) {
673+
return false
674+
}
675+
676+
return beforeInvocationCallOrder[0] < afterInvocationCallOrder[0]
677+
}
678+
679+
def(
680+
['toHaveBeenCalledBefore'],
681+
function (resultSpy: MockInstance, failIfNoFirstInvocation = true) {
682+
const expectSpy = getSpy(this)
683+
684+
if (!isMockFunction(resultSpy)) {
685+
throw new TypeError(
686+
`${utils.inspect(resultSpy)} is not a spy or a call to a spy`,
687+
)
688+
}
689+
690+
this.assert(
691+
isSpyCalledBeforeAnotherSpy(
692+
expectSpy,
693+
resultSpy,
694+
failIfNoFirstInvocation,
695+
),
696+
`expected "${expectSpy.getMockName()}" to have been called before "${resultSpy.getMockName()}"`,
697+
`expected "${expectSpy.getMockName()}" to not have been called before "${resultSpy.getMockName()}"`,
698+
resultSpy,
699+
expectSpy,
700+
)
701+
},
702+
)
703+
def(
704+
['toHaveBeenCalledAfter'],
705+
function (resultSpy: MockInstance, failIfNoFirstInvocation = true) {
706+
const expectSpy = getSpy(this)
707+
708+
if (!isMockFunction(resultSpy)) {
709+
throw new TypeError(
710+
`${utils.inspect(resultSpy)} is not a spy or a call to a spy`,
711+
)
712+
}
713+
714+
this.assert(
715+
isSpyCalledBeforeAnotherSpy(
716+
resultSpy,
717+
expectSpy,
718+
failIfNoFirstInvocation,
719+
),
720+
`expected "${expectSpy.getMockName()}" to have been called after "${resultSpy.getMockName()}"`,
721+
`expected "${expectSpy.getMockName()}" to not have been called after "${resultSpy.getMockName()}"`,
722+
resultSpy,
723+
expectSpy,
724+
)
725+
},
726+
)
659727
def(
660728
['toThrow', 'toThrowError'],
661729
function (expected?: string | Constructable | RegExp | Error) {

packages/expect/src/types.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*
77
*/
88

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

659+
/**
660+
* This assertion checks if a `Mock` was called before another `Mock`.
661+
* @param mock - A mock function created by `vi.spyOn` or `vi.fn`
662+
* @param failIfNoFirstInvocation - Fail if the first mock was never called
663+
* @example
664+
* const mock1 = vi.fn()
665+
* const mock2 = vi.fn()
666+
*
667+
* mock1()
668+
* mock2()
669+
* mock1()
670+
*
671+
* expect(mock1).toHaveBeenCalledBefore(mock2)
672+
*/
673+
toHaveBeenCalledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
674+
675+
/**
676+
* This assertion checks if a `Mock` was called after another `Mock`.
677+
* @param mock - A mock function created by `vi.spyOn` or `vi.fn`
678+
* @param failIfNoFirstInvocation - Fail if the first mock was never called
679+
* @example
680+
* const mock1 = vi.fn()
681+
* const mock2 = vi.fn()
682+
*
683+
* mock2()
684+
* mock1()
685+
* mock2()
686+
*
687+
* expect(mock1).toHaveBeenCalledAfter(mock2)
688+
*/
689+
toHaveBeenCalledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
690+
658691
/**
659692
* Checks that a promise resolves successfully at least once.
660693
*

pnpm-lock.yaml

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)