diff --git a/CHANGELOG.md b/CHANGELOG.md index fadadb127fa3..49a7efb2a0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,7 +125,7 @@ - `[babel-plugin-jest-hoist]` Ignore TS type annotations when looking for out-of-scope references ([#7641](https://github.com/facebook/jest/pull/7641)) - `[jest-config]` Add name to project if one does not exist to pick correct resolver ([#5862](https://github.com/facebook/jest/pull/5862)) - `[jest-runtime]` Pass `watchPathIgnorePatterns` to Haste instance ([#7585](https://github.com/facebook/jest/pull/7585)) -- `[jest-runtime]` Resolve mock files via Haste when using `require.resolve` ([#7687](https://github.com/facebook/jest/pull/7585)) +- `[jest-runtime]` Resolve mock files via Haste when using `require.resolve` ([#7687](https://github.com/facebook/jest/pull/7687)) ### Chore & Maintenance diff --git a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap index 2d2df6b246e0..c9779d1f78b8 100644 --- a/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/toThrowMatchers.test.js.snap @@ -1,5 +1,91 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`.toThrow() asymmetric any-Class fail isNot false 1`] = ` +"expect(received).toThrow(expected) + +Expected asymmetric matcher: Any + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric any-Class fail isNot true 1`] = ` +"expect(received).not.toThrow(expected) + +Expected asymmetric matcher: Any + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric anything fail isNot false 1`] = ` +"expect(received).toThrow(expected) + +Expected asymmetric matcher: Anything + +Thrown value: null +" +`; + +exports[`.toThrow() asymmetric anything fail isNot true 1`] = ` +"expect(received).not.toThrow(expected) + +Expected asymmetric matcher: Anything + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric no-symbol fail isNot false 1`] = ` +"expect(received).toThrow(expected) + +Expected asymmetric matcher: {\\"asymmetricMatch\\": [Function asymmetricMatch]} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric no-symbol fail isNot true 1`] = ` +"expect(received).not.toThrow(expected) + +Expected asymmetric matcher: {\\"asymmetricMatch\\": [Function asymmetricMatch]} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric objectContaining fail isNot false 1`] = ` +"expect(received).toThrow(expected) + +Expected asymmetric matcher: ObjectContaining {\\"name\\": \\"NotError\\"} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrow() asymmetric objectContaining fail isNot true 1`] = ` +"expect(received).not.toThrow(expected) + +Expected asymmetric matcher: ObjectContaining {\\"name\\": \\"Error\\"} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + exports[`.toThrow() error class did not throw at all 1`] = ` "expect(received).toThrow(expected) @@ -39,6 +125,22 @@ Received message: \\"apple\\" at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; +exports[`.toThrow() error-message fail isNot false 1`] = ` +"expect(received).toThrow(expected) + +Expected message: \\"apple\\" +Received message: \\"banana\\" +" +`; + +exports[`.toThrow() error-message fail isNot true 1`] = ` +"expect(received).not.toThrow(expected) + +Expected message: \\"apple\\" +Received message: \\"apple\\" +" +`; + exports[`.toThrow() expected is undefined threw, but should not have (non-error falsey) 1`] = ` "expect(received).not.toThrow() @@ -174,6 +276,92 @@ Received value: \\"Internal Server Error\\" " `; +exports[`.toThrowError() asymmetric any-Class fail isNot false 1`] = ` +"expect(received).toThrowError(expected) + +Expected asymmetric matcher: Any + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric any-Class fail isNot true 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected asymmetric matcher: Any + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric anything fail isNot false 1`] = ` +"expect(received).toThrowError(expected) + +Expected asymmetric matcher: Anything + +Thrown value: null +" +`; + +exports[`.toThrowError() asymmetric anything fail isNot true 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected asymmetric matcher: Anything + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric no-symbol fail isNot false 1`] = ` +"expect(received).toThrowError(expected) + +Expected asymmetric matcher: {\\"asymmetricMatch\\": [Function asymmetricMatch]} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric no-symbol fail isNot true 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected asymmetric matcher: {\\"asymmetricMatch\\": [Function asymmetricMatch]} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric objectContaining fail isNot false 1`] = ` +"expect(received).toThrowError(expected) + +Expected asymmetric matcher: ObjectContaining {\\"name\\": \\"NotError\\"} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + +exports[`.toThrowError() asymmetric objectContaining fail isNot true 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected asymmetric matcher: ObjectContaining {\\"name\\": \\"Error\\"} + +Received name: \\"Error\\" +Received message: \\"apple\\" + + at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" +`; + exports[`.toThrowError() error class did not throw at all 1`] = ` "expect(received).toThrowError(expected) @@ -213,6 +401,22 @@ Received message: \\"apple\\" at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)" `; +exports[`.toThrowError() error-message fail isNot false 1`] = ` +"expect(received).toThrowError(expected) + +Expected message: \\"apple\\" +Received message: \\"banana\\" +" +`; + +exports[`.toThrowError() error-message fail isNot true 1`] = ` +"expect(received).not.toThrowError(expected) + +Expected message: \\"apple\\" +Received message: \\"apple\\" +" +`; + exports[`.toThrowError() expected is undefined threw, but should not have (non-error falsey) 1`] = ` "expect(received).not.toThrowError() diff --git a/packages/expect/src/__tests__/toThrowMatchers.test.js b/packages/expect/src/__tests__/toThrowMatchers.test.js index 251cdcd44c81..88f0ee1447d7 100644 --- a/packages/expect/src/__tests__/toThrowMatchers.test.js +++ b/packages/expect/src/__tests__/toThrowMatchers.test.js @@ -191,6 +191,218 @@ class customError extends Error { }); }); + describe('error-message', () => { + // Received message in report if object has message property. + class ErrorMessage { + // not extending Error! + constructor(message) { + this.message = message; + } + } + const expected = new ErrorMessage('apple'); + + describe('pass', () => { + test('isNot false', () => { + jestExpect(() => { + throw new ErrorMessage('apple'); + })[toThrow](expected); + }); + + test('isNot true', () => { + jestExpect(() => { + throw new ErrorMessage('banana'); + }).not[toThrow](expected); + }); + }); + + describe('fail', () => { + test('isNot false', () => { + expect(() => + jestExpect(() => { + throw new ErrorMessage('banana'); + })[toThrow](expected), + ).toThrowErrorMatchingSnapshot(); + }); + + test('isNot true', () => { + expect(() => + jestExpect(() => { + throw new ErrorMessage('apple'); + }).not[toThrow](expected), + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); + + describe('asymmetric', () => { + describe('any-Class', () => { + describe('pass', () => { + test('isNot false', () => { + jestExpect(() => { + throw new Err('apple'); + })[toThrow](expect.any(Err)); + }); + + test('isNot true', () => { + jestExpect(() => { + throw new Err('apple'); + }).not[toThrow](expect.any(Err2)); + }); + }); + + describe('fail', () => { + test('isNot false', () => { + expect(() => + jestExpect(() => { + throw new Err('apple'); + })[toThrow](expect.any(Err2)), + ).toThrowErrorMatchingSnapshot(); + }); + + test('isNot true', () => { + expect(() => + jestExpect(() => { + throw new Err('apple'); + }).not[toThrow](expect.any(Err)), + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); + + describe('anything', () => { + describe('pass', () => { + test('isNot false', () => { + jestExpect(() => { + throw new customError('apple'); + })[toThrow](expect.anything()); + }); + + test('isNot true', () => { + jestExpect(() => {}).not[toThrow](expect.anything()); + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw null; + }).not[toThrow](expect.anything()); + }); + }); + + describe('fail', () => { + test('isNot false', () => { + expect(() => + jestExpect(() => { + // eslint-disable-next-line no-throw-literal + throw null; + })[toThrow](expect.anything()), + ).toThrowErrorMatchingSnapshot(); + }); + + test('isNot true', () => { + expect(() => + jestExpect(() => { + throw new customError('apple'); + }).not[toThrow](expect.anything()), + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); + + describe('no-symbol', () => { + // Test serialization of asymmetric matcher which has no property: + // this.$$typeof = Symbol.for('jest.asymmetricMatcher') + const matchError = { + asymmetricMatch(received) { + return ( + received !== null && + received !== undefined && + received.name === 'Error' + ); + }, + }; + const matchNotError = { + asymmetricMatch(received) { + return ( + received !== null && + received !== undefined && + received.name !== 'Error' + ); + }, + }; + + describe('pass', () => { + test('isNot false', () => { + jestExpect(() => { + throw new customError('apple'); + })[toThrow](matchError); + }); + + test('isNot true', () => { + jestExpect(() => { + throw new customError('apple'); + }).not[toThrow](matchNotError); + }); + }); + + describe('fail', () => { + test('isNot false', () => { + expect(() => + jestExpect(() => { + throw new customError('apple'); + })[toThrow](matchNotError), + ).toThrowErrorMatchingSnapshot(); + }); + + test('isNot true', () => { + expect(() => + jestExpect(() => { + throw new customError('apple'); + }).not[toThrow](matchError), + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); + + describe('objectContaining', () => { + const matchError = expect.objectContaining({ + name: 'Error', + }); + const matchNotError = expect.objectContaining({ + name: 'NotError', + }); + + describe('pass', () => { + test('isNot false', () => { + jestExpect(() => { + throw new customError('apple'); + })[toThrow](matchError); + }); + + test('isNot true', () => { + jestExpect(() => { + throw new customError('apple'); + }).not[toThrow](matchNotError); + }); + }); + + describe('fail', () => { + test('isNot false', () => { + expect(() => + jestExpect(() => { + throw new customError('apple'); + })[toThrow](matchNotError), + ).toThrowErrorMatchingSnapshot(); + }); + + test('isNot true', () => { + expect(() => + jestExpect(() => { + throw new customError('apple'); + }).not[toThrow](matchError), + ).toThrowErrorMatchingSnapshot(); + }); + }); + }); + }); + describe('promise/async throws if Error-like object is returned', () => { const asyncFn = async (shouldThrow?: boolean, resolve?: boolean) => { let err; diff --git a/packages/expect/src/toThrowMatchers.js b/packages/expect/src/toThrowMatchers.js index 1dfd4f27fdf6..1f2aa67aa3f5 100644 --- a/packages/expect/src/toThrowMatchers.js +++ b/packages/expect/src/toThrowMatchers.js @@ -25,32 +25,44 @@ const DID_NOT_THROW = 'Received function did not throw'; type Thrown = | { + hasMessage: true, isError: true, message: string, value: Error, } | { + hasMessage: boolean, 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, - }; +const getThrown = (e: any): Thrown => { + if ( + e !== null && + e !== undefined && + typeof e.message === 'string' && + typeof e.name === 'string' && + typeof e.stack === 'string' + ) { + return { + hasMessage: true, + isError: true, + message: e.message, + value: e, + }; + } + + const hasMessage = + e !== null && e !== undefined && typeof e.message === 'string'; + + return { + hasMessage, + isError: false, + message: hasMessage ? e.message : typeof e === 'string' ? e : String(e), + value: e, + }; +}; export const createMatcher = (matcherName: string, fromPromise?: boolean) => function(received: Function, expected: any) { @@ -92,6 +104,11 @@ export const createMatcher = (matcherName: string, fromPromise?: boolean) => 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.asymmetricMatch === 'function' + ) { + return toThrowExpectedAsymmetric(matcherName, options, thrown, expected); } else if (expected !== null && typeof expected === 'object') { return toThrowExpectedObject(matcherName, options, thrown, expected); } else { @@ -119,27 +136,65 @@ const toThrowExpectedRegExp = ( 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)) + (thrown !== null && thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')) : () => 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)); + : thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')); + + return {message, pass}; +}; + +type AsymmetricMatcher = { + asymmetricMatch: (received: any) => boolean, +}; + +const toThrowExpectedAsymmetric = ( + matcherName: string, + options: MatcherHintOptions, + thrown: Thrown | null, + expected: AsymmetricMatcher, +) => { + const pass = thrown !== null && expected.asymmetricMatch(thrown.value); + + const message = pass + ? () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected asymmetric matcher: ', expected) + + '\n' + + (thrown !== null && thrown.hasMessage + ? formatReceived('Received name: ', thrown, 'name') + + formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Thrown value: ', thrown, 'value')) + : () => + matcherHint(matcherName, undefined, undefined, options) + + '\n\n' + + formatExpected('Expected asymmetric matcher: ', expected) + + '\n' + + (thrown === null + ? DID_NOT_THROW + : thrown.hasMessage + ? formatReceived('Received name: ', thrown, 'name') + + formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Thrown value: ', thrown, 'value')); return {message, pass}; }; @@ -151,27 +206,26 @@ const toThrowExpectedObject = ( expected: Object, ) => { const pass = thrown !== null && thrown.message === expected.message; - const isNotError = thrown !== null && !thrown.isError; const message = pass ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + formatExpected('Expected message: ', expected.message) + - (isNotError - ? formatReceived('Received value: ', thrown, 'value') - : formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)) + (thrown !== null && thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')) : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + formatExpected('Expected message: ', expected.message) + (thrown === null ? '\n' + DID_NOT_THROW - : isNotError - ? formatReceived('Received value: ', thrown, 'value') - : formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)); + : thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')); return {message, pass}; }; @@ -183,7 +237,6 @@ const toThrowExpectedClass = ( expected: Function, ) => { const pass = thrown !== null && thrown.value instanceof expected; - const isNotError = thrown !== null && !thrown.isError; const message = pass ? () => @@ -192,22 +245,22 @@ const toThrowExpectedClass = ( formatExpected('Expected name: ', expected.name) + formatReceived('Received name: ', thrown, 'name') + '\n' + - (isNotError - ? formatReceived('Received value: ', thrown, 'value') - : formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)) + (thrown !== null && thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')) : () => 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') + + : thrown.hasMessage + ? formatReceived('Received name: ', thrown, 'name') + '\n' + formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)); + formatStack(thrown) + : '\n' + formatReceived('Received value: ', thrown, 'value')); return {message, pass}; }; @@ -219,27 +272,26 @@ const toThrowExpectedString = ( expected: string, ) => { const pass = thrown !== null && thrown.message.includes(expected); - const isNotError = thrown !== null && !thrown.isError; const message = pass ? () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + formatExpected('Expected substring: ', expected) + - (isNotError - ? formatReceived('Received value: ', thrown, 'value') - : formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)) + (thrown !== null && thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')) : () => matcherHint(matcherName, undefined, undefined, options) + '\n\n' + formatExpected('Expected substring: ', expected) + (thrown === null ? '\n' + DID_NOT_THROW - : isNotError - ? formatReceived('Received value: ', thrown, 'value') - : formatReceived('Received message: ', thrown, 'message') + - formatStack(thrown)); + : thrown.hasMessage + ? formatReceived('Received message: ', thrown, 'message') + + formatStack(thrown) + : formatReceived('Received value: ', thrown, 'value')); return {message, pass}; }; @@ -250,17 +302,16 @@ const toThrow = ( thrown: Thrown | null, ) => { const pass = thrown !== null; - const isNotError = thrown !== null && !thrown.isError; const message = pass ? () => matcherHint(matcherName, undefined, '', options) + '\n\n' + - (isNotError - ? formatReceived('Thrown value: ', thrown, 'value') - : formatReceived('Error name: ', thrown, 'name') + + (thrown !== null && thrown.hasMessage + ? formatReceived('Error name: ', thrown, 'name') + formatReceived('Error message: ', thrown, 'message') + - formatStack(thrown)) + formatStack(thrown) + : formatReceived('Thrown value: ', thrown, 'value')) : () => matcherHint(matcherName, undefined, '', options) + '\n\n' +