codeceptjs 4.0.0-beta.3 → 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 (155) 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 +141 -86
  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/dryRun.js +30 -35
  17. package/lib/command/generate.js +10 -14
  18. package/lib/command/gherkin/snippets.js +75 -73
  19. package/lib/command/gherkin/steps.js +1 -1
  20. package/lib/command/info.js +42 -8
  21. package/lib/command/init.js +13 -12
  22. package/lib/command/interactive.js +10 -2
  23. package/lib/command/list.js +1 -1
  24. package/lib/command/run-multiple/chunk.js +48 -45
  25. package/lib/command/run-multiple.js +12 -35
  26. package/lib/command/run-workers.js +21 -58
  27. package/lib/command/utils.js +5 -6
  28. package/lib/command/workers/runTests.js +263 -222
  29. package/lib/container.js +386 -238
  30. package/lib/data/context.js +10 -13
  31. package/lib/data/dataScenarioConfig.js +8 -8
  32. package/lib/data/dataTableArgument.js +6 -6
  33. package/lib/data/table.js +5 -11
  34. package/lib/effects.js +223 -0
  35. package/lib/element/WebElement.js +327 -0
  36. package/lib/els.js +158 -0
  37. package/lib/event.js +21 -17
  38. package/lib/heal.js +88 -80
  39. package/lib/helper/AI.js +2 -1
  40. package/lib/helper/ApiDataFactory.js +4 -7
  41. package/lib/helper/Appium.js +50 -57
  42. package/lib/helper/FileSystem.js +3 -3
  43. package/lib/helper/GraphQLDataFactory.js +4 -4
  44. package/lib/helper/JSONResponse.js +75 -37
  45. package/lib/helper/Mochawesome.js +31 -9
  46. package/lib/helper/Nightmare.js +37 -58
  47. package/lib/helper/Playwright.js +267 -272
  48. package/lib/helper/Protractor.js +56 -87
  49. package/lib/helper/Puppeteer.js +247 -264
  50. package/lib/helper/REST.js +29 -17
  51. package/lib/helper/TestCafe.js +22 -47
  52. package/lib/helper/WebDriver.js +157 -368
  53. package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
  54. package/lib/helper/extras/Popup.js +22 -22
  55. package/lib/helper/network/utils.js +1 -1
  56. package/lib/helper/testcafe/testcafe-utils.js +27 -28
  57. package/lib/listener/emptyRun.js +55 -0
  58. package/lib/listener/exit.js +7 -10
  59. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  60. package/lib/listener/globalTimeout.js +165 -0
  61. package/lib/listener/helpers.js +15 -15
  62. package/lib/listener/mocha.js +1 -1
  63. package/lib/listener/result.js +12 -0
  64. package/lib/listener/retryEnhancer.js +85 -0
  65. package/lib/listener/steps.js +32 -18
  66. package/lib/listener/store.js +20 -0
  67. package/lib/locator.js +1 -1
  68. package/lib/mocha/asyncWrapper.js +231 -0
  69. package/lib/{interfaces → mocha}/bdd.js +3 -3
  70. package/lib/mocha/cli.js +308 -0
  71. package/lib/mocha/factory.js +104 -0
  72. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  73. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  74. package/lib/mocha/hooks.js +112 -0
  75. package/lib/mocha/index.js +12 -0
  76. package/lib/mocha/inject.js +29 -0
  77. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  78. package/lib/mocha/suite.js +82 -0
  79. package/lib/mocha/test.js +181 -0
  80. package/lib/mocha/types.d.ts +42 -0
  81. package/lib/mocha/ui.js +232 -0
  82. package/lib/output.js +93 -65
  83. package/lib/pause.js +160 -138
  84. package/lib/plugin/analyze.js +396 -0
  85. package/lib/plugin/auth.js +435 -0
  86. package/lib/plugin/autoDelay.js +8 -8
  87. package/lib/plugin/autoLogin.js +3 -338
  88. package/lib/plugin/commentStep.js +6 -1
  89. package/lib/plugin/coverage.js +10 -22
  90. package/lib/plugin/customLocator.js +3 -3
  91. package/lib/plugin/customReporter.js +52 -0
  92. package/lib/plugin/eachElement.js +1 -1
  93. package/lib/plugin/fakerTransform.js +1 -1
  94. package/lib/plugin/heal.js +36 -9
  95. package/lib/plugin/htmlReporter.js +1947 -0
  96. package/lib/plugin/pageInfo.js +140 -0
  97. package/lib/plugin/retryFailedStep.js +17 -18
  98. package/lib/plugin/retryTo.js +2 -113
  99. package/lib/plugin/screenshotOnFail.js +17 -58
  100. package/lib/plugin/selenoid.js +15 -35
  101. package/lib/plugin/standardActingHelpers.js +4 -1
  102. package/lib/plugin/stepByStepReport.js +56 -17
  103. package/lib/plugin/stepTimeout.js +5 -12
  104. package/lib/plugin/subtitles.js +4 -4
  105. package/lib/plugin/tryTo.js +3 -102
  106. package/lib/plugin/wdio.js +8 -10
  107. package/lib/recorder.js +155 -124
  108. package/lib/rerun.js +43 -42
  109. package/lib/result.js +161 -0
  110. package/lib/secret.js +1 -2
  111. package/lib/step/base.js +239 -0
  112. package/lib/step/comment.js +10 -0
  113. package/lib/step/config.js +50 -0
  114. package/lib/step/func.js +46 -0
  115. package/lib/step/helper.js +50 -0
  116. package/lib/step/meta.js +99 -0
  117. package/lib/step/record.js +74 -0
  118. package/lib/step/retry.js +11 -0
  119. package/lib/step/section.js +55 -0
  120. package/lib/step.js +21 -332
  121. package/lib/steps.js +50 -0
  122. package/lib/store.js +37 -5
  123. package/lib/template/heal.js +2 -11
  124. package/lib/test-server.js +323 -0
  125. package/lib/timeout.js +66 -0
  126. package/lib/utils.js +351 -218
  127. package/lib/within.js +75 -55
  128. package/lib/workerStorage.js +2 -1
  129. package/lib/workers.js +386 -277
  130. package/package.json +81 -75
  131. package/translations/de-DE.js +5 -3
  132. package/translations/fr-FR.js +5 -4
  133. package/translations/index.js +1 -0
  134. package/translations/it-IT.js +4 -3
  135. package/translations/ja-JP.js +4 -3
  136. package/translations/nl-NL.js +76 -0
  137. package/translations/pl-PL.js +4 -3
  138. package/translations/pt-BR.js +4 -3
  139. package/translations/ru-RU.js +4 -3
  140. package/translations/utils.js +9 -0
  141. package/translations/zh-CN.js +4 -3
  142. package/translations/zh-TW.js +4 -3
  143. package/typings/index.d.ts +197 -187
  144. package/typings/promiseBasedTypes.d.ts +53 -903
  145. package/typings/types.d.ts +372 -1042
  146. package/lib/cli.js +0 -257
  147. package/lib/helper/ExpectHelper.js +0 -391
  148. package/lib/helper/MockServer.js +0 -221
  149. package/lib/helper/SoftExpectHelper.js +0 -381
  150. package/lib/listener/artifacts.js +0 -19
  151. package/lib/listener/timeout.js +0 -109
  152. package/lib/mochaFactory.js +0 -113
  153. package/lib/plugin/debugErrors.js +0 -67
  154. package/lib/scenario.js +0 -224
  155. package/lib/ui.js +0 -236
@@ -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
+ }
@@ -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
@@ -1,11 +1,30 @@
1
- /** @class */
1
+ /**
2
+ * Configuration for a Feature.
3
+ * Can inject values and add custom configuration.
4
+ */
2
5
  class FeatureConfig {
6
+ /**
7
+ * @param {CodeceptJS.Suite} suite
8
+ */
3
9
  constructor(suite) {
4
10
  this.suite = suite
5
11
  }
6
12
 
7
13
  /**
8
- * Retry this suite for x times
14
+ * Set metadata for this suite
15
+ * @param {string} key
16
+ * @param {string} value
17
+ * @returns {this}
18
+ */
19
+ meta(key, value) {
20
+ this.suite.tests.forEach(test => {
21
+ test.meta[key] = value
22
+ })
23
+ return this
24
+ }
25
+
26
+ /**
27
+ * Retry this test for number of times
9
28
  *
10
29
  * @param {number} retries
11
30
  * @returns {this}
@@ -16,24 +35,26 @@ class FeatureConfig {
16
35
  }
17
36
 
18
37
  /**
19
- * Set timeout for this suite
38
+ * Set timeout for this test
20
39
  * @param {number} timeout
21
40
  * @returns {this}
22
- * @deprecated
23
41
  */
24
42
  timeout(timeout) {
25
- console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`)
26
- console.log(`Please use Feature('${this.suite.title}', { timeout: ${timeout / 1000} }) instead`)
27
- console.log('Timeout should be set in seconds')
28
43
  this.suite.timeout(timeout)
29
44
  return this
30
45
  }
31
46
 
47
+ /**
48
+ * @callback FeatureConfigCallback
49
+ * @param {CodeceptJS.Suite} suite
50
+ * @returns {Object<string, any>}
51
+ */
52
+
32
53
  /**
33
54
  * Configures a helper.
34
55
  * Helper name can be omitted and values will be applied to first helper.
35
- * @param {string | Object<string, *>} helper
36
- * @param {Object<string, *>} [obj]
56
+ * @param {string | Object<string, any> | FeatureConfigCallback} helper
57
+ * @param {Object<string, any>} [obj]
37
58
  * @returns {this}
38
59
  */
39
60
  config(helper, obj) {
@@ -57,9 +78,8 @@ class FeatureConfig {
57
78
  * @returns {this}
58
79
  */
59
80
  tag(tagName) {
60
- if (tagName[0] !== '@') {
61
- tagName = `@${tagName}`
62
- }
81
+ if (tagName[0] !== '@') tagName = `@${tagName}`
82
+ if (!this.suite.tags) this.suite.tags = []
63
83
  this.suite.tags.push(tagName)
64
84
  this.suite.title = `${this.suite.title.trim()} ${tagName}`
65
85
  return this
@@ -1,11 +1,13 @@
1
1
  const Gherkin = require('@cucumber/gherkin')
2
2
  const Messages = require('@cucumber/messages')
3
- const { Context, Suite, Test } = require('mocha')
3
+ const { Context, Suite } = require('mocha')
4
4
  const debug = require('debug')('codeceptjs:bdd')
5
5
 
6
+ const { enhanceMochaSuite } = require('./suite')
7
+ const { createTest } = require('./test')
6
8
  const { matchStep } = require('./bdd')
7
9
  const event = require('../event')
8
- const scenario = require('../scenario')
10
+ const { injected, setup, teardown, suiteSetup, suiteTeardown } = require('./asyncWrapper')
9
11
  const Step = require('../step')
10
12
  const DataTableArgument = require('../data/dataTableArgument')
11
13
  const transform = require('../transform')
@@ -28,7 +30,8 @@ module.exports = (text, file) => {
28
30
  throw new Error(`No 'Features' available in Gherkin '${file}' provided!`)
29
31
  }
30
32
  const suite = new Suite(ast.feature.name, new Context())
31
- const tags = ast.feature.tags.map((t) => t.name)
33
+ enhanceMochaSuite(suite)
34
+ const tags = ast.feature.tags.map(t => t.name)
32
35
  suite.title = `${suite.title} ${tags.join(' ')}`.trim()
33
36
  suite.tags = tags || []
34
37
  suite.comment = ast.feature.description
@@ -36,17 +39,17 @@ module.exports = (text, file) => {
36
39
  suite.file = file
37
40
  suite.timeout(0)
38
41
 
39
- suite.beforeEach('codeceptjs.before', () => scenario.setup(suite))
40
- suite.afterEach('codeceptjs.after', () => scenario.teardown(suite))
41
- suite.beforeAll('codeceptjs.beforeSuite', () => scenario.suiteSetup(suite))
42
- suite.afterAll('codeceptjs.afterSuite', () => scenario.suiteTeardown(suite))
42
+ suite.beforeEach('codeceptjs.before', () => setup(suite))
43
+ suite.afterEach('codeceptjs.after', () => teardown(suite))
44
+ suite.beforeAll('codeceptjs.beforeSuite', () => suiteSetup(suite))
45
+ suite.afterAll('codeceptjs.afterSuite', () => suiteTeardown(suite))
43
46
 
44
- const runSteps = async (steps) => {
47
+ const runSteps = async steps => {
45
48
  for (const step of steps) {
46
49
  const metaStep = new Step.MetaStep(null, step.text)
47
50
  metaStep.actor = step.keyword.trim()
48
51
  let helperStep
49
- const setMetaStep = (step) => {
52
+ const setMetaStep = step => {
50
53
  helperStep = step
51
54
  if (step.metaStep) {
52
55
  if (step.metaStep === metaStep) {
@@ -100,18 +103,13 @@ module.exports = (text, file) => {
100
103
  if (child.background) {
101
104
  suite.beforeEach(
102
105
  'Before',
103
- scenario.injected(async () => runSteps(child.background.steps), suite, 'before'),
106
+ injected(async () => runSteps(child.background.steps), suite, 'before'),
104
107
  )
105
108
  continue
106
109
  }
107
- if (
108
- child.scenario &&
109
- (currentLanguage
110
- ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline
111
- : child.scenario.keyword === 'Scenario Outline')
112
- ) {
110
+ if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline === child.scenario.keyword : child.scenario.keyword === 'Scenario Outline')) {
113
111
  for (const examples of child.scenario.examples) {
114
- const fields = examples.tableHeader.cells.map((c) => c.value)
112
+ const fields = examples.tableHeader.cells.map(c => c.value)
115
113
  for (const example of examples.tableBody) {
116
114
  let exampleSteps = [...child.scenario.steps]
117
115
  const current = {}
@@ -120,13 +118,13 @@ module.exports = (text, file) => {
120
118
  const value = transform('gherkin.examples', example.cells[index].value)
121
119
  example.cells[index].value = value
122
120
  current[placeholder] = value
123
- exampleSteps = exampleSteps.map((step) => {
121
+ exampleSteps = exampleSteps.map(step => {
124
122
  step = { ...step }
125
123
  step.text = step.text.split(`<${placeholder}>`).join(value)
126
124
  return step
127
125
  })
128
126
  }
129
- const tags = child.scenario.tags.map((t) => t.name).concat(examples.tags.map((t) => t.name))
127
+ const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name))
130
128
  let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim()
131
129
 
132
130
  for (const [key, value] of Object.entries(current)) {
@@ -135,22 +133,22 @@ module.exports = (text, file) => {
135
133
  }
136
134
  }
137
135
 
138
- const test = new Test(title, async () => runSteps(addExampleInTable(exampleSteps, current)))
136
+ const test = createTest(title, async () => runSteps(addExampleInTable(exampleSteps, current)))
137
+ test.addToSuite(suite)
139
138
  test.tags = suite.tags.concat(tags)
140
139
  test.file = file
141
- suite.addTest(scenario.test(test))
142
140
  }
143
141
  }
144
142
  continue
145
143
  }
146
144
 
147
145
  if (child.scenario) {
148
- const tags = child.scenario.tags.map((t) => t.name)
146
+ const tags = child.scenario.tags.map(t => t.name)
149
147
  const title = `${child.scenario.name} ${tags.join(' ')}`.trim()
150
- const test = new Test(title, async () => runSteps(child.scenario.steps))
148
+ const test = createTest(title, async () => runSteps(child.scenario.steps))
149
+ test.addToSuite(suite)
151
150
  test.tags = suite.tags.concat(tags)
152
151
  test.file = file
153
- suite.addTest(scenario.test(test))
154
152
  }
155
153
  }
156
154
 
@@ -162,8 +160,8 @@ function transformTable(table) {
162
160
  for (const id in table.rows) {
163
161
  const cells = table.rows[id].cells
164
162
  str += cells
165
- .map((c) => c.value)
166
- .map((c) => c.padEnd(15))
163
+ .map(c => c.value)
164
+ .map(c => c.padEnd(15))
167
165
  .join(' | ')
168
166
  str += '\n'
169
167
  }
@@ -172,12 +170,12 @@ function transformTable(table) {
172
170
  function addExampleInTable(exampleSteps, placeholders) {
173
171
  const steps = JSON.parse(JSON.stringify(exampleSteps))
174
172
  for (const placeholder in placeholders) {
175
- steps.map((step) => {
173
+ steps.map(step => {
176
174
  step = { ...step }
177
175
  if (step.dataTable) {
178
176
  for (const id in step.dataTable.rows) {
179
177
  const cells = step.dataTable.rows[id].cells
180
- cells.map((c) => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])))
178
+ cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])))
181
179
  }
182
180
  }
183
181
  return step