diff --git a/CHANGELOG.md b/CHANGELOG.md index ae01d5fc11cd..1c60e81dd02b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - `[jest-config]` Allow % based configuration of `--max-workers` ([#7494](https://github.com/facebook/jest/pull/7494)) - `[jest-runner]` Instantiate the test environment class with the current `testPath` ([#7442](https://github.com/facebook/jest/pull/7442)) - `[jest-config]` Always resolve jest-environment-jsdom from jest-config ([#7476](https://github.com/facebook/jest/pull/7476)) +- `[expect]` Improve report when assertion fails, part 6 ([#7621](https://github.com/facebook/jest/pull/7621)) ### Fixes diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index f9c9278be63f..81c79eae95d1 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -1168,7 +1168,14 @@ test('throws on octopus', () => { }); ``` -If you want to test that a specific error gets thrown, you can provide an argument to `toThrow`. The argument can be a string that should be contained in the error message, a class for the error, or a regex that should match the error message. For example, let's say that `drinkFlavor` is coded like this: +To test that a specific error is thrown, you can provide an argument: + +- regular expression: error message **matches** the pattern +- string: error message **includes** the substring +- error object: error message is **equal to** the message property of the object +- error class: error object is **instance of** class + +For example, let's say that `drinkFlavor` is coded like this: ```js function drinkFlavor(flavor) { @@ -1193,6 +1200,7 @@ test('throws on octopus', () => { // Test the exact error message expect(drinkOctopus).toThrowError(/^yuck, octopus flavor$/); + expect(drinkOctopus).toThrowError(new Error('yuck, octopus flavor')); // Test that we get a DisgustingFlavorError expect(drinkOctopus).toThrowError(DisgustingFlavorError); diff --git a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap index f02f4d14a79c..2d2df6b246e0 100644 --- a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap @@ -1,35 +1,53 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`.toThrow() error class did not throw at all 1`] = ` -"expect(function).toThrow(type) +"expect(received).toThrow(expected) -Expected the function to throw an error of type: - \\"Err\\" -But it didn't throw anything." +Expected name: \\"Err\\" + +Received function did not throw" `; -exports[`.toThrow() error class threw, but class did not match 1`] = ` -"expect(function).toThrow(type) +exports[`.toThrow() error class threw, but class did not match (error) 1`] = ` +"expect(received).toThrow(expected) + +Expected name: \\"Err2\\" +Received name: \\"Error\\" + +Received message: \\"apple\\" -Expected the function to throw an error of type: - \\"Err2\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrow() error class threw, but should not have 1`] = ` -"expect(function).not.toThrow(type) +exports[`.toThrow() error class threw, but class did not match (non-error falsey) 1`] = ` +"expect(received).toThrow(expected) + +Expected name: \\"Err2\\" + +Received value: undefined +" +`; + +exports[`.toThrow() error class threw, but class should not match (error) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected name: \\"Err\\" +Received name: \\"Error\\" + +Received message: \\"apple\\" -Expected the function not to throw an error of type: - \\"Err\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() expected is undefined threw, but should not have (non-error falsey) 1`] = ` +"expect(received).not.toThrow() + +Thrown value: null +" `; exports[`.toThrow() invalid actual 1`] = ` -"expect(received)[.not].toThrow(expected) +"expect(received).toThrow() Matcher error: received value must be a function @@ -38,126 +56,172 @@ Received has value: \\"a string\\"" `; exports[`.toThrow() invalid arguments 1`] = ` -"expect(received)[.not].toThrow(expected) +"expect(received).not.toThrow(expected) -Matcher error: expected value must be a string or regular expression or Error +Matcher error: expected value must be a string or regular expression or class or error Expected has type: number Expected has value: 111" `; exports[`.toThrow() promise/async throws if Error-like object is returned did not throw at all 1`] = ` -[Error: expect(function).toThrow(undefined) +"expect(received).rejects.toThrow() -Expected the function to throw an error. -But it didn't throw anything.] +Received function did not throw" `; exports[`.toThrow() promise/async throws if Error-like object is returned threw, but class did not match 1`] = ` -[Error: expect(function).toThrow(type) +"expect(received).rejects.toThrow(expected) + +Expected name: \\"Err2\\" +Received name: \\"Error\\" -Expected the function to throw an error of type: - "Err2" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)] +Received message: \\"async apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; exports[`.toThrow() promise/async throws if Error-like object is returned threw, but should not have 1`] = ` -[Error: expect(function).not.toThrow() +"expect(received).rejects.not.toThrow() + +Error name: \\"Error\\" +Error message: \\"async apple\\" -Expected the function not to throw an error. -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)] + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; exports[`.toThrow() regexp did not throw at all 1`] = ` -"expect(function).toThrow(regexp) +"expect(received).toThrow(expected) -Expected the function to throw an error matching: - /apple/ -But it didn't throw anything." +Expected pattern: /apple/ + +Received function did not throw" `; -exports[`.toThrow() regexp threw, but message did not match 1`] = ` -"expect(function).toThrow(regexp) +exports[`.toThrow() regexp threw, but message did not match (error) 1`] = ` +"expect(received).toThrow(expected) + +Expected pattern: /banana/ +Received message: \\"apple\\" -Expected the function to throw an error matching: - /banana/ -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrow() regexp threw, but should not have 1`] = ` -"expect(function).not.toThrow(regexp) +exports[`.toThrow() regexp threw, but message did not match (non-error falsey) 1`] = ` +"expect(received).toThrow(expected) -Expected the function not to throw an error matching: - /apple/ -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +Expected pattern: /^[123456789]\\\\d*/ +Received value: 0 +" +`; + +exports[`.toThrow() regexp threw, but message should not match (error) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected pattern: /apple/ +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() regexp threw, but message should not match (non-error truthy) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected pattern: /^[123456789]\\\\d*/ +Received value: 404 +" `; exports[`.toThrow() strings did not throw at all 1`] = ` -"expect(function).toThrow(string) +"expect(received).toThrow(expected) + +Expected substring: \\"apple\\" -Expected the function to throw an error matching: - \\"apple\\" -But it didn't throw anything." +Received function did not throw" `; -exports[`.toThrow() strings threw, but message did not match 1`] = ` -"expect(function).toThrow(string) +exports[`.toThrow() strings threw, but message did not match (error) 1`] = ` +"expect(received).toThrow(expected) + +Expected substring: \\"banana\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() strings threw, but message did not match (non-error falsey) 1`] = ` +"expect(received).toThrow(expected) + +Expected substring: \\"Server Error\\" +Received value: \\"\\" +" +`; + +exports[`.toThrow() strings threw, but message should not match (error) 1`] = ` +"expect(received).not.toThrow(expected) + +Expected substring: \\"apple\\" +Received message: \\"apple\\" -Expected the function to throw an error matching: - \\"banana\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrow() strings threw, but should not have 1`] = ` -"expect(function).not.toThrow(string) +exports[`.toThrow() strings threw, but message should not match (non-error truthy) 1`] = ` +"expect(received).not.toThrow(expected) -Expected the function not to throw an error matching: - \\"apple\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +Expected substring: \\"Server Error\\" +Received value: \\"Internal Server Error\\" +" `; exports[`.toThrowError() error class did not throw at all 1`] = ` -"expect(function).toThrowError(type) +"expect(received).toThrowError(expected) -Expected the function to throw an error of type: - \\"Err\\" -But it didn't throw anything." +Expected name: \\"Err\\" + +Received function did not throw" `; -exports[`.toThrowError() error class threw, but class did not match 1`] = ` -"expect(function).toThrowError(type) +exports[`.toThrowError() error class threw, but class did not match (error) 1`] = ` +"expect(received).toThrowError(expected) + +Expected name: \\"Err2\\" +Received name: \\"Error\\" + +Received message: \\"apple\\" -Expected the function to throw an error of type: - \\"Err2\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrowError() error class threw, but should not have 1`] = ` -"expect(function).not.toThrowError(type) +exports[`.toThrowError() error class threw, but class did not match (non-error falsey) 1`] = ` +"expect(received).toThrowError(expected) + +Expected name: \\"Err2\\" + +Received value: undefined +" +`; + +exports[`.toThrowError() error class threw, but class should not match (error) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected name: \\"Err\\" +Received name: \\"Error\\" + +Received message: \\"apple\\" -Expected the function not to throw an error of type: - \\"Err\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() expected is undefined threw, but should not have (non-error falsey) 1`] = ` +"expect(received).not.toThrowError() + +Thrown value: null +" `; exports[`.toThrowError() invalid actual 1`] = ` -"expect(received)[.not].toThrowError(expected) +"expect(received).toThrowError() Matcher error: received value must be a function @@ -166,92 +230,120 @@ Received has value: \\"a string\\"" `; exports[`.toThrowError() invalid arguments 1`] = ` -"expect(received)[.not].toThrowError(expected) +"expect(received).not.toThrowError(expected) -Matcher error: expected value must be a string or regular expression or Error +Matcher error: expected value must be a string or regular expression or class or error Expected has type: number Expected has value: 111" `; exports[`.toThrowError() promise/async throws if Error-like object is returned did not throw at all 1`] = ` -[Error: expect(function).toThrow(undefined) +"expect(received).rejects.toThrowError() -Expected the function to throw an error. -But it didn't throw anything.] +Received function did not throw" `; exports[`.toThrowError() promise/async throws if Error-like object is returned threw, but class did not match 1`] = ` -[Error: expect(function).toThrow(type) +"expect(received).rejects.toThrowError(expected) + +Expected name: \\"Err2\\" +Received name: \\"Error\\" -Expected the function to throw an error of type: - "Err2" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)] +Received message: \\"async apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; exports[`.toThrowError() promise/async throws if Error-like object is returned threw, but should not have 1`] = ` -[Error: expect(function).not.toThrow() +"expect(received).rejects.not.toThrowError() + +Error name: \\"Error\\" +Error message: \\"async apple\\" -Expected the function not to throw an error. -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)] + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; exports[`.toThrowError() regexp did not throw at all 1`] = ` -"expect(function).toThrowError(regexp) +"expect(received).toThrowError(expected) -Expected the function to throw an error matching: - /apple/ -But it didn't throw anything." +Expected pattern: /apple/ + +Received function did not throw" `; -exports[`.toThrowError() regexp threw, but message did not match 1`] = ` -"expect(function).toThrowError(regexp) +exports[`.toThrowError() regexp threw, but message did not match (error) 1`] = ` +"expect(received).toThrowError(expected) + +Expected pattern: /banana/ +Received message: \\"apple\\" -Expected the function to throw an error matching: - /banana/ -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrowError() regexp threw, but should not have 1`] = ` -"expect(function).not.toThrowError(regexp) +exports[`.toThrowError() regexp threw, but message did not match (non-error falsey) 1`] = ` +"expect(received).toThrowError(expected) -Expected the function not to throw an error matching: - /apple/ -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +Expected pattern: /^[123456789]\\\\d*/ +Received value: 0 +" +`; + +exports[`.toThrowError() regexp threw, but message should not match (error) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected pattern: /apple/ +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() regexp threw, but message should not match (non-error truthy) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected pattern: /^[123456789]\\\\d*/ +Received value: 404 +" `; exports[`.toThrowError() strings did not throw at all 1`] = ` -"expect(function).toThrowError(string) +"expect(received).toThrowError(expected) + +Expected substring: \\"apple\\" -Expected the function to throw an error matching: - \\"apple\\" -But it didn't throw anything." +Received function did not throw" `; -exports[`.toThrowError() strings threw, but message did not match 1`] = ` -"expect(function).toThrowError(string) +exports[`.toThrowError() strings threw, but message did not match (error) 1`] = ` +"expect(received).toThrowError(expected) + +Expected substring: \\"banana\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() strings threw, but message did not match (non-error falsey) 1`] = ` +"expect(received).toThrowError(expected) + +Expected substring: \\"Server Error\\" +Received value: \\"\\" +" +`; + +exports[`.toThrowError() strings threw, but message should not match (error) 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected substring: \\"apple\\" +Received message: \\"apple\\" -Expected the function to throw an error matching: - \\"banana\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; -exports[`.toThrowError() strings threw, but should not have 1`] = ` -"expect(function).not.toThrowError(string) +exports[`.toThrowError() strings threw, but message should not match (non-error truthy) 1`] = ` +"expect(received).not.toThrowError(expected) -Expected the function not to throw an error matching: - \\"apple\\" -Instead, it threw: - Error - at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +Expected substring: \\"Server Error\\" +Received value: \\"Internal Server Error\\" +" `; diff --git a/packages/expect/src/__tests__/toThrowMatchers.test.js b/packages/expect/src/__tests__/toThrowMatchers.test.js index 1aa0efbb7e72..251cdcd44c81 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.js +++ b/packages/expect/src/__tests__/toThrowMatchers.test.js @@ -52,7 +52,7 @@ class customError extends Error { ).toThrowErrorMatchingSnapshot(); }); - test('threw, but message did not match', () => { + test('threw, but message did not match (error)', () => { expect(() => { jestExpect(() => { throw new customError('apple'); @@ -60,19 +60,37 @@ class customError extends Error { }).toThrowErrorMatchingSnapshot(); }); + test('threw, but message did not match (non-error falsey)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw ''; + })[toThrow]('Server Error'); + }).toThrowErrorMatchingSnapshot(); + }); + it('properly escapes strings when matching against errors', () => { jestExpect(() => { throw new TypeError('"this"? throws.'); })[toThrow]('"this"? throws.'); }); - test('threw, but should not have', () => { + test('threw, but message should not match (error)', () => { expect(() => { jestExpect(() => { throw new customError('apple'); }).not[toThrow]('apple'); }).toThrowErrorMatchingSnapshot(); }); + + test('threw, but message should not match (non-error truthy)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw 'Internal Server Error'; + }).not[toThrow]('Server Error'); + }).toThrowErrorMatchingSnapshot(); + }); }); describe('regexp', () => { @@ -92,7 +110,7 @@ class customError extends Error { ).toThrowErrorMatchingSnapshot(); }); - test('threw, but message did not match', () => { + test('threw, but message did not match (error)', () => { expect(() => { jestExpect(() => { throw new customError('apple'); @@ -100,13 +118,31 @@ class customError extends Error { }).toThrowErrorMatchingSnapshot(); }); - test('threw, but should not have', () => { + test('threw, but message did not match (non-error falsey)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw 0; + })[toThrow](/^[123456789]\d*/); + }).toThrowErrorMatchingSnapshot(); + }); + + test('threw, but message should not match (error)', () => { expect(() => { jestExpect(() => { throw new customError('apple'); }).not[toThrow](/apple/); }).toThrowErrorMatchingSnapshot(); }); + + test('threw, but message should not match (non-error truthy)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw 404; + }).not[toThrow](/^[123456789]\d*/); + }).toThrowErrorMatchingSnapshot(); + }); }); describe('error class', () => { @@ -129,7 +165,7 @@ class customError extends Error { ).toThrowErrorMatchingSnapshot(); }); - test('threw, but class did not match', () => { + test('threw, but class did not match (error)', () => { expect(() => { jestExpect(() => { throw new Err('apple'); @@ -137,7 +173,16 @@ class customError extends Error { }).toThrowErrorMatchingSnapshot(); }); - test('threw, but should not have', () => { + test('threw, but class did not match (non-error falsey)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw undefined; + })[toThrow](Err2); + }).toThrowErrorMatchingSnapshot(); + }); + + test('threw, but class should not match (error)', () => { expect(() => { jestExpect(() => { throw new Err('apple'); @@ -199,39 +244,38 @@ class customError extends Error { }); test('did not throw at all', async () => { - let err; - try { - await jestExpect(asyncFn()).rejects.toThrow(); - } catch (error) { - err = error; - } - expect(err).toMatchSnapshot(); + await expect( + jestExpect(asyncFn()).rejects[toThrow](), + ).rejects.toThrowErrorMatchingSnapshot(); }); test('threw, but class did not match', async () => { - let err; - try { - await jestExpect(asyncFn(true)).rejects.toThrow(Err2); - } catch (error) { - err = error; - } - expect(err).toMatchSnapshot(); + await expect( + jestExpect(asyncFn(true)).rejects[toThrow](Err2), + ).rejects.toThrowErrorMatchingSnapshot(); }); test('threw, but should not have', async () => { - let err; - try { - await jestExpect(asyncFn(true)).rejects.not.toThrow(); - } catch (error) { - err = error; - } - expect(err).toMatchSnapshot(); + await expect( + jestExpect(asyncFn(true)).rejects.not[toThrow](), + ).rejects.toThrowErrorMatchingSnapshot(); + }); + }); + + describe('expected is undefined', () => { + test('threw, but should not have (non-error falsey)', () => { + expect(() => { + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw null; + }).not[toThrow](); + }).toThrowErrorMatchingSnapshot(); }); }); test('invalid arguments', () => { expect(() => - jestExpect(() => {})[toThrow](111), + jestExpect(() => {}).not[toThrow](111), ).toThrowErrorMatchingSnapshot(); }); diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index ca6cacf16e3b..1c41fc8e683f 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -66,7 +66,7 @@ const createToThrowErrorMatchingSnapshotMatcher = function(matcher) { const getPromiseMatcher = (name, matcher) => { if (name === 'toThrow' || name === 'toThrowError') { - return createThrowMatcher('.' + name, true); + return createThrowMatcher(name, true); } else if ( name === 'toThrowErrorMatchingSnapshot' || name === 'toThrowErrorMatchingInlineSnapshot' diff --git a/packages/expect/src/toThrowMatchers.js b/packages/expect/src/toThrowMatchers.js index af2ae6c01275..1dfd4f27fdf6 100644 --- a/packages/expect/src/toThrowMatchers.js +++ b/packages/expect/src/toThrowMatchers.js @@ -7,203 +7,305 @@ * @flow */ -import type {MatchersObject} from 'types/Matchers'; +import type {MatcherHintOptions, MatchersObject} from 'types/Matchers'; -import getType from 'jest-get-type'; -import {escapeStrForRegex} from 'jest-regex-util'; import {formatStackTrace, separateMessageFromStack} from 'jest-message-util'; import { EXPECTED_COLOR, RECEIVED_COLOR, - highlightTrailingWhitespace, matcherErrorMessage, matcherHint, printExpected, printReceived, printWithType, } from 'jest-matcher-utils'; -import {equals} from './jasmineUtils'; import {isError} from './utils'; -export const createMatcher = (matcherName: string, fromPromise?: boolean) => ( - actual: Function, - expected: string | Error | RegExp, -) => { - const value = expected; - let error; - - if (fromPromise && isError(actual)) { - error = actual; - } else { - if (typeof actual !== 'function') { - if (!fromPromise) { - throw new Error( - matcherErrorMessage( - matcherHint('[.not]' + matcherName, undefined, undefined), - `${RECEIVED_COLOR('received')} value must be a function`, - printWithType('Received', actual, printReceived), - ), - ); +const DID_NOT_THROW = 'Received function did not throw'; + +type Thrown = + | { + isError: true, + message: string, + value: Error, + } + | { + isError: false, + message: string, + value: any, + }; + +const getThrown = (e: any): Thrown => + e !== null && + e !== undefined && + typeof e.message === 'string' && + typeof e.name === 'string' && + typeof e.stack === 'string' + ? { + isError: true, + message: e.message, + value: e, } + : { + isError: false, + message: typeof e === 'string' ? e : String(e), + value: e, + }; + +export const createMatcher = (matcherName: string, fromPromise?: boolean) => + function(received: Function, expected: any) { + const options = { + isNot: this.isNot, + promise: this.promise, + }; + + let thrown = null; + + if (fromPromise && isError(received)) { + thrown = getThrown(received); } else { - try { - actual(); - } catch (e) { - error = e; + if (typeof received !== 'function') { + if (!fromPromise) { + const placeholder = expected === undefined ? '' : 'expected'; + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, placeholder, options), + `${RECEIVED_COLOR('received')} value must be a function`, + printWithType('Received', received, printReceived), + ), + ); + } + } else { + try { + received(); + } catch (e) { + thrown = getThrown(e); + } } } - } - if (typeof expected === 'string') { - expected = new RegExp(escapeStrForRegex(expected)); - } + if (expected === undefined) { + return toThrow(matcherName, options, thrown); + } else if (typeof expected === 'function') { + return toThrowExpectedClass(matcherName, options, thrown, expected); + } else if (typeof expected === 'string') { + return toThrowExpectedString(matcherName, options, thrown, expected); + } else if (expected !== null && typeof expected.test === 'function') { + return toThrowExpectedRegExp(matcherName, options, thrown, expected); + } else if (expected !== null && typeof expected === 'object') { + return toThrowExpectedObject(matcherName, options, thrown, expected); + } else { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, undefined, options), + `${EXPECTED_COLOR( + 'expected', + )} value must be a string or regular expression or class or error`, + printWithType('Expected', expected, printExpected), + ), + ); + } + }; - if (typeof expected === 'function') { - return toThrowMatchingError(matcherName, error, expected); - } else if (expected && typeof expected.test === 'function') { - return toThrowMatchingStringOrRegexp( - matcherName, - error, - (expected: any), - value, - ); - } else if (expected && typeof expected === 'object') { - return toThrowMatchingErrorInstance(matcherName, error, (expected: any)); - } else if (expected === undefined) { - const pass = error !== undefined; - return { - message: pass - ? () => - matcherHint('.not' + matcherName, 'function', '') + - '\n\n' + - 'Expected the function not to throw an error.\n' + - printActualErrorMessage(error) - : () => - matcherHint(matcherName, 'function', getType(value)) + - '\n\n' + - 'Expected the function to throw an error.\n' + - printActualErrorMessage(error), - pass, - }; - } else { - throw new Error( - matcherErrorMessage( - matcherHint('[.not]' + matcherName, undefined, undefined), - `${EXPECTED_COLOR( - 'expected', - )} value must be a string or regular expression or Error`, - printWithType('Expected', expected, printExpected), - ), - ); - } +const matchers: MatchersObject = { + toThrow: createMatcher('toThrow'), + toThrowError: createMatcher('toThrowError'), }; -const matchers: MatchersObject = { - toThrow: createMatcher('.toThrow'), - toThrowError: createMatcher('.toThrowError'), +const toThrowExpectedRegExp = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, + expected: RegExp, +) => { + const pass = thrown !== null && expected.test(thrown.message); + const isNotError = thrown !== null && !thrown.isError; + + const message = pass + ? () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected pattern: ', expected) + + (isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)) + : () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected pattern: ', expected) + + (thrown === null + ? '\n' + DID_NOT_THROW + : isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)); + + return {message, pass}; }; -const toThrowMatchingStringOrRegexp = ( - name: string, - error: ?Error, - pattern: RegExp, - value: RegExp | string | Error, +const toThrowExpectedObject = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, + expected: Object, ) => { - if (error && !error.message && !error.name) { - error = new Error(error); - } + const pass = thrown !== null && thrown.message === expected.message; + const isNotError = thrown !== null && !thrown.isError; - const pass = !!(error && error.message.match(pattern)); const message = pass ? () => - matcherHint('.not' + name, 'function', getType(value)) + + matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected the function not to throw an error matching:\n` + - ` ${printExpected(value)}\n` + - printActualErrorMessage(error) + formatExpected('Expected message: ', expected.message) + + (isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)) : () => - matcherHint(name, 'function', getType(value)) + + matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected the function to throw an error matching:\n` + - ` ${printExpected(value)}\n` + - printActualErrorMessage(error); + formatExpected('Expected message: ', expected.message) + + (thrown === null + ? '\n' + DID_NOT_THROW + : isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)); return {message, pass}; }; -const toThrowMatchingErrorInstance = ( - name: string, - error: ?Error, - expectedError: Error, +const toThrowExpectedClass = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, + expected: Function, ) => { - if (error && !error.message && !error.name) { - error = new Error(error); - } + const pass = thrown !== null && thrown.value instanceof expected; + const isNotError = thrown !== null && !thrown.isError; + + const message = pass + ? () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected name: ', expected.name) + + formatReceived('Received name: ', thrown, 'name') + + '\n' + + (isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)) + : () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected name: ', expected.name) + + (thrown === null + ? '\n' + DID_NOT_THROW + : isNotError + ? '\n' + formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received name: ', thrown, 'name') + + '\n' + + formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)); + + return {message, pass}; +}; + +const toThrowExpectedString = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, + expected: string, +) => { + const pass = thrown !== null && thrown.message.includes(expected); + const isNotError = thrown !== null && !thrown.isError; - const pass = equals(error, expectedError); const message = pass ? () => - matcherHint('.not' + name, 'function', 'error') + + matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected the function not to throw an error matching:\n` + - ` ${printExpected(expectedError)}\n` + - printActualErrorMessage(error) + formatExpected('Expected substring: ', expected) + + (isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)) : () => - matcherHint(name, 'function', 'error') + + matcherHint(matcherName, undefined, undefined, options) + '\n\n' + - `Expected the function to throw an error matching:\n` + - ` ${printExpected(expectedError)}\n` + - printActualErrorMessage(error); + formatExpected('Expected substring: ', expected) + + (thrown === null + ? '\n' + DID_NOT_THROW + : isNotError + ? formatReceived('Received value: ', thrown, 'value') + : formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown)); return {message, pass}; }; -const toThrowMatchingError = ( - name: string, - error: ?Error, - ErrorClass: typeof Error, +const toThrow = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, ) => { - const pass = !!(error && error instanceof ErrorClass); + const pass = thrown !== null; + const isNotError = thrown !== null && !thrown.isError; + const message = pass ? () => - matcherHint('.not' + name, 'function', 'type') + + matcherHint(matcherName, undefined, '', options) + '\n\n' + - `Expected the function not to throw an error of type:\n` + - ` ${printExpected(ErrorClass.name)}\n` + - printActualErrorMessage(error) + (isNotError + ? formatReceived('Thrown value: ', thrown, 'value') + : formatReceived('Error name: ', thrown, 'name') + + formatReceived('Error message: ', thrown, 'message') + + formatStack(thrown)) : () => - matcherHint(name, 'function', 'type') + + matcherHint(matcherName, undefined, '', options) + '\n\n' + - `Expected the function to throw an error of type:\n` + - ` ${printExpected(ErrorClass.name)}\n` + - printActualErrorMessage(error); + DID_NOT_THROW; return {message, pass}; }; -const printActualErrorMessage = error => { - if (error) { - const {message, stack} = separateMessageFromStack(error.stack); - return ( - `Instead, it threw:\n` + - RECEIVED_COLOR( - ' ' + - highlightTrailingWhitespace(message) + - formatStackTrace( - stack, - { - rootDir: process.cwd(), - testMatch: [], - }, - { - noStackTrace: false, - }, - ), - ) - ); +const formatExpected = (label: string, expected: any) => + label + printExpected(expected) + '\n'; + +const formatReceived = (label: string, thrown: Thrown | null, key: string) => { + if (thrown === null) { + return ''; + } + + if (key === 'message') { + return label + printReceived(thrown.message) + '\n'; + } + + if (key === 'name') { + return thrown.isError + ? label + printReceived(thrown.value.name) + '\n' + : ''; } - return `But it didn't throw anything.`; + if (key === 'value') { + return thrown.isError ? '' : label + printReceived(thrown.value) + '\n'; + } + + return ''; }; +const formatStack = (thrown: Thrown | null) => + thrown === null || !thrown.isError + ? '' + : formatStackTrace( + separateMessageFromStack(thrown.value.stack).stack, + { + rootDir: process.cwd(), + testMatch: [], + }, + { + noStackTrace: false, + }, + ); + export default matchers;