From 66d1fc37dd01e10077a3d01fbf7207b621125346 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 23 Apr 2019 14:36:42 -0400 Subject: [PATCH 1/5] expect: Improve report when matcher fails, part 19 --- .../__snapshots__/matchers.test.js.snap | 23 +++++--- .../toThrowMatchers.test.js.snap | 52 ++++++++++++------ .../src/__tests__/toThrowMatchers.test.js | 17 ++++++ packages/expect/src/matchers.ts | 55 +++++++++---------- packages/expect/src/print.ts | 44 +++++++++++++++ packages/expect/src/toThrowMatchers.ts | 33 ++++++++--- 6 files changed, 162 insertions(+), 62 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index b3f8a191a0a4..a2f6a0c332b2 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,36 @@ 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 +" `; 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..8c1283db8592 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,27 @@ 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 + +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 +184,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 +374,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 +382,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 +393,27 @@ 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 + +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 +469,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__/toThrowMatchers.test.js b/packages/expect/src/__tests__/toThrowMatchers.test.js index b06b597ccb75..661ee287cd3b 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.js +++ b/packages/expect/src/__tests__/toThrowMatchers.test.js @@ -146,6 +146,15 @@ 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; + } + } + it('passes', () => { jestExpect(() => { throw new Err(); @@ -189,6 +198,14 @@ 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(); + }); }); describe('error-message', () => { diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 2309facee19a..1b1d57e55cdc 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -27,7 +27,9 @@ import { import {MatchersObject, MatcherState} from './types'; import { printDiffOrStringify, + printExpectedConstructorName, printReceivedArrayContainExpectedItem, + printReceivedConstructorName, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -267,45 +269,38 @@ 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)}` + printExpectedConstructorName('Expected constructor', expected, true) + + (received.constructor != null && + received.constructor.name !== expected.name + ? printReceivedConstructorName( + 'Received constructor', + received, + true, + ) + : '') : () => 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, + false, + ) + (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, + false, + )); return {message, pass}; }, diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index e5687941b481..4ef5f2a10165 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,46 @@ export const printDiffOrStringify = ( }` ); }; + +export const printExpectedConstructorName = ( + label: string, + expected: Function, + isNot: boolean, +) => printConstructorName(label, expected, isNot, true); + +export const printReceivedConstructorName = ( + label: string, + received: any, // unknown has TypeScript errors :( + isNot: boolean, +) => { + if (received == null) { + return ''; + } + + if (typeof received.constructor === 'function') { + return printConstructorName(label, received.constructor, isNot, false); + } + + return ''; +}; + +const printConstructorName = ( + label: string, + constructor: Function, + isNot: boolean, + isExpected: boolean, +): string => { + if (constructor == null) { + return ''; + } + + return typeof constructor.name !== 'string' + ? `${label} name is not a string\n` + : constructor.name.length === 0 + ? `${label} name is an empty string\n` + : `${label}: ${!isNot ? '' : isExpected ? 'not ' : ' '}${ + isExpected + ? EXPECTED_COLOR(constructor.name) + : RECEIVED_COLOR(constructor.name) + }\n`; +}; diff --git a/packages/expect/src/toThrowMatchers.ts b/packages/expect/src/toThrowMatchers.ts index ca45aefd1800..ceff0139328b 100644 --- a/packages/expect/src/toThrowMatchers.ts +++ b/packages/expect/src/toThrowMatchers.ts @@ -18,6 +18,8 @@ import { MatcherHintOptions, } from 'jest-matcher-utils'; import { + printExpectedConstructorName, + printReceivedConstructorName, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -250,8 +252,17 @@ const toThrowExpectedClass = ( ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - formatExpected('Expected name: ', expected.name) + - formatReceived('Received name: ', thrown, 'name') + + printExpectedConstructorName('Expected constructor', expected, true) + + (thrown !== null && + thrown.value != null && + thrown.value.constructor != null && + thrown.value.constructor.name !== expected.name + ? printReceivedConstructorName( + 'Received constructor', + thrown.value, + true, + ) + : '') + '\n' + (thrown !== null && thrown.hasMessage ? formatReceived('Received message: ', thrown, 'message') + @@ -260,15 +271,21 @@ const toThrowExpectedClass = ( : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - formatExpected('Expected name: ', expected.name) + + printExpectedConstructorName('Expected constructor', expected, false) + (thrown === null ? '\n' + DID_NOT_THROW - : thrown.hasMessage - ? formatReceived('Received name: ', thrown, 'name') + + : (thrown.value != null + ? printReceivedConstructorName( + 'Received constructor', + thrown.value, + false, + ) + : '') + '\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}; }; From 9954ac1db64793a0aadf4c20405efa7efc319ead Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 23 Apr 2019 14:50:43 -0400 Subject: [PATCH 2/5] Delete obsolete code from printConstructorName --- packages/expect/src/print.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index 4ef5f2a10165..f9fe02fcb58d 100644 --- a/packages/expect/src/print.ts +++ b/packages/expect/src/print.ts @@ -161,12 +161,8 @@ const printConstructorName = ( constructor: Function, isNot: boolean, isExpected: boolean, -): string => { - if (constructor == null) { - return ''; - } - - return typeof constructor.name !== 'string' +): string => + typeof constructor.name !== 'string' ? `${label} name is not a string\n` : constructor.name.length === 0 ? `${label} name is an empty string\n` @@ -175,4 +171,3 @@ const printConstructorName = ( ? EXPECTED_COLOR(constructor.name) : RECEIVED_COLOR(constructor.name) }\n`; -}; From f55e9ed9673fd7de48c3b7bcd782a803a9ebe243 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 23 Apr 2019 14:58:24 -0400 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From b2af4b5d98d3418e5ec0d7a64428d7e6590f217e Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 24 Apr 2019 12:59:19 -0400 Subject: [PATCH 4/5] Call printReceivedConstructorName with constructor arg --- packages/expect/src/matchers.ts | 8 ++++---- packages/expect/src/print.ts | 14 ++------------ packages/expect/src/toThrowMatchers.ts | 11 ++++++----- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 1b1d57e55cdc..5ff32ffcef90 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -274,11 +274,11 @@ const matchers: MatchersObject = { matcherHint(matcherName, undefined, undefined, options) + '\n\n' + printExpectedConstructorName('Expected constructor', expected, true) + - (received.constructor != null && - received.constructor.name !== expected.name + (typeof received.constructor === 'function' && + received.constructor !== expected ? printReceivedConstructorName( 'Received constructor', - received, + received.constructor, true, ) : '') @@ -298,7 +298,7 @@ const matchers: MatchersObject = { ? `\nReceived value: ${printReceived(received)}` : printReceivedConstructorName( 'Received constructor', - received, + received.constructor, false, )); diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index f9fe02fcb58d..b2b961cb5a0f 100644 --- a/packages/expect/src/print.ts +++ b/packages/expect/src/print.ts @@ -142,19 +142,9 @@ export const printExpectedConstructorName = ( export const printReceivedConstructorName = ( label: string, - received: any, // unknown has TypeScript errors :( + received: Function, isNot: boolean, -) => { - if (received == null) { - return ''; - } - - if (typeof received.constructor === 'function') { - return printConstructorName(label, received.constructor, isNot, false); - } - - return ''; -}; +) => printConstructorName(label, received, isNot, false); const printConstructorName = ( label: string, diff --git a/packages/expect/src/toThrowMatchers.ts b/packages/expect/src/toThrowMatchers.ts index ceff0139328b..d6092e14b462 100644 --- a/packages/expect/src/toThrowMatchers.ts +++ b/packages/expect/src/toThrowMatchers.ts @@ -255,11 +255,11 @@ const toThrowExpectedClass = ( printExpectedConstructorName('Expected constructor', expected, true) + (thrown !== null && thrown.value != null && - thrown.value.constructor != null && - thrown.value.constructor.name !== expected.name + typeof thrown.value.constructor === 'function' && + thrown.value.constructor !== expected ? printReceivedConstructorName( 'Received constructor', - thrown.value, + thrown.value.constructor, true, ) : '') + @@ -274,10 +274,11 @@ const toThrowExpectedClass = ( printExpectedConstructorName('Expected constructor', expected, false) + (thrown === null ? '\n' + DID_NOT_THROW - : (thrown.value != null + : (thrown.value != null && + typeof thrown.value.constructor === 'function' ? printReceivedConstructorName( 'Received constructor', - thrown.value, + thrown.value.constructor, false, ) : '') + From d0ac0503beee906f1b1d764026f1f6089a173750 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 24 Apr 2019 18:40:00 -0400 Subject: [PATCH 5/5] Append extends to be clearer when negative assertion fails --- .../__snapshots__/matchers.test.js.snap | 10 +++++- .../toThrowMatchers.test.js.snap | 26 ++++++++++++-- .../expect/src/__tests__/matchers.test.js | 5 ++- .../src/__tests__/toThrowMatchers.test.js | 17 +++++++++ packages/expect/src/matchers.ts | 15 ++++---- packages/expect/src/print.ts | 35 +++++++++++++++---- packages/expect/src/toThrowMatchers.ts | 11 +++--- 7 files changed, 94 insertions(+), 25 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index a2f6a0c332b2..aa14992ae949 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -1357,7 +1357,15 @@ exports[`.toBeInstanceOf() passing {} and [Function B] 1`] = ` "expect(received).not.toBeInstanceOf(expected) Expected constructor: not B -Received constructor: C +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 " `; diff --git a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap index 8c1283db8592..f282a7b4e0df 100644 --- a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap @@ -118,7 +118,18 @@ exports[`toThrow error class threw, but class should not match (error subclass) "expect(received).not.toThrow(expected) Expected constructor: not Err -Received constructor: SubErr +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\\" @@ -403,7 +414,18 @@ exports[`toThrowError error class threw, but class should not match (error subcl "expect(received).not.toThrowError(expected) Expected constructor: not Err -Received constructor: SubErr +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\\" 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 661ee287cd3b..77c80dc1f738 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.js +++ b/packages/expect/src/__tests__/toThrowMatchers.test.js @@ -155,6 +155,15 @@ class customError extends Error { } } + 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(); @@ -206,6 +215,14 @@ class customError extends Error { }).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 5ff32ffcef90..977cb205cbb3 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -28,8 +28,10 @@ import {MatchersObject, MatcherState} from './types'; import { printDiffOrStringify, printExpectedConstructorName, + printExpectedConstructorNameNot, printReceivedArrayContainExpectedItem, printReceivedConstructorName, + printReceivedConstructorNameNot, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -273,23 +275,19 @@ const matchers: MatchersObject = { ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - printExpectedConstructorName('Expected constructor', expected, true) + + printExpectedConstructorNameNot('Expected constructor', expected) + (typeof received.constructor === 'function' && received.constructor !== expected - ? printReceivedConstructorName( + ? printReceivedConstructorNameNot( 'Received constructor', received.constructor, - true, + expected, ) : '') : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - printExpectedConstructorName( - 'Expected constructor', - expected, - false, - ) + + printExpectedConstructorName('Expected constructor', expected) + (isPrimitive(received) || Object.getPrototypeOf(received) === null ? `\nReceived value has no prototype\nReceived value: ${printReceived( received, @@ -299,7 +297,6 @@ const matchers: MatchersObject = { : printReceivedConstructorName( 'Received constructor', received.constructor, - false, )); return {message, pass}; diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts index b2b961cb5a0f..1d84c9265a55 100644 --- a/packages/expect/src/print.ts +++ b/packages/expect/src/print.ts @@ -137,14 +137,35 @@ export const printDiffOrStringify = ( export const printExpectedConstructorName = ( label: string, expected: Function, - isNot: boolean, -) => printConstructorName(label, expected, isNot, true); +) => 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, - isNot: boolean, -) => printConstructorName(label, received, isNot, false); +) => 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, @@ -153,11 +174,11 @@ const printConstructorName = ( isExpected: boolean, ): string => typeof constructor.name !== 'string' - ? `${label} name is not a string\n` + ? `${label} name is not a string` : constructor.name.length === 0 - ? `${label} name is an empty string\n` + ? `${label} name is an empty string` : `${label}: ${!isNot ? '' : isExpected ? 'not ' : ' '}${ isExpected ? EXPECTED_COLOR(constructor.name) : RECEIVED_COLOR(constructor.name) - }\n`; + }`; diff --git a/packages/expect/src/toThrowMatchers.ts b/packages/expect/src/toThrowMatchers.ts index d6092e14b462..f1e6c49f42bd 100644 --- a/packages/expect/src/toThrowMatchers.ts +++ b/packages/expect/src/toThrowMatchers.ts @@ -19,7 +19,9 @@ import { } from 'jest-matcher-utils'; import { printExpectedConstructorName, + printExpectedConstructorNameNot, printReceivedConstructorName, + printReceivedConstructorNameNot, printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; @@ -252,15 +254,15 @@ const toThrowExpectedClass = ( ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - printExpectedConstructorName('Expected constructor', expected, true) + + printExpectedConstructorNameNot('Expected constructor', expected) + (thrown !== null && thrown.value != null && typeof thrown.value.constructor === 'function' && thrown.value.constructor !== expected - ? printReceivedConstructorName( + ? printReceivedConstructorNameNot( 'Received constructor', thrown.value.constructor, - true, + expected, ) : '') + '\n' + @@ -271,7 +273,7 @@ const toThrowExpectedClass = ( : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - printExpectedConstructorName('Expected constructor', expected, false) + + printExpectedConstructorName('Expected constructor', expected) + (thrown === null ? '\n' + DID_NOT_THROW : (thrown.value != null && @@ -279,7 +281,6 @@ const toThrowExpectedClass = ( ? printReceivedConstructorName( 'Received constructor', thrown.value.constructor, - false, ) : '') + '\n' +