diff --git a/packages/vitest/src/integrations/chai/jest-expect.ts b/packages/vitest/src/integrations/chai/jest-expect.ts index df1bd4c35b4a..e2cad1d0ad65 100644 --- a/packages/vitest/src/integrations/chai/jest-expect.ts +++ b/packages/vitest/src/integrations/chai/jest-expect.ts @@ -1,7 +1,9 @@ import c from 'picocolors' +import { AssertionError } from 'chai' import type { EnhancedSpy } from '../spy' import { isMockFunction } from '../spy' import { addSerializer } from '../snapshot/port/plugins' +import { toString } from '../utils' import type { Constructable, Test } from '../../types' import { assertTypes } from '../../utils' import { unifiedDiff } from '../../node/diff' @@ -55,11 +57,27 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return function (this: Chai.Assertion & Chai.AssertionStatic, ...args: any[]) { const promise = utils.flag(this, 'promise') const object = utils.flag(this, 'object') + const isNot = utils.flag(this, 'negate') as boolean if (promise === 'rejects') { utils.flag(this, 'object', () => { throw object }) } + // if it got here, it's already resolved + // unless it tries to resolve to a function that should throw + // called as '.resolves[.not].toThrow()` + else if (promise === 'resolves' && typeof object !== 'function') { + if (!isNot) { + const message = utils.flag(this, 'message') || 'expected promise to throw an error, but it didn\'t' + const error = { + showDiff: false, + } + throw new AssertionError(message, error, utils.flag(this, 'ssfi')) + } + else { + return + } + } _super.apply(this, args) } }) @@ -426,11 +444,27 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const obj = this._obj const promise = utils.flag(this, 'promise') + const isNot = utils.flag(this, 'negate') as boolean let thrown: any = null - if (promise) { + if (promise === 'rejects') { thrown = obj } + // if it got here, it's already resolved + // unless it tries to resolve to a function that should throw + // called as .resolves.toThrow(Error) + else if (promise === 'resolves' && typeof obj !== 'function') { + if (!isNot) { + const message = utils.flag(this, 'message') || 'expected promise to throw an error, but it didn\'t' + const error = { + showDiff: false, + } + throw new AssertionError(message, error, utils.flag(this, 'ssfi')) + } + else { + return + } + } else { try { obj() @@ -550,6 +584,10 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { utils.flag(this, 'promise', 'resolves') utils.flag(this, 'error', new Error('resolves')) const obj = utils.flag(this, 'object') + + if (typeof obj?.then !== 'function') + throw new TypeError(`You must provide a Promise to expect() when using .resolves, not '${typeof obj}'.`) + const proxy: any = new Proxy(this, { get: (target, key, receiver) => { const result = Reflect.get(target, key, receiver) @@ -564,7 +602,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return result.call(this, ...args) }, (err: any) => { - throw new Error(`promise rejected "${err}" instead of resolving`) + throw new Error(`promise rejected "${toString(err)}" instead of resolving`) }, ) } @@ -579,6 +617,10 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { utils.flag(this, 'error', new Error('rejects')) const obj = utils.flag(this, 'object') const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat + + if (typeof wrapper?.then !== 'function') + throw new TypeError(`You must provide a Promise to expect() when using .rejects, not '${typeof wrapper}'.`) + const proxy: any = new Proxy(this, { get: (target, key, receiver) => { const result = Reflect.get(target, key, receiver) @@ -589,7 +631,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return async (...args: any[]) => { return wrapper.then( (value: any) => { - throw new Error(`promise resolved "${value}" instead of rejecting`) + throw new Error(`promise resolved "${toString(value)}" instead of rejecting`) }, (err: any) => { utils.flag(this, 'object', err) diff --git a/packages/vitest/src/integrations/utils.ts b/packages/vitest/src/integrations/utils.ts index cc9da21c0dca..dbf0ee9f1f93 100644 --- a/packages/vitest/src/integrations/utils.ts +++ b/packages/vitest/src/integrations/utils.ts @@ -5,3 +5,12 @@ export function getRunningMode() { export function isWatchMode() { return getRunningMode() === 'watch' } + +export function toString(value: any) { + try { + return `${value}` + } + catch (_error) { + return 'unknown' + } +} diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index bcd0cca4a0ef..f534bba158d6 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -435,6 +435,47 @@ describe('async expect', () => { it('resolves', async () => { await expect((async () => 'true')()).resolves.toBe('true') await expect((async () => 'true')()).resolves.not.toBe('true22') + await expect((async () => 'true')()).resolves.not.toThrow() + await expect((async () => new Error('msg'))()).resolves.not.toThrow() // calls chai assertion + await expect((async () => new Error('msg'))()).resolves.not.toThrow(Error) // calls our assertion + await expect((async () => () => { + throw new Error('msg') + })()).resolves.toThrow() + await expect((async () => () => { + return new Error('msg') + })()).resolves.not.toThrow() + await expect((async () => () => { + return new Error('msg') + })()).resolves.not.toThrow(Error) + }) + + it('resolves trows chai', async () => { + const assertion = async () => { + await expect((async () => new Error('msg'))()).resolves.toThrow() + } + + await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t') + }) + + it('resolves trows jest', async () => { + const assertion = async () => { + await expect((async () => new Error('msg'))()).resolves.toThrow(Error) + } + + await expect(assertion).rejects.toThrowError('expected promise to throw an error, but it didn\'t') + }) + + it('throws an error on .resolves when the argument is not a promise', () => { + expect.assertions(2) + + const expectedError = new TypeError('You must provide a Promise to expect() when using .resolves, not \'number\'.') + + try { + expect(1).resolves.toEqual(2) + } + catch (error) { + expect(error).toEqual(expectedError) + } }) it.fails('failed to resolve', async () => { @@ -443,6 +484,12 @@ describe('async expect', () => { })()).resolves.toBe('true') }) + it.fails('failed to throw', async () => { + await expect((async () => { + throw new Error('err') + })()).resolves.not.toThrow() + }) + it('rejects', async () => { await expect((async () => { throw new Error('err') @@ -471,6 +518,26 @@ describe('async expect', () => { it.fails('failed to reject', async () => { await expect((async () => 'test')()).rejects.toBe('test') }) + + it('throws an error on .rejects when the argument (or function result) is not a promise', () => { + expect.assertions(4) + + const expectedError = new TypeError('You must provide a Promise to expect() when using .rejects, not \'number\'.') + + try { + expect(1).rejects.toEqual(2) + } + catch (error) { + expect(error).toEqual(expectedError) + } + + try { + expect(() => 1).rejects.toEqual(2) + } + catch (error) { + expect(error).toEqual(expectedError) + } + }) }) it('timeout', () => new Promise(resolve => setTimeout(resolve, 500)))