codeceptjs 4.0.0-beta.1 → 4.0.0-beta.10.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 +751 -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,696 @@ 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
+ const jsContent = transpile(tsContent, {
694
+ module: 99, // ModuleKind.ESNext
695
+ target: 99, // ScriptTarget.ESNext
696
+ esModuleInterop: true,
697
+ allowSyntheticDefaultImports: true,
698
+ })
699
+
700
+ return jsContent
701
+ }
702
+
703
+ // Create a map to track transpiled files
704
+ const transpiledFiles = new Map()
705
+ const baseDir = path.dirname(importPath)
706
+
707
+ // Transpile main file
708
+ let jsContent = transpileTS(importPath)
709
+
710
+ // Find and transpile all relative TypeScript imports
711
+ // Match: import ... from './file' or '../file' or './file.ts'
712
+ const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
713
+ let match
714
+ const imports = []
715
+
716
+ while ((match = importRegex.exec(jsContent)) !== null) {
717
+ imports.push(match[1])
718
+ }
719
+
720
+ // Transpile each imported TypeScript file
721
+ for (const relativeImport of imports) {
722
+ let importedPath = path.resolve(baseDir, relativeImport)
723
+
724
+ // Handle .js extensions that might actually be .ts files
725
+ if (importedPath.endsWith('.js')) {
726
+ const tsVersion = importedPath.replace(/\.js$/, '.ts')
727
+ if (fs.existsSync(tsVersion)) {
728
+ importedPath = tsVersion
729
+ }
730
+ }
731
+
732
+ // Try adding .ts extension if file doesn't exist and no extension provided
733
+ if (!path.extname(importedPath)) {
734
+ if (fs.existsSync(importedPath + '.ts')) {
735
+ importedPath = importedPath + '.ts'
736
+ }
737
+ }
738
+
739
+ // If it's a TypeScript file, transpile it
740
+ if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
741
+ const transpiledImportContent = transpileTS(importedPath)
742
+ const tempImportFile = importedPath.replace(/\.ts$/, '.temp.mjs')
743
+ fs.writeFileSync(tempImportFile, transpiledImportContent)
744
+ transpiledFiles.set(importedPath, tempImportFile)
745
+ debug(`Transpiled dependency: ${importedPath} -> ${tempImportFile}`)
746
+ }
384
747
  }
385
- });
386
- return methods;
748
+
749
+ // Replace imports in the main file to point to temp .mjs files
750
+ jsContent = jsContent.replace(
751
+ /from\s+['"](\..+?)(?:\.ts)?['"]/g,
752
+ (match, importPath) => {
753
+ let resolvedPath = path.resolve(baseDir, importPath)
754
+
755
+ // Handle .js extension that might be .ts
756
+ if (resolvedPath.endsWith('.js')) {
757
+ const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
758
+ if (transpiledFiles.has(tsVersion)) {
759
+ const tempFile = transpiledFiles.get(tsVersion)
760
+ const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
761
+ return `from './${relPath}'`
762
+ }
763
+ }
764
+
765
+ // Try with .ts extension
766
+ const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
767
+
768
+ // If we transpiled this file, use the temp file
769
+ if (transpiledFiles.has(tsPath)) {
770
+ const tempFile = transpiledFiles.get(tsPath)
771
+ // Get relative path from main temp file to this temp file
772
+ const relPath = path.relative(baseDir, tempFile).replace(/\\/g, '/')
773
+ return `from './${relPath}'`
774
+ }
775
+
776
+ // Otherwise, keep the import as-is
777
+ return match
778
+ }
779
+ )
780
+
781
+ // Create a temporary JS file with .mjs extension for the main file
782
+ tempJsFile = importPath.replace(/\.ts$/, '.temp.mjs')
783
+ fs.writeFileSync(tempJsFile, jsContent)
784
+
785
+ // Store all temp files for cleanup
786
+ const allTempFiles = [tempJsFile, ...Array.from(transpiledFiles.values())]
787
+
788
+ // Attach cleanup handler
789
+ importPath = tempJsFile
790
+ // Store temp files list in a way that cleanup can access them
791
+ tempJsFile = allTempFiles
792
+
793
+ } catch (tsError) {
794
+ throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
795
+ }
796
+ } else if (!ext) {
797
+ // Append .js if no extension provided (ESM resolution requires it)
798
+ importPath = `${importPath}.js`
799
+ }
387
800
  }
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);
801
+
802
+ let obj
803
+ try {
804
+ obj = await import(importPath)
805
+ } catch (importError) {
806
+ // Clean up temp files if created before rethrowing
807
+ if (tempJsFile) {
808
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
809
+ for (const file of filesToClean) {
810
+ try {
811
+ if (fs.existsSync(file)) {
812
+ fs.unlinkSync(file)
813
+ }
814
+ } catch (cleanupError) {
815
+ // Ignore cleanup errors
816
+ }
817
+ }
818
+ }
819
+ throw importError
820
+ } finally {
821
+ // Clean up temp files if created
822
+ if (tempJsFile) {
823
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
824
+ for (const file of filesToClean) {
825
+ try {
826
+ if (fs.existsSync(file)) {
827
+ fs.unlinkSync(file)
828
+ }
829
+ } catch (cleanupError) {
830
+ // Ignore cleanup errors
397
831
  }
398
- });
832
+ }
833
+ }
834
+ }
835
+
836
+ // Handle ESM module wrapper
837
+ let actualObj = obj
838
+ if (obj && obj.__esModule && obj.default) {
839
+ actualObj = obj.default
840
+ } else if (obj.default) {
841
+ actualObj = obj.default
842
+ }
843
+
844
+ // Handle different types of imports
845
+ if (typeof actualObj === 'function') {
846
+ // If it's a class (constructor function)
847
+ if (actualObj.prototype && actualObj.prototype.constructor === actualObj) {
848
+ const ClassName = actualObj
849
+ return new ClassName()
850
+ }
851
+ // If it's a regular function
852
+ return actualObj()
853
+ }
854
+
855
+ if (actualObj && Array.isArray(actualObj)) {
856
+ return actualObj
399
857
  }
400
858
 
401
- return obj;
859
+ // If it's a plain object
860
+ if (actualObj && typeof actualObj === 'object') {
861
+ // Call _init if it exists (for page objects)
862
+ if (actualObj._init && typeof actualObj._init === 'function') {
863
+ actualObj._init()
864
+ }
865
+ return actualObj
866
+ }
867
+
868
+ throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
402
869
  } catch (err) {
403
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
870
+ throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
404
871
  }
405
872
  }
406
873
 
874
+ // Backwards compatibility function that throws an error for sync usage
875
+ function loadSupportObjectSync(modulePath, supportObjectName) {
876
+ throw new Error(`loadSupportObjectSync is deprecated. Support object "${supportObjectName || 'undefined'}" from '${modulePath}' must be loaded asynchronously. Use loadSupportObject() instead.`)
877
+ }
878
+
407
879
  /**
408
880
  * Method collect own property and prototype
409
881
  */
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
882
 
427
- function loadTranslation(locale, vocabularies) {
883
+ async function loadTranslation(locale, vocabularies) {
428
884
  if (!locale) {
429
- return Translation.createEmpty();
885
+ return Translation.createEmpty()
430
886
  }
431
887
 
432
- let translation;
433
- locale = locale.replace('-', '_');
888
+ let translation
434
889
 
435
890
  // 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);
891
+ const langs = await Translation.getLangs()
892
+ if (langs[locale]) {
893
+ translation = new Translation(langs[locale])
894
+ } else if (fileExists(path.join(global.codecept_dir, locale))) {
895
+ // get from a provided file instead
896
+ translation = Translation.createDefault()
897
+ translation.loadVocabulary(locale)
898
+ } else {
899
+ translation = Translation.createDefault()
900
+ }
901
+
902
+ vocabularies.forEach(v => translation.loadVocabulary(v))
903
+
904
+ return translation
905
+ }
906
+
907
+ function getHelperModuleName(helperName, config) {
908
+ // classical require
909
+ if (config[helperName].require) {
910
+ if (config[helperName].require.startsWith('.')) {
911
+ let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
912
+ // Add .js extension if not present for ESM compatibility
913
+ if (!path.extname(helperPath)) {
914
+ helperPath += '.js'
915
+ }
916
+ return helperPath // custom helper
444
917
  }
918
+ return config[helperName].require // plugin helper
445
919
  }
446
920
 
447
- vocabularies.forEach(v => translation.loadVocabulary(v));
921
+ // built-in helpers
922
+ if (helperName.startsWith('@codeceptjs/')) {
923
+ return helperName
924
+ }
925
+
926
+ // built-in helpers
927
+ return `./helper/${helperName}`
928
+ }
929
+ function normalizeAndJoin(basePath, subPath) {
930
+ // Normalize and convert slashes to forward slashes in one step
931
+ const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
932
+ const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
933
+
934
+ // If subPath is absolute (starts with "/"), return it as the final path
935
+ if (normalizedSub.startsWith('/')) {
936
+ return normalizedSub
937
+ }
448
938
 
449
- return translation;
939
+ // Join the paths using POSIX-style
940
+ return path.posix.join(normalizedBase, normalizedSub)
450
941
  }