Skip to content

Commit

Permalink
fix: .resolves and .rejects expectations (#1300)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmyersdev committed May 13, 2022
1 parent c8b2af4 commit 789cc93
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
48 changes: 45 additions & 3 deletions packages/vitest/src/integrations/chai/jest-expect.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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`)
},
)
}
Expand All @@ -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)
Expand All @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions packages/vitest/src/integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}
67 changes: 67 additions & 0 deletions test/core/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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')
Expand Down Expand Up @@ -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)))

0 comments on commit 789cc93

Please sign in to comment.