codeceptjs 4.0.1-beta.1 → 4.0.1-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.
package/lib/actor.js CHANGED
@@ -75,7 +75,8 @@ export default function (obj = {}, container) {
75
75
  if (!container) {
76
76
  container = Container
77
77
  }
78
-
78
+
79
+ // Get existing actor or create a new one
79
80
  const actor = container.actor() || new Actor()
80
81
 
81
82
  // load all helpers once container initialized
@@ -111,14 +112,17 @@ export default function (obj = {}, container) {
111
112
  }
112
113
  })
113
114
 
114
- container.append({
115
- support: {
116
- I: actor,
117
- },
118
- })
115
+ // Update container.support.I to ensure it has the latest actor reference
116
+ if (!container.actor() || container.actor() !== actor) {
117
+ container.append({
118
+ support: {
119
+ I: actor,
120
+ },
121
+ })
122
+ }
119
123
  })
120
- // store.actor = actor;
121
- // add custom steps from actor
124
+
125
+ // add custom steps from actor immediately
122
126
  Object.keys(obj).forEach(key => {
123
127
  const ms = new MetaStep('I', key)
124
128
  ms.setContext(actor)
package/lib/container.js CHANGED
@@ -22,6 +22,7 @@ let container = {
22
22
  helpers: {},
23
23
  support: {},
24
24
  proxySupport: {},
25
+ proxySupportConfig: {}, // Track config used to create proxySupport
25
26
  plugins: {},
26
27
  actor: null,
27
28
  /**
@@ -67,14 +68,15 @@ class Container {
67
68
  container.support = {}
68
69
  container.helpers = await createHelpers(config.helpers || {})
69
70
  container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
70
- container.proxySupport = createSupportObjects(config.include || {})
71
+ container.proxySupportConfig = config.include || {}
72
+ container.proxySupport = createSupportObjects(container.proxySupportConfig)
71
73
  container.plugins = await createPlugins(config.plugins || {}, opts)
72
74
  container.result = new Result()
73
75
 
74
76
  // Preload includes (so proxies can expose real objects synchronously)
75
77
  const includes = config.include || {}
76
78
 
77
- // Ensure I is available for DI modules at import time
79
+ // Check if custom I is provided
78
80
  if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
79
81
  try {
80
82
  const mod = includes.I
@@ -89,7 +91,7 @@ class Container {
89
91
  throw new Error(`Could not include object I: ${e.message}`)
90
92
  }
91
93
  } else {
92
- // Create default actor if not provided via includes
94
+ // Create default actor - this sets up the callback in asyncHelperPromise
93
95
  createActor()
94
96
  }
95
97
 
@@ -110,6 +112,9 @@ class Container {
110
112
  }
111
113
  }
112
114
 
115
+ // Wait for all async helpers to finish loading and populate the actor
116
+ await asyncHelperPromise
117
+
113
118
  if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
114
119
  if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
115
120
  if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
@@ -202,19 +207,12 @@ class Container {
202
207
  static append(newContainer) {
203
208
  container = deepMerge(container, newContainer)
204
209
 
205
- // If new helpers are added, set the helpers property on them
206
- if (newContainer.helpers) {
207
- for (const name in newContainer.helpers) {
208
- if (container.helpers[name] && typeof container.helpers[name] === 'object') {
209
- container.helpers[name].helpers = container.helpers
210
- }
211
- }
212
- }
213
-
214
210
  // If new support objects are added, update the proxy support
215
211
  if (newContainer.support) {
216
- const newProxySupport = createSupportObjects(newContainer.support)
217
- container.proxySupport = { ...container.proxySupport, ...newProxySupport }
212
+ // Merge the new support config with existing config
213
+ container.proxySupportConfig = { ...container.proxySupportConfig, ...newContainer.support }
214
+ // Recreate the proxy with merged config
215
+ container.proxySupport = createSupportObjects(container.proxySupportConfig)
218
216
  }
219
217
 
220
218
  debug('appended', JSON.stringify(newContainer).slice(0, 300))
@@ -230,6 +228,7 @@ class Container {
230
228
  static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
231
229
  container.helpers = newHelpers
232
230
  container.translation = await loadTranslation()
231
+ container.proxySupportConfig = newSupport
233
232
  container.proxySupport = createSupportObjects(newSupport)
234
233
  container.plugins = newPlugins
235
234
  container.sharedKeys = new Set() // Clear shared keys
@@ -301,7 +300,7 @@ async function createHelpers(config) {
301
300
  if (!HelperClass) {
302
301
  const helperResult = requireHelperFromModule(helperName, config)
303
302
  if (helperResult instanceof Promise) {
304
- // Handle async ESM loading
303
+ // Handle async ESM loading - create placeholder
305
304
  helpers[helperName] = {}
306
305
  asyncHelperPromise = asyncHelperPromise
307
306
  .then(() => helperResult)
@@ -320,8 +319,7 @@ async function createHelpers(config) {
320
319
 
321
320
  checkHelperRequirements(ResolvedHelperClass)
322
321
  helpers[helperName] = new ResolvedHelperClass(config[helperName])
323
- if (helpers[helperName]._init) await helpers[helperName]._init()
324
- debug(`helper ${helperName} async initialized`)
322
+ debug(`helper ${helperName} async loaded`)
325
323
  })
326
324
  continue
327
325
  } else {
@@ -341,9 +339,8 @@ async function createHelpers(config) {
341
339
  throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
342
340
  }
343
341
 
344
- debug(`helper ${helperName} async initialized`)
345
-
346
342
  helpers[helperName] = new ResolvedHelperClass(config[helperName])
343
+ debug(`helper ${helperName} async CJS loaded`)
347
344
  })
348
345
 
349
346
  continue
@@ -358,19 +355,18 @@ async function createHelpers(config) {
358
355
  }
359
356
  }
360
357
 
361
- // Set helpers property on each helper to allow access to other helpers
362
- for (const name in helpers) {
363
- if (helpers[name] && typeof helpers[name] === 'object') {
364
- helpers[name].helpers = helpers
358
+ // Don't await here - let Container.create() handle the await
359
+ // This allows actor callbacks to be registered before resolution
360
+ asyncHelperPromise = asyncHelperPromise.then(async () => {
361
+ // Call _init on all helpers after they're all loaded
362
+ for (const name in helpers) {
363
+ if (helpers[name]._init) {
364
+ await helpers[name]._init()
365
+ debug(`helper ${name} _init() called`)
366
+ }
365
367
  }
366
- }
368
+ })
367
369
 
368
- // Wait for async helpers and call _init
369
- await asyncHelperPromise
370
-
371
- for (const name in helpers) {
372
- if (helpers[name]._init) await helpers[name]._init()
373
- }
374
370
  return helpers
375
371
  }
376
372
 
@@ -402,20 +398,52 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
402
398
  throw err
403
399
  }
404
400
  } else {
401
+ // Handle TypeScript files
402
+ let importPath = moduleName
403
+ let tempJsFile = null
404
+ const ext = path.extname(moduleName)
405
+
406
+ if (ext === '.ts') {
407
+ try {
408
+ // Use the TypeScript transpilation utility
409
+ const typescript = await import('typescript')
410
+ const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
411
+
412
+ debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
413
+
414
+ importPath = tempFile
415
+ tempJsFile = allTempFiles
416
+ } catch (tsError) {
417
+ throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
418
+ }
419
+ }
420
+
405
421
  // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
406
422
  try {
407
423
  // Try dynamic import for both CommonJS and ESM modules
408
- const mod = await import(moduleName)
424
+ const mod = await import(importPath)
409
425
  if (!mod && !mod.default) {
410
426
  throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
411
427
  }
412
428
  HelperClass = mod.default || mod
429
+
430
+ // Clean up temp files if created
431
+ if (tempJsFile) {
432
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
433
+ cleanupTempFiles(filesToClean)
434
+ }
413
435
  } catch (err) {
436
+ // Clean up temp files before rethrowing
437
+ if (tempJsFile) {
438
+ const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
439
+ cleanupTempFiles(filesToClean)
440
+ }
441
+
414
442
  if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
415
443
  // This is an ESM module, use dynamic import
416
444
  try {
417
445
  const pathModule = await import('path')
418
- const absolutePath = pathModule.default.resolve(moduleName)
446
+ const absolutePath = pathModule.default.resolve(importPath)
419
447
  const mod = await import(absolutePath)
420
448
  HelperClass = mod.default || mod
421
449
  debug(`helper ${helperName} loaded via ESM import`)
@@ -544,10 +572,17 @@ function createSupportObjects(config) {
544
572
  return [...new Set([...keys, ...container.sharedKeys])]
545
573
  },
546
574
  getOwnPropertyDescriptor(target, prop) {
575
+ // For destructuring to work, we need to return the actual value from the getter
576
+ let value
577
+ if (container.sharedKeys.has(prop) && prop in container.support) {
578
+ value = container.support[prop]
579
+ } else {
580
+ value = lazyLoad(prop)
581
+ }
547
582
  return {
548
583
  enumerable: true,
549
584
  configurable: true,
550
- value: target[prop],
585
+ value: value,
551
586
  }
552
587
  },
553
588
  get(target, key) {
@@ -45,6 +45,8 @@ class GraphQL extends Helper {
45
45
  timeout: 10000,
46
46
  defaultHeaders: {},
47
47
  endpoint: '',
48
+ onRequest: null,
49
+ onResponse: null,
48
50
  }
49
51
  this.options = Object.assign(this.options, config)
50
52
  this.headers = { ...this.options.defaultHeaders }
@@ -87,8 +89,8 @@ class GraphQL extends Helper {
87
89
 
88
90
  request.headers = { ...this.headers, ...request.headers }
89
91
 
90
- if (this.config.onRequest) {
91
- await this.config.onRequest(request)
92
+ if (this.options.onRequest) {
93
+ await this.options.onRequest(request)
92
94
  }
93
95
 
94
96
  this.debugSection('Request', JSON.stringify(request))
@@ -102,8 +104,8 @@ class GraphQL extends Helper {
102
104
  response = err.response
103
105
  }
104
106
 
105
- if (this.config.onResponse) {
106
- await this.config.onResponse(response)
107
+ if (this.options.onResponse) {
108
+ await this.options.onResponse(response)
107
109
  }
108
110
 
109
111
  this.debugSection('Response', JSON.stringify(response.data))
@@ -72,8 +72,8 @@ class JSONResponse extends Helper {
72
72
  if (!this.helpers[this.options.requestHelper]) {
73
73
  throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
74
74
  }
75
- const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse
76
- this.helpers[this.options.requestHelper].config.onResponse = response => {
75
+ const origOnResponse = this.helpers[this.options.requestHelper].options.onResponse
76
+ this.helpers[this.options.requestHelper].options.onResponse = response => {
77
77
  this.response = response
78
78
  if (typeof origOnResponse === 'function') origOnResponse(response)
79
79
  }
@@ -83,7 +83,6 @@ class JSONResponse extends Helper {
83
83
  this.response = null
84
84
  }
85
85
 
86
-
87
86
  /**
88
87
  * Checks that response code is equal to the provided one
89
88
  *
@@ -372,4 +371,4 @@ class JSONResponse extends Helper {
372
371
  }
373
372
  }
374
373
 
375
- export { JSONResponse as default }
374
+ export { JSONResponse, JSONResponse as default }
@@ -355,7 +355,7 @@ class Playwright extends Helper {
355
355
  this.recordingWebSocketMessages = false
356
356
  this.recordedWebSocketMessagesAtLeastOnce = false
357
357
  this.cdpSession = null
358
-
358
+
359
359
  // Filter out invalid customLocatorStrategies (empty arrays, objects without functions)
360
360
  // This can happen in worker threads where config is serialized/deserialized
361
361
  let validCustomLocators = null
@@ -367,7 +367,7 @@ class Playwright extends Helper {
367
367
  validCustomLocators = config.customLocatorStrategies
368
368
  }
369
369
  }
370
-
370
+
371
371
  this.customLocatorStrategies = validCustomLocators
372
372
  this._customLocatorsRegistered = false
373
373
 
@@ -416,6 +416,7 @@ class Playwright extends Helper {
416
416
  ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors,
417
417
  highlightElement: false,
418
418
  storageState: undefined,
419
+ onResponse: null,
419
420
  }
420
421
 
421
422
  process.env.testIdAttribute = 'data-testid'
@@ -794,10 +795,7 @@ class Playwright extends Helper {
794
795
  await Promise.allSettled(pages.map(p => p.close().catch(() => {})))
795
796
  }
796
797
  // Use timeout to prevent hanging (10s should be enough for browser cleanup)
797
- await Promise.race([
798
- this._stopBrowser(),
799
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)),
800
- ])
798
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))])
801
799
  } catch (e) {
802
800
  console.warn('Warning during browser restart in _after:', e.message)
803
801
  // Force cleanup even on timeout
@@ -840,10 +838,7 @@ class Playwright extends Helper {
840
838
  if (this.isRunning) {
841
839
  try {
842
840
  // Add timeout protection to prevent hanging
843
- await Promise.race([
844
- this._stopBrowser(),
845
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)),
846
- ])
841
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))])
847
842
  } catch (e) {
848
843
  console.warn('Warning during suite cleanup:', e.message)
849
844
  // Track suite cleanup failures
@@ -954,10 +949,7 @@ class Playwright extends Helper {
954
949
  if (this.isRunning) {
955
950
  try {
956
951
  // Add timeout protection to prevent hanging
957
- await Promise.race([
958
- this._stopBrowser(),
959
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)),
960
- ])
952
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))])
961
953
  } catch (e) {
962
954
  console.warn('Warning during final cleanup:', e.message)
963
955
  // Force cleanup on timeout
@@ -970,10 +962,7 @@ class Playwright extends Helper {
970
962
  if (this.browser) {
971
963
  try {
972
964
  // Add timeout protection to prevent hanging
973
- await Promise.race([
974
- this._stopBrowser(),
975
- new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)),
976
- ])
965
+ await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))])
977
966
  } catch (e) {
978
967
  console.warn('Warning during forced cleanup:', e.message)
979
968
  // Force cleanup on timeout
@@ -1390,7 +1379,7 @@ class Playwright extends Helper {
1390
1379
  this.context = null
1391
1380
  this.frame = null
1392
1381
  popupStore.clear()
1393
-
1382
+
1394
1383
  // Remove all event listeners to prevent hanging
1395
1384
  if (this.browser) {
1396
1385
  try {
@@ -1399,7 +1388,7 @@ class Playwright extends Helper {
1399
1388
  // Ignore errors if browser is already closed
1400
1389
  }
1401
1390
  }
1402
-
1391
+
1403
1392
  if (this.options.recordHar && this.browserContext) {
1404
1393
  try {
1405
1394
  await this.browserContext.close()
@@ -1408,16 +1397,11 @@ class Playwright extends Helper {
1408
1397
  }
1409
1398
  }
1410
1399
  this.browserContext = null
1411
-
1400
+
1412
1401
  if (this.browser) {
1413
1402
  try {
1414
1403
  // Add timeout to prevent browser.close() from hanging indefinitely
1415
- await Promise.race([
1416
- this.browser.close(),
1417
- new Promise((_, reject) =>
1418
- setTimeout(() => reject(new Error('Browser close timeout')), 5000)
1419
- )
1420
- ])
1404
+ await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))])
1421
1405
  } catch (e) {
1422
1406
  // Ignore errors if browser is already closed or timeout
1423
1407
  if (!e.message?.includes('Browser close timeout')) {
@@ -1539,7 +1523,7 @@ class Playwright extends Helper {
1539
1523
  acceptDownloads: true,
1540
1524
  ...this.options.emulate,
1541
1525
  }
1542
-
1526
+
1543
1527
  try {
1544
1528
  this.browserContext = await this.browser.newContext(contextOptions)
1545
1529
  } catch (err) {
@@ -3183,14 +3167,14 @@ class Playwright extends Helper {
3183
3167
  this.debugSection('Response', await response.text())
3184
3168
 
3185
3169
  // hook to allow JSON response handle this
3186
- if (this.config.onResponse) {
3170
+ if (this.options.onResponse) {
3187
3171
  const axiosResponse = {
3188
3172
  data: await response.json(),
3189
3173
  status: response.status(),
3190
3174
  statusText: response.statusText(),
3191
3175
  headers: response.headers(),
3192
3176
  }
3193
- this.config.onResponse(axiosResponse)
3177
+ this.options.onResponse(axiosResponse)
3194
3178
  }
3195
3179
 
3196
3180
  return response
@@ -4337,11 +4321,11 @@ function isRoleLocatorObject(locator) {
4337
4321
  */
4338
4322
  async function handleRoleLocator(context, locator) {
4339
4323
  if (!isRoleLocatorObject(locator)) return null
4340
-
4324
+
4341
4325
  const options = {}
4342
4326
  if (locator.text) options.name = locator.text
4343
4327
  if (locator.exact !== undefined) options.exact = locator.exact
4344
-
4328
+
4345
4329
  return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
4346
4330
  }
4347
4331
 
@@ -4350,7 +4334,7 @@ async function findElements(matcher, locator) {
4350
4334
  const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
4351
4335
  const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
4352
4336
  const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
4353
-
4337
+
4354
4338
  if (isReactLocator) return findReact(matcher, locator)
4355
4339
  if (isVueLocator) return findVue(matcher, locator)
4356
4340
  if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
@@ -4391,7 +4375,7 @@ async function findCustomElements(matcher, locator) {
4391
4375
  // Always prioritize this.customLocatorStrategies which is set in constructor from config
4392
4376
  // and persists in every worker thread instance
4393
4377
  let strategyFunction = null
4394
-
4378
+
4395
4379
  if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) {
4396
4380
  strategyFunction = this.customLocatorStrategies[locator.type]
4397
4381
  } else if (globalCustomLocatorStrategies.has(locator.type)) {
@@ -4967,7 +4951,7 @@ async function refreshContextSession() {
4967
4951
  this.debugSection('Session', 'Skipping storage cleanup - no active page/context')
4968
4952
  return
4969
4953
  }
4970
-
4954
+
4971
4955
  const currentUrl = await this.grabCurrentUrl()
4972
4956
 
4973
4957
  if (currentUrl.startsWith('http')) {
@@ -94,7 +94,9 @@ const config = {}
94
94
  class REST extends Helper {
95
95
  constructor(config) {
96
96
  super(config)
97
- this.options = {
97
+
98
+ // Set defaults first
99
+ const defaults = {
98
100
  timeout: 10000,
99
101
  defaultHeaders: {},
100
102
  endpoint: '',
@@ -103,15 +105,16 @@ class REST extends Helper {
103
105
  onResponse: null,
104
106
  }
105
107
 
108
+ // Merge config with defaults
109
+ this._setConfig(config)
110
+ this.options = { ...defaults, ...this.options }
111
+
106
112
  if (this.options.maxContentLength) {
107
113
  const maxContentLength = this.options.maxUploadFileSize * 1024 * 1024
108
114
  this.options.maxContentLength = maxContentLength
109
115
  this.options.maxBodyLength = maxContentLength
110
116
  }
111
117
 
112
- // override defaults with config
113
- this._setConfig(config)
114
-
115
118
  this.headers = { ...this.options.defaultHeaders }
116
119
 
117
120
  // Create an agent with SSL certificate
@@ -215,8 +218,9 @@ class REST extends Helper {
215
218
  }
216
219
  }
217
220
 
218
- if (this.config.onRequest) {
219
- await this.config.onRequest(request)
221
+ const onRequest = this.options.onRequest || this.config.onRequest
222
+ if (onRequest) {
223
+ await onRequest(request)
220
224
  }
221
225
 
222
226
  try {
@@ -245,8 +249,9 @@ class REST extends Helper {
245
249
  }
246
250
  response = err.response
247
251
  }
248
- if (this.config.onResponse) {
249
- await this.config.onResponse(response)
252
+ const onResponse = this.options.onResponse || this.config.onResponse
253
+ if (onResponse) {
254
+ await onResponse(response)
250
255
  }
251
256
  try {
252
257
  this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data))
@@ -468,7 +473,8 @@ class REST extends Helper {
468
473
  export { REST as default }
469
474
 
470
475
  function curlize(request) {
471
- if (request.data?.constructor.name.toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'
476
+ // Guard access to nested properties safely in case request.data is undefined
477
+ if ((request.data?.constructor?.name || '').toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'
472
478
  let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", '')
473
479
 
474
480
  if (request.headers) {
@@ -12,15 +12,23 @@ export default function () {
12
12
  return
13
13
  }
14
14
  global.__codeceptConfigListenerInitialized = true
15
-
16
- const helpers = global.container.helpers()
17
15
 
18
16
  enableDynamicConfigFor('suite')
19
17
  enableDynamicConfigFor('test')
20
18
 
21
19
  function enableDynamicConfigFor(type) {
22
20
  event.dispatcher.on(event[type].before, (context = {}) => {
21
+ // Get helpers dynamically at runtime, not at initialization time
22
+ // This ensures we get the actual helper instances, not placeholders
23
+ const helpers = global.container.helpers()
24
+
23
25
  function updateHelperConfig(helper, config) {
26
+ // Guard against undefined or invalid helpers
27
+ if (!helper || !helper.constructor) {
28
+ output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`)
29
+ return
30
+ }
31
+
24
32
  const oldConfig = deepClone(helper.options)
25
33
  try {
26
34
  helper._setConfig(deepMerge(deepClone(oldConfig), config))
@@ -41,7 +49,7 @@ export default function () {
41
49
  for (let name in context.config) {
42
50
  const config = context.config[name]
43
51
  if (name === '0') {
44
- // first helper
52
+ // first helper - get dynamically
45
53
  name = Object.keys(helpers)[0]
46
54
  }
47
55
  const helper = helpers[name]
package/lib/step/meta.js CHANGED
@@ -58,17 +58,24 @@ class MetaStep extends Step {
58
58
  this.status = 'queued'
59
59
  this.setArguments(Array.from(arguments).slice(1))
60
60
  let result
61
+ let hasChildSteps = false
61
62
 
62
63
  const registerStep = step => {
63
64
  this.setMetaStep(null)
64
65
  step.setMetaStep(this)
66
+ hasChildSteps = true
65
67
  }
66
68
  event.dispatcher.prependListener(event.step.before, registerStep)
69
+
70
+ // Start timing
71
+ this.startTime = Date.now()
72
+
67
73
  // Handle async and sync methods.
68
74
  if (fn.constructor.name === 'AsyncFunction') {
69
75
  result = fn
70
76
  .apply(this.context, this.args)
71
77
  .then(result => {
78
+ this.setStatus('success')
72
79
  return result
73
80
  })
74
81
  .catch(error => {
@@ -78,17 +85,27 @@ class MetaStep extends Step {
78
85
  .finally(() => {
79
86
  this.endTime = Date.now()
80
87
  event.dispatcher.removeListener(event.step.before, registerStep)
88
+ // Only emit events if no child steps were registered
89
+ if (!hasChildSteps) {
90
+ event.emit(event.step.started, this)
91
+ event.emit(event.step.finished, this)
92
+ }
81
93
  })
82
94
  } else {
83
95
  try {
84
- this.startTime = Date.now()
85
96
  result = fn.apply(this.context, this.args)
97
+ this.setStatus('success')
86
98
  } catch (error) {
87
99
  this.setStatus('failed')
88
100
  throw error
89
101
  } finally {
90
102
  this.endTime = Date.now()
91
103
  event.dispatcher.removeListener(event.step.before, registerStep)
104
+ // Only emit events if no child steps were registered
105
+ if (!hasChildSteps) {
106
+ event.emit(event.step.started, this)
107
+ event.emit(event.step.finished, this)
108
+ }
92
109
  }
93
110
  }
94
111
 
package/lib/workers.js CHANGED
@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
5
5
  import { Worker } from 'worker_threads'
6
6
  import { EventEmitter } from 'events'
7
7
  import ms from 'ms'
8
+ import merge from 'lodash.merge'
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url)
10
11
  const __dirname = dirname(__filename)
@@ -66,21 +67,21 @@ const createWorker = (workerObject, isPoolMode = false) => {
66
67
  stdout: true,
67
68
  stderr: true,
68
69
  })
69
-
70
+
70
71
  // Pipe worker stdout/stderr to main process
71
72
  if (worker.stdout) {
72
73
  worker.stdout.setEncoding('utf8')
73
- worker.stdout.on('data', (data) => {
74
+ worker.stdout.on('data', data => {
74
75
  process.stdout.write(data)
75
76
  })
76
77
  }
77
78
  if (worker.stderr) {
78
79
  worker.stderr.setEncoding('utf8')
79
- worker.stderr.on('data', (data) => {
80
+ worker.stderr.on('data', data => {
80
81
  process.stderr.write(data)
81
82
  })
82
83
  }
83
-
84
+
84
85
  worker.on('error', err => {
85
86
  console.error(`[Main] Worker Error:`, err)
86
87
  output.error(`Worker Error: ${err.stack}`)
@@ -221,13 +222,13 @@ class WorkerObject {
221
222
 
222
223
  addConfig(config) {
223
224
  const oldConfig = JSON.parse(this.options.override || '{}')
224
-
225
+
225
226
  // Remove customLocatorStrategies from both old and new config before JSON serialization
226
227
  // since functions cannot be serialized and will be lost, causing workers to have empty strategies
227
228
  const configWithoutFunctions = { ...config }
228
-
229
+
229
230
  // Clean both old and new config
230
- const cleanConfig = (cfg) => {
231
+ const cleanConfig = cfg => {
231
232
  if (cfg.helpers) {
232
233
  cfg.helpers = { ...cfg.helpers }
233
234
  Object.keys(cfg.helpers).forEach(helperName => {
@@ -239,14 +240,12 @@ class WorkerObject {
239
240
  }
240
241
  return cfg
241
242
  }
242
-
243
+
243
244
  const cleanedOldConfig = cleanConfig(oldConfig)
244
245
  const cleanedNewConfig = cleanConfig(configWithoutFunctions)
245
-
246
- const newConfig = {
247
- ...cleanedOldConfig,
248
- ...cleanedNewConfig,
249
- }
246
+
247
+ // Deep merge configurations to preserve all helpers from base config
248
+ const newConfig = merge({}, cleanedOldConfig, cleanedNewConfig)
250
249
  this.options.override = JSON.stringify(newConfig)
251
250
  }
252
251
 
@@ -280,8 +279,8 @@ class Workers extends EventEmitter {
280
279
  this.setMaxListeners(50)
281
280
  this.codeceptPromise = initializeCodecept(config.testConfig, config.options)
282
281
  this.codecept = null
283
- this.config = config // Save config
284
- this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
282
+ this.config = config // Save config
283
+ this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
285
284
  this.options = config.options || {}
286
285
  this.errors = []
287
286
  this.numberOfWorkers = 0
@@ -304,11 +303,8 @@ class Workers extends EventEmitter {
304
303
  // Initialize workers in these cases:
305
304
  // 1. Positive number requested AND no manual workers pre-spawned
306
305
  // 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned
307
- const shouldAutoInit = this.workers.length === 0 && (
308
- (Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) ||
309
- (this.numberOfWorkersRequested < 0 && isFunction(this.config.by))
310
- )
311
-
306
+ const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by)))
307
+
312
308
  if (shouldAutoInit) {
313
309
  this._initWorkers(this.numberOfWorkersRequested, this.config)
314
310
  }
@@ -319,7 +315,7 @@ class Workers extends EventEmitter {
319
315
  this.splitTestsByGroups(numberOfWorkers, config)
320
316
  // For function-based grouping, use the actual number of test groups created
321
317
  const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
322
- this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
318
+ this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
323
319
  this.numberOfWorkers = this.workers.length
324
320
  }
325
321
 
@@ -371,9 +367,9 @@ class Workers extends EventEmitter {
371
367
  * @param {Number} numberOfWorkers
372
368
  */
373
369
  createGroupsOfTests(numberOfWorkers) {
374
- // If Codecept isn't initialized yet, return empty groups as a safe fallback
375
- if (!this.codecept) return populateGroups(numberOfWorkers)
376
- const files = this.codecept.testFiles
370
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
371
+ if (!this.codecept) return populateGroups(numberOfWorkers)
372
+ const files = this.codecept.testFiles
377
373
  const mocha = Container.mocha()
378
374
  mocha.files = files
379
375
  mocha.loadFiles()
@@ -430,7 +426,7 @@ class Workers extends EventEmitter {
430
426
  for (const file of files) {
431
427
  this.testPool.push(file)
432
428
  }
433
-
429
+
434
430
  this.testPoolInitialized = true
435
431
  }
436
432
 
@@ -443,7 +439,7 @@ class Workers extends EventEmitter {
443
439
  if (!this.testPoolInitialized) {
444
440
  this._initializeTestPool()
445
441
  }
446
-
442
+
447
443
  return this.testPool.shift()
448
444
  }
449
445
 
@@ -451,9 +447,9 @@ class Workers extends EventEmitter {
451
447
  * @param {Number} numberOfWorkers
452
448
  */
453
449
  createGroupsOfSuites(numberOfWorkers) {
454
- // If Codecept isn't initialized yet, return empty groups as a safe fallback
455
- if (!this.codecept) return populateGroups(numberOfWorkers)
456
- const files = this.codecept.testFiles
450
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
451
+ if (!this.codecept) return populateGroups(numberOfWorkers)
452
+ const files = this.codecept.testFiles
457
453
  const groups = populateGroups(numberOfWorkers)
458
454
 
459
455
  const mocha = Container.mocha()
@@ -494,7 +490,7 @@ class Workers extends EventEmitter {
494
490
  recorder.startUnlessRunning()
495
491
  event.dispatcher.emit(event.workers.before)
496
492
  process.env.RUNS_WITH_WORKERS = 'true'
497
-
493
+
498
494
  // Create workers and set up message handlers immediately (not in recorder queue)
499
495
  // This prevents a race condition where workers start sending messages before handlers are attached
500
496
  const workerThreads = []
@@ -503,11 +499,11 @@ class Workers extends EventEmitter {
503
499
  this._listenWorkerEvents(workerThread)
504
500
  workerThreads.push(workerThread)
505
501
  }
506
-
502
+
507
503
  recorder.add('workers started', () => {
508
504
  // Workers are already running, this is just a placeholder step
509
505
  })
510
-
506
+
511
507
  return new Promise(resolve => {
512
508
  this.on('end', resolve)
513
509
  })
@@ -591,7 +587,7 @@ class Workers extends EventEmitter {
591
587
  // Otherwise skip - we'll emit based on finished state
592
588
  break
593
589
  case event.test.passed:
594
- // Skip individual passed events - we'll emit based on finished state
590
+ // Skip individual passed events - we'll emit based on finished state
595
591
  break
596
592
  case event.test.skipped:
597
593
  this.emit(event.test.skipped, deserializeTest(message.data))
@@ -602,15 +598,15 @@ class Workers extends EventEmitter {
602
598
  const data = message.data
603
599
  const uid = data?.uid
604
600
  const isFailed = !!data?.err || data?.state === 'failed'
605
-
601
+
606
602
  if (uid) {
607
603
  // Track states for each test UID
608
604
  if (!this._testStates) this._testStates = new Map()
609
-
605
+
610
606
  if (!this._testStates.has(uid)) {
611
607
  this._testStates.set(uid, { states: [], lastData: data })
612
608
  }
613
-
609
+
614
610
  const testState = this._testStates.get(uid)
615
611
  testState.states.push({ isFailed, data })
616
612
  testState.lastData = data
@@ -622,7 +618,7 @@ class Workers extends EventEmitter {
622
618
  this.emit(event.test.passed, deserializeTest(data))
623
619
  }
624
620
  }
625
-
621
+
626
622
  this.emit(event.test.finished, deserializeTest(data))
627
623
  }
628
624
  break
@@ -682,11 +678,10 @@ class Workers extends EventEmitter {
682
678
  // For tests with retries configured, emit all failures + final success
683
679
  // For tests without retries, emit only final state
684
680
  const lastState = states[states.length - 1]
685
-
681
+
686
682
  // Check if this test had retries by looking for failure followed by success
687
- const hasRetryPattern = states.length > 1 &&
688
- states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
689
-
683
+ const hasRetryPattern = states.length > 1 && states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
684
+
690
685
  if (hasRetryPattern) {
691
686
  // Emit all intermediate failures and final success for retries
692
687
  for (const state of states) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.1-beta.1",
3
+ "version": "4.0.1-beta.10",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [