From 8d73ecaf5a8047a202db222c85641432d70d2c65 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Mon, 18 Sep 2023 13:25:35 +0200 Subject: [PATCH] fix: print value shape when .resolves and .rejects fails (#4137) --- examples/mocks/test/factory.test.ts | 2 +- packages/expect/src/jest-expect.ts | 49 +++++++++++++++-------------- test/core/test/jest-expect.test.ts | 43 +++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/examples/mocks/test/factory.test.ts b/examples/mocks/test/factory.test.ts index 43c98d706acd..3830c16ebf61 100644 --- a/examples/mocks/test/factory.test.ts +++ b/examples/mocks/test/factory.test.ts @@ -75,7 +75,7 @@ describe('mocking with factory', () => { expect((example as any).then).toBe('a then export') expect((example as any).mocked).toBe(true) expect(example.square(2, 3)).toBe(5) - expect(example.asyncSquare(2, 3)).resolves.toBe(5) + await expect(example.asyncSquare(2, 3)).resolves.toBe(5) }) test('successfully with actual', () => { diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 74650f16b375..24215e9cb00e 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -1,4 +1,3 @@ -import { AssertionError } from 'chai' import { assertTypes, getColors } from '@vitest/utils' import type { Constructable } from '@vitest/utils' import type { EnhancedSpy } from '@vitest/spy' @@ -13,6 +12,7 @@ import { recordAsyncExpect, wrapSoft } from './utils' // Jest Expect Compact export const JestChaiExpect: ChaiPlugin = (chai, utils) => { + const { AssertionError } = chai const c = () => getColors() function def(name: keyof Assertion | (keyof Assertion)[], fn: ((this: Chai.AssertionStatic & Assertion, ...args: any[]) => any)) { @@ -436,11 +436,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { if (called && isNot) msg = formatCalls(spy, msg) - if ((called && isNot) || (!called && !isNot)) { - const err = new Error(msg) - err.name = 'AssertionError' - throw err - } + if ((called && isNot) || (!called && !isNot)) + throw new AssertionError(msg) }) def(['toHaveBeenCalledWith', 'toBeCalledWith'], function (...args) { const spy = getSpy(this) @@ -448,7 +445,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const pass = spy.mock.calls.some(callArg => jestEquals(callArg, args, [iterableEquality])) const isNot = utils.flag(this, 'negate') as boolean - let msg = utils.getMessage( + const msg = utils.getMessage( this, [ pass, @@ -458,12 +455,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ], ) - if ((pass && isNot) || (!pass && !isNot)) { - msg = formatCalls(spy, msg, args) - const err = new Error(msg) - err.name = 'AssertionError' - throw err - } + if ((pass && isNot) || (!pass && !isNot)) + throw new AssertionError(formatCalls(spy, msg, args)) }) def(['toHaveBeenNthCalledWith', 'nthCalledWith'], function (times: number, ...args: any[]) { const spy = getSpy(this) @@ -595,7 +588,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const pass = spy.mock.results.some(({ type, value: result }) => type === 'return' && jestEquals(value, result)) const isNot = utils.flag(this, 'negate') as boolean - let msg = utils.getMessage( + const msg = utils.getMessage( this, [ pass, @@ -605,12 +598,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ], ) - if ((pass && isNot) || (!pass && !isNot)) { - msg = formatReturns(spy, msg, value) - const err = new Error(msg) - err.name = 'AssertionError' - throw err - } + if ((pass && isNot) || (!pass && !isNot)) + throw new AssertionError(formatReturns(spy, msg, value)) }) def(['toHaveLastReturnedWith', 'lastReturnedWith'], function (value: any) { const spy = getSpy(this) @@ -650,8 +639,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }) utils.addProperty(chai.Assertion.prototype, 'resolves', function __VITEST_RESOLVES__(this: any) { + const error = new Error('resolves') utils.flag(this, 'promise', 'resolves') - utils.flag(this, 'error', new Error('resolves')) + utils.flag(this, 'error', error) const test: Test = utils.flag(this, 'vitest-test') const obj = utils.flag(this, 'object') @@ -672,7 +662,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return result.call(this, ...args) }, (err: any) => { - throw new Error(`promise rejected "${String(err)}" instead of resolving`) + const _error = new AssertionError( + `promise rejected "${utils.inspect(err)}" instead of resolving`, + { showDiff: false }, + ) + _error.stack = (error.stack as string).replace(error.message, _error.message) + throw _error }, ) @@ -685,8 +680,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { }) utils.addProperty(chai.Assertion.prototype, 'rejects', function __VITEST_REJECTS__(this: any) { + const error = new Error('rejects') utils.flag(this, 'promise', 'rejects') - utils.flag(this, 'error', new Error('rejects')) + utils.flag(this, 'error', error) const test: Test = utils.flag(this, 'vitest-test') const obj = utils.flag(this, 'object') const wrapper = typeof obj === 'function' ? obj() : obj // for jest compat @@ -704,7 +700,12 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return async (...args: any[]) => { const promise = wrapper.then( (value: any) => { - throw new Error(`promise resolved "${String(value)}" instead of rejecting`) + const _error = new AssertionError( + `promise resolved "${utils.inspect(value)}" instead of rejecting`, + { showDiff: false }, + ) + _error.stack = (error.stack as string).replace(error.message, _error.message) + throw _error }, (err: any) => { utils.flag(this, 'object', err) diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 12960ad2afc5..2cc404d5adff 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -1,5 +1,7 @@ /* eslint-disable no-sparse-arrays */ import { AssertionError } from 'node:assert' +import { fileURLToPath } from 'node:url' +import { resolve } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { generateToBeMessage, setupColors } from '@vitest/expect' import { processError } from '@vitest/utils/error' @@ -604,6 +606,7 @@ describe('async expect', () => { try { expect(1).resolves.toEqual(2) + expect.unreachable() } catch (error) { expect(error).toEqual(expectedError) @@ -658,6 +661,7 @@ describe('async expect', () => { try { expect(1).rejects.toEqual(2) + expect.unreachable() } catch (error) { expect(error).toEqual(expectedError) @@ -665,6 +669,7 @@ describe('async expect', () => { try { expect(() => 1).rejects.toEqual(2) + expect.unreachable() } catch (error) { expect(error).toEqual(expectedError) @@ -686,6 +691,7 @@ describe('async expect', () => { const toStrictEqualError1 = generatedToBeMessage('toStrictEqual', '{ key: \'value\' }', '{ key: \'value\' }') try { expect(actual).toBe({ ...actual }) + expect.unreachable() } catch (error: any) { expect(error.message).toBe(toStrictEqualError1.message) @@ -694,6 +700,7 @@ describe('async expect', () => { const toStrictEqualError2 = generatedToBeMessage('toStrictEqual', 'FakeClass{}', 'FakeClass{}') try { expect(new FakeClass()).toBe(new FakeClass()) + expect.unreachable() } catch (error: any) { expect(error.message).toBe(toStrictEqualError2.message) @@ -702,15 +709,16 @@ describe('async expect', () => { const toEqualError1 = generatedToBeMessage('toEqual', '{}', 'FakeClass{}') try { expect({}).toBe(new FakeClass()) + expect.unreachable() } catch (error: any) { expect(error.message).toBe(toEqualError1.message) - // expect(error).toEqual('1234') } const toEqualError2 = generatedToBeMessage('toEqual', 'FakeClass{}', '{}') try { expect(new FakeClass()).toBe({}) + expect.unreachable() } catch (error: any) { expect(error.message).toBe(toEqualError2.message) @@ -742,22 +750,51 @@ describe('async expect', () => { }) }) + it('printing error message', async () => { + const root = resolve(fileURLToPath(import.meta.url), '../../../../') + // have "\" on windows, and "/" on unix + const filename = fileURLToPath(import.meta.url).replace(root, '') + try { + await expect(Promise.resolve({ foo: { bar: 42 } })).rejects.toThrow() + expect.unreachable() + } + catch (err: any) { + const stack = err.stack.replace(new RegExp(root, 'g'), '') + expect(err.message).toMatchInlineSnapshot('"promise resolved \\"{ foo: { bar: 42 } }\\" instead of rejecting"') + expect(stack).toContain(`at ${filename}`) + } + + try { + const error = new Error('some error') + Object.assign(error, { foo: { bar: 42 } }) + await expect(Promise.reject(error)).resolves.toBe(1) + expect.unreachable() + } + catch (err: any) { + const stack = err.stack.replace(new RegExp(root, 'g'), '') + expect(err.message).toMatchInlineSnapshot('"promise rejected \\"Error: some error { foo: { bar: 42 } }\\" instead of resolving"') + expect(stack).toContain(`at ${filename}`) + } + }) + it('handle thenable objects', async () => { await expect({ then: (resolve: any) => resolve(0) }).resolves.toBe(0) await expect({ then: (_: any, reject: any) => reject(0) }).rejects.toBe(0) try { await expect({ then: (resolve: any) => resolve(0) }).rejects.toBe(0) + expect.unreachable() } catch (error) { - expect(error).toEqual(new Error('promise resolved "0" instead of rejecting')) + expect(error).toEqual(new Error('promise resolved "+0" instead of rejecting')) } try { await expect({ then: (_: any, reject: any) => reject(0) }).resolves.toBe(0) + expect.unreachable() } catch (error) { - expect(error).toEqual(new Error('promise rejected "0" instead of resolving')) + expect(error).toEqual(new Error('promise rejected "+0" instead of resolving')) } }) })