Skip to content

Commit 8fb5de2

Browse files
DavertMikDavertMikclaude
authored
Fix retryFailedStep config: own retry timing, stop mutating shared defaults (#5574)
* Fix retryFailedStep config: own retry timing, stop mutating shared defaults - retryFailedStep now sets minTimeout/maxTimeout/factor in its own defaultConfig instead of falling through to the recorder globals, so these values are configurable per-plugin - stop mutating the shared module-level defaultConfig (Object.assign({}, ...)), the same leak fixed for the recorder in 4ef2af2 — config no longer bleeds across plugin instances - correct JSDoc defaults (minTimeout 150, maxTimeout 10000) - add config unit tests (defaults, overrides, no leak) - rewrite docs/retry.md: helper vs step retries, Playwright-only auto-waiting, WebDriver smartWait note, retry timing formula Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Remove user-facing `when` option from retryFailedStep config The internal retry predicate (config.when) is still set for the recorder; only the configurable customWhen hook is removed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5761229 commit 8fb5de2

4 files changed

Lines changed: 86 additions & 27 deletions

File tree

docs/plugins/retryFailedStep.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sidebar: auto
55
title: retryFailedStep
66
---
77

8+
89
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
910

1011
## retryFailedStep
@@ -28,10 +29,9 @@ Run tests with plugin enabled:
2829
#### Configuration:
2930

3031
* `retries` - number of retries (by default 3),
31-
* `when` - function, when to perform a retry (accepts error as parameter)
3232
* `factor` - The exponential factor to use. Default is 1.5.
33-
* `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
34-
* `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
33+
* `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
34+
* `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
3535
* `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
3636
* `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
3737
* `amOnPage`

docs/retry.md

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,36 @@ CodeceptJS provides flexible retry mechanisms to handle flaky tests. Use retries
2020

2121
## Helper Retries
2222

23-
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.
23+
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).
2424

25-
Configure the timeout in your helper settings:
25+
> WebDriver has a different auto-retry option: [smartWait](/webdriver#smartwait)
26+
27+
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.
2628

2729
```js
2830
helpers: {
2931
Playwright: {
30-
timeout: 5000, // retry actions for up to 5 seconds
31-
waitForAction: 100 // wait 100ms before each action
32+
timeout: 5000, // retry the action until the element is actionable
33+
waitForAction: 100 // fixed pause AFTER click/doubleClick/pressKey
3234
}
3335
}
3436
```
3537

36-
**Learn more:** [Playwright Helper](/helpers/Playwright), [Timeouts](/timeouts)
38+
What each setting does:
39+
40+
```
41+
find element (no wait — fails instantly if locator matches nothing)
42+
→ wait up to `timeout` for it to become actionable ← timeout
43+
→ perform action
44+
→ sleep `waitForAction` ms ← waitForAction (settle pause, not a wait)
45+
```
46+
47+
`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.
48+
3749

3850
## Failed Step Retries
3951

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

4254
```js
4355
plugins: {
@@ -66,18 +78,36 @@ Scenario('manual retries only', { disableRetryFailedStep: true }, ({ I }) => {
6678
})
6779
```
6880

69-
Full plugin options:
70-
71-
| Option | Default | Description |
72-
|--------|---------|-------------|
73-
| `retries` || Retries per step |
74-
| `minTimeout` || Milliseconds before first retry |
75-
| `maxTimeout` | `Infinity` | Max milliseconds between retries |
76-
| `factor` || Exponential backoff multiplier |
77-
| `randomize` | `false` | Randomize timeout intervals |
78-
| `ignoredSteps` | `[]` | Patterns/regex of steps to never retry |
79-
| `deferToScenarioRetries` | `true` | Disable step retries when scenario retries exist |
80-
| `when` | `() => true` | Function receiving error; return `true` to retry |
81+
Defaults: `minTimeout: 150`, `factor: 1.5`, `maxTimeout: 10000`.
82+
83+
84+
> See [plugin reference](/plugins/retry-failed-step) for more options
85+
86+
Retries are calculated via this formula:
87+
88+
```
89+
gap(N) = min(minTimeout × factor^(N-1), maxTimeout)
90+
```
91+
92+
Practically if step fails it will trigger a retry with increasing delay until `maxTimeout` is reached:
93+
94+
```
95+
retries: 2 => 0.15s-0.4s (150,225ms)
96+
retries: 3 => 0.15s-0.7s (150,225,338ms)
97+
retries: 3, minTimeout: 1000 => 1s-4.75s (1s,1.5s,2.25s)
98+
retries: 3, minTimeout: 1000, factor: 2 => 1s-7s (1s,2s,4s)
99+
retries: 5, minTimeout: 1000, factor: 2 => 1s-25s (1s,2s,4s,8s,10s)
100+
```
101+
102+
Playwright `timeout` adds to each attempt only when the element is found:
103+
104+
- `Playwright.timeout: 5000`
105+
- `retries: 2, minTimeout: 1000`
106+
107+
```
108+
element not found => 0 + (1s+1s) = 2s
109+
element found but not interactable => 3×5s + (1s+1s) = 17s
110+
```
81111

82112
## Manual Step Retries
83113

lib/plugin/retryFailedStep.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ const debug = debugModule('codeceptjs:retryFailedStep')
88
const defaultConfig = {
99
retries: 3,
1010
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
11+
minTimeout: 150,
12+
maxTimeout: 10000,
1113
factor: 1.5,
14+
randomize: false,
1215
ignoredSteps: [],
1316
deferToScenarioRetries: true,
1417
}
@@ -44,10 +47,9 @@ const RETRY_PRIORITIES = {
4447
* #### Configuration:
4548
*
4649
* * `retries` - number of retries (by default 3),
47-
* * `when` - function, when to perform a retry (accepts error as parameter)
4850
* * `factor` - The exponential factor to use. Default is 1.5.
49-
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
50-
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
51+
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
52+
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
5153
* * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
5254
* * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
5355
* * `amOnPage`
@@ -89,9 +91,8 @@ const RETRY_PRIORITIES = {
8991
*
9092
*/
9193
export default function (config) {
92-
config = Object.assign(defaultConfig, config)
94+
config = Object.assign({}, defaultConfig, config)
9395
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
94-
const customWhen = config.when
9596

9697
let enableRetry = false
9798

@@ -101,7 +102,6 @@ export default function (config) {
101102
if (!store.autoRetries) return false
102103
if (err && err.isTerminal) return false
103104
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
104-
if (customWhen) return customWhen(err)
105105
return true
106106
}
107107
config.when = when

test/unit/plugin/retryFailedStep_test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,35 @@ describe('retryFailedStep', () => {
7676
expect(counter).to.equal(2)
7777
})
7878

79+
describe('config', () => {
80+
it('applies default retry timing', () => {
81+
retryFailedStep({})
82+
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
83+
expect(cfg.retries).to.equal(3)
84+
expect(cfg.minTimeout).to.equal(150)
85+
expect(cfg.maxTimeout).to.equal(10000)
86+
expect(cfg.factor).to.equal(1.5)
87+
})
88+
89+
it('overrides retry timing from config', () => {
90+
retryFailedStep({ retries: 5, minTimeout: 1000, maxTimeout: 3000, factor: 2 })
91+
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
92+
expect(cfg.retries).to.equal(5)
93+
expect(cfg.minTimeout).to.equal(1000)
94+
expect(cfg.maxTimeout).to.equal(3000)
95+
expect(cfg.factor).to.equal(2)
96+
})
97+
98+
it('does not leak config between instances', () => {
99+
retryFailedStep({ retries: 5, minTimeout: 1000 })
100+
recorder.retries = []
101+
retryFailedStep({})
102+
const cfg = recorder.retries.find(r => r.deferToScenarioRetries !== undefined)
103+
expect(cfg.retries).to.equal(3)
104+
expect(cfg.minTimeout).to.equal(150)
105+
})
106+
})
107+
79108
it('should not retry steps with wait*', async () => {
80109
retryFailedStep({ retries: 2, minTimeout: 1 })
81110
event.dispatcher.emit(event.test.before, createTest('test'))

0 commit comments

Comments
 (0)