From 34ba60f8daea9c09742cb8feff71e32b9b9fb89c Mon Sep 17 00:00:00 2001 From: Itay Sin Malia Date: Sat, 6 Jun 2026 01:22:04 +0300 Subject: [PATCH 1/2] fix: prevent retry config accumulation by having helpers and plugin cleanup their own config after each test - Playwright & Puppeteer: store retry config in this._retryConfig and remove in _after() - retryFailedStep plugin: avoid duplicate pushes and clean config on event.test.after --- lib/helper/Playwright.js | 12 ++++++++++-- lib/helper/Puppeteer.js | 12 ++++++++++-- lib/plugin/retryFailedStep.js | 8 +++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 939988c9a..5ea865d99 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -575,7 +575,8 @@ class Playwright extends Helper { // Clear popup state to ensure clean state for each test popupStore.clear() - recorder.retry({ + // Configure retry for this test; will clean up after test completes + this._retryConfig = { retries: test?.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { @@ -584,7 +585,8 @@ class Playwright extends Helper { // ignore context errors return err.message.includes('context') }, - }) + } + recorder.retry(this._retryConfig) // Start browser if needed (initial start or browser restart strategy) if (!this.isRunning && !this.options.manualStart) await this._startBrowser() @@ -689,6 +691,12 @@ class Playwright extends Helper { } async _after() { + // Clean up our retry config to prevent accumulation + if (this._retryConfig) { + recorder.retries = recorder.retries.filter(r => r !== this._retryConfig) + this._retryConfig = null + } + if (!this.isRunning) return // Clear popup state to prevent leakage between tests diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index ff00f6dd8..094b3752c 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -354,7 +354,8 @@ class Puppeteer extends Helper { async _before(test) { this.sessionPages = {} this.currentRunningTest = test - recorder.retry({ + // Configure retry for this test; will clean up after test completes + this._retryConfig = { retries: test?.opts?.conditionalRetries || 3, when: err => { if (!err || typeof err.message !== 'string') { @@ -363,13 +364,20 @@ class Puppeteer extends Helper { // ignore context errors return err.message.includes('context') }, - }) + } + recorder.retry(this._retryConfig) if (this.options.restart && !this.options.manualStart) return this._startBrowser() if (!this.isRunning && !this.options.manualStart) return this._startBrowser() return this.browser } async _after() { + // Clean up our retry config to prevent accumulation + if (this._retryConfig) { + recorder.retries = recorder.retries.filter(r => r !== this._retryConfig) + this._retryConfig = null + } + if (!this.isRunning) return // Clear popup state to prevent leakage between tests diff --git a/lib/plugin/retryFailedStep.js b/lib/plugin/retryFailedStep.js index 394fa4e5c..2ff51fe86 100644 --- a/lib/plugin/retryFailedStep.js +++ b/lib/plugin/retryFailedStep.js @@ -152,7 +152,9 @@ export default function (config) { test.opts.stepRetryPriority = stepRetryPriority debug('applying retries = %d for test %s', config.retries, test.title) - recorder.retry(config) + if (!recorder.retries.find(r => r === config)) { + recorder.retry(config) + } }) event.dispatcher.on(event.test.started, test => { @@ -171,4 +173,8 @@ export default function (config) { test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries } }) + + event.dispatcher.on(event.test.after, () => { + recorder.retries = recorder.retries.filter(r => r !== config) + }) } From 0b3a5b7c98b7c28049859b91277e6b6f0f01c0e5 Mon Sep 17 00:00:00 2001 From: Itay Sin Malia Date: Mon, 8 Jun 2026 00:07:44 +0300 Subject: [PATCH 2/2] test: add regression tests for retry config accumulation --- .../codecept.retry.accumulation.conf.js | 22 ++++++++++++++++++ .../codecept.retry.multipleScenarios.conf.js | 23 +++++++++++++++++++ .../configs/retryHooks/helper.accumulation.js | 16 +++++++++++++ .../retryHooks/retry_accumulation_test.js | 9 ++++++++ .../retry_multiple_scenarios_test.js | 13 +++++++++++ test/runner/retry_hooks_test.js | 16 +++++++++++++ 6 files changed, 99 insertions(+) create mode 100644 test/data/sandbox/configs/retryHooks/codecept.retry.accumulation.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/codecept.retry.multipleScenarios.conf.js create mode 100644 test/data/sandbox/configs/retryHooks/helper.accumulation.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_accumulation_test.js create mode 100644 test/data/sandbox/configs/retryHooks/retry_multiple_scenarios_test.js diff --git a/test/data/sandbox/configs/retryHooks/codecept.retry.accumulation.conf.js b/test/data/sandbox/configs/retryHooks/codecept.retry.accumulation.conf.js new file mode 100644 index 000000000..06cbf1875 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.retry.accumulation.conf.js @@ -0,0 +1,22 @@ +export const config = { + tests: './retry_accumulation_test.js', + output: './output', + helpers: { + Playwright: { + url: 'http://localhost:8000', + manualStart: true, + }, + AccumulationHelper: { + require: './helper.accumulation.js', + }, + }, + plugins: { + retryFailedStep: { + enabled: true, + retries: 2, + }, + }, + bootstrap: null, + mocha: {}, + name: 'retryAccumulation', +}; diff --git a/test/data/sandbox/configs/retryHooks/codecept.retry.multipleScenarios.conf.js b/test/data/sandbox/configs/retryHooks/codecept.retry.multipleScenarios.conf.js new file mode 100644 index 000000000..d6a9fc8e7 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/codecept.retry.multipleScenarios.conf.js @@ -0,0 +1,23 @@ +export const config = { + tests: './retry_multiple_scenarios_test.js', + output: './output', + helpers: { + Playwright: { + url: 'http://localhost:8000', + show: false, + restart: false, + }, + AccumulationHelper: { + require: './helper.accumulation.js', + }, + }, + plugins: { + retryFailedStep: { + enabled: true, + retries: 2, + }, + }, + bootstrap: null, + mocha: {}, + name: 'retryMultipleScenarios', +}; diff --git a/test/data/sandbox/configs/retryHooks/helper.accumulation.js b/test/data/sandbox/configs/retryHooks/helper.accumulation.js new file mode 100644 index 000000000..f8030ecaf --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/helper.accumulation.js @@ -0,0 +1,16 @@ +import Helper from '../../../../../lib/helper.js'; + +class AccumulationHelper extends Helper { + _before() { + this._failCount = 0; + } + + failingStep() { + this._failCount++; + if (this._failCount <= 2) { + throw new Error('failing step - retry expected'); + } + } +} + +export default AccumulationHelper; diff --git a/test/data/sandbox/configs/retryHooks/retry_accumulation_test.js b/test/data/sandbox/configs/retryHooks/retry_accumulation_test.js new file mode 100644 index 000000000..201c55b91 --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_accumulation_test.js @@ -0,0 +1,9 @@ +Feature('Retry Config Accumulation Test'); + +Scenario('first scenario', async ({ I }) => { + I.failingStep(); +}); + +Scenario('second scenario', async ({ I }) => { + I.failingStep(); +}); diff --git a/test/data/sandbox/configs/retryHooks/retry_multiple_scenarios_test.js b/test/data/sandbox/configs/retryHooks/retry_multiple_scenarios_test.js new file mode 100644 index 000000000..870df10ed --- /dev/null +++ b/test/data/sandbox/configs/retryHooks/retry_multiple_scenarios_test.js @@ -0,0 +1,13 @@ +Feature('Retry FailedStep - Multiple Consequent Scenarios'); + +Scenario('first scenario', async ({ I }) => { + I.failingStep(); +}); + +Scenario('second scenario', async ({ I }) => { + I.failingStep(); +}); + +Scenario('third scenario', async ({ I }) => { + I.failingStep(); +}); diff --git a/test/runner/retry_hooks_test.js b/test/runner/retry_hooks_test.js index 339989738..26bea3497 100644 --- a/test/runner/retry_hooks_test.js +++ b/test/runner/retry_hooks_test.js @@ -66,4 +66,20 @@ describe('CodeceptJS Retry Hooks', function () { done() }) }) + + it('should prevent retry config accumulation across tests', done => { + exec(config_run_config('codecept.retry.accumulation.conf.js', ''), (err, stdout) => { + debug_this_test && console.log(stdout) + expect(stdout).toContain('2 passed') + done() + }) + }) + + it('should retryFailedStep on multiple consequent scenarios', done => { + exec(config_run_config('codecept.retry.multipleScenarios.conf.js', ''), (err, stdout) => { + debug_this_test && console.log(stdout) + expect(stdout).toContain('3 passed') + done() + }) + }) })