codeceptjs 4.0.0-rc.1 → 4.0.0-rc.11

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.
Files changed (49) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +637 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/moveCursorTo.mustache +5 -1
  12. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  13. package/docs/webapi/seeElement.mustache +4 -0
  14. package/docs/webapi/seeInField.mustache +5 -0
  15. package/docs/webapi/selectOption.mustache +5 -0
  16. package/docs/webapi/uncheckOption.mustache +1 -1
  17. package/lib/codecept.js +20 -17
  18. package/lib/command/init.js +0 -3
  19. package/lib/command/run-workers.js +1 -0
  20. package/lib/container.js +19 -4
  21. package/lib/element/WebElement.js +81 -2
  22. package/lib/els.js +12 -6
  23. package/lib/helper/Appium.js +8 -8
  24. package/lib/helper/Playwright.js +224 -138
  25. package/lib/helper/Puppeteer.js +211 -69
  26. package/lib/helper/WebDriver.js +183 -64
  27. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  28. package/lib/helper/errors/NonFocusedType.js +8 -0
  29. package/lib/helper/extras/elementSelection.js +58 -0
  30. package/lib/helper/extras/focusCheck.js +43 -0
  31. package/lib/helper/scripts/dropFile.js +11 -0
  32. package/lib/html.js +14 -1
  33. package/lib/listener/globalRetry.js +32 -6
  34. package/lib/mocha/cli.js +10 -0
  35. package/lib/plugin/aiTrace.js +464 -0
  36. package/lib/plugin/retryFailedStep.js +28 -19
  37. package/lib/plugin/stepByStepReport.js +5 -1
  38. package/lib/step/config.js +15 -2
  39. package/lib/step/record.js +1 -1
  40. package/lib/utils.js +48 -0
  41. package/lib/workers.js +49 -7
  42. package/package.json +5 -3
  43. package/typings/index.d.ts +19 -0
  44. package/lib/listener/enhancedGlobalRetry.js +0 -110
  45. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  46. package/lib/plugin/htmlReporter.js +0 -3648
  47. package/lib/retryCoordinator.js +0 -207
  48. package/typings/promiseBasedTypes.d.ts +0 -9469
  49. package/typings/types.d.ts +0 -11402
@@ -1,7 +1,5 @@
1
1
  import event from '../event.js'
2
-
3
2
  import recorder from '../recorder.js'
4
-
5
3
  import store from '../store.js'
6
4
 
7
5
  const defaultConfig = {
@@ -9,6 +7,15 @@ const defaultConfig = {
9
7
  defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
10
8
  factor: 1.5,
11
9
  ignoredSteps: [],
10
+ deferToScenarioRetries: true,
11
+ }
12
+
13
+ const RETRY_PRIORITIES = {
14
+ MANUAL_STEP: 100,
15
+ STEP_PLUGIN: 50,
16
+ SCENARIO_CONFIG: 30,
17
+ FEATURE_CONFIG: 20,
18
+ HOOK_CONFIG: 10,
12
19
  }
13
20
 
14
21
  /**
@@ -49,6 +56,7 @@ const defaultConfig = {
49
56
  * * `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list.
50
57
  * You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
51
58
  * To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.
59
+ * * `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries.
52
60
  *
53
61
  * #### Example
54
62
  *
@@ -88,73 +96,74 @@ export default function (config) {
88
96
  if (!enableRetry) return
89
97
  if (store.debugMode) return false
90
98
  if (!store.autoRetries) return false
91
- // Don't retry terminal errors (e.g., frame detachment errors)
92
99
  if (err && err.isTerminal) return false
93
- // Don't retry navigation errors that are known to be terminal
94
100
  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
95
101
  if (customWhen) return customWhen(err)
96
102
  return true
97
103
  }
98
104
  config.when = when
99
105
 
100
- // Ensure retry options are available before any steps run
101
106
  if (!recorder.retries.find(r => r === config)) {
102
107
  recorder.retries.push(config)
103
108
  }
104
109
 
105
110
  event.dispatcher.on(event.step.started, step => {
106
- // if a step is ignored - return
107
111
  for (const ignored of config.ignoredSteps) {
108
112
  if (step.name === ignored) return
109
113
  if (ignored instanceof RegExp) {
110
114
  if (step.name.match(ignored)) return
111
115
  } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
112
116
  }
113
- enableRetry = true // enable retry for a step
117
+ enableRetry = true
114
118
  })
115
119
 
116
- // Disable retry only after a successful step; keep it enabled for failure so retry logic can act
117
120
  event.dispatcher.on(event.step.passed, () => {
118
121
  enableRetry = false
119
122
  })
120
123
 
121
124
  event.dispatcher.on(event.test.before, test => {
122
- // pass disableRetryFailedStep is a preferred way to disable retries
123
- // test.disableRetryFailedStep is used for backward compatibility
124
125
  if (!test.opts) test.opts = {}
125
126
  if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
126
127
  store.autoRetries = false
127
- return // disable retry when a test is not active
128
+ return
129
+ }
130
+
131
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
132
+ const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
133
+ const scenarioPriority = test.opts.retryPriority || 0
134
+
135
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
136
+ store.autoRetries = false
137
+ return
128
138
  }
129
139
 
130
- // Don't apply plugin retry logic if there are already manual retries configured
131
- // Check if any retry configs exist that aren't from this plugin
132
140
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
133
141
  if (hasManualRetries) {
134
142
  store.autoRetries = false
135
143
  return
136
144
  }
137
145
 
138
- // this option is used to set the retries inside _before() block of helpers
139
146
  store.autoRetries = true
140
147
  test.opts.conditionalRetries = config.retries
141
- // debug: record applied retries value for tests
148
+ test.opts.stepRetryPriority = stepRetryPriority
149
+
142
150
  if (process.env.DEBUG_RETRY_PLUGIN) {
143
- // eslint-disable-next-line no-console
144
151
  console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
145
152
  }
146
153
  recorder.retry(config)
147
154
  })
148
155
 
149
- // Fallback for environments where event.test.before wasn't emitted (runner scenarios)
150
156
  event.dispatcher.on(event.test.started, test => {
151
157
  if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
152
158
 
153
- // Don't apply plugin retry logic if there are already manual retries configured
154
- // Check if any retry configs exist that aren't from this plugin
155
159
  const hasManualRetries = recorder.retries.some(retry => retry !== config)
156
160
  if (hasManualRetries) return
157
161
 
162
+ const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
163
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
164
+ return
165
+ }
166
+
158
167
  if (!store.autoRetries) {
159
168
  store.autoRetries = true
160
169
  test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
@@ -207,7 +207,11 @@ export default function (config) {
207
207
  stepNum++
208
208
  slides[fileName] = step
209
209
  try {
210
- await helper.saveScreenshot(path.join(dir, fileName), config.fullPageScreenshots)
210
+ const screenshotPath = path.join(dir, fileName)
211
+ await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
212
+
213
+ step.artifacts = step.artifacts || {}
214
+ step.artifacts.screenshot = screenshotPath
211
215
  } catch (err) {
212
216
  output.plugin(`Can't save step screenshot: ${err}`)
213
217
  error = err
@@ -1,20 +1,33 @@
1
+ /**
2
+ * @typedef {Object} StepOptions
3
+ * @property {number|'first'|'last'} [elementIndex] - Select a specific element when multiple match. 1-based positive index, negative from end, or 'first'/'last'.
4
+ * @property {boolean} [exact] - Enable strict mode for this step. Throws if multiple elements match.
5
+ * @property {boolean} [strictMode] - Alias for exact.
6
+ * @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
7
+ */
8
+
1
9
  /**
2
10
  * StepConfig is a configuration object for a step.
3
11
  * It is used to create a new step that is a combination of other steps.
4
12
  */
5
13
  class StepConfig {
6
14
  constructor(opts = {}) {
7
- /** @member {{ opts: Record<string, any>, timeout: number|undefined, retry: number|undefined }} */
15
+ /** @member {{ opts: StepOptions, timeout: number|undefined, retry: number|undefined }} */
8
16
  this.config = {
9
17
  opts,
10
18
  timeout: undefined,
11
19
  retry: undefined,
12
20
  }
21
+ this.__isStepConfig = true
22
+ }
23
+
24
+ static isStepConfig(obj) {
25
+ return obj && (obj instanceof StepConfig || obj.__isStepConfig === true)
13
26
  }
14
27
 
15
28
  /**
16
29
  * Set the options for the step.
17
- * @param {object} opts - The options for the step.
30
+ * @param {StepOptions} opts - The options for the step.
18
31
  * @returns {StepConfig} - The step configuration object.
19
32
  */
20
33
  opts(opts) {
@@ -11,7 +11,7 @@ function recordStep(step, args) {
11
11
 
12
12
  // apply step configuration
13
13
  const lastArg = args[args.length - 1]
14
- if (lastArg instanceof StepConfig) {
14
+ if (StepConfig.isStepConfig(lastArg)) {
15
15
  const stepConfig = args.pop()
16
16
  const { opts, timeout, retry } = stepConfig.getConfig()
17
17
 
package/lib/utils.js CHANGED
@@ -150,6 +150,24 @@ export const decodeUrl = function (url) {
150
150
  return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)))
151
151
  }
152
152
 
153
+ export const normalizePath = function (path) {
154
+ if (path === '' || path === '/') return '/'
155
+ return path
156
+ .replace(/\/+/g, '/')
157
+ .replace(/\/$/, '') || '/'
158
+ }
159
+
160
+ export const resolveUrl = function (url, baseUrl) {
161
+ if (!url) return url
162
+ if (url.indexOf('http') === 0) return url
163
+ if (!baseUrl) return url
164
+ try {
165
+ return new URL(url, baseUrl).href
166
+ } catch (e) {
167
+ return url
168
+ }
169
+ }
170
+
153
171
  export const xpathLocator = {
154
172
  /**
155
173
  * @param {string} string
@@ -640,6 +658,36 @@ export const base64EncodeFile = function (filePath) {
640
658
  return Buffer.from(fs.readFileSync(filePath)).toString('base64')
641
659
  }
642
660
 
661
+ export const getMimeType = function (fileName) {
662
+ const ext = path.extname(fileName).toLowerCase()
663
+ const mimeTypes = {
664
+ '.jpg': 'image/jpeg',
665
+ '.jpeg': 'image/jpeg',
666
+ '.png': 'image/png',
667
+ '.gif': 'image/gif',
668
+ '.bmp': 'image/bmp',
669
+ '.svg': 'image/svg+xml',
670
+ '.webp': 'image/webp',
671
+ '.pdf': 'application/pdf',
672
+ '.txt': 'text/plain',
673
+ '.html': 'text/html',
674
+ '.css': 'text/css',
675
+ '.js': 'application/javascript',
676
+ '.json': 'application/json',
677
+ '.xml': 'application/xml',
678
+ '.zip': 'application/zip',
679
+ '.csv': 'text/csv',
680
+ '.doc': 'application/msword',
681
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
682
+ '.xls': 'application/vnd.ms-excel',
683
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
684
+ '.mp3': 'audio/mpeg',
685
+ '.mp4': 'video/mp4',
686
+ '.wav': 'audio/wav',
687
+ }
688
+ return mimeTypes[ext] || 'application/octet-stream'
689
+ }
690
+
643
691
  export const markdownToAnsi = function (markdown) {
644
692
  return (
645
693
  markdown
package/lib/workers.js CHANGED
@@ -28,7 +28,7 @@ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
28
28
 
29
29
  const initializeCodecept = async (configPath, options = {}) => {
30
30
  const config = await mainConfig.load(configPath || '.')
31
- const codecept = new Codecept(config, options)
31
+ const codecept = new Codecept(config, { ...options, skipDefaultListeners: true })
32
32
  await codecept.init(getTestRoot(configPath))
33
33
  codecept.loadTests()
34
34
 
@@ -625,13 +625,32 @@ class Workers extends EventEmitter {
625
625
 
626
626
  break
627
627
  case event.suite.before:
628
- this.emit(event.suite.before, deserializeSuite(message.data))
628
+ {
629
+ const suite = deserializeSuite(message.data)
630
+ this.emit(event.suite.before, suite)
631
+ event.dispatcher.emit(event.suite.before, suite)
632
+ }
633
+ break
634
+ case event.suite.after:
635
+ {
636
+ const suite = deserializeSuite(message.data)
637
+ this.emit(event.suite.after, suite)
638
+ event.dispatcher.emit(event.suite.after, suite)
639
+ }
629
640
  break
630
641
  case event.test.before:
631
- this.emit(event.test.before, deserializeTest(message.data))
642
+ {
643
+ const test = deserializeTest(message.data)
644
+ this.emit(event.test.before, test)
645
+ event.dispatcher.emit(event.test.before, test)
646
+ }
632
647
  break
633
648
  case event.test.started:
634
- this.emit(event.test.started, deserializeTest(message.data))
649
+ {
650
+ const test = deserializeTest(message.data)
651
+ this.emit(event.test.started, test)
652
+ event.dispatcher.emit(event.test.started, test)
653
+ }
635
654
  break
636
655
  case event.test.failed:
637
656
  // For hook failures, emit immediately as there won't be a test.finished event
@@ -645,7 +664,11 @@ class Workers extends EventEmitter {
645
664
  // Skip individual passed events - we'll emit based on finished state
646
665
  break
647
666
  case event.test.skipped:
648
- this.emit(event.test.skipped, deserializeTest(message.data))
667
+ {
668
+ const test = deserializeTest(message.data)
669
+ this.emit(event.test.skipped, test)
670
+ event.dispatcher.emit(event.test.skipped, test)
671
+ }
649
672
  break
650
673
  case event.test.finished:
651
674
  // Handle different types of test completion properly
@@ -674,28 +697,47 @@ class Workers extends EventEmitter {
674
697
  }
675
698
  }
676
699
 
677
- this.emit(event.test.finished, deserializeTest(data))
700
+ const test = deserializeTest(data)
701
+ this.emit(event.test.finished, test)
702
+ event.dispatcher.emit(event.test.finished, test)
678
703
  }
679
704
  break
680
705
  case event.test.after:
681
- this.emit(event.test.after, deserializeTest(message.data))
706
+ {
707
+ const test = deserializeTest(message.data)
708
+ this.emit(event.test.after, test)
709
+ event.dispatcher.emit(event.test.after, test)
710
+ }
682
711
  break
683
712
  case event.step.finished:
684
713
  this.emit(event.step.finished, message.data)
714
+ event.dispatcher.emit(event.step.finished, message.data)
685
715
  break
686
716
  case event.step.started:
687
717
  this.emit(event.step.started, message.data)
718
+ event.dispatcher.emit(event.step.started, message.data)
688
719
  break
689
720
  case event.step.passed:
690
721
  this.emit(event.step.passed, message.data)
722
+ event.dispatcher.emit(event.step.passed, message.data)
691
723
  break
692
724
  case event.step.failed:
693
725
  this.emit(event.step.failed, message.data, message.data.error)
726
+ event.dispatcher.emit(event.step.failed, message.data, message.data.error)
694
727
  break
695
728
  case event.hook.failed:
696
729
  // Hook failures are already reported as test failures by the worker
697
730
  // Just emit the hook.failed event for listeners
698
731
  this.emit(event.hook.failed, message.data)
732
+ event.dispatcher.emit(event.hook.failed, message.data)
733
+ break
734
+ case event.hook.passed:
735
+ this.emit(event.hook.passed, message.data)
736
+ event.dispatcher.emit(event.hook.passed, message.data)
737
+ break
738
+ case event.hook.finished:
739
+ this.emit(event.hook.finished, message.data)
740
+ event.dispatcher.emit(event.hook.finished, message.data)
699
741
  break
700
742
  }
701
743
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-rc.1",
3
+ "version": "4.0.0-rc.11",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -44,9 +44,10 @@
44
44
  "./store": "./lib/store.js"
45
45
  },
46
46
  "bin": {
47
- "codeceptjs": "./bin/codecept.js"
47
+ "codeceptjs": "./bin/codecept.js",
48
+ "codeceptjs-mcp": "./bin/mcp-server.js"
48
49
  },
49
- "repository": "Codeception/codeceptjs",
50
+ "repository": "codeceptjs/CodeceptJS",
50
51
  "scripts": {
51
52
  "test-server": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010 --read-only",
52
53
  "test-server:writable": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010",
@@ -90,6 +91,7 @@
90
91
  "@cucumber/cucumber-expressions": "18",
91
92
  "@cucumber/gherkin": "38.0.0",
92
93
  "@cucumber/messages": "32.0.1",
94
+ "@modelcontextprotocol/sdk": "^1.26.0",
93
95
  "@xmldom/xmldom": "0.9.8",
94
96
  "acorn": "8.15.0",
95
97
  "ai": "^6.0.43",
@@ -745,3 +745,22 @@ declare module 'codeceptjs/effects' {
745
745
  export const retryTo: RetryTo
746
746
  export const hopeThat: HopeThat
747
747
  }
748
+
749
+ declare module 'codeceptjs/steps' {
750
+ const step: {
751
+ opts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
752
+ timeout(timeout: number): CodeceptJS.StepConfig;
753
+ retry(retry: number): CodeceptJS.StepConfig;
754
+ stepOpts(opts: CodeceptJS.StepOptions): CodeceptJS.StepConfig;
755
+ stepTimeout(timeout: number): CodeceptJS.StepConfig;
756
+ stepRetry(retry: number): CodeceptJS.StepConfig;
757
+ section(name: string): any;
758
+ endSection(): any;
759
+ Section(name: string): any;
760
+ EndSection(): any;
761
+ Given(): any;
762
+ When(): any;
763
+ Then(): any;
764
+ }
765
+ export default step
766
+ }
@@ -1,110 +0,0 @@
1
- import event from '../event.js'
2
- import output from '../output.js'
3
- import Config from '../config.js'
4
- import { isNotSet } from '../utils.js'
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
- export default 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
- export { RETRY_PRIORITIES }
@@ -1,99 +0,0 @@
1
- import event from '../event.js'
2
- import recorder from '../recorder.js'
3
- import store from '../store.js'
4
- import output from '../output.js'
5
- import { RETRY_PRIORITIES } from '../retryCoordinator.js'
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
- export default 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
- }