Skip to content

Commit

Permalink
Add waitNextEventLoopTurnForUnhandledRejectionEvents flag (#14681)
Browse files Browse the repository at this point in the history
  • Loading branch information
stekycz committed Nov 6, 2023
1 parent 4fedfbd commit d1a2ed7
Show file tree
Hide file tree
Showing 25 changed files with 182 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
- `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369))
- `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584))
- `[@jest/core]` [**BREAKING**] Group together open handles with the same stack trace ([#13417](https://github.com/jestjs/jest/pull/13417), & [#14543](https://github.com/jestjs/jest/pull/14543))
Expand Down
8 changes: 8 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ Display individual test results with the test suite hierarchy.

Alias: `-v`. Print the version and exit.

### `--waitNextEventLoopTurnForUnhandledRejectionEvents`

Gives one event loop turn to handle `rejectionHandled`, `uncaughtException` or `unhandledRejection`.

Without this flag Jest may report false-positive errors (e.g. actually handled rejection reported) or not report actually unhandled rejection (or report it for different test case).

This option may add a noticeable overhead for fast test suites.

### `--watch`

Watch files for changes and rerun tests related to changed files. If you want to re-run all tests when a file has changed, use the `--watchAll` option instead.
Expand Down
8 changes: 8 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,14 @@ Default: `false` or `true` if there is only one test file to run

Indicates whether each individual test should be reported during the run. All errors will also still be shown on the bottom after execution.

### `waitNextEventLoopTurnForUnhandledRejectionEvents` \[boolean]

Gives one event loop turn to handle `rejectionHandled`, `uncaughtException` or `unhandledRejection`.

Without this flag Jest may report false-positive errors (e.g. actually handled rejection reported) or not report actually unhandled rejection (or report it for different test case).

This option may add a noticeable overhead for fast test suites.

### `watchPathIgnorePatterns` \[array<string>]

Default: `[]`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`prints useful error for environment methods after test is done 1`] = `
exports[`prints useful error for environment methods after test is done w/ \`waitNextEventLoopTurnForUnhandledRejectionEvents\` 1`] = `
" 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', () => {
Expand All @@ -11,3 +11,15 @@ exports[`prints useful error for environment methods after test is done 1`] = `
13 | });
14 |"
`;

exports[`prints useful error for environment methods after test is done w/o \`waitNextEventLoopTurnForUnhandledRejectionEvents\` 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 |"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ exports[`prints useful error for environment methods after test is done 1`] = `
13 | });
14 |"
`;

exports[`prints useful error for environment methods after test is done 2`] = `
"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 |"
`;
14 changes: 13 additions & 1 deletion e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`prints useful error for requires after test is done 1`] = `
exports[`prints useful error for requires after test is done w/ \`waitNextEventLoopTurnForUnhandledRejectionEvents\` 1`] = `
" ReferenceError: You are trying to \`import\` a file outside of the scope of the test code.
9 | test('require after done', () => {
Expand All @@ -11,3 +11,15 @@ exports[`prints useful error for requires after test is done 1`] = `
13 | expect(double(5)).toBe(10);
14 | }, 0);"
`;
exports[`prints useful error for requires after test is done w/o \`waitNextEventLoopTurnForUnhandledRejectionEvents\` 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);"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ exports[`prints useful error for requires after test is done 1`] = `
13 | expect(double(5)).toBe(10);
14 | }, 0);"
`;
exports[`prints useful error for requires after test is done 2`] = `
"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);"
`;
2 changes: 2 additions & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"/node_modules/",
"<<REPLACED_PNP_PATH>>"
],
"waitNextEventLoopTurnForUnhandledRejectionEvents": false,
"watchPathIgnorePatterns": []
}
],
Expand Down Expand Up @@ -143,6 +144,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"testSequencer": "<<REPLACED_JEST_PACKAGES_DIR>>/jest-test-sequencer/build/index.js",
"updateSnapshot": "none",
"useStderr": false,
"waitNextEventLoopTurnForUnhandledRejectionEvents": false,
"watch": false,
"watchAll": false,
"watchman": true,
Expand Down
14 changes: 13 additions & 1 deletion e2e/__tests__/environmentAfterTeardown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,20 @@ import runJest from '../runJest';

skipSuiteOnJasmine();

test('prints useful error for environment methods after test is done', () => {
test('prints useful error for environment methods after test is done w/o `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
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.',
);
});

test('prints useful error for environment methods after test is done w/ `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
const {stderr} = runJest('environment-after-teardown', [
'--waitNextEventLoopTurnForUnhandledRejectionEvents',
]);
const interestingLines = stderr.split('\n').slice(5, 14).join('\n');

expect(interestingLines).toMatchSnapshot();
Expand Down
23 changes: 15 additions & 8 deletions e2e/__tests__/environmentAfterTeardownJasmine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ 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');
test.each`
jestArgs
${[]}
${['--waitNextEventLoopTurnForUnhandledRejectionEvents']}
`(
'prints useful error for environment methods after test is done',
({jestArgs}) => {
const {stderr} = runJest('environment-after-teardown', jestArgs);
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.',
);
});
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.',
);
},
);
13 changes: 11 additions & 2 deletions e2e/__tests__/fakeTimersLegacy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ describe('requestAnimationFrame', () => {
});

describe('setImmediate', () => {
test('fakes setImmediate', () => {
test('fakes setImmediate w/o `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
const result = runJest('fake-timers-legacy/set-immediate');

expect(result.stderr).toMatch('setImmediate test');
expect(result.exitCode).toBe(0);
});

test('fakes setImmediate w/ `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
// 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');
const result = runJest('fake-timers-legacy/set-immediate', [
'--waitNextEventLoopTurnForUnhandledRejectionEvents',
]);

expect(result.stderr).toMatch('setImmediate test');
expect(result.exitCode).toBe(expectedExitCode);
Expand Down
15 changes: 14 additions & 1 deletion e2e/__tests__/requireAfterTeardown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,22 @@ import runJest from '../runJest';

skipSuiteOnJasmine();

test('prints useful error for requires after test is done', () => {
test('prints useful error for requires after test is done w/o `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
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)',
);
});

test('prints useful error for requires after test is done w/ `waitNextEventLoopTurnForUnhandledRejectionEvents`', () => {
const {stderr} = runJest('require-after-teardown', [
'--waitNextEventLoopTurnForUnhandledRejectionEvents',
]);

const interestingLines = stderr.split('\n').slice(5, 14).join('\n');

expect(interestingLines).toMatchSnapshot();
Expand Down
8 changes: 6 additions & 2 deletions e2e/__tests__/requireAfterTeardownJasmine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import runJest from '../runJest';

skipSuiteOnJestCircus();

test('prints useful error for requires after test is done', () => {
const {stderr} = runJest('require-after-teardown');
test.each`
jestArgs
${[]}
${['--waitNextEventLoopTurnForUnhandledRejectionEvents']}
`('prints useful error for requires after test is done', ({jestArgs}) => {
const {stderr} = runJest('require-after-teardown', jestArgs);

const interestingLines = stderr.split('\n').slice(9, 18).join('\n');

Expand Down
3 changes: 2 additions & 1 deletion e2e/promise-async-handling/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"jest": {
"testEnvironment": "node"
"testEnvironment": "node",
"waitNextEventLoopTurnForUnhandledRejectionEvents": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ export const initialize = async ({
addEventHandler(testCaseReportHandler(testPath, sendMessageToJest));
}

addEventHandler(unhandledRejectionHandler(runtime));
addEventHandler(
unhandledRejectionHandler(
runtime,
globalConfig.waitNextEventLoopTurnForUnhandledRejectionEvents,
),
);

// Return it back to the outer scope (test runner outside the VM).
return {globals: globalsObject, snapshotState};
Expand Down
19 changes: 13 additions & 6 deletions packages/jest-circus/src/unhandledRejectionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@ const untilNextEventLoopTurn = async () => {

export const unhandledRejectionHandler = (
runtime: Runtime,
waitNextEventLoopTurnForUnhandledRejectionEvents: boolean,
): 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();

// We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events
await untilNextEventLoopTurn();
if (waitNextEventLoopTurnForUnhandledRejectionEvents) {
// We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events
await untilNextEventLoopTurn();
}

const {test, describeBlock, hook} = event;
const {asyncError, type} = hook;
Expand Down Expand Up @@ -60,8 +63,10 @@ export const unhandledRejectionHandler = (
) {
runtime.leaveTestCode();

// We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events
await untilNextEventLoopTurn();
if (waitNextEventLoopTurnForUnhandledRejectionEvents) {
// 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');
Expand All @@ -70,8 +75,10 @@ export const unhandledRejectionHandler = (
test.errors.push([error, event.test.asyncError]);
}
} else if (event.name === 'teardown') {
// We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events
await untilNextEventLoopTurn();
if (waitNextEventLoopTurnForUnhandledRejectionEvents) {
// We need to give event loop the time to actually execute `rejectionHandled`, `uncaughtException` or `unhandledRejection` events
await untilNextEventLoopTurn();
}

state.unhandledErrors.push(
...state.unhandledRejectionErrorByPromise.values(),
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,12 @@ export const options: {[key: string]: Options} = {
'Display individual test results with the test suite hierarchy.',
type: 'boolean',
},
waitNextEventLoopTurnForUnhandledRejectionEvents: {
description:
'Gives one event loop turn to handle `rejectionHandled`, ' +
'`uncaughtException` or `unhandledRejection`.',
type: 'boolean',
},
watch: {
description:
'Watch files for changes and rerun tests related to ' +
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/Defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const defaultOptions: Config.DefaultOptions = {
testSequencer: '@jest/test-sequencer',
transformIgnorePatterns: [NODE_MODULES_REGEXP, `\\.pnp\\.[^\\${sep}]+$`],
useStderr: false,
waitNextEventLoopTurnForUnhandledRejectionEvents: false,
watch: false,
watchPathIgnorePatterns: [],
watchman: true,
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-config/src/ValidConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export const initialOptions: Config.InitialOptions = {
updateSnapshot: true,
useStderr: false,
verbose: false,
waitNextEventLoopTurnForUnhandledRejectionEvents: false,
watch: false,
watchAll: false,
watchPathIgnorePatterns: ['<rootDir>/e2e/'],
Expand Down Expand Up @@ -320,6 +321,7 @@ export const initialProjectOptions: Config.InitialProjectOptions = {
},
transformIgnorePatterns: [NODE_MODULES_REGEXP],
unmockedModulePathPatterns: ['mock'],
waitNextEventLoopTurnForUnhandledRejectionEvents: false,
watchPathIgnorePatterns: ['<rootDir>/e2e/'],
workerIdleMemoryLimit: multipleValidOptions(0.2, '50%'),
};
4 changes: 4 additions & 0 deletions packages/jest-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ const groupOptions = (
updateSnapshot: options.updateSnapshot,
useStderr: options.useStderr,
verbose: options.verbose,
waitNextEventLoopTurnForUnhandledRejectionEvents:
options.waitNextEventLoopTurnForUnhandledRejectionEvents,
watch: options.watch,
watchAll: options.watchAll,
watchPlugins: options.watchPlugins,
Expand Down Expand Up @@ -203,6 +205,8 @@ const groupOptions = (
transform: options.transform,
transformIgnorePatterns: options.transformIgnorePatterns,
unmockedModulePathPatterns: options.unmockedModulePathPatterns,
waitNextEventLoopTurnForUnhandledRejectionEvents:
options.waitNextEventLoopTurnForUnhandledRejectionEvents,
watchPathIgnorePatterns: options.watchPathIgnorePatterns,
}),
});
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ export default async function normalize(
case 'testNamePattern':
case 'useStderr':
case 'verbose':
case 'waitNextEventLoopTurnForUnhandledRejectionEvents':
case 'watch':
case 'watchAll':
case 'watchman':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ exports[`prints the config object 1`] = `
"testRunner": "myRunner",
"transform": [],
"transformIgnorePatterns": [],
"waitNextEventLoopTurnForUnhandledRejectionEvents": false,
"watchPathIgnorePatterns": []
},
"globalConfig": {
Expand Down Expand Up @@ -115,6 +116,7 @@ exports[`prints the config object 1`] = `
"updateSnapshot": "none",
"useStderr": false,
"verbose": false,
"waitNextEventLoopTurnForUnhandledRejectionEvents": false,
"watch": true,
"watchAll": false,
"watchPlugins": [],
Expand Down

0 comments on commit d1a2ed7

Please sign in to comment.