From 5f710182e0ab7f1a5ca4d8724753a9de13a96133 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 31 May 2024 14:46:23 +0200 Subject: [PATCH] feat!: add promise-based return assertions, do not auto-resolve returned promises (#5749) --- docs/api/expect.md | 119 +++++++++- docs/api/mock.md | 25 ++- docs/guide/cli-table.md | 1 + packages/expect/src/jest-expect.ts | 245 ++++++++++++++------- packages/expect/src/types.ts | 6 + packages/spy/package.json | 2 +- packages/spy/src/index.ts | 53 ++++- packages/vitest/src/node/cli/cli-config.ts | 2 +- pnpm-lock.yaml | 8 +- test/core/test/fn.test.ts | 57 +++++ test/core/test/jest-mock.test.ts | 30 ++- 11 files changed, 446 insertions(+), 102 deletions(-) diff --git a/docs/api/expect.md b/docs/api/expect.md index 98640fd69536..32b9e68240a4 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -954,7 +954,7 @@ test('spy function returned a value', () => { - **Type**: `(amount: number) => Awaitable` -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`. +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`. ```ts twoslash import { expect, test, vi } from 'vitest' @@ -991,7 +991,7 @@ test('spy function returns a product', () => { - **Type**: `(returnValue: any) => Awaitable` -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`. +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`. ```ts twoslash import { expect, test, vi } from 'vitest' @@ -1025,6 +1025,121 @@ test('spy function returns bananas on second call', () => { }) ``` +## toHaveResolved + +- **Type**: `() => Awaitable` + +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`. + +If the function returned a promise, but it was not resolved yet, this will fail. + +```ts twoslash +// @filename: db/apples.js +/** @type {any} */ +const db = {} +export default db +// @filename: test.ts +// ---cut--- +import { expect, test, vi } from 'vitest' +import db from './db/apples.js' + +async function getApplesPrice(amount: number) { + return amount * await db.get('price') +} + +test('spy function resolved a value', async () => { + const getPriceSpy = vi.fn(getApplesPrice) + + const price = await getPriceSpy(10) + + expect(price).toBe(100) + expect(getPriceSpy).toHaveResolved() +}) +``` + +## toHaveResolvedTimes + +- **Type**: `(amount: number) => Awaitable` + +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`. + +This will only count resolved promises. If the function returned a promise, but it was not resolved yet, it will not be counted. + +```ts twoslash +import { expect, test, vi } from 'vitest' + +test('spy function resolved a value two times', async () => { + const sell = vi.fn((product: string) => Promise.resolve({ product })) + + await sell('apples') + await sell('bananas') + + expect(sell).toHaveResolvedTimes(2) +}) +``` + +## toHaveResolvedWith + +- **Type**: `(returnValue: any) => Awaitable` + +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`. + +If the function returned a promise, but it was not resolved yet, this will fail. + +```ts twoslash +import { expect, test, vi } from 'vitest' + +test('spy function resolved a product', async () => { + const sell = vi.fn((product: string) => Promise.resolve({ product })) + + await sell('apples') + + expect(sell).toHaveResolvedWith({ product: 'apples' }) +}) +``` + +## toHaveLastResolvedWith + +- **Type**: `(returnValue: any) => Awaitable` + +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`. + +If the function returned a promise, but it was not resolved yet, this will fail. + +```ts twoslash +import { expect, test, vi } from 'vitest' + +test('spy function resolves bananas on a last call', async () => { + const sell = vi.fn((product: string) => Promise.resolve({ product })) + + await sell('apples') + await sell('bananas') + + expect(sell).toHaveLastResolvedWith({ product: 'bananas' }) +}) +``` + +## toHaveNthResolvedWith + +- **Type**: `(time: number, returnValue: any) => Awaitable` + +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`. + +If the function returned a promise, but it was not resolved yet, this will fail. + +```ts twoslash +import { expect, test, vi } from 'vitest' + +test('spy function returns bananas on second call', async () => { + const sell = vi.fn((product: string) => Promise.resolve({ product })) + + await sell('apples') + await sell('bananas') + + expect(sell).toHaveNthResolvedWith(2, { product: 'bananas' }) +}) +``` + ## toSatisfy - **Type:** `(predicate: (value: any) => boolean) => Awaitable` diff --git a/docs/api/mock.md b/docs/api/mock.md index 18015f4c8706..d2e3c7f8854b 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -304,7 +304,7 @@ This is an array containing all values that were `returned` from the function. O - `'return'` - function returned without throwing. - `'throw'` - function threw a value. -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. +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. ```js const fn = vi.fn() @@ -332,6 +332,29 @@ fn.mock.results === [ ] ``` +## mock.settledResults + +An array containing all values that were `resolved` or `rejected` from the function. + +This array will be empty if the function was never resolved or rejected. + +```js +const fn = vi.fn().mockResolvedValueOnce('result') + +const result = fn() + +fn.mock.settledResults === [] + +await result + +fn.mock.settledResults === [ + { + type: 'fulfilled', + value: 'result', + }, +] +``` + ## mock.invocationCallOrder The order of mock's execution. This returns an array of numbers that are shared between all defined mocks. diff --git a/docs/guide/cli-table.md b/docs/guide/cli-table.md index e3cb122406dd..0f5f56de05ae 100644 --- a/docs/guide/cli-table.md +++ b/docs/guide/cli-table.md @@ -56,6 +56,7 @@ | `--browser.provider ` | 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"`) | | `--browser.providerOptions ` | Options that are passed down to a browser provider. Visit [`browser.providerOptions`](https://vitest.dev/config/#browser-provideroptions) for more information | | `--browser.isolate` | Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`) | +| `--browser.ui` | Show Vitest UI when running tests (default: `!process.env.CI`) | | `--pool ` | Specify pool, if not running in the browser (default: `threads`) | | `--poolOptions.threads.isolate` | Isolate tests in threads pool (default: `true`) | | `--poolOptions.threads.singleThread` | Run tests inside a single thread (default: `false`) | diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 59107879df7c..5bd03978c6cb 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -1,6 +1,6 @@ import { assertTypes, getColors } from '@vitest/utils' import type { Constructable } from '@vitest/utils' -import type { MockInstance } from '@vitest/spy' +import type { MockInstance, MockResult, MockSettledResult } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' import type { Test } from '@vitest/runner' import type { Assertion, ChaiPlugin } from './types' @@ -434,12 +434,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return `${i}th` } - const formatCalls = (spy: MockInstance, msg: string, actualCall?: any) => { + const formatCalls = (spy: MockInstance, msg: string, showActualCall?: any) => { if (spy.mock.calls) { msg += c().gray(`\n\nReceived: \n\n${spy.mock.calls.map((callArg, i) => { let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call:\n\n`) - if (actualCall) - methodCall += diff(actualCall, callArg, { omitAnnotationLines: true }) + if (showActualCall) + methodCall += diff(showActualCall, callArg, { omitAnnotationLines: true }) else methodCall += stringify(callArg).split('\n').map(line => ` ${line}`).join('\n') @@ -450,11 +450,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { msg += c().gray(`\n\nNumber of calls: ${c().bold(spy.mock.calls.length)}\n`) return msg } - const formatReturns = (spy: MockInstance, msg: string, actualReturn?: any) => { - msg += c().gray(`\n\nReceived: \n\n${spy.mock.results.map((callReturn, i) => { + const formatReturns = (spy: MockInstance, results: MockResult[] | MockSettledResult[], msg: string, showActualReturn?: any) => { + msg += c().gray(`\n\nReceived: \n\n${results.map((callReturn, i) => { let methodCall = c().bold(` ${ordinalOf(i + 1)} ${spy.getMockName()} call return:\n\n`) - if (actualReturn) - methodCall += diff(actualReturn, callReturn.value, { omitAnnotationLines: true }) + if (showActualReturn) + methodCall += diff(showActualReturn, callReturn.value, { omitAnnotationLines: true }) else methodCall += stringify(callReturn).split('\n').map(line => ` ${line}`).join('\n') @@ -640,83 +640,170 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { throw new Error(`"toThrow" expects string, RegExp, function, Error instance or asymmetric matcher, got "${typeof expected}"`) }) - def(['toHaveReturned', 'toReturn'], function () { - const spy = getSpy(this) - const spyName = spy.getMockName() - const calledAndNotThrew = spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== 'throw') - this.assert( - calledAndNotThrew, - `expected "${spyName}" to be successfully called at least once`, - `expected "${spyName}" to not be successfully called`, - calledAndNotThrew, - !calledAndNotThrew, - false, - ) - }) - def(['toHaveReturnedTimes', 'toReturnTimes'], function (times: number) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const successfulReturns = spy.mock.results.reduce((success, { type }) => type === 'throw' ? success : ++success, 0) - this.assert( - successfulReturns === times, - `expected "${spyName}" to be successfully called ${times} times`, - `expected "${spyName}" to not be successfully called ${times} times`, - `expected number of returns: ${times}`, - `received number of returns: ${successfulReturns}`, - false, - ) - }) - def(['toHaveReturnedWith', 'toReturnWith'], function (value: any) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const pass = spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result)) - const isNot = utils.flag(this, 'negate') as boolean - const msg = utils.getMessage( - this, - [ - pass, - `expected "${spyName}" to return with: #{exp} at least once`, - `expected "${spyName}" to not return with: #{exp}`, - value, - ], - ) + interface ReturnMatcher { + name: keyof Assertion | (keyof Assertion)[] + condition: (spy: MockInstance, ...args: T) => boolean + action: string + } - if ((pass && isNot) || (!pass && !isNot)) - throw new AssertionError(formatReturns(spy, msg, value)) + ;([ + { + name: 'toHaveResolved', + condition: spy => + spy.mock.settledResults.length > 0 + && spy.mock.settledResults.some(({ type }) => type === 'fulfilled'), + action: 'resolved', + }, + { + name: ['toHaveReturned', 'toReturn'], + condition: spy => spy.mock.calls.length > 0 && spy.mock.results.some(({ type }) => type !== 'throw'), + action: 'called', + }, + ] satisfies ReturnMatcher[]).forEach(({ name, condition, action }) => { + def(name, function () { + const spy = getSpy(this) + const spyName = spy.getMockName() + const pass = condition(spy) + this.assert( + pass, + `expected "${spyName}" to be successfully ${action} at least once`, + `expected "${spyName}" to not be successfully ${action}`, + pass, + !pass, + false, + ) + }) }) - def(['toHaveLastReturnedWith', 'lastReturnedWith'], function (value: any) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const { value: lastResult } = spy.mock.results[spy.mock.results.length - 1] - const pass = jestEquals(lastResult, value) - this.assert( - pass, - `expected last "${spyName}" call to return #{exp}`, - `expected last "${spyName}" call to not return #{exp}`, - value, - lastResult, - ) + ;([ + { + name: 'toHaveResolvedTimes', + condition: (spy, times) => + spy.mock.settledResults.reduce((s, { type }) => type === 'fulfilled' ? ++s : s, 0) === times, + action: 'resolved', + }, + { + name: ['toHaveReturnedTimes', 'toReturnTimes'], + condition: (spy, times) => + spy.mock.results.reduce((s, { type }) => type === 'throw' ? s : ++s, 0) === times, + action: 'called', + }, + ] satisfies ReturnMatcher<[number]>[]).forEach(({ name, condition, action }) => { + def(name, function (times: number) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const pass = condition(spy, times) + this.assert( + pass, + `expected "${spyName}" to be successfully ${action} ${times} times`, + `expected "${spyName}" to not be successfully ${action} ${times} times`, + `expected resolved times: ${times}`, + `received resolved times: ${pass}`, + false, + ) + }) }) - def(['toHaveNthReturnedWith', 'nthReturnedWith'], function (nthCall: number, value: any) { - const spy = getSpy(this) - const spyName = spy.getMockName() - const isNot = utils.flag(this, 'negate') as boolean - const { type: callType, value: callResult } = spy.mock.results[nthCall - 1] - const ordinalCall = `${ordinalOf(nthCall)} call` - - if (!isNot && callType === 'throw') - chai.assert.fail(`expected ${ordinalCall} to return #{exp}, but instead it threw an error`) + ;([ + { + name: 'toHaveResolvedWith', + condition: (spy, value) => + spy.mock.settledResults.some(({ type, value: result }) => type === 'fulfilled' && jestEquals(value, result)), + action: 'resolve', + }, + { + name: ['toHaveReturnedWith', 'toReturnWith'], + condition: (spy, value) => + spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result)), + action: 'return', + }, + ] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => { + def(name, function (value: any) { + const spy = getSpy(this) + const pass = condition(spy, value) + const isNot = utils.flag(this, 'negate') as boolean - const nthCallReturn = jestEquals(callResult, value) + if ((pass && isNot) || (!pass && !isNot)) { + const spyName = spy.getMockName() + const msg = utils.getMessage( + this, + [ + pass, + `expected "${spyName}" to ${action} with: #{exp} at least once`, + `expected "${spyName}" to not ${action} with: #{exp}`, + value, + ], + ) - this.assert( - nthCallReturn, - `expected ${ordinalCall} "${spyName}" call to return #{exp}`, - `expected ${ordinalCall} "${spyName}" call to not return #{exp}`, - value, - callResult, - ) + const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + throw new AssertionError(formatReturns(spy, results, msg, value)) + } + }) + }) + ;([ + { + name: 'toHaveLastResolvedWith', + condition: (spy, value) => { + const result = spy.mock.settledResults[spy.mock.settledResults.length - 1] + return result && result.type === 'fulfilled' && jestEquals(result.value, value) + }, + action: 'resolve', + }, + { + name: ['toHaveLastReturnedWith', 'lastReturnedWith'], + condition: (spy, value) => { + const result = spy.mock.results[spy.mock.results.length - 1] + return result && result.type === 'return' && jestEquals(result.value, value) + }, + action: 'return', + }, + ] satisfies ReturnMatcher<[any]>[]).forEach(({ name, condition, action }) => { + def(name, function (value: any) { + const spy = getSpy(this) + const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + const result = results[results.length - 1] + const spyName = spy.getMockName() + this.assert( + condition(spy, value), + `expected last "${spyName}" call to ${action} #{exp}`, + `expected last "${spyName}" call to not ${action} #{exp}`, + value, + result?.value, + ) + }) + }) + ;([ + { + name: 'toHaveNthResolvedWith', + condition: (spy, index, value) => { + const result = spy.mock.settledResults[index - 1] + return result && result.type === 'fulfilled' && jestEquals(result.value, value) + }, + action: 'resolve', + }, + { + name: ['toHaveNthReturnedWith', 'nthReturnedWith'], + condition: (spy, index, value) => { + const result = spy.mock.results[index - 1] + return result && result.type === 'return' && jestEquals(result.value, value) + }, + action: 'return', + }, + ] satisfies ReturnMatcher<[number, any]>[]).forEach(({ name, condition, action }) => { + def(name, function (nthCall: number, value: any) { + const spy = getSpy(this) + const spyName = spy.getMockName() + const results = action === 'return' ? spy.mock.results : spy.mock.settledResults + const result = results[nthCall - 1] + const ordinalCall = `${ordinalOf(nthCall)} call` + + this.assert( + condition(spy, nthCall, value), + `expected ${ordinalCall} "${spyName}" call to ${action} #{exp}`, + `expected ${ordinalCall} "${spyName}" call to not ${action} #{exp}`, + value, + result?.value, + ) + }) }) def('toSatisfy', function (matcher: Function, message?: string) { return this.be.satisfy(matcher, message) diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 5afc34018451..1df31519ef15 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -178,6 +178,12 @@ export interface Assertion extends VitestAssertion, toHaveBeenCalledOnce: () => void toSatisfy: (matcher: (value: E) => boolean, message?: string) => void + toHaveResolved: () => void + toHaveResolvedWith: (value: E) => void + toHaveResolvedTimes: (times: number) => void + toHaveLastResolvedWith: (value: E) => void + toHaveNthResolvedWith: (nthCall: number, value: E) => void + resolves: PromisifyAssertion rejects: PromisifyAssertion } diff --git a/packages/spy/package.json b/packages/spy/package.json index b4bf1e9ee519..0405fcc48ac6 100644 --- a/packages/spy/package.json +++ b/packages/spy/package.json @@ -33,6 +33,6 @@ "dev": "rollup -c --watch" }, "dependencies": { - "tinyspy": "^2.2.1" + "tinyspy": "^3.0.0" } } diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 6e3315b56e00..fad7c9dffec6 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -20,7 +20,18 @@ interface MockResultThrow { value: any } -type MockResult = MockResultReturn | MockResultThrow | MockResultIncomplete +interface MockSettledResultFulfilled { + type: 'fulfilled' + value: T +} + +interface MockSettledResultRejected { + type: 'rejected' + value: any +} + +export type MockResult = MockResultReturn | MockResultThrow | MockResultIncomplete +export type MockSettledResult = MockSettledResultFulfilled | MockSettledResultRejected export interface MockContext { /** @@ -60,7 +71,7 @@ export interface MockContext { /** * This is an array containing all values that were `returned` from the function. * - * 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. + * 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. * * @example * const fn = vi.fn() @@ -86,6 +97,34 @@ export interface MockContext { * ] */ results: MockResult[] + /** + * An array containing all values that were `resolved` or `rejected` from the function. + * + * This array will be empty if the function was never resolved or rejected. + * + * @example + * const fn = vi.fn().mockResolvedValueOnce('result') + * + * const result = fn() + * + * fn.mock.settledResults === [] + * fn.mock.results === [ + * { + * type: 'return', + * value: Promise<'result'>, + * }, + * ] + * + * await result + * + * fn.mock.settledResults === [ + * { + * type: 'fulfilled', + * value: 'result', + * }, + * ] + */ + settledResults: MockSettledResult>[] /** * This contains the arguments of the last call. If spy wasn't called, will return `undefined`. */ @@ -368,7 +407,7 @@ function enhanceSpy( const state = tinyspy.getInternalState(spy) - const mockContext = { + const mockContext: MockContext = { get calls() { return state.calls }, @@ -380,7 +419,13 @@ function enhanceSpy( }, get results() { return state.results.map(([callType, value]) => { - const type = callType === 'error' ? 'throw' : 'return' + const type = callType === 'error' ? 'throw' as const : 'return' as const + return { type, value } + }) + }, + get settledResults() { + return state.resolves.map(([callType, value]) => { + const type = callType === 'error' ? 'rejected' as const : 'fulfilled' as const return { type, value } }) }, diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 25f4ccda83d0..6b6e6f28a5ee 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -345,7 +345,7 @@ export const cliOptionsConfig: VitestCLIOptions = { description: 'Run every browser test file in isolation. To disable isolation, use `--browser.isolate=false` (default: `true`)', }, ui: { - description: 'Show Vitest UI when running tests', + description: 'Show Vitest UI when running tests (default: `!process.env.CI`)', }, indexScripts: null, testerScripts: null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 86bf5a7e6c7a..59cbaee9cbb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -690,8 +690,8 @@ importers: packages/spy: dependencies: tinyspy: - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^3.0.0 + version: 3.0.0 packages/ui: dependencies: @@ -14893,8 +14893,8 @@ packages: engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + /tinyspy@3.0.0: + resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==} engines: {node: '>=14.0.0'} dev: false diff --git a/test/core/test/fn.test.ts b/test/core/test/fn.test.ts index 433312de084e..a6b41cbb3d90 100644 --- a/test/core/test/fn.test.ts +++ b/test/core/test/fn.test.ts @@ -56,6 +56,27 @@ describe('mock', () => { expect(fn).toHaveLastReturnedWith('3') }) + it('resolved', async () => { + let i = 0 + + const fn = vitest.fn(() => Promise.resolve(String(++i))) + + expect(fn).not.toHaveResolved() + + await fn() + + expect(fn).toHaveResolved() + expect(fn).toHaveResolvedTimes(1) + expect(fn).toHaveResolvedWith('1') + + await fn() + await fn() + + expect(fn).toHaveResolvedTimes(3) + expect(fn).toHaveNthResolvedWith(2, '2') + expect(fn).toHaveLastResolvedWith('3') + }) + it('throws', () => { let i = 0 @@ -91,4 +112,40 @@ describe('mock', () => { expect(fn).toHaveReturnedTimes(2) expect(fn).toHaveNthReturnedWith(3, '3') }) + + it('rejects', async () => { + let i = 0 + + const fn = vitest.fn(async () => { + if (i === 0) { + ++i + throw new Error('error') + } + + return String(++i) + }) + + try { + await fn() + } + catch {} + expect(fn).not.toHaveResolved() + + await fn() + expect(fn).toHaveResolved() + + await fn() + + try { + expect(fn).toHaveNthResolvedWith(1, '1') + assert.fail('expect should throw, since 1st call is thrown') + } + catch {} + + // not throws + expect(fn).not.toHaveNthResolvedWith(1, '1') + + expect(fn).toHaveResolvedTimes(2) + expect(fn).toHaveNthResolvedWith(3, '3') + }) }) diff --git a/test/core/test/jest-mock.test.ts b/test/core/test/jest-mock.test.ts index f6c2dd479d3c..00b63d7b10d0 100644 --- a/test/core/test/jest-mock.test.ts +++ b/test/core/test/jest-mock.test.ts @@ -5,6 +5,8 @@ describe('jest mock compat layer', () => { const r = returnFactory('return') const e = returnFactory('throw') + const f = returnFactory('fulfilled') + const h = returnFactory('rejected') it('works with name', () => { const spy = vi.fn() @@ -152,12 +154,12 @@ describe('jest mock compat layer', () => { await spy() await spy() - expect(spy.mock.results).toEqual([ - r('original'), - r('3-once'), - r('4-once'), - r('unlimited'), - r('unlimited'), + expect(spy.mock.settledResults).toEqual([ + f('original'), + f('3-once'), + f('4-once'), + f('unlimited'), + f('unlimited'), ]) }) @@ -317,8 +319,12 @@ describe('jest mock compat layer', () => { await safeCall(spy) await safeCall(spy) - expect(spy.mock.results[0]).toEqual(e(new Error('once'))) - expect(spy.mock.results[1]).toEqual(e(new Error('error'))) + expect(spy.mock.results[0]).toEqual({ + type: 'return', + value: expect.any(Promise), + }) + expect(spy.mock.settledResults[0]).toEqual(h(new Error('once'))) + expect(spy.mock.settledResults[1]).toEqual(h(new Error('error'))) }) it('mockResolvedValue', async () => { const spy = vi.fn() @@ -328,8 +334,12 @@ describe('jest mock compat layer', () => { await spy() await spy() - expect(spy.mock.results[0]).toEqual(r('once')) - expect(spy.mock.results[1]).toEqual(r('resolved')) + expect(spy.mock.results[0]).toEqual({ + type: 'return', + value: expect.any(Promise), + }) + expect(spy.mock.settledResults[0]).toEqual(f('once')) + expect(spy.mock.settledResults[1]).toEqual(f('resolved')) }) it('tracks instances made by mocks', () => {