Skip to content

Commit 1d682f2

Browse files
authored
feat: add retryImmediately option to jest.retryTimes (#14977)
1 parent 366e8fb commit 1d682f2

File tree

10 files changed

+146
-17
lines changed

10 files changed

+146
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
66
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))
7+
- `[jest-circus]` Add a `retryImmediately` option to `jest.retryTimes` ([#14696](https://github.com/jestjs/jest/pull/14696))
78
- `[jest-circus, jest-jasmine2]` Allow `setupFilesAfterEnv` to export an async function ([#10962](https://github.com/jestjs/jest/issues/10962))
89
- `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369))
910
- `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584))

docs/JestObjectAPI.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,16 @@ test('will fail', () => {
11391139
});
11401140
```
11411141

1142+
`retryImmediately` option is used to retry the failed test immediately after the failure. If this option is not specified, the tests are retried after Jest is finished running all other tests in the file.
1143+
1144+
```js
1145+
jest.retryTimes(3, {retryImmediately: true});
1146+
1147+
test('will fail', () => {
1148+
expect(true).toBe(false);
1149+
});
1150+
```
1151+
11421152
Returns the `jest` object for chaining.
11431153

11441154
:::caution

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,42 @@ exports[`Test Retries wait before retry with fake timers 1`] = `
113113
PASS __tests__/waitBeforeRetryFakeTimers.test.js
114114
✓ retryTimes set with fake timers"
115115
`;
116+
117+
exports[`Test Retries with flag retryImmediately retry immediately after failed test 1`] = `
118+
"LOGGING RETRY ERRORS retryTimes set
119+
RETRY 1
120+
121+
expect(received).toBeFalsy()
122+
123+
Received: true
124+
125+
15 | expect(true).toBeTruthy();
126+
16 | } else {
127+
> 17 | expect(true).toBeFalsy();
128+
| ^
129+
18 | }
130+
19 | });
131+
20 | it('truthy test', () => {
132+
133+
at Object.toBeFalsy (__tests__/retryImmediately.test.js:17:18)
134+
135+
RETRY 2
136+
137+
expect(received).toBeFalsy()
138+
139+
Received: true
140+
141+
15 | expect(true).toBeTruthy();
142+
16 | } else {
143+
> 17 | expect(true).toBeFalsy();
144+
| ^
145+
18 | }
146+
19 | });
147+
20 | it('truthy test', () => {
148+
149+
at Object.toBeFalsy (__tests__/retryImmediately.test.js:17:18)
150+
151+
PASS __tests__/retryImmediately.test.js
152+
retryTimes set
153+
truthy test"
154+
`;

e2e/__tests__/testRetries.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,26 @@ describe('Test Retries', () => {
6060
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
6161
});
6262

63+
it('with flag retryImmediately retry immediately after failed test', () => {
64+
const logMessage = `console.log
65+
FIRST TRUTHY TEST
66+
67+
at Object.log (__tests__/retryImmediately.test.js:14:13)
68+
69+
console.log
70+
SECOND TRUTHY TEST
71+
72+
at Object.log (__tests__/retryImmediately.test.js:21:11)`;
73+
74+
const result = runJest('test-retries', ['retryImmediately.test.js']);
75+
const stdout = result.stdout.trim();
76+
expect(result.exitCode).toBe(0);
77+
expect(result.failed).toBe(false);
78+
expect(result.stderr).toContain(logErrorsBeforeRetryErrorMessage);
79+
expect(stdout).toBe(logMessage);
80+
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
81+
});
82+
6383
it('reporter shows more than 1 invocation if test is retried', () => {
6484
let jsonResult;
6585

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
'use strict';
8+
9+
jest.retryTimes(3, {logErrorsBeforeRetry: true, retryImmediately: true});
10+
let i = 0;
11+
it('retryTimes set', () => {
12+
i++;
13+
if (i === 3) {
14+
console.log('FIRST TRUTHY TEST');
15+
expect(true).toBeTruthy();
16+
} else {
17+
expect(true).toBeFalsy();
18+
}
19+
});
20+
it('truthy test', () => {
21+
console.log('SECOND TRUTHY TEST');
22+
expect(true).toBeTruthy();
23+
});

packages/jest-circus/src/run.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import shuffleArray, {
1515
rngBuilder,
1616
} from './shuffleArray';
1717
import {dispatch, getState} from './state';
18-
import {RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
18+
import {RETRY_IMMEDIATELY, RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
1919
import {
2020
callAsyncCircusFn,
2121
getAllHooksForDescribe,
@@ -78,11 +78,33 @@ const _runTestsForDescribeBlock = async (
7878
(global as Global.Global)[WAIT_BEFORE_RETRY] as string,
7979
10,
8080
) || 0;
81+
82+
const retryImmediately: boolean =
83+
// eslint-disable-next-line no-restricted-globals
84+
((global as Global.Global)[RETRY_IMMEDIATELY] as any) || false;
85+
8186
const deferredRetryTests = [];
8287

8388
if (rng) {
8489
describeBlock.children = shuffleArray(describeBlock.children, rng);
8590
}
91+
92+
const rerunTest = async (test: Circus.TestEntry) => {
93+
let numRetriesAvailable = retryTimes;
94+
95+
while (numRetriesAvailable > 0 && test.errors.length > 0) {
96+
// Clear errors so retries occur
97+
await dispatch({name: 'test_retry', test});
98+
99+
if (waitBeforeRetry > 0) {
100+
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
101+
}
102+
103+
await _runTest(test, isSkipped);
104+
numRetriesAvailable--;
105+
}
106+
};
107+
86108
for (const child of describeBlock.children) {
87109
switch (child.type) {
88110
case 'describeBlock': {
@@ -91,12 +113,22 @@ const _runTestsForDescribeBlock = async (
91113
}
92114
case 'test': {
93115
const hasErrorsBeforeTestRun = child.errors.length > 0;
116+
const hasRetryTimes = retryTimes > 0;
94117
await _runTest(child, isSkipped);
95118

119+
// If immediate retry is set, we retry the test immediately after the first run
96120
if (
121+
retryImmediately &&
97122
hasErrorsBeforeTestRun === false &&
98-
retryTimes > 0 &&
99-
child.errors.length > 0
123+
hasRetryTimes
124+
) {
125+
await rerunTest(child);
126+
}
127+
128+
if (
129+
hasErrorsBeforeTestRun === false &&
130+
hasRetryTimes &&
131+
!retryImmediately
100132
) {
101133
deferredRetryTests.push(child);
102134
}
@@ -107,19 +139,7 @@ const _runTestsForDescribeBlock = async (
107139

108140
// Re-run failed tests n-times if configured
109141
for (const test of deferredRetryTests) {
110-
let numRetriesAvailable = retryTimes;
111-
112-
while (numRetriesAvailable > 0 && test.errors.length > 0) {
113-
// Clear errors so retries occur
114-
await dispatch({name: 'test_retry', test});
115-
116-
if (waitBeforeRetry > 0) {
117-
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
118-
}
119-
120-
await _runTest(test, isSkipped);
121-
numRetriesAvailable--;
122-
}
142+
await rerunTest(test);
123143
}
124144

125145
if (!isSkipped) {

packages/jest-circus/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
export const STATE_SYM = Symbol('JEST_STATE_SYMBOL');
99
export const RETRY_TIMES = Symbol.for('RETRY_TIMES');
10+
export const RETRY_IMMEDIATELY = Symbol.for('RETRY_IMMEDIATELY');
1011
export const WAIT_BEFORE_RETRY = Symbol.for('WAIT_BEFORE_RETRY');
1112
// To pass this value from Runtime object to state we need to use global[sym]
1213
export const TEST_TIMEOUT_SYMBOL = Symbol.for('TEST_TIMEOUT_SYMBOL');

packages/jest-environment/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,12 +300,19 @@ export interface Jest {
300300
*
301301
* `waitBeforeRetry` is the number of milliseconds to wait before retrying
302302
*
303+
* `retryImmediately` is the flag to retry the failed test immediately after
304+
* failure
305+
*
303306
* @remarks
304307
* Only available with `jest-circus` runner.
305308
*/
306309
retryTimes(
307310
numRetries: number,
308-
options?: {logErrorsBeforeRetry?: boolean; waitBeforeRetry?: number},
311+
options?: {
312+
logErrorsBeforeRetry?: boolean;
313+
retryImmediately?: boolean;
314+
waitBeforeRetry?: number;
315+
},
309316
): Jest;
310317
/**
311318
* Exhausts tasks queued by `setImmediate()`.

packages/jest-runtime/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ type ResolveOptions = Parameters<typeof require.resolve>[1] & {
123123
const testTimeoutSymbol = Symbol.for('TEST_TIMEOUT_SYMBOL');
124124
const retryTimesSymbol = Symbol.for('RETRY_TIMES');
125125
const waitBeforeRetrySymbol = Symbol.for('WAIT_BEFORE_RETRY');
126+
const retryImmediatelySybmbol = Symbol.for('RETRY_IMMEDIATELY');
126127
const logErrorsBeforeRetrySymbol = Symbol.for('LOG_ERRORS_BEFORE_RETRY');
127128

128129
const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
@@ -2292,6 +2293,8 @@ export default class Runtime {
22922293
options?.logErrorsBeforeRetry;
22932294
this._environment.global[waitBeforeRetrySymbol] =
22942295
options?.waitBeforeRetry;
2296+
this._environment.global[retryImmediatelySybmbol] =
2297+
options?.retryImmediately;
22952298

22962299
return jestObject;
22972300
};

packages/jest-types/__typetests__/jest.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,11 @@ expect(jest.retryTimes(3, {logErrorsBeforeRetry: 'all'})).type.toRaiseError();
667667
expect(jest.retryTimes({logErrorsBeforeRetry: true})).type.toRaiseError();
668668
expect(jest.retryTimes(3, {waitBeforeRetry: 1000})).type.toEqual<typeof jest>();
669669
expect(jest.retryTimes(3, {waitBeforeRetry: true})).type.toRaiseError();
670+
expect(jest.retryTimes(3, {retryImmediately: true})).type.toEqual<
671+
typeof jest
672+
>();
673+
expect(jest.retryTimes(3, {retryImmediately: 'now'})).type.toRaiseError();
674+
expect(jest.retryTimes(3, {retryImmediately: 1000})).type.toRaiseError();
670675
expect(jest.retryTimes({logErrorsBeforeRetry: 'all'})).type.toRaiseError();
671676
expect(jest.retryTimes()).type.toRaiseError();
672677

0 commit comments

Comments
 (0)