codeceptjs 3.6.10 → 3.7.0-beta.10

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