codeceptjs 3.7.0-beta.4 → 3.7.0-beta.6
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 +47 -92
- package/lib/command/check.js +173 -0
- package/lib/command/definitions.js +2 -0
- package/lib/command/run-workers.js +1 -1
- package/lib/command/workers/runTests.js +112 -109
- package/lib/container.js +9 -0
- package/lib/effects.js +218 -0
- package/lib/heal.js +10 -0
- package/lib/helper/Appium.js +27 -16
- package/lib/helper/Playwright.js +15 -0
- package/lib/helper/Puppeteer.js +5 -0
- package/lib/helper/WebDriver.js +9 -1
- package/lib/listener/emptyRun.js +2 -5
- package/lib/listener/globalTimeout.js +15 -3
- package/lib/listener/steps.js +3 -0
- package/lib/mocha/cli.js +22 -5
- package/lib/mocha/featureConfig.js +13 -0
- package/lib/mocha/scenarioConfig.js +11 -0
- package/lib/mocha/test.js +15 -0
- package/lib/mocha/types.d.ts +6 -0
- package/lib/output.js +74 -73
- package/lib/pause.js +3 -7
- package/lib/plugin/heal.js +30 -0
- package/lib/plugin/retryTo.js +18 -126
- package/lib/plugin/stepTimeout.js +1 -1
- package/lib/plugin/tryTo.js +13 -111
- package/lib/recorder.js +1 -1
- package/lib/step/base.js +180 -0
- package/lib/step/config.js +50 -0
- package/lib/step/helper.js +47 -0
- package/lib/step/meta.js +91 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/timeout.js +42 -0
- package/lib/step.js +15 -348
- package/lib/steps.js +23 -0
- package/lib/store.js +2 -0
- package/lib/utils.js +58 -0
- package/lib/within.js +2 -2
- package/lib/workers.js +2 -12
- package/package.json +9 -7
- package/typings/index.d.ts +5 -4
- package/typings/promiseBasedTypes.d.ts +520 -6
- package/typings/types.d.ts +562 -44
- package/lib/step/section.js +0 -25
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
const tty = require('tty')
|
|
1
|
+
const tty = require('tty')
|
|
2
2
|
|
|
3
3
|
if (!tty.getWindowSize) {
|
|
4
4
|
// this is really old method, long removed from Node, but Mocha
|
|
5
5
|
// reporters fall back on it if they cannot use `process.stdout.getWindowSize`
|
|
6
6
|
// we need to polyfill it.
|
|
7
|
-
tty.getWindowSize = () => [40, 80]
|
|
7
|
+
tty.getWindowSize = () => [40, 80]
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const { parentPort, workerData } = require('worker_threads')
|
|
11
|
-
const event = require('../../event')
|
|
12
|
-
const container = require('../../container')
|
|
13
|
-
const { getConfig } = require('../utils')
|
|
14
|
-
const { tryOrDefault, deepMerge } = require('../../utils')
|
|
10
|
+
const { parentPort, workerData } = require('worker_threads')
|
|
11
|
+
const event = require('../../event')
|
|
12
|
+
const container = require('../../container')
|
|
13
|
+
const { getConfig } = require('../utils')
|
|
14
|
+
const { tryOrDefault, deepMerge } = require('../../utils')
|
|
15
15
|
|
|
16
|
-
let stdout = ''
|
|
16
|
+
let stdout = ''
|
|
17
17
|
|
|
18
|
-
const stderr = ''
|
|
18
|
+
const stderr = ''
|
|
19
19
|
|
|
20
20
|
// Requiring of Codecept need to be after tty.getWindowSize is available.
|
|
21
|
-
const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')
|
|
21
|
+
const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')
|
|
22
22
|
|
|
23
|
-
const { options, tests, testRoot, workerIndex } = workerData
|
|
23
|
+
const { options, tests, testRoot, workerIndex } = workerData
|
|
24
24
|
|
|
25
25
|
// hide worker output
|
|
26
26
|
if (!options.debug && !options.verbose)
|
|
27
27
|
process.stdout.write = string => {
|
|
28
|
-
stdout += string
|
|
29
|
-
return true
|
|
30
|
-
}
|
|
28
|
+
stdout += string
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
32
|
+
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
33
33
|
|
|
34
34
|
// important deep merge so dynamic things e.g. functions on config are not overridden
|
|
35
|
-
const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs)
|
|
35
|
+
const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs)
|
|
36
36
|
|
|
37
37
|
// Load test and run
|
|
38
|
-
const codecept = new Codecept(config, options)
|
|
39
|
-
codecept.init(testRoot)
|
|
40
|
-
codecept.loadTests()
|
|
41
|
-
const mocha = container.mocha()
|
|
42
|
-
filterTests()
|
|
38
|
+
const codecept = new Codecept(config, options)
|
|
39
|
+
codecept.init(testRoot)
|
|
40
|
+
codecept.loadTests()
|
|
41
|
+
const mocha = container.mocha()
|
|
42
|
+
filterTests()
|
|
43
43
|
|
|
44
44
|
// run tests
|
|
45
|
-
(async function () {
|
|
45
|
+
;(async function () {
|
|
46
46
|
if (mocha.suite.total()) {
|
|
47
|
-
await runTests()
|
|
47
|
+
await runTests()
|
|
48
48
|
}
|
|
49
|
-
})()
|
|
49
|
+
})()
|
|
50
50
|
|
|
51
51
|
async function runTests() {
|
|
52
52
|
try {
|
|
53
|
-
await codecept.bootstrap()
|
|
53
|
+
await codecept.bootstrap()
|
|
54
54
|
} catch (err) {
|
|
55
|
-
throw new Error(`Error while running bootstrap file :${err}`)
|
|
55
|
+
throw new Error(`Error while running bootstrap file :${err}`)
|
|
56
56
|
}
|
|
57
|
-
listenToParentThread()
|
|
58
|
-
initializeListeners()
|
|
59
|
-
disablePause()
|
|
57
|
+
listenToParentThread()
|
|
58
|
+
initializeListeners()
|
|
59
|
+
disablePause()
|
|
60
60
|
try {
|
|
61
|
-
await codecept.run()
|
|
61
|
+
await codecept.run()
|
|
62
62
|
} finally {
|
|
63
|
-
await codecept.teardown()
|
|
63
|
+
await codecept.teardown()
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function filterTests() {
|
|
68
|
-
const files = codecept.testFiles
|
|
69
|
-
mocha.files = files
|
|
70
|
-
mocha.loadFiles()
|
|
68
|
+
const files = codecept.testFiles
|
|
69
|
+
mocha.files = files
|
|
70
|
+
mocha.loadFiles()
|
|
71
71
|
|
|
72
72
|
for (const suite of mocha.suite.suites) {
|
|
73
|
-
suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
|
|
73
|
+
suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function initializeListeners() {
|
|
78
78
|
function simplifyError(error) {
|
|
79
79
|
if (error) {
|
|
80
|
-
const { stack, uncaught, message, actual, expected } = error
|
|
80
|
+
const { stack, uncaught, message, actual, expected } = error
|
|
81
81
|
|
|
82
82
|
return {
|
|
83
83
|
stack,
|
|
@@ -85,36 +85,36 @@ function initializeListeners() {
|
|
|
85
85
|
message,
|
|
86
86
|
actual,
|
|
87
87
|
expected,
|
|
88
|
-
}
|
|
88
|
+
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
return null
|
|
91
|
+
return null
|
|
92
92
|
}
|
|
93
93
|
function simplifyTest(test, err = null) {
|
|
94
|
-
test = { ...test }
|
|
94
|
+
test = { ...test }
|
|
95
95
|
|
|
96
96
|
if (test.start && !test.duration) {
|
|
97
|
-
const end = new Date()
|
|
98
|
-
test.duration = end - test.start
|
|
97
|
+
const end = new Date()
|
|
98
|
+
test.duration = end - test.start
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (test.err) {
|
|
102
|
-
err = simplifyError(test.err)
|
|
103
|
-
test.status = 'failed'
|
|
102
|
+
err = simplifyError(test.err)
|
|
103
|
+
test.status = 'failed'
|
|
104
104
|
} else if (err) {
|
|
105
|
-
err = simplifyError(err)
|
|
106
|
-
test.status = 'failed'
|
|
105
|
+
err = simplifyError(err)
|
|
106
|
+
test.status = 'failed'
|
|
107
107
|
}
|
|
108
|
-
const parent = {}
|
|
108
|
+
const parent = {}
|
|
109
109
|
if (test.parent) {
|
|
110
|
-
parent.title = test.parent.title
|
|
110
|
+
parent.title = test.parent.title
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (test.opts) {
|
|
114
114
|
Object.keys(test.opts).forEach(k => {
|
|
115
|
-
if (typeof test.opts[k] === 'object') delete test.opts[k]
|
|
116
|
-
if (typeof test.opts[k] === 'function') delete test.opts[k]
|
|
117
|
-
})
|
|
115
|
+
if (typeof test.opts[k] === 'object') delete test.opts[k]
|
|
116
|
+
if (typeof test.opts[k] === 'function') delete test.opts[k]
|
|
117
|
+
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
return {
|
|
@@ -125,30 +125,33 @@ function initializeListeners() {
|
|
|
125
125
|
retries: test._retries,
|
|
126
126
|
title: test.title,
|
|
127
127
|
status: test.status,
|
|
128
|
+
notes: test.notes || [],
|
|
129
|
+
meta: test.meta || {},
|
|
130
|
+
artifacts: test.artifacts || [],
|
|
128
131
|
duration: test.duration || 0,
|
|
129
132
|
err,
|
|
130
133
|
parent,
|
|
131
134
|
steps: test.steps && test.steps.length > 0 ? simplifyStepsInTestObject(test.steps, err) : [],
|
|
132
|
-
}
|
|
135
|
+
}
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
function simplifyStepsInTestObject(steps, err) {
|
|
136
|
-
steps = [...steps]
|
|
137
|
-
const _steps = []
|
|
139
|
+
steps = [...steps]
|
|
140
|
+
const _steps = []
|
|
138
141
|
|
|
139
142
|
for (step of steps) {
|
|
140
|
-
const _args = []
|
|
143
|
+
const _args = []
|
|
141
144
|
|
|
142
145
|
if (step.args) {
|
|
143
146
|
for (const arg of step.args) {
|
|
144
147
|
// check if arg is a JOI object
|
|
145
148
|
if (arg && arg.$_root) {
|
|
146
|
-
_args.push(JSON.stringify(arg).slice(0, 300))
|
|
149
|
+
_args.push(JSON.stringify(arg).slice(0, 300))
|
|
147
150
|
// check if arg is a function
|
|
148
151
|
} else if (arg && typeof arg === 'function') {
|
|
149
|
-
_args.push(arg.name)
|
|
152
|
+
_args.push(arg.name)
|
|
150
153
|
} else {
|
|
151
|
-
_args.push(arg)
|
|
154
|
+
_args.push(arg)
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
}
|
|
@@ -164,38 +167,38 @@ function initializeListeners() {
|
|
|
164
167
|
finishedAt: step.finishedAt,
|
|
165
168
|
duration: step.duration,
|
|
166
169
|
err,
|
|
167
|
-
})
|
|
170
|
+
})
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
return _steps
|
|
173
|
+
return _steps
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
function simplifyStep(step, err = null) {
|
|
174
|
-
step = { ...step }
|
|
177
|
+
step = { ...step }
|
|
175
178
|
|
|
176
179
|
if (step.startTime && !step.duration) {
|
|
177
|
-
const end = new Date()
|
|
178
|
-
step.duration = end - step.startTime
|
|
180
|
+
const end = new Date()
|
|
181
|
+
step.duration = end - step.startTime
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
if (step.err) {
|
|
182
|
-
err = simplifyError(step.err)
|
|
183
|
-
step.status = 'failed'
|
|
185
|
+
err = simplifyError(step.err)
|
|
186
|
+
step.status = 'failed'
|
|
184
187
|
} else if (err) {
|
|
185
|
-
err = simplifyError(err)
|
|
186
|
-
step.status = 'failed'
|
|
188
|
+
err = simplifyError(err)
|
|
189
|
+
step.status = 'failed'
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
const parent = {}
|
|
192
|
+
const parent = {}
|
|
190
193
|
if (step.metaStep) {
|
|
191
|
-
parent.title = step.metaStep.actor
|
|
194
|
+
parent.title = step.metaStep.actor
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
if (step.opts) {
|
|
195
198
|
Object.keys(step.opts).forEach(k => {
|
|
196
|
-
if (typeof step.opts[k] === 'object') delete step.opts[k]
|
|
197
|
-
if (typeof step.opts[k] === 'function') delete step.opts[k]
|
|
198
|
-
})
|
|
199
|
+
if (typeof step.opts[k] === 'object') delete step.opts[k]
|
|
200
|
+
if (typeof step.opts[k] === 'function') delete step.opts[k]
|
|
201
|
+
})
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
return {
|
|
@@ -207,43 +210,43 @@ function initializeListeners() {
|
|
|
207
210
|
err,
|
|
208
211
|
parent,
|
|
209
212
|
test: simplifyTest(step.test),
|
|
210
|
-
}
|
|
213
|
+
}
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
collectStats()
|
|
216
|
+
collectStats()
|
|
214
217
|
// suite
|
|
215
|
-
event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: simplifyTest(suite) }))
|
|
216
|
-
event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: simplifyTest(suite) }))
|
|
218
|
+
event.dispatcher.on(event.suite.before, suite => sendToParentThread({ event: event.suite.before, workerIndex, data: simplifyTest(suite) }))
|
|
219
|
+
event.dispatcher.on(event.suite.after, suite => sendToParentThread({ event: event.suite.after, workerIndex, data: simplifyTest(suite) }))
|
|
217
220
|
|
|
218
221
|
// calculate duration
|
|
219
|
-
event.dispatcher.on(event.test.started, test => (test.start = new Date()))
|
|
222
|
+
event.dispatcher.on(event.test.started, test => (test.start = new Date()))
|
|
220
223
|
|
|
221
224
|
// tests
|
|
222
|
-
event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: simplifyTest(test) }))
|
|
223
|
-
event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: simplifyTest(test) }))
|
|
225
|
+
event.dispatcher.on(event.test.before, test => sendToParentThread({ event: event.test.before, workerIndex, data: simplifyTest(test) }))
|
|
226
|
+
event.dispatcher.on(event.test.after, test => sendToParentThread({ event: event.test.after, workerIndex, data: simplifyTest(test) }))
|
|
224
227
|
// we should force-send correct errors to prevent race condition
|
|
225
|
-
event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: simplifyTest(test, err) }))
|
|
226
|
-
event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: simplifyTest(test, err) }))
|
|
227
|
-
event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: simplifyTest(test, err) }))
|
|
228
|
-
event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: simplifyTest(test) }))
|
|
229
|
-
event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: simplifyTest(test) }))
|
|
228
|
+
event.dispatcher.on(event.test.finished, (test, err) => sendToParentThread({ event: event.test.finished, workerIndex, data: simplifyTest(test, err) }))
|
|
229
|
+
event.dispatcher.on(event.test.failed, (test, err) => sendToParentThread({ event: event.test.failed, workerIndex, data: simplifyTest(test, err) }))
|
|
230
|
+
event.dispatcher.on(event.test.passed, (test, err) => sendToParentThread({ event: event.test.passed, workerIndex, data: simplifyTest(test, err) }))
|
|
231
|
+
event.dispatcher.on(event.test.started, test => sendToParentThread({ event: event.test.started, workerIndex, data: simplifyTest(test) }))
|
|
232
|
+
event.dispatcher.on(event.test.skipped, test => sendToParentThread({ event: event.test.skipped, workerIndex, data: simplifyTest(test) }))
|
|
230
233
|
|
|
231
234
|
// steps
|
|
232
|
-
event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: simplifyStep(step) }))
|
|
233
|
-
event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: simplifyStep(step) }))
|
|
234
|
-
event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: simplifyStep(step) }))
|
|
235
|
-
event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: simplifyStep(step) }))
|
|
235
|
+
event.dispatcher.on(event.step.finished, step => sendToParentThread({ event: event.step.finished, workerIndex, data: simplifyStep(step) }))
|
|
236
|
+
event.dispatcher.on(event.step.started, step => sendToParentThread({ event: event.step.started, workerIndex, data: simplifyStep(step) }))
|
|
237
|
+
event.dispatcher.on(event.step.passed, step => sendToParentThread({ event: event.step.passed, workerIndex, data: simplifyStep(step) }))
|
|
238
|
+
event.dispatcher.on(event.step.failed, step => sendToParentThread({ event: event.step.failed, workerIndex, data: simplifyStep(step) }))
|
|
236
239
|
|
|
237
|
-
event.dispatcher.on(event.hook.failed, (test, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: simplifyTest(test, err) }))
|
|
238
|
-
event.dispatcher.on(event.hook.passed, (test, err) => sendToParentThread({ event: event.hook.passed, workerIndex, data: simplifyTest(test, err) }))
|
|
239
|
-
event.dispatcher.on(event.all.failures, data => sendToParentThread({ event: event.all.failures, workerIndex, data }))
|
|
240
|
+
event.dispatcher.on(event.hook.failed, (test, err) => sendToParentThread({ event: event.hook.failed, workerIndex, data: simplifyTest(test, err) }))
|
|
241
|
+
event.dispatcher.on(event.hook.passed, (test, err) => sendToParentThread({ event: event.hook.passed, workerIndex, data: simplifyTest(test, err) }))
|
|
242
|
+
event.dispatcher.on(event.all.failures, data => sendToParentThread({ event: event.all.failures, workerIndex, data }))
|
|
240
243
|
|
|
241
244
|
// all
|
|
242
|
-
event.dispatcher.once(event.all.result, () => parentPort.close())
|
|
245
|
+
event.dispatcher.once(event.all.result, () => parentPort.close())
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
function disablePause() {
|
|
246
|
-
global.pause = () => {}
|
|
249
|
+
global.pause = () => {}
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
function collectStats() {
|
|
@@ -253,36 +256,36 @@ function collectStats() {
|
|
|
253
256
|
skipped: 0,
|
|
254
257
|
tests: 0,
|
|
255
258
|
pending: 0,
|
|
256
|
-
}
|
|
259
|
+
}
|
|
257
260
|
event.dispatcher.on(event.test.skipped, () => {
|
|
258
|
-
stats.skipped
|
|
259
|
-
})
|
|
261
|
+
stats.skipped++
|
|
262
|
+
})
|
|
260
263
|
event.dispatcher.on(event.test.passed, () => {
|
|
261
|
-
stats.passes
|
|
262
|
-
})
|
|
264
|
+
stats.passes++
|
|
265
|
+
})
|
|
263
266
|
event.dispatcher.on(event.test.failed, test => {
|
|
264
267
|
if (test.ctx._runnable.title.includes('hook: AfterSuite')) {
|
|
265
|
-
stats.failedHooks += 1
|
|
268
|
+
stats.failedHooks += 1
|
|
266
269
|
}
|
|
267
|
-
stats.failures
|
|
268
|
-
})
|
|
270
|
+
stats.failures++
|
|
271
|
+
})
|
|
269
272
|
event.dispatcher.on(event.test.skipped, () => {
|
|
270
|
-
stats.pending
|
|
271
|
-
})
|
|
273
|
+
stats.pending++
|
|
274
|
+
})
|
|
272
275
|
event.dispatcher.on(event.test.finished, () => {
|
|
273
|
-
stats.tests
|
|
274
|
-
})
|
|
276
|
+
stats.tests++
|
|
277
|
+
})
|
|
275
278
|
event.dispatcher.once(event.all.after, () => {
|
|
276
|
-
sendToParentThread({ event: event.all.after, data: stats })
|
|
277
|
-
})
|
|
279
|
+
sendToParentThread({ event: event.all.after, data: stats })
|
|
280
|
+
})
|
|
278
281
|
}
|
|
279
282
|
|
|
280
283
|
function sendToParentThread(data) {
|
|
281
|
-
parentPort.postMessage(data)
|
|
284
|
+
parentPort.postMessage(data)
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
function listenToParentThread() {
|
|
285
288
|
parentPort.on('message', eventData => {
|
|
286
|
-
container.append({ support: eventData.data })
|
|
287
|
-
})
|
|
289
|
+
container.append({ support: eventData.data })
|
|
290
|
+
})
|
|
288
291
|
}
|
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')
|
|
@@ -38,6 +39,7 @@ class Container {
|
|
|
38
39
|
* @param {*} opts
|
|
39
40
|
*/
|
|
40
41
|
static create(config, opts) {
|
|
42
|
+
debug('creating container')
|
|
41
43
|
asyncHelperPromise = Promise.resolve()
|
|
42
44
|
|
|
43
45
|
// dynamically create mocha instance
|
|
@@ -134,6 +136,7 @@ class Container {
|
|
|
134
136
|
static append(newContainer) {
|
|
135
137
|
const deepMerge = require('./utils').deepMerge
|
|
136
138
|
container = deepMerge(container, newContainer)
|
|
139
|
+
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
/**
|
|
@@ -150,6 +153,7 @@ class Container {
|
|
|
150
153
|
container.plugins = newPlugins || {}
|
|
151
154
|
asyncHelperPromise = Promise.resolve()
|
|
152
155
|
store.actor = null
|
|
156
|
+
debug('container cleared')
|
|
153
157
|
}
|
|
154
158
|
|
|
155
159
|
/**
|
|
@@ -216,6 +220,8 @@ function createHelpers(config) {
|
|
|
216
220
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
debug(`helper ${helperName} async initialized`)
|
|
224
|
+
|
|
219
225
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
220
226
|
})
|
|
221
227
|
|
|
@@ -225,6 +231,7 @@ function createHelpers(config) {
|
|
|
225
231
|
checkHelperRequirements(HelperClass)
|
|
226
232
|
|
|
227
233
|
helpers[helperName] = new HelperClass(config[helperName])
|
|
234
|
+
debug(`helper ${helperName} initialized`)
|
|
228
235
|
} catch (err) {
|
|
229
236
|
throw new Error(`Could not load helper ${helperName} (${err.message})`)
|
|
230
237
|
}
|
|
@@ -322,6 +329,7 @@ function createSupportObjects(config) {
|
|
|
322
329
|
if (container.support[name]._init) {
|
|
323
330
|
container.support[name]._init()
|
|
324
331
|
}
|
|
332
|
+
debug(`support object ${name} initialized`)
|
|
325
333
|
} catch (err) {
|
|
326
334
|
throw new Error(`Initialization failed for ${name}: ${container.support[name]}\n${err.message}\n${err.stack}`)
|
|
327
335
|
}
|
|
@@ -334,6 +342,7 @@ function createSupportObjects(config) {
|
|
|
334
342
|
const ms = new MetaStep(name, prop)
|
|
335
343
|
ms.setContext(currentObject)
|
|
336
344
|
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
345
|
+
debug(`metastep is created for ${name}.${prop.toString()}()`)
|
|
337
346
|
return ms.run.bind(ms, currentValue)
|
|
338
347
|
}
|
|
339
348
|
|
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/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();
|