codeceptjs 3.6.10 → 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 +89 -119
- package/bin/codecept.js +9 -2
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +66 -102
- package/lib/ai.js +130 -121
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +87 -83
- package/lib/command/check.js +186 -0
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +8 -26
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +10 -8
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +1 -3
- package/lib/command/init.js +8 -12
- package/lib/command/interactive.js +2 -2
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +5 -57
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +68 -232
- package/lib/container.js +354 -237
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/effects.js +218 -0
- package/lib/els.js +158 -0
- package/lib/event.js +19 -17
- package/lib/heal.js +88 -80
- package/lib/helper/AI.js +2 -1
- package/lib/helper/ApiDataFactory.js +3 -6
- package/lib/helper/Appium.js +45 -51
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +3 -3
- package/lib/helper/JSONResponse.js +57 -37
- package/lib/helper/Nightmare.js +35 -53
- package/lib/helper/Playwright.js +211 -252
- package/lib/helper/Protractor.js +54 -77
- package/lib/helper/Puppeteer.js +139 -232
- package/lib/helper/REST.js +5 -17
- package/lib/helper/TestCafe.js +21 -44
- package/lib/helper/WebDriver.js +131 -169
- package/lib/helper/testcafe/testcafe-utils.js +26 -27
- package/lib/listener/emptyRun.js +55 -0
- package/lib/listener/exit.js +7 -10
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/globalTimeout.js +165 -0
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/result.js +12 -0
- package/lib/listener/steps.js +20 -18
- package/lib/listener/store.js +20 -0
- package/lib/mocha/asyncWrapper.js +216 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +308 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +24 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +112 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +29 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +21 -6
- package/lib/mocha/suite.js +81 -0
- package/lib/mocha/test.js +159 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +219 -0
- package/lib/output.js +82 -62
- package/lib/pause.js +155 -138
- package/lib/plugin/analyze.js +349 -0
- package/lib/plugin/autoDelay.js +6 -6
- package/lib/plugin/autoLogin.js +6 -7
- package/lib/plugin/commentStep.js +6 -1
- package/lib/plugin/coverage.js +10 -19
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +36 -9
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryFailedStep.js +4 -4
- package/lib/plugin/retryTo.js +18 -118
- package/lib/plugin/screenshotOnFail.js +17 -49
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +56 -17
- package/lib/plugin/stepTimeout.js +5 -12
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +17 -107
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +146 -125
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/secret.js +1 -1
- 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 +21 -332
- package/lib/steps.js +50 -0
- package/lib/store.js +10 -2
- package/lib/template/heal.js +2 -11
- package/lib/timeout.js +66 -0
- package/lib/utils.js +317 -216
- package/lib/within.js +73 -55
- package/lib/workers.js +259 -275
- package/package.json +56 -54
- package/typings/index.d.ts +175 -186
- package/typings/promiseBasedTypes.d.ts +164 -17
- package/typings/types.d.ts +284 -115
- package/lib/cli.js +0 -256
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -113
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const promiseRetry = require('promise-retry')
|
|
2
|
+
const event = require('../event')
|
|
3
|
+
const recorder = require('../recorder')
|
|
4
|
+
const assertThrown = require('../assert/throws')
|
|
5
|
+
const { ucfirst, isAsyncFunction } = require('../utils')
|
|
6
|
+
const { getInjectedArguments } = require('./inject')
|
|
7
|
+
const { fireHook } = require('./hooks')
|
|
8
|
+
|
|
9
|
+
const injectHook = function (inject, suite) {
|
|
10
|
+
try {
|
|
11
|
+
inject()
|
|
12
|
+
} catch (err) {
|
|
13
|
+
recorder.throw(err)
|
|
14
|
+
}
|
|
15
|
+
recorder.catch(err => {
|
|
16
|
+
suiteTestFailedHookError(suite, err)
|
|
17
|
+
throw err
|
|
18
|
+
})
|
|
19
|
+
return recorder.promise()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function suiteTestFailedHookError(suite, err, hookName) {
|
|
23
|
+
suite.eachTest(test => {
|
|
24
|
+
test.err = err
|
|
25
|
+
event.emit(event.test.failed, test, err, ucfirst(hookName))
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeDoneCallableOnce(done) {
|
|
30
|
+
let called = false
|
|
31
|
+
return function (err) {
|
|
32
|
+
if (called) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
called = true
|
|
36
|
+
return done(err)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wraps test function, injects support objects from container,
|
|
42
|
+
* starts promise chain with recorder, performs before/after hooks
|
|
43
|
+
* through event system.
|
|
44
|
+
*/
|
|
45
|
+
module.exports.test = test => {
|
|
46
|
+
const testFn = test.fn
|
|
47
|
+
if (!testFn) {
|
|
48
|
+
return test
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test.timeout(0)
|
|
52
|
+
test.async = true
|
|
53
|
+
|
|
54
|
+
test.fn = function (done) {
|
|
55
|
+
const doneFn = makeDoneCallableOnce(done)
|
|
56
|
+
recorder.errHandler(err => {
|
|
57
|
+
recorder.session.start('teardown')
|
|
58
|
+
recorder.cleanAsyncErr()
|
|
59
|
+
if (test.throws) {
|
|
60
|
+
// check that test should actually fail
|
|
61
|
+
try {
|
|
62
|
+
assertThrown(err, test.throws)
|
|
63
|
+
event.emit(event.test.passed, test)
|
|
64
|
+
event.emit(event.test.finished, test)
|
|
65
|
+
recorder.add(doneFn)
|
|
66
|
+
return
|
|
67
|
+
} catch (newErr) {
|
|
68
|
+
err = newErr
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
test.err = err
|
|
72
|
+
event.emit(event.test.failed, test, err)
|
|
73
|
+
event.emit(event.test.finished, test)
|
|
74
|
+
recorder.add(() => doneFn(err))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (isAsyncFunction(testFn)) {
|
|
78
|
+
event.emit(event.test.started, test)
|
|
79
|
+
testFn
|
|
80
|
+
.call(test, getInjectedArguments(testFn, test))
|
|
81
|
+
.then(() => {
|
|
82
|
+
recorder.add('fire test.passed', () => {
|
|
83
|
+
event.emit(event.test.passed, test)
|
|
84
|
+
event.emit(event.test.finished, test)
|
|
85
|
+
})
|
|
86
|
+
recorder.add('finish test', doneFn)
|
|
87
|
+
})
|
|
88
|
+
.catch(err => {
|
|
89
|
+
recorder.throw(err)
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
recorder.catch()
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
event.emit(event.test.started, test)
|
|
99
|
+
testFn.call(test, getInjectedArguments(testFn, test))
|
|
100
|
+
} catch (err) {
|
|
101
|
+
recorder.throw(err)
|
|
102
|
+
} finally {
|
|
103
|
+
recorder.add('fire test.passed', () => {
|
|
104
|
+
event.emit(event.test.passed, test)
|
|
105
|
+
event.emit(event.test.finished, test)
|
|
106
|
+
})
|
|
107
|
+
recorder.add('finish test', doneFn)
|
|
108
|
+
recorder.catch()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return test
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Injects arguments to function from controller
|
|
116
|
+
*/
|
|
117
|
+
module.exports.injected = function (fn, suite, hookName) {
|
|
118
|
+
return function (done) {
|
|
119
|
+
const doneFn = makeDoneCallableOnce(done)
|
|
120
|
+
const errHandler = err => {
|
|
121
|
+
recorder.session.start('teardown')
|
|
122
|
+
recorder.cleanAsyncErr()
|
|
123
|
+
if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName)
|
|
124
|
+
if (hookName === 'after') event.emit(event.test.after, suite)
|
|
125
|
+
if (hookName === 'afterSuite') event.emit(event.suite.after, suite)
|
|
126
|
+
recorder.add(() => doneFn(err))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
recorder.errHandler(err => {
|
|
130
|
+
errHandler(err)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (!fn) throw new Error('fn is not defined')
|
|
134
|
+
|
|
135
|
+
fireHook(event.hook.started, suite)
|
|
136
|
+
|
|
137
|
+
this.test.body = fn.toString()
|
|
138
|
+
|
|
139
|
+
if (!recorder.isRunning()) {
|
|
140
|
+
recorder.errHandler(err => {
|
|
141
|
+
errHandler(err)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const opts = suite.opts || {}
|
|
146
|
+
const retries = opts[`retry${ucfirst(hookName)}`] || 0
|
|
147
|
+
|
|
148
|
+
const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
|
|
149
|
+
|
|
150
|
+
promiseRetry(
|
|
151
|
+
async (retry, number) => {
|
|
152
|
+
try {
|
|
153
|
+
recorder.startUnlessRunning()
|
|
154
|
+
await fn.call(this, { ...getInjectedArguments(fn), suite, test: currentTest })
|
|
155
|
+
await recorder.promise().catch(err => retry(err))
|
|
156
|
+
} catch (err) {
|
|
157
|
+
retry(err)
|
|
158
|
+
} finally {
|
|
159
|
+
if (number < retries) {
|
|
160
|
+
recorder.stop()
|
|
161
|
+
recorder.start()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
{ retries },
|
|
166
|
+
)
|
|
167
|
+
.then(() => {
|
|
168
|
+
recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite))
|
|
169
|
+
recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
|
|
170
|
+
recorder.add(`finish ${hookName} hook`, doneFn)
|
|
171
|
+
recorder.catch()
|
|
172
|
+
})
|
|
173
|
+
.catch(e => {
|
|
174
|
+
recorder.throw(e)
|
|
175
|
+
recorder.catch(e => {
|
|
176
|
+
const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr()
|
|
177
|
+
errHandler(err)
|
|
178
|
+
})
|
|
179
|
+
recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e))
|
|
180
|
+
recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Starts promise chain, so helpers could enqueue their hooks
|
|
187
|
+
*/
|
|
188
|
+
module.exports.setup = function (suite) {
|
|
189
|
+
return injectHook(() => {
|
|
190
|
+
recorder.startUnlessRunning()
|
|
191
|
+
event.emit(event.test.before, suite && suite.ctx && suite.ctx.currentTest)
|
|
192
|
+
}, suite)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports.teardown = function (suite) {
|
|
196
|
+
return injectHook(() => {
|
|
197
|
+
recorder.startUnlessRunning()
|
|
198
|
+
event.emit(event.test.after, suite && suite.ctx && suite.ctx.currentTest)
|
|
199
|
+
}, suite)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports.suiteSetup = function (suite) {
|
|
203
|
+
return injectHook(() => {
|
|
204
|
+
recorder.startUnlessRunning()
|
|
205
|
+
event.emit(event.suite.before, suite)
|
|
206
|
+
}, suite)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports.suiteTeardown = function (suite) {
|
|
210
|
+
return injectHook(() => {
|
|
211
|
+
recorder.startUnlessRunning()
|
|
212
|
+
event.emit(event.suite.after, suite)
|
|
213
|
+
}, suite)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports.getInjectedArguments = getInjectedArguments
|
|
@@ -27,7 +27,7 @@ const addStep = (step, fn) => {
|
|
|
27
27
|
|
|
28
28
|
const parameterTypeRegistry = new ParameterTypeRegistry()
|
|
29
29
|
|
|
30
|
-
const matchStep =
|
|
30
|
+
const matchStep = step => {
|
|
31
31
|
for (const stepName in steps) {
|
|
32
32
|
if (stepName.indexOf('/') === 0) {
|
|
33
33
|
const regExpArr = stepName.match(/^\/(.*?)\/([gimy]*)$/) || []
|
|
@@ -43,7 +43,7 @@ const matchStep = (step) => {
|
|
|
43
43
|
const res = expression.match(step)
|
|
44
44
|
if (res) {
|
|
45
45
|
const fn = steps[stepName]
|
|
46
|
-
fn.params = res.map(
|
|
46
|
+
fn.params = res.map(arg => arg.getValue())
|
|
47
47
|
return fn
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -58,7 +58,7 @@ const getSteps = () => {
|
|
|
58
58
|
return steps
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const defineParameterType =
|
|
61
|
+
const defineParameterType = options => {
|
|
62
62
|
const parameterType = buildParameterType(options)
|
|
63
63
|
parameterTypeRegistry.defineParameterType(parameterType)
|
|
64
64
|
}
|
package/lib/mocha/cli.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const {
|
|
2
|
+
reporters: { Base },
|
|
3
|
+
} = require('mocha')
|
|
4
|
+
const ms = require('ms')
|
|
5
|
+
const figures = require('figures')
|
|
6
|
+
const event = require('../event')
|
|
7
|
+
const AssertionFailedError = require('../assert/error')
|
|
8
|
+
const output = require('../output')
|
|
9
|
+
const { cloneTest } = require('./test')
|
|
10
|
+
const cursor = Base.cursor
|
|
11
|
+
let currentMetaStep = []
|
|
12
|
+
let codeceptjsEventDispatchersRegistered = false
|
|
13
|
+
|
|
14
|
+
class Cli extends Base {
|
|
15
|
+
constructor(runner, opts) {
|
|
16
|
+
super(runner)
|
|
17
|
+
let level = 0
|
|
18
|
+
this.loadedTests = []
|
|
19
|
+
opts = opts.reporterOptions || opts
|
|
20
|
+
if (opts.steps) level = 1
|
|
21
|
+
if (opts.debug) level = 2
|
|
22
|
+
if (opts.verbose) level = 3
|
|
23
|
+
output.level(level)
|
|
24
|
+
output.print(`CodeceptJS v${require('../codecept').version()} ${output.standWithUkraine()}`)
|
|
25
|
+
output.print(`Using test root "${global.codecept_dir}"`)
|
|
26
|
+
|
|
27
|
+
const showSteps = level >= 1
|
|
28
|
+
|
|
29
|
+
if (level >= 2) {
|
|
30
|
+
const Containter = require('../container')
|
|
31
|
+
output.print(output.styles.debug(`Helpers: ${Object.keys(Containter.helpers()).join(', ')}`))
|
|
32
|
+
output.print(output.styles.debug(`Plugins: ${Object.keys(Containter.plugins()).join(', ')}`))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (level >= 3) {
|
|
36
|
+
process.on('warning', warning => {
|
|
37
|
+
console.log('\nWarning Details:')
|
|
38
|
+
console.log('Name:', warning.name)
|
|
39
|
+
console.log('Message:', warning.message)
|
|
40
|
+
console.log('Stack:', warning.stack)
|
|
41
|
+
console.log('-------------------')
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
runner.on('start', () => {
|
|
46
|
+
console.log()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
runner.on('suite', suite => {
|
|
50
|
+
output.suite.started(suite)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
runner.on('fail', test => {
|
|
54
|
+
if (test.ctx.currentTest) {
|
|
55
|
+
this.loadedTests.push(test.ctx.currentTest.uid)
|
|
56
|
+
}
|
|
57
|
+
if (showSteps && test.steps) {
|
|
58
|
+
return output.scenario.failed(test)
|
|
59
|
+
}
|
|
60
|
+
cursor.CR()
|
|
61
|
+
output.test.failed(test)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
runner.on('pending', test => {
|
|
65
|
+
if (test.parent && test.parent.pending) {
|
|
66
|
+
const suite = test.parent
|
|
67
|
+
const skipInfo = suite.opts.skipInfo || {}
|
|
68
|
+
skipTestConfig(test, skipInfo.message)
|
|
69
|
+
} else {
|
|
70
|
+
skipTestConfig(test, null)
|
|
71
|
+
}
|
|
72
|
+
this.loadedTests.push(test.uid)
|
|
73
|
+
cursor.CR()
|
|
74
|
+
output.test.skipped(test)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
runner.on('pass', test => {
|
|
78
|
+
if (showSteps && test.steps) {
|
|
79
|
+
return output.scenario.passed(test)
|
|
80
|
+
}
|
|
81
|
+
cursor.CR()
|
|
82
|
+
output.test.passed(test)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (showSteps) {
|
|
86
|
+
runner.on('test', test => {
|
|
87
|
+
currentMetaStep = []
|
|
88
|
+
if (test.steps) {
|
|
89
|
+
output.test.started(test)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (!codeceptjsEventDispatchersRegistered) {
|
|
94
|
+
codeceptjsEventDispatchersRegistered = true
|
|
95
|
+
|
|
96
|
+
event.dispatcher.on(event.bddStep.started, step => {
|
|
97
|
+
output.stepShift = 2
|
|
98
|
+
output.step(step)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
event.dispatcher.on(event.step.started, step => {
|
|
102
|
+
let processingStep = step
|
|
103
|
+
const metaSteps = []
|
|
104
|
+
let isHidden = false
|
|
105
|
+
while (processingStep.metaStep) {
|
|
106
|
+
metaSteps.unshift(processingStep.metaStep)
|
|
107
|
+
processingStep = processingStep.metaStep
|
|
108
|
+
if (processingStep.collapsed) isHidden = true
|
|
109
|
+
}
|
|
110
|
+
const shift = metaSteps.length
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
|
|
113
|
+
if (currentMetaStep[i] !== metaSteps[i]) {
|
|
114
|
+
output.stepShift = 3 + 2 * i
|
|
115
|
+
if (!metaSteps[i]) continue
|
|
116
|
+
// bdd steps are handled by bddStep.started
|
|
117
|
+
if (metaSteps[i].isBDD()) continue
|
|
118
|
+
output.step(metaSteps[i])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
currentMetaStep = metaSteps
|
|
122
|
+
if (isHidden) return
|
|
123
|
+
output.stepShift = 3 + 2 * shift
|
|
124
|
+
output.step(step)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
event.dispatcher.on(event.step.finished, () => {
|
|
128
|
+
output.stepShift = 0
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
runner.on('suite end', suite => {
|
|
134
|
+
let skippedCount = 0
|
|
135
|
+
const grep = runner._grep
|
|
136
|
+
for (const test of suite.tests) {
|
|
137
|
+
if (!test.state && !this.loadedTests.includes(test.uid)) {
|
|
138
|
+
if (matchTest(grep, test.title)) {
|
|
139
|
+
if (!test.opts) {
|
|
140
|
+
test.opts = {}
|
|
141
|
+
}
|
|
142
|
+
if (!test.opts.skipInfo) {
|
|
143
|
+
test.opts.skipInfo = {}
|
|
144
|
+
}
|
|
145
|
+
skipTestConfig(test, "Skipped due to failure in 'before' hook")
|
|
146
|
+
output.test.skipped(test)
|
|
147
|
+
skippedCount += 1
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const container = require('../container')
|
|
153
|
+
container.result().addStats({ pending: skippedCount, tests: skippedCount })
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
runner.on('end', this.result.bind(this))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
result() {
|
|
160
|
+
const container = require('../container')
|
|
161
|
+
container.result().addStats(this.stats)
|
|
162
|
+
container.result().finish()
|
|
163
|
+
|
|
164
|
+
const stats = container.result().stats
|
|
165
|
+
console.log()
|
|
166
|
+
|
|
167
|
+
// passes
|
|
168
|
+
if (stats.failures) {
|
|
169
|
+
output.print(output.styles.bold('-- FAILURES:'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const failuresLog = []
|
|
173
|
+
|
|
174
|
+
// failures
|
|
175
|
+
if (stats.failures) {
|
|
176
|
+
// append step traces
|
|
177
|
+
this.failures = this.failures.map(test => {
|
|
178
|
+
// we will change the stack trace, so we need to clone the test
|
|
179
|
+
const err = test.err
|
|
180
|
+
|
|
181
|
+
let log = ''
|
|
182
|
+
let originalMessage = err.message
|
|
183
|
+
|
|
184
|
+
if (err instanceof AssertionFailedError) {
|
|
185
|
+
err.message = err.inspect()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// multi-line error messages (for Playwright)
|
|
189
|
+
if (err.message && err.message.includes('\n')) {
|
|
190
|
+
const lines = err.message.split('\n')
|
|
191
|
+
const truncatedLines = lines.slice(0, 5)
|
|
192
|
+
if (lines.length > 5) {
|
|
193
|
+
truncatedLines.push('...')
|
|
194
|
+
}
|
|
195
|
+
err.message = truncatedLines.join('\n').replace(/^/gm, ' ').trim()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// add new line before the message
|
|
199
|
+
err.message = '\n ' + err.message
|
|
200
|
+
|
|
201
|
+
// explicitly show file with error
|
|
202
|
+
if (test.file) {
|
|
203
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} ${output.styles.basic(test.file)}\n`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const steps = test.steps || (test.ctx && test.ctx.test.steps)
|
|
207
|
+
|
|
208
|
+
if (steps && steps.length) {
|
|
209
|
+
let scenarioTrace = ''
|
|
210
|
+
steps
|
|
211
|
+
.reverse()
|
|
212
|
+
.slice(0, 10)
|
|
213
|
+
.forEach(step => {
|
|
214
|
+
const hasFailed = step.status === 'failed'
|
|
215
|
+
let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
|
|
216
|
+
if (hasFailed) line = output.styles.bold(line)
|
|
217
|
+
scenarioTrace += `\n${line}`
|
|
218
|
+
})
|
|
219
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// display artifacts in debug mode
|
|
223
|
+
if (test?.artifacts && Object.keys(test.artifacts).length) {
|
|
224
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Artifacts:')}`
|
|
225
|
+
for (const artifact of Object.keys(test.artifacts)) {
|
|
226
|
+
log += `\n- ${artifact}: ${test.artifacts[artifact]}`
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// display metadata
|
|
231
|
+
if (test.meta && Object.keys(test.meta).length) {
|
|
232
|
+
log += `\n\n${output.styles.basic(figures.circle)} ${output.styles.section('Metadata:')}`
|
|
233
|
+
for (const [key, value] of Object.entries(test.meta)) {
|
|
234
|
+
log += `\n- ${key}: ${value}`
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
let stack = err.stack
|
|
240
|
+
stack = (stack || '').replace(originalMessage, '')
|
|
241
|
+
stack = stack ? stack.split('\n') : []
|
|
242
|
+
|
|
243
|
+
if (stack[0] && stack[0].includes(err.message)) {
|
|
244
|
+
stack.shift()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (stack[0] && stack[0].trim() == 'Error:') {
|
|
248
|
+
stack.shift()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (output.level() < 3) {
|
|
252
|
+
stack = stack.slice(0, 3)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.error(e)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// we will change the stack trace, so we need to clone the test
|
|
261
|
+
test = cloneTest(test)
|
|
262
|
+
test.err = err
|
|
263
|
+
return test
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const originalLog = Base.consoleLog
|
|
267
|
+
Base.consoleLog = (...data) => {
|
|
268
|
+
failuresLog.push([...data])
|
|
269
|
+
originalLog(...data)
|
|
270
|
+
}
|
|
271
|
+
Base.list(this.failures)
|
|
272
|
+
Base.consoleLog = originalLog
|
|
273
|
+
console.log()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
container.result().addFailures(failuresLog)
|
|
277
|
+
|
|
278
|
+
output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks)
|
|
279
|
+
|
|
280
|
+
if (stats.failures && output.level() < 3) {
|
|
281
|
+
output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace'))
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function matchTest(grep, test) {
|
|
287
|
+
if (grep) {
|
|
288
|
+
return grep.test(test)
|
|
289
|
+
}
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function skipTestConfig(test, message) {
|
|
294
|
+
if (!test.opts) {
|
|
295
|
+
test.opts = {}
|
|
296
|
+
}
|
|
297
|
+
if (!test.opts.skipInfo) {
|
|
298
|
+
test.opts.skipInfo = {}
|
|
299
|
+
}
|
|
300
|
+
test.opts.skipInfo.message = test.opts.skipInfo.message || message
|
|
301
|
+
test.opts.skipInfo.isFastSkipped = true
|
|
302
|
+
event.emit(event.test.skipped, test)
|
|
303
|
+
test.state = 'skipped'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = function (runner, opts) {
|
|
307
|
+
return new Cli(runner, opts)
|
|
308
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const Mocha = require('mocha')
|
|
2
|
+
const fsPath = require('path')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const reporter = require('./cli')
|
|
5
|
+
const gherkinParser = require('./gherkin')
|
|
6
|
+
const output = require('../output')
|
|
7
|
+
const ConnectionRefused = require('../helper/errors/ConnectionRefused')
|
|
8
|
+
|
|
9
|
+
const scenarioUi = fsPath.join(__dirname, './ui.js')
|
|
10
|
+
|
|
11
|
+
let mocha
|
|
12
|
+
|
|
13
|
+
class MochaFactory {
|
|
14
|
+
static create(config, opts) {
|
|
15
|
+
mocha = new Mocha(Object.assign(config, opts))
|
|
16
|
+
output.process(opts.child)
|
|
17
|
+
mocha.ui(scenarioUi)
|
|
18
|
+
|
|
19
|
+
Mocha.Runner.prototype.uncaught = function (err) {
|
|
20
|
+
if (err) {
|
|
21
|
+
if (err.toString().indexOf('ECONNREFUSED') >= 0) {
|
|
22
|
+
err = new ConnectionRefused(err)
|
|
23
|
+
}
|
|
24
|
+
output.error(err)
|
|
25
|
+
output.print(err.stack)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
output.error('Uncaught undefined exception')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mocha.loadFiles = fn => {
|
|
33
|
+
// load features
|
|
34
|
+
if (mocha.suite.suites.length === 0) {
|
|
35
|
+
mocha.files.filter(file => file.match(/\.feature$/)).forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)))
|
|
36
|
+
|
|
37
|
+
// remove feature files
|
|
38
|
+
mocha.files = mocha.files.filter(file => !file.match(/\.feature$/))
|
|
39
|
+
|
|
40
|
+
Mocha.prototype.loadFiles.call(mocha, fn)
|
|
41
|
+
|
|
42
|
+
// add ids for each test and check uniqueness
|
|
43
|
+
const dupes = []
|
|
44
|
+
let missingFeatureInFile = []
|
|
45
|
+
const seenTests = []
|
|
46
|
+
mocha.suite.eachTest(test => {
|
|
47
|
+
const name = test.fullTitle()
|
|
48
|
+
if (seenTests.includes(test.uid)) {
|
|
49
|
+
dupes.push(name)
|
|
50
|
+
}
|
|
51
|
+
seenTests.push(test.uid)
|
|
52
|
+
|
|
53
|
+
if (name.slice(0, name.indexOf(':')) === '') {
|
|
54
|
+
missingFeatureInFile.push(test.file)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
if (dupes.length) {
|
|
58
|
+
// ideally this should be no-op and throw (breaking change)...
|
|
59
|
+
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (missingFeatureInFile.length) {
|
|
63
|
+
missingFeatureInFile = [...new Set(missingFeatureInFile)]
|
|
64
|
+
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const presetReporter = opts.reporter || config.reporter
|
|
70
|
+
// use standard reporter
|
|
71
|
+
if (!presetReporter) {
|
|
72
|
+
mocha.reporter(reporter, opts)
|
|
73
|
+
return mocha
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// load custom reporter with options
|
|
77
|
+
const reporterOptions = Object.assign(config.reporterOptions || {})
|
|
78
|
+
|
|
79
|
+
if (opts.reporterOptions !== undefined) {
|
|
80
|
+
opts.reporterOptions.split(',').forEach(opt => {
|
|
81
|
+
const L = opt.split('=')
|
|
82
|
+
if (L.length > 2 || L.length === 0) {
|
|
83
|
+
throw new Error(`invalid reporter option '${opt}'`)
|
|
84
|
+
} else if (L.length === 2) {
|
|
85
|
+
reporterOptions[L[0]] = L[1]
|
|
86
|
+
} else {
|
|
87
|
+
reporterOptions[L[0]] = true
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter')
|
|
93
|
+
if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
|
|
94
|
+
Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes)
|
|
95
|
+
delete reporterOptions['codeceptjs-cli-reporter']
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// custom reporters
|
|
99
|
+
mocha.reporter(presetReporter, reporterOptions)
|
|
100
|
+
return mocha
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = MochaFactory
|