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

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