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
package/lib/container.js CHANGED
@@ -1,31 +1,48 @@
1
- const glob = require('glob');
2
- const path = require('path');
3
- const { MetaStep } = require('./step');
4
- const { fileExists, isFunction, isAsyncFunction } = require('./utils');
5
- const Translation = require('./translation');
6
- const MochaFactory = require('./mochaFactory');
7
- const recorder = require('./recorder');
8
- const event = require('./event');
9
- const WorkerStorage = require('./workerStorage');
10
- const store = require('./store');
11
- const ai = require('./ai');
1
+ import { globSync } from 'glob'
2
+ import path from 'path'
3
+ import debugModule from 'debug'
4
+ const debug = debugModule('codeceptjs:container')
5
+ import { MetaStep } from './step.js'
6
+ import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
7
+ import Translation from './translation.js'
8
+ import MochaFactory from './mocha/factory.js'
9
+ import recorder from './recorder.js'
10
+ import event from './event.js'
11
+ import WorkerStorage from './workerStorage.js'
12
+ import store from './store.js'
13
+ import Result from './result.js'
14
+ import ai from './ai.js'
15
+ import actorFactory from './actor.js'
16
+
17
+ let asyncHelperPromise
12
18
 
13
19
  let container = {
14
20
  helpers: {},
15
21
  support: {},
22
+ proxySupport: {},
16
23
  plugins: {},
24
+ actor: null,
17
25
  /**
18
26
  * @type {Mocha | {}}
19
27
  * @ignore
20
28
  */
21
29
  mocha: {},
22
30
  translation: {},
23
- };
31
+ /** @type {Result | null} */
32
+ result: null,
33
+ }
24
34
 
25
35
  /**
26
36
  * Dependency Injection Container
27
37
  */
28
38
  class Container {
39
+ /**
40
+ * Get the standard acting helpers of CodeceptJS Container
41
+ *
42
+ */
43
+ static get STANDARD_ACTING_HELPERS() {
44
+ return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium']
45
+ }
29
46
  /**
30
47
  * Create container with all required helpers and support objects
31
48
  *
@@ -33,22 +50,70 @@ class Container {
33
50
  * @param {*} config
34
51
  * @param {*} opts
35
52
  */
36
- static create(config, opts) {
37
- const mochaConfig = config.mocha || {};
38
- if (config.grep && !opts.grep) {
39
- mochaConfig.grep = config.grep;
53
+ static async create(config, opts) {
54
+ debug('creating container')
55
+ asyncHelperPromise = Promise.resolve()
56
+
57
+ // dynamically create mocha instance
58
+ const mochaConfig = config.mocha || {}
59
+ if (config.grep && !opts.grep) mochaConfig.grep = config.grep
60
+ this.createMocha = () => (container.mocha = MochaFactory.create(mochaConfig, opts || {}))
61
+ this.createMocha()
62
+
63
+ // create support objects
64
+ container.support = {}
65
+ container.helpers = await createHelpers(config.helpers || {})
66
+ container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
67
+ container.proxySupport = createSupportObjects(config.include || {})
68
+ container.plugins = await createPlugins(config.plugins || {}, opts)
69
+ container.result = new Result()
70
+
71
+ // Preload includes (so proxies can expose real objects synchronously)
72
+ const includes = config.include || {}
73
+
74
+ // Ensure I is available for DI modules at import time
75
+ if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
76
+ try {
77
+ const mod = includes.I
78
+ if (typeof mod === 'string') {
79
+ container.support.I = await loadSupportObject(mod, 'I')
80
+ } else if (typeof mod === 'function') {
81
+ container.support.I = await loadSupportObject(mod, 'I')
82
+ } else if (mod && typeof mod === 'object') {
83
+ container.support.I = mod
84
+ }
85
+ } catch (e) {
86
+ throw new Error(`Could not include object I: ${e.message}`)
87
+ }
88
+ } else {
89
+ // Create default actor if not provided via includes
90
+ createActor()
40
91
  }
41
- this.createMocha = () => {
42
- container.mocha = MochaFactory.create(mochaConfig, opts || {});
43
- };
44
- this.createMocha();
45
- container.helpers = createHelpers(config.helpers || {});
46
- container.translation = loadTranslation(config.translation || null, config.vocabularies || []);
47
- container.support = createSupportObjects(config.include || {});
48
- container.plugins = createPlugins(config.plugins || {}, opts);
49
- if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
50
- if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
51
- if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
92
+
93
+ // Load remaining includes except I
94
+ for (const [name, mod] of Object.entries(includes)) {
95
+ if (name === 'I') continue
96
+ try {
97
+ if (typeof mod === 'string') {
98
+ container.support[name] = await loadSupportObject(mod, name)
99
+ } else if (typeof mod === 'function') {
100
+ // function or class
101
+ container.support[name] = await loadSupportObject(mod, name)
102
+ } else if (mod && typeof mod === 'object') {
103
+ container.support[name] = mod
104
+ }
105
+ } catch (e) {
106
+ throw new Error(`Could not include object ${name}: ${e.message}`)
107
+ }
108
+ }
109
+
110
+ if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
111
+ if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
112
+ if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
113
+ }
114
+
115
+ static actor() {
116
+ return container.support.I
52
117
  }
53
118
 
54
119
  /**
@@ -60,9 +125,9 @@ class Container {
60
125
  */
61
126
  static plugins(name) {
62
127
  if (!name) {
63
- return container.plugins;
128
+ return container.plugins
64
129
  }
65
- return container.plugins[name];
130
+ return container.plugins[name]
66
131
  }
67
132
 
68
133
  /**
@@ -74,9 +139,10 @@ class Container {
74
139
  */
75
140
  static support(name) {
76
141
  if (!name) {
77
- return container.support;
142
+ return container.proxySupport
78
143
  }
79
- return container.support[name];
144
+ // Always return the proxy to ensure MetaStep creation works
145
+ return container.proxySupport[name]
80
146
  }
81
147
 
82
148
  /**
@@ -88,9 +154,9 @@ class Container {
88
154
  */
89
155
  static helpers(name) {
90
156
  if (!name) {
91
- return container.helpers;
157
+ return container.helpers
92
158
  }
93
- return container.helpers[name];
159
+ return container.helpers[name]
94
160
  }
95
161
 
96
162
  /**
@@ -99,7 +165,7 @@ class Container {
99
165
  * @api
100
166
  */
101
167
  static translation() {
102
- return container.translation;
168
+ return container.translation
103
169
  }
104
170
 
105
171
  /**
@@ -109,7 +175,19 @@ class Container {
109
175
  * @returns { * }
110
176
  */
111
177
  static mocha() {
112
- return container.mocha;
178
+ return container.mocha
179
+ }
180
+
181
+ /**
182
+ * Get result
183
+ *
184
+ * @returns {Result}
185
+ */
186
+ static result() {
187
+ if (!container.result) {
188
+ container.result = new Result()
189
+ }
190
+ return container.result
113
191
  }
114
192
 
115
193
  /**
@@ -119,8 +197,15 @@ class Container {
119
197
  * @param {Object<string, *>} newContainer
120
198
  */
121
199
  static append(newContainer) {
122
- const deepMerge = require('./utils').deepMerge;
123
- container = deepMerge(container, newContainer);
200
+ container = deepMerge(container, newContainer)
201
+
202
+ // If new support objects are added, update the proxy support
203
+ if (newContainer.support) {
204
+ const newProxySupport = createSupportObjects(newContainer.support)
205
+ container.proxySupport = { ...container.proxySupport, ...newProxySupport }
206
+ }
207
+
208
+ debug('appended', JSON.stringify(newContainer).slice(0, 300))
124
209
  }
125
210
 
126
211
  /**
@@ -130,11 +215,25 @@ class Container {
130
215
  * @param {Object<string, *>} newSupport
131
216
  * @param {Object<string, *>} newPlugins
132
217
  */
133
- static clear(newHelpers, newSupport, newPlugins) {
134
- container.helpers = newHelpers || {};
135
- container.support = newSupport || {};
136
- container.plugins = newPlugins || {};
137
- container.translation = loadTranslation();
218
+ static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
219
+ container.helpers = newHelpers
220
+ container.translation = await loadTranslation()
221
+ container.proxySupport = createSupportObjects(newSupport)
222
+ container.plugins = newPlugins
223
+ asyncHelperPromise = Promise.resolve()
224
+ store.actor = null
225
+ debug('container cleared')
226
+ }
227
+
228
+ /**
229
+ * @param {Function|null} fn
230
+ * @returns {Promise<void>}
231
+ */
232
+ static async started(fn = null) {
233
+ if (fn) {
234
+ asyncHelperPromise = asyncHelperPromise.then(fn)
235
+ }
236
+ return asyncHelperPromise
138
237
  }
139
238
 
140
239
  /**
@@ -144,299 +243,545 @@ class Container {
144
243
  * @param {Object} options - set {local: true} to not share among workers
145
244
  */
146
245
  static share(data, options = {}) {
147
- Container.append({ support: data });
246
+ Container.append({ support: data })
148
247
  if (!options.local) {
149
- WorkerStorage.share(data);
248
+ WorkerStorage.share(data)
150
249
  }
151
250
  }
251
+
252
+ static createMocha(config = {}, opts = {}) {
253
+ const mochaConfig = config?.mocha || {}
254
+ if (config?.grep && !opts?.grep) {
255
+ mochaConfig.grep = config.grep
256
+ }
257
+ container.mocha = MochaFactory.create(mochaConfig, opts || {})
258
+ }
152
259
  }
153
260
 
154
- module.exports = Container;
261
+ export default Container
155
262
 
156
- function createHelpers(config) {
157
- const helpers = {};
158
- let moduleName;
159
- for (const helperName in config) {
263
+ async function createHelpers(config) {
264
+ const helpers = {}
265
+ for (let helperName in config) {
160
266
  try {
161
- if (config[helperName].require) {
162
- if (config[helperName].require.startsWith('.')) {
163
- moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
164
- } else {
165
- moduleName = config[helperName].require; // plugin helper
166
- }
167
- } else {
168
- moduleName = `./helper/${helperName}`; // built-in helper
267
+ let HelperClass
268
+
269
+ // Check if helper class was stored in config during ESM import processing
270
+ if (config[helperName]._helperClass) {
271
+ HelperClass = config[helperName]._helperClass
272
+ debug(`helper ${helperName} loaded from ESM import`)
169
273
  }
170
274
 
171
- // @ts-ignore
172
- let HelperClass;
173
- // check if the helper is the built-in, use the require() syntax.
174
- if (moduleName.startsWith('./helper/')) {
175
- HelperClass = require(moduleName);
176
- } else {
177
- // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
178
- HelperClass = require(moduleName).default || require(moduleName);
275
+ // ESM import (legacy check)
276
+ if (!HelperClass && typeof helperName === 'function' && helperName.prototype) {
277
+ HelperClass = helperName
278
+ helperName = HelperClass.constructor.name
179
279
  }
180
280
 
181
- if (HelperClass._checkRequirements) {
182
- const requirements = HelperClass._checkRequirements();
183
- if (requirements) {
184
- let install;
185
- if (require('./utils').installedLocally()) {
186
- install = `npm install --save-dev ${requirements.join(' ')}`;
187
- } else {
188
- console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
189
- install = `[sudo] npm install -g ${requirements.join(' ')}`;
190
- }
191
- throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
281
+ // classical require - may be async for ESM modules
282
+ if (!HelperClass) {
283
+ const helperResult = requireHelperFromModule(helperName, config)
284
+ if (helperResult instanceof Promise) {
285
+ // Handle async ESM loading
286
+ helpers[helperName] = {}
287
+ asyncHelperPromise = asyncHelperPromise
288
+ .then(() => helperResult)
289
+ .then(async ResolvedHelperClass => {
290
+ debug(`helper ${helperName} resolved type: ${typeof ResolvedHelperClass}`, ResolvedHelperClass)
291
+
292
+ // Extract default export from ESM module wrapper if needed
293
+ if (ResolvedHelperClass && ResolvedHelperClass.__esModule && ResolvedHelperClass.default) {
294
+ ResolvedHelperClass = ResolvedHelperClass.default
295
+ debug(`extracted default export for ${helperName}, new type: ${typeof ResolvedHelperClass}`)
296
+ }
297
+
298
+ if (typeof ResolvedHelperClass !== 'function') {
299
+ throw new Error(`Helper '${helperName}' is not a class. Got: ${typeof ResolvedHelperClass}`)
300
+ }
301
+
302
+ checkHelperRequirements(ResolvedHelperClass)
303
+ helpers[helperName] = new ResolvedHelperClass(config[helperName])
304
+ if (helpers[helperName]._init) await helpers[helperName]._init()
305
+ debug(`helper ${helperName} async initialized`)
306
+ })
307
+ continue
308
+ } else {
309
+ HelperClass = helperResult
192
310
  }
193
311
  }
194
- helpers[helperName] = new HelperClass(config[helperName]);
312
+
313
+ // handle async CJS modules that use dynamic import
314
+ if (isAsyncFunction(HelperClass)) {
315
+ helpers[helperName] = {}
316
+
317
+ asyncHelperPromise = asyncHelperPromise
318
+ .then(() => HelperClass())
319
+ .then(ResolvedHelperClass => {
320
+ // Check if ResolvedHelperClass is a constructor function
321
+ if (typeof ResolvedHelperClass?.constructor !== 'function') {
322
+ throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
323
+ }
324
+
325
+ debug(`helper ${helperName} async initialized`)
326
+
327
+ helpers[helperName] = new ResolvedHelperClass(config[helperName])
328
+ })
329
+
330
+ continue
331
+ }
332
+
333
+ checkHelperRequirements(HelperClass)
334
+
335
+ helpers[helperName] = new HelperClass(config[helperName])
336
+ debug(`helper ${helperName} initialized`)
195
337
  } catch (err) {
196
- throw new Error(`Could not load helper ${helperName} from module '${moduleName}':\n${err.message}\n${err.stack}`);
338
+ throw new Error(`Could not load helper ${helperName} (${err.message})`)
197
339
  }
198
340
  }
199
341
 
200
342
  for (const name in helpers) {
201
- if (helpers[name]._init) helpers[name]._init();
343
+ if (helpers[name]._init) await helpers[name]._init()
202
344
  }
203
- return helpers;
345
+ return helpers
204
346
  }
205
347
 
206
- function createSupportObjects(config) {
207
- const objects = {};
208
-
209
- for (const name in config) {
210
- objects[name] = {}; // placeholders
211
- }
212
-
213
- if (!config.I) {
214
- objects.I = require('./actor')();
215
-
216
- if (container.translation.I !== 'I') {
217
- objects[container.translation.I] = objects.I;
348
+ function checkHelperRequirements(HelperClass) {
349
+ if (HelperClass._checkRequirements) {
350
+ const requirements = HelperClass._checkRequirements()
351
+ if (requirements) {
352
+ let install
353
+ if (installedLocally()) {
354
+ install = `npm install --save-dev ${requirements.join(' ')}`
355
+ } else {
356
+ console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation')
357
+ install = `[sudo] npm install -g ${requirements.join(' ')}`
358
+ }
359
+ throw new Error(`Required modules are not installed.\n\nRUN: ${install}`)
218
360
  }
219
361
  }
362
+ }
220
363
 
221
- container.support = objects;
222
-
223
- function lazyLoad(name) {
224
- let newObj = getSupportObject(config, name);
364
+ async function requireHelperFromModule(helperName, config, HelperClass) {
365
+ const moduleName = getHelperModuleName(helperName, config)
366
+ if (moduleName.startsWith('./helper/')) {
225
367
  try {
226
- if (typeof newObj === 'function') {
227
- newObj = newObj();
228
- } else if (newObj._init) {
229
- newObj._init();
368
+ // For built-in helpers, use direct relative import with .js extension
369
+ const helperPath = `${moduleName}.js`
370
+ const mod = await import(helperPath)
371
+ HelperClass = mod.default || mod
372
+ } catch (err) {
373
+ throw err
374
+ }
375
+ } else {
376
+ // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
377
+ try {
378
+ // Try dynamic import for both CommonJS and ESM modules
379
+ const mod = await import(moduleName)
380
+ if (!mod && !mod.default) {
381
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
230
382
  }
383
+ HelperClass = mod.default || mod
231
384
  } catch (err) {
232
- throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}\n${err.stack}`);
385
+ if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
386
+ // This is an ESM module, use dynamic import
387
+ try {
388
+ const pathModule = await import('path')
389
+ const absolutePath = pathModule.default.resolve(moduleName)
390
+ const mod = await import(absolutePath)
391
+ HelperClass = mod.default || mod
392
+ debug(`helper ${helperName} loaded via ESM import`)
393
+ } catch (importErr) {
394
+ throw new Error(`Helper module '${moduleName}' could not be imported as ESM: ${importErr.message}`)
395
+ }
396
+ } else if (err.code === 'MODULE_NOT_FOUND') {
397
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
398
+ } else {
399
+ throw err
400
+ }
233
401
  }
234
- return newObj;
235
402
  }
403
+ return HelperClass
404
+ }
236
405
 
406
+ function createSupportObjects(config) {
237
407
  const asyncWrapper = function (f) {
238
408
  return function () {
239
- return f.apply(this, arguments).catch((e) => {
240
- recorder.saveFirstAsyncError(e);
241
- throw e;
242
- });
243
- };
244
- };
245
-
246
- Object.keys(objects).forEach((object) => {
247
- const currentObject = objects[object];
248
- Object.keys(currentObject).forEach((method) => {
249
- const currentMethod = currentObject[method];
250
- if (currentMethod && currentMethod[Symbol.toStringTag] === 'AsyncFunction') {
251
- objects[object][method] = asyncWrapper(currentMethod);
252
- }
253
- });
254
- });
409
+ return f.apply(this, arguments).catch(e => {
410
+ recorder.saveFirstAsyncError(e)
411
+ throw e
412
+ })
413
+ }
414
+ }
255
415
 
256
- return new Proxy({}, {
257
- has(target, key) {
258
- return key in config;
259
- },
260
- ownKeys() {
261
- return Reflect.ownKeys(config);
262
- },
263
- get(target, key) {
264
- // configured but not in support object, yet: load the module
265
- if (key in objects && !(key in target)) {
266
- // load default I
267
- if (key in objects && !(key in config)) {
268
- return target[key] = objects[key];
269
- }
416
+ function lazyLoad(name) {
417
+ return new Proxy(
418
+ {},
419
+ {
420
+ get(target, prop) {
421
+ // behavr like array or
422
+ if (prop === 'length') return Object.keys(config).length
423
+ if (prop === Symbol.iterator) {
424
+ return function* () {
425
+ for (let i = 0; i < Object.keys(config).length; i++) {
426
+ yield target[i]
427
+ }
428
+ }
429
+ }
430
+
431
+ // load actual name from vocabulary
432
+ if (container.translation && container.translation.I && name === 'I') {
433
+ // Use translated name for I
434
+ const actualName = container.translation.I
435
+ if (actualName !== 'I') {
436
+ name = actualName
437
+ }
438
+ }
270
439
 
271
- // load new object
272
- const object = lazyLoad(key);
273
- // check that object is a real object and not an array
274
- if (Object.prototype.toString.call(object) === '[object Object]') {
275
- return target[key] = Object.assign(objects[key], object);
440
+ if (name === 'I') {
441
+ if (!container.support.I) {
442
+ // Actor will be created during container.create()
443
+ return undefined
444
+ }
445
+ methodsOfObject(container.support.I)
446
+ return container.support.I[prop]
447
+ }
448
+
449
+ if (!container.support[name] && typeof config[name] === 'object') {
450
+ container.support[name] = config[name]
451
+ }
452
+
453
+ if (!container.support[name]) {
454
+ // Cannot load object synchronously in proxy getter
455
+ // Return undefined and log warning - object should be pre-loaded during container creation
456
+ debug(`Support object ${name} not pre-loaded, returning undefined`)
457
+ return undefined
458
+ }
459
+
460
+ const currentObject = container.support[name]
461
+ let currentValue = currentObject[prop]
462
+
463
+ if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
464
+ const ms = new MetaStep(name, prop)
465
+ ms.setContext(currentObject)
466
+ if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
467
+ debug(`metastep is created for ${name}.${prop.toString()}()`)
468
+ return ms.run.bind(ms, currentValue)
469
+ }
470
+
471
+ return currentValue
472
+ },
473
+ has(target, prop) {
474
+ if (!container.support[name]) {
475
+ // Note: This is sync, so we can't use async loadSupportObject here
476
+ // The object will be loaded lazily on first property access
477
+ return false
478
+ }
479
+ return prop in container.support[name]
480
+ },
481
+ getOwnPropertyDescriptor(target, prop) {
482
+ if (!container.support[name]) {
483
+ // Object will be loaded on property access
484
+ return {
485
+ enumerable: true,
486
+ configurable: true,
487
+ value: undefined,
488
+ }
489
+ }
490
+ return {
491
+ enumerable: true,
492
+ configurable: true,
493
+ value: container.support[name][prop],
494
+ }
495
+ },
496
+ ownKeys() {
497
+ if (!container.support[name]) {
498
+ return []
499
+ }
500
+ return Reflect.ownKeys(container.support[name])
501
+ },
502
+ },
503
+ )
504
+ }
505
+
506
+ const keys = Reflect.ownKeys(config)
507
+ return new Proxy(
508
+ {},
509
+ {
510
+ has(target, key) {
511
+ return keys.includes(key)
512
+ },
513
+ ownKeys() {
514
+ return keys
515
+ },
516
+ getOwnPropertyDescriptor(target, prop) {
517
+ return {
518
+ enumerable: true,
519
+ configurable: true,
520
+ value: target[prop],
276
521
  }
277
- target[key] = object;
278
- }
279
- return target[key];
522
+ },
523
+ get(target, key) {
524
+ if (typeof key === 'symbol') {
525
+ // safely ignore symbol-based meta properties used by tooling
526
+ return undefined
527
+ }
528
+ // Allow special I even if not declared in includes
529
+ if (key === 'I') {
530
+ return lazyLoad('I')
531
+ }
532
+ if (!keys.includes(key)) {
533
+ throw new Error(`Support object "${String(key)}" is not defined`)
534
+ }
535
+ return lazyLoad(key)
536
+ },
280
537
  },
281
- });
538
+ )
282
539
  }
283
540
 
284
- function createPlugins(config, options = {}) {
285
- const plugins = {};
541
+ function createActor(actorPath) {
542
+ if (container.support.I) return container.support.I
286
543
 
287
- const enabledPluginsByOptions = (options.plugins || '').split(',');
544
+ // Default actor
545
+ container.support.I = actorFactory({}, Container)
546
+
547
+ return container.support.I
548
+ }
549
+
550
+ async function loadPluginAsync(modulePath, config) {
551
+ let pluginMod
552
+ try {
553
+ // Try dynamic import first (works for both ESM and CJS)
554
+ pluginMod = await import(modulePath)
555
+ } catch (err) {
556
+ throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
557
+ }
558
+
559
+ const pluginFactory = pluginMod.default || pluginMod
560
+ if (typeof pluginFactory !== 'function') {
561
+ throw new Error(`Plugin '${modulePath}' is not a function. Expected a plugin factory function.`)
562
+ }
563
+
564
+ return pluginFactory(config)
565
+ }
566
+
567
+ async function loadPluginFallback(modulePath, config) {
568
+ // This function is kept for backwards compatibility but now uses dynamic import
569
+ return await loadPluginAsync(modulePath, config)
570
+ }
571
+
572
+ async function createPlugins(config, options = {}) {
573
+ const plugins = {}
574
+
575
+ const enabledPluginsByOptions = (options.plugins || '').split(',')
288
576
  for (const pluginName in config) {
289
- if (!config[pluginName]) config[pluginName] = {};
290
- if (!config[pluginName].enabled && (enabledPluginsByOptions.indexOf(pluginName) < 0)) {
291
- continue; // plugin is disabled
577
+ if (!config[pluginName]) config[pluginName] = {}
578
+ if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
579
+ continue // plugin is disabled
292
580
  }
293
- let module;
581
+ let module
294
582
  try {
295
583
  if (config[pluginName].require) {
296
- module = config[pluginName].require;
297
- if (module.startsWith('.')) { // local
298
- module = path.resolve(global.codecept_dir, module); // custom plugin
584
+ module = config[pluginName].require
585
+ if (module.startsWith('.')) {
586
+ // local
587
+ module = path.resolve(global.codecept_dir, module) // custom plugin
299
588
  }
300
589
  } else {
301
- module = `./plugin/${pluginName}`;
590
+ module = `./plugin/${pluginName}.js`
302
591
  }
303
- plugins[pluginName] = require(module)(config[pluginName]);
592
+
593
+ // Use async loading for all plugins (ESM and CJS)
594
+ plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
595
+ debug(`plugin ${pluginName} loaded via async import`)
304
596
  } catch (err) {
305
- throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`);
597
+ throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
306
598
  }
307
599
  }
308
- return plugins;
600
+ return plugins
309
601
  }
310
602
 
311
- function getSupportObject(config, name) {
312
- const module = config[name];
313
- if (typeof module === 'string') {
314
- return loadSupportObject(module, name);
315
- }
316
- return module;
317
- }
603
+ async function loadGherkinStepsAsync(paths) {
604
+ global.Before = fn => event.dispatcher.on(event.test.started, fn)
605
+ global.After = fn => event.dispatcher.on(event.test.finished, fn)
606
+ global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
318
607
 
319
- function loadGherkinSteps(paths) {
320
- global.Before = fn => event.dispatcher.on(event.test.started, fn);
321
- global.After = fn => event.dispatcher.on(event.test.finished, fn);
322
- global.Fail = fn => event.dispatcher.on(event.test.failed, fn);
608
+ // Import BDD module to access step file tracking functions
609
+ const bddModule = await import('./mocha/bdd.js')
323
610
 
324
611
  // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
325
612
  // If gherkin.steps is Array, it will go the old way
326
613
  // This is done so that we need not enter all Step Definition files under config.gherkin.steps
327
614
  if (Array.isArray(paths)) {
328
615
  for (const path of paths) {
329
- loadSupportObject(path, `Step Definition from ${path}`);
616
+ // Set context for step definition file location tracking
617
+ bddModule.setCurrentStepFile(path)
618
+ await loadSupportObject(path, `Step Definition from ${path}`)
619
+ bddModule.clearCurrentStepFile()
330
620
  }
331
621
  } else {
332
- const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : '';
622
+ const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
333
623
  if (folderPath !== '') {
334
- glob.sync(folderPath).forEach((file) => {
335
- loadSupportObject(file, `Step Definition from ${file}`);
336
- });
624
+ const files = globSync(folderPath)
625
+ for (const file of files) {
626
+ // Set context for step definition file location tracking
627
+ bddModule.setCurrentStepFile(file)
628
+ await loadSupportObject(file, `Step Definition from ${file}`)
629
+ bddModule.clearCurrentStepFile()
630
+ }
337
631
  }
338
632
  }
339
633
 
340
- delete global.Before;
341
- delete global.After;
342
- delete global.Fail;
634
+ delete global.Before
635
+ delete global.After
636
+ delete global.Fail
343
637
  }
344
638
 
345
- function loadSupportObject(modulePath, supportObjectName) {
346
- if (modulePath.charAt(0) === '.') {
347
- modulePath = path.join(global.codecept_dir, modulePath);
348
- }
349
- try {
350
- const obj = require(modulePath);
639
+ function loadGherkinSteps(paths) {
640
+ global.Before = fn => event.dispatcher.on(event.test.started, fn)
641
+ global.After = fn => event.dispatcher.on(event.test.finished, fn)
642
+ global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
351
643
 
352
- if (typeof obj === 'function') {
353
- const fobj = obj();
644
+ // Gherkin step loading must be handled asynchronously
645
+ throw new Error('Gherkin step loading must be converted to async. Use loadGherkinStepsAsync() instead.')
354
646
 
355
- if (fobj.constructor.name === 'Actor') {
356
- const methods = getObjectMethods(fobj);
357
- Object.keys(methods)
358
- .forEach(key => {
359
- fobj[key] = methods[key];
360
- });
647
+ delete global.Before
648
+ delete global.After
649
+ delete global.Fail
650
+ }
361
651
 
362
- return methods;
652
+ async function loadSupportObject(modulePath, supportObjectName) {
653
+ if (!modulePath) {
654
+ throw new Error(`Support object "${supportObjectName}" is not defined`)
655
+ }
656
+ // If function/class provided directly
657
+ if (typeof modulePath === 'function') {
658
+ try {
659
+ // class constructor
660
+ if (modulePath.prototype && modulePath.prototype.constructor === modulePath) {
661
+ return new modulePath()
363
662
  }
663
+ // plain function factory
664
+ return modulePath()
665
+ } catch (err) {
666
+ throw new Error(`Could not include object ${supportObjectName} from function: ${err.message}`)
364
667
  }
365
- if (typeof obj !== 'function'
366
- && Object.getPrototypeOf(obj) !== Object.prototype
367
- && !Array.isArray(obj)
368
- ) {
369
- const methods = getObjectMethods(obj);
370
- Object.keys(methods)
371
- .filter(key => !key.startsWith('_'))
372
- .forEach(key => {
373
- const currentMethod = methods[key];
374
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
375
- const ms = new MetaStep(supportObjectName, key);
376
- ms.setContext(methods);
377
- methods[key] = ms.run.bind(ms, currentMethod);
378
- }
379
- });
380
- return methods;
668
+ }
669
+ if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
670
+ modulePath = path.join(global.codecept_dir, modulePath)
671
+ }
672
+ try {
673
+ // Use dynamic import for both ESM and CJS modules
674
+ let importPath = modulePath
675
+ // Append .js if no extension provided (ESM resolution requires it)
676
+ if (typeof importPath === 'string') {
677
+ const ext = path.extname(importPath)
678
+ if (!ext) importPath = `${importPath}.js`
381
679
  }
382
- if (!Array.isArray(obj)) {
383
- Object.keys(obj)
384
- .filter(key => !key.startsWith('_'))
385
- .forEach(key => {
386
- const currentMethod = obj[key];
387
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
388
- const ms = new MetaStep(supportObjectName, key);
389
- ms.setContext(obj);
390
- obj[key] = ms.run.bind(ms, currentMethod);
391
- }
392
- });
680
+ const obj = await import(importPath)
681
+
682
+ // Handle ESM module wrapper
683
+ let actualObj = obj
684
+ if (obj && obj.__esModule && obj.default) {
685
+ actualObj = obj.default
686
+ } else if (obj.default) {
687
+ actualObj = obj.default
393
688
  }
394
689
 
395
- return obj;
690
+ // Handle different types of imports
691
+ if (typeof actualObj === 'function') {
692
+ // If it's a class (constructor function)
693
+ if (actualObj.prototype && actualObj.prototype.constructor === actualObj) {
694
+ const ClassName = actualObj
695
+ return new ClassName()
696
+ }
697
+ // If it's a regular function
698
+ return actualObj()
699
+ }
700
+
701
+ if (actualObj && Array.isArray(actualObj)) {
702
+ return actualObj
703
+ }
704
+
705
+ // If it's a plain object
706
+ if (actualObj && typeof actualObj === 'object') {
707
+ // Call _init if it exists (for page objects)
708
+ if (actualObj._init && typeof actualObj._init === 'function') {
709
+ actualObj._init()
710
+ }
711
+ return actualObj
712
+ }
713
+
714
+ throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
396
715
  } catch (err) {
397
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
716
+ throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
398
717
  }
399
718
  }
400
719
 
720
+ // Backwards compatibility function that throws an error for sync usage
721
+ function loadSupportObjectSync(modulePath, supportObjectName) {
722
+ throw new Error(`loadSupportObjectSync is deprecated. Support object "${supportObjectName || 'undefined'}" from '${modulePath}' must be loaded asynchronously. Use loadSupportObject() instead.`)
723
+ }
724
+
401
725
  /**
402
726
  * Method collect own property and prototype
403
727
  */
404
- function getObjectMethods(obj) {
405
- const methodsSet = new Set();
406
- let protoObj = Reflect.getPrototypeOf(obj);
407
- do {
408
- if (protoObj.constructor.prototype !== Object.prototype) {
409
- const keys = Reflect.ownKeys(protoObj);
410
- keys.forEach(k => methodsSet.add(k));
411
- }
412
- } while (protoObj = Reflect.getPrototypeOf(protoObj));
413
- Reflect.ownKeys(obj).forEach(k => methodsSet.add(k));
414
- const methods = {};
415
- for (const key of methodsSet.keys()) {
416
- if (key !== 'constructor') methods[key] = obj[key];
417
- }
418
- return methods;
419
- }
420
728
 
421
- function loadTranslation(locale, vocabularies) {
729
+ async function loadTranslation(locale, vocabularies) {
422
730
  if (!locale) {
423
- return Translation.createEmpty();
731
+ return Translation.createEmpty()
424
732
  }
425
733
 
426
- let translation;
734
+ let translation
427
735
 
428
736
  // check if it is a known translation
429
- if (Translation.langs[locale]) {
430
- translation = new Translation(Translation.langs[locale]);
737
+ const langs = await Translation.getLangs()
738
+ if (langs[locale]) {
739
+ translation = new Translation(langs[locale])
431
740
  } else if (fileExists(path.join(global.codecept_dir, locale))) {
432
741
  // get from a provided file instead
433
- translation = Translation.createDefault();
434
- translation.loadVocabulary(locale);
742
+ translation = Translation.createDefault()
743
+ translation.loadVocabulary(locale)
435
744
  } else {
436
- translation = Translation.createDefault();
745
+ translation = Translation.createDefault()
746
+ }
747
+
748
+ vocabularies.forEach(v => translation.loadVocabulary(v))
749
+
750
+ return translation
751
+ }
752
+
753
+ function getHelperModuleName(helperName, config) {
754
+ // classical require
755
+ if (config[helperName].require) {
756
+ if (config[helperName].require.startsWith('.')) {
757
+ let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
758
+ // Add .js extension if not present for ESM compatibility
759
+ if (!path.extname(helperPath)) {
760
+ helperPath += '.js'
761
+ }
762
+ return helperPath // custom helper
763
+ }
764
+ return config[helperName].require // plugin helper
437
765
  }
438
766
 
439
- vocabularies.forEach(v => translation.loadVocabulary(v));
767
+ // built-in helpers
768
+ if (helperName.startsWith('@codeceptjs/')) {
769
+ return helperName
770
+ }
771
+
772
+ // built-in helpers
773
+ return `./helper/${helperName}`
774
+ }
775
+ function normalizeAndJoin(basePath, subPath) {
776
+ // Normalize and convert slashes to forward slashes in one step
777
+ const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
778
+ const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
779
+
780
+ // If subPath is absolute (starts with "/"), return it as the final path
781
+ if (normalizedSub.startsWith('/')) {
782
+ return normalizedSub
783
+ }
440
784
 
441
- return translation;
785
+ // Join the paths using POSIX-style
786
+ return path.posix.join(normalizedBase, normalizedSub)
442
787
  }