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) + }) } 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() + }) + }) })