diff --git a/docs/api/expect.md b/docs/api/expect.md
index a511cbb36ee5..c4bbbd2e016c 100644
--- a/docs/api/expect.md
+++ b/docs/api/expect.md
@@ -876,6 +876,44 @@ test('spy function', () => {
})
```
+## toHaveBeenCalledBefore 2.2.0 {#tohavebeencalledbefore}
+
+- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable`
+
+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 2.2.0 {#tohavebeencalledafter}
+
+- **Type**: `(mock: MockInstance, failIfNoFirstInvocation?: boolean) => Awaitable`
+
+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 2.2.0 {#tohavebeencalledexactlyoncewith}
- **Type**: `(...args: any[]) => Awaitable`
diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts
index 6616e6f9d23a..c15f277edd9e 100644
--- a/packages/expect/src/jest-expect.ts
+++ b/packages/expect/src/jest-expect.ts
@@ -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) {
diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts
index e53b059e555e..5e1a76a897bd 100644
--- a/packages/expect/src/types.ts
+++ b/packages/expect/src/types.ts
@@ -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'
@@ -655,6 +656,38 @@ export interface Assertion
*/
toSatisfy: (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.
*
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e4a2cedd5393..5cbc1225559a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6007,8 +6007,8 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
- get-east-asian-width@1.2.0:
- resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==}
+ get-east-asian-width@1.3.0:
+ resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-intrinsic@1.2.4:
@@ -6144,8 +6144,8 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
- graphql@16.8.1:
- resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==}
+ graphql@16.9.0:
+ resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
gtoken@7.0.1:
@@ -8393,8 +8393,8 @@ packages:
streamx@2.15.1:
resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==}
- streamx@2.18.0:
- resolution: {integrity: sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==}
+ streamx@2.20.1:
+ resolution: {integrity: sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==}
strict-event-emitter@0.5.1:
resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==}
@@ -13079,7 +13079,7 @@ snapshots:
bare-stream@2.1.3:
dependencies:
- streamx: 2.18.0
+ streamx: 2.20.1
optional: true
base64-js@1.5.1: {}
@@ -14950,7 +14950,7 @@ snapshots:
get-caller-file@2.0.5: {}
- get-east-asian-width@1.2.0: {}
+ get-east-asian-width@1.3.0: {}
get-intrinsic@1.2.4:
dependencies:
@@ -15141,7 +15141,7 @@ snapshots:
graphemer@1.4.0: {}
- graphql@16.8.1: {}
+ graphql@16.9.0: {}
gtoken@7.0.1:
dependencies:
@@ -16464,7 +16464,7 @@ snapshots:
'@types/cookie': 0.6.0
'@types/statuses': 2.0.5
chalk: 4.1.2
- graphql: 16.8.1
+ graphql: 16.9.0
headers-polyfill: 4.0.3
is-node-process: 1.2.0
outvariant: 1.4.3
@@ -17726,7 +17726,7 @@ snapshots:
fast-fifo: 1.3.2
queue-tick: 1.0.1
- streamx@2.18.0:
+ streamx@2.20.1:
dependencies:
fast-fifo: 1.3.2
queue-tick: 1.0.1
@@ -17754,7 +17754,7 @@ snapshots:
string-width@7.0.0:
dependencies:
emoji-regex: 10.3.0
- get-east-asian-width: 1.2.0
+ get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string.prototype.matchall@4.0.11:
diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts
index 79ffc4874b39..4e230a1cff2d 100644
--- a/test/core/test/jest-expect.test.ts
+++ b/test/core/test/jest-expect.test.ts
@@ -698,6 +698,158 @@ describe('toHaveBeenCalledExactlyOnceWith', () => {
})
})
+describe('toHaveBeenCalledBefore', () => {
+ it('success if expect mock is called before result mock', () => {
+ const expectMock = vi.fn()
+ const resultMock = vi.fn()
+
+ expectMock()
+ resultMock()
+
+ expect(expectMock).toHaveBeenCalledBefore(resultMock)
+ })
+
+ it('throws if expect is not a spy', () => {
+ expect(() => {
+ expect(1).toHaveBeenCalledBefore(vi.fn())
+ }).toThrow(/^1 is not a spy or a call to a spy/)
+ })
+
+ it('throws if result is not a spy', () => {
+ expect(() => {
+ expect(vi.fn()).toHaveBeenCalledBefore(1 as any)
+ }).toThrow(/^1 is not a spy or a call to a spy/)
+ })
+
+ it('throws if expect mock is called after result mock', () => {
+ const expectMock = vi.fn()
+ const resultMock = vi.fn()
+
+ resultMock()
+ expectMock()
+
+ expect(() => {
+ expect(expectMock).toHaveBeenCalledBefore(resultMock)
+ }).toThrow(/^expected "spy" to have been called before "spy"/)
+ })
+
+ it('throws with correct mock name if failed', () => {
+ const mock1 = vi.fn().mockName('mock1')
+ const mock2 = vi.fn().mockName('mock2')
+
+ mock2()
+ mock1()
+
+ expect(() => {
+ expect(mock1).toHaveBeenCalledBefore(mock2)
+ }).toThrow(/^expected "mock1" to have been called before "mock2"/)
+ })
+
+ it('fails if expect mock is not called', () => {
+ const resultMock = vi.fn()
+
+ resultMock()
+
+ expect(() => {
+ expect(vi.fn()).toHaveBeenCalledBefore(resultMock)
+ }).toThrow(/^expected "spy" to have been called before "spy"/)
+ })
+
+ it('not fails if expect mock is not called with option `failIfNoFirstInvocation` set to false', () => {
+ const resultMock = vi.fn()
+
+ resultMock()
+
+ expect(vi.fn()).toHaveBeenCalledBefore(resultMock, false)
+ })
+
+ it('fails if result mock is not called', () => {
+ const expectMock = vi.fn()
+
+ expectMock()
+
+ expect(() => {
+ expect(expectMock).toHaveBeenCalledBefore(vi.fn())
+ }).toThrow(/^expected "spy" to have been called before "spy"/)
+ })
+})
+
+describe('toHaveBeenCalledAfter', () => {
+ it('success if expect mock is called after result mock', () => {
+ const resultMock = vi.fn()
+ const expectMock = vi.fn()
+
+ resultMock()
+ expectMock()
+
+ expect(expectMock).toHaveBeenCalledAfter(resultMock)
+ })
+
+ it('throws if expect is not a spy', () => {
+ expect(() => {
+ expect(1).toHaveBeenCalledAfter(vi.fn())
+ }).toThrow(/^1 is not a spy or a call to a spy/)
+ })
+
+ it('throws if result is not a spy', () => {
+ expect(() => {
+ expect(vi.fn()).toHaveBeenCalledAfter(1 as any)
+ }).toThrow(/^1 is not a spy or a call to a spy/)
+ })
+
+ it('throws if expect mock is called before result mock', () => {
+ const resultMock = vi.fn()
+ const expectMock = vi.fn()
+
+ expectMock()
+ resultMock()
+
+ expect(() => {
+ expect(expectMock).toHaveBeenCalledAfter(resultMock)
+ }).toThrow(/^expected "spy" to have been called after "spy"/)
+ })
+
+ it('throws with correct mock name if failed', () => {
+ const mock1 = vi.fn().mockName('mock1')
+ const mock2 = vi.fn().mockName('mock2')
+
+ mock1()
+ mock2()
+
+ expect(() => {
+ expect(mock1).toHaveBeenCalledAfter(mock2)
+ }).toThrow(/^expected "mock1" to have been called after "mock2"/)
+ })
+
+ it('fails if result mock is not called', () => {
+ const expectMock = vi.fn()
+
+ expectMock()
+
+ expect(() => {
+ expect(expectMock).toHaveBeenCalledAfter(vi.fn())
+ }).toThrow(/^expected "spy" to have been called after "spy"/)
+ })
+
+ it('not fails if result mock is not called with option `failIfNoFirstInvocation` set to false', () => {
+ const expectMock = vi.fn()
+
+ expectMock()
+
+ expect(expectMock).toHaveBeenCalledAfter(vi.fn(), false)
+ })
+
+ it('fails if expect mock is not called', () => {
+ const resultMock = vi.fn()
+
+ resultMock()
+
+ expect(() => {
+ expect(vi.fn()).toHaveBeenCalledAfter(resultMock)
+ }).toThrow(/^expected "spy" to have been called after "spy"/)
+ })
+})
+
describe('async expect', () => {
it('resolves', async () => {
await expect((async () => 'true')()).resolves.toBe('true')