codeceptjs 3.7.5-beta.9 → 3.7.6-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/codecept.js CHANGED
@@ -296,29 +296,6 @@ program
296
296
 
297
297
  .action(require('../lib/command/run-rerun'))
298
298
 
299
- program
300
- .command('run-failed-tests')
301
- .description('Re-run tests that failed in the previous test run')
302
- .option(commandFlags.config.flag, commandFlags.config.description)
303
- .option(commandFlags.profile.flag, commandFlags.profile.description)
304
- .option(commandFlags.verbose.flag, commandFlags.verbose.description)
305
- .option(commandFlags.debug.flag, commandFlags.debug.description)
306
- .option(commandFlags.steps.flag, commandFlags.steps.description)
307
- .option('-o, --override [value]', 'override current config options')
308
- .option('-f, --file [path]', 'path to failed tests file (default: ./failed-tests.json)')
309
- .option('-g, --grep <pattern>', 'only run failed tests matching <pattern>')
310
- .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
311
- .option('--features', 'run only *.feature files and skip tests')
312
- .option('--tests', 'run only JS test files and skip features')
313
- .option('--colors', 'force enabling of colors')
314
- .option('--no-colors', 'force disabling of colors')
315
- .option('-R, --reporter <name>', 'specify the reporter to use')
316
- .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
317
- .option('--workers <number>', 'run failed tests in parallel using specified number of workers')
318
- .option('--suites', 'parallel execution of suites not single tests (when using --workers)')
319
- .option('--by <strategy>', 'test distribution strategy when using --workers: "test", "suite", or "pool"')
320
- .action(errorHandler(require('../lib/command/run-failed-tests')))
321
-
322
299
  program.on('command:*', cmd => {
323
300
  console.log(`\nUnknown command ${cmd}\n`)
324
301
  program.outputHelp()
package/lib/effects.js CHANGED
@@ -89,7 +89,7 @@ async function hopeThat(callback) {
89
89
  * - Restores the session state after each attempt, whether successful or not.
90
90
  *
91
91
  * @example
92
- * const { hopeThat } = require('codeceptjs/effects')
92
+ * const { retryTo } = require('codeceptjs/effects')
93
93
  * await retryTo((tries) => {
94
94
  * if (tries < 3) {
95
95
  * I.see('Non-existent element'); // Simulates a failure
@@ -275,7 +275,7 @@ class Appium extends Webdriver {
275
275
  const _convertedCaps = {}
276
276
  for (const [key, value] of Object.entries(capabilities)) {
277
277
  if (!key.startsWith(vendorPrefix.appium)) {
278
- if (key !== 'platformName' && key !== 'bstack:options') {
278
+ if (key !== 'platformName' && key !== 'bstack:options' && key !== 'sauce:options') {
279
279
  _convertedCaps[`${vendorPrefix.appium}:${key}`] = value
280
280
  } else {
281
281
  _convertedCaps[`${key}`] = value
@@ -360,10 +360,6 @@ class Playwright extends Helper {
360
360
  // override defaults with config
361
361
  this._setConfig(config)
362
362
 
363
- // Call _init() to register selector engines - use setTimeout to avoid blocking constructor
364
- setTimeout(() => {
365
- this._init().catch(console.error)
366
- }, 0)
367
363
  }
368
364
 
369
365
  _validateConfig(config) {
@@ -642,6 +638,9 @@ class Playwright extends Helper {
642
638
  async _after() {
643
639
  if (!this.isRunning) return
644
640
 
641
+ // Clear popup state to prevent leakage between tests
642
+ popupStore.clear()
643
+
645
644
  if (this.isElectron) {
646
645
  this.browser.close()
647
646
  this.electronSessions.forEach(session => session.close())
@@ -330,6 +330,9 @@ class Puppeteer extends Helper {
330
330
  async _after() {
331
331
  if (!this.isRunning) return
332
332
 
333
+ // Clear popup state to prevent leakage between tests
334
+ popupStore.clear()
335
+
333
336
  // close other sessions
334
337
  const contexts = this.browser.browserContexts()
335
338
  const defaultCtx = contexts.shift()
@@ -2588,7 +2591,8 @@ class Puppeteer extends Helper {
2588
2591
  *
2589
2592
  * {{> stopRecordingTraffic }}
2590
2593
  */
2591
- stopRecordingTraffic() {
2594
+ async stopRecordingTraffic() {
2595
+ await this.page.setRequestInterception(false)
2592
2596
  stopRecordingTraffic.call(this)
2593
2597
  }
2594
2598
 
@@ -281,6 +281,27 @@ class REST extends Helper {
281
281
  return this._executeRequest(request)
282
282
  }
283
283
 
284
+ /**
285
+ * Send HEAD request to REST API
286
+ *
287
+ * ```js
288
+ * I.sendHeadRequest('/api/users.json');
289
+ * ```
290
+ *
291
+ * @param {*} url
292
+ * @param {object} [headers={}] - the headers object to be sent. By default, it is sent as an empty object
293
+ *
294
+ * @returns {Promise<*>} response
295
+ */
296
+ async sendHeadRequest(url, headers = {}) {
297
+ const request = {
298
+ baseURL: this._url(url),
299
+ method: 'HEAD',
300
+ headers,
301
+ }
302
+ return this._executeRequest(request)
303
+ }
304
+
284
305
  /**
285
306
  * Sends POST request to API.
286
307
  *
@@ -491,7 +491,7 @@ class WebDriver extends Helper {
491
491
  }
492
492
  config.capabilities.browserName = config.browser || config.capabilities.browserName
493
493
 
494
- // WebDriver Bidi Protocol. Default: false
494
+ // WebDriver Bidi Protocol. Default: true
495
495
  config.capabilities.webSocketUrl = config.bidiProtocol ?? config.capabilities.webSocketUrl ?? true
496
496
 
497
497
  config.capabilities.browserVersion = config.browserVersion || config.capabilities.browserVersion
@@ -629,8 +629,11 @@ class WebDriver extends Helper {
629
629
 
630
630
  this.browser.on('dialog', () => {})
631
631
 
632
- await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
633
- this.browser.on('log.entryAdded', logEvents)
632
+ // Check for Bidi, because "sessionSubscribe" is an exclusive Bidi protocol feature. Otherwise, error will be thrown.
633
+ if (this.browser.capabilities && this.browser.capabilities.webSocketUrl) {
634
+ await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
635
+ this.browser.on('log.entryAdded', logEvents)
636
+ }
634
637
 
635
638
  return this.browser
636
639
  }
@@ -30,7 +30,7 @@ async function seeTraffic({
30
30
  throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
31
31
  }
32
32
 
33
- if (!this.recording || !this.recordedAtLeastOnce) {
33
+ if (!this.recordedAtLeastOnce) {
34
34
  throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
35
35
  }
36
36
 
@@ -66,7 +66,7 @@ async function seeTraffic({
66
66
  }
67
67
 
68
68
  async function grabRecordedNetworkTraffics() {
69
- if (!this.recording || !this.recordedAtLeastOnce) {
69
+ if (!this.recordedAtLeastOnce) {
70
70
  throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
71
71
  }
72
72
 
@@ -107,6 +107,8 @@ async function grabRecordedNetworkTraffics() {
107
107
  function stopRecordingTraffic() {
108
108
  // @ts-ignore
109
109
  this.page.removeAllListeners('request');
110
+ // @ts-ignore
111
+ this.page.removeAllListeners('requestfinished');
110
112
  this.recording = false;
111
113
  }
112
114
 
@@ -0,0 +1,110 @@
1
+ const event = require('../event')
2
+ const output = require('../output')
3
+ const Config = require('../config')
4
+ const { isNotSet } = require('../utils')
5
+
6
+ const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
+
8
+ /**
9
+ * Priority levels for retry mechanisms (higher number = higher priority)
10
+ * This ensures consistent behavior when multiple retry mechanisms are active
11
+ */
12
+ const RETRY_PRIORITIES = {
13
+ MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
14
+ STEP_PLUGIN: 50, // retryFailedStep plugin
15
+ SCENARIO_CONFIG: 30, // Global scenario retry config
16
+ FEATURE_CONFIG: 20, // Global feature retry config
17
+ HOOK_CONFIG: 10, // Hook retry config - lowest priority
18
+ }
19
+
20
+ /**
21
+ * Enhanced global retry mechanism that coordinates with other retry types
22
+ */
23
+ module.exports = function () {
24
+ event.dispatcher.on(event.suite.before, suite => {
25
+ let retryConfig = Config.get('retry')
26
+ if (!retryConfig) return
27
+
28
+ if (Number.isInteger(+retryConfig)) {
29
+ // is number - apply as feature-level retry
30
+ const retryNum = +retryConfig
31
+ output.log(`[Global Retry] Feature retries: ${retryNum}`)
32
+
33
+ // Only set if not already set by higher priority mechanism
34
+ if (isNotSet(suite.retries())) {
35
+ suite.retries(retryNum)
36
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
37
+ }
38
+ return
39
+ }
40
+
41
+ if (!Array.isArray(retryConfig)) {
42
+ retryConfig = [retryConfig]
43
+ }
44
+
45
+ for (const config of retryConfig) {
46
+ if (config.grep) {
47
+ if (!suite.title.includes(config.grep)) continue
48
+ }
49
+
50
+ // Handle hook retries with priority awareness
51
+ hooks
52
+ .filter(hook => !!config[hook])
53
+ .forEach(hook => {
54
+ const retryKey = `retry${hook}`
55
+ if (isNotSet(suite.opts[retryKey])) {
56
+ suite.opts[retryKey] = config[hook]
57
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
58
+ }
59
+ })
60
+
61
+ // Handle feature-level retries
62
+ if (config.Feature) {
63
+ if (isNotSet(suite.retries()) || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
64
+ suite.retries(config.Feature)
65
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
66
+ output.log(`[Global Retry] Feature retries: ${config.Feature}`)
67
+ }
68
+ }
69
+ }
70
+ })
71
+
72
+ event.dispatcher.on(event.test.before, test => {
73
+ let retryConfig = Config.get('retry')
74
+ if (!retryConfig) return
75
+
76
+ if (Number.isInteger(+retryConfig)) {
77
+ // Only set if not already set by higher priority mechanism
78
+ if (test.retries() === -1) {
79
+ test.retries(retryConfig)
80
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
81
+ output.log(`[Global Retry] Scenario retries: ${retryConfig}`)
82
+ }
83
+ return
84
+ }
85
+
86
+ if (!Array.isArray(retryConfig)) {
87
+ retryConfig = [retryConfig]
88
+ }
89
+
90
+ retryConfig = retryConfig.filter(config => !!config.Scenario)
91
+
92
+ for (const config of retryConfig) {
93
+ if (config.grep) {
94
+ if (!test.fullTitle().includes(config.grep)) continue
95
+ }
96
+
97
+ if (config.Scenario) {
98
+ // Respect priority system
99
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
100
+ test.retries(config.Scenario)
101
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
102
+ output.log(`[Global Retry] Scenario retries: ${config.Scenario}`)
103
+ }
104
+ }
105
+ }
106
+ })
107
+ }
108
+
109
+ // Export priority constants for use by other retry mechanisms
110
+ module.exports.RETRY_PRIORITIES = RETRY_PRIORITIES
package/lib/mocha/test.js CHANGED
@@ -121,7 +121,6 @@ function serializeTest(test, error = null) {
121
121
  }
122
122
 
123
123
  return {
124
- file: test.file ? relativeDir(test.file) : undefined,
125
124
  opts: test.opts || {},
126
125
  tags: test.tags || [],
127
126
  uid: test.uid,
@@ -0,0 +1,99 @@
1
+ const event = require('../event')
2
+ const recorder = require('../recorder')
3
+ const store = require('../store')
4
+ const output = require('../output')
5
+ const { RETRY_PRIORITIES } = require('../retryCoordinator')
6
+
7
+ const defaultConfig = {
8
+ retries: 3,
9
+ defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
10
+ factor: 1.5,
11
+ ignoredSteps: [],
12
+ }
13
+
14
+ /**
15
+ * Enhanced retryFailedStep plugin that coordinates with other retry mechanisms
16
+ *
17
+ * This plugin provides step-level retries and coordinates with global retry settings
18
+ * to avoid conflicts and provide predictable behavior.
19
+ */
20
+ module.exports = config => {
21
+ config = Object.assign({}, defaultConfig, config)
22
+ config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
23
+ const customWhen = config.when
24
+
25
+ let enableRetry = false
26
+
27
+ const when = err => {
28
+ if (!enableRetry) return false
29
+ if (store.debugMode) return false
30
+ if (!store.autoRetries) return false
31
+ if (customWhen) return customWhen(err)
32
+ return true
33
+ }
34
+ config.when = when
35
+
36
+ event.dispatcher.on(event.step.started, step => {
37
+ // if a step is ignored - return
38
+ for (const ignored of config.ignoredSteps) {
39
+ if (step.name === ignored) return
40
+ if (ignored instanceof RegExp) {
41
+ if (step.name.match(ignored)) return
42
+ } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
43
+ }
44
+ enableRetry = true // enable retry for a step
45
+ })
46
+
47
+ event.dispatcher.on(event.step.finished, () => {
48
+ enableRetry = false
49
+ })
50
+
51
+ event.dispatcher.on(event.test.before, test => {
52
+ // pass disableRetryFailedStep is a preferred way to disable retries
53
+ // test.disableRetryFailedStep is used for backward compatibility
54
+ if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
55
+ store.autoRetries = false
56
+ output.log(`[Step Retry] Disabled for test: ${test.title}`)
57
+ return // disable retry when a test is not active
58
+ }
59
+
60
+ // Check if step retries should be disabled due to higher priority scenario retries
61
+ const scenarioRetries = test.retries()
62
+ const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
63
+ const scenarioPriority = test.opts.retryPriority || 0
64
+
65
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
66
+ // Scenario retries are configured with higher or equal priority
67
+ // Option 1: Disable step retries (conservative approach)
68
+ store.autoRetries = false
69
+ output.log(`[Step Retry] Deferred to scenario retries (${scenarioRetries} retries)`)
70
+ return
71
+
72
+ // Option 2: Reduce step retries to avoid excessive total retries
73
+ // const reducedStepRetries = Math.max(1, Math.floor(config.retries / scenarioRetries))
74
+ // config.retries = reducedStepRetries
75
+ // output.log(`[Step Retry] Reduced to ${reducedStepRetries} retries due to scenario retries (${scenarioRetries})`)
76
+ }
77
+
78
+ // this option is used to set the retries inside _before() block of helpers
79
+ store.autoRetries = true
80
+ test.opts.conditionalRetries = config.retries
81
+ test.opts.stepRetryPriority = stepRetryPriority
82
+
83
+ recorder.retry(config)
84
+
85
+ output.log(`[Step Retry] Enabled with ${config.retries} retries for test: ${test.title}`)
86
+ })
87
+
88
+ // Add coordination info for debugging
89
+ event.dispatcher.on(event.test.finished, test => {
90
+ if (test.state === 'passed' && test.opts.conditionalRetries && store.autoRetries) {
91
+ const stepRetries = test.opts.conditionalRetries || 0
92
+ const scenarioRetries = test.retries() || 0
93
+
94
+ if (stepRetries > 0 && scenarioRetries > 0) {
95
+ output.log(`[Retry Coordination] Test used both step retries (${stepRetries}) and scenario retries (${scenarioRetries})`)
96
+ }
97
+ }
98
+ })
99
+ }
package/lib/recorder.js CHANGED
@@ -11,6 +11,7 @@ let running = false
11
11
  let errFn
12
12
  let queueId = 0
13
13
  let sessionId = null
14
+ let sessionStack = [] // Stack to support nested sessions
14
15
  let asyncErr = null
15
16
  let ignoredErrs = []
16
17
 
@@ -89,6 +90,7 @@ module.exports = {
89
90
  if (promise && running) this.catch()
90
91
  queueId++
91
92
  sessionId = null
93
+ sessionStack = [] // Clear the session stack
92
94
  asyncErr = null
93
95
  log(`${currentQueue()} Starting recording promises`)
94
96
  promise = Promise.resolve()
@@ -123,8 +125,13 @@ module.exports = {
123
125
  */
124
126
  start(name) {
125
127
  if (sessionId) {
126
- debug(`${currentQueue()}Session already started as ${sessionId}`)
127
- this.restore(sessionId)
128
+ debug(`${currentQueue()}Session already started as ${sessionId}, nesting session ${name}`)
129
+ // Push current session to stack instead of restoring it
130
+ sessionStack.push({
131
+ id: sessionId,
132
+ promise: promise,
133
+ running: this.running,
134
+ })
128
135
  }
129
136
  debug(`${currentQueue()}Starting <${name}> session`)
130
137
  tasks.push('--->')
@@ -142,9 +149,18 @@ module.exports = {
142
149
  tasks.push('<---')
143
150
  debug(`${currentQueue()}Finalize <${name}> session`)
144
151
  this.running = false
145
- sessionId = null
146
152
  this.catch(errFn)
147
153
  promise = promise.then(() => oldPromises.pop())
154
+
155
+ // Restore parent session from stack if available
156
+ if (sessionStack.length > 0) {
157
+ const parentSession = sessionStack.pop()
158
+ sessionId = parentSession.id
159
+ this.running = parentSession.running
160
+ debug(`${currentQueue()}Restored parent session <${sessionId}>`)
161
+ } else {
162
+ sessionId = null
163
+ }
148
164
  },
149
165
 
150
166
  /**
@@ -0,0 +1,207 @@
1
+ const output = require('./output')
2
+
3
+ /**
4
+ * Retry Coordinator - Central coordination for all retry mechanisms
5
+ *
6
+ * This module provides:
7
+ * 1. Priority-based retry coordination
8
+ * 2. Unified configuration validation
9
+ * 3. Consolidated retry reporting
10
+ * 4. Conflict detection and resolution
11
+ */
12
+
13
+ /**
14
+ * Priority levels for retry mechanisms (higher number = higher priority)
15
+ */
16
+ const RETRY_PRIORITIES = {
17
+ MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
18
+ STEP_PLUGIN: 50, // retryFailedStep plugin
19
+ SCENARIO_CONFIG: 30, // Global scenario retry config
20
+ FEATURE_CONFIG: 20, // Global feature retry config
21
+ HOOK_CONFIG: 10, // Hook retry config - lowest priority
22
+ }
23
+
24
+ /**
25
+ * Retry mechanism types
26
+ */
27
+ const RETRY_TYPES = {
28
+ MANUAL_STEP: 'manual-step',
29
+ STEP_PLUGIN: 'step-plugin',
30
+ SCENARIO: 'scenario',
31
+ FEATURE: 'feature',
32
+ HOOK: 'hook',
33
+ }
34
+
35
+ /**
36
+ * Global retry coordination state
37
+ */
38
+ let retryState = {
39
+ activeTest: null,
40
+ activeSuite: null,
41
+ retryHistory: [],
42
+ conflicts: [],
43
+ }
44
+
45
+ /**
46
+ * Registers a retry mechanism for coordination
47
+ * @param {string} type - Type of retry mechanism
48
+ * @param {Object} config - Retry configuration
49
+ * @param {Object} target - Target object (test, suite, etc.)
50
+ * @param {number} priority - Priority level
51
+ */
52
+ function registerRetry(type, config, target, priority = 0) {
53
+ const retryInfo = {
54
+ type,
55
+ config,
56
+ target,
57
+ priority,
58
+ timestamp: Date.now(),
59
+ }
60
+
61
+ // Detect conflicts
62
+ const existingRetries = retryState.retryHistory.filter(r => r.target === target && r.type !== type && r.priority !== priority)
63
+
64
+ if (existingRetries.length > 0) {
65
+ const conflict = {
66
+ newRetry: retryInfo,
67
+ existingRetries: existingRetries,
68
+ resolved: false,
69
+ }
70
+ retryState.conflicts.push(conflict)
71
+ handleRetryConflict(conflict)
72
+ }
73
+
74
+ retryState.retryHistory.push(retryInfo)
75
+
76
+ output.log(`[Retry Coordinator] Registered ${type} retry (priority: ${priority})`)
77
+ }
78
+
79
+ /**
80
+ * Handles conflicts between retry mechanisms
81
+ * @param {Object} conflict - Conflict information
82
+ */
83
+ function handleRetryConflict(conflict) {
84
+ const { newRetry, existingRetries } = conflict
85
+
86
+ // Find highest priority retry
87
+ const allRetries = [newRetry, ...existingRetries]
88
+ const highestPriority = Math.max(...allRetries.map(r => r.priority))
89
+ const winningRetry = allRetries.find(r => r.priority === highestPriority)
90
+
91
+ // Log the conflict resolution
92
+ output.log(`[Retry Coordinator] Conflict detected:`)
93
+ allRetries.forEach(retry => {
94
+ const status = retry === winningRetry ? 'ACTIVE' : 'DEFERRED'
95
+ output.log(` - ${retry.type} (priority: ${retry.priority}) [${status}]`)
96
+ })
97
+
98
+ conflict.resolved = true
99
+ conflict.winner = winningRetry
100
+ }
101
+
102
+ /**
103
+ * Gets the effective retry configuration for a target
104
+ * @param {Object} target - Target object (test, suite, etc.)
105
+ * @returns {Object} Effective retry configuration
106
+ */
107
+ function getEffectiveRetryConfig(target) {
108
+ const targetRetries = retryState.retryHistory.filter(r => r.target === target)
109
+
110
+ if (targetRetries.length === 0) {
111
+ return { type: 'none', retries: 0 }
112
+ }
113
+
114
+ // Find highest priority retry
115
+ const highestPriority = Math.max(...targetRetries.map(r => r.priority))
116
+ const effectiveRetry = targetRetries.find(r => r.priority === highestPriority)
117
+
118
+ return {
119
+ type: effectiveRetry.type,
120
+ retries: effectiveRetry.config.retries || effectiveRetry.config,
121
+ config: effectiveRetry.config,
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Generates a retry summary report
127
+ * @returns {Object} Retry summary
128
+ */
129
+ function generateRetrySummary() {
130
+ const summary = {
131
+ totalRetryMechanisms: retryState.retryHistory.length,
132
+ conflicts: retryState.conflicts.length,
133
+ byType: {},
134
+ recommendations: [],
135
+ }
136
+
137
+ // Count by type
138
+ retryState.retryHistory.forEach(retry => {
139
+ summary.byType[retry.type] = (summary.byType[retry.type] || 0) + 1
140
+ })
141
+
142
+ // Generate recommendations
143
+ if (summary.conflicts > 0) {
144
+ summary.recommendations.push('Consider consolidating retry configurations to avoid conflicts')
145
+ }
146
+
147
+ if (summary.byType[RETRY_TYPES.STEP_PLUGIN] && summary.byType[RETRY_TYPES.SCENARIO]) {
148
+ summary.recommendations.push('Step-level and scenario-level retries are both active - consider using only one approach')
149
+ }
150
+
151
+ return summary
152
+ }
153
+
154
+ /**
155
+ * Resets the retry coordination state (useful for testing)
156
+ */
157
+ function reset() {
158
+ retryState = {
159
+ activeTest: null,
160
+ activeSuite: null,
161
+ retryHistory: [],
162
+ conflicts: [],
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Validates retry configuration for common issues
168
+ * @param {Object} config - Configuration object
169
+ * @returns {Array} Array of validation warnings
170
+ */
171
+ function validateConfig(config) {
172
+ const warnings = []
173
+
174
+ if (!config) return warnings
175
+
176
+ // Check for potential configuration conflicts
177
+ if (config.retry && config.plugins && config.plugins.retryFailedStep) {
178
+ const globalRetries = typeof config.retry === 'number' ? config.retry : config.retry.Scenario || config.retry.Feature
179
+ const stepRetries = config.plugins.retryFailedStep.retries || 3
180
+
181
+ if (globalRetries && stepRetries) {
182
+ warnings.push(`Both global retries (${globalRetries}) and step retries (${stepRetries}) are configured - total executions could be ${globalRetries * (stepRetries + 1)}`)
183
+ }
184
+ }
185
+
186
+ // Check for excessive retry counts
187
+ if (config.retry) {
188
+ const retryValues = typeof config.retry === 'number' ? [config.retry] : Object.values(config.retry)
189
+ const maxRetries = Math.max(...retryValues.filter(v => typeof v === 'number'))
190
+
191
+ if (maxRetries > 5) {
192
+ warnings.push(`High retry count detected (${maxRetries}) - consider investigating test stability instead`)
193
+ }
194
+ }
195
+
196
+ return warnings
197
+ }
198
+
199
+ module.exports = {
200
+ RETRY_PRIORITIES,
201
+ RETRY_TYPES,
202
+ registerRetry,
203
+ getEffectiveRetryConfig,
204
+ generateRetrySummary,
205
+ validateConfig,
206
+ reset,
207
+ }