codeceptjs 4.0.0-beta.4 → 4.0.0-beta.5

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.
Files changed (150) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +139 -87
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/generate.js +10 -14
  17. package/lib/command/gherkin/snippets.js +75 -73
  18. package/lib/command/gherkin/steps.js +1 -1
  19. package/lib/command/info.js +42 -8
  20. package/lib/command/init.js +13 -12
  21. package/lib/command/interactive.js +10 -2
  22. package/lib/command/list.js +1 -1
  23. package/lib/command/run-multiple/chunk.js +48 -45
  24. package/lib/command/run-multiple.js +12 -35
  25. package/lib/command/run-workers.js +21 -58
  26. package/lib/command/utils.js +5 -6
  27. package/lib/command/workers/runTests.js +262 -220
  28. package/lib/container.js +386 -238
  29. package/lib/data/context.js +10 -13
  30. package/lib/data/dataScenarioConfig.js +8 -8
  31. package/lib/data/dataTableArgument.js +6 -6
  32. package/lib/data/table.js +5 -11
  33. package/lib/effects.js +223 -0
  34. package/lib/element/WebElement.js +327 -0
  35. package/lib/els.js +158 -0
  36. package/lib/event.js +21 -17
  37. package/lib/heal.js +88 -80
  38. package/lib/helper/AI.js +2 -1
  39. package/lib/helper/ApiDataFactory.js +3 -6
  40. package/lib/helper/Appium.js +47 -51
  41. package/lib/helper/FileSystem.js +3 -3
  42. package/lib/helper/GraphQLDataFactory.js +3 -3
  43. package/lib/helper/JSONResponse.js +75 -37
  44. package/lib/helper/Mochawesome.js +31 -9
  45. package/lib/helper/Nightmare.js +35 -53
  46. package/lib/helper/Playwright.js +262 -267
  47. package/lib/helper/Protractor.js +54 -77
  48. package/lib/helper/Puppeteer.js +246 -260
  49. package/lib/helper/REST.js +5 -17
  50. package/lib/helper/TestCafe.js +21 -44
  51. package/lib/helper/WebDriver.js +151 -170
  52. package/lib/helper/extras/Popup.js +22 -22
  53. package/lib/helper/testcafe/testcafe-utils.js +26 -27
  54. package/lib/listener/emptyRun.js +55 -0
  55. package/lib/listener/exit.js +7 -10
  56. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  57. package/lib/listener/globalTimeout.js +165 -0
  58. package/lib/listener/helpers.js +15 -15
  59. package/lib/listener/mocha.js +1 -1
  60. package/lib/listener/result.js +12 -0
  61. package/lib/listener/retryEnhancer.js +85 -0
  62. package/lib/listener/steps.js +32 -18
  63. package/lib/listener/store.js +20 -0
  64. package/lib/mocha/asyncWrapper.js +231 -0
  65. package/lib/{interfaces → mocha}/bdd.js +3 -3
  66. package/lib/mocha/cli.js +308 -0
  67. package/lib/mocha/factory.js +104 -0
  68. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  69. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  70. package/lib/mocha/hooks.js +112 -0
  71. package/lib/mocha/index.js +12 -0
  72. package/lib/mocha/inject.js +29 -0
  73. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  74. package/lib/mocha/suite.js +82 -0
  75. package/lib/mocha/test.js +181 -0
  76. package/lib/mocha/types.d.ts +42 -0
  77. package/lib/mocha/ui.js +232 -0
  78. package/lib/output.js +82 -62
  79. package/lib/pause.js +160 -138
  80. package/lib/plugin/analyze.js +396 -0
  81. package/lib/plugin/auth.js +435 -0
  82. package/lib/plugin/autoDelay.js +8 -8
  83. package/lib/plugin/autoLogin.js +3 -338
  84. package/lib/plugin/commentStep.js +6 -1
  85. package/lib/plugin/coverage.js +10 -19
  86. package/lib/plugin/customLocator.js +3 -3
  87. package/lib/plugin/customReporter.js +52 -0
  88. package/lib/plugin/eachElement.js +1 -1
  89. package/lib/plugin/fakerTransform.js +1 -1
  90. package/lib/plugin/heal.js +36 -9
  91. package/lib/plugin/htmlReporter.js +1947 -0
  92. package/lib/plugin/pageInfo.js +140 -0
  93. package/lib/plugin/retryFailedStep.js +17 -18
  94. package/lib/plugin/retryTo.js +2 -113
  95. package/lib/plugin/screenshotOnFail.js +17 -58
  96. package/lib/plugin/selenoid.js +15 -35
  97. package/lib/plugin/standardActingHelpers.js +4 -1
  98. package/lib/plugin/stepByStepReport.js +56 -17
  99. package/lib/plugin/stepTimeout.js +5 -12
  100. package/lib/plugin/subtitles.js +4 -4
  101. package/lib/plugin/tryTo.js +3 -102
  102. package/lib/plugin/wdio.js +8 -10
  103. package/lib/recorder.js +155 -124
  104. package/lib/rerun.js +43 -42
  105. package/lib/result.js +161 -0
  106. package/lib/secret.js +1 -1
  107. package/lib/step/base.js +239 -0
  108. package/lib/step/comment.js +10 -0
  109. package/lib/step/config.js +50 -0
  110. package/lib/step/func.js +46 -0
  111. package/lib/step/helper.js +50 -0
  112. package/lib/step/meta.js +99 -0
  113. package/lib/step/record.js +74 -0
  114. package/lib/step/retry.js +11 -0
  115. package/lib/step/section.js +55 -0
  116. package/lib/step.js +21 -332
  117. package/lib/steps.js +50 -0
  118. package/lib/store.js +37 -5
  119. package/lib/template/heal.js +2 -11
  120. package/lib/test-server.js +323 -0
  121. package/lib/timeout.js +66 -0
  122. package/lib/utils.js +351 -218
  123. package/lib/within.js +75 -55
  124. package/lib/workerStorage.js +2 -1
  125. package/lib/workers.js +386 -276
  126. package/package.json +76 -70
  127. package/translations/de-DE.js +4 -3
  128. package/translations/fr-FR.js +4 -3
  129. package/translations/index.js +1 -0
  130. package/translations/it-IT.js +4 -3
  131. package/translations/ja-JP.js +4 -3
  132. package/translations/nl-NL.js +76 -0
  133. package/translations/pl-PL.js +4 -3
  134. package/translations/pt-BR.js +4 -3
  135. package/translations/ru-RU.js +4 -3
  136. package/translations/utils.js +9 -0
  137. package/translations/zh-CN.js +4 -3
  138. package/translations/zh-TW.js +4 -3
  139. package/typings/index.d.ts +188 -186
  140. package/typings/promiseBasedTypes.d.ts +18 -705
  141. package/typings/types.d.ts +301 -804
  142. package/lib/cli.js +0 -256
  143. package/lib/helper/ExpectHelper.js +0 -391
  144. package/lib/helper/SoftExpectHelper.js +0 -381
  145. package/lib/listener/artifacts.js +0 -19
  146. package/lib/listener/timeout.js +0 -109
  147. package/lib/mochaFactory.js +0 -113
  148. package/lib/plugin/debugErrors.js +0 -67
  149. package/lib/scenario.js +0 -224
  150. package/lib/ui.js +0 -236
@@ -2,47 +2,55 @@ const debug = require('debug')('codeceptjs:steps')
2
2
  const event = require('../event')
3
3
  const store = require('../store')
4
4
  const output = require('../output')
5
+ const { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks')
6
+ const recorder = require('../recorder')
5
7
 
6
8
  let currentTest
7
9
  let currentHook
8
10
 
11
+ // Session names that should not contribute steps to the main test trace
12
+ const EXCLUDED_SESSIONS = ['tryTo', 'hopeThat']
13
+
9
14
  /**
10
15
  * Register steps inside tests
11
16
  */
12
17
  module.exports = function () {
13
- event.dispatcher.on(event.test.before, (test) => {
18
+ event.dispatcher.on(event.test.before, test => {
14
19
  test.startedAt = +new Date()
15
- test.artifacts = {}
16
20
  })
17
21
 
18
- event.dispatcher.on(event.test.started, (test) => {
22
+ event.dispatcher.on(event.test.started, test => {
19
23
  currentTest = test
20
24
  currentTest.steps = []
21
25
  if (!('retryNum' in currentTest)) currentTest.retryNum = 0
22
26
  else currentTest.retryNum += 1
27
+ output.scenario.started(test)
23
28
  })
24
29
 
25
- event.dispatcher.on(event.test.after, (test) => {
30
+ event.dispatcher.on(event.test.after, test => {
26
31
  currentTest = null
27
32
  })
28
33
 
29
- event.dispatcher.on(event.test.finished, (test) => {})
34
+ event.dispatcher.on(event.test.finished, test => {})
30
35
 
31
- event.dispatcher.on(event.hook.started, (suite) => {
32
- currentHook = suite.ctx.test
36
+ event.dispatcher.on(event.hook.started, hook => {
37
+ currentHook = hook.ctx.test
33
38
  currentHook.steps = []
34
39
 
35
- if (suite.ctx && suite.ctx.test) output.log(`--- STARTED ${suite.ctx.test.title} ---`)
40
+ output.hook.started(hook)
41
+
42
+ if (hook.ctx && hook.ctx.test) debug(`--- STARTED ${hook.ctx.test.title} ---`)
36
43
  })
37
44
 
38
- event.dispatcher.on(event.hook.passed, (suite) => {
45
+ event.dispatcher.on(event.hook.passed, hook => {
39
46
  currentHook = null
40
- if (suite.ctx && suite.ctx.test) output.log(`--- ENDED ${suite.ctx.test.title} ---`)
47
+ output.hook.passed(hook)
48
+ if (hook.ctx && hook.ctx.test) debug(`--- ENDED ${hook.ctx.test.title} ---`)
41
49
  })
42
50
 
43
51
  event.dispatcher.on(event.test.failed, () => {
44
52
  const cutSteps = function (current) {
45
- const failureIndex = current.steps.findIndex((el) => el.status === 'failed')
53
+ const failureIndex = current.steps.findIndex(el => el.status === 'failed')
46
54
  // To be sure that failed test will be failed in report
47
55
  current.state = 'failed'
48
56
  current.steps.length = failureIndex + 1
@@ -65,19 +73,25 @@ module.exports = function () {
65
73
  currentTest.state = 'passed'
66
74
  })
67
75
 
68
- event.dispatcher.on(event.step.started, (step) => {
69
- step.startedAt = +new Date()
70
- step.test = currentTest
76
+ event.dispatcher.on(event.step.started, step => {
77
+ store.currentStep = step
71
78
  if (currentHook && Array.isArray(currentHook.steps)) {
72
79
  return currentHook.steps.push(step)
73
80
  }
74
81
  if (!currentTest || !currentTest.steps) return
82
+
83
+ // Check if we're in a session that should be excluded from main test steps
84
+ const currentSessionId = recorder.getCurrentSessionId()
85
+ if (currentSessionId && EXCLUDED_SESSIONS.includes(currentSessionId)) {
86
+ // Skip adding this step to the main test steps
87
+ return
88
+ }
89
+
75
90
  currentTest.steps.push(step)
76
91
  })
77
92
 
78
- event.dispatcher.on(event.step.finished, (step) => {
79
- step.finishedAt = +new Date()
80
- if (step.startedAt) step.duration = step.finishedAt - step.startedAt
81
- debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`)
93
+ event.dispatcher.on(event.step.finished, step => {
94
+ store.currentStep = null
95
+ store.stepOptions = null
82
96
  })
83
97
  }
@@ -0,0 +1,20 @@
1
+ const event = require('../event')
2
+ const store = require('../store')
3
+
4
+ module.exports = function () {
5
+ event.dispatcher.on(event.suite.before, suite => {
6
+ store.currentSuite = suite
7
+ })
8
+
9
+ event.dispatcher.on(event.suite.after, () => {
10
+ store.currentSuite = null
11
+ })
12
+
13
+ event.dispatcher.on(event.test.before, test => {
14
+ store.currentTest = test
15
+ })
16
+
17
+ event.dispatcher.on(event.test.finished, () => {
18
+ store.currentTest = null
19
+ })
20
+ }
@@ -0,0 +1,231 @@
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
+ if (hookName) hookName = ucfirst(hookName)
26
+ event.emit(event.test.failed, test, err, hookName)
27
+ })
28
+ }
29
+
30
+ function makeDoneCallableOnce(done) {
31
+ let called = false
32
+ return function (err) {
33
+ if (called) {
34
+ return
35
+ }
36
+ called = true
37
+ return done(err)
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Wraps test function, injects support objects from container,
43
+ * starts promise chain with recorder, performs before/after hooks
44
+ * through event system.
45
+ */
46
+ module.exports.test = test => {
47
+ const testFn = test.fn
48
+ if (!testFn) {
49
+ return test
50
+ }
51
+
52
+ test.timeout(0)
53
+ test.async = true
54
+
55
+ test.fn = function (done) {
56
+ const doneFn = makeDoneCallableOnce(done)
57
+ recorder.errHandler(err => {
58
+ recorder.session.start('teardown')
59
+ recorder.cleanAsyncErr()
60
+ if (test.throws) {
61
+ // check that test should actually fail
62
+ try {
63
+ assertThrown(err, test.throws)
64
+ event.emit(event.test.passed, test)
65
+ event.emit(event.test.finished, test)
66
+ recorder.add(doneFn)
67
+ return
68
+ } catch (newErr) {
69
+ err = newErr
70
+ }
71
+ }
72
+ test.err = err
73
+ event.emit(event.test.failed, test, err)
74
+ event.emit(event.test.finished, test)
75
+ recorder.add(() => doneFn(err))
76
+ })
77
+
78
+ if (isAsyncFunction(testFn)) {
79
+ event.emit(event.test.started, test)
80
+ testFn
81
+ .call(test, getInjectedArguments(testFn, test))
82
+ .then(() => {
83
+ recorder.add('fire test.passed', () => {
84
+ event.emit(event.test.passed, test)
85
+ event.emit(event.test.finished, test)
86
+ })
87
+ recorder.add('finish test', doneFn)
88
+ })
89
+ .catch(err => {
90
+ recorder.throw(err)
91
+ })
92
+ .finally(() => {
93
+ recorder.catch()
94
+ })
95
+ return
96
+ }
97
+
98
+ try {
99
+ event.emit(event.test.started, test)
100
+ testFn.call(test, getInjectedArguments(testFn, test))
101
+ } catch (err) {
102
+ recorder.throw(err)
103
+ } finally {
104
+ recorder.add('fire test.passed', () => {
105
+ event.emit(event.test.passed, test)
106
+ event.emit(event.test.finished, test)
107
+ })
108
+ recorder.add('finish test', doneFn)
109
+ recorder.catch()
110
+ }
111
+ }
112
+ return test
113
+ }
114
+
115
+ /**
116
+ * Injects arguments to function from controller
117
+ */
118
+ module.exports.injected = function (fn, suite, hookName) {
119
+ return function (done) {
120
+ const doneFn = makeDoneCallableOnce(done)
121
+ const errHandler = err => {
122
+ recorder.session.start('teardown')
123
+ recorder.cleanAsyncErr()
124
+ if (['before', 'beforeSuite'].includes(hookName)) {
125
+ suiteTestFailedHookError(suite, err, hookName)
126
+ }
127
+ if (hookName === 'after') {
128
+ suiteTestFailedHookError(suite, err, hookName)
129
+ suite.eachTest(test => {
130
+ event.emit(event.test.after, test)
131
+ })
132
+ }
133
+ if (hookName === 'afterSuite') {
134
+ suiteTestFailedHookError(suite, err, hookName)
135
+ event.emit(event.suite.after, suite)
136
+ }
137
+ recorder.add(() => doneFn(err))
138
+ }
139
+
140
+ recorder.errHandler(err => {
141
+ errHandler(err)
142
+ })
143
+
144
+ if (!fn) throw new Error('fn is not defined')
145
+
146
+ fireHook(event.hook.started, suite)
147
+
148
+ this.test.body = fn.toString()
149
+
150
+ if (!recorder.isRunning()) {
151
+ recorder.errHandler(err => {
152
+ errHandler(err)
153
+ })
154
+ }
155
+
156
+ const opts = suite.opts || {}
157
+ const retries = opts[`retry${ucfirst(hookName)}`] || 0
158
+
159
+ const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
160
+
161
+ promiseRetry(
162
+ async (retry, number) => {
163
+ try {
164
+ recorder.startUnlessRunning()
165
+ await fn.call(this, { ...getInjectedArguments(fn), suite, test: currentTest })
166
+ await recorder.promise().catch(err => retry(err))
167
+ } catch (err) {
168
+ retry(err)
169
+ } finally {
170
+ if (number < retries) {
171
+ recorder.stop()
172
+ recorder.start()
173
+ }
174
+ }
175
+ },
176
+ { retries },
177
+ )
178
+ .then(() => {
179
+ recorder.add('fire hook.passed', () => fireHook(event.hook.passed, suite))
180
+ recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
181
+ recorder.add(`finish ${hookName} hook`, doneFn)
182
+ recorder.catch()
183
+ })
184
+ .catch(e => {
185
+ recorder.throw(e)
186
+ recorder.catch(e => {
187
+ const err = recorder.getAsyncErr() === null ? e : recorder.getAsyncErr()
188
+ errHandler(err)
189
+ })
190
+ recorder.add('fire hook.failed', () => fireHook(event.hook.failed, suite, e))
191
+ recorder.add('fire hook.finished', () => fireHook(event.hook.finished, suite))
192
+ })
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Starts promise chain, so helpers could enqueue their hooks
198
+ */
199
+ module.exports.setup = function (suite) {
200
+ const { enhanceMochaTest } = require('./test')
201
+ return injectHook(() => {
202
+ recorder.startUnlessRunning()
203
+ event.emit(event.test.before, enhanceMochaTest(suite?.ctx?.currentTest))
204
+ }, suite)
205
+ }
206
+
207
+ module.exports.teardown = function (suite) {
208
+ const { enhanceMochaTest } = require('./test')
209
+ return injectHook(() => {
210
+ recorder.startUnlessRunning()
211
+ event.emit(event.test.after, enhanceMochaTest(suite?.ctx?.currentTest))
212
+ }, suite)
213
+ }
214
+
215
+ module.exports.suiteSetup = function (suite) {
216
+ const { enhanceMochaSuite } = require('./suite')
217
+ return injectHook(() => {
218
+ recorder.startUnlessRunning()
219
+ event.emit(event.suite.before, enhanceMochaSuite(suite))
220
+ }, suite)
221
+ }
222
+
223
+ module.exports.suiteTeardown = function (suite) {
224
+ const { enhanceMochaSuite } = require('./suite')
225
+ return injectHook(() => {
226
+ recorder.startUnlessRunning()
227
+ event.emit(event.suite.after, enhanceMochaSuite(suite))
228
+ }, suite)
229
+ }
230
+
231
+ 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 = (step) => {
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((arg) => arg.getValue())
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 = (options) => {
61
+ const defineParameterType = options => {
62
62
  const parameterType = buildParameterType(options)
63
63
  parameterTypeRegistry.defineParameterType(parameterType)
64
64
  }
@@ -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:')} file://${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
+ }