From 91cb74916694327d1612901e94dbf2d933cd8aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Tue, 25 Apr 2023 22:02:34 +0200 Subject: [PATCH 01/15] Add failing integration tests --- .../promiseAsyncHandling.test.ts.snap | 222 ++++++++++++++++++ e2e/__tests__/promiseAsyncHandling.test.ts | 68 ++++++ .../__tests__/rejectionHandled.test.js | 74 ++++++ .../unhandledRejectionAfterAll.test.js | 18 ++ .../unhandledRejectionAfterEach.test.js | 20 ++ .../unhandledRejectionBeforeAll.test.js | 18 ++ .../unhandledRejectionBeforeEach.test.js | 20 ++ .../__tests__/unhandledRejectionTest.test.js | 34 +++ e2e/promise-async-handling/package.json | 5 + 9 files changed, 479 insertions(+) create mode 100644 e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap create mode 100644 e2e/__tests__/promiseAsyncHandling.test.ts create mode 100644 e2e/promise-async-handling/__tests__/rejectionHandled.test.js create mode 100644 e2e/promise-async-handling/__tests__/unhandledRejectionAfterAll.test.js create mode 100644 e2e/promise-async-handling/__tests__/unhandledRejectionAfterEach.test.js create mode 100644 e2e/promise-async-handling/__tests__/unhandledRejectionBeforeAll.test.js create mode 100644 e2e/promise-async-handling/__tests__/unhandledRejectionBeforeEach.test.js create mode 100644 e2e/promise-async-handling/__tests__/unhandledRejectionTest.test.js create mode 100644 e2e/promise-async-handling/package.json diff --git a/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap new file mode 100644 index 000000000000..1f090f0bc8db --- /dev/null +++ b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap @@ -0,0 +1,222 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fails because of unhandled promise rejection in afterAll hook 1`] = ` +Object { + "rest": "FAIL __tests__/unhandledRejectionAfterAll.test.js + + + ● Test suite failed to run + + REJECTED + + 11 | + 12 | afterAll(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionAfterAll.test.js:13:18)", + "summary": "Test Suites: 1 failed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /unhandledRejectionAfterAll.test.js/i.", +} +`; + +exports[`fails because of unhandled promise rejection in afterEach hook 1`] = ` +Object { + "rest": "FAIL __tests__/unhandledRejectionAfterEach.test.js + ✕ foo #1 + ✕ foo #2 + + ● foo #1 + + REJECTED + + 11 | + 12 | afterEach(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionAfterEach.test.js:13:18) + + ● foo #2 + + REJECTED + + 11 | + 12 | afterEach(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionAfterEach.test.js:13:18)", + "summary": "Test Suites: 1 failed, 1 total +Tests: 2 failed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /unhandledRejectionAfterEach.test.js/i.", +} +`; + +exports[`fails because of unhandled promise rejection in beforeAll hook 1`] = ` +Object { + "rest": "FAIL __tests__/unhandledRejectionBeforeAll.test.js + + + ● Test suite failed to run + + REJECTED + + 11 | + 12 | beforeAll(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionBeforeAll.test.js:13:18)", + "summary": "Test Suites: 1 failed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /unhandledRejectionBeforeAll.test.js/i.", +} +`; + +exports[`fails because of unhandled promise rejection in beforeEach hook 1`] = ` +Object { + "rest": "FAIL __tests__/unhandledRejectionBeforeEach.test.js + ✕ foo #1 + ✕ foo #2 + + ● foo #1 + + REJECTED + + 11 | + 12 | beforeEach(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionBeforeEach.test.js:13:18) + + ● foo #2 + + REJECTED + + 11 | + 12 | beforeEach(async () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | + 15 | await promisify(setTimeout)(0); + 16 | }); + + at Object. (__tests__/unhandledRejectionBeforeEach.test.js:13:18)", + "summary": "Test Suites: 1 failed, 1 total +Tests: 2 failed, 2 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /unhandledRejectionBeforeEach.test.js/i.", +} +`; + +exports[`fails because of unhandled promise rejection in test 1`] = ` +Object { + "rest": "FAIL __tests__/unhandledRejectionTest.test.js + ✓ w/o event loop turn after rejection + ✕ w/ event loop turn after rejection in async function + ✕ w/ event loop turn after rejection in sync function + ✕ combined w/ another failure _after_ promise rejection + + ● w/ event loop turn after rejection in async function + + REJECTED + + 11 | + 12 | test('w/o event loop turn after rejection', () => { + > 13 | Promise.reject(new Error('REJECTED')); + | ^ + 14 | }); + 15 | + 16 | test('w/ event loop turn after rejection in async function', async () => { + + at Object. (__tests__/unhandledRejectionTest.test.js:13:18) + + ● w/ event loop turn after rejection in async function + + REJECTED + + 15 | + 16 | test('w/ event loop turn after rejection in async function', async () => { + > 17 | Promise.reject(new Error('REJECTED')); + | ^ + 18 | + 19 | await promisify(setTimeout)(0); + 20 | }); + + at Object. (__tests__/unhandledRejectionTest.test.js:17:18) + + ● w/ event loop turn after rejection in sync function + + REJECTED + + 21 | + 22 | test('w/ event loop turn after rejection in sync function', done => { + > 23 | Promise.reject(new Error('REJECTED')); + | ^ + 24 | + 25 | setTimeout(done, 0); + 26 | }); + + at Object. (__tests__/unhandledRejectionTest.test.js:23:18) + + ● combined w/ another failure _after_ promise rejection + + REJECTED + + 27 | + 28 | test('combined w/ another failure _after_ promise rejection', async () => { + > 29 | Promise.reject(new Error('REJECTED')); + | ^ + 30 | + 31 | await promisify(setTimeout)(0); + 32 | + + at Object. (__tests__/unhandledRejectionTest.test.js:29:18) + + ● combined w/ another failure _after_ promise rejection + + expect(received).toBe(expected) // Object.is equality + + Expected: false + Received: true + + 31 | await promisify(setTimeout)(0); + 32 | + > 33 | expect(true).toBe(false); + | ^ + 34 | }); + 35 | + + at Object.toBe (__tests__/unhandledRejectionTest.test.js:33:16)", + "summary": "Test Suites: 1 failed, 1 total +Tests: 3 failed, 1 passed, 4 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /unhandledRejectionTest.test.js/i.", +} +`; diff --git a/e2e/__tests__/promiseAsyncHandling.test.ts b/e2e/__tests__/promiseAsyncHandling.test.ts new file mode 100644 index 000000000000..2345ac7f3c0a --- /dev/null +++ b/e2e/__tests__/promiseAsyncHandling.test.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import {extractSortedSummary} from '../Utils'; +import runJest from '../runJest'; + +const dir = path.resolve(__dirname, '../promise-async-handling'); + +test('fails because of unhandled promise rejection in test', () => { + const {stderr, exitCode} = runJest(dir, ['unhandledRejectionTest.test.js']); + + expect(exitCode).toBe(1); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); + +test('fails because of unhandled promise rejection in beforeAll hook', () => { + const {stderr, exitCode} = runJest(dir, [ + 'unhandledRejectionBeforeAll.test.js', + ]); + + expect(exitCode).toBe(1); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); + +test('fails because of unhandled promise rejection in beforeEach hook', () => { + const {stderr, exitCode} = runJest(dir, [ + 'unhandledRejectionBeforeEach.test.js', + ]); + + expect(exitCode).toBe(1); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); + +test('fails because of unhandled promise rejection in afterEach hook', () => { + const {stderr, exitCode} = runJest(dir, [ + 'unhandledRejectionAfterEach.test.js', + ]); + + expect(exitCode).toBe(1); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); + +test('fails because of unhandled promise rejection in afterAll hook', () => { + const {stderr, exitCode} = runJest(dir, [ + 'unhandledRejectionAfterAll.test.js', + ]); + + expect(exitCode).toBe(1); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); + +test('succeeds for async handled promise rejections', () => { + const {stderr, exitCode} = runJest(dir, ['rejectionHandled.test.js']); + + expect(exitCode).toBe(0); + const sortedSummary = extractSortedSummary(stderr); + expect(sortedSummary).toMatchSnapshot(); +}); diff --git a/e2e/promise-async-handling/__tests__/rejectionHandled.test.js b/e2e/promise-async-handling/__tests__/rejectionHandled.test.js new file mode 100644 index 000000000000..c36e9b8d6f0f --- /dev/null +++ b/e2e/promise-async-handling/__tests__/rejectionHandled.test.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +beforeAll(async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + await expect(promise).rejects.toThrow(/REJECTED/); +}); + +beforeEach(async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + await expect(promise).rejects.toThrow(/REJECTED/); +}); + +afterEach(async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + await expect(promise).rejects.toThrow(/REJECTED/); +}); + +afterAll(async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + await expect(promise).rejects.toThrow(/REJECTED/); +}); + +test('async function succeeds because the promise is eventually awaited by assertion', async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + await expect(promise).rejects.toThrow(/REJECTED/); +}); + +test('async function succeeds because the promise is eventually directly awaited', async () => { + const promise = Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + try { + await promise; + } catch (error) { + expect(error).toEqual(new Error('REJECTED')); + } +}); + +test('sync function succeeds because the promise is eventually handled by `.catch` handler', done => { + const promise = Promise.reject(new Error('REJECTED')); + + setTimeout(() => { + promise + .catch(error => { + expect(error).toEqual(new Error('REJECTED')); + }) + .finally(done); + }, 0); +}); diff --git a/e2e/promise-async-handling/__tests__/unhandledRejectionAfterAll.test.js b/e2e/promise-async-handling/__tests__/unhandledRejectionAfterAll.test.js new file mode 100644 index 000000000000..e6fdf75760ca --- /dev/null +++ b/e2e/promise-async-handling/__tests__/unhandledRejectionAfterAll.test.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +afterAll(async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); +}); + +test('foo', () => {}); diff --git a/e2e/promise-async-handling/__tests__/unhandledRejectionAfterEach.test.js b/e2e/promise-async-handling/__tests__/unhandledRejectionAfterEach.test.js new file mode 100644 index 000000000000..6c77b8159630 --- /dev/null +++ b/e2e/promise-async-handling/__tests__/unhandledRejectionAfterEach.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +afterEach(async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); +}); + +test('foo #1', () => {}); + +test('foo #2', () => {}); diff --git a/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeAll.test.js b/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeAll.test.js new file mode 100644 index 000000000000..af4654197346 --- /dev/null +++ b/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeAll.test.js @@ -0,0 +1,18 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +beforeAll(async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); +}); + +test('foo', () => {}); diff --git a/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeEach.test.js b/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeEach.test.js new file mode 100644 index 000000000000..28f3186d736c --- /dev/null +++ b/e2e/promise-async-handling/__tests__/unhandledRejectionBeforeEach.test.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +beforeEach(async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); +}); + +test('foo #1', () => {}); + +test('foo #2', () => {}); diff --git a/e2e/promise-async-handling/__tests__/unhandledRejectionTest.test.js b/e2e/promise-async-handling/__tests__/unhandledRejectionTest.test.js new file mode 100644 index 000000000000..f63e5772c2e6 --- /dev/null +++ b/e2e/promise-async-handling/__tests__/unhandledRejectionTest.test.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +'use strict'; + +const {promisify} = require('util'); + +test('w/o event loop turn after rejection', () => { + Promise.reject(new Error('REJECTED')); +}); + +test('w/ event loop turn after rejection in async function', async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); +}); + +test('w/ event loop turn after rejection in sync function', done => { + Promise.reject(new Error('REJECTED')); + + setTimeout(done, 0); +}); + +test('combined w/ another failure _after_ promise rejection', async () => { + Promise.reject(new Error('REJECTED')); + + await promisify(setTimeout)(0); + + expect(true).toBe(false); +}); diff --git a/e2e/promise-async-handling/package.json b/e2e/promise-async-handling/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/promise-async-handling/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} From bc66d547b89b9f6a6b760f52a85c05463cfa5c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Tue, 25 Apr 2023 13:14:59 +0200 Subject: [PATCH 02/15] Prevent false test failures caused by promise rejections handled asynchronously --- .../promiseAsyncHandling.test.ts.snap | 56 +++++++++------ packages/jest-circus/src/eventHandler.ts | 32 ++++++++- .../jest-circus/src/globalErrorHandlers.ts | 38 +++++++++-- packages/jest-circus/src/state.ts | 3 + .../src/unhandledRejectionHandler.ts | 68 +++++++++++++++++++ packages/jest-circus/src/utils.ts | 1 + packages/jest-types/src/Circus.ts | 8 +++ 7 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 packages/jest-circus/src/unhandledRejectionHandler.ts diff --git a/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap index 1f090f0bc8db..402a65e570c7 100644 --- a/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap +++ b/e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap @@ -70,9 +70,9 @@ Ran all test suites matching /unhandledRejectionAfterEach.test.js/i.", exports[`fails because of unhandled promise rejection in beforeAll hook 1`] = ` Object { "rest": "FAIL __tests__/unhandledRejectionBeforeAll.test.js + ✕ foo - - ● Test suite failed to run + ● foo REJECTED @@ -86,7 +86,7 @@ Object { at Object. (__tests__/unhandledRejectionBeforeAll.test.js:13:18)", "summary": "Test Suites: 1 failed, 1 total -Tests: 1 passed, 1 total +Tests: 1 failed, 1 total Snapshots: 0 total Time: <> Ran all test suites matching /unhandledRejectionBeforeAll.test.js/i.", @@ -137,12 +137,12 @@ Ran all test suites matching /unhandledRejectionBeforeEach.test.js/i.", exports[`fails because of unhandled promise rejection in test 1`] = ` Object { "rest": "FAIL __tests__/unhandledRejectionTest.test.js - ✓ w/o event loop turn after rejection + ✕ w/o event loop turn after rejection ✕ w/ event loop turn after rejection in async function ✕ w/ event loop turn after rejection in sync function ✕ combined w/ another failure _after_ promise rejection - ● w/ event loop turn after rejection in async function + ● w/o event loop turn after rejection REJECTED @@ -184,20 +184,6 @@ Object { at Object. (__tests__/unhandledRejectionTest.test.js:23:18) - ● combined w/ another failure _after_ promise rejection - - REJECTED - - 27 | - 28 | test('combined w/ another failure _after_ promise rejection', async () => { - > 29 | Promise.reject(new Error('REJECTED')); - | ^ - 30 | - 31 | await promisify(setTimeout)(0); - 32 | - - at Object. (__tests__/unhandledRejectionTest.test.js:29:18) - ● combined w/ another failure _after_ promise rejection expect(received).toBe(expected) // Object.is equality @@ -212,11 +198,39 @@ Object { 34 | }); 35 | - at Object.toBe (__tests__/unhandledRejectionTest.test.js:33:16)", + at Object.toBe (__tests__/unhandledRejectionTest.test.js:33:16) + + ● combined w/ another failure _after_ promise rejection + + REJECTED + + 27 | + 28 | test('combined w/ another failure _after_ promise rejection', async () => { + > 29 | Promise.reject(new Error('REJECTED')); + | ^ + 30 | + 31 | await promisify(setTimeout)(0); + 32 | + + at Object. (__tests__/unhandledRejectionTest.test.js:29:18)", "summary": "Test Suites: 1 failed, 1 total -Tests: 3 failed, 1 passed, 4 total +Tests: 4 failed, 4 total Snapshots: 0 total Time: <> Ran all test suites matching /unhandledRejectionTest.test.js/i.", } `; + +exports[`succeeds for async handled promise rejections 1`] = ` +Object { + "rest": "PASS __tests__/rejectionHandled.test.js + ✓ async function succeeds because the promise is eventually awaited by assertion + ✓ async function succeeds because the promise is eventually directly awaited + ✓ sync function succeeds because the promise is eventually handled by \`.catch\` handler", + "summary": "Test Suites: 1 passed, 1 total +Tests: 3 passed, 3 total +Snapshots: 0 total +Time: <> +Ran all test suites matching /rejectionHandled.test.js/i.", +} +`; diff --git a/packages/jest-circus/src/eventHandler.ts b/packages/jest-circus/src/eventHandler.ts index 0a437df1ed9a..f051bf1c1588 100644 --- a/packages/jest-circus/src/eventHandler.ts +++ b/packages/jest-circus/src/eventHandler.ts @@ -269,9 +269,35 @@ const eventHandler: Circus.EventHandler = (event, state) => { // execution, which will result in one test's error failing another test. // In any way, it should be possible to track where the error was thrown // from. - state.currentlyRunningTest - ? state.currentlyRunningTest.errors.push(event.error) - : state.unhandledErrors.push(event.error); + if (state.currentlyRunningTest) { + if (event.promise) { + state.currentlyRunningTest.unhandledRejectionErrorByPromise.set( + event.promise, + event.error, + ); + } else { + state.currentlyRunningTest.errors.push(event.error); + } + } else { + if (event.promise) { + state.unhandledRejectionErrorByPromise.set( + event.promise, + event.error, + ); + } else { + state.unhandledErrors.push(event.error); + } + } + break; + } + case 'error_handled': { + if (state.currentlyRunningTest) { + state.currentlyRunningTest.unhandledRejectionErrorByPromise.delete( + event.promise, + ); + } else { + state.unhandledRejectionErrorByPromise.delete(event.promise); + } break; } } diff --git a/packages/jest-circus/src/globalErrorHandlers.ts b/packages/jest-circus/src/globalErrorHandlers.ts index 27146fc4a1f2..b46b17881e96 100644 --- a/packages/jest-circus/src/globalErrorHandlers.ts +++ b/packages/jest-circus/src/globalErrorHandlers.ts @@ -8,29 +8,50 @@ import type {Circus} from '@jest/types'; import {dispatchSync} from './state'; -const uncaught: NodeJS.UncaughtExceptionListener & - NodeJS.UnhandledRejectionListener = (error: unknown) => { +const uncaughtExceptionListener: NodeJS.UncaughtExceptionListener = ( + error: unknown, +) => { dispatchSync({error, name: 'error'}); }; +const unhandledRejectionListener: NodeJS.UnhandledRejectionListener = ( + error: unknown, + promise: Promise, +) => { + dispatchSync({error, name: 'error', promise}); +}; + +const rejectionHandledListener: NodeJS.RejectionHandledListener = ( + promise: Promise, +) => { + dispatchSync({name: 'error_handled', promise}); +}; + export const injectGlobalErrorHandlers = ( parentProcess: NodeJS.Process, ): Circus.GlobalErrorHandlers => { const uncaughtException = process.listeners('uncaughtException').slice(); const unhandledRejection = process.listeners('unhandledRejection').slice(); + const rejectionHandled = process.listeners('rejectionHandled').slice(); parentProcess.removeAllListeners('uncaughtException'); parentProcess.removeAllListeners('unhandledRejection'); - parentProcess.on('uncaughtException', uncaught); - parentProcess.on('unhandledRejection', uncaught); - return {uncaughtException, unhandledRejection}; + parentProcess.removeAllListeners('rejectionHandled'); + parentProcess.on('uncaughtException', uncaughtExceptionListener); + parentProcess.on('unhandledRejection', unhandledRejectionListener); + parentProcess.on('rejectionHandled', rejectionHandledListener); + return {rejectionHandled, uncaughtException, unhandledRejection}; }; export const restoreGlobalErrorHandlers = ( parentProcess: NodeJS.Process, originalErrorHandlers: Circus.GlobalErrorHandlers, ): void => { - parentProcess.removeListener('uncaughtException', uncaught); - parentProcess.removeListener('unhandledRejection', uncaught); + parentProcess.removeListener('uncaughtException', uncaughtExceptionListener); + parentProcess.removeListener( + 'unhandledRejection', + unhandledRejectionListener, + ); + parentProcess.removeListener('rejectionHandled', rejectionHandledListener); for (const listener of originalErrorHandlers.uncaughtException) { parentProcess.on('uncaughtException', listener); @@ -38,4 +59,7 @@ export const restoreGlobalErrorHandlers = ( for (const listener of originalErrorHandlers.unhandledRejection) { parentProcess.on('unhandledRejection', listener); } + for (const listener of originalErrorHandlers.rejectionHandled) { + parentProcess.on('rejectionHandled', listener); + } }; diff --git a/packages/jest-circus/src/state.ts b/packages/jest-circus/src/state.ts index 0540dfd41116..4c5141f3f6e4 100644 --- a/packages/jest-circus/src/state.ts +++ b/packages/jest-circus/src/state.ts @@ -9,10 +9,12 @@ import type {Circus} from '@jest/types'; import eventHandler from './eventHandler'; import formatNodeAssertErrors from './formatNodeAssertErrors'; import {STATE_SYM} from './types'; +import unhandledRejectionHandler from './unhandledRejectionHandler'; import {makeDescribe} from './utils'; const eventHandlers: Array = [ eventHandler, + unhandledRejectionHandler, formatNodeAssertErrors, ]; @@ -34,6 +36,7 @@ const createState = (): Circus.State => { testNamePattern: null, testTimeout: 5000, unhandledErrors: [], + unhandledRejectionErrorByPromise: new Map(), }; }; diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts new file mode 100644 index 000000000000..1bcf0f8a594a --- /dev/null +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type {Circus} from '@jest/types'; +import {addErrorToEachTestUnderDescribe, invariant} from './utils'; + +const untilNextEventLoopTurn = async () => { + return new Promise(resolve => { + setTimeout(resolve, 0); + }); +}; + +const unhandledRejectionHandler: Circus.EventHandler = async ( + event, + state, +): Promise => { + if (event.name === 'hook_success' || event.name === 'hook_failure') { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + + const {test, describeBlock, hook} = event; + const {asyncError, type} = hook; + + if (type === 'beforeAll') { + invariant(describeBlock, 'always present for `*All` hooks'); + for (const error of state.unhandledRejectionErrorByPromise.values()) { + 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. + for (const error of state.unhandledRejectionErrorByPromise.values()) { + state.unhandledErrors.push([error, asyncError]); + } + } else { + invariant(test, 'always present for `*Each` hooks'); + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, asyncError]); + } + } + } else if ( + event.name === 'test_fn_success' || + event.name === 'test_fn_failure' + ) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + + const {test} = event; + + invariant(test, 'always present for `*Each` hooks'); + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, event.test.asyncError]); + } + } else if (event.name === 'teardown') { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + + state.unhandledErrors.push( + ...state.unhandledRejectionErrorByPromise.values(), + ); + } +}; + +export default unhandledRejectionHandler; diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 13cf3b321e05..a098017c9833 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -85,6 +85,7 @@ export const makeTest = ( startedAt: null, status: null, timeout, + unhandledRejectionErrorByPromise: new Map(), }); // Traverse the tree of describe blocks and return true if at least one describe diff --git a/packages/jest-types/src/Circus.ts b/packages/jest-types/src/Circus.ts index f7d1c0374923..3ea5f01c81bf 100644 --- a/packages/jest-types/src/Circus.ts +++ b/packages/jest-types/src/Circus.ts @@ -81,6 +81,11 @@ export type SyncEvent = // an `afterAll` hook) name: 'error'; error: Exception; + promise?: Promise; + } + | { + name: 'error_handled'; + promise: Promise; }; export type AsyncEvent = @@ -198,6 +203,7 @@ export type RunResult = { export type TestResults = Array; export type GlobalErrorHandlers = { + rejectionHandled: Array<(promise: Promise) => void>; uncaughtException: Array<(exception: Exception) => void>; unhandledRejection: Array< (exception: Exception, promise: Promise) => void @@ -223,6 +229,7 @@ export type State = { unhandledErrors: Array; includeTestLocationInResult: boolean; maxConcurrency: number; + unhandledRejectionErrorByPromise: Map, Exception>; }; export type DescribeBlock = { @@ -256,4 +263,5 @@ export type TestEntry = { status?: TestStatus | null; // whether the test has been skipped or run already timeout?: number; failing: boolean; + unhandledRejectionErrorByPromise: Map, Exception>; }; From 67966d7a7dd7c7b2369c5a34f614e92ce51cb3c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Tue, 25 Apr 2023 22:15:15 +0200 Subject: [PATCH 03/15] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb90714a9e6..1c591ceaed2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Fixes +- `[jest-circus]` Prevent false test failures caused by promise rejections handled asynchronously ([#14110](https://github.com/jestjs/jest/pull/14110)) - `[jest-config]` Handle frozen config object ([#14054](https://github.com/facebook/jest/pull/14054)) - `[jest-core]` Always use workers in watch mode to avoid crashes ([#14059](https://github.com/facebook/jest/pull/14059)). - `[jest-environment-jsdom, jest-environment-node]` Fix assignment of `customExportConditions` via `testEnvironmentOptions` when custom env subclass defines a default value ([#13989](https://github.com/facebook/jest/pull/13989)) From 302e12dbbda6ba8b730b0db3214b7b94c1c36212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Tue, 25 Apr 2023 22:44:22 +0200 Subject: [PATCH 04/15] Skip promise async handling tests for Jasmine --- e2e/__tests__/promiseAsyncHandling.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e/__tests__/promiseAsyncHandling.test.ts b/e2e/__tests__/promiseAsyncHandling.test.ts index 2345ac7f3c0a..606bae071097 100644 --- a/e2e/__tests__/promiseAsyncHandling.test.ts +++ b/e2e/__tests__/promiseAsyncHandling.test.ts @@ -6,11 +6,14 @@ */ import * as path from 'path'; +import {skipSuiteOnJasmine} from '@jest/test-utils'; import {extractSortedSummary} from '../Utils'; import runJest from '../runJest'; const dir = path.resolve(__dirname, '../promise-async-handling'); +skipSuiteOnJasmine(); + test('fails because of unhandled promise rejection in test', () => { const {stderr, exitCode} = runJest(dir, ['unhandledRejectionTest.test.js']); From 9efe383df599a986c2bc1c1ce1255f1a31007c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Tue, 25 Apr 2023 23:48:08 +0200 Subject: [PATCH 05/15] Wait until next event loop turn when there is a reason to wait for rejectionHandled event only Awaiting for the next turn is potentially conflicting with fake timers usage. --- .../src/unhandledRejectionHandler.ts | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index 1bcf0f8a594a..08b68dde9171 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -19,49 +19,60 @@ const unhandledRejectionHandler: Circus.EventHandler = async ( state, ): Promise => { if (event.name === 'hook_success' || event.name === 'hook_failure') { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - const {test, describeBlock, hook} = event; const {asyncError, type} = hook; if (type === 'beforeAll') { invariant(describeBlock, 'always present for `*All` hooks'); - for (const error of state.unhandledRejectionErrorByPromise.values()) { - addErrorToEachTestUnderDescribe(describeBlock, error, asyncError); + if (state.unhandledRejectionErrorByPromise.size > 0) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + for (const error of state.unhandledRejectionErrorByPromise.values()) { + 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. - for (const error of state.unhandledRejectionErrorByPromise.values()) { - state.unhandledErrors.push([error, asyncError]); + if (state.unhandledRejectionErrorByPromise.size > 0) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + // Attaching `afterAll` errors to each test makes execution flow + // too complicated, so we'll consider them to be global. + for (const error of state.unhandledRejectionErrorByPromise.values()) { + state.unhandledErrors.push([error, asyncError]); + } } } else { invariant(test, 'always present for `*Each` hooks'); - for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, asyncError]); + if (test.unhandledRejectionErrorByPromise.size > 0) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, asyncError]); + } } } } else if ( event.name === 'test_fn_success' || event.name === 'test_fn_failure' ) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - const {test} = event; - invariant(test, 'always present for `*Each` hooks'); - for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, event.test.asyncError]); + + if (test.unhandledRejectionErrorByPromise.size > 0) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, event.test.asyncError]); + } } } else if (event.name === 'teardown') { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - - state.unhandledErrors.push( - ...state.unhandledRejectionErrorByPromise.values(), - ); + if (state.unhandledRejectionErrorByPromise.size > 0) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + state.unhandledErrors.push( + ...state.unhandledRejectionErrorByPromise.values(), + ); + } } }; From 644b7664b585edae105e767c21ed491bf6de5f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Wed, 26 Apr 2023 10:58:03 +0200 Subject: [PATCH 06/15] Fix failing test for detection of unhandled rejections --- .../src/unhandledRejectionHandler.ts | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index 08b68dde9171..f102a6298eb5 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -8,9 +8,14 @@ import type {Circus} from '@jest/types'; import {addErrorToEachTestUnderDescribe, invariant} from './utils'; +/** + * Let's keep the original setTimeout for the usage below so we do not need to care about usage of fake timers in tests. + */ +const originalSetTimeout = globalThis.setTimeout; + const untilNextEventLoopTurn = async () => { return new Promise(resolve => { - setTimeout(resolve, 0); + originalSetTimeout(resolve, 0); }); }; @@ -19,60 +24,49 @@ const unhandledRejectionHandler: Circus.EventHandler = async ( state, ): Promise => { if (event.name === 'hook_success' || event.name === 'hook_failure') { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + const {test, describeBlock, hook} = event; const {asyncError, type} = hook; if (type === 'beforeAll') { invariant(describeBlock, 'always present for `*All` hooks'); - if (state.unhandledRejectionErrorByPromise.size > 0) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - for (const error of state.unhandledRejectionErrorByPromise.values()) { - addErrorToEachTestUnderDescribe(describeBlock, error, asyncError); - } + for (const error of state.unhandledRejectionErrorByPromise.values()) { + addErrorToEachTestUnderDescribe(describeBlock, error, asyncError); } } else if (type === 'afterAll') { - if (state.unhandledRejectionErrorByPromise.size > 0) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - // Attaching `afterAll` errors to each test makes execution flow - // too complicated, so we'll consider them to be global. - for (const error of state.unhandledRejectionErrorByPromise.values()) { - state.unhandledErrors.push([error, asyncError]); - } + // Attaching `afterAll` errors to each test makes execution flow + // too complicated, so we'll consider them to be global. + for (const error of state.unhandledRejectionErrorByPromise.values()) { + state.unhandledErrors.push([error, asyncError]); } } else { invariant(test, 'always present for `*Each` hooks'); - if (test.unhandledRejectionErrorByPromise.size > 0) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, asyncError]); - } + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, asyncError]); } } } else if ( event.name === 'test_fn_success' || event.name === 'test_fn_failure' ) { + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + const {test} = event; invariant(test, 'always present for `*Each` hooks'); - if (test.unhandledRejectionErrorByPromise.size > 0) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, event.test.asyncError]); - } + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, event.test.asyncError]); } } else if (event.name === 'teardown') { - if (state.unhandledRejectionErrorByPromise.size > 0) { - // We need to give event loop the time to actually execute `rejectionHandled` event - await untilNextEventLoopTurn(); - state.unhandledErrors.push( - ...state.unhandledRejectionErrorByPromise.values(), - ); - } + // We need to give event loop the time to actually execute `rejectionHandled` event + await untilNextEventLoopTurn(); + + state.unhandledErrors.push( + ...state.unhandledRejectionErrorByPromise.values(), + ); } }; From e5640126bf3dd2403caf729207314dca4d9e72ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Wed, 26 Apr 2023 10:59:15 +0200 Subject: [PATCH 07/15] Update fake timers legacy test Legacy fake timers execute original setImmediate in its fake implementation. I believe removing that behaviour would cause more harm than failing the test here that seems to be synthetic anyway. --- e2e/__tests__/fakeTimersLegacy.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/fakeTimersLegacy.test.ts b/e2e/__tests__/fakeTimersLegacy.test.ts index 268ce5aa80c2..d16439102f29 100644 --- a/e2e/__tests__/fakeTimersLegacy.test.ts +++ b/e2e/__tests__/fakeTimersLegacy.test.ts @@ -42,7 +42,7 @@ describe('setImmediate', () => { const result = runJest('fake-timers-legacy/set-immediate'); expect(result.stderr).toMatch('setImmediate test'); - expect(result.exitCode).toBe(0); + expect(result.exitCode).toBe(1); }); }); From fc973e2d788162584a7c05c20c510f9c48959d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Wed, 26 Apr 2023 16:00:01 +0200 Subject: [PATCH 08/15] Fix fake timers legacy test for Jasmine runner --- e2e/__tests__/fakeTimersLegacy.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e/__tests__/fakeTimersLegacy.test.ts b/e2e/__tests__/fakeTimersLegacy.test.ts index d16439102f29..3ad3f3a7fb3f 100644 --- a/e2e/__tests__/fakeTimersLegacy.test.ts +++ b/e2e/__tests__/fakeTimersLegacy.test.ts @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import {isJestJasmineRun} from '@jest/test-utils'; import runJest from '../runJest'; describe('enableGlobally', () => { @@ -39,10 +40,13 @@ describe('requestAnimationFrame', () => { describe('setImmediate', () => { test('fakes setImmediate', () => { + // Jasmine runner does not handle unhandled promise rejections that are causing the test to fail in Jest circus + const expectedExitCode = isJestJasmineRun() ? 0 : 1; + const result = runJest('fake-timers-legacy/set-immediate'); expect(result.stderr).toMatch('setImmediate test'); - expect(result.exitCode).toBe(1); + expect(result.exitCode).toBe(expectedExitCode); }); }); From f2592e5fb8d8dffb1aa101d572fecdfffcb82e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Wed, 26 Apr 2023 16:05:44 +0200 Subject: [PATCH 09/15] Update failing tests because of one extra event loop turn --- .../__snapshots__/environmentAfterTeardown.test.ts.snap | 2 +- e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap | 2 +- e2e/environment-after-teardown/__tests__/afterTeardown.test.js | 2 +- e2e/require-after-teardown/__tests__/lateRequire.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap index e851be178ae8..ae9fa242a258 100644 --- a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap @@ -7,7 +7,7 @@ exports[`prints useful error for environment methods after test is done 1`] = ` 10 | setTimeout(() => { > 11 | jest.clearAllTimers(); | ^ - 12 | }, 0); + 12 | }, 10); 13 | }); 14 |" `; diff --git a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap index 1a3d3fd9b988..76b46645afb2 100644 --- a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap @@ -9,5 +9,5 @@ exports[`prints useful error for requires after test is done 1`] = ` | ^ 12 | 13 | expect(double(5)).toBe(10); - 14 | }, 0);" + 14 | }, 10);" `; diff --git a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js index fb23021297b3..1da6aacd7879 100644 --- a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js +++ b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js @@ -9,5 +9,5 @@ test('access environment methods after done', () => { setTimeout(() => { jest.clearAllTimers(); - }, 0); + }, 10); }); diff --git a/e2e/require-after-teardown/__tests__/lateRequire.test.js b/e2e/require-after-teardown/__tests__/lateRequire.test.js index 5ad13a131afd..5636fabdbff4 100644 --- a/e2e/require-after-teardown/__tests__/lateRequire.test.js +++ b/e2e/require-after-teardown/__tests__/lateRequire.test.js @@ -11,5 +11,5 @@ test('require after done', () => { const double = require('../'); expect(double(5)).toBe(10); - }, 0); + }, 10); }); From d7be001d7c1bee55d8086d16686108b6797dd5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Wed, 26 Apr 2023 16:32:41 +0200 Subject: [PATCH 10/15] Give event loop more time to proceed in background --- .../__snapshots__/environmentAfterTeardown.test.ts.snap | 2 +- e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap | 2 +- e2e/environment-after-teardown/__tests__/afterTeardown.test.js | 2 +- e2e/require-after-teardown/__tests__/lateRequire.test.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap index ae9fa242a258..2af30dfb6bbf 100644 --- a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap @@ -7,7 +7,7 @@ exports[`prints useful error for environment methods after test is done 1`] = ` 10 | setTimeout(() => { > 11 | jest.clearAllTimers(); | ^ - 12 | }, 10); + 12 | }, 100); 13 | }); 14 |" `; diff --git a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap index 76b46645afb2..32009e6f8db4 100644 --- a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap @@ -9,5 +9,5 @@ exports[`prints useful error for requires after test is done 1`] = ` | ^ 12 | 13 | expect(double(5)).toBe(10); - 14 | }, 10);" + 14 | }, 100);" `; diff --git a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js index 1da6aacd7879..dbc9bb1231f1 100644 --- a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js +++ b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js @@ -9,5 +9,5 @@ test('access environment methods after done', () => { setTimeout(() => { jest.clearAllTimers(); - }, 10); + }, 100); }); diff --git a/e2e/require-after-teardown/__tests__/lateRequire.test.js b/e2e/require-after-teardown/__tests__/lateRequire.test.js index 5636fabdbff4..1316a525ee2e 100644 --- a/e2e/require-after-teardown/__tests__/lateRequire.test.js +++ b/e2e/require-after-teardown/__tests__/lateRequire.test.js @@ -11,5 +11,5 @@ test('require after done', () => { const double = require('../'); expect(double(5)).toBe(10); - }, 10); + }, 100); }); From 64ef973333681a856e8e6f99f56da9174d23f405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 12 May 2023 22:41:26 +0200 Subject: [PATCH 11/15] Make usage of original timer function consistent with other code in jest-circus --- packages/jest-circus/src/unhandledRejectionHandler.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index f102a6298eb5..8b47ead55a5e 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -8,14 +8,13 @@ import type {Circus} from '@jest/types'; import {addErrorToEachTestUnderDescribe, invariant} from './utils'; -/** - * Let's keep the original setTimeout for the usage below so we do not need to care about usage of fake timers in tests. - */ -const originalSetTimeout = globalThis.setTimeout; +// Global values can be overwritten by mocks or tests. We'll capture +// the original values in the variables before we require any files. +const {setImmediate} = globalThis; const untilNextEventLoopTurn = async () => { return new Promise(resolve => { - originalSetTimeout(resolve, 0); + setImmediate(resolve); }); }; From d7b963c983ea7074ca8ac7d844a7ce51ec6672b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 12 May 2023 23:08:48 +0200 Subject: [PATCH 12/15] =?UTF-8?q?Fix=20usage=20of=20Node.js=20timer=20func?= =?UTF-8?q?tion=20=F0=9F=A4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/jest-circus/src/unhandledRejectionHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index 8b47ead55a5e..7a0a16b25b30 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -10,11 +10,11 @@ import {addErrorToEachTestUnderDescribe, invariant} from './utils'; // Global values can be overwritten by mocks or tests. We'll capture // the original values in the variables before we require any files. -const {setImmediate} = globalThis; +const {setTimeout} = globalThis; const untilNextEventLoopTurn = async () => { return new Promise(resolve => { - setImmediate(resolve); + setTimeout(resolve, 0); }); }; From 00ecc4650cda9abc2aebd772f5780a9401e0105d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Mon, 22 May 2023 22:49:57 +0200 Subject: [PATCH 13/15] Update comments --- packages/jest-circus/src/unhandledRejectionHandler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index 7a0a16b25b30..c8c6c842ac6d 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -23,7 +23,7 @@ const unhandledRejectionHandler: Circus.EventHandler = async ( state, ): Promise => { if (event.name === 'hook_success' || event.name === 'hook_failure') { - // We need to give event loop the time to actually execute `rejectionHandled` event + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events await untilNextEventLoopTurn(); const {test, describeBlock, hook} = event; @@ -50,7 +50,7 @@ const unhandledRejectionHandler: Circus.EventHandler = async ( event.name === 'test_fn_success' || event.name === 'test_fn_failure' ) { - // We need to give event loop the time to actually execute `rejectionHandled` event + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events await untilNextEventLoopTurn(); const {test} = event; @@ -60,7 +60,7 @@ const unhandledRejectionHandler: Circus.EventHandler = async ( test.errors.push([error, event.test.asyncError]); } } else if (event.name === 'teardown') { - // We need to give event loop the time to actually execute `rejectionHandled` event + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events await untilNextEventLoopTurn(); state.unhandledErrors.push( From b4123052adf13193410efc3eb128de9165fa1649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 26 May 2023 14:05:05 +0200 Subject: [PATCH 14/15] Fix reporting of requires or Jest env method access after "teardown" --- .../environmentAfterTeardown.test.ts.snap | 4 +- .../requireAfterTeardown.test.ts.snap | 4 +- .../environmentAfterTeardown.test.ts | 6 +- e2e/__tests__/requireAfterTeardown.test.ts | 6 +- .../__tests__/afterTeardown.test.js | 2 +- .../__tests__/lateRequire.test.js | 2 +- .../legacy-code-todo-rewrite/jestAdapter.ts | 1 + .../jestAdapterInit.ts | 6 ++ packages/jest-circus/src/state.ts | 2 - .../src/unhandledRejectionHandler.ts | 94 ++++++++++--------- packages/jest-runtime/src/index.ts | 29 ++++++ 11 files changed, 99 insertions(+), 57 deletions(-) diff --git a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap index 2af30dfb6bbf..ced63eb48256 100644 --- a/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`prints useful error for environment methods after test is done 1`] = ` -"ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From __tests__/afterTeardown.test.js. +" ReferenceError: You are trying to access a property or method of the Jest environment outside of the scope of the test code. 9 | test('access environment methods after done', () => { 10 | setTimeout(() => { > 11 | jest.clearAllTimers(); | ^ - 12 | }, 100); + 12 | }, 0); 13 | }); 14 |" `; diff --git a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap index 32009e6f8db4..afa6ff3749e2 100644 --- a/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap +++ b/e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`prints useful error for requires after test is done 1`] = ` -"ReferenceError: You are trying to \`import\` a file after the Jest environment has been torn down. From __tests__/lateRequire.test.js. +" ReferenceError: You are trying to \`import\` a file outside of the scope of the test code. 9 | test('require after done', () => { 10 | setTimeout(() => { @@ -9,5 +9,5 @@ exports[`prints useful error for requires after test is done 1`] = ` | ^ 12 | 13 | expect(double(5)).toBe(10); - 14 | }, 100);" + 14 | }, 0);" `; diff --git a/e2e/__tests__/environmentAfterTeardown.test.ts b/e2e/__tests__/environmentAfterTeardown.test.ts index 1216014a2af4..1405b946bf7e 100644 --- a/e2e/__tests__/environmentAfterTeardown.test.ts +++ b/e2e/__tests__/environmentAfterTeardown.test.ts @@ -9,10 +9,10 @@ import runJest from '../runJest'; test('prints useful error for environment methods after test is done', () => { const {stderr} = runJest('environment-after-teardown'); - const interestingLines = stderr.split('\n').slice(9, 18).join('\n'); + const interestingLines = stderr.split('\n').slice(5, 14).join('\n'); expect(interestingLines).toMatchSnapshot(); - expect(stderr.split('\n')[9]).toBe( - 'ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From __tests__/afterTeardown.test.js.', + expect(stderr.split('\n')[5]).toMatch( + 'ReferenceError: You are trying to access a property or method of the Jest environment outside of the scope of the test code.', ); }); diff --git a/e2e/__tests__/requireAfterTeardown.test.ts b/e2e/__tests__/requireAfterTeardown.test.ts index cb9607549b85..c25c531a0013 100644 --- a/e2e/__tests__/requireAfterTeardown.test.ts +++ b/e2e/__tests__/requireAfterTeardown.test.ts @@ -10,10 +10,10 @@ import runJest from '../runJest'; test('prints useful error for requires after test is done', () => { const {stderr} = runJest('require-after-teardown'); - const interestingLines = stderr.split('\n').slice(9, 18).join('\n'); + const interestingLines = stderr.split('\n').slice(5, 14).join('\n'); expect(interestingLines).toMatchSnapshot(); - expect(stderr.split('\n')[19]).toMatch( - new RegExp('(__tests__/lateRequire.test.js:11:20)'), + expect(stderr.split('\n')[16]).toMatch( + '(__tests__/lateRequire.test.js:11:20)', ); }); diff --git a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js index dbc9bb1231f1..fb23021297b3 100644 --- a/e2e/environment-after-teardown/__tests__/afterTeardown.test.js +++ b/e2e/environment-after-teardown/__tests__/afterTeardown.test.js @@ -9,5 +9,5 @@ test('access environment methods after done', () => { setTimeout(() => { jest.clearAllTimers(); - }, 100); + }, 0); }); diff --git a/e2e/require-after-teardown/__tests__/lateRequire.test.js b/e2e/require-after-teardown/__tests__/lateRequire.test.js index 1316a525ee2e..5ad13a131afd 100644 --- a/e2e/require-after-teardown/__tests__/lateRequire.test.js +++ b/e2e/require-after-teardown/__tests__/lateRequire.test.js @@ -11,5 +11,5 @@ test('require after done', () => { const double = require('../'); expect(double(5)).toBe(10); - }, 100); + }, 0); }); diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 80b4503621b7..1aae32296423 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -33,6 +33,7 @@ const jestAdapter = async ( globalConfig, localRequire: runtime.requireModule.bind(runtime), parentProcess: process, + runtime, sendMessageToJest, setGlobalsForRuntime: runtime.setGlobalsForRuntime.bind(runtime), testPath, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index f9ac8076bd55..416d6555ddbf 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -16,6 +16,7 @@ import { } from '@jest/test-result'; import type {Circus, Config, Global} from '@jest/types'; import {formatExecError, formatResultsErrors} from 'jest-message-util'; +import type Runtime from 'jest-runtime'; import { SnapshotState, addSerializer, @@ -30,6 +31,7 @@ import { getState as getRunnerState, } from '../state'; import testCaseReportHandler from '../testCaseReportHandler'; +import {unhandledRejectionHandler} from '../unhandledRejectionHandler'; import {getTestID} from '../utils'; interface RuntimeGlobals extends Global.TestFrameworkGlobals { @@ -39,6 +41,7 @@ interface RuntimeGlobals extends Global.TestFrameworkGlobals { export const initialize = async ({ config, environment, + runtime, globalConfig, localRequire, parentProcess, @@ -48,6 +51,7 @@ export const initialize = async ({ }: { config: Config.ProjectConfig; environment: JestEnvironment; + runtime: Runtime; globalConfig: Config.GlobalConfig; localRequire: (path: string) => T; testPath: string; @@ -129,6 +133,8 @@ export const initialize = async ({ addEventHandler(testCaseReportHandler(testPath, sendMessageToJest)); } + addEventHandler(unhandledRejectionHandler(runtime)); + // Return it back to the outer scope (test runner outside the VM). return {globals: globalsObject, snapshotState}; }; diff --git a/packages/jest-circus/src/state.ts b/packages/jest-circus/src/state.ts index 4c5141f3f6e4..5dbc152f8399 100644 --- a/packages/jest-circus/src/state.ts +++ b/packages/jest-circus/src/state.ts @@ -9,12 +9,10 @@ import type {Circus} from '@jest/types'; import eventHandler from './eventHandler'; import formatNodeAssertErrors from './formatNodeAssertErrors'; import {STATE_SYM} from './types'; -import unhandledRejectionHandler from './unhandledRejectionHandler'; import {makeDescribe} from './utils'; const eventHandlers: Array = [ eventHandler, - unhandledRejectionHandler, formatNodeAssertErrors, ]; diff --git a/packages/jest-circus/src/unhandledRejectionHandler.ts b/packages/jest-circus/src/unhandledRejectionHandler.ts index c8c6c842ac6d..309f9e0b2e10 100644 --- a/packages/jest-circus/src/unhandledRejectionHandler.ts +++ b/packages/jest-circus/src/unhandledRejectionHandler.ts @@ -6,6 +6,7 @@ */ import type {Circus} from '@jest/types'; +import type Runtime from 'jest-runtime'; import {addErrorToEachTestUnderDescribe, invariant} from './utils'; // Global values can be overwritten by mocks or tests. We'll capture @@ -18,55 +19,62 @@ const untilNextEventLoopTurn = async () => { }); }; -const unhandledRejectionHandler: Circus.EventHandler = async ( - event, - state, -): Promise => { - if (event.name === 'hook_success' || event.name === 'hook_failure') { - // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events - await untilNextEventLoopTurn(); +export const unhandledRejectionHandler = ( + runtime: Runtime, +): Circus.EventHandler => { + return async (event, state) => { + if (event.name === 'hook_start') { + runtime.enterTestCode(); + } else if (event.name === 'hook_success' || event.name === 'hook_failure') { + runtime.leaveTestCode(); - const {test, describeBlock, hook} = event; - const {asyncError, type} = hook; + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events + await untilNextEventLoopTurn(); - if (type === 'beforeAll') { - invariant(describeBlock, 'always present for `*All` hooks'); - for (const error of state.unhandledRejectionErrorByPromise.values()) { - 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. - for (const error of state.unhandledRejectionErrorByPromise.values()) { - state.unhandledErrors.push([error, asyncError]); + const {test, describeBlock, hook} = event; + const {asyncError, type} = hook; + + if (type === 'beforeAll') { + invariant(describeBlock, 'always present for `*All` hooks'); + for (const error of state.unhandledRejectionErrorByPromise.values()) { + 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. + for (const error of state.unhandledRejectionErrorByPromise.values()) { + state.unhandledErrors.push([error, asyncError]); + } + } else { + invariant(test, 'always present for `*Each` hooks'); + for (const error of test.unhandledRejectionErrorByPromise.values()) { + test.errors.push([error, asyncError]); + } } - } else { + } else if (event.name === 'test_fn_start') { + runtime.enterTestCode(); + } else if ( + event.name === 'test_fn_success' || + event.name === 'test_fn_failure' + ) { + runtime.leaveTestCode(); + + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events + await untilNextEventLoopTurn(); + + const {test} = event; invariant(test, 'always present for `*Each` hooks'); + for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, asyncError]); + test.errors.push([error, event.test.asyncError]); } - } - } else if ( - event.name === 'test_fn_success' || - event.name === 'test_fn_failure' - ) { - // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events - await untilNextEventLoopTurn(); - - const {test} = event; - invariant(test, 'always present for `*Each` hooks'); + } else if (event.name === 'teardown') { + // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events + await untilNextEventLoopTurn(); - for (const error of test.unhandledRejectionErrorByPromise.values()) { - test.errors.push([error, event.test.asyncError]); + state.unhandledErrors.push( + ...state.unhandledRejectionErrorByPromise.values(), + ); } - } else if (event.name === 'teardown') { - // We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events - await untilNextEventLoopTurn(); - - state.unhandledErrors.push( - ...state.unhandledRejectionErrorByPromise.values(), - ); - } + }; }; - -export default unhandledRejectionHandler; diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index f9945f8e7f72..b2e1735fb488 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -208,6 +208,7 @@ export default class Runtime { private readonly esmConditions: Array; private readonly cjsConditions: Array; private isTornDown = false; + private isInsideTestCode: boolean | undefined; constructor( config: Config.ProjectConfig, @@ -562,6 +563,11 @@ export default class Runtime { // @ts-expect-error - exiting return; } + if (this.isInsideTestCode === false) { + throw new ReferenceError( + 'You are trying to `import` a file outside of the scope of the test code.', + ); + } if (specifier === '@jest/globals') { const fromCache = this._esmoduleRegistry.get('@jest/globals'); @@ -706,6 +712,11 @@ export default class Runtime { process.exitCode = 1; return; } + if (this.isInsideTestCode === false) { + throw new ReferenceError( + 'You are trying to `import` a file outside of the scope of the test code.', + ); + } if (module.status === 'unlinked') { // since we might attempt to link the same module in parallel, stick the promise in a weak map so every call to @@ -1342,6 +1353,14 @@ export default class Runtime { this._moduleMocker.clearAllMocks(); } + enterTestCode(): void { + this.isInsideTestCode = true; + } + + leaveTestCode(): void { + this.isInsideTestCode = false; + } + teardown(): void { this.restoreAllMocks(); this.resetModules(); @@ -1485,6 +1504,11 @@ export default class Runtime { process.exitCode = 1; return; } + if (this.isInsideTestCode === false) { + throw new ReferenceError( + 'You are trying to `import` a file outside of the scope of the test code.', + ); + } // If the environment was disposed, prevent this module from being executed. if (!this._environment.global) { @@ -2169,6 +2193,11 @@ export default class Runtime { ); process.exitCode = 1; } + if (this.isInsideTestCode === false) { + throw new ReferenceError( + 'You are trying to access a property or method of the Jest environment outside of the scope of the test code.', + ); + } return this._fakeTimersImplementation!; }; From b775b190b1eea4f10f81e3ae8b35aeb8db8c4e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20S=CC=8Ctekl?= Date: Fri, 26 May 2023 15:11:13 +0200 Subject: [PATCH 15/15] Fix tests for Jasmine --- ...vironmentAfterTeardownJasmine.test.ts.snap | 13 +++++++++++ .../requireAfterTeardownJasmine.test.ts.snap | 13 +++++++++++ .../environmentAfterTeardown.test.ts | 3 +++ .../environmentAfterTeardownJasmine.test.ts | 21 ++++++++++++++++++ e2e/__tests__/requireAfterTeardown.test.ts | 3 +++ .../requireAfterTeardownJasmine.test.ts | 22 +++++++++++++++++++ 6 files changed, 75 insertions(+) create mode 100644 e2e/__tests__/__snapshots__/environmentAfterTeardownJasmine.test.ts.snap create mode 100644 e2e/__tests__/__snapshots__/requireAfterTeardownJasmine.test.ts.snap create mode 100644 e2e/__tests__/environmentAfterTeardownJasmine.test.ts create mode 100644 e2e/__tests__/requireAfterTeardownJasmine.test.ts diff --git a/e2e/__tests__/__snapshots__/environmentAfterTeardownJasmine.test.ts.snap b/e2e/__tests__/__snapshots__/environmentAfterTeardownJasmine.test.ts.snap new file mode 100644 index 000000000000..e851be178ae8 --- /dev/null +++ b/e2e/__tests__/__snapshots__/environmentAfterTeardownJasmine.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prints useful error for environment methods after test is done 1`] = ` +"ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From __tests__/afterTeardown.test.js. + + 9 | test('access environment methods after done', () => { + 10 | setTimeout(() => { + > 11 | jest.clearAllTimers(); + | ^ + 12 | }, 0); + 13 | }); + 14 |" +`; diff --git a/e2e/__tests__/__snapshots__/requireAfterTeardownJasmine.test.ts.snap b/e2e/__tests__/__snapshots__/requireAfterTeardownJasmine.test.ts.snap new file mode 100644 index 000000000000..1a3d3fd9b988 --- /dev/null +++ b/e2e/__tests__/__snapshots__/requireAfterTeardownJasmine.test.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prints useful error for requires after test is done 1`] = ` +"ReferenceError: You are trying to \`import\` a file after the Jest environment has been torn down. From __tests__/lateRequire.test.js. + + 9 | test('require after done', () => { + 10 | setTimeout(() => { + > 11 | const double = require('../'); + | ^ + 12 | + 13 | expect(double(5)).toBe(10); + 14 | }, 0);" +`; diff --git a/e2e/__tests__/environmentAfterTeardown.test.ts b/e2e/__tests__/environmentAfterTeardown.test.ts index 1405b946bf7e..7e71888b5ddb 100644 --- a/e2e/__tests__/environmentAfterTeardown.test.ts +++ b/e2e/__tests__/environmentAfterTeardown.test.ts @@ -5,8 +5,11 @@ * LICENSE file in the root directory of this source tree. */ +import {skipSuiteOnJasmine} from '@jest/test-utils'; import runJest from '../runJest'; +skipSuiteOnJasmine(); + test('prints useful error for environment methods after test is done', () => { const {stderr} = runJest('environment-after-teardown'); const interestingLines = stderr.split('\n').slice(5, 14).join('\n'); diff --git a/e2e/__tests__/environmentAfterTeardownJasmine.test.ts b/e2e/__tests__/environmentAfterTeardownJasmine.test.ts new file mode 100644 index 000000000000..36e769478e0f --- /dev/null +++ b/e2e/__tests__/environmentAfterTeardownJasmine.test.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {skipSuiteOnJestCircus} from '@jest/test-utils'; +import runJest from '../runJest'; + +skipSuiteOnJestCircus(); + +test('prints useful error for environment methods after test is done', () => { + const {stderr} = runJest('environment-after-teardown'); + const interestingLines = stderr.split('\n').slice(9, 18).join('\n'); + + expect(interestingLines).toMatchSnapshot(); + expect(stderr.split('\n')[9]).toBe( + 'ReferenceError: You are trying to access a property or method of the Jest environment after it has been torn down. From __tests__/afterTeardown.test.js.', + ); +}); diff --git a/e2e/__tests__/requireAfterTeardown.test.ts b/e2e/__tests__/requireAfterTeardown.test.ts index c25c531a0013..764a593db864 100644 --- a/e2e/__tests__/requireAfterTeardown.test.ts +++ b/e2e/__tests__/requireAfterTeardown.test.ts @@ -5,8 +5,11 @@ * LICENSE file in the root directory of this source tree. */ +import {skipSuiteOnJasmine} from '@jest/test-utils'; import runJest from '../runJest'; +skipSuiteOnJasmine(); + test('prints useful error for requires after test is done', () => { const {stderr} = runJest('require-after-teardown'); diff --git a/e2e/__tests__/requireAfterTeardownJasmine.test.ts b/e2e/__tests__/requireAfterTeardownJasmine.test.ts new file mode 100644 index 000000000000..3eeb390ad8ea --- /dev/null +++ b/e2e/__tests__/requireAfterTeardownJasmine.test.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {skipSuiteOnJestCircus} from '@jest/test-utils'; +import runJest from '../runJest'; + +skipSuiteOnJestCircus(); + +test('prints useful error for requires after test is done', () => { + const {stderr} = runJest('require-after-teardown'); + + const interestingLines = stderr.split('\n').slice(9, 18).join('\n'); + + expect(interestingLines).toMatchSnapshot(); + expect(stderr.split('\n')[19]).toMatch( + '(__tests__/lateRequire.test.js:11:20)', + ); +});