diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a2785cb7c69..c4de63c19772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ### Features +* `[expect]` Add stack trace for async errors + ([#6008](https://github.com/facebook/jest/pull/6008)) +* `[jest-jasmine2]` Add stack trace for timeouts + ([#6008](https://github.com/facebook/jest/pull/6008)) +* `[jest-jasmine2]` Add stack trace for thrown non-`Error`s + ([#6008](https://github.com/facebook/jest/pull/6008)) * `[jest-runtime]` Prevent modules from marking themselves as their own parent ([#5235](https://github.com/facebook/jest/issues/5235)) * `[jest-mock]` Add support for auto-mocking generator functions diff --git a/integration-tests/__tests__/__snapshots__/expect-async-matcher.test.js.snap b/integration-tests/__tests__/__snapshots__/expect-async-matcher.test.js.snap index 53443ffd492a..3076bd3d1d5d 100644 --- a/integration-tests/__tests__/__snapshots__/expect-async-matcher.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/expect-async-matcher.test.js.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`shows the correct errors in stderr when failing tests 1`] = ` -Object { - "rest": "FAIL __tests__/failure.test.js +"FAIL __tests__/failure.test.js ✕ fail with expected non promise values ✕ fail with expected non promise values and not ✕ fail with expected promise values @@ -30,30 +29,47 @@ Object { ● fail with expected promise values - Error - Error: Expected value to have length: - 2 - Received: - 1 - received.length: - 1 + Expected value to have length: + 2 + Received: + 1 + received.length: + 1 + + 22 | + 23 | it('fail with expected promise values', async () => { + > 24 | await (expect(Promise.resolve([1])): any).resolves.toHaveLengthAsync( + | ^ + 25 | Promise.resolve(2) + 26 | ); + 27 | }); + + at __tests__/failure.test.js:24:54 + at __tests__/failure.test.js:11:191 + at __tests__/failure.test.js:11:437 + at __tests__/failure.test.js:11:99 ● fail with expected promise values and not - Error - Error: Expected value to not have length: - 2 - Received: - 1,2 - received.length: - 2 + Expected value to not have length: + 2 + Received: + 1,2 + received.length: + 2 + + 28 | + 29 | it('fail with expected promise values and not', async () => { + > 30 | await (expect(Promise.resolve([1, 2])).resolves.not: any).toHaveLengthAsync( + | ^ + 31 | Promise.resolve(2) + 32 | ); + 33 | }); + + at __tests__/failure.test.js:30:61 + at __tests__/failure.test.js:11:191 + at __tests__/failure.test.js:11:437 + at __tests__/failure.test.js:11:99 -", - "summary": "Test Suites: 1 failed, 1 total -Tests: 4 failed, 4 total -Snapshots: 0 total -Time: <> -Ran all test suites matching /failure.test.js/i. -", -} +" `; diff --git a/integration-tests/__tests__/__snapshots__/failures.test.js.snap b/integration-tests/__tests__/__snapshots__/failures.test.js.snap index e2c73766829e..4285e01d8eb2 100644 --- a/integration-tests/__tests__/__snapshots__/failures.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/failures.test.js.snap @@ -83,20 +83,50 @@ exports[`not throwing Error objects 5`] = ` ● Promise thrown during test thrown: Promise {} + + 5 | }; + 6 | + > 7 | test('Promise thrown during test', () => { + | ^ + 8 | throw Promise.resolve(5); + 9 | }); + 10 | + - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/during_tests.test.js:7:1 ● Boolean thrown during test thrown: false + + 9 | }); + 10 | + > 11 | test('Boolean thrown during test', () => { + | ^ + 12 | // eslint-disable-next-line no-throw-literal + 13 | throw false; + 14 | }); - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 + + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/during_tests.test.js:11:1 ● undefined thrown during test thrown: undefined + + 14 | }); + 15 | + > 16 | test('undefined thrown during test', () => { + | ^ + 17 | // eslint-disable-next-line no-throw-literal + 18 | throw undefined; + 19 | }); + - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/during_tests.test.js:16:1 ● Object thrown during test @@ -108,8 +138,18 @@ exports[`not throwing Error objects 5`] = ` }, ], } + + 19 | }); + 20 | + > 21 | test('Object thrown during test', () => { + | ^ + 22 | // eslint-disable-next-line no-throw-literal + 23 | throw deepObject; + 24 | }); - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 + + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/during_tests.test.js:21:1 ● Error during test @@ -150,15 +190,17 @@ exports[`not throwing Error objects 5`] = ` ], } + 33 | }); 34 | - 35 | test('done(non-error)', done => { - > 36 | done(deepObject); - | ^ + > 35 | test('done(non-error)', done => { + | ^ + 36 | done(deepObject); 37 | }); 38 | - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 - at __tests__/during_tests.test.js:36:3 + + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/during_tests.test.js:35:1 " `; @@ -191,9 +233,42 @@ exports[`works with assertions in separate files 1`] = ` exports[`works with async failures 1`] = ` "FAIL __tests__/async_failures.test.js - ✕ something async + ✕ resolve, but fail + ✕ reject, but fail + ✕ expect reject + ✕ expect resolve + ✕ timeout + + ● resolve, but fail + + expect(received).toEqual(expected) + + Expected value to equal: + {\\"baz\\": \\"bar\\"} + Received: + {\\"foo\\": \\"bar\\"} + + Difference: + + - Expected + + Received + + Object { + - \\"baz\\": \\"bar\\", + + \\"foo\\": \\"bar\\", + } + + 10 | + 11 | test('resolve, but fail', () => { + > 12 | return expect(Promise.resolve({foo: 'bar'})).resolves.toEqual({baz: 'bar'}); + | ^ + 13 | }); + 14 | + 15 | test('reject, but fail', () => { + + at __tests__/async_failures.test.js:12:57 - ● something async + ● reject, but fail expect(received).toEqual(expected) @@ -211,8 +286,65 @@ exports[`works with async failures 1`] = ` - \\"baz\\": \\"bar\\", + \\"foo\\": \\"bar\\", } + + 14 | + 15 | test('reject, but fail', () => { + > 16 | return expect(Promise.reject({foo: 'bar'})).rejects.toEqual({baz: 'bar'}); + | ^ + 17 | }); + 18 | + 19 | test('expect reject', () => { - at packages/expect/build/index.js:160:61 + at __tests__/async_failures.test.js:16:55 + + ● expect reject + + expect(received).rejects.toEqual() + + Expected received Promise to reject, instead it resolved to value + {\\"foo\\": \\"bar\\"} + + 18 | + 19 | test('expect reject', () => { + > 20 | return expect(Promise.resolve({foo: 'bar'})).rejects.toEqual({foo: 'bar'}); + | ^ + 21 | }); + 22 | + 23 | test('expect resolve', () => { + + at __tests__/async_failures.test.js:20:10 + + ● expect resolve + + expect(received).resolves.toEqual() + + Expected received Promise to resolve, instead it rejected to value + {\\"foo\\": \\"bar\\"} + + 22 | + 23 | test('expect resolve', () => { + > 24 | return expect(Promise.reject({foo: 'bar'})).resolves.toEqual({foo: 'bar'}); + | ^ + 25 | }); + 26 | + 27 | test('timeout', done => { + + at __tests__/async_failures.test.js:24:10 + + ● timeout + + Timeout - Async callback was not invoked within the 5ms timeout specified by jest.setTimeout. + + 25 | }); + 26 | + > 27 | test('timeout', done => { + | ^ + 28 | jest.setTimeout(5); + 29 | + 30 | setTimeout(done, 10); + + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/async_failures.test.js:27:1 " `; @@ -254,6 +386,8 @@ exports[`works with node assert 1`] = ` 18 | test('assert with a message', () => { at __tests__/node_assertion_error.test.js:15:3 + + at __tests__/node_assertion_error.test.js:14:1 ● assert with a message @@ -276,6 +410,8 @@ exports[`works with node assert 1`] = ` 22 | test('assert.ok', () => { at __tests__/node_assertion_error.test.js:19:3 + + at __tests__/node_assertion_error.test.js:18:1 ● assert.ok @@ -295,6 +431,8 @@ exports[`works with node assert 1`] = ` 26 | test('assert.ok with a message', () => { at __tests__/node_assertion_error.test.js:23:10 + + at __tests__/node_assertion_error.test.js:22:1 ● assert.ok with a message @@ -317,6 +455,8 @@ exports[`works with node assert 1`] = ` 30 | test('assert.equal', () => { at __tests__/node_assertion_error.test.js:27:10 + + at __tests__/node_assertion_error.test.js:26:1 ● assert.equal @@ -336,6 +476,8 @@ exports[`works with node assert 1`] = ` 34 | test('assert.notEqual', () => { at __tests__/node_assertion_error.test.js:31:10 + + at __tests__/node_assertion_error.test.js:30:1 ● assert.notEqual @@ -359,6 +501,8 @@ exports[`works with node assert 1`] = ` 38 | test('assert.deepEqual', () => { at __tests__/node_assertion_error.test.js:35:10 + + at __tests__/node_assertion_error.test.js:34:1 ● assert.deepEqual @@ -392,6 +536,8 @@ exports[`works with node assert 1`] = ` 42 | test('assert.deepEqual with a message', () => { at __tests__/node_assertion_error.test.js:39:10 + + at __tests__/node_assertion_error.test.js:38:1 ● assert.deepEqual with a message @@ -428,6 +574,8 @@ exports[`works with node assert 1`] = ` 46 | test('assert.notDeepEqual', () => { at __tests__/node_assertion_error.test.js:43:10 + + at __tests__/node_assertion_error.test.js:42:1 ● assert.notDeepEqual @@ -451,6 +599,8 @@ exports[`works with node assert 1`] = ` 50 | test('assert.strictEqual', () => { at __tests__/node_assertion_error.test.js:47:10 + + at __tests__/node_assertion_error.test.js:46:1 ● assert.strictEqual @@ -470,6 +620,8 @@ exports[`works with node assert 1`] = ` 54 | test('assert.notStrictEqual', () => { at __tests__/node_assertion_error.test.js:51:10 + + at __tests__/node_assertion_error.test.js:50:1 ● assert.notStrictEqual @@ -496,6 +648,8 @@ exports[`works with node assert 1`] = ` 58 | test('assert.deepStrictEqual', () => { at __tests__/node_assertion_error.test.js:55:10 + + at __tests__/node_assertion_error.test.js:54:1 ● assert.deepStrictEqual @@ -525,6 +679,8 @@ exports[`works with node assert 1`] = ` 62 | test('assert.notDeepStrictEqual', () => { at __tests__/node_assertion_error.test.js:59:10 + + at __tests__/node_assertion_error.test.js:58:1 ● assert.notDeepStrictEqual @@ -548,12 +704,24 @@ exports[`works with node assert 1`] = ` 66 | test('assert.ifError', () => { at __tests__/node_assertion_error.test.js:63:10 + + at __tests__/node_assertion_error.test.js:62:1 ● assert.ifError thrown: 1 + + 64 | }); + 65 | + > 66 | test('assert.ifError', () => { + | ^ + 67 | assert.ifError(1); + 68 | }); + 69 | + - at packages/jest-jasmine2/build/expectation_result_factory.js:54:47 + at packages/jest-jasmine2/build/jasmine/Spec.js:79:20 + at __tests__/node_assertion_error.test.js:66:1 ● assert.doesNotThrow @@ -575,6 +743,8 @@ exports[`works with node assert 1`] = ` 74 | }); at __tests__/node_assertion_error.test.js:71:10 + + at __tests__/node_assertion_error.test.js:70:1 ● assert.throws @@ -594,6 +764,8 @@ exports[`works with node assert 1`] = ` 79 | at __tests__/node_assertion_error.test.js:77:10 + + at __tests__/node_assertion_error.test.js:76:1 " `; diff --git a/integration-tests/__tests__/expect-async-matcher.test.js b/integration-tests/__tests__/expect-async-matcher.test.js index 24ac7e1d747f..85e668718385 100644 --- a/integration-tests/__tests__/expect-async-matcher.test.js +++ b/integration-tests/__tests__/expect-async-matcher.test.js @@ -26,5 +26,11 @@ test('shows the correct errors in stderr when failing tests', () => { const result = runJest(dir, ['failure.test.js']); expect(result.status).toBe(1); - expect(extractSummary(result.stderr)).toMatchSnapshot(); + + const rest = extractSummary(result.stderr) + .rest.split('\n') + .filter(line => line.indexOf('packages/expect/build/index.js') === -1) + .join('\n'); + + expect(rest).toMatchSnapshot(); }); diff --git a/integration-tests/__tests__/failures.test.js b/integration-tests/__tests__/failures.test.js index e8fd6d0e65bf..e5debe43b264 100644 --- a/integration-tests/__tests__/failures.test.js +++ b/integration-tests/__tests__/failures.test.js @@ -110,7 +110,12 @@ test('works with assertions in separate files', () => { test('works with async failures', () => { const {stderr} = runJest(dir, ['async_failures.test.js']); - expect(normalizeDots(extractSummary(stderr).rest)).toMatchSnapshot(); + const rest = extractSummary(stderr) + .rest.split('\n') + .filter(line => line.indexOf('packages/expect/build/index.js') === -1) + .join('\n'); + + expect(normalizeDots(rest)).toMatchSnapshot(); }); test('works with snapshot failures', () => { diff --git a/integration-tests/failures/__tests__/async_failures.test.js b/integration-tests/failures/__tests__/async_failures.test.js index 71b92a99f6ef..5c828bd6fa1e 100644 --- a/integration-tests/failures/__tests__/async_failures.test.js +++ b/integration-tests/failures/__tests__/async_failures.test.js @@ -8,6 +8,24 @@ */ 'use strict'; -test('something async', () => { +test('resolve, but fail', () => { return expect(Promise.resolve({foo: 'bar'})).resolves.toEqual({baz: 'bar'}); }); + +test('reject, but fail', () => { + return expect(Promise.reject({foo: 'bar'})).rejects.toEqual({baz: 'bar'}); +}); + +test('expect reject', () => { + return expect(Promise.resolve({foo: 'bar'})).rejects.toEqual({foo: 'bar'}); +}); + +test('expect resolve', () => { + return expect(Promise.reject({foo: 'bar'})).resolves.toEqual({foo: 'bar'}); +}); + +test('timeout', done => { + jest.setTimeout(5); + + setTimeout(done, 10); +}); diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index f78dbfac79a3..b863c714ea4f 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -88,6 +88,8 @@ const expect = (actual: any, ...rest): ExpectationObject => { resolves: {not: {}}, }; + const err = new JestAssertionError(); + Object.keys(allMatchers).forEach(name => { const matcher = allMatchers[name]; const promiseMatcher = getPromiseMatcher(name, matcher) || matcher; @@ -99,12 +101,14 @@ const expect = (actual: any, ...rest): ExpectationObject => { promiseMatcher, false, actual, + err, ); expectation.resolves.not[name] = makeResolveMatcher( name, promiseMatcher, true, actual, + err, ); expectation.rejects[name] = makeRejectMatcher( @@ -112,12 +116,14 @@ const expect = (actual: any, ...rest): ExpectationObject => { promiseMatcher, false, actual, + err, ); expectation.rejects.not[name] = makeRejectMatcher( name, promiseMatcher, true, actual, + err, ); }); @@ -136,6 +142,7 @@ const makeResolveMatcher = ( matcher: RawMatcherFn, isNot: boolean, actual: Promise, + outerErr: JestAssertionError, ): PromiseMatcherFn => (...args) => { const matcherStatement = `.resolves.${isNot ? 'not.' : ''}${matcherName}`; if (!isPromise(actual)) { @@ -147,17 +154,19 @@ const makeResolveMatcher = ( ); } + const innerErr = new JestAssertionError(); + return actual.then( - result => makeThrowingMatcher(matcher, isNot, result).apply(null, args), + result => + makeThrowingMatcher(matcher, isNot, result, innerErr).apply(null, args), reason => { - const err = new JestAssertionError( + outerErr.message = utils.matcherHint(matcherStatement, 'received', '') + - '\n\n' + - `Expected ${utils.RECEIVED_COLOR('received')} Promise to resolve, ` + - 'instead it rejected to value\n' + - ` ${utils.printReceived(reason)}`, - ); - return Promise.reject(err); + '\n\n' + + `Expected ${utils.RECEIVED_COLOR('received')} Promise to resolve, ` + + 'instead it rejected to value\n' + + ` ${utils.printReceived(reason)}`; + return Promise.reject(outerErr); }, ); }; @@ -167,6 +176,7 @@ const makeRejectMatcher = ( matcher: RawMatcherFn, isNot: boolean, actual: Promise, + outerErr: JestAssertionError, ): PromiseMatcherFn => (...args) => { const matcherStatement = `.rejects.${isNot ? 'not.' : ''}${matcherName}`; if (!isPromise(actual)) { @@ -178,18 +188,20 @@ const makeRejectMatcher = ( ); } + const innerErr = new JestAssertionError(); + return actual.then( result => { - const err = new JestAssertionError( + outerErr.message = utils.matcherHint(matcherStatement, 'received', '') + - '\n\n' + - `Expected ${utils.RECEIVED_COLOR('received')} Promise to reject, ` + - 'instead it resolved to value\n' + - ` ${utils.printReceived(result)}`, - ); - return Promise.reject(err); + '\n\n' + + `Expected ${utils.RECEIVED_COLOR('received')} Promise to reject, ` + + 'instead it resolved to value\n' + + ` ${utils.printReceived(result)}`; + return Promise.reject(outerErr); }, - reason => makeThrowingMatcher(matcher, isNot, reason).apply(null, args), + reason => + makeThrowingMatcher(matcher, isNot, reason, innerErr).apply(null, args), ); }; @@ -197,6 +209,7 @@ const makeThrowingMatcher = ( matcher: RawMatcherFn, isNot: boolean, actual: any, + err?: JestAssertionError, ): ThrowingMatcherFn => { return function throwingMatcher(...args): any { let throws = true; @@ -223,16 +236,24 @@ const makeThrowingMatcher = ( if ((result.pass && isNot) || (!result.pass && !isNot)) { // XOR const message = getMessage(result.message); - const error = new JestAssertionError(message); + let error; + + if (err) { + error = err; + error.message = message; + } else { + error = new JestAssertionError(message); + + // Try to remove this function from the stack trace frame. + // Guard for some environments (browsers) that do not support this feature. + if (Error.captureStackTrace) { + Error.captureStackTrace(error, throwingMatcher); + } + } // Passing the result of the matcher with the error so that a custom // reporter could access the actual and expected objects of the result // for example in order to display a custom visual diff error.matcherResult = result; - // Try to remove this function from the stack trace frame. - // Guard for some environments (browsers) that do not support this feature. - if (Error.captureStackTrace) { - Error.captureStackTrace(error, throwingMatcher); - } if (throws) { throw error; diff --git a/packages/jest-jasmine2/src/expectation_result_factory.js b/packages/jest-jasmine2/src/expectation_result_factory.js index 9ace1d7719cc..32e04989a969 100644 --- a/packages/jest-jasmine2/src/expectation_result_factory.js +++ b/packages/jest-jasmine2/src/expectation_result_factory.js @@ -31,13 +31,20 @@ function messageFormatter({error, message, passed}) { return `thrown: ${prettyFormat(error, {maxDepth: 3})}`; } -function stackFormatter(options, errorMessage) { +function stackFormatter(options, initError, errorMessage) { if (options.passed) { return ''; } - const stack = - (options.error && options.error.stack) || new Error(errorMessage).stack; - return stack; + + if (options.error && options.error.stack) { + return options.error.stack; + } + + if (initError) { + return errorMessage + '\n' + initError.stack; + } + + return new Error(errorMessage).stack; } type Options = { @@ -49,9 +56,12 @@ type Options = { message?: string, }; -export default function expectationResultFactory(options: Options) { +export default function expectationResultFactory( + options: Options, + initError?: Error, +) { const message = messageFormatter(options); - const stack = stackFormatter(options, message); + const stack = stackFormatter(options, initError, message); if (options.passed) { return { diff --git a/packages/jest-jasmine2/src/jasmine/Spec.js b/packages/jest-jasmine2/src/jasmine/Spec.js index 251ed59b2a9a..7cdfe84abee9 100644 --- a/packages/jest-jasmine2/src/jasmine/Spec.js +++ b/packages/jest-jasmine2/src/jasmine/Spec.js @@ -59,6 +59,11 @@ export default function Spec(attrs: Object) { this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure; + this.initError = new Error(); + this.initError.name = ''; + + this.queueableFn.initError = this.initError; + this.result = { id: this.id, description: this.description, @@ -71,7 +76,7 @@ export default function Spec(attrs: Object) { } Spec.prototype.addExpectationResult = function(passed, data, isError) { - const expectationResult = expectationResultFactory(data); + const expectationResult = expectationResultFactory(data, this.initError); if (passed) { this.result.passedExpectations.push(expectationResult); } else { diff --git a/packages/jest-jasmine2/src/queue_runner.js b/packages/jest-jasmine2/src/queue_runner.js index 062cfc0bdd51..5155a43374ef 100644 --- a/packages/jest-jasmine2/src/queue_runner.js +++ b/packages/jest-jasmine2/src/queue_runner.js @@ -34,7 +34,7 @@ export default function queueRunner(options: Options) { onCancel(resolve); }); - const mapper = ({fn, timeout}) => { + const mapper = ({fn, timeout, initError = new Error()}) => { let promise = new Promise(resolve => { const next = function(err) { if (err) { @@ -69,12 +69,11 @@ export default function queueRunner(options: Options) { options.clearTimeout, options.setTimeout, () => { - const error = new Error( + initError.message = 'Timeout - Async callback was not invoked within the ' + - timeoutMs + - 'ms timeout specified by jest.setTimeout.', - ); - options.onException(error); + timeoutMs + + 'ms timeout specified by jest.setTimeout.'; + options.onException(initError); }, ); };