diff --git a/CHANGELOG.md b/CHANGELOG.md index a02a7db5ac37..80a3d94bf4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[expect]`: Improve report when matcher fails, part 13 ([#8077](https://github.com/facebook/jest/pull/8077)) - `[@jest/core]` Filter API pre-filter setup hook ([#8142](https://github.com/facebook/jest/pull/8142)) - `[jest-snapshot]` Improve report when matcher fails, part 14 ([#8132](https://github.com/facebook/jest/pull/8132)) - `[@jest/reporter]` Display todo and skip test descriptions when verbose is true ([#8038](https://github.com/facebook/jest/pull/8038)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index 42237ee166b2..34be35ea0c08 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -1198,84 +1198,114 @@ Received: 34" `; exports[`.toBeInstanceOf() failing "a" and [Function String] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: String -Received constructor: String +Received value has no prototype Received value: \\"a\\"" `; +exports[`.toBeInstanceOf() failing /\\w+/ and [Function anonymous] 1`] = ` +"expect(received).toBeInstanceOf(expected) + +Expected constructor name is an empty string +Received constructor: RegExp +Received value: /\\\\w+/" +`; + exports[`.toBeInstanceOf() failing {} and [Function A] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: A -Received constructor: undefined +Received value has no prototype Received value: {}" `; exports[`.toBeInstanceOf() failing {} and [Function B] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: B Received constructor: A Received value: {}" `; +exports[`.toBeInstanceOf() failing {} and [Function RegExp] 1`] = ` +"expect(received).toBeInstanceOf(expected) + +Expected constructor: RegExp +Received constructor name is an empty string +Received value: {}" +`; + exports[`.toBeInstanceOf() failing 1 and [Function Number] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: Number -Received constructor: Number +Received value has no prototype Received value: 1" `; exports[`.toBeInstanceOf() failing null and [Function String] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: String -Received constructor: +Received value has no prototype Received value: null" `; exports[`.toBeInstanceOf() failing true and [Function Boolean] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: Boolean -Received constructor: Boolean +Received value has no prototype Received value: true" `; exports[`.toBeInstanceOf() failing undefined and [Function String] 1`] = ` -"expect(value).toBeInstanceOf(constructor) +"expect(received).toBeInstanceOf(expected) Expected constructor: String -Received constructor: +Received value has no prototype Received value: undefined" `; exports[`.toBeInstanceOf() passing [] and [Function Array] 1`] = ` -"expect(value).not.toBeInstanceOf(constructor) +"expect(received).not.toBeInstanceOf(expected) -Expected constructor: Array +Expected constructor: not Array Received value: []" `; exports[`.toBeInstanceOf() passing {} and [Function A] 1`] = ` -"expect(value).not.toBeInstanceOf(constructor) +"expect(received).not.toBeInstanceOf(expected) -Expected constructor: A +Expected constructor: not A +Received value: {}" +`; + +exports[`.toBeInstanceOf() passing {} and [Function B] 1`] = ` +"expect(received).not.toBeInstanceOf(expected) + +Expected constructor: not B +Received value: {}" +`; + +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(value).not.toBeInstanceOf(constructor) +"expect(received).not.toBeInstanceOf(expected) -Expected constructor: Map +Expected constructor: not Map Received value: Map {}" `; exports[`.toBeInstanceOf() throws if constructor is not a function 1`] = ` -"expect(received).toBeInstanceOf(expected) +"expect(received).toBeInstanceOf(expected) Matcher error: expected value must be a function diff --git a/packages/expect/src/__tests__/matchers.test.js b/packages/expect/src/__tests__/matchers.test.js index dc31c645f5d9..953de2f2fcaf 100644 --- a/packages/expect/src/__tests__/matchers.test.js +++ b/packages/expect/src/__tests__/matchers.test.js @@ -684,8 +684,28 @@ describe('.toEqual()', () => { describe('.toBeInstanceOf()', () => { class A {} class B {} + class C extends B {} - [[new Map(), Map], [[], Array], [new A(), A]].forEach(([a, b]) => { + class HasStaticNameMethod { + constructor() {} + static name() {} + } + + function DefinesNameProp() {} + Object.defineProperty(DefinesNameProp, 'name', { + configurable: true, + enumerable: false, + value: '', + writable: true, + }); + + [ + [new Map(), Map], + [[], Array], + [new A(), A], + [new C(), B], // subclass + [new HasStaticNameMethod(), HasStaticNameMethod], + ].forEach(([a, b]) => { test(`passing ${stringify(a)} and ${stringify(b)}`, () => { expect(() => jestExpect(a).not.toBeInstanceOf(b), @@ -703,6 +723,8 @@ describe('.toBeInstanceOf()', () => { [Object.create(null), A], [undefined, String], [null, String], + [/\w+/, function() {}], + [new DefinesNameProp(), RegExp], ].forEach(([a, b]) => { test(`failing ${stringify(a)} and ${stringify(b)}`, () => { expect(() => diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index bad0f0424f36..60c53d9ec436 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -6,7 +6,7 @@ * */ -import getType from 'jest-get-type'; +import getType, {isPrimitive} from 'jest-get-type'; import { EXPECTED_COLOR, RECEIVED_COLOR, @@ -220,45 +220,62 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeInstanceOf(this: MatcherState, received: any, constructor: Function) { - const constType = getType(constructor); + toBeInstanceOf(this: MatcherState, received: any, expected: Function) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; - if (constType !== 'function') { + if (typeof expected !== 'function') { throw new Error( matcherErrorMessage( - matcherHint('.toBeInstanceOf', undefined, undefined, { - isNot: this.isNot, - }), + matcherHint('toBeInstanceOf', undefined, undefined, options), `${EXPECTED_COLOR('expected')} value must be a function`, - printWithType('Expected', constructor, printExpected), + printWithType('Expected', expected, printExpected), ), ); } - const pass = received instanceof constructor; + + 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('.toBeInstanceOf', 'value', 'constructor', { - isNot: this.isNot, - }) + + matcherHint('toBeInstanceOf', undefined, undefined, options) + '\n\n' + - `Expected constructor: ${EXPECTED_COLOR( - constructor.name || String(constructor), - )}\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)}` : () => - matcherHint('.toBeInstanceOf', 'value', 'constructor', { - isNot: this.isNot, - }) + + matcherHint('toBeInstanceOf', undefined, undefined, options) + '\n\n' + - `Expected constructor: ${EXPECTED_COLOR( - constructor.name || String(constructor), - )}\n` + - `Received constructor: ${RECEIVED_COLOR( - received != null - ? received.constructor && received.constructor.name - : '', - )}\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`) + + (isPrimitive(received) || Object.getPrototypeOf(received) === null + ? 'Received value has no prototype\n' + : 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)}`; return {message, pass};