diff --git a/CHANGELOG.md b/CHANGELOG.md index d26f7237dbde..86c0cb5a6081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ - `[jest-runtime]`: Migrate to TypeScript ([#7964](https://github.com/facebook/jest/pull/7964), [#7988](https://github.com/facebook/jest/pull/7988)) - `[@jest/fake-timers]`: Extract FakeTimers class from `jest-util` into a new separate package ([#7987](https://github.com/facebook/jest/pull/7987)) - `[jest-repl]`: Migrate to TypeScript ([#8000](https://github.com/facebook/jest/pull/8000)) +- `[jest-jasmine2]`: Throw explicit error when errors happen after test is considered complete +- `[jest-circus]`: Throw explicit error when errors happen after test is considered complete ### Performance diff --git a/e2e/__tests__/failures.test.ts b/e2e/__tests__/failures.test.ts index f99bed6a4938..851bb3d1a39b 100644 --- a/e2e/__tests__/failures.test.ts +++ b/e2e/__tests__/failures.test.ts @@ -12,9 +12,9 @@ import runJest from '../runJest'; const dir = path.resolve(__dirname, '../failures'); -const normalizeDots = text => text.replace(/\.{1,}$/gm, '.'); +const normalizeDots = (text: string) => text.replace(/\.{1,}$/gm, '.'); -function cleanStderr(stderr) { +function cleanStderr(stderr: string) { const {rest} = extractSummary(stderr); return rest .replace(/.*(jest-jasmine2|jest-circus).*\n/g, '') @@ -182,3 +182,12 @@ test('works with named snapshot failures', () => { wrap(result.substring(0, result.indexOf('Snapshot Summary'))), ).toMatchSnapshot(); }); + +test('errors after test has completed', () => { + const {stderr} = runJest(dir, ['errorAfterTestComplete.test.js']); + + expect(stderr).toMatch( + /Error: Caught error after test environment was torn down/, + ); + expect(stderr).toMatch(/Failed: "fail async"/); +}); diff --git a/e2e/failures/__tests__/errorAfterTestComplete.test.js b/e2e/failures/__tests__/errorAfterTestComplete.test.js new file mode 100644 index 000000000000..6bfb7dc3ae79 --- /dev/null +++ b/e2e/failures/__tests__/errorAfterTestComplete.test.js @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+jsinfra + */ +'use strict'; + +test('a failing test', done => { + setTimeout(() => done('fail async'), 5); + done(); +}); diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 7917d1356e9d..b09c21f445e0 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -153,12 +153,17 @@ const _makeTimeoutMessage = (timeout: number, isHook: boolean) => // the original values in the variables before we require any files. const {setTimeout, clearTimeout} = global; +function checkIsError(error: any): error is Error { + return !!(error && (error as Error).message && (error as Error).stack); +} + export const callAsyncCircusFn = ( fn: AsyncFn, testContext: TestContext | undefined, {isHook, timeout}: {isHook?: boolean | null; timeout: number}, ): Promise => { let timeoutID: NodeJS.Timeout; + let completed = false; return new Promise((resolve, reject) => { timeoutID = setTimeout( @@ -170,15 +175,26 @@ export const callAsyncCircusFn = ( // soon as `done` called. if (fn.length) { const done = (reason?: Error | string): void => { - const isError = - reason && (reason as Error).message && (reason as Error).stack; - return reason - ? reject( - isError - ? reason - : new Error(`Failed: ${prettyFormat(reason, {maxDepth: 3})}`), - ) - : resolve(); + let errorAsErrorObject: Error; + + if (checkIsError(reason)) { + errorAsErrorObject = reason; + } else { + errorAsErrorObject = new Error( + `Failed: ${prettyFormat(reason, {maxDepth: 3})}`, + ); + } + + // Consider always throwing, regardless if `reason` is set or not + if (completed && reason) { + errorAsErrorObject.message = + 'Caught error after test environment was torn down\n\n' + + errorAsErrorObject.message; + + throw errorAsErrorObject; + } + + return reason ? reject(errorAsErrorObject) : resolve(); }; return fn.call(testContext, done); @@ -230,6 +246,9 @@ export const callAsyncCircusFn = ( timeoutID.unref && timeoutID.unref(); clearTimeout(timeoutID); throw error; + }) + .finally(() => { + completed = true; }); }; diff --git a/packages/jest-jasmine2/src/jasmine/Env.js b/packages/jest-jasmine2/src/jasmine/Env.js index 49e8963980b7..c1e6b70992b0 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.js +++ b/packages/jest-jasmine2/src/jasmine/Env.js @@ -586,13 +586,24 @@ export default function(j$) { message = check.message; } - currentRunnable().addExpectationResult(false, { + const errorAsErrorObject = checkIsError ? error : new Error(message); + const runnable = currentRunnable(); + + if (!runnable) { + errorAsErrorObject.message = + 'Caught error after test environment was torn down\n\n' + + errorAsErrorObject.message; + + throw errorAsErrorObject; + } + + runnable.addExpectationResult(false, { matcherName: '', passed: false, expected: '', actual: '', message, - error: checkIsError ? error : new Error(message), + error: errorAsErrorObject, }); }; }