Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add waitNextEventLoopTurnForUnhandledRejectionEvents flag #14681

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
stekycz marked this conversation as resolved.
Show resolved Hide resolved

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).
stekycz marked this conversation as resolved.
Show resolved Hide resolved

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
Loading