codeceptjs 4.0.0-rc.1 → 4.0.0-rc.10
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/README.md +39 -27
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/codecept.js +20 -17
- package/lib/command/init.js +0 -3
- package/lib/command/run-workers.js +1 -0
- package/lib/container.js +19 -4
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/Playwright.js +219 -137
- package/lib/helper/Puppeteer.js +207 -69
- package/lib/helper/WebDriver.js +179 -64
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/mocha/cli.js +10 -0
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +1 -1
- package/lib/utils.js +48 -0
- package/lib/workers.js +49 -7
- package/package.json +5 -3
- package/typings/index.d.ts +19 -0
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/step/config.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
30
|
+
* @param {StepOptions} opts - The options for the step.
|
|
18
31
|
* @returns {StepConfig} - The step configuration object.
|
|
19
32
|
*/
|
|
20
33
|
opts(opts) {
|
package/lib/step/record.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "4.0.0-rc.10",
|
|
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": "
|
|
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",
|
package/typings/index.d.ts
CHANGED
|
@@ -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
|
-
}
|