codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.9.esm-aria
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 +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +105 -16
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +59 -47
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5534 -3764
- package/typings/types.d.ts +5789 -3775
|
@@ -121,6 +121,7 @@ export default function (config) {
|
|
|
121
121
|
event.dispatcher.on(event.test.before, test => {
|
|
122
122
|
// pass disableRetryFailedStep is a preferred way to disable retries
|
|
123
123
|
// test.disableRetryFailedStep is used for backward compatibility
|
|
124
|
+
if (!test.opts) test.opts = {}
|
|
124
125
|
if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
|
|
125
126
|
store.autoRetries = false
|
|
126
127
|
return // disable retry when a test is not active
|
package/lib/recorder.js
CHANGED
|
@@ -12,6 +12,7 @@ let running = false
|
|
|
12
12
|
let errFn
|
|
13
13
|
let queueId = 0
|
|
14
14
|
let sessionId = null
|
|
15
|
+
let sessionStack = [] // Stack to support nested sessions
|
|
15
16
|
let asyncErr = null
|
|
16
17
|
let ignoredErrs = []
|
|
17
18
|
|
|
@@ -90,6 +91,7 @@ export default {
|
|
|
90
91
|
if (promise && running) this.catch()
|
|
91
92
|
queueId++
|
|
92
93
|
sessionId = null
|
|
94
|
+
sessionStack = [] // Clear the session stack
|
|
93
95
|
asyncErr = null
|
|
94
96
|
output.log(`${currentQueue()} Starting recording promises`)
|
|
95
97
|
promise = Promise.resolve()
|
|
@@ -124,8 +126,13 @@ export default {
|
|
|
124
126
|
*/
|
|
125
127
|
start(name) {
|
|
126
128
|
if (sessionId) {
|
|
127
|
-
debug(`${currentQueue()}Session already started as ${sessionId}`)
|
|
128
|
-
|
|
129
|
+
debug(`${currentQueue()}Session already started as ${sessionId}, nesting session ${name}`)
|
|
130
|
+
// Push current session to stack instead of restoring it
|
|
131
|
+
sessionStack.push({
|
|
132
|
+
id: sessionId,
|
|
133
|
+
promise: promise,
|
|
134
|
+
running: this.running,
|
|
135
|
+
})
|
|
129
136
|
}
|
|
130
137
|
debug(`${currentQueue()}Starting <${name}> session`)
|
|
131
138
|
tasks.push('--->')
|
|
@@ -143,9 +150,18 @@ export default {
|
|
|
143
150
|
tasks.push('<---')
|
|
144
151
|
debug(`${currentQueue()}Finalize <${name}> session`)
|
|
145
152
|
this.running = false
|
|
146
|
-
sessionId = null
|
|
147
153
|
this.catch(errFn)
|
|
148
154
|
promise = promise.then(() => oldPromises.pop())
|
|
155
|
+
|
|
156
|
+
// Restore parent session from stack if available
|
|
157
|
+
if (sessionStack.length > 0) {
|
|
158
|
+
const parentSession = sessionStack.pop()
|
|
159
|
+
sessionId = parentSession.id
|
|
160
|
+
this.running = parentSession.running
|
|
161
|
+
debug(`${currentQueue()}Restored parent session <${sessionId}>`)
|
|
162
|
+
} else {
|
|
163
|
+
sessionId = null
|
|
164
|
+
}
|
|
149
165
|
},
|
|
150
166
|
|
|
151
167
|
/**
|
|
@@ -398,6 +414,15 @@ export default {
|
|
|
398
414
|
toString() {
|
|
399
415
|
return `Queue: ${currentQueue()}\n\nTasks: ${this.scheduled()}`
|
|
400
416
|
},
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get current session ID
|
|
420
|
+
* @return {string|null}
|
|
421
|
+
* @inner
|
|
422
|
+
*/
|
|
423
|
+
getCurrentSessionId() {
|
|
424
|
+
return sessionId
|
|
425
|
+
},
|
|
401
426
|
}
|
|
402
427
|
|
|
403
428
|
function getTimeoutPromise(timeoutMs, taskName) {
|
package/lib/result.js
CHANGED
|
@@ -3,22 +3,21 @@ import path from 'path'
|
|
|
3
3
|
import { serializeTest } from './mocha/test.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @property {number}
|
|
10
|
-
* @property {number}
|
|
11
|
-
* @property {number}
|
|
12
|
-
* @property {
|
|
13
|
-
* @property {
|
|
14
|
-
* @property {
|
|
15
|
-
|
|
16
|
-
|
|
6
|
+
* @typedef {Object} Stats Statistics for a test result.
|
|
7
|
+
* @property {number} passes Number of passed tests.
|
|
8
|
+
* @property {number} failures Number of failed tests.
|
|
9
|
+
* @property {number} tests Total number of tests.
|
|
10
|
+
* @property {number} pending Number of pending tests.
|
|
11
|
+
* @property {number} failedHooks Number of failed hooks.
|
|
12
|
+
* @property {Date} start Start time of the test run.
|
|
13
|
+
* @property {Date} end End time of the test run.
|
|
14
|
+
* @property {number} duration Duration of the test run, in milliseconds.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result of a test run. Will be emitted for example in "event.all.result" events.
|
|
17
19
|
*/
|
|
18
20
|
class Result {
|
|
19
|
-
/**
|
|
20
|
-
* Create Result of the test run
|
|
21
|
-
*/
|
|
22
21
|
constructor() {
|
|
23
22
|
this._startTime = new Date()
|
|
24
23
|
this._endTime = null
|
|
@@ -27,6 +26,9 @@ class Result {
|
|
|
27
26
|
this.start()
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resets all collected stats, tests, and failure reports.
|
|
31
|
+
*/
|
|
30
32
|
reset() {
|
|
31
33
|
this._stats = {
|
|
32
34
|
passes: 0,
|
|
@@ -39,43 +41,85 @@ class Result {
|
|
|
39
41
|
duration: 0,
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* @type {CodeceptJS.Test[]}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
43
48
|
this._tests = []
|
|
44
49
|
|
|
45
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* @type {string[][]}
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
46
54
|
this._failures = []
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Sets the start time to the current time.
|
|
59
|
+
*/
|
|
49
60
|
start() {
|
|
50
61
|
this._startTime = new Date()
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Sets the end time to the current time.
|
|
66
|
+
*/
|
|
53
67
|
finish() {
|
|
54
68
|
this._endTime = new Date()
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Whether this result contains any failed tests.
|
|
73
|
+
*
|
|
74
|
+
* @type {boolean}
|
|
75
|
+
* @readonly
|
|
76
|
+
*/
|
|
57
77
|
get hasFailed() {
|
|
58
78
|
return this._stats.failures > 0
|
|
59
79
|
}
|
|
60
80
|
|
|
81
|
+
/**
|
|
82
|
+
* All collected tests.
|
|
83
|
+
*
|
|
84
|
+
* @type {CodeceptJS.Test[]}
|
|
85
|
+
* @readonly
|
|
86
|
+
*/
|
|
61
87
|
get tests() {
|
|
62
88
|
return this._tests
|
|
63
89
|
}
|
|
64
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The failure reports (array of strings per failed test).
|
|
93
|
+
*
|
|
94
|
+
* @type {string[][]}
|
|
95
|
+
* @readonly
|
|
96
|
+
*/
|
|
65
97
|
get failures() {
|
|
66
98
|
return this._failures.filter(f => f && (!Array.isArray(f) || f.length > 0))
|
|
67
99
|
}
|
|
68
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The test statistics.
|
|
103
|
+
*
|
|
104
|
+
* @type {Stats}
|
|
105
|
+
* @readonly
|
|
106
|
+
*/
|
|
69
107
|
get stats() {
|
|
70
108
|
return this._stats
|
|
71
109
|
}
|
|
72
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The start time of the test run.
|
|
113
|
+
*
|
|
114
|
+
* @type {Date}
|
|
115
|
+
* @readonly
|
|
116
|
+
*/
|
|
73
117
|
get startTime() {
|
|
74
118
|
return this._startTime
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
78
|
-
*
|
|
122
|
+
* Adds a test to this result.
|
|
79
123
|
*
|
|
80
124
|
* @param {CodeceptJS.Test} test
|
|
81
125
|
*/
|
|
@@ -90,34 +134,67 @@ class Result {
|
|
|
90
134
|
}
|
|
91
135
|
|
|
92
136
|
/**
|
|
93
|
-
*
|
|
137
|
+
* Adds failure reports to this result.
|
|
94
138
|
*
|
|
95
|
-
* @param {
|
|
139
|
+
* @param {string[][]} newFailures
|
|
96
140
|
*/
|
|
97
141
|
addFailures(newFailures) {
|
|
98
142
|
this._failures.push(...newFailures)
|
|
99
143
|
}
|
|
100
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Whether this result contains any failed tests.
|
|
147
|
+
*
|
|
148
|
+
* @type {boolean}
|
|
149
|
+
* @readonly
|
|
150
|
+
*/
|
|
101
151
|
get hasFailures() {
|
|
102
152
|
return this.stats.failures > 0
|
|
103
153
|
}
|
|
104
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The duration of the test run, in milliseconds.
|
|
157
|
+
*
|
|
158
|
+
* @type {number}
|
|
159
|
+
* @readonly
|
|
160
|
+
*/
|
|
105
161
|
get duration() {
|
|
106
162
|
return this._endTime ? +this._endTime - +this._startTime : 0
|
|
107
163
|
}
|
|
108
164
|
|
|
165
|
+
/**
|
|
166
|
+
* All failed tests.
|
|
167
|
+
*
|
|
168
|
+
* @type {CodeceptJS.Test[]}
|
|
169
|
+
* readonly
|
|
170
|
+
*/
|
|
109
171
|
get failedTests() {
|
|
110
172
|
return this._tests.filter(test => test.state === 'failed')
|
|
111
173
|
}
|
|
112
174
|
|
|
175
|
+
/**
|
|
176
|
+
* All passed tests.
|
|
177
|
+
*
|
|
178
|
+
* @type {CodeceptJS.Test[]}
|
|
179
|
+
* @readonly
|
|
180
|
+
*/
|
|
113
181
|
get passedTests() {
|
|
114
182
|
return this._tests.filter(test => test.state === 'passed')
|
|
115
183
|
}
|
|
116
184
|
|
|
185
|
+
/**
|
|
186
|
+
* All skipped tests.
|
|
187
|
+
*
|
|
188
|
+
* @type {CodeceptJS.Test[]}
|
|
189
|
+
* @readonly
|
|
190
|
+
*/
|
|
117
191
|
get skippedTests() {
|
|
118
192
|
return this._tests.filter(test => test.state === 'skipped' || test.state === 'pending')
|
|
119
193
|
}
|
|
120
194
|
|
|
195
|
+
/**
|
|
196
|
+
* @returns {object} The JSON representation of this result.
|
|
197
|
+
*/
|
|
121
198
|
simplify() {
|
|
122
199
|
return {
|
|
123
200
|
hasFailed: this.hasFailed,
|
|
@@ -129,9 +206,9 @@ class Result {
|
|
|
129
206
|
}
|
|
130
207
|
|
|
131
208
|
/**
|
|
132
|
-
*
|
|
209
|
+
* Saves this result to a JSON file.
|
|
133
210
|
*
|
|
134
|
-
* @param {string} fileName
|
|
211
|
+
* @param {string} [fileName] Path to the JSON file, relative to `output_dir`. Defaults to "result.json".
|
|
135
212
|
*/
|
|
136
213
|
save(fileName) {
|
|
137
214
|
if (!fileName) fileName = 'result.json'
|
|
@@ -139,9 +216,9 @@ class Result {
|
|
|
139
216
|
}
|
|
140
217
|
|
|
141
218
|
/**
|
|
142
|
-
*
|
|
219
|
+
* Adds stats to this result.
|
|
143
220
|
*
|
|
144
|
-
* @param {
|
|
221
|
+
* @param {Partial<Stats>} [newStats]
|
|
145
222
|
*/
|
|
146
223
|
addStats(newStats = {}) {
|
|
147
224
|
this._stats.passes += newStats.passes || 0
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import output from './output.js'
|
|
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
|
+
export {
|
|
200
|
+
RETRY_PRIORITIES,
|
|
201
|
+
RETRY_TYPES,
|
|
202
|
+
registerRetry,
|
|
203
|
+
getEffectiveRetryConfig,
|
|
204
|
+
generateRetrySummary,
|
|
205
|
+
validateConfig,
|
|
206
|
+
reset,
|
|
207
|
+
}
|
package/lib/step/base.js
CHANGED
|
@@ -225,7 +225,7 @@ class Step {
|
|
|
225
225
|
processingStep = this
|
|
226
226
|
|
|
227
227
|
while (processingStep.metaStep) {
|
|
228
|
-
if (processingStep.metaStep.actor?.match(/^(Given|When|Then|And)/)) {
|
|
228
|
+
if (processingStep.metaStep.actor?.match(/^(Given|When|Then|And|But)/)) {
|
|
229
229
|
hasBDD = true
|
|
230
230
|
break
|
|
231
231
|
} else {
|
package/lib/step/comment.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import FuncStep from './func.js'
|
|
2
2
|
|
|
3
3
|
class CommentStep extends FuncStep {
|
|
4
4
|
constructor(name, comment) {
|
|
@@ -7,4 +7,4 @@ class CommentStep extends FuncStep {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
export default CommentStep
|
package/lib/step/meta.js
CHANGED
|
@@ -15,7 +15,7 @@ class MetaStep extends Step {
|
|
|
15
15
|
|
|
16
16
|
/** @return {boolean} */
|
|
17
17
|
isBDD() {
|
|
18
|
-
if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And)/)) {
|
|
18
|
+
if (this.actor && this.actor.match && this.actor.match(/^(Given|When|Then|And|But)/)) {
|
|
19
19
|
return true
|
|
20
20
|
}
|
|
21
21
|
return false
|
package/lib/template/heal.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default (html, extraPrompt = '', rootLocator = null) => [
|
|
2
|
+
{
|
|
3
|
+
role: 'user',
|
|
4
|
+
content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
|
|
5
|
+
Here is an sample page object:
|
|
6
|
+
|
|
7
|
+
const { I } = inject();
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
|
|
11
|
+
// setting locators
|
|
12
|
+
element1: '#selector',
|
|
13
|
+
element2: '.selector',
|
|
14
|
+
element3: locate().withText('text'),
|
|
15
|
+
|
|
16
|
+
// setting methods
|
|
17
|
+
doSomethingOnPage(params) {
|
|
18
|
+
// ...
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
I want to generate a Page Object for the page I provide.
|
|
23
|
+
Write JavaScript code in similar manner to list all locators on the page.
|
|
24
|
+
Use locators in order of preference: by text (use locate().withText()), label, CSS, XPath.
|
|
25
|
+
Avoid TailwindCSS, Bootstrap or React style formatting classes in locators.
|
|
26
|
+
Add methods to interact with page when needed.
|
|
27
|
+
${extraPrompt}
|
|
28
|
+
${rootLocator ? `All provided elements are inside '${rootLocator}'. Declare it as root variable and for every locator use locate(...).inside(root)` : ''}
|
|
29
|
+
Add only locators from this HTML: \n\n${html}`,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default (html, { step, error, prevSteps }) => {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
role: 'user',
|
|
5
|
+
content: `As a test automation engineer I am testing web application using CodeceptJS.
|
|
6
|
+
I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
|
|
7
|
+
Propose how to adjust ${step.toCode()} step to fix the test.
|
|
8
|
+
Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
|
|
9
|
+
Here is the error message: ${error.message}
|
|
10
|
+
Here is HTML code of a page where the failure has happened: \n\n${html}`,
|
|
11
|
+
},
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default (html, input) => [
|
|
2
|
+
{
|
|
3
|
+
role: 'user',
|
|
4
|
+
content: `I am test engineer writing test in CodeceptJS
|
|
5
|
+
I have opened web page and I want to use CodeceptJS to ${input} on this page
|
|
6
|
+
Provide me valid CodeceptJS code to accomplish it
|
|
7
|
+
Use only locators from this HTML: \n\n${html}`,
|
|
8
|
+
},
|
|
9
|
+
]
|