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