Skip to content

Commit 222565a

Browse files
authored
feat(circus): enable writing async test event handlers (#9397)
1 parent 44a960d commit 222565a

File tree

16 files changed

+219
-60
lines changed

16 files changed

+219
-60
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- `[babel-jest]` Support passing `supportsDynamicImport` and `supportsStaticESM` ([#9766](https://github.com/facebook/jest/pull/9766))
66
- `[babel-preset-jest]` Enable all syntax plugins not enabled by default that works on current version of Node ([#9774](https://github.com/facebook/jest/pull/9774))
7+
- `[jest-circus]` Enable writing async test event handlers ([#9392](https://github.com/facebook/jest/pull/9392))
78
- `[jest-runtime, @jest/transformer]` Support passing `supportsDynamicImport` and `supportsStaticESM` ([#9597](https://github.com/facebook/jest/pull/9597))
89

910
### Fixes

docs/Configuration.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ test('use jsdom in this test file', () => {
900900

901901
You can create your own module that will be used for setting up the test environment. The module must export a class with `setup`, `teardown` and `runScript` methods. You can also pass variables from this module to your test suites by assigning them to `this.global` object – this will make them available in your test suites as global variables.
902902

903-
The class may optionally expose a `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/facebook/jest/tree/master/packages/jest-circus).
903+
The class may optionally expose an asynchronous `handleTestEvent` method to bind to events fired by [`jest-circus`](https://github.com/facebook/jest/tree/master/packages/jest-circus). Normally, `jest-circus` test runner would pause until a promise returned from `handleTestEvent` gets fulfilled, **except for the next events**: `start_describe_definition`, `finish_describe_definition`, `add_hook`, `add_test` or `error` (for the up-to-date list you can look at [SyncEvent type in the types definitions](https://github.com/facebook/jest/tree/master/packages/jest-types/src/Circus.ts)). That is caused by backward compatibility reasons and `process.on('unhandledRejection', callback)` signature, but that usually should not be a problem for most of the use cases.
904904

905905
Any docblock pragmas in test files will be passed to the environment constructor and can be used for per-test configuration. If the pragma does not have a value, it will be present in the object with it's value set to an empty string. If the pragma is not present, it will not be present in the object.
906906

@@ -940,7 +940,7 @@ class CustomEnvironment extends NodeEnvironment {
940940
return super.runScript(script);
941941
}
942942

943-
handleTestEvent(event, state) {
943+
async handleTestEvent(event, state) {
944944
if (event.name === 'test_start') {
945945
// ...
946946
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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 {skipSuiteOnJasmine} from '@jest/test-utils';
9+
import runJest from '../runJest';
10+
11+
skipSuiteOnJasmine();
12+
13+
it('calls asynchronous handleTestEvent in testEnvironment', () => {
14+
const result = runJest('test-environment-circus-async');
15+
expect(result.failed).toEqual(true);
16+
17+
const lines = result.stdout.split('\n');
18+
expect(lines).toMatchInlineSnapshot(`
19+
Array [
20+
"setup",
21+
"warning: add_hook is a sync event",
22+
"warning: start_describe_definition is a sync event",
23+
"warning: add_hook is a sync event",
24+
"warning: add_hook is a sync event",
25+
"warning: add_test is a sync event",
26+
"warning: add_test is a sync event",
27+
"warning: finish_describe_definition is a sync event",
28+
"add_hook",
29+
"start_describe_definition",
30+
"add_hook",
31+
"add_hook",
32+
"add_test",
33+
"add_test",
34+
"finish_describe_definition",
35+
"run_start",
36+
"run_describe_start",
37+
"run_describe_start",
38+
"test_start: passing test",
39+
"hook_start: beforeEach",
40+
"hook_success: beforeEach",
41+
"hook_start: beforeEach",
42+
"hook_success: beforeEach",
43+
"test_fn_start: passing test",
44+
"test_fn_success: passing test",
45+
"hook_start: afterEach",
46+
"hook_failure: afterEach",
47+
"test_done: passing test",
48+
"test_start: failing test",
49+
"hook_start: beforeEach",
50+
"hook_success: beforeEach",
51+
"hook_start: beforeEach",
52+
"hook_success: beforeEach",
53+
"test_fn_start: failing test",
54+
"test_fn_failure: failing test",
55+
"hook_start: afterEach",
56+
"hook_failure: afterEach",
57+
"test_done: failing test",
58+
"run_describe_finish",
59+
"run_describe_finish",
60+
"run_finish",
61+
"teardown",
62+
]
63+
`);
64+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
'use strict';
9+
10+
const JSDOMEnvironment = require('jest-environment-jsdom');
11+
12+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
13+
14+
class TestEnvironment extends JSDOMEnvironment {
15+
async handleTestEvent(event) {
16+
await this.assertRunnerWaitsForHandleTestEvent(event);
17+
18+
if (event.hook) {
19+
console.log(event.name + ': ' + event.hook.type);
20+
} else if (event.test) {
21+
console.log(event.name + ': ' + event.test.name);
22+
} else {
23+
console.log(event.name);
24+
}
25+
}
26+
27+
async assertRunnerWaitsForHandleTestEvent(event) {
28+
if (this.pendingEvent) {
29+
console.log(`warning: ${this.pendingEvent.name} is a sync event`);
30+
}
31+
32+
this.pendingEvent = event;
33+
await sleep(0);
34+
this.pendingEvent = null;
35+
}
36+
}
37+
38+
module.exports = TestEnvironment;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
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+
* @jest-environment ./CircusAsyncHandleTestEventEnvironment.js
8+
*/
9+
10+
describe('suite', () => {
11+
beforeEach(() => {});
12+
afterEach(() => {
13+
throw new Error();
14+
});
15+
16+
test('passing test', () => {
17+
expect(true).toBe(true);
18+
});
19+
20+
test('failing test', () => {
21+
expect(true).toBe(false);
22+
});
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

packages/jest-circus/README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {Event, State} from 'jest-circus';
1818
class MyCustomEnvironment extends NodeEnvironment {
1919
//...
2020

21-
handleTestEvent(event: Event, state: State) {
21+
async handleTestEvent(event: Event, state: State) {
2222
if (event.name === 'test_start') {
2323
// ...
2424
}
@@ -28,6 +28,8 @@ class MyCustomEnvironment extends NodeEnvironment {
2828

2929
Mutating event or state data is currently unsupported and may cause unexpected behavior or break in a future release without warning. New events, event data, and/or state data will not be considered a breaking change and may be added in any minor release.
3030

31+
Note, that `jest-circus` test runner would pause until a promise returned from `handleTestEvent` gets fulfilled. **However, there are a few events that do not conform to this rule, namely**: `start_describe_definition`, `finish_describe_definition`, `add_hook`, `add_test` or `error` (for the up-to-date list you can look at [SyncEvent type in the types definitions](https://github.com/facebook/jest/tree/master/packages/jest-types/src/Circus.ts)). That is caused by backward compatibility reasons and `process.on('unhandledRejection', callback)` signature, but that usually should not be a problem for most of the use cases.
32+
3133
## Installation
3234

3335
Install `jest-circus` using yarn:

packages/jest-circus/src/eventHandler.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {
2121
restoreGlobalErrorHandlers,
2222
} from './globalErrorHandlers';
2323

24-
const eventHandler: Circus.EventHandler = (event, state): void => {
24+
// TODO: investigate why a shorter (event, state) signature results into TS7006 compiler error
25+
const eventHandler: Circus.EventHandler = (
26+
event: Circus.Event,
27+
state: Circus.State,
28+
): void => {
2529
switch (event.name) {
2630
case 'include_test_location_in_result': {
2731
state.includeTestLocationInResult = true;

packages/jest-circus/src/globalErrorHandlers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
*/
77

88
import type {Circus} from '@jest/types';
9-
import {dispatch} from './state';
9+
import {dispatchSync} from './state';
1010

1111
const uncaught: NodeJS.UncaughtExceptionListener &
1212
NodeJS.UnhandledRejectionListener = (error: unknown) => {
13-
dispatch({error, name: 'error'});
13+
dispatchSync({error, name: 'error'});
1414
};
1515

1616
export const injectGlobalErrorHandlers = (

packages/jest-circus/src/index.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {bind as bindEach} from 'jest-each';
1010
import {formatExecError} from 'jest-message-util';
1111
import {ErrorWithStack, isPromise} from 'jest-util';
1212
import type {Circus, Global} from '@jest/types';
13-
import {dispatch} from './state';
13+
import {dispatchSync} from './state';
1414

1515
type THook = (fn: Circus.HookFn, timeout?: number) => void;
1616
type DescribeFn = (
@@ -52,7 +52,7 @@ const _dispatchDescribe = (
5252
asyncError.message = `Invalid second argument, ${blockFn}. It must be a callback function.`;
5353
throw asyncError;
5454
}
55-
dispatch({
55+
dispatchSync({
5656
asyncError,
5757
blockName,
5858
mode,
@@ -91,7 +91,7 @@ const _dispatchDescribe = (
9191
);
9292
}
9393

94-
dispatch({blockName, mode, name: 'finish_describe_definition'});
94+
dispatchSync({blockName, mode, name: 'finish_describe_definition'});
9595
};
9696

9797
const _addHook = (
@@ -109,7 +109,7 @@ const _addHook = (
109109
throw asyncError;
110110
}
111111

112-
dispatch({asyncError, fn, hookType, name: 'add_hook', timeout});
112+
dispatchSync({asyncError, fn, hookType, name: 'add_hook', timeout});
113113
};
114114

115115
// Hooks have to pass themselves to the HOF in order for us to trim stack traces.
@@ -179,7 +179,7 @@ const test: Global.It = (() => {
179179
throw asyncError;
180180
}
181181

182-
return dispatch({
182+
return dispatchSync({
183183
asyncError,
184184
fn,
185185
mode,

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const jestAdapter = async (
3838
config.prettierPath ? require(config.prettierPath) : null;
3939
const getBabelTraverse = () => require('@babel/traverse').default;
4040

41-
const {globals, snapshotState} = initialize({
41+
const {globals, snapshotState} = await initialize({
4242
config,
4343
environment,
4444
getBabelTraverse,

packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type Process = NodeJS.Process;
3636

3737
// TODO: hard to type
3838
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
39-
export const initialize = ({
39+
export const initialize = async ({
4040
config,
4141
environment,
4242
getPrettier,
@@ -107,14 +107,14 @@ export const initialize = ({
107107
addEventHandler(environment.handleTestEvent.bind(environment));
108108
}
109109

110-
dispatch({
110+
await dispatch({
111111
name: 'setup',
112112
parentProcess,
113113
testNamePattern: globalConfig.testNamePattern,
114114
});
115115

116116
if (config.testLocationInResults) {
117-
dispatch({
117+
await dispatch({
118118
name: 'include_test_location_in_result',
119119
});
120120
}
@@ -220,7 +220,8 @@ export const runAndTransformResultsToJestFormat = async ({
220220
.join('\n');
221221
}
222222

223-
dispatch({name: 'teardown'});
223+
await dispatch({name: 'teardown'});
224+
224225
return {
225226
...createEmptyTestResult(),
226227
console: undefined,
@@ -248,7 +249,7 @@ const handleSnapshotStateAfterRetry = (snapshotState: SnapshotStateType) => (
248249
}
249250
};
250251

251-
const eventHandler = (event: Circus.Event) => {
252+
const eventHandler = async (event: Circus.Event) => {
252253
switch (event.name) {
253254
case 'test_start': {
254255
setState({currentTestName: getTestID(event.test)});

0 commit comments

Comments
 (0)