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 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 @@ -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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 |"
`;
236 changes: 236 additions & 0 deletions e2e/__tests__/__snapshots__/promiseAsyncHandling.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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.",
}
`;
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);"
`;
9 changes: 6 additions & 3 deletions e2e/__tests__/environmentAfterTeardown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
* 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(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.',
);
});
21 changes: 21 additions & 0 deletions e2e/__tests__/environmentAfterTeardownJasmine.test.ts
Original file line number Diff line number Diff line change
@@ -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.',
);
});
6 changes: 5 additions & 1 deletion e2e/__tests__/fakeTimersLegacy.test.ts
Original file line number Diff line number Diff line change
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
Loading