codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20

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 (209) hide show
  1. package/README.md +133 -120
  2. package/bin/codecept.js +107 -96
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/docs/webapi/click.mustache +5 -1
  6. package/lib/actor.js +71 -103
  7. package/lib/ai.js +159 -188
  8. package/lib/assert/empty.js +22 -24
  9. package/lib/assert/equal.js +30 -37
  10. package/lib/assert/error.js +14 -14
  11. package/lib/assert/include.js +43 -48
  12. package/lib/assert/throws.js +11 -11
  13. package/lib/assert/truth.js +22 -22
  14. package/lib/assert.js +20 -18
  15. package/lib/codecept.js +262 -162
  16. package/lib/colorUtils.js +50 -52
  17. package/lib/command/check.js +206 -0
  18. package/lib/command/configMigrate.js +56 -51
  19. package/lib/command/definitions.js +96 -109
  20. package/lib/command/dryRun.js +77 -79
  21. package/lib/command/generate.js +234 -194
  22. package/lib/command/gherkin/init.js +42 -33
  23. package/lib/command/gherkin/snippets.js +76 -74
  24. package/lib/command/gherkin/steps.js +20 -17
  25. package/lib/command/info.js +74 -38
  26. package/lib/command/init.js +301 -290
  27. package/lib/command/interactive.js +41 -32
  28. package/lib/command/list.js +28 -27
  29. package/lib/command/run-multiple/chunk.js +51 -48
  30. package/lib/command/run-multiple/collection.js +5 -5
  31. package/lib/command/run-multiple/run.js +5 -1
  32. package/lib/command/run-multiple.js +97 -97
  33. package/lib/command/run-rerun.js +19 -25
  34. package/lib/command/run-workers.js +68 -92
  35. package/lib/command/run.js +39 -27
  36. package/lib/command/utils.js +80 -64
  37. package/lib/command/workers/runTests.js +388 -226
  38. package/lib/config.js +109 -50
  39. package/lib/container.js +641 -261
  40. package/lib/data/context.js +60 -61
  41. package/lib/data/dataScenarioConfig.js +47 -47
  42. package/lib/data/dataTableArgument.js +32 -32
  43. package/lib/data/table.js +22 -22
  44. package/lib/effects.js +307 -0
  45. package/lib/element/WebElement.js +327 -0
  46. package/lib/els.js +160 -0
  47. package/lib/event.js +173 -163
  48. package/lib/globals.js +141 -0
  49. package/lib/heal.js +89 -85
  50. package/lib/helper/AI.js +131 -41
  51. package/lib/helper/ApiDataFactory.js +107 -75
  52. package/lib/helper/Appium.js +542 -404
  53. package/lib/helper/FileSystem.js +100 -79
  54. package/lib/helper/GraphQL.js +44 -43
  55. package/lib/helper/GraphQLDataFactory.js +52 -52
  56. package/lib/helper/JSONResponse.js +126 -88
  57. package/lib/helper/Mochawesome.js +54 -29
  58. package/lib/helper/Playwright.js +2547 -1316
  59. package/lib/helper/Puppeteer.js +1578 -1181
  60. package/lib/helper/REST.js +209 -68
  61. package/lib/helper/WebDriver.js +1482 -1342
  62. package/lib/helper/errors/ConnectionRefused.js +6 -6
  63. package/lib/helper/errors/ElementAssertion.js +11 -16
  64. package/lib/helper/errors/ElementNotFound.js +5 -9
  65. package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
  66. package/lib/helper/extras/Console.js +11 -11
  67. package/lib/helper/extras/PlaywrightLocator.js +110 -0
  68. package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
  69. package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
  70. package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
  71. package/lib/helper/extras/Popup.js +22 -22
  72. package/lib/helper/extras/React.js +27 -28
  73. package/lib/helper/network/actions.js +36 -42
  74. package/lib/helper/network/utils.js +78 -84
  75. package/lib/helper/scripts/blurElement.js +5 -5
  76. package/lib/helper/scripts/focusElement.js +5 -5
  77. package/lib/helper/scripts/highlightElement.js +8 -8
  78. package/lib/helper/scripts/isElementClickable.js +34 -34
  79. package/lib/helper.js +2 -3
  80. package/lib/history.js +23 -19
  81. package/lib/hooks.js +8 -8
  82. package/lib/html.js +94 -104
  83. package/lib/index.js +38 -27
  84. package/lib/listener/config.js +30 -23
  85. package/lib/listener/emptyRun.js +54 -0
  86. package/lib/listener/enhancedGlobalRetry.js +110 -0
  87. package/lib/listener/exit.js +16 -18
  88. package/lib/listener/globalRetry.js +70 -0
  89. package/lib/listener/globalTimeout.js +181 -0
  90. package/lib/listener/helpers.js +76 -51
  91. package/lib/listener/mocha.js +10 -11
  92. package/lib/listener/result.js +11 -0
  93. package/lib/listener/retryEnhancer.js +85 -0
  94. package/lib/listener/steps.js +71 -59
  95. package/lib/listener/store.js +20 -0
  96. package/lib/locator.js +214 -197
  97. package/lib/mocha/asyncWrapper.js +274 -0
  98. package/lib/mocha/bdd.js +167 -0
  99. package/lib/mocha/cli.js +341 -0
  100. package/lib/mocha/factory.js +163 -0
  101. package/lib/mocha/featureConfig.js +89 -0
  102. package/lib/mocha/gherkin.js +231 -0
  103. package/lib/mocha/hooks.js +121 -0
  104. package/lib/mocha/index.js +21 -0
  105. package/lib/mocha/inject.js +46 -0
  106. package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
  107. package/lib/mocha/suite.js +89 -0
  108. package/lib/mocha/test.js +184 -0
  109. package/lib/mocha/types.d.ts +42 -0
  110. package/lib/mocha/ui.js +242 -0
  111. package/lib/output.js +141 -71
  112. package/lib/parser.js +47 -44
  113. package/lib/pause.js +173 -145
  114. package/lib/plugin/analyze.js +403 -0
  115. package/lib/plugin/{autoLogin.js → auth.js} +178 -79
  116. package/lib/plugin/autoDelay.js +36 -40
  117. package/lib/plugin/coverage.js +131 -78
  118. package/lib/plugin/customLocator.js +22 -21
  119. package/lib/plugin/customReporter.js +53 -0
  120. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  121. package/lib/plugin/heal.js +101 -110
  122. package/lib/plugin/htmlReporter.js +3648 -0
  123. package/lib/plugin/pageInfo.js +140 -0
  124. package/lib/plugin/pauseOnFail.js +12 -11
  125. package/lib/plugin/retryFailedStep.js +82 -47
  126. package/lib/plugin/screenshotOnFail.js +111 -92
  127. package/lib/plugin/stepByStepReport.js +159 -101
  128. package/lib/plugin/stepTimeout.js +20 -25
  129. package/lib/plugin/subtitles.js +38 -38
  130. package/lib/recorder.js +193 -130
  131. package/lib/rerun.js +94 -49
  132. package/lib/result.js +238 -0
  133. package/lib/retryCoordinator.js +207 -0
  134. package/lib/secret.js +20 -18
  135. package/lib/session.js +95 -89
  136. package/lib/step/base.js +239 -0
  137. package/lib/step/comment.js +10 -0
  138. package/lib/step/config.js +50 -0
  139. package/lib/step/func.js +46 -0
  140. package/lib/step/helper.js +50 -0
  141. package/lib/step/meta.js +99 -0
  142. package/lib/step/record.js +74 -0
  143. package/lib/step/retry.js +11 -0
  144. package/lib/step/section.js +55 -0
  145. package/lib/step.js +18 -329
  146. package/lib/steps.js +54 -0
  147. package/lib/store.js +38 -7
  148. package/lib/template/heal.js +3 -12
  149. package/lib/template/prompts/generatePageObject.js +31 -0
  150. package/lib/template/prompts/healStep.js +13 -0
  151. package/lib/template/prompts/writeStep.js +9 -0
  152. package/lib/test-server.js +334 -0
  153. package/lib/timeout.js +60 -0
  154. package/lib/transform.js +8 -8
  155. package/lib/translation.js +34 -21
  156. package/lib/utils/loaderCheck.js +124 -0
  157. package/lib/utils/mask_data.js +47 -0
  158. package/lib/utils/typescript.js +237 -0
  159. package/lib/utils.js +411 -228
  160. package/lib/workerStorage.js +37 -34
  161. package/lib/workers.js +532 -296
  162. package/package.json +124 -95
  163. package/translations/de-DE.js +5 -3
  164. package/translations/fr-FR.js +5 -4
  165. package/translations/index.js +22 -12
  166. package/translations/it-IT.js +4 -3
  167. package/translations/ja-JP.js +4 -3
  168. package/translations/nl-NL.js +76 -0
  169. package/translations/pl-PL.js +4 -3
  170. package/translations/pt-BR.js +4 -3
  171. package/translations/ru-RU.js +4 -3
  172. package/translations/utils.js +10 -0
  173. package/translations/zh-CN.js +4 -3
  174. package/translations/zh-TW.js +4 -3
  175. package/typings/index.d.ts +546 -185
  176. package/typings/promiseBasedTypes.d.ts +150 -875
  177. package/typings/types.d.ts +547 -992
  178. package/lib/cli.js +0 -249
  179. package/lib/dirname.js +0 -5
  180. package/lib/helper/Expect.js +0 -425
  181. package/lib/helper/ExpectHelper.js +0 -399
  182. package/lib/helper/MockServer.js +0 -223
  183. package/lib/helper/Nightmare.js +0 -1411
  184. package/lib/helper/Protractor.js +0 -1835
  185. package/lib/helper/SoftExpectHelper.js +0 -381
  186. package/lib/helper/TestCafe.js +0 -1410
  187. package/lib/helper/clientscripts/nightmare.js +0 -213
  188. package/lib/helper/testcafe/testControllerHolder.js +0 -42
  189. package/lib/helper/testcafe/testcafe-utils.js +0 -63
  190. package/lib/interfaces/bdd.js +0 -98
  191. package/lib/interfaces/featureConfig.js +0 -69
  192. package/lib/interfaces/gherkin.js +0 -195
  193. package/lib/listener/artifacts.js +0 -19
  194. package/lib/listener/retry.js +0 -68
  195. package/lib/listener/timeout.js +0 -109
  196. package/lib/mochaFactory.js +0 -110
  197. package/lib/plugin/allure.js +0 -15
  198. package/lib/plugin/commentStep.js +0 -136
  199. package/lib/plugin/debugErrors.js +0 -67
  200. package/lib/plugin/eachElement.js +0 -127
  201. package/lib/plugin/fakerTransform.js +0 -49
  202. package/lib/plugin/retryTo.js +0 -121
  203. package/lib/plugin/selenoid.js +0 -371
  204. package/lib/plugin/standardActingHelpers.js +0 -9
  205. package/lib/plugin/tryTo.js +0 -105
  206. package/lib/plugin/wdio.js +0 -246
  207. package/lib/scenario.js +0 -222
  208. package/lib/ui.js +0 -238
  209. 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:')} file://${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,163 @@
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
+ import { initMochaGlobals } from '../globals.js'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = fsPath.dirname(__filename)
13
+
14
+ let mocha
15
+
16
+ class MochaFactory {
17
+ static create(config, opts) {
18
+ mocha = new Mocha(Object.assign(config, opts))
19
+ output.process(opts.child)
20
+ mocha.ui(scenarioUiFunction)
21
+
22
+ // Manually trigger UI setup for globals to be available in ESM context
23
+ // This ensures Feature, Scenario, Before, etc. are available immediately
24
+ if (mocha.suite && mocha.suite.emit) {
25
+ const context = {}
26
+ mocha.suite.emit('pre-require', context, '', mocha)
27
+ // Also set globals immediately so they're available when ESM modules load
28
+ initMochaGlobals(context)
29
+ }
30
+
31
+ Mocha.Runner.prototype.uncaught = function (err) {
32
+ if (err) {
33
+ if (err.toString().indexOf('ECONNREFUSED') >= 0) {
34
+ // Handle ECONNREFUSED without dynamic import for now
35
+ err = new Error('Connection refused: ' + err.toString())
36
+ }
37
+ output.error(err)
38
+ output.print(err.stack)
39
+ process.exit(1)
40
+ }
41
+ output.error('Uncaught undefined exception')
42
+ process.exit(1)
43
+ }
44
+
45
+ // Override loadFiles to handle feature files
46
+ const originalLoadFiles = Mocha.prototype.loadFiles
47
+ mocha.loadFiles = function (fn) {
48
+ // load features
49
+ const featureFiles = this.files.filter(file => file.match(/\.feature$/))
50
+ if (featureFiles.length > 0) {
51
+ // Load translations for Gherkin features
52
+ loadTranslations().catch(() => {
53
+ // Ignore if translations can't be loaded
54
+ })
55
+
56
+ for (const file of featureFiles) {
57
+ const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
58
+ this.suite.addSuite(suite)
59
+ }
60
+
61
+ // remove feature files
62
+ const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
63
+ this.files = this.files.filter(file => !file.match(/\.feature$/))
64
+
65
+ // Load JavaScript test files using ESM imports
66
+ if (jsFiles.length > 0) {
67
+ try {
68
+ // Try original loadFiles first for compatibility
69
+ originalLoadFiles.call(this, fn)
70
+ } catch (e) {
71
+ // If original loadFiles fails, load ESM files manually
72
+ if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
73
+ // Load ESM files by importing them synchronously using top-level await workaround
74
+ for (const file of jsFiles) {
75
+ try {
76
+ // Convert file path to file:// URL for dynamic import
77
+ const fileUrl = `file://${file}`
78
+ // Use import() but don't await it - let it load in the background
79
+ import(fileUrl).catch(importErr => {
80
+ // If dynamic import fails, the file may have syntax errors or other issues
81
+ console.error(`Failed to load test file ${file}:`, importErr.message)
82
+ })
83
+ if (fn) fn()
84
+ } catch (fileErr) {
85
+ console.error(`Error processing test file ${file}:`, fileErr.message)
86
+ if (fn) fn(fileErr)
87
+ }
88
+ }
89
+ } else {
90
+ throw e
91
+ }
92
+ }
93
+ }
94
+
95
+ // add ids for each test and check uniqueness
96
+ const dupes = []
97
+ let missingFeatureInFile = []
98
+ const seenTests = []
99
+ this.suite.eachTest(test => {
100
+ if (!test) {
101
+ return // Skip undefined tests
102
+ }
103
+ const name = test.fullTitle()
104
+ if (seenTests.includes(test.uid)) {
105
+ dupes.push(name)
106
+ }
107
+ seenTests.push(test.uid)
108
+
109
+ if (name.slice(0, name.indexOf(':')) === '') {
110
+ missingFeatureInFile.push(test.file)
111
+ }
112
+ })
113
+ if (dupes.length) {
114
+ // ideally this should be no-op and throw (breaking change)...
115
+ output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
116
+ }
117
+
118
+ if (missingFeatureInFile.length) {
119
+ missingFeatureInFile = [...new Set(missingFeatureInFile)]
120
+ output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
121
+ }
122
+ } else {
123
+ // Use original for non-feature files
124
+ originalLoadFiles.call(this, fn)
125
+ }
126
+ }
127
+
128
+ const presetReporter = opts.reporter || config.reporter
129
+ // use standard reporter
130
+ if (!presetReporter) {
131
+ mocha.reporter(reporter, opts)
132
+ return mocha
133
+ }
134
+
135
+ // load custom reporter with options
136
+ const reporterOptions = Object.assign(config.reporterOptions || {})
137
+
138
+ if (opts.reporterOptions !== undefined) {
139
+ opts.reporterOptions.split(',').forEach(opt => {
140
+ const L = opt.split('=')
141
+ if (L.length > 2 || L.length === 0) {
142
+ throw new Error(`invalid reporter option '${opt}'`)
143
+ } else if (L.length === 2) {
144
+ reporterOptions[L[0]] = L[1]
145
+ } else {
146
+ reporterOptions[L[0]] = true
147
+ }
148
+ })
149
+ }
150
+
151
+ const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter')
152
+ if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
153
+ Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes)
154
+ delete reporterOptions['codeceptjs-cli-reporter']
155
+ }
156
+
157
+ // custom reporters
158
+ mocha.reporter(presetReporter, reporterOptions)
159
+ return mocha
160
+ }
161
+ }
162
+
163
+ export default MochaFactory
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Configuration for a Feature.
3
+ * Can inject values and add custom configuration.
4
+ */
5
+ class FeatureConfig {
6
+ /**
7
+ * @param {CodeceptJS.Suite} suite
8
+ */
9
+ constructor(suite) {
10
+ this.suite = suite
11
+ }
12
+
13
+ /**
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
28
+ *
29
+ * @param {number} retries
30
+ * @returns {this}
31
+ */
32
+ retry(retries) {
33
+ this.suite.retries(retries)
34
+ return this
35
+ }
36
+
37
+ /**
38
+ * Set timeout for this test
39
+ * @param {number} timeout
40
+ * @returns {this}
41
+ */
42
+ timeout(timeout) {
43
+ this.suite.timeout(timeout)
44
+ return this
45
+ }
46
+
47
+ /**
48
+ * @callback FeatureConfigCallback
49
+ * @param {CodeceptJS.Suite} suite
50
+ * @returns {Object<string, any>}
51
+ */
52
+
53
+ /**
54
+ * Configures a helper.
55
+ * Helper name can be omitted and values will be applied to first helper.
56
+ * @param {string | Object<string, any> | FeatureConfigCallback} helper
57
+ * @param {Object<string, any>} [obj]
58
+ * @returns {this}
59
+ */
60
+ config(helper, obj) {
61
+ if (!obj) {
62
+ obj = helper
63
+ helper = 0
64
+ }
65
+ if (typeof obj === 'function') {
66
+ obj = obj(this.suite)
67
+ }
68
+ if (!this.suite.config) {
69
+ this.suite.config = {}
70
+ }
71
+ this.suite.config[helper] = obj
72
+ return this
73
+ }
74
+
75
+ /**
76
+ * Append a tag name to scenario title
77
+ * @param {string} tagName
78
+ * @returns {this}
79
+ */
80
+ tag(tagName) {
81
+ if (tagName[0] !== '@') tagName = `@${tagName}`
82
+ if (!this.suite.tags) this.suite.tags = []
83
+ this.suite.tags.push(tagName)
84
+ this.suite.title = `${this.suite.title.trim()} ${tagName}`
85
+ return this
86
+ }
87
+ }
88
+
89
+ export default FeatureConfig