Skip to content

Commit 5f71018

Browse files
authoredMay 31, 2024··
feat!: add promise-based return assertions, do not auto-resolve returned promises (#5749)
1 parent 48c502f commit 5f71018

File tree

11 files changed

+446
-102
lines changed

11 files changed

+446
-102
lines changed
 

‎docs/api/expect.md

+117-2
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ test('spy function returned a value', () => {
954954

955955
- **Type**: `(amount: number) => Awaitable<void>`
956956

957-
This assertion checks if a function has successfully returned a value exact amount of times (i.e., did not throw an error). Requires a spy function to be passed to `expect`.
957+
This assertion checks if a function has successfully returned a value an exact amount of times (i.e., did not throw an error). Requires a spy function to be passed to `expect`.
958958

959959
```ts twoslash
960960
import { expect, test, vi } from 'vitest'
@@ -991,7 +991,7 @@ test('spy function returns a product', () => {
991991

992992
- **Type**: `(returnValue: any) => Awaitable<void>`
993993

994-
You can call this assertion to check if a function has successfully returned a value with certain parameters on its last invoking. Requires a spy function to be passed to `expect`.
994+
You can call this assertion to check if a function has successfully returned a certain value when it was last invoked. Requires a spy function to be passed to `expect`.
995995

996996
```ts twoslash
997997
import { expect, test, vi } from 'vitest'
@@ -1025,6 +1025,121 @@ test('spy function returns bananas on second call', () => {
10251025
})
10261026
```
10271027

1028+
## toHaveResolved
1029+
1030+
- **Type**: `() => Awaitable<void>`
1031+
1032+
This assertion checks if a function has successfully resolved a value at least once (i.e., did not reject). Requires a spy function to be passed to `expect`.
1033+
1034+
If the function returned a promise, but it was not resolved yet, this will fail.
1035+
1036+
```ts twoslash
1037+
// @filename: db/apples.js
1038+
/** @type {any} */
1039+
const db = {}
1040+
export default db
1041+
// @filename: test.ts
1042+
// ---cut---
1043+
import { expect, test, vi } from 'vitest'
1044+
import db from './db/apples.js'
1045+
1046+
async function getApplesPrice(amount: number) {
1047+
return amount * await db.get('price')
1048+
}
1049+
1050+
test('spy function resolved a value', async () => {
1051+
const getPriceSpy = vi.fn(getApplesPrice)
1052+
1053+
const price = await getPriceSpy(10)
1054+
1055+
expect(price).toBe(100)
1056+
expect(getPriceSpy).toHaveResolved()
1057+
})
1058+
```
1059+
1060+
## toHaveResolvedTimes
1061+
1062+
- **Type**: `(amount: number) => Awaitable<void>`
1063+
1064+
This assertion checks if a function has successfully resolved a value an exact amount of times (i.e., did not reject). Requires a spy function to be passed to `expect`.
1065+
1066+
This will only count resolved promises. If the function returned a promise, but it was not resolved yet, it will not be counted.
1067+
1068+
```ts twoslash
1069+
import { expect, test, vi } from 'vitest'
1070+
1071+
test('spy function resolved a value two times', async () => {
1072+
const sell = vi.fn((product: string) => Promise.resolve({ product }))
1073+
1074+
await sell('apples')
1075+
await sell('bananas')
1076+
1077+
expect(sell).toHaveResolvedTimes(2)
1078+
})
1079+
```
1080+
1081+
## toHaveResolvedWith
1082+
1083+
- **Type**: `(returnValue: any) => Awaitable<void>`
1084+
1085+
You can call this assertion to check if a function has successfully resolved a certain value at least once. Requires a spy function to be passed to `expect`.
1086+
1087+
If the function returned a promise, but it was not resolved yet, this will fail.
1088+
1089+
```ts twoslash
1090+
import { expect, test, vi } from 'vitest'
1091+
1092+
test('spy function resolved a product', async () => {
1093+
const sell = vi.fn((product: string) => Promise.resolve({ product }))
1094+
1095+
await sell('apples')
1096+
1097+
expect(sell).toHaveResolvedWith({ product: 'apples' })
1098+
})
1099+
```
1100+
1101+
## toHaveLastResolvedWith
1102+
1103+
- **Type**: `(returnValue: any) => Awaitable<void>`
1104+
1105+
You can call this assertion to check if a function has successfully resolved a certain value when it was last invoked. Requires a spy function to be passed to `expect`.
1106+
1107+
If the function returned a promise, but it was not resolved yet, this will fail.
1108+
1109+
```ts twoslash
1110+
import { expect, test, vi } from 'vitest'
1111+
1112+
test('spy function resolves bananas on a last call', async () => {
1113+
const sell = vi.fn((product: string) => Promise.resolve({ product }))
1114+
1115+
await sell('apples')
1116+
await sell('bananas')
1117+
1118+
expect(sell).toHaveLastResolvedWith({ product: 'bananas' })
1119+
})
1120+
```
1121+
1122+
## toHaveNthResolvedWith
1123+
1124+
- **Type**: `(time: number, returnValue: any) => Awaitable<void>`
1125+
1126+
You can call this assertion to check if a function has successfully resolved a certain value on a specific invokation. Requires a spy function to be passed to `expect`.
1127+
1128+
If the function returned a promise, but it was not resolved yet, this will fail.
1129+
1130+
```ts twoslash
1131+
import { expect, test, vi } from 'vitest'
1132+
1133+
test('spy function returns bananas on second call', async () => {
1134+
const sell = vi.fn((product: string) => Promise.resolve({ product }))
1135+
1136+
await sell('apples')
1137+
await sell('bananas')
1138+
1139+
expect(sell).toHaveNthResolvedWith(2, { product: 'bananas' })
1140+
})
1141+
```
1142+
10281143
## toSatisfy
10291144

10301145
- **Type:** `(predicate: (value: any) => boolean) => Awaitable<void>`

‎docs/api/mock.md

+24-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ This is an array containing all values that were `returned` from the function. O
304304
- `'return'` - function returned without throwing.
305305
- `'throw'` - function threw a value.
306306

307-
The `value` property contains the returned value or thrown error. If the function returned a promise, the `value` will be the _resolved_ value, not the actual `Promise`, unless it was never resolved.
307+
The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected.
308308

309309
```js
310310
const fn = vi.fn()
@@ -332,6 +332,29 @@ fn.mock.results === [
332332
]
333333
```
334334

335+
## mock.settledResults
336+
337+
An array containing all values that were `resolved` or `rejected` from the function.
338+
339+
This array will be empty if the function was never resolved or rejected.
340+
341+
```js
342+
const fn = vi.fn().mockResolvedValueOnce('result')
343+
344+
const result = fn()
345+
346+
fn.mock.settledResults === []
347+
348+
await result
349+
350+
fn.mock.settledResults === [
351+
{
352+
type: 'fulfilled',
353+
value: 'result',
354+
},
355+
]
356+
```
357+
335358
## mock.invocationCallOrder
336359

337360
The order of mock's execution. This returns an array of numbers that are shared between all defined mocks.

‎docs/guide/cli-table.md

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
| `--browser.provider <name>` | Provider used to run browser tests. Some browsers are only available for specific providers. Can be "webdriverio", "playwright", or the path to a custom provider. Visit [`browser.provider`](https://vitest.dev/config/#browser-provider) for more information (default: `"webdriverio"`) |
5757
| `--browser.providerOptions <options>` | Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information |
5858
| `--browser.isolate` | Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`) |
59+
| `--browser.ui` | Show Vitest UI when running tests (default: `!process.env.CI`) |
5960
| `--pool <pool>` | Specify pool, if not running in the browser (default: `threads`) |
6061
| `--poolOptions.threads.isolate` | Isolate tests in threads pool (default: `true`) |
6162
| `--poolOptions.threads.singleThread` | Run tests inside a single thread (default: `false`) |

‎packages/expect/src/jest-expect.ts

+166-79
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { assertTypes, getColors } from '@vitest/utils'
22
import type { Constructable } from '@vitest/utils'
3-
import type { MockInstance } from '@vitest/spy'
3+
import type { MockInstance, MockResult, MockSettledResult } from '@vitest/spy'
44
import { isMockFunction } from '@vitest/spy'
55
import type { Test } from '@vitest/runner'
66
import type { Assertion, ChaiPlugin } from './types'
@@ -434,12 +434,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
434434

435435
return `${i}th`
436436
}
437-
const formatCalls = (spy: MockInstance, msg: string, actualCall?: any) => {
437+
const formatCalls = (spy: MockInstance, msg: string, showActualCall?: any) => {
438438
if (spy.mock.calls) {
439439
msg += c().gray(`\n\nReceived: \n\n${spy.mock.calls.map((callArg, i) => {
440440
let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call:\n\n`)
441-
if (actualCall)
442-
methodCall += diff(actualCall, callArg, { omitAnnotationLines: true })
441+
if (showActualCall)
442+
methodCall += diff(showActualCall, callArg, { omitAnnotationLines: true })
443443
else
444444
methodCall += stringify(callArg).split('\n').map(line => ` ${line}`).join('\n')
445445
@@ -450,11 +450,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
450450
msg += c().gray(`\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`)
451451
return msg
452452
}
453-
const formatReturns = (spy: MockInstance, msg: string, actualReturn?: any) => {
454-
msg += c().gray(`\n\nReceived: \n\n${spy.mock.results.map((callReturn, i) => {
453+
const formatReturns = (spy: MockInstance, results: MockResult<any>[] | MockSettledResult<any>[], msg: string, showActualReturn?: any) => {
454+
msg += c().gray(`\n\nReceived: \n\n${results.map((callReturn, i) => {
455455
let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call return:\n\n`)
456-
if (actualReturn)
457-
methodCall += diff(actualReturn, callReturn.value, { omitAnnotationLines: true })
456+
if (showActualReturn)
457+
methodCall += diff(showActualReturn, callReturn.value, { omitAnnotationLines: true })
458458
else
459459
methodCall += stringify(callReturn).split('\n').map(line => ` ${line}`).join('\n')
460460
@@ -640,83 +640,170 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
640640

641641
throw new Error(`"toThrow" expects string, RegExp, function, Error instance or asymmetric matcher, got "${typeof expected}"`)
642642
})
643-
def(['toHaveReturned', 'toReturn'], function () {
644-
const spy = getSpy(this)
645-
const spyName = spy.getMockName()
646-
const calledAndNotThrew = spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== 'throw')
647-
this.assert(
648-
calledAndNotThrew,
649-
`expected "${spyName}" to be successfully called at least once`,
650-
`expected "${spyName}" to not be successfully called`,
651-
calledAndNotThrew,
652-
!calledAndNotThrew,
653-
false,
654-
)
655-
})
656-
def(['toHaveReturnedTimes', 'toReturnTimes'], function (times: number) {
657-
const spy = getSpy(this)
658-
const spyName = spy.getMockName()
659-
const successfulReturns = spy.mock.results.reduce((success, { type }) => type === 'throw' ? success : ++success, 0)
660-
this.assert(
661-
successfulReturns === times,
662-
`expected "${spyName}" to be successfully called ${times} times`,
663-
`expected "${spyName}" to not be successfully called ${times} times`,
664-
`expected number of returns: ${times}`,
665-
`received number of returns: ${successfulReturns}`,
666-
false,
667-
)
668-
})
669-
def(['toHaveReturnedWith', 'toReturnWith'], function (value: any) {
670-
const spy = getSpy(this)
671-
const spyName = spy.getMockName()
672-
const pass = spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result))
673-
const isNot = utils.flag(this, 'negate') as boolean
674643

675-
const msg = utils.getMessage(
676-
this,
677-
[
678-
pass,
679-
`expected "${spyName}" to return with: #{exp} at least once`,
680-
`expected "${spyName}" to not return with: #{exp}`,
681-
value,
682-
],
683-
)
644+
interface ReturnMatcher<T extends any[] = []> {
645+
name: keyof Assertion | (keyof Assertion)[]
646+
condition: (spy: MockInstance, ...args: T) => boolean
647+
action: string
648+
}
684649

685-
if ((pass && isNot) || (!pass && !isNot))
686-
throw new AssertionError(formatReturns(spy, msg, value))
650+
;([
651+
{
652+
name: 'toHaveResolved',
653+
condition: spy =>
654+
spy.mock.settledResults.length > 0
655+
&& spy.mock.settledResults.some(({ type }) => type === 'fulfilled'),
656+
action: 'resolved',
657+
},
658+
{
659+
name: ['toHaveReturned', 'toReturn'],
660+
condition: spy => spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== 'throw'),
661+
action: 'called',
662+
},
663+
] satisfies ReturnMatcher[]).forEach(({ name, condition, action }) => {
664+
def(name, function () {
665+
const spy = getSpy(this)
666+
const spyName = spy.getMockName()
667+
const pass = condition(spy)
668+
this.assert(
669+
pass,
670+
`expected "${spyName}" to be successfully ${action} at least once`,
671+
`expected "${spyName}" to not be successfully ${action}`,
672+
pass,
673+
!pass,
674+
false,
675+
)
676+
})
687677
})
688-
def(['toHaveLastReturnedWith', 'lastReturnedWith'], function (value: any) {
689-
const spy = getSpy(this)
690-
const spyName = spy.getMockName()
691-
const { value: lastResult } = spy.mock.results[spy.mock.results.length - 1]
692-
const pass = jestEquals(lastResult, value)
693-
this.assert(
694-
pass,
695-
`expected last "${spyName}" call to return #{exp}`,
696-
`expected last "${spyName}" call to not return #{exp}`,
697-
value,
698-
lastResult,
699-
)
678+
;([
679+
{
680+
name: 'toHaveResolvedTimes',
681+
condition: (spy, times) =>
682+
spy.mock.settledResults.reduce((s, { type }) => type === 'fulfilled' ? ++s : s, 0) === times,
683+
action: 'resolved',
684+
},
685+
{
686+
name: ['toHaveReturnedTimes', 'toReturnTimes'],
687+
condition: (spy, times) =>
688+
spy.mock.results.reduce((s, { type }) => type === 'throw' ? s : ++s, 0) === times,
689+
action: 'called',
690+
},
691+
] satisfies ReturnMatcher<[number]>[]).forEach(({ name, condition, action }) => {
692+
def(name, function (times: number) {
693+
const spy = getSpy(this)
694+
const spyName = spy.getMockName()
695+
const pass = condition(spy, times)
696+
this.assert(
697+
pass,
698+
`expected "${spyName}" to be successfully ${action} ${times} times`,
699+
`expected "${spyName}" to not be successfully ${action} ${times} times`,
700+
`expected resolved times: ${times}`,
701+
`received resolved times: ${pass}`,
702+
false,
703+
)
704+
})
700705
})
701-
def(['toHaveNthReturnedWith', 'nthReturnedWith'], function (nthCall: number, value: any) {
702-
const spy = getSpy(this)
703-
const spyName = spy.getMockName()
704-
const isNot = utils.flag(this, 'negate') as boolean
705-
const { type: callType, value: callResult } = spy.mock.results[nthCall - 1]
706-
const ordinalCall = `${ordinalOf(nthCall)} call`
707-
708-
if (!isNot && callType === 'throw')
709-
chai.assert.fail(`expected ${ordinalCall} to return #{exp}, but instead it threw an error`)
706+
;([
707+
{
708+
name: 'toHaveResolvedWith',
709+
condition: (spy, value) =>
710+
spy.mock.settledResults.some(({ type, value: result }) => type === 'fulfilled' && jestEquals(value, result)),
711+
action: 'resolve',
712+
},
713+
{
714+
name: ['toHaveReturnedWith', 'toReturnWith'],
715+
condition: (spy, value) =>
716+
spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result)),
717+
action: 'return',
718+
},
719+
] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => {
720+
def(name, function (value: any) {
721+
const spy = getSpy(this)
722+
const pass = condition(spy, value)
723+
const isNot = utils.flag(this, 'negate') as boolean
710724

711-
const nthCallReturn = jestEquals(callResult, value)
725+
if ((pass && isNot) || (!pass && !isNot)) {
726+
const spyName = spy.getMockName()
727+
const msg = utils.getMessage(
728+
this,
729+
[
730+
pass,
731+
`expected "${spyName}" to ${action} with: #{exp} at least once`,
732+
`expected "${spyName}" to not ${action} with: #{exp}`,
733+
value,
734+
],
735+
)
712736

713-
this.assert(
714-
nthCallReturn,
715-
`expected ${ordinalCall} "${spyName}" call to return #{exp}`,
716-
`expected ${ordinalCall} "${spyName}" call to not return #{exp}`,
717-
value,
718-
callResult,
719-
)
737+
const results = action === 'return' ? spy.mock.results : spy.mock.settledResults
738+
throw new AssertionError(formatReturns(spy, results, msg, value))
739+
}
740+
})
741+
})
742+
;([
743+
{
744+
name: 'toHaveLastResolvedWith',
745+
condition: (spy, value) => {
746+
const result = spy.mock.settledResults[spy.mock.settledResults.length - 1]
747+
return result && result.type === 'fulfilled' && jestEquals(result.value, value)
748+
},
749+
action: 'resolve',
750+
},
751+
{
752+
name: ['toHaveLastReturnedWith', 'lastReturnedWith'],
753+
condition: (spy, value) => {
754+
const result = spy.mock.results[spy.mock.results.length - 1]
755+
return result && result.type === 'return' && jestEquals(result.value, value)
756+
},
757+
action: 'return',
758+
},
759+
] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => {
760+
def(name, function (value: any) {
761+
const spy = getSpy(this)
762+
const results = action === 'return' ? spy.mock.results : spy.mock.settledResults
763+
const result = results[results.length - 1]
764+
const spyName = spy.getMockName()
765+
this.assert(
766+
condition(spy, value),
767+
`expected last "${spyName}" call to ${action} #{exp}`,
768+
`expected last "${spyName}" call to not ${action} #{exp}`,
769+
value,
770+
result?.value,
771+
)
772+
})
773+
})
774+
;([
775+
{
776+
name: 'toHaveNthResolvedWith',
777+
condition: (spy, index, value) => {
778+
const result = spy.mock.settledResults[index - 1]
779+
return result && result.type === 'fulfilled' && jestEquals(result.value, value)
780+
},
781+
action: 'resolve',
782+
},
783+
{
784+
name: ['toHaveNthReturnedWith', 'nthReturnedWith'],
785+
condition: (spy, index, value) => {
786+
const result = spy.mock.results[index - 1]
787+
return result && result.type === 'return' && jestEquals(result.value, value)
788+
},
789+
action: 'return',
790+
},
791+
] satisfies ReturnMatcher<[number, any]>[]).forEach(({ name, condition, action }) => {
792+
def(name, function (nthCall: number, value: any) {
793+
const spy = getSpy(this)
794+
const spyName = spy.getMockName()
795+
const results = action === 'return' ? spy.mock.results : spy.mock.settledResults
796+
const result = results[nthCall - 1]
797+
const ordinalCall = `${ordinalOf(nthCall)} call`
798+
799+
this.assert(
800+
condition(spy, nthCall, value),
801+
`expected ${ordinalCall} "${spyName}" call to ${action} #{exp}`,
802+
`expected ${ordinalCall} "${spyName}" call to not ${action} #{exp}`,
803+
value,
804+
result?.value,
805+
)
806+
})
720807
})
721808
def('toSatisfy', function (matcher: Function, message?: string) {
722809
return this.be.satisfy(matcher, message)

‎packages/expect/src/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ export interface Assertion<T = any> extends VitestAssertion<Chai.Assertion, T>,
178178
toHaveBeenCalledOnce: () => void
179179
toSatisfy: <E>(matcher: (value: E) => boolean, message?: string) => void
180180

181+
toHaveResolved: () => void
182+
toHaveResolvedWith: <E>(value: E) => void
183+
toHaveResolvedTimes: (times: number) => void
184+
toHaveLastResolvedWith: <E>(value: E) => void
185+
toHaveNthResolvedWith: <E>(nthCall: number, value: E) => void
186+
181187
resolves: PromisifyAssertion<T>
182188
rejects: PromisifyAssertion<T>
183189
}

‎packages/spy/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
"dev": "rollup -c --watch"
3434
},
3535
"dependencies": {
36-
"tinyspy": "^2.2.1"
36+
"tinyspy": "^3.0.0"
3737
}
3838
}

‎packages/spy/src/index.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ interface MockResultThrow {
2020
value: any
2121
}
2222

23-
type MockResult<T> = MockResultReturn<T> | MockResultThrow | MockResultIncomplete
23+
interface MockSettledResultFulfilled<T> {
24+
type: 'fulfilled'
25+
value: T
26+
}
27+
28+
interface MockSettledResultRejected {
29+
type: 'rejected'
30+
value: any
31+
}
32+
33+
export type MockResult<T> = MockResultReturn<T> | MockResultThrow | MockResultIncomplete
34+
export type MockSettledResult<T> = MockSettledResultFulfilled<T> | MockSettledResultRejected
2435

2536
export interface MockContext<TArgs, TReturns> {
2637
/**
@@ -60,7 +71,7 @@ export interface MockContext<TArgs, TReturns> {
6071
/**
6172
* This is an array containing all values that were `returned` from the function.
6273
*
63-
* The `value` property contains the returned value or thrown error. If the function returned a promise, the `value` will be the _resolved_ value, not the actual `Promise`, unless it was never resolved.
74+
* The `value` property contains the returned value or thrown error. If the function returned a `Promise`, then `result` will always be `'return'` even if the promise was rejected.
6475
*
6576
* @example
6677
* const fn = vi.fn()
@@ -86,6 +97,34 @@ export interface MockContext<TArgs, TReturns> {
8697
* ]
8798
*/
8899
results: MockResult<TReturns>[]
100+
/**
101+
* An array containing all values that were `resolved` or `rejected` from the function.
102+
*
103+
* This array will be empty if the function was never resolved or rejected.
104+
*
105+
* @example
106+
* const fn = vi.fn().mockResolvedValueOnce('result')
107+
*
108+
* const result = fn()
109+
*
110+
* fn.mock.settledResults === []
111+
* fn.mock.results === [
112+
* {
113+
* type: 'return',
114+
* value: Promise<'result'>,
115+
* },
116+
* ]
117+
*
118+
* await result
119+
*
120+
* fn.mock.settledResults === [
121+
* {
122+
* type: 'fulfilled',
123+
* value: 'result',
124+
* },
125+
* ]
126+
*/
127+
settledResults: MockSettledResult<Awaited<TReturns>>[]
89128
/**
90129
* This contains the arguments of the last call. If spy wasn't called, will return `undefined`.
91130
*/
@@ -368,7 +407,7 @@ function enhanceSpy<TArgs extends any[], TReturns>(
368407

369408
const state = tinyspy.getInternalState(spy)
370409

371-
const mockContext = {
410+
const mockContext: MockContext<TArgs, TReturns> = {
372411
get calls() {
373412
return state.calls
374413
},
@@ -380,7 +419,13 @@ function enhanceSpy<TArgs extends any[], TReturns>(
380419
},
381420
get results() {
382421
return state.results.map(([callType, value]) => {
383-
const type = callType === 'error' ? 'throw' : 'return'
422+
const type = callType === 'error' ? 'throw' as const : 'return' as const
423+
return { type, value }
424+
})
425+
},
426+
get settledResults() {
427+
return state.resolves.map(([callType, value]) => {
428+
const type = callType === 'error' ? 'rejected' as const : 'fulfilled' as const
384429
return { type, value }
385430
})
386431
},

‎packages/vitest/src/node/cli/cli-config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
345345
description: 'Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`)',
346346
},
347347
ui: {
348-
description: 'Show Vitest UI when running tests',
348+
description: 'Show Vitest UI when running tests (default: `!process.env.CI`)',
349349
},
350350
indexScripts: null,
351351
testerScripts: null,

‎pnpm-lock.yaml

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

‎test/core/test/fn.test.ts

+57
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ describe('mock', () => {
5656
expect(fn).toHaveLastReturnedWith('3')
5757
})
5858

59+
it('resolved', async () => {
60+
let i = 0
61+
62+
const fn = vitest.fn(() => Promise.resolve(String(++i)))
63+
64+
expect(fn).not.toHaveResolved()
65+
66+
await fn()
67+
68+
expect(fn).toHaveResolved()
69+
expect(fn).toHaveResolvedTimes(1)
70+
expect(fn).toHaveResolvedWith('1')
71+
72+
await fn()
73+
await fn()
74+
75+
expect(fn).toHaveResolvedTimes(3)
76+
expect(fn).toHaveNthResolvedWith(2, '2')
77+
expect(fn).toHaveLastResolvedWith('3')
78+
})
79+
5980
it('throws', () => {
6081
let i = 0
6182

@@ -91,4 +112,40 @@ describe('mock', () => {
91112
expect(fn).toHaveReturnedTimes(2)
92113
expect(fn).toHaveNthReturnedWith(3, '3')
93114
})
115+
116+
it('rejects', async () => {
117+
let i = 0
118+
119+
const fn = vitest.fn(async () => {
120+
if (i === 0) {
121+
++i
122+
throw new Error('error')
123+
}
124+
125+
return String(++i)
126+
})
127+
128+
try {
129+
await fn()
130+
}
131+
catch {}
132+
expect(fn).not.toHaveResolved()
133+
134+
await fn()
135+
expect(fn).toHaveResolved()
136+
137+
await fn()
138+
139+
try {
140+
expect(fn).toHaveNthResolvedWith(1, '1')
141+
assert.fail('expect should throw, since 1st call is thrown')
142+
}
143+
catch {}
144+
145+
// not throws
146+
expect(fn).not.toHaveNthResolvedWith(1, '1')
147+
148+
expect(fn).toHaveResolvedTimes(2)
149+
expect(fn).toHaveNthResolvedWith(3, '3')
150+
})
94151
})

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

+20-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ describe('jest mock compat layer', () => {
55

66
const r = returnFactory('return')
77
const e = returnFactory('throw')
8+
const f = returnFactory('fulfilled')
9+
const h = returnFactory('rejected')
810

911
it('works with name', () => {
1012
const spy = vi.fn()
@@ -152,12 +154,12 @@ describe('jest mock compat layer', () => {
152154
await spy()
153155
await spy()
154156

155-
expect(spy.mock.results).toEqual([
156-
r('original'),
157-
r('3-once'),
158-
r('4-once'),
159-
r('unlimited'),
160-
r('unlimited'),
157+
expect(spy.mock.settledResults).toEqual([
158+
f('original'),
159+
f('3-once'),
160+
f('4-once'),
161+
f('unlimited'),
162+
f('unlimited'),
161163
])
162164
})
163165

@@ -317,8 +319,12 @@ describe('jest mock compat layer', () => {
317319
await safeCall(spy)
318320
await safeCall(spy)
319321

320-
expect(spy.mock.results[0]).toEqual(e(new Error('once')))
321-
expect(spy.mock.results[1]).toEqual(e(new Error('error')))
322+
expect(spy.mock.results[0]).toEqual({
323+
type: 'return',
324+
value: expect.any(Promise),
325+
})
326+
expect(spy.mock.settledResults[0]).toEqual(h(new Error('once')))
327+
expect(spy.mock.settledResults[1]).toEqual(h(new Error('error')))
322328
})
323329
it('mockResolvedValue', async () => {
324330
const spy = vi.fn()
@@ -328,8 +334,12 @@ describe('jest mock compat layer', () => {
328334
await spy()
329335
await spy()
330336

331-
expect(spy.mock.results[0]).toEqual(r('once'))
332-
expect(spy.mock.results[1]).toEqual(r('resolved'))
337+
expect(spy.mock.results[0]).toEqual({
338+
type: 'return',
339+
value: expect.any(Promise),
340+
})
341+
expect(spy.mock.settledResults[0]).toEqual(f('once'))
342+
expect(spy.mock.settledResults[1]).toEqual(f('resolved'))
333343
})
334344

335345
it('tracks instances made by mocks', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.