Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -4,6 +4,7 @@

- `[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))
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))
- `[jest-circus]` Add a `immediately` option to `jest.retryTimes` ([#14696](https://github.com/jestjs/jest/pull/14696))
- `[jest-circus, jest-jasmine2]` Allow `setupFilesAfterEnv` to export an async function ([#10962](https://github.com/jestjs/jest/issues/10962))
- `[jest-config]` [**BREAKING**] Add `mts` and `cts` to default `moduleFileExtensions` config ([#14369](https://github.com/facebook/jest/pull/14369))
- `[jest-config]` [**BREAKING**] Update `testMatch` and `testRegex` default option for supporting `mjs`, `cjs`, `mts`, and `cts` ([#14584](https://github.com/jestjs/jest/pull/14584))
Expand Down
10 changes: 10 additions & 0 deletions docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,16 @@ test('will fail', () => {
});
```

`immediately` option is used to retry the failed test immediately.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just trying to clarify. Feel free to rework.

Suggested change
`immediately` option is used to retry the failed test immediately.
`immediately` 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 test in a file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice point, now it's more clear, I'll update PR.


```js
jest.retryTimes(3, {immediately: true});

test('will fail', () => {
expect(true).toBe(false);
});
```

Returns the `jest` object for chaining.

:::caution
Expand Down
39 changes: 39 additions & 0 deletions e2e/__tests__/__snapshots__/testRetries.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Test Retries immediately retry after failed test 1`] = `
"LOGGING RETRY ERRORS retryTimes set
RETRY 1

expect(received).toBeFalsy()

Received: true

15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 | it('truthy test', () => {

at Object.toBeFalsy (__tests__/immediatelyRetry.test.js:17:18)

RETRY 2

expect(received).toBeFalsy()

Received: true

15 | expect(true).toBeTruthy();
16 | } else {
> 17 | expect(true).toBeFalsy();
| ^
18 | }
19 | });
20 | it('truthy test', () => {

at Object.toBeFalsy (__tests__/immediatelyRetry.test.js:17:18)

PASS __tests__/immediatelyRetry.test.js
✓ retryTimes set
✓ truthy test"
`;

exports[`Test Retries logs error(s) before retry 1`] = `
"LOGGING RETRY ERRORS retryTimes set
RETRY 1
Expand Down
20 changes: 20 additions & 0 deletions e2e/__tests__/testRetries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ describe('Test Retries', () => {
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
});

it('immediately retry after failed test', () => {
const logMessage = `console.log
FIRST TRUTHY TEST

at Object.log (__tests__/immediatelyRetry.test.js:14:13)

console.log
SECOND TRUTHY TEST

at Object.log (__tests__/immediatelyRetry.test.js:21:11)`;

const result = runJest('test-retries', ['immediatelyRetry.test.js']);
const stdout = result.stdout.trim();
expect(result.exitCode).toBe(0);
expect(result.failed).toBe(false);
expect(result.stderr).toContain(logErrorsBeforeRetryErrorMessage);
expect(stdout).toBe(logMessage);
expect(extractSummary(result.stderr).rest).toMatchSnapshot();
});

it('reporter shows more than 1 invocation if test is retried', () => {
let jsonResult;

Expand Down
23 changes: 23 additions & 0 deletions e2e/test-retries/__tests__/immediatelyRetry.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* 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';

jest.retryTimes(3, {immediately: true, logErrorsBeforeRetry: true});
let i = 0;
it('retryTimes set', () => {
i++;
if (i === 3) {
console.log('FIRST TRUTHY TEST');
expect(true).toBeTruthy();
} else {
expect(true).toBeFalsy();
}
});
it('truthy test', () => {
console.log('SECOND TRUTHY TEST');
expect(true).toBeTruthy();
});
51 changes: 35 additions & 16 deletions packages/jest-circus/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import shuffleArray, {
rngBuilder,
} from './shuffleArray';
import {dispatch, getState} from './state';
import {RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
import {IMMEDIATELY, RETRY_TIMES, WAIT_BEFORE_RETRY} from './types';
import {
callAsyncCircusFn,
getAllHooksForDescribe,
Expand Down Expand Up @@ -78,11 +78,32 @@ const _runTestsForDescribeBlock = async (
(global as Global.Global)[WAIT_BEFORE_RETRY] as string,
10,
) || 0;
const retryImmediately: boolean =
// eslint-disable-next-line no-restricted-globals
((global as Global.Global)[IMMEDIATELY] as any) || false;

const deferredRetryTests = [];

if (rng) {
describeBlock.children = shuffleArray(describeBlock.children, rng);
}

const rerunTest = async (test: Circus.TestEntry) => {
let numRetriesAvailable = retryTimes;

while (numRetriesAvailable > 0 && test.errors.length > 0) {
// Clear errors so retries occur
await dispatch({name: 'test_retry', test});

if (waitBeforeRetry > 0) {
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
}

await _runTest(test, isSkipped);
numRetriesAvailable--;
}
};

for (const child of describeBlock.children) {
switch (child.type) {
case 'describeBlock': {
Expand All @@ -91,12 +112,22 @@ const _runTestsForDescribeBlock = async (
}
case 'test': {
const hasErrorsBeforeTestRun = child.errors.length > 0;
const hasRetryTimes = retryTimes > 0;
await _runTest(child, isSkipped);

//If immediate retry is set, we retry the test immediately after the first run
if (
retryImmediately &&
hasErrorsBeforeTestRun === false &&
retryTimes > 0 &&
child.errors.length > 0
hasRetryTimes
) {
await rerunTest(child);
}

if (
hasErrorsBeforeTestRun === false &&
hasRetryTimes &&
!retryImmediately
) {
deferredRetryTests.push(child);
}
Expand All @@ -107,19 +138,7 @@ const _runTestsForDescribeBlock = async (

// Re-run failed tests n-times if configured
for (const test of deferredRetryTests) {
let numRetriesAvailable = retryTimes;

while (numRetriesAvailable > 0 && test.errors.length > 0) {
// Clear errors so retries occur
await dispatch({name: 'test_retry', test});

if (waitBeforeRetry > 0) {
await new Promise(resolve => setTimeout(resolve, waitBeforeRetry));
}

await _runTest(test, isSkipped);
numRetriesAvailable--;
}
await rerunTest(test);
}

if (!isSkipped) {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-circus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

export const STATE_SYM = Symbol('JEST_STATE_SYMBOL');
export const RETRY_TIMES = Symbol.for('RETRY_TIMES');
export const IMMEDIATELY = Symbol.for('IMMEDIATELY');
export const WAIT_BEFORE_RETRY = Symbol.for('WAIT_BEFORE_RETRY');
// To pass this value from Runtime object to state we need to use global[sym]
export const TEST_TIMEOUT_SYMBOL = Symbol.for('TEST_TIMEOUT_SYMBOL');
Expand Down
9 changes: 8 additions & 1 deletion packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,19 @@ export interface Jest {
*
* `waitBeforeRetry` is the number of milliseconds to wait before retrying
*
* `immediately` is the flag to retry the failed tests immediately after
* failure
*
* @remarks
* Only available with `jest-circus` runner.
*/
retryTimes(
numRetries: number,
options?: {logErrorsBeforeRetry?: boolean; waitBeforeRetry?: number},
options?: {
logErrorsBeforeRetry?: boolean;
waitBeforeRetry?: number;
immediately?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to keep alphabetical order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

},
): Jest;
/**
* Exhausts tasks queued by `setImmediate()`.
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type ResolveOptions = Parameters<typeof require.resolve>[1] & {
const testTimeoutSymbol = Symbol.for('TEST_TIMEOUT_SYMBOL');
const retryTimesSymbol = Symbol.for('RETRY_TIMES');
const waitBeforeRetrySymbol = Symbol.for('WAIT_BEFORE_RETRY');
const immediatelySybmbol = Symbol.for('IMMEDIATELY');
const logErrorsBeforeRetrySymbol = Symbol.for('LOG_ERRORS_BEFORE_RETRY');

const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
Expand Down Expand Up @@ -2292,6 +2293,7 @@ export default class Runtime {
options?.logErrorsBeforeRetry;
this._environment.global[waitBeforeRetrySymbol] =
options?.waitBeforeRetry;
this._environment.global[immediatelySybmbol] = options?.immediately;

return jestObject;
};
Expand Down