codeceptjs 3.7.0-beta.1 → 3.7.0-beta.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 +9 -10
- package/bin/codecept.js +7 -0
- package/lib/actor.js +46 -92
- package/lib/ai.js +130 -121
- package/lib/codecept.js +2 -2
- package/lib/command/check.js +186 -0
- package/lib/command/definitions.js +3 -1
- package/lib/command/interactive.js +1 -1
- package/lib/command/run-workers.js +2 -54
- package/lib/command/workers/runTests.js +64 -225
- package/lib/container.js +27 -0
- package/lib/effects.js +218 -0
- package/lib/els.js +87 -106
- package/lib/event.js +18 -17
- package/lib/heal.js +10 -0
- package/lib/helper/AI.js +2 -1
- package/lib/helper/Appium.js +31 -22
- package/lib/helper/Playwright.js +22 -1
- package/lib/helper/Puppeteer.js +5 -0
- package/lib/helper/WebDriver.js +29 -8
- package/lib/listener/emptyRun.js +2 -5
- package/lib/listener/exit.js +5 -8
- package/lib/listener/globalTimeout.js +66 -10
- package/lib/listener/result.js +12 -0
- package/lib/listener/steps.js +3 -6
- package/lib/listener/store.js +9 -1
- package/lib/mocha/asyncWrapper.js +15 -3
- package/lib/mocha/cli.js +79 -28
- package/lib/mocha/featureConfig.js +13 -0
- package/lib/mocha/hooks.js +32 -3
- package/lib/mocha/inject.js +5 -0
- package/lib/mocha/scenarioConfig.js +11 -0
- package/lib/mocha/suite.js +27 -1
- package/lib/mocha/test.js +102 -3
- package/lib/mocha/types.d.ts +11 -0
- package/lib/output.js +75 -73
- package/lib/pause.js +3 -10
- package/lib/plugin/analyze.js +349 -0
- package/lib/plugin/autoDelay.js +2 -2
- package/lib/plugin/commentStep.js +5 -0
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/heal.js +30 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryTo.js +18 -118
- package/lib/plugin/screenshotOnFail.js +12 -17
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +6 -5
- package/lib/plugin/stepTimeout.js +1 -1
- package/lib/plugin/tryTo.js +17 -107
- package/lib/recorder.js +5 -5
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/step/base.js +228 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +20 -347
- package/lib/steps.js +50 -0
- package/lib/store.js +4 -0
- package/lib/timeout.js +66 -0
- package/lib/utils.js +93 -0
- package/lib/within.js +2 -2
- package/lib/workers.js +29 -49
- package/package.json +23 -20
- package/typings/index.d.ts +5 -4
- package/typings/promiseBasedTypes.d.ts +617 -7
- package/typings/types.d.ts +663 -34
- package/lib/listener/artifacts.js +0 -19
- package/lib/plugin/debugErrors.js +0 -67
package/lib/container.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const glob = require('glob')
|
|
2
2
|
const path = require('path')
|
|
3
|
+
const debug = require('debug')('codeceptjs:container')
|
|
3
4
|
const { MetaStep } = require('./step')
|
|
4
5
|
const { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally } = require('./utils')
|
|
5
6
|
const Translation = require('./translation')
|
|
@@ -8,6 +9,7 @@ const recorder = require('./recorder')
|
|
|
8
9
|
const event = require('./event')
|
|
9
10
|
const WorkerStorage = require('./workerStorage')
|
|
10
11
|
const store = require('./store')
|
|
12
|
+
const Result = require('./result')
|
|
11
13
|
const ai = require('./ai')
|
|
12
14
|
|
|
13
15
|
let asyncHelperPromise
|
|
@@ -24,6 +26,8 @@ let container = {
|
|
|
24
26
|
*/
|
|
25
27
|
mocha: {},
|
|
26
28
|
translation: {},
|
|
29
|
+
/** @type {Result | null} */
|
|
30
|
+
result: null,
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -38,6 +42,7 @@ class Container {
|
|
|
38
42
|
* @param {*} opts
|
|
39
43
|
*/
|
|
40
44
|
static create(config, opts) {
|
|
45
|
+
debug('creating container')
|
|
41
46
|
asyncHelperPromise = Promise.resolve()
|
|
42
47
|
|
|
43
48
|
// dynamically create mocha instance
|
|
@@ -52,6 +57,7 @@ class Container {
|
|
|
52
57
|
container.translation = loadTranslation(config.translation || null, config.vocabularies || [])
|
|
53
58
|
container.proxySupport = createSupportObjects(config.include || {})
|
|
54
59
|
container.plugins = createPlugins(config.plugins || {}, opts)
|
|
60
|
+
container.result = new Result()
|
|
55
61
|
|
|
56
62
|
createActor(config.include?.I)
|
|
57
63
|
|
|
@@ -125,6 +131,18 @@ class Container {
|
|
|
125
131
|
return container.mocha
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Get result
|
|
136
|
+
*
|
|
137
|
+
* @returns {Result}
|
|
138
|
+
*/
|
|
139
|
+
static result() {
|
|
140
|
+
if (!container.result) {
|
|
141
|
+
container.result = new Result()
|
|
142
|
+
}
|
|
143
|
+
return container.result
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
/**
|
|
129
147
|
* Append new services to container
|
|
130
148
|
*
|
|
@@ -134,6 +152,7 @@ class Container {
|
|
|
134
152
|
static append(newContainer) {
|
|
135
153
|
const deepMerge = require('./utils').deepMerge
|
|
136
154
|
container = deepMerge(container, newContainer)
|
|
155
|
+
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
@@ -150,6 +169,7 @@ class Container {
|
|
|
150
169
|
container.plugins = newPlugins || {}
|
|
151
170
|
asyncHelperPromise = Promise.resolve()
|
|
152
171
|
store.actor = null
|
|
172
|
+
debug('container cleared')
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
/**
|
|
@@ -185,6 +205,8 @@ class Container {
|
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
207
|
|
|
208
|
+
Container.STANDARD_ACTING_HELPERS = ['Playwright', 'WebDriver', 'Puppeteer', 'Appium', 'TestCafe']
|
|
209
|
+
|
|
188
210
|
module.exports = Container
|
|
189
211
|
|
|
190
212
|
function createHelpers(config) {
|
|
@@ -216,6 +238,8 @@ function createHelpers(config) {
|
|
|
216
238
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
217
239
|
}
|
|
218
240
|
|
|
241
|
+
debug(`helper ${helperName} async initialized`)
|
|
242
|
+
|
|
219
243
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
220
244
|
})
|
|
221
245
|
|
|
@@ -225,6 +249,7 @@ function createHelpers(config) {
|
|
|
225
249
|
checkHelperRequirements(HelperClass)
|
|
226
250
|
|
|
227
251
|
helpers[helperName] = new HelperClass(config[helperName])
|
|
252
|
+
debug(`helper ${helperName} initialized`)
|
|
228
253
|
} catch (err) {
|
|
229
254
|
throw new Error(`Could not load helper ${helperName} (${err.message})`)
|
|
230
255
|
}
|
|
@@ -322,6 +347,7 @@ function createSupportObjects(config) {
|
|
|
322
347
|
if (container.support[name]._init) {
|
|
323
348
|
container.support[name]._init()
|
|
324
349
|
}
|
|
350
|
+
debug(`support object ${name} initialized`)
|
|
325
351
|
} catch (err) {
|
|
326
352
|
throw new Error(`Initialization failed for ${name}: ${container.support[name]}\n${err.message}\n${err.stack}`)
|
|
327
353
|
}
|
|
@@ -334,6 +360,7 @@ function createSupportObjects(config) {
|
|
|
334
360
|
const ms = new MetaStep(name, prop)
|
|
335
361
|
ms.setContext(currentObject)
|
|
336
362
|
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
363
|
+
debug(`metastep is created for ${name}.${prop.toString()}()`)
|
|
337
364
|
return ms.run.bind(ms, currentValue)
|
|
338
365
|
}
|
|
339
366
|
|
package/lib/effects.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const recorder = require('./recorder')
|
|
2
|
+
const { debug } = require('./output')
|
|
3
|
+
const store = require('./store')
|
|
4
|
+
const event = require('./event')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A utility function for CodeceptJS tests that acts as a soft assertion.
|
|
8
|
+
* Executes a callback within a recorded session, ensuring errors are handled gracefully without failing the test immediately.
|
|
9
|
+
*
|
|
10
|
+
* @async
|
|
11
|
+
* @function hopeThat
|
|
12
|
+
* @param {Function} callback - The callback function containing the logic to validate.
|
|
13
|
+
* This function should perform the desired assertion or condition check.
|
|
14
|
+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the assertion or condition was successful,
|
|
15
|
+
* or `false` if an error occurred.
|
|
16
|
+
*
|
|
17
|
+
* @description
|
|
18
|
+
* - Designed for use in CodeceptJS tests as a "soft assertion."
|
|
19
|
+
* Unlike standard assertions, it does not stop the test execution on failure.
|
|
20
|
+
* - Starts a new recorder session named 'hopeThat' and manages state restoration.
|
|
21
|
+
* - Logs errors and attaches them as notes to the test, enabling post-test reporting of soft assertion failures.
|
|
22
|
+
* - Resets the `store.hopeThat` flag after the execution, ensuring clean state for subsequent operations.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const { hopeThat } = require('codeceptjs/effects')
|
|
26
|
+
* await hopeThat(() => {
|
|
27
|
+
* I.see('Welcome'); // Perform a soft assertion
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
|
|
31
|
+
*/
|
|
32
|
+
async function hopeThat(callback) {
|
|
33
|
+
if (store.dryRun) return
|
|
34
|
+
const sessionName = 'hopeThat'
|
|
35
|
+
|
|
36
|
+
let result = false
|
|
37
|
+
return recorder.add(
|
|
38
|
+
'hopeThat',
|
|
39
|
+
() => {
|
|
40
|
+
recorder.session.start(sessionName)
|
|
41
|
+
store.hopeThat = true
|
|
42
|
+
callback()
|
|
43
|
+
recorder.add(() => {
|
|
44
|
+
result = true
|
|
45
|
+
recorder.session.restore(sessionName)
|
|
46
|
+
return result
|
|
47
|
+
})
|
|
48
|
+
recorder.session.catch(err => {
|
|
49
|
+
result = false
|
|
50
|
+
const msg = err.inspect ? err.inspect() : err.toString()
|
|
51
|
+
debug(`Unsuccessful assertion > ${msg}`)
|
|
52
|
+
event.dispatcher.once(event.test.finished, test => {
|
|
53
|
+
test.notes.push({ type: 'conditionalError', text: msg })
|
|
54
|
+
})
|
|
55
|
+
recorder.session.restore(sessionName)
|
|
56
|
+
return result
|
|
57
|
+
})
|
|
58
|
+
return recorder.add(
|
|
59
|
+
'result',
|
|
60
|
+
() => {
|
|
61
|
+
store.hopeThat = undefined
|
|
62
|
+
return result
|
|
63
|
+
},
|
|
64
|
+
true,
|
|
65
|
+
false,
|
|
66
|
+
)
|
|
67
|
+
},
|
|
68
|
+
false,
|
|
69
|
+
false,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
|
|
75
|
+
*
|
|
76
|
+
* @async
|
|
77
|
+
* @function retryTo
|
|
78
|
+
* @param {Function} callback - The function to execute, which will be retried upon failure.
|
|
79
|
+
* Receives the current retry count as an argument.
|
|
80
|
+
* @param {number} maxTries - The maximum number of attempts to retry the callback.
|
|
81
|
+
* @param {number} [pollInterval=200] - The delay (in milliseconds) between retry attempts.
|
|
82
|
+
* @returns {Promise<void|any>} A promise that resolves when the callback executes successfully, or rejects after reaching the maximum retries.
|
|
83
|
+
*
|
|
84
|
+
* @description
|
|
85
|
+
* - This function is designed for use in CodeceptJS tests to handle intermittent or flaky test steps.
|
|
86
|
+
* - Starts a new recorder session for each retry attempt, ensuring proper state management and error handling.
|
|
87
|
+
* - Logs errors and retries the callback until it either succeeds or the maximum number of attempts is reached.
|
|
88
|
+
* - Restores the session state after each attempt, whether successful or not.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const { hopeThat } = require('codeceptjs/effects')
|
|
92
|
+
* await retryTo((tries) => {
|
|
93
|
+
* if (tries < 3) {
|
|
94
|
+
* I.see('Non-existent element'); // Simulates a failure
|
|
95
|
+
* } else {
|
|
96
|
+
* I.see('Welcome'); // Succeeds on the 3rd attempt
|
|
97
|
+
* }
|
|
98
|
+
* }, 5, 300); // Retry up to 5 times, with a 300ms interval
|
|
99
|
+
*
|
|
100
|
+
* @throws Will reject with the last error encountered if the maximum retries are exceeded.
|
|
101
|
+
*/
|
|
102
|
+
async function retryTo(callback, maxTries, pollInterval = 200) {
|
|
103
|
+
const sessionName = 'retryTo'
|
|
104
|
+
|
|
105
|
+
return new Promise((done, reject) => {
|
|
106
|
+
let tries = 1
|
|
107
|
+
|
|
108
|
+
function handleRetryException(err) {
|
|
109
|
+
recorder.throw(err)
|
|
110
|
+
reject(err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tryBlock = async () => {
|
|
114
|
+
tries++
|
|
115
|
+
recorder.session.start(`${sessionName} ${tries}`)
|
|
116
|
+
try {
|
|
117
|
+
await callback(tries)
|
|
118
|
+
} catch (err) {
|
|
119
|
+
handleRetryException(err)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Call done if no errors
|
|
123
|
+
recorder.add(() => {
|
|
124
|
+
recorder.session.restore(`${sessionName} ${tries}`)
|
|
125
|
+
done(null)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Catch errors and retry
|
|
129
|
+
recorder.session.catch(err => {
|
|
130
|
+
recorder.session.restore(`${sessionName} ${tries}`)
|
|
131
|
+
if (tries <= maxTries) {
|
|
132
|
+
debug(`Error ${err}... Retrying`)
|
|
133
|
+
recorder.add(`${sessionName} ${tries}`, () => setTimeout(tryBlock, pollInterval))
|
|
134
|
+
} else {
|
|
135
|
+
// if maxTries reached
|
|
136
|
+
handleRetryException(err)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
recorder.add(sessionName, tryBlock).catch(err => {
|
|
142
|
+
console.error('An error occurred:', err)
|
|
143
|
+
done(null)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A CodeceptJS utility function to attempt a step or callback without failing the test.
|
|
150
|
+
* If the step fails, the test continues execution without interruption, and the result is logged.
|
|
151
|
+
*
|
|
152
|
+
* @async
|
|
153
|
+
* @function tryTo
|
|
154
|
+
* @param {Function} callback - The function to execute, which may succeed or fail.
|
|
155
|
+
* This function contains the logic to be attempted.
|
|
156
|
+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the step succeeds, or `false` if it fails.
|
|
157
|
+
*
|
|
158
|
+
* @description
|
|
159
|
+
* - Useful for scenarios where certain steps are optional or their failure should not interrupt the test flow.
|
|
160
|
+
* - Starts a new recorder session named 'tryTo' for isolation and error handling.
|
|
161
|
+
* - Captures errors during execution and logs them for debugging purposes.
|
|
162
|
+
* - Ensures the `store.tryTo` flag is reset after execution to maintain a clean state.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* const { tryTo } = require('codeceptjs/effects')
|
|
166
|
+
* const wasSuccessful = await tryTo(() => {
|
|
167
|
+
* I.see('Welcome'); // Attempt to find an element on the page
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* if (!wasSuccessful) {
|
|
171
|
+
* I.say('Optional step failed, but test continues.');
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* @throws Will handle errors internally, logging them and returning `false` as the result.
|
|
175
|
+
*/
|
|
176
|
+
async function tryTo(callback) {
|
|
177
|
+
if (store.dryRun) return
|
|
178
|
+
const sessionName = 'tryTo'
|
|
179
|
+
|
|
180
|
+
let result = false
|
|
181
|
+
return recorder.add(
|
|
182
|
+
sessionName,
|
|
183
|
+
() => {
|
|
184
|
+
recorder.session.start(sessionName)
|
|
185
|
+
store.tryTo = true
|
|
186
|
+
callback()
|
|
187
|
+
recorder.add(() => {
|
|
188
|
+
result = true
|
|
189
|
+
recorder.session.restore(sessionName)
|
|
190
|
+
return result
|
|
191
|
+
})
|
|
192
|
+
recorder.session.catch(err => {
|
|
193
|
+
result = false
|
|
194
|
+
const msg = err.inspect ? err.inspect() : err.toString()
|
|
195
|
+
debug(`Unsuccessful try > ${msg}`)
|
|
196
|
+
recorder.session.restore(sessionName)
|
|
197
|
+
return result
|
|
198
|
+
})
|
|
199
|
+
return recorder.add(
|
|
200
|
+
'result',
|
|
201
|
+
() => {
|
|
202
|
+
store.tryTo = undefined
|
|
203
|
+
return result
|
|
204
|
+
},
|
|
205
|
+
true,
|
|
206
|
+
false,
|
|
207
|
+
)
|
|
208
|
+
},
|
|
209
|
+
false,
|
|
210
|
+
false,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
hopeThat,
|
|
216
|
+
retryTo,
|
|
217
|
+
tryTo,
|
|
218
|
+
}
|
package/lib/els.js
CHANGED
|
@@ -1,115 +1,124 @@
|
|
|
1
|
-
const output = require('./output')
|
|
2
|
-
const store = require('./store')
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const { truth } = require('./assert/truth')
|
|
8
|
-
const { isAsyncFunction, humanizeFunction } = require('./utils')
|
|
1
|
+
const output = require('./output')
|
|
2
|
+
const store = require('./store')
|
|
3
|
+
const container = require('./container')
|
|
4
|
+
const StepConfig = require('./step/config')
|
|
5
|
+
const recordStep = require('./step/record')
|
|
6
|
+
const FuncStep = require('./step/func')
|
|
7
|
+
const { truth } = require('./assert/truth')
|
|
8
|
+
const { isAsyncFunction, humanizeFunction } = require('./utils')
|
|
9
9
|
|
|
10
10
|
function element(purpose, locator, fn) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
purpose = 'first element';
|
|
11
|
+
let stepConfig
|
|
12
|
+
if (arguments[arguments.length - 1] instanceof StepConfig) {
|
|
13
|
+
stepConfig = arguments[arguments.length - 1]
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (!fn || fn === stepConfig) {
|
|
17
|
+
fn = locator
|
|
18
|
+
locator = purpose
|
|
19
|
+
purpose = 'first element'
|
|
20
|
+
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
output.debug(`Found ${els.length} elements, using first element`);
|
|
22
|
+
const step = prepareStep(purpose, locator, fn)
|
|
23
|
+
if (!step) return
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
return executeStep(
|
|
26
|
+
step,
|
|
27
|
+
async () => {
|
|
28
|
+
const els = await step.helper._locate(locator)
|
|
29
|
+
output.debug(`Found ${els.length} elements, using first element`)
|
|
30
|
+
|
|
31
|
+
return fn(els[0])
|
|
32
|
+
},
|
|
33
|
+
stepConfig,
|
|
34
|
+
)
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function eachElement(purpose, locator, fn) {
|
|
29
38
|
if (!fn) {
|
|
30
|
-
fn = locator
|
|
31
|
-
locator = purpose
|
|
32
|
-
purpose = 'for each element'
|
|
39
|
+
fn = locator
|
|
40
|
+
locator = purpose
|
|
41
|
+
purpose = 'for each element'
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
const step = prepareStep(purpose, locator, fn)
|
|
36
|
-
if (!step) return
|
|
44
|
+
const step = prepareStep(purpose, locator, fn)
|
|
45
|
+
if (!step) return
|
|
37
46
|
|
|
38
47
|
return executeStep(step, async () => {
|
|
39
|
-
const els = await step.helper._locate(locator)
|
|
40
|
-
output.debug(`Found ${els.length} elements for each elements to iterate`)
|
|
48
|
+
const els = await step.helper._locate(locator)
|
|
49
|
+
output.debug(`Found ${els.length} elements for each elements to iterate`)
|
|
41
50
|
|
|
42
|
-
const errs = []
|
|
43
|
-
let i = 0
|
|
51
|
+
const errs = []
|
|
52
|
+
let i = 0
|
|
44
53
|
for (const el of els) {
|
|
45
54
|
try {
|
|
46
|
-
await fn(el, i)
|
|
55
|
+
await fn(el, i)
|
|
47
56
|
} catch (err) {
|
|
48
|
-
output.error(`eachElement: failed operation on element #${i} ${el}`)
|
|
49
|
-
errs.push(err)
|
|
57
|
+
output.error(`eachElement: failed operation on element #${i} ${el}`)
|
|
58
|
+
errs.push(err)
|
|
50
59
|
}
|
|
51
|
-
i
|
|
60
|
+
i++
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
if (errs.length) {
|
|
55
|
-
throw errs[0]
|
|
64
|
+
throw errs[0]
|
|
56
65
|
}
|
|
57
|
-
})
|
|
66
|
+
})
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
function expectElement(locator, fn) {
|
|
61
|
-
const step = prepareStep('expect element to be', locator, fn)
|
|
62
|
-
if (!step) return
|
|
70
|
+
const step = prepareStep('expect element to be', locator, fn)
|
|
71
|
+
if (!step) return
|
|
63
72
|
|
|
64
73
|
return executeStep(step, async () => {
|
|
65
|
-
const els = await step.helper._locate(locator)
|
|
66
|
-
output.debug(`Found ${els.length} elements, first will be used for assertion`)
|
|
74
|
+
const els = await step.helper._locate(locator)
|
|
75
|
+
output.debug(`Found ${els.length} elements, first will be used for assertion`)
|
|
67
76
|
|
|
68
|
-
const result = await fn(els[0])
|
|
69
|
-
const assertion = truth(`element (${locator})`, fn.toString())
|
|
70
|
-
assertion.assert(result)
|
|
71
|
-
})
|
|
77
|
+
const result = await fn(els[0])
|
|
78
|
+
const assertion = truth(`element (${locator})`, fn.toString())
|
|
79
|
+
assertion.assert(result)
|
|
80
|
+
})
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
function expectAnyElement(locator, fn) {
|
|
75
|
-
const step = prepareStep('expect any element to be', locator, fn)
|
|
76
|
-
if (!step) return
|
|
84
|
+
const step = prepareStep('expect any element to be', locator, fn)
|
|
85
|
+
if (!step) return
|
|
77
86
|
|
|
78
87
|
return executeStep(step, async () => {
|
|
79
|
-
const els = await step.helper._locate(locator)
|
|
80
|
-
output.debug(`Found ${els.length} elements, at least one should pass the assertion`)
|
|
88
|
+
const els = await step.helper._locate(locator)
|
|
89
|
+
output.debug(`Found ${els.length} elements, at least one should pass the assertion`)
|
|
81
90
|
|
|
82
|
-
const assertion = truth(`any element of (${locator})`, fn.toString())
|
|
91
|
+
const assertion = truth(`any element of (${locator})`, fn.toString())
|
|
83
92
|
|
|
84
|
-
let found = false
|
|
93
|
+
let found = false
|
|
85
94
|
for (const el of els) {
|
|
86
|
-
const result = await fn(el)
|
|
95
|
+
const result = await fn(el)
|
|
87
96
|
if (result) {
|
|
88
|
-
found = true
|
|
89
|
-
break
|
|
97
|
+
found = true
|
|
98
|
+
break
|
|
90
99
|
}
|
|
91
100
|
}
|
|
92
|
-
if (!found) throw assertion.getException()
|
|
93
|
-
})
|
|
101
|
+
if (!found) throw assertion.getException()
|
|
102
|
+
})
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
function expectAllElements(locator, fn) {
|
|
97
|
-
const step = prepareStep('expect all elements', locator, fn)
|
|
98
|
-
if (!step) return
|
|
106
|
+
const step = prepareStep('expect all elements', locator, fn)
|
|
107
|
+
if (!step) return
|
|
99
108
|
|
|
100
109
|
return executeStep(step, async () => {
|
|
101
|
-
const els = await step.helper._locate(locator)
|
|
102
|
-
output.debug(`Found ${els.length} elements, all should pass the assertion`)
|
|
110
|
+
const els = await step.helper._locate(locator)
|
|
111
|
+
output.debug(`Found ${els.length} elements, all should pass the assertion`)
|
|
103
112
|
|
|
104
|
-
let i = 1
|
|
113
|
+
let i = 1
|
|
105
114
|
for (const el of els) {
|
|
106
|
-
output.debug(`checking element #${i}: ${el}`)
|
|
107
|
-
const result = await fn(el)
|
|
108
|
-
const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
|
|
109
|
-
assertion.assert(result)
|
|
110
|
-
i
|
|
115
|
+
output.debug(`checking element #${i}: ${el}`)
|
|
116
|
+
const result = await fn(el)
|
|
117
|
+
const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
|
|
118
|
+
assertion.assert(result)
|
|
119
|
+
i++
|
|
111
120
|
}
|
|
112
|
-
})
|
|
121
|
+
})
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
module.exports = {
|
|
@@ -118,60 +127,32 @@ module.exports = {
|
|
|
118
127
|
expectElement,
|
|
119
128
|
expectAnyElement,
|
|
120
129
|
expectAllElements,
|
|
121
|
-
}
|
|
130
|
+
}
|
|
122
131
|
|
|
123
132
|
function prepareStep(purpose, locator, fn) {
|
|
124
|
-
if (store.dryRun) return
|
|
125
|
-
const helpers = Object.values(container.helpers())
|
|
133
|
+
if (store.dryRun) return
|
|
134
|
+
const helpers = Object.values(container.helpers())
|
|
126
135
|
|
|
127
|
-
const helper = helpers.filter(h => !!h._locate)[0]
|
|
136
|
+
const helper = helpers.filter(h => !!h._locate)[0]
|
|
128
137
|
|
|
129
138
|
if (!helper) {
|
|
130
|
-
throw new Error('No helper enabled with _locate method with returns a list of elements.')
|
|
139
|
+
throw new Error('No helper enabled with _locate method with returns a list of elements.')
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
if (!isAsyncFunction(fn)) {
|
|
134
|
-
throw new Error('Async function should be passed into each element')
|
|
143
|
+
throw new Error('Async function should be passed into each element')
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
const isAssertion = purpose.startsWith('expect')
|
|
146
|
+
const isAssertion = purpose.startsWith('expect')
|
|
138
147
|
|
|
139
|
-
const step = new
|
|
140
|
-
step.
|
|
141
|
-
step.setArguments([humanizeFunction(fn)])
|
|
142
|
-
step.helperMethod = '_locate';
|
|
148
|
+
const step = new FuncStep(`${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`)
|
|
149
|
+
step.setHelper(helper)
|
|
150
|
+
step.setArguments([humanizeFunction(fn)]) // user defined function is a passed argument
|
|
143
151
|
|
|
144
|
-
return step
|
|
152
|
+
return step
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
async function executeStep(step, action) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
event.emit(event.step.started, step);
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
await action();
|
|
154
|
-
} catch (err) {
|
|
155
|
-
recorder.throw(err);
|
|
156
|
-
event.emit(event.step.failed, step, err);
|
|
157
|
-
event.emit(event.step.finished, step);
|
|
158
|
-
// event.emit(event.step.after, step)
|
|
159
|
-
error = err;
|
|
160
|
-
// await recorder.promise();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
event.emit(event.step.after, step);
|
|
165
|
-
event.emit(event.step.passed, step);
|
|
166
|
-
event.emit(event.step.finished, step);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// await recorder.promise();
|
|
170
|
-
|
|
171
|
-
// if (error) {
|
|
172
|
-
// console.log('error', error.inspect())
|
|
173
|
-
// return recorder.throw(error);
|
|
174
|
-
// }
|
|
175
|
-
|
|
176
|
-
return promise;
|
|
155
|
+
async function executeStep(step, action, stepConfig = {}) {
|
|
156
|
+
step.setCallable(action)
|
|
157
|
+
return recordStep(step, [stepConfig])
|
|
177
158
|
}
|
package/lib/event.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const debug = require('debug')('codeceptjs:event')
|
|
2
|
-
const events = require('events')
|
|
3
|
-
const { error } = require('./output')
|
|
1
|
+
const debug = require('debug')('codeceptjs:event')
|
|
2
|
+
const events = require('events')
|
|
3
|
+
const { error } = require('./output')
|
|
4
4
|
|
|
5
|
-
const dispatcher = new events.EventEmitter()
|
|
5
|
+
const dispatcher = new events.EventEmitter()
|
|
6
6
|
|
|
7
|
-
dispatcher.setMaxListeners(50)
|
|
7
|
+
dispatcher.setMaxListeners(50)
|
|
8
8
|
/**
|
|
9
9
|
* @namespace
|
|
10
10
|
* @alias event
|
|
@@ -59,6 +59,7 @@ module.exports = {
|
|
|
59
59
|
started: 'hook.start',
|
|
60
60
|
passed: 'hook.passed',
|
|
61
61
|
failed: 'hook.failed',
|
|
62
|
+
finished: 'hook.finished',
|
|
62
63
|
},
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -141,33 +142,33 @@ module.exports = {
|
|
|
141
142
|
* @param {*} [param]
|
|
142
143
|
*/
|
|
143
144
|
emit(event, param) {
|
|
144
|
-
let msg = `Emitted | ${event}
|
|
145
|
+
let msg = `Emitted | ${event}`
|
|
145
146
|
if (param && param.toString()) {
|
|
146
|
-
msg += ` (${param.toString()})
|
|
147
|
+
msg += ` (${param.toString()})`
|
|
147
148
|
}
|
|
148
|
-
debug(msg)
|
|
149
|
+
debug(msg)
|
|
149
150
|
try {
|
|
150
|
-
this.dispatcher.emit.apply(this.dispatcher, arguments)
|
|
151
|
+
this.dispatcher.emit.apply(this.dispatcher, arguments)
|
|
151
152
|
} catch (err) {
|
|
152
|
-
error(`Error processing ${event} event:`)
|
|
153
|
-
error(err.stack)
|
|
153
|
+
error(`Error processing ${event} event:`)
|
|
154
|
+
error(err.stack)
|
|
154
155
|
}
|
|
155
156
|
},
|
|
156
157
|
|
|
157
158
|
/** for testing only! */
|
|
158
159
|
cleanDispatcher: () => {
|
|
159
|
-
let event
|
|
160
|
+
let event
|
|
160
161
|
for (event in this.test) {
|
|
161
|
-
this.dispatcher.removeAllListeners(this.test[event])
|
|
162
|
+
this.dispatcher.removeAllListeners(this.test[event])
|
|
162
163
|
}
|
|
163
164
|
for (event in this.suite) {
|
|
164
|
-
this.dispatcher.removeAllListeners(this.test[event])
|
|
165
|
+
this.dispatcher.removeAllListeners(this.test[event])
|
|
165
166
|
}
|
|
166
167
|
for (event in this.step) {
|
|
167
|
-
this.dispatcher.removeAllListeners(this.test[event])
|
|
168
|
+
this.dispatcher.removeAllListeners(this.test[event])
|
|
168
169
|
}
|
|
169
170
|
for (event in this.all) {
|
|
170
|
-
this.dispatcher.removeAllListeners(this.test[event])
|
|
171
|
+
this.dispatcher.removeAllListeners(this.test[event])
|
|
171
172
|
}
|
|
172
173
|
},
|
|
173
|
-
}
|
|
174
|
+
}
|
package/lib/heal.js
CHANGED
|
@@ -129,6 +129,16 @@ class Heal {
|
|
|
129
129
|
snippet: codeSnippet,
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
+
if (failureContext?.test) {
|
|
133
|
+
const test = failureContext.test
|
|
134
|
+
let note = `This test was healed by '${suggestion.name}'`
|
|
135
|
+
note += `\n\nReplace the failed code:\n\n`
|
|
136
|
+
note += colors.red(`- ${failedStep.toCode()}\n`)
|
|
137
|
+
note += colors.green(`+ ${codeSnippet}\n`)
|
|
138
|
+
test.addNote('heal', note)
|
|
139
|
+
test.meta.healed = true
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
133
143
|
this.numHealed++
|
|
134
144
|
// recorder.session.restore();
|