codeceptjs 4.0.1-beta.6 → 4.0.1-beta.9

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,7 +112,7 @@ class Container {
110
112
  }
111
113
  }
112
114
 
113
- // Wait for all async helpers to finish loading and populate the actor with helper methods
115
+ // Wait for all async helpers to finish loading and populate the actor
114
116
  await asyncHelperPromise
115
117
 
116
118
  if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
@@ -207,8 +209,10 @@ class Container {
207
209
 
208
210
  // If new support objects are added, update the proxy support
209
211
  if (newContainer.support) {
210
- const newProxySupport = createSupportObjects(newContainer.support)
211
- 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)
212
216
  }
213
217
 
214
218
  debug('appended', JSON.stringify(newContainer).slice(0, 300))
@@ -224,6 +228,7 @@ class Container {
224
228
  static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
225
229
  container.helpers = newHelpers
226
230
  container.translation = await loadTranslation()
231
+ container.proxySupportConfig = newSupport
227
232
  container.proxySupport = createSupportObjects(newSupport)
228
233
  container.plugins = newPlugins
229
234
  container.sharedKeys = new Set() // Clear shared keys
@@ -350,16 +355,17 @@ async function createHelpers(config) {
350
355
  }
351
356
  }
352
357
 
353
- // Wait for all async helpers to be fully loaded
354
- await asyncHelperPromise
355
-
356
- // Call _init on all helpers after they're all loaded
357
- for (const name in helpers) {
358
- if (helpers[name]._init) {
359
- await helpers[name]._init()
360
- debug(`helper ${name} _init() called`)
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
+ }
361
367
  }
362
- }
368
+ })
363
369
 
364
370
  return helpers
365
371
  }
@@ -534,10 +540,17 @@ function createSupportObjects(config) {
534
540
  return [...new Set([...keys, ...container.sharedKeys])]
535
541
  },
536
542
  getOwnPropertyDescriptor(target, prop) {
543
+ // For destructuring to work, we need to return the actual value from the getter
544
+ let value
545
+ if (container.sharedKeys.has(prop) && prop in container.support) {
546
+ value = container.support[prop]
547
+ } else {
548
+ value = lazyLoad(prop)
549
+ }
537
550
  return {
538
551
  enumerable: true,
539
552
  configurable: true,
540
- value: target[prop],
553
+ value: value,
541
554
  }
542
555
  },
543
556
  get(target, key) {
@@ -218,8 +218,9 @@ class REST extends Helper {
218
218
  }
219
219
  }
220
220
 
221
- if (this.options.onRequest) {
222
- await this.options.onRequest(request)
221
+ const onRequest = this.options.onRequest || this.config.onRequest
222
+ if (onRequest) {
223
+ await onRequest(request)
223
224
  }
224
225
 
225
226
  try {
@@ -248,8 +249,9 @@ class REST extends Helper {
248
249
  }
249
250
  response = err.response
250
251
  }
251
- if (this.options.onResponse) {
252
- await this.options.onResponse(response)
252
+ const onResponse = this.options.onResponse || this.config.onResponse
253
+ if (onResponse) {
254
+ await onResponse(response)
253
255
  }
254
256
  try {
255
257
  this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data))
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.6",
3
+ "version": "4.0.1-beta.9",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [