codeceptjs 4.0.0-beta.3 → 4.0.0-beta.5

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 (155) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +141 -86
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/dryRun.js +30 -35
  17. package/lib/command/generate.js +10 -14
  18. package/lib/command/gherkin/snippets.js +75 -73
  19. package/lib/command/gherkin/steps.js +1 -1
  20. package/lib/command/info.js +42 -8
  21. package/lib/command/init.js +13 -12
  22. package/lib/command/interactive.js +10 -2
  23. package/lib/command/list.js +1 -1
  24. package/lib/command/run-multiple/chunk.js +48 -45
  25. package/lib/command/run-multiple.js +12 -35
  26. package/lib/command/run-workers.js +21 -58
  27. package/lib/command/utils.js +5 -6
  28. package/lib/command/workers/runTests.js +263 -222
  29. package/lib/container.js +386 -238
  30. package/lib/data/context.js +10 -13
  31. package/lib/data/dataScenarioConfig.js +8 -8
  32. package/lib/data/dataTableArgument.js +6 -6
  33. package/lib/data/table.js +5 -11
  34. package/lib/effects.js +223 -0
  35. package/lib/element/WebElement.js +327 -0
  36. package/lib/els.js +158 -0
  37. package/lib/event.js +21 -17
  38. package/lib/heal.js +88 -80
  39. package/lib/helper/AI.js +2 -1
  40. package/lib/helper/ApiDataFactory.js +4 -7
  41. package/lib/helper/Appium.js +50 -57
  42. package/lib/helper/FileSystem.js +3 -3
  43. package/lib/helper/GraphQLDataFactory.js +4 -4
  44. package/lib/helper/JSONResponse.js +75 -37
  45. package/lib/helper/Mochawesome.js +31 -9
  46. package/lib/helper/Nightmare.js +37 -58
  47. package/lib/helper/Playwright.js +267 -272
  48. package/lib/helper/Protractor.js +56 -87
  49. package/lib/helper/Puppeteer.js +247 -264
  50. package/lib/helper/REST.js +29 -17
  51. package/lib/helper/TestCafe.js +22 -47
  52. package/lib/helper/WebDriver.js +157 -368
  53. package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
  54. package/lib/helper/extras/Popup.js +22 -22
  55. package/lib/helper/network/utils.js +1 -1
  56. package/lib/helper/testcafe/testcafe-utils.js +27 -28
  57. package/lib/listener/emptyRun.js +55 -0
  58. package/lib/listener/exit.js +7 -10
  59. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  60. package/lib/listener/globalTimeout.js +165 -0
  61. package/lib/listener/helpers.js +15 -15
  62. package/lib/listener/mocha.js +1 -1
  63. package/lib/listener/result.js +12 -0
  64. package/lib/listener/retryEnhancer.js +85 -0
  65. package/lib/listener/steps.js +32 -18
  66. package/lib/listener/store.js +20 -0
  67. package/lib/locator.js +1 -1
  68. package/lib/mocha/asyncWrapper.js +231 -0
  69. package/lib/{interfaces → mocha}/bdd.js +3 -3
  70. package/lib/mocha/cli.js +308 -0
  71. package/lib/mocha/factory.js +104 -0
  72. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  73. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  74. package/lib/mocha/hooks.js +112 -0
  75. package/lib/mocha/index.js +12 -0
  76. package/lib/mocha/inject.js +29 -0
  77. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  78. package/lib/mocha/suite.js +82 -0
  79. package/lib/mocha/test.js +181 -0
  80. package/lib/mocha/types.d.ts +42 -0
  81. package/lib/mocha/ui.js +232 -0
  82. package/lib/output.js +93 -65
  83. package/lib/pause.js +160 -138
  84. package/lib/plugin/analyze.js +396 -0
  85. package/lib/plugin/auth.js +435 -0
  86. package/lib/plugin/autoDelay.js +8 -8
  87. package/lib/plugin/autoLogin.js +3 -338
  88. package/lib/plugin/commentStep.js +6 -1
  89. package/lib/plugin/coverage.js +10 -22
  90. package/lib/plugin/customLocator.js +3 -3
  91. package/lib/plugin/customReporter.js +52 -0
  92. package/lib/plugin/eachElement.js +1 -1
  93. package/lib/plugin/fakerTransform.js +1 -1
  94. package/lib/plugin/heal.js +36 -9
  95. package/lib/plugin/htmlReporter.js +1947 -0
  96. package/lib/plugin/pageInfo.js +140 -0
  97. package/lib/plugin/retryFailedStep.js +17 -18
  98. package/lib/plugin/retryTo.js +2 -113
  99. package/lib/plugin/screenshotOnFail.js +17 -58
  100. package/lib/plugin/selenoid.js +15 -35
  101. package/lib/plugin/standardActingHelpers.js +4 -1
  102. package/lib/plugin/stepByStepReport.js +56 -17
  103. package/lib/plugin/stepTimeout.js +5 -12
  104. package/lib/plugin/subtitles.js +4 -4
  105. package/lib/plugin/tryTo.js +3 -102
  106. package/lib/plugin/wdio.js +8 -10
  107. package/lib/recorder.js +155 -124
  108. package/lib/rerun.js +43 -42
  109. package/lib/result.js +161 -0
  110. package/lib/secret.js +1 -2
  111. package/lib/step/base.js +239 -0
  112. package/lib/step/comment.js +10 -0
  113. package/lib/step/config.js +50 -0
  114. package/lib/step/func.js +46 -0
  115. package/lib/step/helper.js +50 -0
  116. package/lib/step/meta.js +99 -0
  117. package/lib/step/record.js +74 -0
  118. package/lib/step/retry.js +11 -0
  119. package/lib/step/section.js +55 -0
  120. package/lib/step.js +21 -332
  121. package/lib/steps.js +50 -0
  122. package/lib/store.js +37 -5
  123. package/lib/template/heal.js +2 -11
  124. package/lib/test-server.js +323 -0
  125. package/lib/timeout.js +66 -0
  126. package/lib/utils.js +351 -218
  127. package/lib/within.js +75 -55
  128. package/lib/workerStorage.js +2 -1
  129. package/lib/workers.js +386 -277
  130. package/package.json +81 -75
  131. package/translations/de-DE.js +5 -3
  132. package/translations/fr-FR.js +5 -4
  133. package/translations/index.js +1 -0
  134. package/translations/it-IT.js +4 -3
  135. package/translations/ja-JP.js +4 -3
  136. package/translations/nl-NL.js +76 -0
  137. package/translations/pl-PL.js +4 -3
  138. package/translations/pt-BR.js +4 -3
  139. package/translations/ru-RU.js +4 -3
  140. package/translations/utils.js +9 -0
  141. package/translations/zh-CN.js +4 -3
  142. package/translations/zh-TW.js +4 -3
  143. package/typings/index.d.ts +197 -187
  144. package/typings/promiseBasedTypes.d.ts +53 -903
  145. package/typings/types.d.ts +372 -1042
  146. package/lib/cli.js +0 -257
  147. package/lib/helper/ExpectHelper.js +0 -391
  148. package/lib/helper/MockServer.js +0 -221
  149. package/lib/helper/SoftExpectHelper.js +0 -381
  150. package/lib/listener/artifacts.js +0 -19
  151. package/lib/listener/timeout.js +0 -109
  152. package/lib/mochaFactory.js +0 -113
  153. package/lib/plugin/debugErrors.js +0 -67
  154. package/lib/scenario.js +0 -224
  155. package/lib/ui.js +0 -236
package/lib/container.js CHANGED
@@ -1,31 +1,47 @@
1
- const glob = require('glob');
2
- const path = require('path');
3
- const { MetaStep } = require('./step');
4
- const { fileExists, isFunction, isAsyncFunction } = require('./utils');
5
- const Translation = require('./translation');
6
- const MochaFactory = require('./mochaFactory');
7
- const recorder = require('./recorder');
8
- const event = require('./event');
9
- const WorkerStorage = require('./workerStorage');
10
- const store = require('./store');
11
- const ai = require('./ai');
1
+ const { globSync } = require('glob')
2
+ const path = require('path')
3
+ const debug = require('debug')('codeceptjs:container')
4
+ const { MetaStep } = require('./step')
5
+ const { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally } = require('./utils')
6
+ const Translation = require('./translation')
7
+ const MochaFactory = require('./mocha/factory')
8
+ const recorder = require('./recorder')
9
+ const event = require('./event')
10
+ const WorkerStorage = require('./workerStorage')
11
+ const store = require('./store')
12
+ const Result = require('./result')
13
+ const ai = require('./ai')
14
+
15
+ let asyncHelperPromise
12
16
 
13
17
  let container = {
14
18
  helpers: {},
15
19
  support: {},
20
+ proxySupport: {},
16
21
  plugins: {},
22
+ actor: null,
17
23
  /**
18
24
  * @type {Mocha | {}}
19
25
  * @ignore
20
26
  */
21
27
  mocha: {},
22
28
  translation: {},
23
- };
29
+ /** @type {Result | null} */
30
+ result: null,
31
+ sharedKeys: new Set() // Track keys shared via share() function
32
+ }
24
33
 
25
34
  /**
26
35
  * Dependency Injection Container
27
36
  */
28
37
  class Container {
38
+ /**
39
+ * Get the standard acting helpers of CodeceptJS Container
40
+ *
41
+ */
42
+ static get STANDARD_ACTING_HELPERS() {
43
+ return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium', 'TestCafe']
44
+ }
29
45
  /**
30
46
  * Create container with all required helpers and support objects
31
47
  *
@@ -34,21 +50,32 @@ class Container {
34
50
  * @param {*} opts
35
51
  */
36
52
  static create(config, opts) {
37
- const mochaConfig = config.mocha || {};
38
- if (config.grep && !opts.grep) {
39
- mochaConfig.grep = config.grep;
40
- }
41
- this.createMocha = () => {
42
- container.mocha = MochaFactory.create(mochaConfig, opts || {});
43
- };
44
- this.createMocha();
45
- container.helpers = createHelpers(config.helpers || {});
46
- container.translation = loadTranslation(config.translation || null, config.vocabularies || []);
47
- container.support = createSupportObjects(config.include || {});
48
- container.plugins = createPlugins(config.plugins || {}, opts);
49
- if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
50
- if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
51
- if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
53
+ debug('creating container')
54
+ asyncHelperPromise = Promise.resolve()
55
+
56
+ // dynamically create mocha instance
57
+ const mochaConfig = config.mocha || {}
58
+ if (config.grep && !opts.grep) mochaConfig.grep = config.grep
59
+ this.createMocha = () => (container.mocha = MochaFactory.create(mochaConfig, opts || {}))
60
+ this.createMocha()
61
+
62
+ // create support objects
63
+ container.support = {}
64
+ container.helpers = createHelpers(config.helpers || {})
65
+ container.translation = loadTranslation(config.translation || null, config.vocabularies || [])
66
+ container.proxySupport = createSupportObjects(config.include || {})
67
+ container.plugins = createPlugins(config.plugins || {}, opts)
68
+ container.result = new Result()
69
+
70
+ createActor(config.include?.I)
71
+
72
+ if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
73
+ if (config.gherkin) loadGherkinSteps(config.gherkin.steps || [])
74
+ if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
75
+ }
76
+
77
+ static actor() {
78
+ return container.support.I
52
79
  }
53
80
 
54
81
  /**
@@ -60,9 +87,9 @@ class Container {
60
87
  */
61
88
  static plugins(name) {
62
89
  if (!name) {
63
- return container.plugins;
90
+ return container.plugins
64
91
  }
65
- return container.plugins[name];
92
+ return container.plugins[name]
66
93
  }
67
94
 
68
95
  /**
@@ -74,9 +101,9 @@ class Container {
74
101
  */
75
102
  static support(name) {
76
103
  if (!name) {
77
- return container.support;
104
+ return container.proxySupport
78
105
  }
79
- return container.support[name];
106
+ return container.support[name] || container.proxySupport[name]
80
107
  }
81
108
 
82
109
  /**
@@ -88,9 +115,9 @@ class Container {
88
115
  */
89
116
  static helpers(name) {
90
117
  if (!name) {
91
- return container.helpers;
118
+ return container.helpers
92
119
  }
93
- return container.helpers[name];
120
+ return container.helpers[name]
94
121
  }
95
122
 
96
123
  /**
@@ -99,7 +126,7 @@ class Container {
99
126
  * @api
100
127
  */
101
128
  static translation() {
102
- return container.translation;
129
+ return container.translation
103
130
  }
104
131
 
105
132
  /**
@@ -109,7 +136,19 @@ class Container {
109
136
  * @returns { * }
110
137
  */
111
138
  static mocha() {
112
- return container.mocha;
139
+ return container.mocha
140
+ }
141
+
142
+ /**
143
+ * Get result
144
+ *
145
+ * @returns {Result}
146
+ */
147
+ static result() {
148
+ if (!container.result) {
149
+ container.result = new Result()
150
+ }
151
+ return container.result
113
152
  }
114
153
 
115
154
  /**
@@ -119,8 +158,9 @@ class Container {
119
158
  * @param {Object<string, *>} newContainer
120
159
  */
121
160
  static append(newContainer) {
122
- const deepMerge = require('./utils').deepMerge;
123
- container = deepMerge(container, newContainer);
161
+ const deepMerge = require('./utils').deepMerge
162
+ container = deepMerge(container, newContainer)
163
+ debug('appended', JSON.stringify(newContainer).slice(0, 300))
124
164
  }
125
165
 
126
166
  /**
@@ -130,11 +170,26 @@ class Container {
130
170
  * @param {Object<string, *>} newSupport
131
171
  * @param {Object<string, *>} newPlugins
132
172
  */
133
- static clear(newHelpers, newSupport, newPlugins) {
134
- container.helpers = newHelpers || {};
135
- container.support = newSupport || {};
136
- container.plugins = newPlugins || {};
137
- container.translation = loadTranslation();
173
+ static clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
174
+ container.helpers = newHelpers
175
+ container.translation = loadTranslation()
176
+ container.proxySupport = createSupportObjects(newSupport)
177
+ container.plugins = newPlugins
178
+ container.sharedKeys = new Set() // Clear shared keys
179
+ asyncHelperPromise = Promise.resolve()
180
+ store.actor = null
181
+ debug('container cleared')
182
+ }
183
+
184
+ /**
185
+ * @param {Function|null} fn
186
+ * @returns {Promise<void>}
187
+ */
188
+ static async started(fn = null) {
189
+ if (fn) {
190
+ asyncHelperPromise = asyncHelperPromise.then(fn)
191
+ }
192
+ return asyncHelperPromise
138
193
  }
139
194
 
140
195
  /**
@@ -144,299 +199,392 @@ class Container {
144
199
  * @param {Object} options - set {local: true} to not share among workers
145
200
  */
146
201
  static share(data, options = {}) {
147
- Container.append({ support: data });
202
+ // Instead of using append which replaces the entire container,
203
+ // directly update the support object to maintain proxy references
204
+ Object.assign(container.support, data)
205
+
206
+ // Track which keys were explicitly shared
207
+ Object.keys(data).forEach(key => container.sharedKeys.add(key))
208
+
148
209
  if (!options.local) {
149
- WorkerStorage.share(data);
210
+ WorkerStorage.share(data)
150
211
  }
151
212
  }
213
+
214
+ static createMocha(config = {}, opts = {}) {
215
+ const mochaConfig = config?.mocha || {}
216
+ if (config?.grep && !opts?.grep) {
217
+ mochaConfig.grep = config.grep
218
+ }
219
+ container.mocha = MochaFactory.create(mochaConfig, opts || {})
220
+ }
152
221
  }
153
222
 
154
- module.exports = Container;
223
+ module.exports = Container
155
224
 
156
225
  function createHelpers(config) {
157
- const helpers = {};
158
- let moduleName;
159
- for (const helperName in config) {
226
+ const helpers = {}
227
+ for (let helperName in config) {
160
228
  try {
161
- if (config[helperName].require) {
162
- if (config[helperName].require.startsWith('.')) {
163
- moduleName = path.resolve(global.codecept_dir, config[helperName].require); // custom helper
164
- } else {
165
- moduleName = config[helperName].require; // plugin helper
166
- }
167
- } else {
168
- moduleName = `./helper/${helperName}`; // built-in helper
229
+ let HelperClass
230
+
231
+ // ESM import
232
+ if (helperName?.constructor === Function && helperName.prototype) {
233
+ HelperClass = helperName
234
+ helperName = HelperClass.constructor.name
169
235
  }
170
236
 
171
- // @ts-ignore
172
- let HelperClass;
173
- // check if the helper is the built-in, use the require() syntax.
174
- if (moduleName.startsWith('./helper/')) {
175
- HelperClass = require(moduleName);
176
- } else {
177
- // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
178
- HelperClass = require(moduleName).default || require(moduleName);
237
+ // classical require
238
+ if (!HelperClass) {
239
+ HelperClass = requireHelperFromModule(helperName, config)
179
240
  }
180
241
 
181
- if (HelperClass._checkRequirements) {
182
- const requirements = HelperClass._checkRequirements();
183
- if (requirements) {
184
- let install;
185
- if (require('./utils').installedLocally()) {
186
- install = `npm install --save-dev ${requirements.join(' ')}`;
187
- } else {
188
- console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation');
189
- install = `[sudo] npm install -g ${requirements.join(' ')}`;
190
- }
191
- throw new Error(`Required modules are not installed.\n\nRUN: ${install}`);
192
- }
242
+ // handle async CJS modules that use dynamic import
243
+ if (isAsyncFunction(HelperClass)) {
244
+ helpers[helperName] = {}
245
+
246
+ asyncHelperPromise = asyncHelperPromise
247
+ .then(() => HelperClass())
248
+ .then(ResolvedHelperClass => {
249
+ // Check if ResolvedHelperClass is a constructor function
250
+ if (typeof ResolvedHelperClass?.constructor !== 'function') {
251
+ throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
252
+ }
253
+
254
+ debug(`helper ${helperName} async initialized`)
255
+
256
+ helpers[helperName] = new ResolvedHelperClass(config[helperName])
257
+ })
258
+
259
+ continue
193
260
  }
194
- helpers[helperName] = new HelperClass(config[helperName]);
261
+
262
+ checkHelperRequirements(HelperClass)
263
+
264
+ helpers[helperName] = new HelperClass(config[helperName])
265
+ debug(`helper ${helperName} initialized`)
195
266
  } catch (err) {
196
- throw new Error(`Could not load helper ${helperName} from module '${moduleName}':\n${err.message}\n${err.stack}`);
267
+ throw new Error(`Could not load helper ${helperName} (${err.message})`)
197
268
  }
198
269
  }
199
270
 
200
271
  for (const name in helpers) {
201
- if (helpers[name]._init) helpers[name]._init();
272
+ if (helpers[name]._init) helpers[name]._init()
202
273
  }
203
- return helpers;
274
+ return helpers
204
275
  }
205
276
 
206
- function createSupportObjects(config) {
207
- const objects = {};
208
-
209
- for (const name in config) {
210
- objects[name] = {}; // placeholders
211
- }
212
-
213
- if (!config.I) {
214
- objects.I = require('./actor')();
215
-
216
- if (container.translation.I !== 'I') {
217
- objects[container.translation.I] = objects.I;
277
+ function checkHelperRequirements(HelperClass) {
278
+ if (HelperClass._checkRequirements) {
279
+ const requirements = HelperClass._checkRequirements()
280
+ if (requirements) {
281
+ let install
282
+ if (installedLocally()) {
283
+ install = `npm install --save-dev ${requirements.join(' ')}`
284
+ } else {
285
+ console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation')
286
+ install = `[sudo] npm install -g ${requirements.join(' ')}`
287
+ }
288
+ throw new Error(`Required modules are not installed.\n\nRUN: ${install}`)
218
289
  }
219
290
  }
291
+ }
220
292
 
221
- container.support = objects;
222
-
223
- function lazyLoad(name) {
224
- let newObj = getSupportObject(config, name);
293
+ function requireHelperFromModule(helperName, config, HelperClass) {
294
+ const moduleName = getHelperModuleName(helperName, config)
295
+ if (moduleName.startsWith('./helper/')) {
296
+ HelperClass = require(moduleName)
297
+ } else {
298
+ // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
225
299
  try {
226
- if (typeof newObj === 'function') {
227
- newObj = newObj();
228
- } else if (newObj._init) {
229
- newObj._init();
300
+ const mod = require(moduleName)
301
+ if (!mod && !mod.default) {
302
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
230
303
  }
304
+ HelperClass = mod.default || mod
231
305
  } catch (err) {
232
- throw new Error(`Initialization failed for ${name}: ${newObj}\n${err.message}\n${err.stack}`);
306
+ if (err.code === 'MODULE_NOT_FOUND') {
307
+ throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
308
+ }
309
+ throw err
233
310
  }
234
- return newObj;
235
311
  }
312
+ return HelperClass
313
+ }
236
314
 
315
+ function createSupportObjects(config) {
237
316
  const asyncWrapper = function (f) {
238
317
  return function () {
239
- return f.apply(this, arguments).catch((e) => {
240
- recorder.saveFirstAsyncError(e);
241
- throw e;
242
- });
243
- };
244
- };
245
-
246
- Object.keys(objects).forEach((object) => {
247
- const currentObject = objects[object];
248
- Object.keys(currentObject).forEach((method) => {
249
- const currentMethod = currentObject[method];
250
- if (currentMethod && currentMethod[Symbol.toStringTag] === 'AsyncFunction') {
251
- objects[object][method] = asyncWrapper(currentMethod);
252
- }
253
- });
254
- });
318
+ return f.apply(this, arguments).catch(e => {
319
+ recorder.saveFirstAsyncError(e)
320
+ throw e
321
+ })
322
+ }
323
+ }
255
324
 
256
- return new Proxy({}, {
257
- has(target, key) {
258
- return key in config;
259
- },
260
- ownKeys() {
261
- return Reflect.ownKeys(config);
262
- },
263
- get(target, key) {
264
- // configured but not in support object, yet: load the module
265
- if (key in objects && !(key in target)) {
266
- // load default I
267
- if (key in objects && !(key in config)) {
268
- return target[key] = objects[key];
269
- }
325
+ function lazyLoad(name) {
326
+ return new Proxy(
327
+ {},
328
+ {
329
+ get(target, prop) {
330
+ // behavr like array or
331
+ if (prop === 'length') return Object.keys(config).length
332
+ if (prop === Symbol.iterator) {
333
+ return function* () {
334
+ for (let i = 0; i < Object.keys(config).length; i++) {
335
+ yield target[i]
336
+ }
337
+ }
338
+ }
339
+
340
+ // load actual name from vocabulary
341
+ if (container.translation.name) {
342
+ name = container.translation.name
343
+ }
344
+
345
+ if (name === 'I') {
346
+ const actor = createActor(config.I)
347
+ methodsOfObject(actor)
348
+ return actor[prop]
349
+ }
350
+
351
+ if (!container.support[name] && typeof config[name] === 'object') {
352
+ container.support[name] = config[name]
353
+ }
354
+
355
+ if (!container.support[name]) {
356
+ // Load object on first access
357
+ const supportObject = loadSupportObject(config[name])
358
+ container.support[name] = supportObject
359
+ try {
360
+ if (container.support[name]._init) {
361
+ container.support[name]._init()
362
+ }
363
+ debug(`support object ${name} initialized`)
364
+ } catch (err) {
365
+ throw new Error(`Initialization failed for ${name}: ${container.support[name]}\n${err.message}\n${err.stack}`)
366
+ }
367
+ }
270
368
 
271
- // load new object
272
- const object = lazyLoad(key);
273
- // check that object is a real object and not an array
274
- if (Object.prototype.toString.call(object) === '[object Object]') {
275
- return target[key] = Object.assign(objects[key], object);
369
+ const currentObject = container.support[name]
370
+ let currentValue = currentObject[prop]
371
+
372
+ if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
373
+ const ms = new MetaStep(name, prop)
374
+ ms.setContext(currentObject)
375
+ if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
376
+ debug(`metastep is created for ${name}.${prop.toString()}()`)
377
+ return ms.run.bind(ms, currentValue)
378
+ }
379
+
380
+ return currentValue
381
+ },
382
+ has(target, prop) {
383
+ container.support[name] = container.support[name] || loadSupportObject(config[name])
384
+ return prop in container.support[name]
385
+ },
386
+ getOwnPropertyDescriptor(target, prop) {
387
+ container.support[name] = container.support[name] || loadSupportObject(config[name])
388
+ return {
389
+ enumerable: true,
390
+ configurable: true,
391
+ value: this.get(target, prop),
392
+ }
393
+ },
394
+ ownKeys() {
395
+ container.support[name] = container.support[name] || loadSupportObject(config[name])
396
+ return Reflect.ownKeys(container.support[name])
397
+ },
398
+ },
399
+ )
400
+ }
401
+
402
+ const keys = Reflect.ownKeys(config)
403
+ return new Proxy(
404
+ {},
405
+ {
406
+ has(target, key) {
407
+ return keys.includes(key) || container.sharedKeys.has(key)
408
+ },
409
+ ownKeys() {
410
+ // Return both original config keys and explicitly shared keys
411
+ return [...new Set([...keys, ...container.sharedKeys])]
412
+ },
413
+ getOwnPropertyDescriptor(target, prop) {
414
+ return {
415
+ enumerable: true,
416
+ configurable: true,
417
+ value: this.get(target, prop),
276
418
  }
277
- target[key] = object;
278
- }
279
- return target[key];
419
+ },
420
+ get(target, key) {
421
+ // First check if this is an explicitly shared property
422
+ if (container.sharedKeys.has(key) && key in container.support) {
423
+ return container.support[key]
424
+ }
425
+ return lazyLoad(key)
426
+ },
280
427
  },
281
- });
428
+ )
429
+ }
430
+
431
+ function createActor(actorPath) {
432
+ if (container.support.I) return container.support.I
433
+
434
+ if (actorPath) {
435
+ container.support.I = loadSupportObject(actorPath)
436
+ } else {
437
+ const actor = require('./actor')
438
+ container.support.I = actor()
439
+ }
440
+
441
+ return container.support.I
282
442
  }
283
443
 
284
444
  function createPlugins(config, options = {}) {
285
- const plugins = {};
445
+ const plugins = {}
286
446
 
287
- const enabledPluginsByOptions = (options.plugins || '').split(',');
447
+ const enabledPluginsByOptions = (options.plugins || '').split(',')
288
448
  for (const pluginName in config) {
289
- if (!config[pluginName]) config[pluginName] = {};
290
- if (!config[pluginName].enabled && (enabledPluginsByOptions.indexOf(pluginName) < 0)) {
291
- continue; // plugin is disabled
449
+ if (!config[pluginName]) config[pluginName] = {}
450
+ if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
451
+ continue // plugin is disabled
292
452
  }
293
- let module;
453
+ let module
294
454
  try {
295
455
  if (config[pluginName].require) {
296
- module = config[pluginName].require;
297
- if (module.startsWith('.')) { // local
298
- module = path.resolve(global.codecept_dir, module); // custom plugin
456
+ module = config[pluginName].require
457
+ if (module.startsWith('.')) {
458
+ // local
459
+ module = path.resolve(global.codecept_dir, module) // custom plugin
299
460
  }
300
461
  } else {
301
- module = `./plugin/${pluginName}`;
462
+ module = `./plugin/${pluginName}`
302
463
  }
303
- plugins[pluginName] = require(module)(config[pluginName]);
464
+ plugins[pluginName] = require(module)(config[pluginName])
304
465
  } catch (err) {
305
- throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`);
466
+ throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
306
467
  }
307
468
  }
308
- return plugins;
309
- }
310
-
311
- function getSupportObject(config, name) {
312
- const module = config[name];
313
- if (typeof module === 'string') {
314
- return loadSupportObject(module, name);
315
- }
316
- return module;
469
+ return plugins
317
470
  }
318
471
 
319
472
  function loadGherkinSteps(paths) {
320
- global.Before = fn => event.dispatcher.on(event.test.started, fn);
321
- global.After = fn => event.dispatcher.on(event.test.finished, fn);
322
- global.Fail = fn => event.dispatcher.on(event.test.failed, fn);
473
+ global.Before = fn => event.dispatcher.on(event.test.started, fn)
474
+ global.After = fn => event.dispatcher.on(event.test.finished, fn)
475
+ global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
323
476
 
324
477
  // If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
325
478
  // If gherkin.steps is Array, it will go the old way
326
479
  // This is done so that we need not enter all Step Definition files under config.gherkin.steps
327
480
  if (Array.isArray(paths)) {
328
481
  for (const path of paths) {
329
- loadSupportObject(path, `Step Definition from ${path}`);
482
+ loadSupportObject(path, `Step Definition from ${path}`)
330
483
  }
331
484
  } else {
332
- const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : '';
485
+ const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
333
486
  if (folderPath !== '') {
334
- glob.sync(folderPath).forEach((file) => {
335
- loadSupportObject(file, `Step Definition from ${file}`);
336
- });
487
+ globSync(folderPath).forEach(file => {
488
+ loadSupportObject(file, `Step Definition from ${file}`)
489
+ })
337
490
  }
338
491
  }
339
492
 
340
- delete global.Before;
341
- delete global.After;
342
- delete global.Fail;
493
+ delete global.Before
494
+ delete global.After
495
+ delete global.Fail
343
496
  }
344
497
 
345
498
  function loadSupportObject(modulePath, supportObjectName) {
499
+ if (!modulePath) {
500
+ throw new Error(`Support object "${supportObjectName}" is not defined`)
501
+ }
346
502
  if (modulePath.charAt(0) === '.') {
347
- modulePath = path.join(global.codecept_dir, modulePath);
503
+ modulePath = path.join(global.codecept_dir, modulePath)
348
504
  }
349
505
  try {
350
- const obj = require(modulePath);
506
+ const obj = require(modulePath)
351
507
 
508
+ // Handle different types of imports
352
509
  if (typeof obj === 'function') {
353
- const fobj = obj();
354
-
355
- if (fobj.constructor.name === 'Actor') {
356
- const methods = getObjectMethods(fobj);
357
- Object.keys(methods)
358
- .forEach(key => {
359
- fobj[key] = methods[key];
360
- });
361
-
362
- return methods;
510
+ // If it's a class (constructor function)
511
+ if (obj.prototype && obj.prototype.constructor === obj) {
512
+ const ClassName = obj
513
+ return new ClassName()
363
514
  }
515
+ // If it's a regular function
516
+ return obj()
364
517
  }
365
- if (typeof obj !== 'function'
366
- && Object.getPrototypeOf(obj) !== Object.prototype
367
- && !Array.isArray(obj)
368
- ) {
369
- const methods = getObjectMethods(obj);
370
- Object.keys(methods)
371
- .filter(key => !key.startsWith('_'))
372
- .forEach(key => {
373
- const currentMethod = methods[key];
374
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
375
- const ms = new MetaStep(supportObjectName, key);
376
- ms.setContext(methods);
377
- methods[key] = ms.run.bind(ms, currentMethod);
378
- }
379
- });
380
- return methods;
518
+
519
+ if (obj && Array.isArray(obj)) {
520
+ return obj
381
521
  }
382
- if (!Array.isArray(obj)) {
383
- Object.keys(obj)
384
- .filter(key => !key.startsWith('_'))
385
- .forEach(key => {
386
- const currentMethod = obj[key];
387
- if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
388
- const ms = new MetaStep(supportObjectName, key);
389
- ms.setContext(obj);
390
- obj[key] = ms.run.bind(ms, currentMethod);
391
- }
392
- });
522
+
523
+ // If it's a plain object
524
+ if (obj && typeof obj === 'object') {
525
+ return obj
393
526
  }
394
527
 
395
- return obj;
528
+ throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof obj}`)
396
529
  } catch (err) {
397
- throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`);
530
+ throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
398
531
  }
399
532
  }
400
533
 
401
534
  /**
402
535
  * Method collect own property and prototype
403
536
  */
404
- function getObjectMethods(obj) {
405
- const methodsSet = new Set();
406
- let protoObj = Reflect.getPrototypeOf(obj);
407
- do {
408
- if (protoObj.constructor.prototype !== Object.prototype) {
409
- const keys = Reflect.ownKeys(protoObj);
410
- keys.forEach(k => methodsSet.add(k));
411
- }
412
- } while (protoObj = Reflect.getPrototypeOf(protoObj));
413
- Reflect.ownKeys(obj).forEach(k => methodsSet.add(k));
414
- const methods = {};
415
- for (const key of methodsSet.keys()) {
416
- if (key !== 'constructor') methods[key] = obj[key];
417
- }
418
- return methods;
419
- }
420
537
 
421
538
  function loadTranslation(locale, vocabularies) {
422
539
  if (!locale) {
423
- return Translation.createEmpty();
540
+ return Translation.createEmpty()
424
541
  }
425
542
 
426
- let translation;
543
+ let translation
427
544
 
428
545
  // check if it is a known translation
429
546
  if (Translation.langs[locale]) {
430
- translation = new Translation(Translation.langs[locale]);
547
+ translation = new Translation(Translation.langs[locale])
431
548
  } else if (fileExists(path.join(global.codecept_dir, locale))) {
432
549
  // get from a provided file instead
433
- translation = Translation.createDefault();
434
- translation.loadVocabulary(locale);
550
+ translation = Translation.createDefault()
551
+ translation.loadVocabulary(locale)
435
552
  } else {
436
- translation = Translation.createDefault();
553
+ translation = Translation.createDefault()
554
+ }
555
+
556
+ vocabularies.forEach(v => translation.loadVocabulary(v))
557
+
558
+ return translation
559
+ }
560
+
561
+ function getHelperModuleName(helperName, config) {
562
+ // classical require
563
+ if (config[helperName].require) {
564
+ if (config[helperName].require.startsWith('.')) {
565
+ return path.resolve(global.codecept_dir, config[helperName].require) // custom helper
566
+ }
567
+ return config[helperName].require // plugin helper
568
+ }
569
+
570
+ // built-in helpers
571
+ if (helperName.startsWith('@codeceptjs/')) {
572
+ return helperName
437
573
  }
438
574
 
439
- vocabularies.forEach(v => translation.loadVocabulary(v));
575
+ // built-in helpers
576
+ return `./helper/${helperName}`
577
+ }
578
+ function normalizeAndJoin(basePath, subPath) {
579
+ // Normalize and convert slashes to forward slashes in one step
580
+ const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
581
+ const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
582
+
583
+ // If subPath is absolute (starts with "/"), return it as the final path
584
+ if (normalizedSub.startsWith('/')) {
585
+ return normalizedSub
586
+ }
440
587
 
441
- return translation;
588
+ // Join the paths using POSIX-style
589
+ return path.posix.join(normalizedBase, normalizedSub)
442
590
  }