codeceptjs 4.0.0-beta.4 → 4.0.0-beta.6.esm-aria

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 (188) hide show
  1. package/README.md +89 -119
  2. package/bin/codecept.js +53 -54
  3. package/docs/webapi/clearCookie.mustache +1 -1
  4. package/lib/actor.js +70 -102
  5. package/lib/ai.js +131 -121
  6. package/lib/assert/empty.js +11 -12
  7. package/lib/assert/equal.js +16 -21
  8. package/lib/assert/error.js +2 -2
  9. package/lib/assert/include.js +11 -15
  10. package/lib/assert/throws.js +3 -5
  11. package/lib/assert/truth.js +10 -7
  12. package/lib/assert.js +18 -18
  13. package/lib/codecept.js +112 -101
  14. package/lib/colorUtils.js +48 -50
  15. package/lib/command/check.js +206 -0
  16. package/lib/command/configMigrate.js +13 -14
  17. package/lib/command/definitions.js +24 -36
  18. package/lib/command/dryRun.js +16 -16
  19. package/lib/command/generate.js +38 -39
  20. package/lib/command/gherkin/init.js +36 -38
  21. package/lib/command/gherkin/snippets.js +76 -74
  22. package/lib/command/gherkin/steps.js +21 -18
  23. package/lib/command/info.js +49 -15
  24. package/lib/command/init.js +41 -37
  25. package/lib/command/interactive.js +22 -13
  26. package/lib/command/list.js +11 -10
  27. package/lib/command/run-multiple/chunk.js +50 -47
  28. package/lib/command/run-multiple/collection.js +5 -5
  29. package/lib/command/run-multiple/run.js +3 -3
  30. package/lib/command/run-multiple.js +27 -47
  31. package/lib/command/run-rerun.js +6 -7
  32. package/lib/command/run-workers.js +15 -66
  33. package/lib/command/run.js +8 -8
  34. package/lib/command/utils.js +22 -21
  35. package/lib/command/workers/runTests.js +131 -241
  36. package/lib/config.js +111 -49
  37. package/lib/container.js +589 -244
  38. package/lib/data/context.js +16 -18
  39. package/lib/data/dataScenarioConfig.js +9 -9
  40. package/lib/data/dataTableArgument.js +7 -7
  41. package/lib/data/table.js +6 -12
  42. package/lib/effects.js +307 -0
  43. package/lib/els.js +160 -0
  44. package/lib/event.js +24 -19
  45. package/lib/globals.js +141 -0
  46. package/lib/heal.js +89 -81
  47. package/lib/helper/AI.js +3 -2
  48. package/lib/helper/ApiDataFactory.js +19 -19
  49. package/lib/helper/Appium.js +47 -51
  50. package/lib/helper/FileSystem.js +35 -15
  51. package/lib/helper/GraphQL.js +1 -1
  52. package/lib/helper/GraphQLDataFactory.js +4 -4
  53. package/lib/helper/JSONResponse.js +72 -45
  54. package/lib/helper/Mochawesome.js +14 -11
  55. package/lib/helper/Playwright.js +832 -434
  56. package/lib/helper/Puppeteer.js +393 -292
  57. package/lib/helper/REST.js +32 -27
  58. package/lib/helper/WebDriver.js +320 -219
  59. package/lib/helper/errors/ConnectionRefused.js +6 -6
  60. package/lib/helper/errors/ElementAssertion.js +11 -16
  61. package/lib/helper/errors/ElementNotFound.js +5 -9
  62. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  63. package/lib/helper/extras/Console.js +11 -11
  64. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  65. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  66. package/lib/helper/extras/PlaywrightRestartOpts.js +23 -23
  67. package/lib/helper/extras/Popup.js +22 -22
  68. package/lib/helper/extras/React.js +29 -30
  69. package/lib/helper/network/actions.js +33 -48
  70. package/lib/helper/network/utils.js +76 -83
  71. package/lib/helper/scripts/blurElement.js +6 -6
  72. package/lib/helper/scripts/focusElement.js +6 -6
  73. package/lib/helper/scripts/highlightElement.js +9 -9
  74. package/lib/helper/scripts/isElementClickable.js +34 -34
  75. package/lib/helper.js +2 -1
  76. package/lib/history.js +23 -20
  77. package/lib/hooks.js +10 -10
  78. package/lib/html.js +90 -100
  79. package/lib/index.js +48 -21
  80. package/lib/listener/config.js +8 -9
  81. package/lib/listener/emptyRun.js +54 -0
  82. package/lib/listener/exit.js +10 -12
  83. package/lib/listener/{retry.js → globalRetry.js} +10 -10
  84. package/lib/listener/globalTimeout.js +166 -0
  85. package/lib/listener/helpers.js +43 -24
  86. package/lib/listener/mocha.js +4 -5
  87. package/lib/listener/result.js +11 -0
  88. package/lib/listener/steps.js +26 -23
  89. package/lib/listener/store.js +20 -0
  90. package/lib/locator.js +213 -192
  91. package/lib/mocha/asyncWrapper.js +264 -0
  92. package/lib/mocha/bdd.js +167 -0
  93. package/lib/mocha/cli.js +341 -0
  94. package/lib/mocha/factory.js +160 -0
  95. package/lib/{interfaces → mocha}/featureConfig.js +33 -13
  96. package/lib/{interfaces → mocha}/gherkin.js +75 -45
  97. package/lib/mocha/hooks.js +121 -0
  98. package/lib/mocha/index.js +21 -0
  99. package/lib/mocha/inject.js +46 -0
  100. package/lib/{interfaces → mocha}/scenarioConfig.js +32 -8
  101. package/lib/mocha/suite.js +89 -0
  102. package/lib/mocha/test.js +178 -0
  103. package/lib/mocha/types.d.ts +42 -0
  104. package/lib/mocha/ui.js +229 -0
  105. package/lib/output.js +86 -64
  106. package/lib/parser.js +44 -44
  107. package/lib/pause.js +160 -139
  108. package/lib/plugin/analyze.js +403 -0
  109. package/lib/plugin/{autoLogin.js → auth.js} +137 -43
  110. package/lib/plugin/autoDelay.js +19 -15
  111. package/lib/plugin/coverage.js +22 -27
  112. package/lib/plugin/customLocator.js +5 -5
  113. package/lib/plugin/customReporter.js +53 -0
  114. package/lib/plugin/heal.js +49 -17
  115. package/lib/plugin/pageInfo.js +140 -0
  116. package/lib/plugin/pauseOnFail.js +4 -3
  117. package/lib/plugin/retryFailedStep.js +60 -19
  118. package/lib/plugin/screenshotOnFail.js +80 -83
  119. package/lib/plugin/stepByStepReport.js +70 -31
  120. package/lib/plugin/stepTimeout.js +7 -13
  121. package/lib/plugin/subtitles.js +10 -9
  122. package/lib/recorder.js +167 -126
  123. package/lib/rerun.js +94 -50
  124. package/lib/result.js +161 -0
  125. package/lib/secret.js +18 -17
  126. package/lib/session.js +95 -89
  127. package/lib/step/base.js +239 -0
  128. package/lib/step/comment.js +10 -0
  129. package/lib/step/config.js +50 -0
  130. package/lib/step/func.js +46 -0
  131. package/lib/step/helper.js +50 -0
  132. package/lib/step/meta.js +99 -0
  133. package/lib/step/record.js +74 -0
  134. package/lib/step/retry.js +11 -0
  135. package/lib/step/section.js +55 -0
  136. package/lib/step.js +18 -332
  137. package/lib/steps.js +54 -0
  138. package/lib/store.js +37 -5
  139. package/lib/template/heal.js +2 -11
  140. package/lib/timeout.js +60 -0
  141. package/lib/transform.js +8 -8
  142. package/lib/translation.js +32 -18
  143. package/lib/utils.js +354 -250
  144. package/lib/workerStorage.js +16 -16
  145. package/lib/workers.js +366 -282
  146. package/package.json +107 -95
  147. package/translations/de-DE.js +5 -4
  148. package/translations/fr-FR.js +5 -4
  149. package/translations/index.js +23 -9
  150. package/translations/it-IT.js +5 -4
  151. package/translations/ja-JP.js +5 -4
  152. package/translations/nl-NL.js +76 -0
  153. package/translations/pl-PL.js +5 -4
  154. package/translations/pt-BR.js +5 -4
  155. package/translations/ru-RU.js +5 -4
  156. package/translations/utils.js +18 -0
  157. package/translations/zh-CN.js +5 -4
  158. package/translations/zh-TW.js +5 -4
  159. package/typings/index.d.ts +177 -186
  160. package/typings/promiseBasedTypes.d.ts +3573 -5941
  161. package/typings/types.d.ts +4042 -6370
  162. package/lib/cli.js +0 -256
  163. package/lib/helper/ExpectHelper.js +0 -391
  164. package/lib/helper/Nightmare.js +0 -1504
  165. package/lib/helper/Protractor.js +0 -1863
  166. package/lib/helper/SoftExpectHelper.js +0 -381
  167. package/lib/helper/TestCafe.js +0 -1414
  168. package/lib/helper/clientscripts/nightmare.js +0 -213
  169. package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -43
  170. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  171. package/lib/helper/testcafe/testcafe-utils.js +0 -62
  172. package/lib/interfaces/bdd.js +0 -81
  173. package/lib/listener/artifacts.js +0 -19
  174. package/lib/listener/timeout.js +0 -109
  175. package/lib/mochaFactory.js +0 -113
  176. package/lib/plugin/allure.js +0 -15
  177. package/lib/plugin/commentStep.js +0 -136
  178. package/lib/plugin/debugErrors.js +0 -67
  179. package/lib/plugin/eachElement.js +0 -127
  180. package/lib/plugin/fakerTransform.js +0 -49
  181. package/lib/plugin/retryTo.js +0 -127
  182. package/lib/plugin/selenoid.js +0 -384
  183. package/lib/plugin/standardActingHelpers.js +0 -3
  184. package/lib/plugin/tryTo.js +0 -115
  185. package/lib/plugin/wdio.js +0 -249
  186. package/lib/scenario.js +0 -224
  187. package/lib/ui.js +0 -236
  188. package/lib/within.js +0 -70
@@ -0,0 +1,341 @@
1
+ import mocha from 'mocha'
2
+ const { reporters: { Base } } = mocha
3
+ import ms from 'ms'
4
+ import figures from 'figures'
5
+ import { readFileSync } from 'fs'
6
+ import { fileURLToPath } from 'url'
7
+ import { dirname, join } from 'path'
8
+ import event from '../event.js'
9
+ import AssertionFailedError from '../assert/error.js'
10
+ import output from '../output.js'
11
+ import test, { cloneTest } from './test.js'
12
+
13
+ // Get version from package.json to avoid circular dependency
14
+ const __filename = fileURLToPath(import.meta.url)
15
+ const __dirname = dirname(__filename)
16
+ const packagePath = join(__dirname, '../../package.json')
17
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'))
18
+ const codeceptVersion = packageJson.version
19
+ const cursor = Base.cursor
20
+ let currentMetaStep = []
21
+ let codeceptjsEventDispatchersRegistered = false
22
+
23
+ // Lazy container loading to avoid circular dependencies
24
+ let containerPromise = null
25
+ const getContainer = () => {
26
+ if (!containerPromise) {
27
+ containerPromise = import('../container.js').then(module => module.default || module)
28
+ }
29
+ return containerPromise
30
+ }
31
+
32
+ class Cli extends Base {
33
+ constructor(runner, opts) {
34
+ super(runner)
35
+ let level = 0
36
+ this.loadedTests = []
37
+ opts = opts.reporterOptions || opts
38
+ if (opts.steps) level = 1
39
+ if (opts.debug) level = 2
40
+ if (opts.verbose) level = 3
41
+ output.level(level)
42
+ output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
43
+ output.print(`Using test root "${global.codecept_dir}"`)
44
+
45
+ const showSteps = level >= 1
46
+
47
+ if (level >= 2) {
48
+ // Load container asynchronously to avoid circular dependency
49
+ getContainer().then(Container => {
50
+ output.print(output.styles.debug(`Helpers: ${Object.keys(Container.helpers()).join(', ')}`))
51
+ output.print(output.styles.debug(`Plugins: ${Object.keys(Container.plugins()).join(', ')}`))
52
+ }).catch(() => {
53
+ // Silently fail if container can't be loaded
54
+ output.print(output.styles.debug('Helpers: [loading...]'))
55
+ output.print(output.styles.debug('Plugins: [loading...]'))
56
+ })
57
+ }
58
+
59
+ if (level >= 3) {
60
+ process.on('warning', warning => {
61
+ console.log('\nWarning Details:')
62
+ console.log('Name:', warning.name)
63
+ console.log('Message:', warning.message)
64
+ console.log('Stack:', warning.stack)
65
+ console.log('-------------------')
66
+ })
67
+ }
68
+
69
+ runner.on('start', () => {
70
+ console.log()
71
+ })
72
+
73
+ runner.on('suite', suite => {
74
+ output.suite.started(suite)
75
+ })
76
+
77
+ runner.on('fail', test => {
78
+ if (test.ctx.currentTest) {
79
+ this.loadedTests.push(test.ctx.currentTest.uid)
80
+ }
81
+ if (showSteps && test.steps) {
82
+ return output.scenario.failed(test)
83
+ }
84
+ cursor.CR()
85
+ output.test.failed(test)
86
+ })
87
+
88
+ runner.on('pending', test => {
89
+ if (test.parent && test.parent.pending) {
90
+ const suite = test.parent
91
+ const skipInfo = suite.opts.skipInfo || {}
92
+ skipTestConfig(test, skipInfo.message)
93
+ } else {
94
+ skipTestConfig(test, null)
95
+ }
96
+ this.loadedTests.push(test.uid)
97
+ cursor.CR()
98
+ output.test.skipped(test)
99
+ })
100
+
101
+ runner.on('pass', test => {
102
+ if (showSteps && test.steps) {
103
+ return output.scenario.passed(test)
104
+ }
105
+ cursor.CR()
106
+ output.test.passed(test)
107
+ })
108
+
109
+ if (showSteps) {
110
+ runner.on('test', test => {
111
+ currentMetaStep = []
112
+ if (test.steps) {
113
+ output.test.started(test)
114
+ }
115
+ })
116
+
117
+ if (!codeceptjsEventDispatchersRegistered) {
118
+ codeceptjsEventDispatchersRegistered = true
119
+
120
+ event.dispatcher.on(event.bddStep.started, step => {
121
+ output.stepShift = 2
122
+ output.step(step)
123
+ })
124
+
125
+ event.dispatcher.on(event.step.started, step => {
126
+ let processingStep = step
127
+ const metaSteps = []
128
+ let isHidden = false
129
+ while (processingStep.metaStep) {
130
+ metaSteps.unshift(processingStep.metaStep)
131
+ processingStep = processingStep.metaStep
132
+ if (processingStep.collapsed) isHidden = true
133
+ }
134
+ const shift = metaSteps.length
135
+
136
+ for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
137
+ if (currentMetaStep[i] !== metaSteps[i]) {
138
+ output.stepShift = 3 + 2 * i
139
+ if (!metaSteps[i]) continue
140
+ // bdd steps are handled by bddStep.started
141
+ if (metaSteps[i].isBDD()) continue
142
+ output.step(metaSteps[i])
143
+ }
144
+ }
145
+ currentMetaStep = metaSteps
146
+ if (isHidden) return
147
+ output.stepShift = 3 + 2 * shift
148
+ output.step(step)
149
+ })
150
+
151
+ event.dispatcher.on(event.step.finished, () => {
152
+ output.stepShift = 0
153
+ })
154
+ }
155
+ }
156
+
157
+ runner.on('suite end', suite => {
158
+ let skippedCount = 0
159
+ const grep = runner._grep
160
+ for (const test of suite.tests) {
161
+ if (!test.state && !this.loadedTests.includes(test.uid)) {
162
+ if (matchTest(grep, test.title)) {
163
+ if (!test.opts) {
164
+ test.opts = {}
165
+ }
166
+ if (!test.opts.skipInfo) {
167
+ test.opts.skipInfo = {}
168
+ }
169
+ skipTestConfig(test, "Skipped due to failure in 'before' hook")
170
+ output.test.skipped(test)
171
+ skippedCount += 1
172
+ }
173
+ }
174
+ }
175
+
176
+ getContainer().then(Container => {
177
+ Container.result().addStats({ pending: skippedCount, tests: skippedCount })
178
+ }).catch(() => {
179
+ // Silently fail if container can't be loaded
180
+ })
181
+ })
182
+
183
+ runner.on('end', this.result.bind(this))
184
+ }
185
+
186
+ async result() {
187
+ try {
188
+ const Container = await getContainer()
189
+ Container.result().addStats(this.stats)
190
+ Container.result().finish()
191
+
192
+ const stats = Container.result().stats
193
+ console.log()
194
+
195
+ // passes
196
+ if (stats.failures) {
197
+ output.print(output.styles.bold('-- FAILURES:'))
198
+ }
199
+
200
+ const failuresLog = []
201
+
202
+ // failures
203
+ if (stats.failures) {
204
+ // append step traces
205
+ this.failures = this.failures.map(test => {
206
+ // we will change the stack trace, so we need to clone the test
207
+ const err = test.err
208
+
209
+ let log = ''
210
+ let originalMessage = err.message
211
+
212
+ if (err instanceof AssertionFailedError) {
213
+ err.message = err.inspect()
214
+ }
215
+
216
+ // multi-line error messages (for Playwright)
217
+ if (err.message && err.message.includes('\n')) {
218
+ const lines = err.message.split('\n')
219
+ const truncatedLines = lines.slice(0, 5)
220
+ if (lines.length > 5) {
221
+ truncatedLines.push('...')
222
+ }
223
+ err.message = truncatedLines.join('\n').replace(/^/gm, ' ').trim()
224
+ }
225
+
226
+ // add new line before the message
227
+ err.message = '\n ' + err.message
228
+
229
+ // explicitly show file with error
230
+ if (test.file) {
231
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} ${output.styles.basic(test.file)}\n`
232
+ }
233
+
234
+ const steps = test.steps || (test.ctx && test.ctx.test.steps)
235
+
236
+ if (steps && steps.length) {
237
+ let scenarioTrace = ''
238
+ steps
239
+ .reverse()
240
+ .slice(0, 10)
241
+ .forEach(step => {
242
+ const hasFailed = step.status === 'failed'
243
+ let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
244
+ if (hasFailed) line = output.styles.bold(line)
245
+ scenarioTrace += `\n${line}`
246
+ })
247
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
248
+ }
249
+
250
+ // display artifacts in debug mode
251
+ if (test?.artifacts && Object.keys(test.artifacts).length) {
252
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Artifacts:')}`
253
+ for (const artifact of Object.keys(test.artifacts)) {
254
+ log += `\n- ${artifact}: ${test.artifacts[artifact]}`
255
+ }
256
+ }
257
+
258
+ // display metadata
259
+ if (test.meta && Object.keys(test.meta).length) {
260
+ log += `\n\n${output.styles.basic(figures.circle)} ${output.styles.section('Metadata:')}`
261
+ for (const [key, value] of Object.entries(test.meta)) {
262
+ log += `\n- ${key}: ${value}`
263
+ }
264
+ }
265
+
266
+ try {
267
+ let stack = err.stack
268
+ stack = (stack || '').replace(originalMessage, '')
269
+ stack = stack ? stack.split('\n') : []
270
+
271
+ if (stack[0] && stack[0].includes(err.message)) {
272
+ stack.shift()
273
+ }
274
+
275
+ if (stack[0] && stack[0].trim() == 'Error:') {
276
+ stack.shift()
277
+ }
278
+
279
+ if (output.level() < 3) {
280
+ stack = stack.slice(0, 3)
281
+ }
282
+
283
+ err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`
284
+ } catch (e) {
285
+ console.error(e)
286
+ }
287
+
288
+ // we will change the stack trace, so we need to clone the test
289
+ test = cloneTest(test)
290
+ test.err = err
291
+ return test
292
+ })
293
+
294
+ const originalLog = Base.consoleLog
295
+ Base.consoleLog = (...data) => {
296
+ failuresLog.push([...data])
297
+ originalLog(...data)
298
+ }
299
+ Base.list(this.failures)
300
+ Base.consoleLog = originalLog
301
+ console.log()
302
+ }
303
+
304
+ Container.result().addFailures(failuresLog)
305
+
306
+ output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks)
307
+
308
+ if (stats.failures && output.level() < 3) {
309
+ output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace'))
310
+ }
311
+ } catch (error) {
312
+ // Fallback behavior if container can't be loaded
313
+ console.log('Error loading container:', error.message)
314
+ output.result(0, 1, 0, 0, 0)
315
+ }
316
+ }
317
+ }
318
+
319
+ function matchTest(grep, test) {
320
+ if (grep) {
321
+ return grep.test(test)
322
+ }
323
+ return true
324
+ }
325
+
326
+ function skipTestConfig(test, message) {
327
+ if (!test.opts) {
328
+ test.opts = {}
329
+ }
330
+ if (!test.opts.skipInfo) {
331
+ test.opts.skipInfo = {}
332
+ }
333
+ test.opts.skipInfo.message = test.opts.skipInfo.message || message
334
+ test.opts.skipInfo.isFastSkipped = true
335
+ event.emit(event.test.skipped, test)
336
+ test.state = 'skipped'
337
+ }
338
+
339
+ export default function (runner, opts) {
340
+ return new Cli(runner, opts)
341
+ }
@@ -0,0 +1,160 @@
1
+ import Mocha from 'mocha'
2
+ import fsPath from 'path'
3
+ import fs from 'fs'
4
+ import { fileURLToPath } from 'url'
5
+ import reporter from './cli.js'
6
+ import gherkinParser, { loadTranslations } from './gherkin.js'
7
+ import output from '../output.js'
8
+ import scenarioUiFunction from './ui.js'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = fsPath.dirname(__filename)
12
+
13
+ let mocha
14
+
15
+ class MochaFactory {
16
+ static create(config, opts) {
17
+ mocha = new Mocha(Object.assign(config, opts))
18
+ output.process(opts.child)
19
+ mocha.ui(scenarioUiFunction)
20
+
21
+ // Manually trigger UI setup for globals to be available in ESM context
22
+ // This ensures Feature, Scenario, Before, etc. are available immediately
23
+ if (mocha.suite && mocha.suite.emit) {
24
+ const context = {}
25
+ mocha.suite.emit('pre-require', context, '', mocha)
26
+ }
27
+
28
+ Mocha.Runner.prototype.uncaught = function (err) {
29
+ if (err) {
30
+ if (err.toString().indexOf('ECONNREFUSED') >= 0) {
31
+ // Handle ECONNREFUSED without dynamic import for now
32
+ err = new Error('Connection refused: ' + err.toString())
33
+ }
34
+ output.error(err)
35
+ output.print(err.stack)
36
+ process.exit(1)
37
+ }
38
+ output.error('Uncaught undefined exception')
39
+ process.exit(1)
40
+ }
41
+
42
+ // Override loadFiles to handle feature files
43
+ const originalLoadFiles = Mocha.prototype.loadFiles
44
+ mocha.loadFiles = function (fn) {
45
+ // load features
46
+ const featureFiles = this.files.filter(file => file.match(/\.feature$/))
47
+ if (featureFiles.length > 0) {
48
+ // Load translations for Gherkin features
49
+ loadTranslations().catch(() => {
50
+ // Ignore if translations can't be loaded
51
+ })
52
+
53
+ for (const file of featureFiles) {
54
+ const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
55
+ this.suite.addSuite(suite)
56
+ }
57
+
58
+ // remove feature files
59
+ const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
60
+ this.files = this.files.filter(file => !file.match(/\.feature$/))
61
+
62
+ // Load JavaScript test files using ESM imports
63
+ if (jsFiles.length > 0) {
64
+ try {
65
+ // Try original loadFiles first for compatibility
66
+ originalLoadFiles.call(this, fn)
67
+ } catch (e) {
68
+ // If original loadFiles fails, load ESM files manually
69
+ if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
70
+ // Load ESM files by importing them synchronously using top-level await workaround
71
+ for (const file of jsFiles) {
72
+ try {
73
+ // Convert file path to file:// URL for dynamic import
74
+ const fileUrl = `file://${file}`
75
+ // Use import() but don't await it - let it load in the background
76
+ import(fileUrl).catch(importErr => {
77
+ // If dynamic import fails, the file may have syntax errors or other issues
78
+ console.error(`Failed to load test file ${file}:`, importErr.message)
79
+ })
80
+ if (fn) fn()
81
+ } catch (fileErr) {
82
+ console.error(`Error processing test file ${file}:`, fileErr.message)
83
+ if (fn) fn(fileErr)
84
+ }
85
+ }
86
+ } else {
87
+ throw e
88
+ }
89
+ }
90
+ }
91
+
92
+ // add ids for each test and check uniqueness
93
+ const dupes = []
94
+ let missingFeatureInFile = []
95
+ const seenTests = []
96
+ this.suite.eachTest(test => {
97
+ if (!test) {
98
+ return // Skip undefined tests
99
+ }
100
+ const name = test.fullTitle()
101
+ if (seenTests.includes(test.uid)) {
102
+ dupes.push(name)
103
+ }
104
+ seenTests.push(test.uid)
105
+
106
+ if (name.slice(0, name.indexOf(':')) === '') {
107
+ missingFeatureInFile.push(test.file)
108
+ }
109
+ })
110
+ if (dupes.length) {
111
+ // ideally this should be no-op and throw (breaking change)...
112
+ output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
113
+ }
114
+
115
+ if (missingFeatureInFile.length) {
116
+ missingFeatureInFile = [...new Set(missingFeatureInFile)]
117
+ output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
118
+ }
119
+ } else {
120
+ // Use original for non-feature files
121
+ originalLoadFiles.call(this, fn)
122
+ }
123
+ }
124
+
125
+ const presetReporter = opts.reporter || config.reporter
126
+ // use standard reporter
127
+ if (!presetReporter) {
128
+ mocha.reporter(reporter, opts)
129
+ return mocha
130
+ }
131
+
132
+ // load custom reporter with options
133
+ const reporterOptions = Object.assign(config.reporterOptions || {})
134
+
135
+ if (opts.reporterOptions !== undefined) {
136
+ opts.reporterOptions.split(',').forEach(opt => {
137
+ const L = opt.split('=')
138
+ if (L.length > 2 || L.length === 0) {
139
+ throw new Error(`invalid reporter option '${opt}'`)
140
+ } else if (L.length === 2) {
141
+ reporterOptions[L[0]] = L[1]
142
+ } else {
143
+ reporterOptions[L[0]] = true
144
+ }
145
+ })
146
+ }
147
+
148
+ const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter')
149
+ if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
150
+ Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes)
151
+ delete reporterOptions['codeceptjs-cli-reporter']
152
+ }
153
+
154
+ // custom reporters
155
+ mocha.reporter(presetReporter, reporterOptions)
156
+ return mocha
157
+ }
158
+ }
159
+
160
+ export default 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,13 +78,12 @@ 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
66
86
  }
67
87
  }
68
88
 
69
- module.exports = FeatureConfig
89
+ export default FeatureConfig