codeceptjs 4.0.0-beta.2 → 4.0.0-beta.21

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 +73 -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 +765 -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 +54 -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
package/lib/container.js CHANGED
@@ -1,34 +1,96 @@
1
- import glob from 'glob';
2
- import path, { dirname } from 'path';
3
- import importSync from 'import-sync';
4
- import { fileURLToPath } from 'url';
5
- import { MetaStep } from './step.js';
6
- import {
7
- fileExists, isFunction, isAsyncFunction, deepMerge,
8
- } from './utils.js';
9
- import Translation from './translation.js';
10
- import { MochaFactory } from './mochaFactory.js';
11
- // eslint-disable-next-line import/no-named-as-default
12
- import recorder from './recorder.js';
13
- import * as event from './event.js';
14
- import * as WorkerStorage from './workerStorage.js';
15
- import { store } from './store.js';
16
- import { actor } from './actor.js';
17
-
18
- import ai from './ai.js';
1
+ import { globSync } from 'glob'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+ import debugModule from 'debug'
5
+ const debug = debugModule('codeceptjs:container')
6
+ import { MetaStep } from './step.js'
7
+ import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
8
+ import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
9
+ import Translation from './translation.js'
10
+ import MochaFactory from './mocha/factory.js'
11
+ import recorder from './recorder.js'
12
+ import event from './event.js'
13
+ import WorkerStorage from './workerStorage.js'
14
+ import store from './store.js'
15
+ import Result from './result.js'
16
+ import ai from './ai.js'
17
+ import actorFactory from './actor.js'
18
+
19
+ let asyncHelperPromise
20
+ let tsxLoaderRegistered = false
21
+
22
+ /**
23
+ * Automatically register tsx ESM loader for TypeScript imports
24
+ * This allows loading .ts files without NODE_OPTIONS
25
+ */
26
+ async function ensureTsxLoader() {
27
+ if (tsxLoaderRegistered) return true
28
+
29
+ try {
30
+ // Check if tsx is available
31
+ const { createRequire } = await import('module')
32
+ const { pathToFileURL } = await import('url')
33
+ const userRequire = createRequire(pathToFileURL(path.join(global.codecept_dir || process.cwd(), 'package.json')).href)
34
+
35
+ // Try to resolve tsx from user's project
36
+ try {
37
+ userRequire.resolve('tsx')
38
+ } catch {
39
+ debug('tsx not found in user project')
40
+ return false
41
+ }
42
+
43
+ // Register tsx/esm loader dynamically
44
+ // Use Node.js register() API from node:module (Node 18.19+, 20.6+)
45
+ try {
46
+ const { register } = await import('node:module')
47
+ if (typeof register === 'function') {
48
+ debug('Registering tsx ESM loader via node:module register()')
49
+ const tsxPath = userRequire.resolve('tsx/esm')
50
+ register(tsxPath, import.meta.url)
51
+ tsxLoaderRegistered = true
52
+ return true
53
+ }
54
+ } catch (registerErr) {
55
+ debug('node:module register() not available:', registerErr.message)
56
+ }
57
+
58
+ debug('module.register not available, tsx loader must be set via NODE_OPTIONS')
59
+ return false
60
+ } catch (err) {
61
+ debug('Failed to register tsx loader:', err.message)
62
+ return false
63
+ }
64
+ }
19
65
 
20
66
  let container = {
21
67
  helpers: {},
22
68
  support: {},
69
+ proxySupport: {},
23
70
  plugins: {},
71
+ actor: null,
72
+ /**
73
+ * @type {Mocha | {}}
74
+ * @ignore
75
+ */
24
76
  mocha: {},
25
77
  translation: {},
26
- };
78
+ /** @type {Result | null} */
79
+ result: null,
80
+ sharedKeys: new Set() // Track keys shared via share() function
81
+ }
27
82
 
28
83
  /**
29
84
  * Dependency Injection Container
30
85
  */
31
- export default class Container {
86
+ class Container {
87
+ /**
88
+ * Get the standard acting helpers of CodeceptJS Container
89
+ *
90
+ */
91
+ static get STANDARD_ACTING_HELPERS() {
92
+ return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium']
93
+ }
32
94
  /**
33
95
  * Create container with all required helpers and support objects
34
96
  *
@@ -36,22 +98,70 @@ export default class Container {
36
98
  * @param {*} config
37
99
  * @param {*} opts
38
100
  */
39
- static create(config, opts) {
40
- const mochaConfig = config.mocha || {};
41
- if (config.grep && !opts.grep) {
42
- mochaConfig.grep = config.grep;
101
+ static async create(config, opts) {
102
+ debug('creating container')
103
+ asyncHelperPromise = Promise.resolve()
104
+
105
+ // dynamically create mocha instance
106
+ const mochaConfig = config.mocha || {}
107
+ if (config.grep && !opts.grep) mochaConfig.grep = config.grep
108
+ this.createMocha = () => (container.mocha = MochaFactory.create(mochaConfig, opts || {}))
109
+ this.createMocha()
110
+
111
+ // create support objects
112
+ container.support = {}
113
+ container.helpers = await createHelpers(config.helpers || {})
114
+ container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
115
+ container.proxySupport = createSupportObjects(config.include || {})
116
+ container.plugins = await createPlugins(config.plugins || {}, opts)
117
+ container.result = new Result()
118
+
119
+ // Preload includes (so proxies can expose real objects synchronously)
120
+ const includes = config.include || {}
121
+
122
+ // Ensure I is available for DI modules at import time
123
+ if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
124
+ try {
125
+ const mod = includes.I
126
+ if (typeof mod === 'string') {
127
+ container.support.I = await loadSupportObject(mod, 'I')
128
+ } else if (typeof mod === 'function') {
129
+ container.support.I = await loadSupportObject(mod, 'I')
130
+ } else if (mod && typeof mod === 'object') {
131
+ container.support.I = mod
132
+ }
133
+ } catch (e) {
134
+ throw new Error(`Could not include object I: ${e.message}`)
135
+ }
136
+ } else {
137
+ // Create default actor if not provided via includes
138
+ createActor()
139
+ }
140
+
141
+ // Load remaining includes except I
142
+ for (const [name, mod] of Object.entries(includes)) {
143
+ if (name === 'I') continue
144
+ try {
145
+ if (typeof mod === 'string') {
146
+ container.support[name] = await loadSupportObject(mod, name)
147
+ } else if (typeof mod === 'function') {
148
+ // function or class
149
+ container.support[name] = await loadSupportObject(mod, name)
150
+ } else if (mod && typeof mod === 'object') {
151
+ container.support[name] = mod
152
+ }
153
+ } catch (e) {
154
+ throw new Error(`Could not include object ${name}: ${e.message}`)
155
+ }
43
156
  }
44
- this.createMocha = () => {
45
- container.mocha = MochaFactory.create(mochaConfig, opts || {});
46
- };
47
- this.createMocha();
48
- container.helpers = createHelpers(config.helpers || {});
49
- container.translation = loadTranslation(config.translation || null, config.vocabularies || []);
50
- container.support = createSupportObjects(config.include || {});
51
- container.plugins = createPlugins(config.plugins || {}, opts);
52
- if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
53
- if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
54
- if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
157
+
158
+ if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
159
+ if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
160
+ if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
161
+ }
162
+
163
+ static actor() {
164
+ return container.support.I
55
165
  }
56
166
 
57
167
  /**
@@ -62,7 +172,10 @@ export default class Container {
62
172
  * @returns { * }
63
173
  */
64
174
  static plugins(name) {
65
- return name ? container.plugins[name] : container.plugins;
175
+ if (!name) {
176
+ return container.plugins
177
+ }
178
+ return container.plugins[name]
66
179
  }
67
180
 
68
181
  /**
@@ -74,9 +187,10 @@ export default class Container {
74
187
  */
75
188
  static support(name) {
76
189
  if (!name) {
77
- return container.support;
190
+ return container.proxySupport
78
191
  }
79
- return container.support[name];
192
+ // Always return the proxy to ensure MetaStep creation works
193
+ return container.proxySupport[name]
80
194
  }
81
195
 
82
196
  /**
@@ -88,9 +202,9 @@ export default class Container {
88
202
  */
89
203
  static helpers(name) {
90
204
  if (!name) {
91
- return container.helpers;
205
+ return container.helpers
92
206
  }
93
- return container.helpers[name];
207
+ return container.helpers[name]
94
208
  }
95
209
 
96
210
  /**
@@ -99,7 +213,7 @@ export default class Container {
99
213
  * @api
100
214
  */
101
215
  static translation() {
102
- return container.translation;
216
+ return container.translation
103
217
  }
104
218
 
105
219
  /**
@@ -109,7 +223,19 @@ export default class Container {
109
223
  * @returns { * }
110
224
  */
111
225
  static mocha() {
112
- return container.mocha;
226
+ return container.mocha
227
+ }
228
+
229
+ /**
230
+ * Get result
231
+ *
232
+ * @returns {Result}
233
+ */
234
+ static result() {
235
+ if (!container.result) {
236
+ container.result = new Result()
237
+ }
238
+ return container.result
113
239
  }
114
240
 
115
241
  /**
@@ -119,7 +245,15 @@ export default class Container {
119
245
  * @param {Object<string, *>} newContainer
120
246
  */
121
247
  static append(newContainer) {
122
- container = deepMerge(container, newContainer);
248
+ container = deepMerge(container, newContainer)
249
+
250
+ // If new support objects are added, update the proxy support
251
+ if (newContainer.support) {
252
+ const newProxySupport = createSupportObjects(newContainer.support)
253
+ container.proxySupport = { ...container.proxySupport, ...newProxySupport }
254
+ }
255
+
256
+ debug('appended', JSON.stringify(newContainer).slice(0, 300))
123
257
  }
124
258
 
125
259
  /**
@@ -129,11 +263,26 @@ export default class Container {
129
263
  * @param {Object<string, *>} newSupport
130
264
  * @param {Object<string, *>} newPlugins
131
265
  */
132
- static clear(newHelpers, newSupport, newPlugins) {
133
- container.helpers = newHelpers || {};
134
- container.support = newSupport || {};
135
- container.plugins = newPlugins || {};
136
- container.translation = loadTranslation();
266
+ static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
267
+ container.helpers = newHelpers
268
+ container.translation = await loadTranslation()
269
+ container.proxySupport = createSupportObjects(newSupport)
270
+ container.plugins = newPlugins
271
+ container.sharedKeys = new Set() // Clear shared keys
272
+ asyncHelperPromise = Promise.resolve()
273
+ store.actor = null
274
+ debug('container cleared')
275
+ }
276
+
277
+ /**
278
+ * @param {Function|null} fn
279
+ * @returns {Promise<void>}
280
+ */
281
+ static async started(fn = null) {
282
+ if (fn) {
283
+ asyncHelperPromise = asyncHelperPromise.then(fn)
284
+ }
285
+ return asyncHelperPromise
137
286
  }
138
287
 
139
288
  /**
@@ -143,308 +292,663 @@ export default class Container {
143
292
  * @param {Object} options - set {local: true} to not share among workers
144
293
  */
145
294
  static share(data, options = {}) {
146
- Container.append({ support: data });
295
+ // Instead of using append which replaces the entire container,
296
+ // directly update the support object to maintain proxy references
297
+ Object.assign(container.support, data)
298
+
299
+ // Track which keys were explicitly shared
300
+ Object.keys(data).forEach(key => container.sharedKeys.add(key))
301
+
147
302
  if (!options.local) {
148
- WorkerStorage.share(data);
303
+ WorkerStorage.share(data)
149
304
  }
150
305
  }
306
+
307
+ static createMocha(config = {}, opts = {}) {
308
+ const mochaConfig = config?.mocha || {}
309
+ if (config?.grep && !opts?.grep) {
310
+ mochaConfig.grep = config.grep
311
+ }
312
+ container.mocha = MochaFactory.create(mochaConfig, opts || {})
313
+ }
151
314
  }
152
315
 
153
- function createHelpers(config) {
154
- const helpers = {};
155
- let moduleName;
156
- for (const helperName in config) {
316
+ export default Container
317
+
318
+ async function createHelpers(config) {
319
+ const helpers = {}
320
+ for (let helperName in config) {
157
321
  try {
158
- if (config[helperName].require) {
159
- if (config[helperName].require.startsWith('.')) {
160
- // @ts-ignore
161
- moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
162
- } else {
163
- moduleName = config[helperName].require; // plugin helper
164
- }
165
- } else {
166
- moduleName = `./helper/${helperName}.js`; // built-in helper
322
+ let HelperClass
323
+
324
+ // Check if helper class was stored in config during ESM import processing
325
+ if (config[helperName]._helperClass) {
326
+ HelperClass = config[helperName]._helperClass
327
+ debug(`helper ${helperName} loaded from ESM import`)
167
328
  }
168
329
 
169
- // @ts-ignore
170
- let HelperClass;
171
- // check if the helper is the built-in, use the require() syntax.
172
- if (moduleName.startsWith('./helper/')) {
173
- const __dirname = dirname(fileURLToPath(import.meta.url));
174
- HelperClass = importSync(path.resolve(__dirname, moduleName)).default;
175
- } else {
176
- // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
177
- HelperClass = importSync(path.resolve(moduleName)).default;
330
+ // ESM import (legacy check)
331
+ if (!HelperClass && typeof helperName === 'function' && helperName.prototype) {
332
+ HelperClass = helperName
333
+ helperName = HelperClass.constructor.name
178
334
  }
179
335
 
180
- if (HelperClass && HelperClass._checkRequirements) {
181
- const requirements = HelperClass._checkRequirements();
182
- if (requirements) {
183
- let install;
184
- if (importSync('./utils.js').installedLocally()) {
185
- install = `npm install --save-dev ${requirements.join(' ')}`;
186
- } else {
187
- console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
188
- install = `[sudo] npm install -g ${requirements.join(' ')}`;
189
- }
190
- throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
336
+ // classical require - may be async for ESM modules
337
+ if (!HelperClass) {
338
+ const helperResult = requireHelperFromModule(helperName, config)
339
+ if (helperResult instanceof Promise) {
340
+ // Handle async ESM loading
341
+ helpers[helperName] = {}
342
+ asyncHelperPromise = asyncHelperPromise
343
+ .then(() => helperResult)
344
+ .then(async ResolvedHelperClass => {
345
+ debug(`helper ${helperName} resolved type: ${typeof ResolvedHelperClass}`, ResolvedHelperClass)
346
+
347
+ // Extract default export from ESM module wrapper if needed
348
+ if (ResolvedHelperClass && ResolvedHelperClass.__esModule && ResolvedHelperClass.default) {
349
+ ResolvedHelperClass = ResolvedHelperClass.default
350
+ debug(`extracted default export for ${helperName}, new type: ${typeof ResolvedHelperClass}`)
351
+ }
352
+
353
+ if (typeof ResolvedHelperClass !== 'function') {
354
+ throw new Error(`Helper '${helperName}' is not a class. Got: ${typeof ResolvedHelperClass}`)
355
+ }
356
+
357
+ checkHelperRequirements(ResolvedHelperClass)
358
+ helpers[helperName] = new ResolvedHelperClass(config[helperName])
359
+ if (helpers[helperName]._init) await helpers[helperName]._init()
360
+ debug(`helper ${helperName} async initialized`)
361
+ })
362
+ continue
363
+ } else {
364
+ HelperClass = helperResult
191
365
  }
192
366
  }
193
- helpers[helperName] = new HelperClass(config[helperName]);
367
+
368
+ // handle async CJS modules that use dynamic import
369
+ if (isAsyncFunction(HelperClass)) {
370
+ helpers[helperName] = {}
371
+
372
+ asyncHelperPromise = asyncHelperPromise
373
+ .then(() => HelperClass())
374
+ .then(ResolvedHelperClass => {
375
+ // Check if ResolvedHelperClass is a constructor function
376
+ if (typeof ResolvedHelperClass?.constructor !== 'function') {
377
+ throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
378
+ }
379
+
380
+ debug(`helper ${helperName} async initialized`)
381
+
382
+ helpers[helperName] = new ResolvedHelperClass(config[helperName])
383
+ })
384
+
385
+ continue
386
+ }
387
+
388
+ checkHelperRequirements(HelperClass)
389
+
390
+ helpers[helperName] = new HelperClass(config[helperName])
391
+ debug(`helper ${helperName} initialized`)
194
392
  } catch (err) {
195
- throw new Error(`Could not load helper ${helperName} from module '${moduleName}':\n${err.message}\n${err.stack}`);
393
+ throw new Error(`Could not load helper ${helperName} (${err.message})`)
196
394
  }
197
395
  }
198
396
 
397
+ // Wait for all async helpers to be resolved before calling _init
398
+ await asyncHelperPromise
399
+
199
400
  for (const name in helpers) {
200
- if (helpers[name]._init) helpers[name]._init();
401
+ if (helpers[name]._init) await helpers[name]._init()
201
402
  }
202
- return helpers;
403
+ return helpers
203
404
  }
204
405
 
205
- export function createSupportObjects(config) {
206
- const objects = {};
207
-
208
- for (const name in config) {
209
- objects[name] = {}; // placeholders
210
- }
211
-
212
- if (!config.I) {
213
- objects.I = actor();
214
-
215
- if (container.translation.I !== 'I') {
216
- objects[container.translation.I] = objects.I;
406
+ function checkHelperRequirements(HelperClass) {
407
+ if (HelperClass._checkRequirements) {
408
+ const requirements = HelperClass._checkRequirements()
409
+ if (requirements) {
410
+ let install
411
+ if (installedLocally()) {
412
+ install = `npm install --save-dev ${requirements.join(' ')}`
413
+ } else {
414
+ console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation')
415
+ install = `[sudo] npm install -g ${requirements.join(' ')}`
416
+ }
417
+ throw new Error(`Required modules are not installed.\n\nRUN: ${install}`)
217
418
  }
218
419
  }
420
+ }
219
421
 
220
- container.support = objects;
221
-
222
- function lazyLoad(name) {
223
- let newObj = getSupportObject(config, name);
422
+ async function requireHelperFromModule(helperName, config, HelperClass) {
423
+ const moduleName = getHelperModuleName(helperName, config)
424
+ if (moduleName.startsWith('./helper/')) {
224
425
  try {
225
- if (typeof newObj === 'function') {
226
- newObj = newObj();
227
- } else if (newObj._init) {
228
- newObj._init();
426
+ // For built-in helpers, use direct relative import with .js extension
427
+ const helperPath = `${moduleName}.js`
428
+ const mod = await import(helperPath)
429
+ HelperClass = mod.default || mod
430
+ } catch (err) {
431
+ throw err
432
+ }
433
+ } else {
434
+ // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
435
+ try {
436
+ // Try dynamic import for both CommonJS and ESM modules
437
+ const mod = await import(moduleName)
438
+ if (!mod && !mod.default) {
439
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
229
440
  }
441
+ HelperClass = mod.default || mod
230
442
  } catch (err) {
231
- throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}\n${err.stack}`);
443
+ if (err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || (err.message && err.message.includes('Unknown file extension'))) {
444
+ // This is likely a TypeScript helper file. Transpile it to a temporary JS file
445
+ // and import that as a reliable fallback (no NODE_OPTIONS required).
446
+ try {
447
+ const pathModule = await import('path')
448
+ const absolutePath = pathModule.default.resolve(moduleName)
449
+
450
+ // Attempt to load local 'typescript' to transpile the helper
451
+ let typescript
452
+ try {
453
+ typescript = await import('typescript')
454
+ } catch (tsImportErr) {
455
+ throw new Error(`TypeScript helper detected (${moduleName}). Please install 'typescript' in your project: npm install --save-dev typescript`)
456
+ }
457
+
458
+ const { tempFile, allTempFiles } = await transpileTypeScript(absolutePath, typescript)
459
+ debug(`Transpiled TypeScript helper: ${moduleName} -> ${tempFile}`)
460
+
461
+ try {
462
+ try {
463
+ const mod = await import(tempFile)
464
+ HelperClass = mod.default || mod
465
+ debug(`helper ${helperName} loaded from transpiled JS: ${tempFile}`)
466
+ } catch (importTempErr) {
467
+ // If import fails due to CommonJS named export incompatibility,
468
+ // try a quick transform: convert named imports from CommonJS packages
469
+ // into default import + destructuring. This resolves cases like
470
+ // "Named export 'Helper' not found. The requested module '@codeceptjs/helper' is a CommonJS module"
471
+ const msg = importTempErr && importTempErr.message || ''
472
+ const commonJsMatch = msg.match(/The requested module '(.+?)' is a CommonJS module/)
473
+ if (commonJsMatch) {
474
+ // Read the transpiled file, perform heuristic replacement, and import again
475
+ const fs = await import('fs')
476
+ let content = fs.readFileSync(tempFile, 'utf8')
477
+ // Heuristic: replace "import { X, Y } from 'mod'" with default import + destructure
478
+ content = content.replace(/import\s+\{([^}]+)\}\s+from\s+(['"])([^'"\)]+)\2/gm, (m, names, q, modName) => {
479
+ // Only adjust imports for the module reported in the error or for local modules
480
+ if (modName === commonJsMatch[1] || modName.startsWith('.') || !modName.includes('/')) {
481
+ const cleanedNames = names.trim()
482
+ return `import pkg__interop from ${q}${modName}${q};\nconst { ${cleanedNames} } = pkg__interop`
483
+ }
484
+ return m
485
+ })
486
+
487
+ // Write to a secondary temp file
488
+ const os = await import('os')
489
+ const path = await import('path')
490
+ const fallbackTemp = path.default.join(os.default.tmpdir(), `helper-fallback-${Date.now()}.mjs`)
491
+ fs.writeFileSync(fallbackTemp, content, 'utf8')
492
+ try {
493
+ const mod = await import(fallbackTemp)
494
+ HelperClass = mod.default || mod
495
+ debug(`helper ${helperName} loaded from transpiled JS after CommonJS interop fix: ${fallbackTemp}`)
496
+ } finally {
497
+ try { fs.unlinkSync(fallbackTemp) } catch (e) {}
498
+ }
499
+ } else {
500
+ throw importTempErr
501
+ }
502
+ }
503
+ } finally {
504
+ // Cleanup transpiled temporary files
505
+ const filesToClean = Array.isArray(allTempFiles) ? allTempFiles : [allTempFiles]
506
+ cleanupTempFiles(filesToClean)
507
+ }
508
+ } catch (importErr) {
509
+ throw new Error(
510
+ `Helper '${helperName}' is a TypeScript file but could not be loaded.\n` +
511
+ `Path: ${moduleName}\n` +
512
+ `Error: ${importErr.message}\n\n` +
513
+ `To load TypeScript helpers, install 'typescript' in your project or use an ESM loader (e.g. tsx):\n` +
514
+ ` npm install --save-dev typescript\n` +
515
+ ` OR run CodeceptJS with an ESM loader: NODE_OPTIONS='--import tsx' npx codeceptjs run\n\n` +
516
+ `CodeceptJS will transpile TypeScript helpers automatically at runtime if 'typescript' is available.`
517
+ )
518
+ }
519
+ } else if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
520
+ // This is an ESM module, use dynamic import
521
+ try {
522
+ const pathModule = await import('path')
523
+ const absolutePath = pathModule.default.resolve(moduleName)
524
+ const mod = await import(absolutePath)
525
+ HelperClass = mod.default || mod
526
+ debug(`helper ${helperName} loaded via ESM import`)
527
+ } catch (importErr) {
528
+ throw new Error(`Helper module '${moduleName}' could not be imported as ESM: ${importErr.message}`)
529
+ }
530
+ } else if (err.code === 'MODULE_NOT_FOUND') {
531
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
532
+ } else {
533
+ throw err
534
+ }
232
535
  }
233
- return newObj;
234
536
  }
537
+ return HelperClass
538
+ }
235
539
 
540
+ function createSupportObjects(config) {
236
541
  const asyncWrapper = function (f) {
237
542
  return function () {
238
- return f.apply(this, arguments).catch((e) => {
239
- recorder.saveFirstAsyncError(e);
240
- throw e;
241
- });
242
- };
243
- };
244
-
245
- Object.keys(objects).forEach((object) => {
246
- const currentObject = objects[object];
247
- Object.keys(currentObject).forEach((method) => {
248
- const currentMethod = currentObject[method];
249
- if (currentMethod && currentMethod[Symbol.toStringTag] === 'AsyncFunction') {
250
- objects[object][method] = asyncWrapper(currentMethod);
251
- }
252
- });
253
- });
543
+ return f.apply(this, arguments).catch(e => {
544
+ recorder.saveFirstAsyncError(e)
545
+ throw e
546
+ })
547
+ }
548
+ }
254
549
 
255
- return new Proxy({}, {
256
- has(target, key) {
257
- return key in config;
258
- },
259
- ownKeys() {
260
- return Reflect.ownKeys(config);
261
- },
262
- get(target, key) {
263
- // configured but not in support object, yet: load the module
264
- if (key in objects && !(key in target)) {
265
- // load default I
266
- if (key in objects && !(key in config)) {
267
- return target[key] = objects[key];
268
- }
550
+ function lazyLoad(name) {
551
+ return new Proxy(
552
+ {},
553
+ {
554
+ get(target, prop) {
555
+ // behavr like array or
556
+ if (prop === 'length') return Object.keys(config).length
557
+ if (prop === Symbol.iterator) {
558
+ return function* () {
559
+ for (let i = 0; i < Object.keys(config).length; i++) {
560
+ yield target[i]
561
+ }
562
+ }
563
+ }
564
+
565
+ // load actual name from vocabulary
566
+ if (container.translation && container.translation.I && name === 'I') {
567
+ // Use translated name for I
568
+ const actualName = container.translation.I
569
+ if (actualName !== 'I') {
570
+ name = actualName
571
+ }
572
+ }
573
+
574
+ if (name === 'I') {
575
+ if (!container.support.I) {
576
+ // Actor will be created during container.create()
577
+ return undefined
578
+ }
579
+ methodsOfObject(container.support.I)
580
+ return container.support.I[prop]
581
+ }
582
+
583
+ if (!container.support[name] && typeof config[name] === 'object') {
584
+ container.support[name] = config[name]
585
+ }
586
+
587
+ if (!container.support[name]) {
588
+ // Cannot load object synchronously in proxy getter
589
+ // Return undefined and log warning - object should be pre-loaded during container creation
590
+ debug(`Support object ${name} not pre-loaded, returning undefined`)
591
+ return undefined
592
+ }
593
+
594
+ const currentObject = container.support[name]
595
+ let currentValue = currentObject[prop]
596
+
597
+ if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
598
+ const ms = new MetaStep(name, prop)
599
+ ms.setContext(currentObject)
600
+ if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
601
+ debug(`metastep is created for ${name}.${prop.toString()}()`)
602
+ return ms.run.bind(ms, currentValue)
603
+ }
604
+
605
+ return currentValue
606
+ },
607
+ has(target, prop) {
608
+ if (!container.support[name]) {
609
+ // Note: This is sync, so we can't use async loadSupportObject here
610
+ // The object will be loaded lazily on first property access
611
+ return false
612
+ }
613
+ return prop in container.support[name]
614
+ },
615
+ getOwnPropertyDescriptor(target, prop) {
616
+ if (!container.support[name]) {
617
+ // Object will be loaded on property access
618
+ return {
619
+ enumerable: true,
620
+ configurable: true,
621
+ value: undefined,
622
+ }
623
+ }
624
+ return {
625
+ enumerable: true,
626
+ configurable: true,
627
+ value: container.support[name][prop],
628
+ }
629
+ },
630
+ ownKeys() {
631
+ if (!container.support[name]) {
632
+ return []
633
+ }
634
+ return Reflect.ownKeys(container.support[name])
635
+ },
636
+ },
637
+ )
638
+ }
269
639
 
270
- // load new object
271
- const object = lazyLoad(key);
272
- // check that object is a real object and not an array
273
- if (Object.prototype.toString.call(object) === '[object Object]') {
274
- return target[key] = Object.assign(objects[key], object);
640
+ const keys = Reflect.ownKeys(config)
641
+ return new Proxy(
642
+ {},
643
+ {
644
+ has(target, key) {
645
+ return keys.includes(key) || container.sharedKeys.has(key)
646
+ },
647
+ ownKeys() {
648
+ // Return both original config keys and explicitly shared keys
649
+ return [...new Set([...keys, ...container.sharedKeys])]
650
+ },
651
+ getOwnPropertyDescriptor(target, prop) {
652
+ return {
653
+ enumerable: true,
654
+ configurable: true,
655
+ value: target[prop],
275
656
  }
276
- target[key] = object;
277
- }
278
- return target[key];
657
+ },
658
+ get(target, key) {
659
+ // First check if this is an explicitly shared property
660
+ if (container.sharedKeys.has(key) && key in container.support) {
661
+ return container.support[key]
662
+ }
663
+ return lazyLoad(key)
664
+ },
279
665
  },
280
- });
666
+ )
281
667
  }
282
668
 
283
- function createPlugins(config, options = {}) {
284
- const plugins = {};
669
+ function createActor(actorPath) {
670
+ if (container.support.I) return container.support.I
671
+
672
+ // Default actor
673
+ container.support.I = actorFactory({}, Container)
285
674
 
286
- const enabledPluginsByOptions = (options.plugins || '').split(',');
675
+ return container.support.I
676
+ }
677
+
678
+ async function loadPluginAsync(modulePath, config) {
679
+ let pluginMod
680
+ try {
681
+ // Try dynamic import first (works for both ESM and CJS)
682
+ pluginMod = await import(modulePath)
683
+ } catch (err) {
684
+ throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
685
+ }
686
+
687
+ const pluginFactory = pluginMod.default || pluginMod
688
+ if (typeof pluginFactory !== 'function') {
689
+ throw new Error(`Plugin '${modulePath}' is not a function. Expected a plugin factory function.`)
690
+ }
691
+
692
+ return pluginFactory(config)
693
+ }
694
+
695
+ async function loadPluginFallback(modulePath, config) {
696
+ // This function is kept for backwards compatibility but now uses dynamic import
697
+ return await loadPluginAsync(modulePath, config)
698
+ }
699
+
700
+ async function createPlugins(config, options = {}) {
701
+ const plugins = {}
702
+
703
+ const enabledPluginsByOptions = (options.plugins || '').split(',')
287
704
  for (const pluginName in config) {
288
- if (!config[pluginName]) config[pluginName] = {};
289
- if (!config[pluginName].enabled && (enabledPluginsByOptions.indexOf(pluginName) < 0)) {
290
- continue; // plugin is disabled
705
+ if (!config[pluginName]) config[pluginName] = {}
706
+ if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
707
+ continue // plugin is disabled
291
708
  }
292
- let module;
709
+ let module
293
710
  try {
294
711
  if (config[pluginName].require) {
295
- module = config[pluginName].require;
296
- if (module.startsWith('.')) { // local
297
- // @ts-ignore
298
- module = path.resolve(global.codecept_dir, module); // custom plugin
712
+ module = config[pluginName].require
713
+ if (module.startsWith('.')) {
714
+ // local
715
+ module = path.resolve(global.codecept_dir, module) // custom plugin
299
716
  }
300
717
  } else {
301
- module = `./plugin/${pluginName}.js`;
718
+ module = `./plugin/${pluginName}.js`
302
719
  }
303
- plugins[pluginName] = importSync(module).default(config[pluginName]);
720
+
721
+ // Use async loading for all plugins (ESM and CJS)
722
+ plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
723
+ debug(`plugin ${pluginName} loaded via async import`)
304
724
  } catch (err) {
305
- throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`);
725
+ throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
306
726
  }
307
727
  }
308
- return plugins;
728
+ return plugins
309
729
  }
310
730
 
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
- }
731
+ async function loadGherkinStepsAsync(paths) {
732
+ global.Before = fn => event.dispatcher.on(event.test.started, fn)
733
+ global.After = fn => event.dispatcher.on(event.test.finished, fn)
734
+ global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
318
735
 
319
- export function loadGherkinSteps(paths) {
320
- // @ts-ignore
321
- global.Before = fn => event.dispatcher.on(event.test.started, fn);
322
- // @ts-ignore
323
- global.After = fn => event.dispatcher.on(event.test.finished, fn);
324
- global.Fail = fn => event.dispatcher.on(event.test.failed, fn);
736
+ // Import BDD module to access step file tracking functions
737
+ const bddModule = await import('./mocha/bdd.js')
325
738
 
326
739
  // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
327
740
  // If gherkin.steps is Array, it will go the old way
328
741
  // This is done so that we need not enter all Step Definition files under config.gherkin.steps
329
742
  if (Array.isArray(paths)) {
330
743
  for (const path of paths) {
331
- loadSupportObject(path, `Step Definition from ${path}`);
744
+ // Set context for step definition file location tracking
745
+ bddModule.setCurrentStepFile(path)
746
+ await loadSupportObject(path, `Step Definition from ${path}`)
747
+ bddModule.clearCurrentStepFile()
332
748
  }
333
749
  } else {
334
- // @ts-ignore
335
- const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : '';
750
+ const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
336
751
  if (folderPath !== '') {
337
- glob.sync(folderPath).forEach((file) => {
338
- loadSupportObject(file, `Step Definition from ${file}`);
339
- });
752
+ const files = globSync(folderPath)
753
+ for (const file of files) {
754
+ // Set context for step definition file location tracking
755
+ bddModule.setCurrentStepFile(file)
756
+ await loadSupportObject(file, `Step Definition from ${file}`)
757
+ bddModule.clearCurrentStepFile()
758
+ }
340
759
  }
341
760
  }
342
761
 
343
- // @ts-ignore
344
- delete global.Before;
345
- // @ts-ignore
346
- delete global.After;
347
- delete global.Fail;
762
+ delete global.Before
763
+ delete global.After
764
+ delete global.Fail
765
+ }
766
+
767
+ function loadGherkinSteps(paths) {
768
+ global.Before = fn => event.dispatcher.on(event.test.started, fn)
769
+ global.After = fn => event.dispatcher.on(event.test.finished, fn)
770
+ global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
771
+
772
+ // Gherkin step loading must be handled asynchronously
773
+ throw new Error('Gherkin step loading must be converted to async. Use loadGherkinStepsAsync() instead.')
774
+
775
+ delete global.Before
776
+ delete global.After
777
+ delete global.Fail
348
778
  }
349
779
 
350
- function loadSupportObject(modulePath, supportObjectName) {
351
- if (modulePath.charAt(0) === '.') {
352
- // @ts-ignore
353
- modulePath = path.join(global.codecept_dir, modulePath);
780
+ async function loadSupportObject(modulePath, supportObjectName) {
781
+ if (!modulePath) {
782
+ throw new Error(`Support object "${supportObjectName}" is not defined`)
783
+ }
784
+ // If function/class provided directly
785
+ if (typeof modulePath === 'function') {
786
+ try {
787
+ // class constructor
788
+ if (modulePath.prototype && modulePath.prototype.constructor === modulePath) {
789
+ return new modulePath()
790
+ }
791
+ // plain function factory
792
+ return modulePath()
793
+ } catch (err) {
794
+ throw new Error(`Could not include object ${supportObjectName} from function: ${err.message}`)
795
+ }
796
+ }
797
+ if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
798
+ modulePath = path.join(global.codecept_dir, modulePath)
354
799
  }
355
800
  try {
356
- const obj = importSync(modulePath).default || importSync(modulePath);
357
-
358
- if (typeof obj === 'function') {
359
- const fobj = obj();
801
+ // Use dynamic import for both ESM and CJS modules
802
+ let importPath = modulePath
803
+ let tempJsFile = null
804
+
805
+ if (typeof importPath === 'string') {
806
+ const ext = path.extname(importPath)
807
+
808
+ // Handle TypeScript files
809
+ if (ext === '.ts') {
810
+ try {
811
+ // Use the TypeScript transpilation utility
812
+ const typescript = await import('typescript')
813
+ const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
814
+
815
+ debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
816
+
817
+ // Attach cleanup handler
818
+ importPath = tempFile
819
+ // Store temp files list in a way that cleanup can access them
820
+ tempJsFile = allTempFiles
821
+
822
+ } catch (tsError) {
823
+ throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
824
+ }
825
+ } else if (!ext) {
826
+ // Append .js if no extension provided (ESM resolution requires it)
827
+ importPath = `${importPath}.js`
828
+ }
829
+ }
830
+
831
+ let obj
832
+ try {
833
+ obj = await import(importPath)
834
+ } catch (importError) {
835
+ // Clean up temp files if created before rethrowing
836
+ if (tempJsFile) {
837
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
838
+ cleanupTempFiles(filesToClean)
839
+ }
840
+ throw importError
841
+ } finally {
842
+ // Clean up temp files if created
843
+ if (tempJsFile) {
844
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
845
+ cleanupTempFiles(filesToClean)
846
+ }
847
+ }
360
848
 
361
- if (fobj.constructor.name === 'Actor') {
362
- const methods = getObjectMethods(fobj);
363
- Object.keys(methods)
364
- .forEach(key => {
365
- fobj[key] = methods[key];
366
- });
849
+ // Handle ESM module wrapper
850
+ let actualObj = obj
851
+ if (obj && obj.__esModule && obj.default) {
852
+ actualObj = obj.default
853
+ } else if (obj.default) {
854
+ actualObj = obj.default
855
+ }
367
856
 
368
- return methods;
857
+ // Handle different types of imports
858
+ if (typeof actualObj === 'function') {
859
+ // If it's a class (constructor function)
860
+ if (actualObj.prototype && actualObj.prototype.constructor === actualObj) {
861
+ const ClassName = actualObj
862
+ return new ClassName()
369
863
  }
864
+ // If it's a regular function
865
+ return actualObj()
370
866
  }
371
- if (typeof obj !== 'function'
372
- && Object.getPrototypeOf(obj) !== Object.prototype
373
- && !Array.isArray(obj)
374
- ) {
375
- const methods = getObjectMethods(obj);
376
- Object.keys(methods)
377
- .filter(key => !key.startsWith('_'))
378
- .forEach(key => {
379
- const currentMethod = methods[key];
380
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
381
- const ms = new MetaStep(supportObjectName, key);
382
- ms.setContext(methods);
383
- methods[key] = ms.run.bind(ms, currentMethod);
384
- }
385
- });
386
- return methods;
867
+
868
+ if (actualObj && Array.isArray(actualObj)) {
869
+ return actualObj
387
870
  }
388
- if (!Array.isArray(obj)) {
389
- Object.keys(obj)
390
- .filter(key => !key.startsWith('_'))
391
- .forEach(key => {
392
- const currentMethod = obj[key];
393
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
394
- const ms = new MetaStep(supportObjectName, key);
395
- ms.setContext(obj);
396
- obj[key] = ms.run.bind(ms, currentMethod);
397
- }
398
- });
871
+
872
+ // If it's a plain object
873
+ if (actualObj && typeof actualObj === 'object') {
874
+ // Call _init if it exists (for page objects)
875
+ if (actualObj._init && typeof actualObj._init === 'function') {
876
+ actualObj._init()
877
+ }
878
+ return actualObj
399
879
  }
400
880
 
401
- return obj;
881
+ throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
402
882
  } catch (err) {
403
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
883
+ throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
404
884
  }
405
885
  }
406
886
 
887
+ // Backwards compatibility function that throws an error for sync usage
888
+ function loadSupportObjectSync(modulePath, supportObjectName) {
889
+ throw new Error(`loadSupportObjectSync is deprecated. Support object "${supportObjectName || 'undefined'}" from '${modulePath}' must be loaded asynchronously. Use loadSupportObject() instead.`)
890
+ }
891
+
407
892
  /**
408
893
  * Method collect own property and prototype
409
894
  */
410
- function getObjectMethods(obj) {
411
- const methodsSet = new Set();
412
- let protoObj = Reflect.getPrototypeOf(obj);
413
- do {
414
- if (protoObj?.constructor.prototype !== Object.prototype) {
415
- const keys = Reflect.ownKeys(protoObj);
416
- keys.forEach(k => methodsSet.add(k));
417
- }
418
- } while (protoObj = Reflect.getPrototypeOf(protoObj));
419
- Reflect.ownKeys(obj).forEach(k => methodsSet.add(k));
420
- const methods = {};
421
- for (const key of methodsSet.keys()) {
422
- if (key !== 'constructor') methods[key] = obj[key];
423
- }
424
- return methods;
425
- }
426
895
 
427
- function loadTranslation(locale, vocabularies) {
896
+ async function loadTranslation(locale, vocabularies) {
428
897
  if (!locale) {
429
- return Translation.createEmpty();
898
+ return Translation.createEmpty()
430
899
  }
431
900
 
432
- let translation;
433
- locale = locale.replace('-', '_');
901
+ let translation
434
902
 
435
903
  // check if it is a known translation
436
- if (Translation.langs[locale]) {
437
- translation = new Translation(Translation.langs[locale]);
438
- } else { // @ts-ignore
439
- translation = Translation.createDefault();
440
- // @ts-ignore
441
- if (fileExists(path.join(global.codecept_dir, locale))) {
442
- // get from a provided file instead
443
- translation.loadVocabulary(locale);
904
+ const langs = await Translation.getLangs()
905
+ if (langs[locale]) {
906
+ translation = new Translation(langs[locale])
907
+ } else if (fileExists(path.join(global.codecept_dir, locale))) {
908
+ // get from a provided file instead
909
+ translation = Translation.createDefault()
910
+ translation.loadVocabulary(locale)
911
+ } else {
912
+ translation = Translation.createDefault()
913
+ }
914
+
915
+ vocabularies.forEach(v => translation.loadVocabulary(v))
916
+
917
+ return translation
918
+ }
919
+
920
+ function getHelperModuleName(helperName, config) {
921
+ // classical require
922
+ if (config[helperName].require) {
923
+ if (config[helperName].require.startsWith('.')) {
924
+ let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
925
+ // Add .js extension if not present for ESM compatibility
926
+ if (!path.extname(helperPath)) {
927
+ helperPath += '.js'
928
+ }
929
+ return helperPath // custom helper
444
930
  }
931
+ return config[helperName].require // plugin helper
932
+ }
933
+
934
+ // built-in helpers
935
+ if (helperName.startsWith('@codeceptjs/')) {
936
+ return helperName
445
937
  }
446
938
 
447
- vocabularies.forEach(v => translation.loadVocabulary(v));
939
+ // built-in helpers
940
+ return `./helper/${helperName}`
941
+ }
942
+ function normalizeAndJoin(basePath, subPath) {
943
+ // Normalize and convert slashes to forward slashes in one step
944
+ const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
945
+ const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
946
+
947
+ // If subPath is absolute (starts with "/"), return it as the final path
948
+ if (normalizedSub.startsWith('/')) {
949
+ return normalizedSub
950
+ }
448
951
 
449
- return translation;
952
+ // Join the paths using POSIX-style
953
+ return path.posix.join(normalizedBase, normalizedSub)
450
954
  }