diff --git a/CHANGELOG.md b/CHANGELOG.md index 179398c50251..0bc7e98bf629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `retryImmediately` 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)) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index 82007ee41703..8b0e6ece428f 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -1139,6 +1139,16 @@ test('will fail', () => { }); ``` +`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. + +```js +jest.retryTimes(3, {retryImmediately: true}); + +test('will fail', () => { + expect(true).toBe(false); +}); +``` + Returns the `jest` object for chaining. :::caution diff --git a/e2e/__tests__/__snapshots__/testRetries.test.ts.snap b/e2e/__tests__/__snapshots__/testRetries.test.ts.snap index 3317a40cbceb..ff71c0d297a7 100644 --- a/e2e/__tests__/__snapshots__/testRetries.test.ts.snap +++ b/e2e/__tests__/__snapshots__/testRetries.test.ts.snap @@ -113,3 +113,42 @@ exports[`Test Retries wait before retry with fake timers 1`] = ` PASS __tests__/waitBeforeRetryFakeTimers.test.js ✓ retryTimes set with fake timers" `; + +exports[`Test Retries with flag retryImmediately retry immediately 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__/retryImmediately.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__/retryImmediately.test.js:17:18) + +PASS __tests__/retryImmediately.test.js + ✓ retryTimes set + ✓ truthy test" +`; diff --git a/e2e/__tests__/testRetries.test.ts b/e2e/__tests__/testRetries.test.ts index acb3ffc25f39..4eac7e19be16 100644 --- a/e2e/__tests__/testRetries.test.ts +++ b/e2e/__tests__/testRetries.test.ts @@ -60,6 +60,26 @@ describe('Test Retries', () => { expect(extractSummary(result.stderr).rest).toMatchSnapshot(); }); + it('with flag retryImmediately retry immediately after failed test', () => { + const logMessage = `console.log + FIRST TRUTHY TEST + + at Object.log (__tests__/retryImmediately.test.js:14:13) + + console.log + SECOND TRUTHY TEST + + at Object.log (__tests__/retryImmediately.test.js:21:11)`; + + const result = runJest('test-retries', ['retryImmediately.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; diff --git a/e2e/test-retries/__tests__/retryImmediately.test.js b/e2e/test-retries/__tests__/retryImmediately.test.js new file mode 100644 index 000000000000..26d3388ec200 --- /dev/null +++ b/e2e/test-retries/__tests__/retryImmediately.test.js @@ -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, {logErrorsBeforeRetry: true, retryImmediately: 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(); +}); diff --git a/packages/jest-circus/src/run.ts b/packages/jest-circus/src/run.ts index 40f9695959ec..4f8ae2e6dcfb 100644 --- a/packages/jest-circus/src/run.ts +++ b/packages/jest-circus/src/run.ts @@ -15,7 +15,7 @@ import shuffleArray, { rngBuilder, } from './shuffleArray'; import {dispatch, getState} from './state'; -import {RETRY_TIMES, WAIT_BEFORE_RETRY} from './types'; +import {RETRY_IMMEDIATELY, RETRY_TIMES, WAIT_BEFORE_RETRY} from './types'; import { callAsyncCircusFn, getAllHooksForDescribe, @@ -78,11 +78,33 @@ 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)[RETRY_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': { @@ -91,12 +113,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); } @@ -107,19 +139,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) { diff --git a/packages/jest-circus/src/types.ts b/packages/jest-circus/src/types.ts index 0e64cf823033..4b2a6a7632e7 100644 --- a/packages/jest-circus/src/types.ts +++ b/packages/jest-circus/src/types.ts @@ -7,6 +7,7 @@ export const STATE_SYM = Symbol('JEST_STATE_SYMBOL'); export const RETRY_TIMES = Symbol.for('RETRY_TIMES'); +export const RETRY_IMMEDIATELY = Symbol.for('RETRY_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'); diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index ad3e6c1be1ce..eed42adcffd7 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -300,12 +300,19 @@ export interface Jest { * * `waitBeforeRetry` is the number of milliseconds to wait before retrying * + * `retryImmediately` is the flag to retry the failed test immediately after + * failure + * * @remarks * Only available with `jest-circus` runner. */ retryTimes( numRetries: number, - options?: {logErrorsBeforeRetry?: boolean; waitBeforeRetry?: number}, + options?: { + logErrorsBeforeRetry?: boolean; + retryImmediately?: boolean; + waitBeforeRetry?: number; + }, ): Jest; /** * Exhausts tasks queued by `setImmediate()`. diff --git a/packages/jest-runtime/src/index.ts b/packages/jest-runtime/src/index.ts index 1c004f26ca40..551bce13a28b 100644 --- a/packages/jest-runtime/src/index.ts +++ b/packages/jest-runtime/src/index.ts @@ -123,6 +123,7 @@ type ResolveOptions = Parameters[1] & { const testTimeoutSymbol = Symbol.for('TEST_TIMEOUT_SYMBOL'); const retryTimesSymbol = Symbol.for('RETRY_TIMES'); const waitBeforeRetrySymbol = Symbol.for('WAIT_BEFORE_RETRY'); +const retryImmediatelySybmbol = Symbol.for('RETRY_IMMEDIATELY'); const logErrorsBeforeRetrySymbol = Symbol.for('LOG_ERRORS_BEFORE_RETRY'); const NODE_MODULES = `${path.sep}node_modules${path.sep}`; @@ -2292,6 +2293,8 @@ export default class Runtime { options?.logErrorsBeforeRetry; this._environment.global[waitBeforeRetrySymbol] = options?.waitBeforeRetry; + this._environment.global[retryImmediatelySybmbol] = + options?.retryImmediately; return jestObject; }; diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index bdd26ae82811..7de8bf5a323d 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -667,6 +667,11 @@ expect(jest.retryTimes(3, {logErrorsBeforeRetry: 'all'})).type.toRaiseError(); expect(jest.retryTimes({logErrorsBeforeRetry: true})).type.toRaiseError(); expect(jest.retryTimes(3, {waitBeforeRetry: 1000})).type.toEqual(); expect(jest.retryTimes(3, {waitBeforeRetry: true})).type.toRaiseError(); +expect(jest.retryTimes(3, {retryImmediately: true})).type.toEqual< + typeof jest +>(); +expect(jest.retryTimes(3, {retryImmediately: 'now'})).type.toRaiseError(); +expect(jest.retryTimes(3, {retryImmediately: 1000})).type.toRaiseError(); expect(jest.retryTimes({logErrorsBeforeRetry: 'all'})).type.toRaiseError(); expect(jest.retryTimes()).type.toRaiseError();