mirror of https://github.com/facebook/jest.git
Prevent false test failures caused by promise rejections handled asynchronously (#14315)
This commit is contained in:
parent
e7b1e757f7
commit
db42fe5071
|
@ -14,6 +14,7 @@
|
|||
### Fixes
|
||||
|
||||
- `[babel-plugin-jest-hoist]` Use `denylist` instead of the deprecated `blacklist` for Babel 8 support ([#14109](https://github.com/jestjs/jest/pull/14109))
|
||||
- `[jest-circus]` [**BREAKING**] Prevent false test failures caused by promise rejections handled asynchronously ([#14315](https://github.com/jestjs/jest/pull/14315))
|
||||
- `[@jest/expect-utils]` Fix comparison of `DataView` ([#14408](https://github.com/jestjs/jest/pull/14408))
|
||||
- `[jest-leak-detector]` Make leak-detector more aggressive when running GC ([#14526](https://github.com/jestjs/jest/pull/14526))
|
||||
- `[jest-util]` Make sure `isInteractive` works in a browser ([#14552](https://github.com/jestjs/jest/pull/14552))
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 |"
|
||||
`;
|
|
@ -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.",
|
||||
}
|
||||
`;
|
|
@ -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(() => {
|
||||
|
|
|
@ -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);"
|
||||
`;
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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.',
|
||||
);
|
||||
});
|
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -5,15 +5,18 @@
|
|||
* 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 requires after test is done', () => {
|
||||
const {stderr} = runJest('require-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')[19]).toMatch(
|
||||
new RegExp('(__tests__/lateRequire.test.js:11:20)'),
|
||||
expect(stderr.split('\n')[16]).toMatch(
|
||||
'(__tests__/lateRequire.test.js:11:20)',
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 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 requires after test is done', () => {
|
||||
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)',
|
||||
);
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
beforeAll(async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
await expect(promise).rejects.toThrow(/REJECTED/);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
await expect(promise).rejects.toThrow(/REJECTED/);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
await expect(promise).rejects.toThrow(/REJECTED/);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
await expect(promise).rejects.toThrow(/REJECTED/);
|
||||
});
|
||||
|
||||
test('async function succeeds because the promise is eventually awaited by assertion', async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
await expect(promise).rejects.toThrow(/REJECTED/);
|
||||
});
|
||||
|
||||
test('async function succeeds because the promise is eventually directly awaited', async () => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
expect(error).toEqual(new Error('REJECTED'));
|
||||
}
|
||||
});
|
||||
|
||||
test('sync function succeeds because the promise is eventually handled by `.catch` handler', done => {
|
||||
const promise = Promise.reject(new Error('REJECTED'));
|
||||
|
||||
setTimeout(() => {
|
||||
promise
|
||||
.catch(error => {
|
||||
expect(error).toEqual(new Error('REJECTED'));
|
||||
})
|
||||
.finally(done);
|
||||
}, 0);
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
afterAll(async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
});
|
||||
|
||||
test('foo', () => {});
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
afterEach(async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
});
|
||||
|
||||
test('foo #1', () => {});
|
||||
|
||||
test('foo #2', () => {});
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
beforeAll(async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
});
|
||||
|
||||
test('foo', () => {});
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
beforeEach(async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
});
|
||||
|
||||
test('foo #1', () => {});
|
||||
|
||||
test('foo #2', () => {});
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const {promisify} = require('util');
|
||||
|
||||
test('w/o event loop turn after rejection', () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
});
|
||||
|
||||
test('w/ event loop turn after rejection in async function', async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
});
|
||||
|
||||
test('w/ event loop turn after rejection in sync function', done => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
setTimeout(done, 0);
|
||||
});
|
||||
|
||||
test('combined w/ another failure _after_ promise rejection', async () => {
|
||||
Promise.reject(new Error('REJECTED'));
|
||||
|
||||
await promisify(setTimeout)(0);
|
||||
|
||||
expect(true).toBe(false);
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
|
@ -272,9 +272,35 @@ const eventHandler: Circus.EventHandler = (event, state) => {
|
|||
// execution, which will result in one test's error failing another test.
|
||||
// In any way, it should be possible to track where the error was thrown
|
||||
// from.
|
||||
state.currentlyRunningTest
|
||||
? state.currentlyRunningTest.errors.push(event.error)
|
||||
: state.unhandledErrors.push(event.error);
|
||||
if (state.currentlyRunningTest) {
|
||||
if (event.promise) {
|
||||
state.currentlyRunningTest.unhandledRejectionErrorByPromise.set(
|
||||
event.promise,
|
||||
event.error,
|
||||
);
|
||||
} else {
|
||||
state.currentlyRunningTest.errors.push(event.error);
|
||||
}
|
||||
} else {
|
||||
if (event.promise) {
|
||||
state.unhandledRejectionErrorByPromise.set(
|
||||
event.promise,
|
||||
event.error,
|
||||
);
|
||||
} else {
|
||||
state.unhandledErrors.push(event.error);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'error_handled': {
|
||||
if (state.currentlyRunningTest) {
|
||||
state.currentlyRunningTest.unhandledRejectionErrorByPromise.delete(
|
||||
event.promise,
|
||||
);
|
||||
} else {
|
||||
state.unhandledRejectionErrorByPromise.delete(event.promise);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,29 +9,50 @@ import type * as Process from 'process';
|
|||
import type {Circus} from '@jest/types';
|
||||
import {dispatchSync} from './state';
|
||||
|
||||
const uncaught: NodeJS.UncaughtExceptionListener &
|
||||
NodeJS.UnhandledRejectionListener = (error: unknown) => {
|
||||
const uncaughtExceptionListener: NodeJS.UncaughtExceptionListener = (
|
||||
error: unknown,
|
||||
) => {
|
||||
dispatchSync({error, name: 'error'});
|
||||
};
|
||||
|
||||
const unhandledRejectionListener: NodeJS.UnhandledRejectionListener = (
|
||||
error: unknown,
|
||||
promise: Promise<unknown>,
|
||||
) => {
|
||||
dispatchSync({error, name: 'error', promise});
|
||||
};
|
||||
|
||||
const rejectionHandledListener: NodeJS.RejectionHandledListener = (
|
||||
promise: Promise<unknown>,
|
||||
) => {
|
||||
dispatchSync({name: 'error_handled', promise});
|
||||
};
|
||||
|
||||
export const injectGlobalErrorHandlers = (
|
||||
parentProcess: typeof Process,
|
||||
): Circus.GlobalErrorHandlers => {
|
||||
const uncaughtException = process.listeners('uncaughtException').slice();
|
||||
const unhandledRejection = process.listeners('unhandledRejection').slice();
|
||||
const rejectionHandled = process.listeners('rejectionHandled').slice();
|
||||
parentProcess.removeAllListeners('uncaughtException');
|
||||
parentProcess.removeAllListeners('unhandledRejection');
|
||||
parentProcess.on('uncaughtException', uncaught);
|
||||
parentProcess.on('unhandledRejection', uncaught);
|
||||
return {uncaughtException, unhandledRejection};
|
||||
parentProcess.removeAllListeners('rejectionHandled');
|
||||
parentProcess.on('uncaughtException', uncaughtExceptionListener);
|
||||
parentProcess.on('unhandledRejection', unhandledRejectionListener);
|
||||
parentProcess.on('rejectionHandled', rejectionHandledListener);
|
||||
return {rejectionHandled, uncaughtException, unhandledRejection};
|
||||
};
|
||||
|
||||
export const restoreGlobalErrorHandlers = (
|
||||
parentProcess: typeof Process,
|
||||
originalErrorHandlers: Circus.GlobalErrorHandlers,
|
||||
): void => {
|
||||
parentProcess.removeListener('uncaughtException', uncaught);
|
||||
parentProcess.removeListener('unhandledRejection', uncaught);
|
||||
parentProcess.removeListener('uncaughtException', uncaughtExceptionListener);
|
||||
parentProcess.removeListener(
|
||||
'unhandledRejection',
|
||||
unhandledRejectionListener,
|
||||
);
|
||||
parentProcess.removeListener('rejectionHandled', rejectionHandledListener);
|
||||
|
||||
for (const listener of originalErrorHandlers.uncaughtException) {
|
||||
parentProcess.on('uncaughtException', listener);
|
||||
|
@ -39,4 +60,7 @@ export const restoreGlobalErrorHandlers = (
|
|||
for (const listener of originalErrorHandlers.unhandledRejection) {
|
||||
parentProcess.on('unhandledRejection', listener);
|
||||
}
|
||||
for (const listener of originalErrorHandlers.rejectionHandled) {
|
||||
parentProcess.on('rejectionHandled', listener);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ const jestAdapter = async (
|
|||
globalConfig,
|
||||
localRequire: runtime.requireModule.bind(runtime),
|
||||
parentProcess: process,
|
||||
runtime,
|
||||
sendMessageToJest,
|
||||
setGlobalsForRuntime: runtime.setGlobalsForRuntime.bind(runtime),
|
||||
testPath,
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from '@jest/test-result';
|
||||
import type {Circus, Config, Global} from '@jest/types';
|
||||
import {formatExecError, formatResultsErrors} from 'jest-message-util';
|
||||
import type Runtime from 'jest-runtime';
|
||||
import {
|
||||
SnapshotState,
|
||||
addSerializer,
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
getState as getRunnerState,
|
||||
} from '../state';
|
||||
import testCaseReportHandler from '../testCaseReportHandler';
|
||||
import {unhandledRejectionHandler} from '../unhandledRejectionHandler';
|
||||
import {getTestID} from '../utils';
|
||||
|
||||
interface RuntimeGlobals extends Global.TestFrameworkGlobals {
|
||||
|
@ -40,6 +42,7 @@ interface RuntimeGlobals extends Global.TestFrameworkGlobals {
|
|||
export const initialize = async ({
|
||||
config,
|
||||
environment,
|
||||
runtime,
|
||||
globalConfig,
|
||||
localRequire,
|
||||
parentProcess,
|
||||
|
@ -49,6 +52,7 @@ export const initialize = async ({
|
|||
}: {
|
||||
config: Config.ProjectConfig;
|
||||
environment: JestEnvironment;
|
||||
runtime: Runtime;
|
||||
globalConfig: Config.GlobalConfig;
|
||||
localRequire: <T = unknown>(path: string) => T;
|
||||
testPath: string;
|
||||
|
@ -128,6 +132,8 @@ export const initialize = async ({
|
|||
addEventHandler(testCaseReportHandler(testPath, sendMessageToJest));
|
||||
}
|
||||
|
||||
addEventHandler(unhandledRejectionHandler(runtime));
|
||||
|
||||
// Return it back to the outer scope (test runner outside the VM).
|
||||
return {globals: globalsObject, snapshotState};
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ const createState = (): Circus.State => {
|
|||
testNamePattern: null,
|
||||
testTimeout: 5000,
|
||||
unhandledErrors: [],
|
||||
unhandledRejectionErrorByPromise: new Map(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 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 type {Circus} from '@jest/types';
|
||||
import type Runtime from 'jest-runtime';
|
||||
import {invariant} from 'jest-util';
|
||||
import {addErrorToEachTestUnderDescribe} from './utils';
|
||||
|
||||
// Global values can be overwritten by mocks or tests. We'll capture
|
||||
// the original values in the variables before we require any files.
|
||||
const {setTimeout} = globalThis;
|
||||
|
||||
const untilNextEventLoopTurn = async () => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
};
|
||||
|
||||
export const unhandledRejectionHandler = (
|
||||
runtime: Runtime,
|
||||
): 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();
|
||||
|
||||
const {test, describeBlock, hook} = event;
|
||||
const {asyncError, type} = hook;
|
||||
|
||||
if (type === 'beforeAll') {
|
||||
invariant(describeBlock, 'always present for `*All` hooks');
|
||||
for (const error of state.unhandledRejectionErrorByPromise.values()) {
|
||||
addErrorToEachTestUnderDescribe(describeBlock, error, asyncError);
|
||||
}
|
||||
} else if (type === 'afterAll') {
|
||||
// Attaching `afterAll` errors to each test makes execution flow
|
||||
// too complicated, so we'll consider them to be global.
|
||||
for (const error of state.unhandledRejectionErrorByPromise.values()) {
|
||||
state.unhandledErrors.push([error, asyncError]);
|
||||
}
|
||||
} else {
|
||||
invariant(test, 'always present for `*Each` hooks');
|
||||
for (const error of test.unhandledRejectionErrorByPromise.values()) {
|
||||
test.errors.push([error, asyncError]);
|
||||
}
|
||||
}
|
||||
} else if (event.name === 'test_fn_start') {
|
||||
runtime.enterTestCode();
|
||||
} else if (
|
||||
event.name === 'test_fn_success' ||
|
||||
event.name === 'test_fn_failure'
|
||||
) {
|
||||
runtime.leaveTestCode();
|
||||
|
||||
// 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');
|
||||
|
||||
for (const error of test.unhandledRejectionErrorByPromise.values()) {
|
||||
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();
|
||||
|
||||
state.unhandledErrors.push(
|
||||
...state.unhandledRejectionErrorByPromise.values(),
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -86,6 +86,7 @@ export const makeTest = (
|
|||
startedAt: null,
|
||||
status: null,
|
||||
timeout,
|
||||
unhandledRejectionErrorByPromise: new Map(),
|
||||
});
|
||||
|
||||
// Traverse the tree of describe blocks and return true if at least one describe
|
||||
|
|
|
@ -213,6 +213,7 @@ export default class Runtime {
|
|||
private readonly esmConditions: Array<string>;
|
||||
private readonly cjsConditions: Array<string>;
|
||||
private isTornDown = false;
|
||||
private isInsideTestCode: boolean | undefined;
|
||||
|
||||
constructor(
|
||||
config: Config.ProjectConfig,
|
||||
|
@ -570,6 +571,11 @@ export default class Runtime {
|
|||
// @ts-expect-error - exiting
|
||||
return;
|
||||
}
|
||||
if (this.isInsideTestCode === false) {
|
||||
throw new ReferenceError(
|
||||
'You are trying to `import` a file outside of the scope of the test code.',
|
||||
);
|
||||
}
|
||||
|
||||
const registry = this._isolatedModuleRegistry
|
||||
? this._isolatedModuleRegistry
|
||||
|
@ -718,6 +724,11 @@ export default class Runtime {
|
|||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (this.isInsideTestCode === false) {
|
||||
throw new ReferenceError(
|
||||
'You are trying to `import` a file outside of the scope of the test code.',
|
||||
);
|
||||
}
|
||||
|
||||
if (module.status === 'unlinked') {
|
||||
// since we might attempt to link the same module in parallel, stick the promise in a weak map so every call to
|
||||
|
@ -1355,6 +1366,14 @@ export default class Runtime {
|
|||
this._moduleMocker.clearAllMocks();
|
||||
}
|
||||
|
||||
enterTestCode(): void {
|
||||
this.isInsideTestCode = true;
|
||||
}
|
||||
|
||||
leaveTestCode(): void {
|
||||
this.isInsideTestCode = false;
|
||||
}
|
||||
|
||||
teardown(): void {
|
||||
this.restoreAllMocks();
|
||||
this.resetModules();
|
||||
|
@ -1498,6 +1517,11 @@ export default class Runtime {
|
|||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (this.isInsideTestCode === false) {
|
||||
throw new ReferenceError(
|
||||
'You are trying to `import` a file outside of the scope of the test code.',
|
||||
);
|
||||
}
|
||||
|
||||
// If the environment was disposed, prevent this module from being executed.
|
||||
if (!this._environment.global) {
|
||||
|
@ -2166,6 +2190,11 @@ export default class Runtime {
|
|||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (this.isInsideTestCode === false) {
|
||||
throw new ReferenceError(
|
||||
'You are trying to access a property or method of the Jest environment outside of the scope of the test code.',
|
||||
);
|
||||
}
|
||||
|
||||
return this._fakeTimersImplementation!;
|
||||
};
|
||||
|
|
|
@ -82,6 +82,11 @@ export type SyncEvent =
|
|||
// an `afterAll` hook)
|
||||
name: 'error';
|
||||
error: Exception;
|
||||
promise?: Promise<unknown>;
|
||||
}
|
||||
| {
|
||||
name: 'error_handled';
|
||||
promise: Promise<unknown>;
|
||||
};
|
||||
|
||||
export type AsyncEvent =
|
||||
|
@ -214,6 +219,7 @@ export type RunResult = {
|
|||
export type TestResults = Array<TestResult>;
|
||||
|
||||
export type GlobalErrorHandlers = {
|
||||
rejectionHandled: Array<(promise: Promise<unknown>) => void>;
|
||||
uncaughtException: Array<NodeJS.UncaughtExceptionListener>;
|
||||
unhandledRejection: Array<NodeJS.UnhandledRejectionListener>;
|
||||
};
|
||||
|
@ -237,6 +243,7 @@ export type State = {
|
|||
unhandledErrors: Array<Exception>;
|
||||
includeTestLocationInResult: boolean;
|
||||
maxConcurrency: number;
|
||||
unhandledRejectionErrorByPromise: Map<Promise<unknown>, Exception>;
|
||||
};
|
||||
|
||||
export type DescribeBlock = {
|
||||
|
@ -270,4 +277,5 @@ export type TestEntry = {
|
|||
status?: TestStatus | null; // whether the test has been skipped or run already
|
||||
timeout?: number;
|
||||
failing: boolean;
|
||||
unhandledRejectionErrorByPromise: Map<Promise<unknown>, Exception>;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue