Skip to content

Commit db42fe5

Browse files
authored
Prevent false test failures caused by promise rejections handled asynchronously (#14315)
1 parent e7b1e75 commit db42fe5

28 files changed

+772
-19
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
### Fixes
1515

1616
- `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109))
17+
- `[jest-circus]` [**BREAKING**] Prevent false test failures caused by promise rejections handled asynchronously ([#14315](https://github.com/jestjs/jest/pull/14315))
1718
- `[@jest/expect-utils]` Fix comparison of `DataView` ([#14408](https://github.com/jestjs/jest/pull/14408))
1819
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))
1920
- `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552))

e2e/__tests__/__snapshots__/environmentAfterTeardown.test.ts.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`prints useful error for environment methods after test is done 1`] = `
4-
"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.
4+
" ReferenceError: You are trying to access a property or method of the Jest environment outside of the scope of the test code.
55
66
9 | test('access environment methods after done', () => {
77
10 | setTimeout(() => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`prints useful error for environment methods after test is done 1`] = `
4+
"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.
5+
6+
9 | test('access environment methods after done', () => {
7+
10 | setTimeout(() => {
8+
> 11 | jest.clearAllTimers();
9+
| ^
10+
12 | }, 0);
11+
13 | });
12+
14 |"
13+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`fails because of unhandled promise rejection in afterAll hook 1`] = `
4+
Object {
5+
"rest": "FAIL __tests__/unhandledRejectionAfterAll.test.js
6+
7+
8+
Test suite failed to run
9+
10+
REJECTED
11+
12+
11 |
13+
12 | afterAll(async () => {
14+
> 13 | Promise.reject(new Error('REJECTED'));
15+
| ^
16+
14 |
17+
15 | await promisify(setTimeout)(0);
18+
16 | });
19+
20+
at Object.<anonymous> (__tests__/unhandledRejectionAfterAll.test.js:13:18)",
21+
"summary": "Test Suites: 1 failed, 1 total
22+
Tests: 1 passed, 1 total
23+
Snapshots: 0 total
24+
Time: <<REPLACED>>
25+
Ran all test suites matching /unhandledRejectionAfterAll.test.js/i.",
26+
}
27+
`;
28+
29+
exports[`fails because of unhandled promise rejection in afterEach hook 1`] = `
30+
Object {
31+
"rest": "FAIL __tests__/unhandledRejectionAfterEach.test.js
32+
foo #1
33+
foo #2
34+
35+
foo #1
36+
37+
REJECTED
38+
39+
11 |
40+
12 | afterEach(async () => {
41+
> 13 | Promise.reject(new Error('REJECTED'));
42+
| ^
43+
14 |
44+
15 | await promisify(setTimeout)(0);
45+
16 | });
46+
47+
at Object.<anonymous> (__tests__/unhandledRejectionAfterEach.test.js:13:18)
48+
49+
● foo #2
50+
51+
REJECTED
52+
53+
11 |
54+
12 | afterEach(async () => {
55+
> 13 | Promise.reject(new Error('REJECTED'));
56+
| ^
57+
14 |
58+
15 | await promisify(setTimeout)(0);
59+
16 | });
60+
61+
at Object.<anonymous> (__tests__/unhandledRejectionAfterEach.test.js:13:18)",
62+
"summary": "Test Suites: 1 failed, 1 total
63+
Tests: 2 failed, 2 total
64+
Snapshots: 0 total
65+
Time: <<REPLACED>>
66+
Ran all test suites matching /unhandledRejectionAfterEach.test.js/i.",
67+
}
68+
`;
69+
70+
exports[`fails because of unhandled promise rejection in beforeAll hook 1`] = `
71+
Object {
72+
"rest": "FAIL __tests__/unhandledRejectionBeforeAll.test.js
73+
foo
74+
75+
foo
76+
77+
REJECTED
78+
79+
11 |
80+
12 | beforeAll(async () => {
81+
> 13 | Promise.reject(new Error('REJECTED'));
82+
| ^
83+
14 |
84+
15 | await promisify(setTimeout)(0);
85+
16 | });
86+
87+
at Object.<anonymous> (__tests__/unhandledRejectionBeforeAll.test.js:13:18)",
88+
"summary": "Test Suites: 1 failed, 1 total
89+
Tests: 1 failed, 1 total
90+
Snapshots: 0 total
91+
Time: <<REPLACED>>
92+
Ran all test suites matching /unhandledRejectionBeforeAll.test.js/i.",
93+
}
94+
`;
95+
96+
exports[`fails because of unhandled promise rejection in beforeEach hook 1`] = `
97+
Object {
98+
"rest": "FAIL __tests__/unhandledRejectionBeforeEach.test.js
99+
foo #1
100+
foo #2
101+
102+
foo #1
103+
104+
REJECTED
105+
106+
11 |
107+
12 | beforeEach(async () => {
108+
> 13 | Promise.reject(new Error('REJECTED'));
109+
| ^
110+
14 |
111+
15 | await promisify(setTimeout)(0);
112+
16 | });
113+
114+
at Object.<anonymous> (__tests__/unhandledRejectionBeforeEach.test.js:13:18)
115+
116+
● foo #2
117+
118+
REJECTED
119+
120+
11 |
121+
12 | beforeEach(async () => {
122+
> 13 | Promise.reject(new Error('REJECTED'));
123+
| ^
124+
14 |
125+
15 | await promisify(setTimeout)(0);
126+
16 | });
127+
128+
at Object.<anonymous> (__tests__/unhandledRejectionBeforeEach.test.js:13:18)",
129+
"summary": "Test Suites: 1 failed, 1 total
130+
Tests: 2 failed, 2 total
131+
Snapshots: 0 total
132+
Time: <<REPLACED>>
133+
Ran all test suites matching /unhandledRejectionBeforeEach.test.js/i.",
134+
}
135+
`;
136+
137+
exports[`fails because of unhandled promise rejection in test 1`] = `
138+
Object {
139+
"rest": "FAIL __tests__/unhandledRejectionTest.test.js
140+
w/o event loop turn after rejection
141+
w/ event loop turn after rejection in async function
142+
✕ w/ event loop turn after rejection in sync function
143+
✕ combined w/ another failure _after_ promise rejection
144+
145+
● w/o event loop turn after rejection
146+
147+
REJECTED
148+
149+
11 |
150+
12 | test('w/o event loop turn after rejection', () => {
151+
> 13 | Promise.reject(new Error('REJECTED'));
152+
| ^
153+
14 | });
154+
15 |
155+
16 | test('w/ event loop turn after rejection in async function', async () => {
156+
157+
at Object.<anonymous> (__tests__/unhandledRejectionTest.test.js:13:18)
158+
159+
● w/ event loop turn after rejection in async function
160+
161+
REJECTED
162+
163+
15 |
164+
16 | test('w/ event loop turn after rejection in async function', async () => {
165+
> 17 | Promise.reject(new Error('REJECTED'));
166+
| ^
167+
18 |
168+
19 | await promisify(setTimeout)(0);
169+
20 | });
170+
171+
at Object.<anonymous> (__tests__/unhandledRejectionTest.test.js:17:18)
172+
173+
● w/ event loop turn after rejection in sync function
174+
175+
REJECTED
176+
177+
21 |
178+
22 | test('w/ event loop turn after rejection in sync function', done => {
179+
> 23 | Promise.reject(new Error('REJECTED'));
180+
| ^
181+
24 |
182+
25 | setTimeout(done, 0);
183+
26 | });
184+
185+
at Object.<anonymous> (__tests__/unhandledRejectionTest.test.js:23:18)
186+
187+
● combined w/ another failure _after_ promise rejection
188+
189+
expect(received).toBe(expected) // Object.is equality
190+
191+
Expected: false
192+
Received: true
193+
194+
31 | await promisify(setTimeout)(0);
195+
32 |
196+
> 33 | expect(true).toBe(false);
197+
| ^
198+
34 | });
199+
35 |
200+
201+
at Object.toBe (__tests__/unhandledRejectionTest.test.js:33:16)
202+
203+
● combined w/ another failure _after_ promise rejection
204+
205+
REJECTED
206+
207+
27 |
208+
28 | test('combined w/ another failure _after_ promise rejection', async () => {
209+
> 29 | Promise.reject(new Error('REJECTED'));
210+
| ^
211+
30 |
212+
31 | await promisify(setTimeout)(0);
213+
32 |
214+
215+
at Object.<anonymous> (__tests__/unhandledRejectionTest.test.js:29:18)",
216+
"summary": "Test Suites: 1 failed, 1 total
217+
Tests: 4 failed, 4 total
218+
Snapshots: 0 total
219+
Time: <<REPLACED>>
220+
Ran all test suites matching /unhandledRejectionTest.test.js/i.",
221+
}
222+
`;
223+
224+
exports[`succeeds for async handled promise rejections 1`] = `
225+
Object {
226+
"rest": "PASS __tests__/rejectionHandled.test.js
227+
async function succeeds because the promise is eventually awaited by assertion
228+
async function succeeds because the promise is eventually directly awaited
229+
✓ sync function succeeds because the promise is eventually handled by \`.catch\` handler",
230+
"summary": "Test Suites: 1 passed, 1 total
231+
Tests: 3 passed, 3 total
232+
Snapshots: 0 total
233+
Time: <<REPLACED>>
234+
Ran all test suites matching /rejectionHandled.test.js/i.",
235+
}
236+
`;

e2e/__tests__/__snapshots__/requireAfterTeardown.test.ts.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`prints useful error for requires after test is done 1`] = `
4-
"ReferenceError: You are trying to \`import\` a file after the Jest environment has been torn down. From __tests__/lateRequire.test.js.
4+
" ReferenceError: You are trying to \`import\` a file outside of the scope of the test code.
55
66
9 | test('require after done', () => {
77
10 | setTimeout(() => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`prints useful error for requires after test is done 1`] = `
4+
"ReferenceError: You are trying to \`import\` a file after the Jest environment has been torn down. From __tests__/lateRequire.test.js.
5+
6+
9 | test('require after done', () => {
7+
10 | setTimeout(() => {
8+
> 11 | const double = require('../');
9+
| ^
10+
12 |
11+
13 | expect(double(5)).toBe(10);
12+
14 | }, 0);"
13+
`;

e2e/__tests__/environmentAfterTeardown.test.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {skipSuiteOnJasmine} from '@jest/test-utils';
89
import runJest from '../runJest';
910

11+
skipSuiteOnJasmine();
12+
1013
test('prints useful error for environment methods after test is done', () => {
1114
const {stderr} = runJest('environment-after-teardown');
12-
const interestingLines = stderr.split('\n').slice(9, 18).join('\n');
15+
const interestingLines = stderr.split('\n').slice(5, 14).join('\n');
1316

1417
expect(interestingLines).toMatchSnapshot();
15-
expect(stderr.split('\n')[9]).toBe(
16-
'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.',
18+
expect(stderr.split('\n')[5]).toMatch(
19+
'ReferenceError: You are trying to access a property or method of the Jest environment outside of the scope of the test code.',
1720
);
1821
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {skipSuiteOnJestCircus} from '@jest/test-utils';
9+
import runJest from '../runJest';
10+
11+
skipSuiteOnJestCircus();
12+
13+
test('prints useful error for environment methods after test is done', () => {
14+
const {stderr} = runJest('environment-after-teardown');
15+
const interestingLines = stderr.split('\n').slice(9, 18).join('\n');
16+
17+
expect(interestingLines).toMatchSnapshot();
18+
expect(stderr.split('\n')[9]).toBe(
19+
'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.',
20+
);
21+
});

e2e/__tests__/fakeTimersLegacy.test.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
import {isJestJasmineRun} from '@jest/test-utils';
89
import runJest from '../runJest';
910

1011
describe('enableGlobally', () => {
@@ -39,10 +40,13 @@ describe('requestAnimationFrame', () => {
3940

4041
describe('setImmediate', () => {
4142
test('fakes setImmediate', () => {
43+
// Jasmine runner does not handle unhandled promise rejections that are causing the test to fail in Jest circus
44+
const expectedExitCode = isJestJasmineRun() ? 0 : 1;
45+
4246
const result = runJest('fake-timers-legacy/set-immediate');
4347

4448
expect(result.stderr).toMatch('setImmediate test');
45-
expect(result.exitCode).toBe(0);
49+
expect(result.exitCode).toBe(expectedExitCode);
4650
});
4751
});
4852

0 commit comments

Comments
 (0)