diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae290315327..eee76bba68e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `[docs]` Add DynamoDB guide ([#8319](https://github.com/facebook/jest/pull/8319)) - `[expect]` Improve report when matcher fails, part 17 ([#8349](https://github.com/facebook/jest/pull/8349)) - `[expect]` Improve report when matcher fails, part 18 ([#8356](https://github.com/facebook/jest/pull/8356)) +- `[expect]` Improve report when matcher fails, part 19 ([#8367](https://github.com/facebook/jest/pull/8367)) ### Fixes diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index b3f8a191a0a4..aa14992ae949 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -1265,6 +1265,7 @@ exports[`.toBeInstanceOf() failing "a" and [Function String] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: String + Received value has no prototype Received value: \\"a\\"" `; @@ -1274,13 +1275,14 @@ exports[`.toBeInstanceOf() failing /\\w+/ and [Function anonymous] 1`] = ` Expected constructor name is an empty string Received constructor: RegExp -Received value: /\\\\w+/" +" `; exports[`.toBeInstanceOf() failing {} and [Function A] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: A + Received value has no prototype Received value: {}" `; @@ -1290,7 +1292,7 @@ exports[`.toBeInstanceOf() failing {} and [Function B] 1`] = ` Expected constructor: B Received constructor: A -Received value: {}" +" `; exports[`.toBeInstanceOf() failing {} and [Function RegExp] 1`] = ` @@ -1298,13 +1300,14 @@ exports[`.toBeInstanceOf() failing {} and [Function RegExp] 1`] = ` Expected constructor: RegExp Received constructor name is an empty string -Received value: {}" +" `; exports[`.toBeInstanceOf() failing 1 and [Function Number] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: Number + Received value has no prototype Received value: 1" `; @@ -1313,6 +1316,7 @@ exports[`.toBeInstanceOf() failing null and [Function String] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: String + Received value has no prototype Received value: null" `; @@ -1321,6 +1325,7 @@ exports[`.toBeInstanceOf() failing true and [Function Boolean] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: Boolean + Received value has no prototype Received value: true" `; @@ -1329,6 +1334,7 @@ exports[`.toBeInstanceOf() failing undefined and [Function String] 1`] = ` "expect(received).toBeInstanceOf(expected) Expected constructor: String + Received value has no prototype Received value: undefined" `; @@ -1337,35 +1343,44 @@ exports[`.toBeInstanceOf() passing [] and [Function Array] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor: not Array -Received value: []" +" `; exports[`.toBeInstanceOf() passing {} and [Function A] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor: not A -Received value: {}" +" `; exports[`.toBeInstanceOf() passing {} and [Function B] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor: not B -Received value: {}" +Received constructor: C extends B +" +`; + +exports[`.toBeInstanceOf() passing {} and [Function B] 2`] = ` +"expect(received).not.toBeInstanceOf(expected) + +Expected constructor: not B +Received constructor: E extends … extends B +" `; exports[`.toBeInstanceOf() passing {} and [Function name() {}] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor name is not a string -Received value: {}" +" `; exports[`.toBeInstanceOf() passing Map {} and [Function Map] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor: not Map -Received value: Map {}" +" `; exports[`.toBeInstanceOf() throws if constructor is not a function 1`] = ` diff --git a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap index f9a60f4a5c99..f282a7b4e0df 100644 --- a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap @@ -89,7 +89,7 @@ Received message: \\"apple\\" exports[`toThrow error class did not throw at all 1`] = ` "expect(received).toThrow(expected) -Expected name: \\"Err\\" +Expected constructor: Err Received function did not throw" `; @@ -97,8 +97,8 @@ Received function did not throw" exports[`toThrow error class threw, but class did not match (error) 1`] = ` "expect(received).toThrow(expected) -Expected name: \\"Err2\\" -Received name: \\"Error\\" +Expected constructor: Err2 +Received constructor: Err Received message: \\"apple\\" @@ -108,17 +108,38 @@ Received message: \\"apple\\" exports[`toThrow error class threw, but class did not match (non-error falsey) 1`] = ` "expect(received).toThrow(expected) -Expected name: \\"Err2\\" +Expected constructor: Err2 Received value: undefined " `; +exports[`toThrow error class threw, but class should not match (error subclass) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected constructor: not Err +Received constructor: SubErr extends Err + +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`toThrow error class threw, but class should not match (error subsubclass) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected constructor: not Err +Received constructor: SubSubErr extends … extends Err + +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + exports[`toThrow error class threw, but class should not match (error) 1`] = ` "expect(received).not.toThrow(expected) -Expected name: \\"Err\\" -Received name: \\"Error\\" +Expected constructor: not Err Received message: \\"apple\\" @@ -174,8 +195,8 @@ Received function did not throw" exports[`toThrow promise/async throws if Error-like object is returned threw, but class did not match 1`] = ` "expect(received).rejects.toThrow(expected) -Expected name: \\"Err2\\" -Received name: \\"Error\\" +Expected constructor: Err2 +Received constructor: Err Received message: \\"async apple\\" @@ -364,7 +385,7 @@ Received message: \\"apple\\" exports[`toThrowError error class did not throw at all 1`] = ` "expect(received).toThrowError(expected) -Expected name: \\"Err\\" +Expected constructor: Err Received function did not throw" `; @@ -372,8 +393,8 @@ Received function did not throw" exports[`toThrowError error class threw, but class did not match (error) 1`] = ` "expect(received).toThrowError(expected) -Expected name: \\"Err2\\" -Received name: \\"Error\\" +Expected constructor: Err2 +Received constructor: Err Received message: \\"apple\\" @@ -383,17 +404,38 @@ Received message: \\"apple\\" exports[`toThrowError error class threw, but class did not match (non-error falsey) 1`] = ` "expect(received).toThrowError(expected) -Expected name: \\"Err2\\" +Expected constructor: Err2 Received value: undefined " `; +exports[`toThrowError error class threw, but class should not match (error subclass) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected constructor: not Err +Received constructor: SubErr extends Err + +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`toThrowError error class threw, but class should not match (error subsubclass) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected constructor: not Err +Received constructor: SubSubErr extends … extends Err + +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + exports[`toThrowError error class threw, but class should not match (error) 1`] = ` "expect(received).not.toThrowError(expected) -Expected name: \\"Err\\" -Received name: \\"Error\\" +Expected constructor: not Err Received message: \\"apple\\" @@ -449,8 +491,8 @@ Received function did not throw" exports[`toThrowError promise/async throws if Error-like object is returned threw, but class did not match 1`] = ` "expect(received).rejects.toThrowError(expected) -Expected name: \\"Err2\\" -Received name: \\"Error\\" +Expected constructor: Err2 +Received constructor: Err Received message: \\"async apple\\" diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index ad143ffa0437..a5fbe8c37afb 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -687,6 +687,8 @@ describe('.toBeInstanceOf()', () => { class A {} class B {} class C extends B {} + class D extends C {} + class E extends D {} class HasStaticNameMethod { constructor() {} @@ -705,7 +707,8 @@ describe('.toBeInstanceOf()', () => { [new Map(), Map], [[], Array], [new A(), A], - [new C(), B], // subclass + [new C(), B], // C extends B + [new E(), B], // E extends … extends B [new HasStaticNameMethod(), HasStaticNameMethod], ].forEach(([a, b]) => { test(`passing ${stringify(a)} and ${stringify(b)}`, () => { diff --git a/packages/expect/src/__tests__/toThrowMatchers.test.js b/packages/expect/src/__tests__/toThrowMatchers.test.js index b06b597ccb75..77c80dc1f738 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.js +++ b/packages/expect/src/__tests__/toThrowMatchers.test.js @@ -146,6 +146,24 @@ class customError extends Error { }); describe('error class', () => { + class SubErr extends Err { + constructor(...args) { + super(...args); + // In a carefully written error subclass, + // name property is equal to constructor name. + this.name = this.constructor.name; + } + } + + class SubSubErr extends SubErr { + constructor(...args) { + super(...args); + // In a carefully written error subclass, + // name property is equal to constructor name. + this.name = this.constructor.name; + } + } + it('passes', () => { jestExpect(() => { throw new Err(); @@ -189,6 +207,22 @@ class customError extends Error { }).not[toThrow](Err); }).toThrowErrorMatchingSnapshot(); }); + + test('threw, but class should not match (error subclass)', () => { + expect(() => { + jestExpect(() => { + throw new SubErr('apple'); + }).not[toThrow](Err); + }).toThrowErrorMatchingSnapshot(); + }); + + test('threw, but class should not match (error subsubclass)', () => { + expect(() => { + jestExpect(() => { + throw new SubSubErr('apple'); + }).not[toThrow](Err); + }).toThrowErrorMatchingSnapshot(); + }); }); describe('error-message', () => { diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 2309facee19a..977cb205cbb3 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -27,7 +27,11 @@ import { import {MatchersObject, MatcherState} from './types'; import { printDiffOrStringify, + printExpectedConstructorName, + printExpectedConstructorNameNot, printReceivedArrayContainExpectedItem, + printReceivedConstructorName, + printReceivedConstructorNameNot, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -267,45 +271,33 @@ const matchers: MatchersObject = { const pass = received instanceof expected; - const NAME_IS_NOT_STRING = ' name is not a string\n'; - const NAME_IS_EMPTY_STRING = ' name is an empty string\n'; - const message = pass ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - // A truthy test for `expected.name` property has false positive for: - // function with a defined name property - // class with a static name method - (typeof expected.name !== 'string' - ? 'Expected constructor' + NAME_IS_NOT_STRING - : expected.name.length === 0 - ? 'Expected constructor' + NAME_IS_EMPTY_STRING - : `Expected constructor: not ${EXPECTED_COLOR(expected.name)}\n`) + - `Received value: ${printReceived(received)}` + printExpectedConstructorNameNot('Expected constructor', expected) + + (typeof received.constructor === 'function' && + received.constructor !== expected + ? printReceivedConstructorNameNot( + 'Received constructor', + received.constructor, + expected, + ) + : '') : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - // A truthy test for `expected.name` property has false positive for: - // function with a defined name property - // class with a static name method - (typeof expected.name !== 'string' - ? 'Expected constructor' + NAME_IS_NOT_STRING - : expected.name.length === 0 - ? 'Expected constructor' + NAME_IS_EMPTY_STRING - : `Expected constructor: ${EXPECTED_COLOR(expected.name)}\n`) + + printExpectedConstructorName('Expected constructor', expected) + (isPrimitive(received) || Object.getPrototypeOf(received) === null - ? 'Received value has no prototype\n' + ? `\nReceived value has no prototype\nReceived value: ${printReceived( + received, + )}` : typeof received.constructor !== 'function' - ? '' - : typeof received.constructor.name !== 'string' - ? 'Received constructor' + NAME_IS_NOT_STRING - : received.constructor.name.length === 0 - ? 'Received constructor' + NAME_IS_EMPTY_STRING - : `Received constructor: ${RECEIVED_COLOR( - received.constructor.name, - )}\n`) + - `Received value: ${printReceived(received)}`; + ? `\nReceived value: ${printReceived(received)}` + : printReceivedConstructorName( + 'Received constructor', + received.constructor, + )); return {message, pass}; }, diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index e5687941b481..1d84c9265a55 100644 --- a/packages/expect/src/print.ts +++ b/packages/expect/src/print.ts @@ -8,6 +8,7 @@ import getType, {isPrimitive} from 'jest-get-type'; import { + EXPECTED_COLOR, INVERTED_COLOR, RECEIVED_COLOR, diff, @@ -132,3 +133,52 @@ export const printDiffOrStringify = ( }` ); }; + +export const printExpectedConstructorName = ( + label: string, + expected: Function, +) => printConstructorName(label, expected, false, true) + '\n'; + +export const printExpectedConstructorNameNot = ( + label: string, + expected: Function, +) => printConstructorName(label, expected, true, true) + '\n'; + +export const printReceivedConstructorName = ( + label: string, + received: Function, +) => printConstructorName(label, received, false, false) + '\n'; + +export function printReceivedConstructorNameNot( + label: string, + received: Function, + expected: Function, +) { + let printed = printConstructorName(label, received, true, false); + + if (typeof received.name === 'string' && received.name.length !== 0) { + printed += ` ${ + Object.getPrototypeOf(received) === expected + ? 'extends' + : 'extends … extends' + } ${EXPECTED_COLOR(expected.name)}`; + } + + return printed + '\n'; +} + +const printConstructorName = ( + label: string, + constructor: Function, + isNot: boolean, + isExpected: boolean, +): string => + typeof constructor.name !== 'string' + ? `${label} name is not a string` + : constructor.name.length === 0 + ? `${label} name is an empty string` + : `${label}: ${!isNot ? '' : isExpected ? 'not ' : ' '}${ + isExpected + ? EXPECTED_COLOR(constructor.name) + : RECEIVED_COLOR(constructor.name) + }`; diff --git a/packages/expect/src/toThrowMatchers.ts b/packages/expect/src/toThrowMatchers.ts index ca45aefd1800..f1e6c49f42bd 100644 --- a/packages/expect/src/toThrowMatchers.ts +++ b/packages/expect/src/toThrowMatchers.ts @@ -18,6 +18,10 @@ import { MatcherHintOptions, } from 'jest-matcher-utils'; import { + printExpectedConstructorName, + printExpectedConstructorNameNot, + printReceivedConstructorName, + printReceivedConstructorNameNot, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -250,8 +254,17 @@ const toThrowExpectedClass = ( ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - formatExpected('Expected name: ', expected.name) + - formatReceived('Received name: ', thrown, 'name') + + printExpectedConstructorNameNot('Expected constructor', expected) + + (thrown !== null && + thrown.value != null && + typeof thrown.value.constructor === 'function' && + thrown.value.constructor !== expected + ? printReceivedConstructorNameNot( + 'Received constructor', + thrown.value.constructor, + expected, + ) + : '') + '\n' + (thrown !== null && thrown.hasMessage ? formatReceived('Received message: ', thrown, 'message') + @@ -260,15 +273,21 @@ const toThrowExpectedClass = ( : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - formatExpected('Expected name: ', expected.name) + + printExpectedConstructorName('Expected constructor', expected) + (thrown === null ? '\n' + DID_NOT_THROW - : thrown.hasMessage - ? formatReceived('Received name: ', thrown, 'name') + + : (thrown.value != null && + typeof thrown.value.constructor === 'function' + ? printReceivedConstructorName( + 'Received constructor', + thrown.value.constructor, + ) + : '') + '\n' + - formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown) - : '\n' + formatReceived('Received value: ', thrown, 'value')); + (thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value'))); return {message, pass}; };