diff --git a/integration-tests/__tests__/__snapshots__/failures.test.js.snap b/integration-tests/__tests__/__snapshots__/failures.test.js.snap index 562518436741..4d4359ef2564 100644 --- a/integration-tests/__tests__/__snapshots__/failures.test.js.snap +++ b/integration-tests/__tests__/__snapshots__/failures.test.js.snap @@ -144,7 +144,6 @@ exports[`not throwing Error objects 5`] = ` 9 | }); 10 | - at packages/jest-jasmine2/build/jasmine/Spec.js:85:20 at __tests__/during_tests.test.js:7:1 ● Boolean thrown during test @@ -159,7 +158,6 @@ exports[`not throwing Error objects 5`] = ` 13 | throw false; 14 | }); - at packages/jest-jasmine2/build/jasmine/Spec.js:85:20 at __tests__/during_tests.test.js:11:1 ● undefined thrown during test @@ -174,7 +172,6 @@ exports[`not throwing Error objects 5`] = ` 18 | throw undefined; 19 | }); - at packages/jest-jasmine2/build/jasmine/Spec.js:85:20 at __tests__/during_tests.test.js:16:1 ● Object thrown during test @@ -196,7 +193,6 @@ exports[`not throwing Error objects 5`] = ` 23 | throw deepObject; 24 | }); - at packages/jest-jasmine2/build/jasmine/Spec.js:85:20 at __tests__/during_tests.test.js:21:1 ● Error during test @@ -229,7 +225,7 @@ exports[`not throwing Error objects 5`] = ` ● done(non-error) - Failed: Object { + thrown: Object { \\"notAnError\\": Array [ Object { \\"hello\\": true, @@ -246,12 +242,11 @@ exports[`not throwing Error objects 5`] = ` 38 | 39 | test('returned promise rejection', () => { - at packages/jest-jasmine2/build/jasmine/Env.js:537:34 at __tests__/during_tests.test.js:36:3 ● returned promise rejection - Failed: Object { + thrown: Object { \\"notAnError\\": Array [ Object { \\"hello\\": true, @@ -268,7 +263,6 @@ exports[`not throwing Error objects 5`] = ` 41 | }); 42 | - at packages/jest-jasmine2/build/jasmine_async.js:102:24 at __tests__/during_tests.test.js:39:1 " diff --git a/integration-tests/__tests__/failures.test.js b/integration-tests/__tests__/failures.test.js index 4a129fe021de..afddc2e91b05 100644 --- a/integration-tests/__tests__/failures.test.js +++ b/integration-tests/__tests__/failures.test.js @@ -18,24 +18,31 @@ const normalizeDots = text => text.replace(/\.{1,}$/gm, '.'); SkipOnWindows.suite(); +function cleanStderr(stderr) { + const {rest} = extractSummary(stderr); + return rest + .replace(/.*(jest-jasmine2|jest-circus).*\n/g, '') + .replace(new RegExp('Failed: Object {', 'g'), 'thrown: Object {'); +} + test('not throwing Error objects', () => { let stderr; stderr = runJest(dir, ['throw_number.test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(cleanStderr(stderr)).toMatchSnapshot(); stderr = runJest(dir, ['throw_string.test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(cleanStderr(stderr)).toMatchSnapshot(); stderr = runJest(dir, ['throw_object.test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(cleanStderr(stderr)).toMatchSnapshot(); stderr = runJest(dir, ['assertion_count.test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(cleanStderr(stderr)).toMatchSnapshot(); stderr = runJest(dir, ['during_tests.test.js']).stderr; - expect(extractSummary(stderr).rest).toMatchSnapshot(); + expect(cleanStderr(stderr)).toMatchSnapshot(); }); test('works with node assert', () => { const nodeMajorVersion = Number(process.versions.node.split('.')[0]); const {stderr} = runJest(dir, ['node_assertion_error.test.js']); - let summary = normalizeDots(extractSummary(stderr).rest); + let summary = normalizeDots(cleanStderr(stderr)); // Node 9 started to include the error for `doesNotThrow` // https://github.com/nodejs/node/pull/12167 @@ -127,7 +134,6 @@ test('works with node assert', () => { 68 | }); 69 | - at packages/jest-jasmine2/build/jasmine/Spec.js:85:20 at __tests__/node_assertion_error.test.js:66:1 `; @@ -141,7 +147,7 @@ test('works with node assert', () => { test('works with assertions in separate files', () => { const {stderr} = runJest(dir, ['test_macro.test.js']); - expect(normalizeDots(extractSummary(stderr).rest)).toMatchSnapshot(); + expect(normalizeDots(cleanStderr(stderr))).toMatchSnapshot(); }); test('works with async failures', () => { @@ -158,7 +164,7 @@ test('works with async failures', () => { test('works with snapshot failures', () => { const {stderr} = runJest(dir, ['snapshot.test.js']); - const result = normalizeDots(extractSummary(stderr).rest); + const result = normalizeDots(cleanStderr(stderr)); expect( result.substring(0, result.indexOf('Snapshot Summary')), @@ -168,7 +174,7 @@ test('works with snapshot failures', () => { test('works with named snapshot failures', () => { const {stderr} = runJest(dir, ['snapshot_named.test.js']); - const result = normalizeDots(extractSummary(stderr).rest); + const result = normalizeDots(cleanStderr(stderr)); expect( result.substring(0, result.indexOf('Snapshot Summary')), diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index d3f53a3c6b1e..0d6be54f053e 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -14,7 +14,8 @@ "jest-matcher-utils": "^23.0.0", "jest-message-util": "^23.0.0", "jest-snapshot": "^23.0.0", - "jest-util": "^23.0.0" + "jest-util": "^23.0.0", + "pretty-format": "^23.0.0" }, "devDependencies": { "jest-runtime": "^23.0.0" diff --git a/packages/jest-circus/src/event_handler.js b/packages/jest-circus/src/event_handler.js index 8352b8004ce3..422c47f655b2 100644 --- a/packages/jest-circus/src/event_handler.js +++ b/packages/jest-circus/src/event_handler.js @@ -43,33 +43,42 @@ const handler: EventHandler = (event, state): void => { } case 'add_hook': { const {currentDescribeBlock} = state; - const {fn, hookType: type, timeout} = event; + const {asyncError, fn, hookType: type, timeout} = event; const parent = currentDescribeBlock; - currentDescribeBlock.hooks.push({fn, parent, timeout, type}); + currentDescribeBlock.hooks.push({asyncError, fn, parent, timeout, type}); break; } case 'add_test': { const {currentDescribeBlock} = state; - const {fn, mode, testName: name, timeout} = event; - const test = makeTest(fn, mode, name, currentDescribeBlock, timeout); - test.mode === 'only' && (state.hasFocusedTests = true); + const {asyncError, fn, mode, testName: name, timeout} = event; + const test = makeTest( + fn, + mode, + name, + currentDescribeBlock, + timeout, + asyncError, + ); + if (test.mode === 'only') { + state.hasFocusedTests = true; + } currentDescribeBlock.tests.push(test); break; } case 'hook_failure': { const {test, describeBlock, error, hook} = event; - const {type} = hook; + const {asyncError, type} = hook; if (type === 'beforeAll') { invariant(describeBlock, 'always present for `*All` hooks'); - addErrorToEachTestUnderDescribe(describeBlock, error); + addErrorToEachTestUnderDescribe(describeBlock, error, asyncError); } else if (type === 'afterAll') { // Attaching `afterAll` errors to each test makes execution flow // too complicated, so we'll consider them to be global. - state.unhandledErrors.push(error); + state.unhandledErrors.push([error, asyncError]); } else { invariant(test, 'always present for `*Each` hooks'); - test.errors.push(error); + test.errors.push([error, asyncError]); } break; } @@ -87,7 +96,11 @@ const handler: EventHandler = (event, state): void => { break; } case 'test_fn_failure': { - event.test.errors.push(event.error); + const { + error, + test: {asyncError}, + } = event; + event.test.errors.push([error, asyncError]); break; } case 'run_start': { diff --git a/packages/jest-circus/src/format_node_assert_errors.js b/packages/jest-circus/src/format_node_assert_errors.js index 01a2140652fd..61a51ef813c8 100644 --- a/packages/jest-circus/src/format_node_assert_errors.js +++ b/packages/jest-circus/src/format_node_assert_errors.js @@ -7,12 +7,13 @@ * @flow strict-local */ -import type {DiffOptions} from 'jest-diff/src/diff_strings.js'; +import type {DiffOptions} from 'jest-diff/src/diff_strings'; import type {Event, State} from 'types/Circus'; import {printExpected, printReceived} from 'jest-matcher-utils'; import chalk from 'chalk'; import diff from 'jest-diff'; +import prettyFormat from 'pretty-format'; type AssertionError = {| actual: ?string, @@ -34,31 +35,40 @@ const assertOperatorsMap = { const humanReadableOperators = { deepEqual: 'to deeply equal', deepStrictEqual: 'to deeply and strictly equal', + equal: 'to be equal', notDeepEqual: 'not to deeply equal', notDeepStrictEqual: 'not to deeply and strictly equal', + notEqual: 'to not be equal', + notStrictEqual: 'not be strictly equal', + strictEqual: 'to strictly be equal', }; export default (event: Event, state: State) => { switch (event.name) { case 'test_done': { - let assert; - try { - // Use indirect require so that Metro Bundler does not attempt to - // bundle `assert`, which does not exist in React Native. - // eslint-disable-next-line no-useless-call - assert = require.call(null, 'assert'); - event.test.errors = event.test.errors.map(error => { - return error instanceof assert.AssertionError - ? assertionErrorMessage(error, {expand: state.expand}) - : error; - }); - } catch (error) { - // We are running somewhere where `assert` isn't available, like a - // browser or React Native. Since assert isn't available, presumably - // none of the errors we get through this event listener will be - // `AssertionError`s, so we don't need to do anything. - break; - } + event.test.errors = event.test.errors.map(errors => { + let error; + if (Array.isArray(errors)) { + const [originalError, asyncError] = errors; + + if (originalError == null) { + error = asyncError; + } else if (!originalError.stack) { + error = asyncError; + + error.message = originalError.message + ? originalError.message + : `thrown: ${prettyFormat(originalError, {maxDepth: 3})}`; + } else { + error = originalError; + } + } else { + error = errors; + } + return error.code === 'ERR_ASSERTION' + ? {message: assertionErrorMessage(error, {expand: state.expand})} + : errors; + }); } } }; @@ -76,12 +86,15 @@ const getOperatorName = (operator: ?string, stack: string) => { return ''; }; -const operatorMessage = (operator: ?string, negator: boolean) => - typeof operator === 'string' - ? operator.startsWith('!') || operator.startsWith('=') - ? `${negator ? 'not ' : ''}to be (operator: ${operator}):\n` - : `${humanReadableOperators[operator] || operator} to:\n` +const operatorMessage = (operator: ?string) => { + const niceOperatorName = getOperatorName(operator, ''); + // $FlowFixMe: we default to the operator itself, so holes in the map doesn't matter + const humanReadableOperator = humanReadableOperators[niceOperatorName]; + + return typeof operator === 'string' + ? `${humanReadableOperator || niceOperatorName} to:\n` : ''; +}; const assertThrowingMatcherHint = (operatorName: string) => { return ( @@ -114,13 +127,13 @@ const assertMatcherHint = (operator: ?string, operatorName: string) => { }; function assertionErrorMessage(error: AssertionError, options: DiffOptions) { - const {expected, actual, message, operator, stack} = error; + const {expected, actual, generatedMessage, message, operator, stack} = error; const diffString = diff(expected, actual, options); - const negator = - typeof operator === 'string' && - (operator.startsWith('!') || operator.startsWith('not')); - const hasCustomMessage = !error.generatedMessage; + const hasCustomMessage = !generatedMessage; const operatorName = getOperatorName(operator, stack); + const trimmedStack = stack + .replace(message, '') + .replace(/AssertionError(.*)/g, ''); if (operatorName === 'doesNotThrow') { return ( @@ -130,7 +143,7 @@ function assertionErrorMessage(error: AssertionError, options: DiffOptions) { chalk.reset(`Instead, it threw:\n`) + ` ${printReceived(actual)}` + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + - stack.replace(/AssertionError(.*)/g, '') + trimmedStack ); } @@ -141,19 +154,19 @@ function assertionErrorMessage(error: AssertionError, options: DiffOptions) { chalk.reset(`Expected the function to throw an error.\n`) + chalk.reset(`But it didn't throw anything.`) + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + - stack.replace(/AssertionError(.*)/g, '') + trimmedStack ); } return ( assertMatcherHint(operator, operatorName) + '\n\n' + - chalk.reset(`Expected value ${operatorMessage(operator, negator)}`) + + chalk.reset(`Expected value ${operatorMessage(operator)}`) + ` ${printExpected(expected)}\n` + chalk.reset(`Received:\n`) + ` ${printReceived(actual)}` + chalk.reset(hasCustomMessage ? '\n\nMessage:\n ' + message : '') + (diffString ? `\n\nDifference:\n\n${diffString}` : '') + - stack.replace(/AssertionError(.*)/g, '') + trimmedStack ); } diff --git a/packages/jest-circus/src/index.js b/packages/jest-circus/src/index.js index 1b85a4ad3435..d933476ea460 100644 --- a/packages/jest-circus/src/index.js +++ b/packages/jest-circus/src/index.js @@ -28,13 +28,18 @@ describe.skip = (blockName: BlockName, blockFn: BlockFn) => _dispatchDescribe(blockFn, blockName, 'skip'); const _dispatchDescribe = (blockFn, blockName, mode?: BlockMode) => { - dispatch({blockName, mode, name: 'start_describe_definition'}); + dispatch({ + asyncError: new Error(), + blockName, + mode, + name: 'start_describe_definition', + }); blockFn(); dispatch({blockName, mode, name: 'finish_describe_definition'}); }; const _addHook = (fn: HookFn, hookType: HookType, timeout: ?number) => - dispatch({fn, hookType, name: 'add_hook', timeout}); + dispatch({asyncError: new Error(), fn, hookType, name: 'add_hook', timeout}); const beforeEach: THook = (fn, timeout) => _addHook(fn, 'beforeEach', timeout); const beforeAll: THook = (fn, timeout) => _addHook(fn, 'beforeAll', timeout); const afterEach: THook = (fn, timeout) => _addHook(fn, 'afterEach', timeout); @@ -54,13 +59,51 @@ const test = (testName: TestName, fn: TestFn, timeout?: number) => { `Invalid second argument, ${fn}. It must be a callback function.`, ); } - return dispatch({fn, name: 'add_test', testName, timeout}); + + const asyncError = new Error(); + if (Error.captureStackTrace) { + Error.captureStackTrace(asyncError, test); + } + + return dispatch({ + asyncError, + fn, + name: 'add_test', + testName, + timeout, + }); }; const it = test; -test.skip = (testName: TestName, fn?: TestFn, timeout?: number) => - dispatch({fn, mode: 'skip', name: 'add_test', testName, timeout}); -test.only = (testName: TestName, fn: TestFn, timeout?: number) => - dispatch({fn, mode: 'only', name: 'add_test', testName, timeout}); +test.skip = (testName: TestName, fn?: TestFn, timeout?: number) => { + const asyncError = new Error(); + if (Error.captureStackTrace) { + Error.captureStackTrace(asyncError, test); + } + + return dispatch({ + asyncError, + fn, + mode: 'skip', + name: 'add_test', + testName, + timeout, + }); +}; +test.only = (testName: TestName, fn: TestFn, timeout?: number) => { + const asyncError = new Error(); + if (Error.captureStackTrace) { + Error.captureStackTrace(asyncError, test); + } + + return dispatch({ + asyncError, + fn, + mode: 'only', + name: 'add_test', + testName, + timeout, + }); +}; module.exports = { afterAll, diff --git a/packages/jest-circus/src/utils.js b/packages/jest-circus/src/utils.js index 01ac8d92257b..261e48bcc341 100644 --- a/packages/jest-circus/src/utils.js +++ b/packages/jest-circus/src/utils.js @@ -24,6 +24,8 @@ import type { } from 'types/Circus'; import {convertDescriptorToString} from 'jest-util'; +import prettyFormat from 'pretty-format'; + export const makeDescribe = ( name: BlockName, parent: ?DescribeBlock, @@ -51,6 +53,7 @@ export const makeTest = ( name: TestName, parent: DescribeBlock, timeout: ?number, + asyncError: Exception, ): TestEntry => { let _mode = mode; if (!mode) { @@ -59,6 +62,7 @@ export const makeTest = ( } return { + asyncError, duration: null, errors: [], fn, @@ -141,11 +145,20 @@ export const callAsyncFn = ( timeout, ); - // If this fn accepts `done` callback we return a promise that fullfills as + // If this fn accepts `done` callback we return a promise that fulfills as // soon as `done` called. if (fn.length) { - const done = (reason?: Error | string): void => - reason ? reject(reason) : resolve(); + const done = (reason?: Error | string): void => { + // $FlowFixMe: It doesn't approve of .stack + const isError = reason && reason.message && reason.stack; + return reason + ? reject( + isError + ? reason + : new Error(`Failed: ${prettyFormat(reason, {maxDepth: 3})}`), + ) + : resolve(); + }; return fn.call(testContext, done); } @@ -252,28 +265,43 @@ export const getTestID = (test: TestEntry) => { return titles.join(' '); }; -const _formatError = (error: ?Exception): string => { - if (!error) { - return 'NO ERROR MESSAGE OR STACK TRACE SPECIFIED'; - } else if (error.stack) { - return error.stack; - } else if (error.message) { - return error.message; +const _formatError = (errors: ?Exception | [?Exception, Exception]): string => { + let error; + let asyncError; + + if (Array.isArray(errors)) { + error = errors[0]; + asyncError = errors[1]; } else { - return `${String(error)} thrown`; + error = errors; + asyncError = new Error(); + } + + if (error) { + if (error.stack) { + return error.stack; + } + if (error.message) { + return error.message; + } } + + asyncError.message = `thrown: ${prettyFormat(error, {maxDepth: 3})}`; + + return asyncError.stack; }; export const addErrorToEachTestUnderDescribe = ( describeBlock: DescribeBlock, error: Exception, + asyncError: Exception, ) => { for (const test of describeBlock.tests) { - test.errors.push(error); + test.errors.push([error, asyncError]); } for (const child of describeBlock.children) { - addErrorToEachTestUnderDescribe(child, error); + addErrorToEachTestUnderDescribe(child, error, asyncError); } }; diff --git a/packages/jest-jasmine2/src/assert_support.js b/packages/jest-jasmine2/src/assert_support.js index e5265a0e867e..d005caeba46e 100644 --- a/packages/jest-jasmine2/src/assert_support.js +++ b/packages/jest-jasmine2/src/assert_support.js @@ -56,7 +56,7 @@ const getOperatorName = (operator: ?string, stack: string) => { const operatorMessage = (operator: ?string) => { const niceOperatorName = getOperatorName(operator, ''); - // $FlowFixMe: we default to the operator itseld, so holes in the map doesn't matter + // $FlowFixMe: we default to the operator itself, so holes in the map doesn't matter const humanReadableOperator = humanReadableOperators[niceOperatorName]; return typeof operator === 'string' diff --git a/types/Circus.js b/types/Circus.js index 0bf0bb7ccf30..f652c88a7251 100644 --- a/types/Circus.js +++ b/types/Circus.js @@ -22,6 +22,7 @@ export type TestContext = Object; export type Exception = any; // Since in JS anything can be thrown as an error. export type FormattedError = string; // String representation of error. export type Hook = { + asyncError: Exception, fn: HookFn, type: HookType, parent: DescribeBlock, @@ -32,6 +33,7 @@ export type EventHandler = (event: Event, state: State) => void; export type Event = | {| + asyncError: Exception, mode: BlockMode, name: 'start_describe_definition', blockName: BlockName, @@ -42,12 +44,14 @@ export type Event = blockName: BlockName, |} | {| + asyncError: Exception, name: 'add_hook', hookType: HookType, fn: HookFn, timeout: ?number, |} | {| + asyncError: Exception, name: 'add_test', testName: TestName, fn?: TestFn, @@ -119,6 +123,7 @@ export type Event = | {| // Any unhandled error that happened outside of test/hooks (unless it is // an `afterAll` hook) + asyncError: Exception, name: 'error', error: Exception, |} @@ -161,8 +166,11 @@ export type DescribeBlock = {| tests: Array, |}; +type TestError = Exception | Array<[?Exception, Exception]>; // the error from the test, as well as a backup error for async + export type TestEntry = {| - errors: Array, + asyncError: Exception, // Used if the test failure contains no usable stack trace + errors: TestError, fn: ?TestFn, mode: TestMode, name: TestName,