Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions docs/plugins/retryFailedStep.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ sidebar: auto
title: retryFailedStep
---


<!-- Generated by documentation.js. Update this documentation by updating the source code. -->

## retryFailedStep
Expand All @@ -28,10 +29,9 @@ Run tests with plugin enabled:
#### Configuration:

* `retries` - number of retries (by default 3),
* `when` - function, when to perform a retry (accepts error as parameter)
* `factor` - The exponential factor to use. Default is 1.5.
* `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
* `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
* `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
* `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
* `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
* `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
* `amOnPage`
Expand Down
66 changes: 48 additions & 18 deletions docs/retry.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,36 @@ CodeceptJS provides flexible retry mechanisms to handle flaky tests. Use retries

## Helper Retries

Browser automation helpers (Playwright, Puppeteer, WebDriver) have **built-in retry mechanisms** for element interactions. When you call `I.click('Button')`, Playwright automatically waits for the element to exist, be visible, stable, and enabled — retrying for up to 5 seconds.
Plawright has a built-in retry mechanism for element interactions. When you call `I.click('Button')`, after the element is located Playwright keeps retrying until it is actionable — up to `timeout` (default 5s).

Configure the timeout in your helper settings:
> WebDriver has a different auto-retry option: [smartWait](/webdriver#smartwait)

Even though the handle exists (from `.all()`), Playwright still waits for it to become visible, stable (not mid-animation), enabled, not covered by an overlay/modal, and not rerendering.

```js
helpers: {
Playwright: {
timeout: 5000, // retry actions for up to 5 seconds
waitForAction: 100 // wait 100ms before each action
timeout: 5000, // retry the action until the element is actionable
waitForAction: 100 // fixed pause AFTER click/doubleClick/pressKey
}
}
```

**Learn more:** [Playwright Helper](/helpers/Playwright), [Timeouts](/timeouts)
What each setting does:

```
find element (no wait — fails instantly if locator matches nothing)
→ wait up to `timeout` for it to become actionable ← timeout
→ perform action
→ sleep `waitForAction` ms ← waitForAction (settle pause, not a wait)
```

`timeout` covers the action. If the locator matches nothing yet, the step fails immediately. Use [Failed Step Retries](#failed-step-retries) to cover that gap.


## Failed Step Retries

Automatically retry all failed steps without modifying test code:
CodeceptJS retries all failed steps by default by using the `retryFailedStep` plugin.

```js
plugins: {
Expand Down Expand Up @@ -66,18 +78,36 @@ Scenario('manual retries only', { disableRetryFailedStep: true }, ({ I }) => {
})
```

Full plugin options:

| Option | Default | Description |
|--------|---------|-------------|
| `retries` | — | Retries per step |
| `minTimeout` | — | Milliseconds before first retry |
| `maxTimeout` | `Infinity` | Max milliseconds between retries |
| `factor` | — | Exponential backoff multiplier |
| `randomize` | `false` | Randomize timeout intervals |
| `ignoredSteps` | `[]` | Patterns/regex of steps to never retry |
| `deferToScenarioRetries` | `true` | Disable step retries when scenario retries exist |
| `when` | `() => true` | Function receiving error; return `true` to retry |
Defaults: `minTimeout: 150`, `factor: 1.5`, `maxTimeout: 10000`.


> See [plugin reference](/plugins/retry-failed-step) for more options

Retries are calculated via this formula:

```
gap(N) = min(minTimeout × factor^(N-1), maxTimeout)
```

Practically if step fails it will trigger a retry with increasing delay until `maxTimeout` is reached:

```
retries: 2 => 0.15s-0.4s (150,225ms)
retries: 3 => 0.15s-0.7s (150,225,338ms)
retries: 3, minTimeout: 1000 => 1s-4.75s (1s,1.5s,2.25s)
retries: 3, minTimeout: 1000, factor: 2 => 1s-7s (1s,2s,4s)
retries: 5, minTimeout: 1000, factor: 2 => 1s-25s (1s,2s,4s,8s,10s)
```

Playwright `timeout` adds to each attempt only when the element is found:

- `Playwright.timeout: 5000`
- `retries: 2, minTimeout: 1000`

```
element not found => 0 + (1s+1s) = 2s
element found but not interactable => 3×5s + (1s+1s) = 17s
```

## Manual Step Retries

Expand Down
12 changes: 6 additions & 6 deletions lib/plugin/retryFailedStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const debug = debugModule('codeceptjs:retryFailedStep')
const defaultConfig = {
retries: 3,
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
minTimeout: 150,
maxTimeout: 10000,
factor: 1.5,
randomize: false,
ignoredSteps: [],
deferToScenarioRetries: true,
}
Expand Down Expand Up @@ -44,10 +47,9 @@ const RETRY_PRIORITIES = {
* #### Configuration:
*
* * `retries` - number of retries (by default 3),
* * `when` - function, when to perform a retry (accepts error as parameter)
* * `factor` - The exponential factor to use. Default is 1.5.
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
* * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
* * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
* * `amOnPage`
Expand Down Expand Up @@ -89,9 +91,8 @@ const RETRY_PRIORITIES = {
*
*/
export default function (config) {
config = Object.assign(defaultConfig, config)
config = Object.assign({}, defaultConfig, config)
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
const customWhen = config.when

let enableRetry = false

Expand All @@ -101,7 +102,6 @@ export default function (config) {
if (!store.autoRetries) return false
if (err && err.isTerminal) return false
if (err && err.message && (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed'))) return false
if (customWhen) return customWhen(err)
return true
}
config.when = when
Expand Down
29 changes: 29 additions & 0 deletions test/unit/plugin/retryFailedStep_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,35 @@ describe('retryFailedStep', () => {
expect(counter).to.equal(2)
})

describe('config', () => {
it('applies default retry timing', () => {
retryFailedStep({})
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
expect(cfg.retries).to.equal(3)
expect(cfg.minTimeout).to.equal(150)
expect(cfg.maxTimeout).to.equal(10000)
expect(cfg.factor).to.equal(1.5)
})

it('overrides retry timing from config', () => {
retryFailedStep({ retries: 5, minTimeout: 1000, maxTimeout: 3000, factor: 2 })
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
expect(cfg.retries).to.equal(5)
expect(cfg.minTimeout).to.equal(1000)
expect(cfg.maxTimeout).to.equal(3000)
expect(cfg.factor).to.equal(2)
})

it('does not leak config between instances', () => {
retryFailedStep({ retries: 5, minTimeout: 1000 })
recorder.retries = []
retryFailedStep({})
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
expect(cfg.retries).to.equal(3)
expect(cfg.minTimeout).to.equal(150)
})
})

it('should not retry steps with wait*', async () => {
retryFailedStep({ retries: 2, minTimeout: 1 })
event.dispatcher.emit(event.test.before, createTest('test'))
Expand Down