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

Prevent false test failures caused by promise rejections handled asynchronously #14110

Merged
merged 18 commits into from Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
Expand Up @@ -7,7 +7,7 @@ exports[`prints useful error for environment methods after test is done 1`] = `
10 | setTimeout(() => {
> 11 | jest.clearAllTimers();
| ^
12 | }, 0);
12 | }, 100);
13 | });
14 |"
`;
236 changes: 236 additions & 0 deletions e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap
@@ -0,0 +1,236 @@
// 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.<anonymous> (__tests__/unhandledRejectionAfterAll.test.js:13:18)",
"summary": "Test Suites: 1 failed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
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.<anonymous> (__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.<anonymous> (__tests__/unhandledRejectionAfterEach.test.js:13:18)",
"summary": "Test Suites: 1 failed, 1 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
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

● foo

REJECTED

11 |
12 | beforeAll(async () => {
> 13 | Promise.reject(new Error('REJECTED'));
| ^
14 |
15 | await promisify(setTimeout)(0);
16 | });

at Object.<anonymous> (__tests__/unhandledRejectionBeforeAll.test.js:13:18)",
"summary": "Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
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.<anonymous> (__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.<anonymous> (__tests__/unhandledRejectionBeforeEach.test.js:13:18)",
"summary": "Test Suites: 1 failed, 1 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
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/o event loop turn after rejection

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.<anonymous> (__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.<anonymous> (__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.<anonymous> (__tests__/unhandledRejectionTest.test.js:23: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)

● 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.<anonymous> (__tests__/unhandledRejectionTest.test.js:29:18)",
"summary": "Test Suites: 1 failed, 1 total
Tests: 4 failed, 4 total
Snapshots: 0 total
Time: <<REPLACED>>
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: <<REPLACED>>
Ran all test suites matching /rejectionHandled.test.js/i.",
}
`;
Expand Up @@ -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 | }, 100);"
`;
6 changes: 5 additions & 1 deletion e2e/__tests__/fakeTimersLegacy.test.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(0);
expect(result.exitCode).toBe(expectedExitCode);
});
});

Expand Down
71 changes: 71 additions & 0 deletions e2e/__tests__/promiseAsyncHandling.test.ts
@@ -0,0 +1,71 @@
/**
* 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 {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']);

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();
});
Expand Up @@ -9,5 +9,5 @@
test('access environment methods after done', () => {
setTimeout(() => {
jest.clearAllTimers();
}, 0);
}, 100);
});