Skip to content

Commit 85e6f99

Browse files
Barbapapazessheremet-va
andauthoredNov 13, 2024··
feat(expect): add toHaveBeenCalledAfter and toHaveBeenCalledBefore utility (#6056)
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
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

+38
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

+68
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

+33
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

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/core/test/jest-expect.test.ts

+152
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,158 @@ describe('toHaveBeenCalledExactlyOnceWith', () => {
698698
})
699699
})
700700

701+
describe('toHaveBeenCalledBefore', () => {
702+
it('success if expect mock is called before result mock', () => {
703+
const expectMock = vi.fn()
704+
const resultMock = vi.fn()
705+
706+
expectMock()
707+
resultMock()
708+
709+
expect(expectMock).toHaveBeenCalledBefore(resultMock)
710+
})
711+
712+
it('throws if expect is not a spy', () => {
713+
expect(() => {
714+
expect(1).toHaveBeenCalledBefore(vi.fn())
715+
}).toThrow(/^1 is not a spy or a call to a spy/)
716+
})
717+
718+
it('throws if result is not a spy', () => {
719+
expect(() => {
720+
expect(vi.fn()).toHaveBeenCalledBefore(1 as any)
721+
}).toThrow(/^1 is not a spy or a call to a spy/)
722+
})
723+
724+
it('throws if expect mock is called after result mock', () => {
725+
const expectMock = vi.fn()
726+
const resultMock = vi.fn()
727+
728+
resultMock()
729+
expectMock()
730+
731+
expect(() => {
732+
expect(expectMock).toHaveBeenCalledBefore(resultMock)
733+
}).toThrow(/^expected "spy" to have been called before "spy"/)
734+
})
735+
736+
it('throws with correct mock name if failed', () => {
737+
const mock1 = vi.fn().mockName('mock1')
738+
const mock2 = vi.fn().mockName('mock2')
739+
740+
mock2()
741+
mock1()
742+
743+
expect(() => {
744+
expect(mock1).toHaveBeenCalledBefore(mock2)
745+
}).toThrow(/^expected "mock1" to have been called before "mock2"/)
746+
})
747+
748+
it('fails if expect mock is not called', () => {
749+
const resultMock = vi.fn()
750+
751+
resultMock()
752+
753+
expect(() => {
754+
expect(vi.fn()).toHaveBeenCalledBefore(resultMock)
755+
}).toThrow(/^expected "spy" to have been called before "spy"/)
756+
})
757+
758+
it('not fails if expect mock is not called with option `failIfNoFirstInvocation` set to false', () => {
759+
const resultMock = vi.fn()
760+
761+
resultMock()
762+
763+
expect(vi.fn()).toHaveBeenCalledBefore(resultMock, false)
764+
})
765+
766+
it('fails if result mock is not called', () => {
767+
const expectMock = vi.fn()
768+
769+
expectMock()
770+
771+
expect(() => {
772+
expect(expectMock).toHaveBeenCalledBefore(vi.fn())
773+
}).toThrow(/^expected "spy" to have been called before "spy"/)
774+
})
775+
})
776+
777+
describe('toHaveBeenCalledAfter', () => {
778+
it('success if expect mock is called after result mock', () => {
779+
const resultMock = vi.fn()
780+
const expectMock = vi.fn()
781+
782+
resultMock()
783+
expectMock()
784+
785+
expect(expectMock).toHaveBeenCalledAfter(resultMock)
786+
})
787+
788+
it('throws if expect is not a spy', () => {
789+
expect(() => {
790+
expect(1).toHaveBeenCalledAfter(vi.fn())
791+
}).toThrow(/^1 is not a spy or a call to a spy/)
792+
})
793+
794+
it('throws if result is not a spy', () => {
795+
expect(() => {
796+
expect(vi.fn()).toHaveBeenCalledAfter(1 as any)
797+
}).toThrow(/^1 is not a spy or a call to a spy/)
798+
})
799+
800+
it('throws if expect mock is called before result mock', () => {
801+
const resultMock = vi.fn()
802+
const expectMock = vi.fn()
803+
804+
expectMock()
805+
resultMock()
806+
807+
expect(() => {
808+
expect(expectMock).toHaveBeenCalledAfter(resultMock)
809+
}).toThrow(/^expected "spy" to have been called after "spy"/)
810+
})
811+
812+
it('throws with correct mock name if failed', () => {
813+
const mock1 = vi.fn().mockName('mock1')
814+
const mock2 = vi.fn().mockName('mock2')
815+
816+
mock1()
817+
mock2()
818+
819+
expect(() => {
820+
expect(mock1).toHaveBeenCalledAfter(mock2)
821+
}).toThrow(/^expected "mock1" to have been called after "mock2"/)
822+
})
823+
824+
it('fails if result mock is not called', () => {
825+
const expectMock = vi.fn()
826+
827+
expectMock()
828+
829+
expect(() => {
830+
expect(expectMock).toHaveBeenCalledAfter(vi.fn())
831+
}).toThrow(/^expected "spy" to have been called after "spy"/)
832+
})
833+
834+
it('not fails if result mock is not called with option `failIfNoFirstInvocation` set to false', () => {
835+
const expectMock = vi.fn()
836+
837+
expectMock()
838+
839+
expect(expectMock).toHaveBeenCalledAfter(vi.fn(), false)
840+
})
841+
842+
it('fails if expect mock is not called', () => {
843+
const resultMock = vi.fn()
844+
845+
resultMock()
846+
847+
expect(() => {
848+
expect(vi.fn()).toHaveBeenCalledAfter(resultMock)
849+
}).toThrow(/^expected "spy" to have been called after "spy"/)
850+
})
851+
})
852+
701853
describe('async expect', () => {
702854
it('resolves', async () => {
703855
await expect((async () => 'true')()).resolves.toBe('true')

0 commit comments

Comments
 (0)
Please sign in to comment.