codeceptjs 4.0.0-beta.9.esm-aria → 4.0.0-rc.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 (69) hide show
  1. package/README.md +39 -27
  2. package/bin/codecept.js +2 -2
  3. package/bin/mcp-server.js +610 -0
  4. package/docs/webapi/appendField.mustache +5 -0
  5. package/docs/webapi/attachFile.mustache +12 -0
  6. package/docs/webapi/checkOption.mustache +1 -1
  7. package/docs/webapi/clearField.mustache +5 -0
  8. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  9. package/docs/webapi/dontSeeElement.mustache +4 -0
  10. package/docs/webapi/dontSeeInField.mustache +5 -0
  11. package/docs/webapi/fillField.mustache +5 -0
  12. package/docs/webapi/moveCursorTo.mustache +5 -1
  13. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  14. package/docs/webapi/seeElement.mustache +4 -0
  15. package/docs/webapi/seeInField.mustache +5 -0
  16. package/docs/webapi/selectOption.mustache +5 -0
  17. package/docs/webapi/uncheckOption.mustache +1 -1
  18. package/lib/actor.js +12 -8
  19. package/lib/codecept.js +51 -18
  20. package/lib/command/definitions.js +14 -7
  21. package/lib/command/init.js +2 -4
  22. package/lib/command/run-workers.js +13 -2
  23. package/lib/command/workers/runTests.js +121 -9
  24. package/lib/config.js +24 -33
  25. package/lib/container.js +177 -28
  26. package/lib/element/WebElement.js +81 -2
  27. package/lib/els.js +12 -6
  28. package/lib/helper/Appium.js +8 -8
  29. package/lib/helper/GraphQL.js +6 -4
  30. package/lib/helper/JSONResponse.js +3 -4
  31. package/lib/helper/Playwright.js +339 -505
  32. package/lib/helper/Puppeteer.js +324 -89
  33. package/lib/helper/REST.js +15 -9
  34. package/lib/helper/WebDriver.js +311 -81
  35. package/lib/helper/errors/ElementNotFound.js +5 -2
  36. package/lib/helper/errors/MultipleElementsFound.js +52 -0
  37. package/lib/helper/extras/elementSelection.js +58 -0
  38. package/lib/helper/scripts/dropFile.js +11 -0
  39. package/lib/html.js +14 -1
  40. package/lib/listener/config.js +11 -3
  41. package/lib/listener/globalRetry.js +32 -6
  42. package/lib/listener/helpers.js +2 -14
  43. package/lib/locator.js +32 -0
  44. package/lib/mocha/cli.js +16 -0
  45. package/lib/mocha/factory.js +7 -27
  46. package/lib/mocha/gherkin.js +4 -4
  47. package/lib/mocha/test.js +4 -2
  48. package/lib/output.js +2 -2
  49. package/lib/plugin/aiTrace.js +464 -0
  50. package/lib/plugin/auth.js +2 -1
  51. package/lib/plugin/retryFailedStep.js +28 -19
  52. package/lib/plugin/stepByStepReport.js +5 -1
  53. package/lib/step/base.js +14 -1
  54. package/lib/step/config.js +15 -2
  55. package/lib/step/meta.js +18 -1
  56. package/lib/step/record.js +9 -1
  57. package/lib/utils/loaderCheck.js +162 -0
  58. package/lib/utils/typescript.js +449 -0
  59. package/lib/utils.js +48 -0
  60. package/lib/workers.js +163 -54
  61. package/package.json +43 -32
  62. package/typings/index.d.ts +120 -4
  63. package/lib/helper/extras/PlaywrightLocator.js +0 -110
  64. package/lib/listener/enhancedGlobalRetry.js +0 -110
  65. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  66. package/lib/plugin/htmlReporter.js +0 -3648
  67. package/lib/retryCoordinator.js +0 -207
  68. package/typings/promiseBasedTypes.d.ts +0 -11011
  69. package/typings/types.d.ts +0 -13073
package/lib/utils.js CHANGED
@@ -150,6 +150,24 @@ export const decodeUrl = function (url) {
150
150
  return decodeURIComponent(decodeURIComponent(decodeURIComponent(url)))
151
151
  }
152
152
 
153
+ export const normalizePath = function (path) {
154
+ if (path === '' || path === '/') return '/'
155
+ return path
156
+ .replace(/\/+/g, '/')
157
+ .replace(/\/$/, '') || '/'
158
+ }
159
+
160
+ export const resolveUrl = function (url, baseUrl) {
161
+ if (!url) return url
162
+ if (url.indexOf('http') === 0) return url
163
+ if (!baseUrl) return url
164
+ try {
165
+ return new URL(url, baseUrl).href
166
+ } catch (e) {
167
+ return url
168
+ }
169
+ }
170
+
153
171
  export const xpathLocator = {
154
172
  /**
155
173
  * @param {string} string
@@ -640,6 +658,36 @@ export const base64EncodeFile = function (filePath) {
640
658
  return Buffer.from(fs.readFileSync(filePath)).toString('base64')
641
659
  }
642
660
 
661
+ export const getMimeType = function (fileName) {
662
+ const ext = path.extname(fileName).toLowerCase()
663
+ const mimeTypes = {
664
+ '.jpg': 'image/jpeg',
665
+ '.jpeg': 'image/jpeg',
666
+ '.png': 'image/png',
667
+ '.gif': 'image/gif',
668
+ '.bmp': 'image/bmp',
669
+ '.svg': 'image/svg+xml',
670
+ '.webp': 'image/webp',
671
+ '.pdf': 'application/pdf',
672
+ '.txt': 'text/plain',
673
+ '.html': 'text/html',
674
+ '.css': 'text/css',
675
+ '.js': 'application/javascript',
676
+ '.json': 'application/json',
677
+ '.xml': 'application/xml',
678
+ '.zip': 'application/zip',
679
+ '.csv': 'text/csv',
680
+ '.doc': 'application/msword',
681
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
682
+ '.xls': 'application/vnd.ms-excel',
683
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
684
+ '.mp3': 'audio/mpeg',
685
+ '.mp4': 'video/mp4',
686
+ '.wav': 'audio/wav',
687
+ }
688
+ return mimeTypes[ext] || 'application/octet-stream'
689
+ }
690
+
643
691
  export const markdownToAnsi = function (markdown) {
644
692
  return (
645
693
  markdown
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)
@@ -27,7 +28,7 @@ const pathToWorker = path.join(__dirname, 'command', 'workers', 'runTests.js')
27
28
 
28
29
  const initializeCodecept = async (configPath, options = {}) => {
29
30
  const config = await mainConfig.load(configPath || '.')
30
- const codecept = new Codecept(config, options)
31
+ const codecept = new Codecept(config, { ...options, skipDefaultListeners: true })
31
32
  await codecept.init(getTestRoot(configPath))
32
33
  codecept.loadTests()
33
34
 
@@ -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,14 @@ 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
- // since functions cannot be serialized and will be lost, causing workers to have empty strategies
227
+ // since functions cannot be serialized and will be lost, causing workers to have empty strategies.
228
+ // Note: Only WebDriver helper supports customLocatorStrategies
227
229
  const configWithoutFunctions = { ...config }
228
-
230
+
229
231
  // Clean both old and new config
230
- const cleanConfig = (cfg) => {
232
+ const cleanConfig = cfg => {
231
233
  if (cfg.helpers) {
232
234
  cfg.helpers = { ...cfg.helpers }
233
235
  Object.keys(cfg.helpers).forEach(helperName => {
@@ -239,14 +241,12 @@ class WorkerObject {
239
241
  }
240
242
  return cfg
241
243
  }
242
-
244
+
243
245
  const cleanedOldConfig = cleanConfig(oldConfig)
244
246
  const cleanedNewConfig = cleanConfig(configWithoutFunctions)
245
-
246
- const newConfig = {
247
- ...cleanedOldConfig,
248
- ...cleanedNewConfig,
249
- }
247
+
248
+ // Deep merge configurations to preserve all helpers from base config
249
+ const newConfig = merge({}, cleanedOldConfig, cleanedNewConfig)
250
250
  this.options.override = JSON.stringify(newConfig)
251
251
  }
252
252
 
@@ -280,8 +280,8 @@ class Workers extends EventEmitter {
280
280
  this.setMaxListeners(50)
281
281
  this.codeceptPromise = initializeCodecept(config.testConfig, config.options)
282
282
  this.codecept = null
283
- this.config = config // Save config
284
- this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
283
+ this.config = config // Save config
284
+ this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count
285
285
  this.options = config.options || {}
286
286
  this.errors = []
287
287
  this.numberOfWorkers = 0
@@ -304,11 +304,8 @@ class Workers extends EventEmitter {
304
304
  // Initialize workers in these cases:
305
305
  // 1. Positive number requested AND no manual workers pre-spawned
306
306
  // 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
-
307
+ const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by)))
308
+
312
309
  if (shouldAutoInit) {
313
310
  this._initWorkers(this.numberOfWorkersRequested, this.config)
314
311
  }
@@ -319,7 +316,7 @@ class Workers extends EventEmitter {
319
316
  this.splitTestsByGroups(numberOfWorkers, config)
320
317
  // For function-based grouping, use the actual number of test groups created
321
318
  const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers
322
- this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns)
319
+ this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns)
323
320
  this.numberOfWorkers = this.workers.length
324
321
  }
325
322
 
@@ -371,9 +368,12 @@ class Workers extends EventEmitter {
371
368
  * @param {Number} numberOfWorkers
372
369
  */
373
370
  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
371
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
372
+ if (!this.codecept) return populateGroups(numberOfWorkers)
373
+ const files = this.codecept.testFiles
374
+
375
+ // Create a fresh mocha instance to avoid state pollution
376
+ Container.createMocha(this.codecept.config.mocha || {}, this.options)
377
377
  const mocha = Container.mocha()
378
378
  mocha.files = files
379
379
  mocha.loadFiles()
@@ -388,6 +388,10 @@ class Workers extends EventEmitter {
388
388
  groupCounter++
389
389
  }
390
390
  })
391
+
392
+ // Clean up after collecting test UIDs
393
+ mocha.unloadFiles()
394
+
391
395
  return groups
392
396
  }
393
397
 
@@ -430,7 +434,7 @@ class Workers extends EventEmitter {
430
434
  for (const file of files) {
431
435
  this.testPool.push(file)
432
436
  }
433
-
437
+
434
438
  this.testPoolInitialized = true
435
439
  }
436
440
 
@@ -443,7 +447,7 @@ class Workers extends EventEmitter {
443
447
  if (!this.testPoolInitialized) {
444
448
  this._initializeTestPool()
445
449
  }
446
-
450
+
447
451
  return this.testPool.shift()
448
452
  }
449
453
 
@@ -451,14 +455,17 @@ class Workers extends EventEmitter {
451
455
  * @param {Number} numberOfWorkers
452
456
  */
453
457
  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
458
+ // If Codecept isn't initialized yet, return empty groups as a safe fallback
459
+ if (!this.codecept) return populateGroups(numberOfWorkers)
460
+ const files = this.codecept.testFiles
457
461
  const groups = populateGroups(numberOfWorkers)
458
462
 
463
+ // Create a fresh mocha instance to avoid state pollution
464
+ Container.createMocha(this.codecept.config.mocha || {}, this.options)
459
465
  const mocha = Container.mocha()
460
466
  mocha.files = files
461
467
  mocha.loadFiles()
468
+
462
469
  mocha.suite.suites.forEach(suite => {
463
470
  const i = indexOfSmallestElement(groups)
464
471
  suite.tests.forEach(test => {
@@ -467,6 +474,10 @@ class Workers extends EventEmitter {
467
474
  }
468
475
  })
469
476
  })
477
+
478
+ // Clean up after collecting test UIDs
479
+ mocha.unloadFiles()
480
+
470
481
  return groups
471
482
  }
472
483
 
@@ -494,7 +505,7 @@ class Workers extends EventEmitter {
494
505
  recorder.startUnlessRunning()
495
506
  event.dispatcher.emit(event.workers.before)
496
507
  process.env.RUNS_WITH_WORKERS = 'true'
497
-
508
+
498
509
  // Create workers and set up message handlers immediately (not in recorder queue)
499
510
  // This prevents a race condition where workers start sending messages before handlers are attached
500
511
  const workerThreads = []
@@ -503,13 +514,29 @@ class Workers extends EventEmitter {
503
514
  this._listenWorkerEvents(workerThread)
504
515
  workerThreads.push(workerThread)
505
516
  }
506
-
517
+
507
518
  recorder.add('workers started', () => {
508
519
  // Workers are already running, this is just a placeholder step
509
520
  })
510
-
521
+
522
+ // Add overall timeout to prevent infinite hanging
523
+ const overallTimeout = setTimeout(() => {
524
+ console.error('[Main] Overall timeout reached (10 minutes). Force terminating remaining workers...')
525
+ workerThreads.forEach(w => {
526
+ try {
527
+ w.terminate()
528
+ } catch (e) {
529
+ // ignore
530
+ }
531
+ })
532
+ this._finishRun()
533
+ }, 600000) // 10 minutes
534
+
511
535
  return new Promise(resolve => {
512
- this.on('end', resolve)
536
+ this.on('end', () => {
537
+ clearTimeout(overallTimeout)
538
+ resolve()
539
+ })
513
540
  })
514
541
  }
515
542
 
@@ -532,8 +559,32 @@ class Workers extends EventEmitter {
532
559
  if (this.isPoolMode) {
533
560
  this.activeWorkers.set(worker, { available: true, workerIndex: null })
534
561
  }
562
+
563
+ // Track last activity time to detect hanging workers
564
+ let lastActivity = Date.now()
565
+ let currentTest = null
566
+ const workerTimeout = 300000 // 5 minutes
567
+
568
+ const timeoutChecker = setInterval(() => {
569
+ const elapsed = Date.now() - lastActivity
570
+ if (elapsed > workerTimeout) {
571
+ console.error(`[Main] Worker appears to be hanging (no activity for ${Math.floor(elapsed/1000)}s). Terminating...`)
572
+ if (currentTest) {
573
+ console.error(`[Main] Last test: ${currentTest}`)
574
+ }
575
+ clearInterval(timeoutChecker)
576
+ worker.terminate()
577
+ }
578
+ }, 30000) // Check every 30 seconds
535
579
 
536
580
  worker.on('message', message => {
581
+ lastActivity = Date.now() // Update activity timestamp
582
+
583
+ // Track current test
584
+ if (message.event === event.test.started && message.data) {
585
+ currentTest = message.data.title || message.data.fullTitle
586
+ }
587
+
537
588
  output.process(message.workerIndex)
538
589
 
539
590
  // Handle test requests for pool mode
@@ -574,13 +625,32 @@ class Workers extends EventEmitter {
574
625
 
575
626
  break
576
627
  case event.suite.before:
577
- this.emit(event.suite.before, deserializeSuite(message.data))
628
+ {
629
+ const suite = deserializeSuite(message.data)
630
+ this.emit(event.suite.before, suite)
631
+ event.dispatcher.emit(event.suite.before, suite)
632
+ }
633
+ break
634
+ case event.suite.after:
635
+ {
636
+ const suite = deserializeSuite(message.data)
637
+ this.emit(event.suite.after, suite)
638
+ event.dispatcher.emit(event.suite.after, suite)
639
+ }
578
640
  break
579
641
  case event.test.before:
580
- this.emit(event.test.before, deserializeTest(message.data))
642
+ {
643
+ const test = deserializeTest(message.data)
644
+ this.emit(event.test.before, test)
645
+ event.dispatcher.emit(event.test.before, test)
646
+ }
581
647
  break
582
648
  case event.test.started:
583
- this.emit(event.test.started, deserializeTest(message.data))
649
+ {
650
+ const test = deserializeTest(message.data)
651
+ this.emit(event.test.started, test)
652
+ event.dispatcher.emit(event.test.started, test)
653
+ }
584
654
  break
585
655
  case event.test.failed:
586
656
  // For hook failures, emit immediately as there won't be a test.finished event
@@ -591,10 +661,14 @@ class Workers extends EventEmitter {
591
661
  // Otherwise skip - we'll emit based on finished state
592
662
  break
593
663
  case event.test.passed:
594
- // Skip individual passed events - we'll emit based on finished state
664
+ // Skip individual passed events - we'll emit based on finished state
595
665
  break
596
666
  case event.test.skipped:
597
- this.emit(event.test.skipped, deserializeTest(message.data))
667
+ {
668
+ const test = deserializeTest(message.data)
669
+ this.emit(event.test.skipped, test)
670
+ event.dispatcher.emit(event.test.skipped, test)
671
+ }
598
672
  break
599
673
  case event.test.finished:
600
674
  // Handle different types of test completion properly
@@ -602,15 +676,15 @@ class Workers extends EventEmitter {
602
676
  const data = message.data
603
677
  const uid = data?.uid
604
678
  const isFailed = !!data?.err || data?.state === 'failed'
605
-
679
+
606
680
  if (uid) {
607
681
  // Track states for each test UID
608
682
  if (!this._testStates) this._testStates = new Map()
609
-
683
+
610
684
  if (!this._testStates.has(uid)) {
611
- this._testStates.set(uid, { states: [], lastData: data })
685
+ this._testStates.set(uid, { states: [], lastData: data, workerIndex: message.workerIndex })
612
686
  }
613
-
687
+
614
688
  const testState = this._testStates.get(uid)
615
689
  testState.states.push({ isFailed, data })
616
690
  testState.lastData = data
@@ -622,39 +696,72 @@ class Workers extends EventEmitter {
622
696
  this.emit(event.test.passed, deserializeTest(data))
623
697
  }
624
698
  }
625
-
626
- this.emit(event.test.finished, deserializeTest(data))
699
+
700
+ const test = deserializeTest(data)
701
+ this.emit(event.test.finished, test)
702
+ event.dispatcher.emit(event.test.finished, test)
627
703
  }
628
704
  break
629
705
  case event.test.after:
630
- this.emit(event.test.after, deserializeTest(message.data))
706
+ {
707
+ const test = deserializeTest(message.data)
708
+ this.emit(event.test.after, test)
709
+ event.dispatcher.emit(event.test.after, test)
710
+ }
631
711
  break
632
712
  case event.step.finished:
633
713
  this.emit(event.step.finished, message.data)
714
+ event.dispatcher.emit(event.step.finished, message.data)
634
715
  break
635
716
  case event.step.started:
636
717
  this.emit(event.step.started, message.data)
718
+ event.dispatcher.emit(event.step.started, message.data)
637
719
  break
638
720
  case event.step.passed:
639
721
  this.emit(event.step.passed, message.data)
722
+ event.dispatcher.emit(event.step.passed, message.data)
640
723
  break
641
724
  case event.step.failed:
642
725
  this.emit(event.step.failed, message.data, message.data.error)
726
+ event.dispatcher.emit(event.step.failed, message.data, message.data.error)
643
727
  break
644
728
  case event.hook.failed:
645
729
  // Hook failures are already reported as test failures by the worker
646
730
  // Just emit the hook.failed event for listeners
647
731
  this.emit(event.hook.failed, message.data)
732
+ event.dispatcher.emit(event.hook.failed, message.data)
733
+ break
734
+ case event.hook.passed:
735
+ this.emit(event.hook.passed, message.data)
736
+ event.dispatcher.emit(event.hook.passed, message.data)
737
+ break
738
+ case event.hook.finished:
739
+ this.emit(event.hook.finished, message.data)
740
+ event.dispatcher.emit(event.hook.finished, message.data)
648
741
  break
649
742
  }
650
743
  })
651
744
 
652
745
  worker.on('error', err => {
746
+ console.error(`[Main] Worker error:`, err.message || err)
747
+ if (currentTest) {
748
+ console.error(`[Main] Failed during test: ${currentTest}`)
749
+ }
653
750
  this.errors.push(err)
654
751
  })
655
752
 
656
- worker.on('exit', () => {
753
+ worker.on('exit', (code) => {
754
+ clearInterval(timeoutChecker)
657
755
  this.closedWorkers += 1
756
+
757
+ if (code !== 0) {
758
+ console.error(`[Main] Worker exited with code ${code}`)
759
+ if (currentTest) {
760
+ console.error(`[Main] Last test running: ${currentTest}`)
761
+ }
762
+ // Mark as failed
763
+ process.exitCode = 1
764
+ }
658
765
 
659
766
  if (this.isPoolMode) {
660
767
  // Pool mode: finish when all workers have exited and no more tests
@@ -670,7 +777,7 @@ class Workers extends EventEmitter {
670
777
 
671
778
  _finishRun() {
672
779
  event.dispatcher.emit(event.workers.after, { tests: this.workers.map(worker => worker.tests) })
673
- if (Container.result().hasFailed) {
780
+ if (Container.result().hasFailed || this.errors.length > 0) {
674
781
  process.exitCode = 1
675
782
  } else {
676
783
  process.exitCode = 0
@@ -678,15 +785,17 @@ class Workers extends EventEmitter {
678
785
 
679
786
  // Emit states for all tracked tests before emitting results
680
787
  if (this._testStates) {
681
- for (const [uid, { states, lastData }] of this._testStates) {
788
+ for (const [uid, { states, lastData, workerIndex }] of this._testStates) {
789
+ // Set correct worker index for output
790
+ output.process(workerIndex)
791
+
682
792
  // For tests with retries configured, emit all failures + final success
683
793
  // For tests without retries, emit only final state
684
794
  const lastState = states[states.length - 1]
685
-
795
+
686
796
  // 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
-
797
+ const hasRetryPattern = states.length > 1 && states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed)
798
+
690
799
  if (hasRetryPattern) {
691
800
  // Emit all intermediate failures and final success for retries
692
801
  for (const state of states) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.0-beta.9.esm-aria",
3
+ "version": "4.0.0-rc.10",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [
@@ -44,9 +44,10 @@
44
44
  "./store": "./lib/store.js"
45
45
  },
46
46
  "bin": {
47
- "codeceptjs": "./bin/codecept.js"
47
+ "codeceptjs": "./bin/codecept.js",
48
+ "codeceptjs-mcp": "./bin/mcp-server.js"
48
49
  },
49
- "repository": "Codeception/codeceptjs",
50
+ "repository": "codeceptjs/CodeceptJS",
50
51
  "scripts": {
51
52
  "test-server": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010 --read-only",
52
53
  "test-server:writable": "node bin/test-server.js test/data/rest/db.json --host 0.0.0.0 -p 8010",
@@ -88,31 +89,32 @@
88
89
  "@codeceptjs/configure": "1.0.6",
89
90
  "@codeceptjs/helper": "2.0.4",
90
91
  "@cucumber/cucumber-expressions": "18",
91
- "@cucumber/gherkin": "35.1.0",
92
- "@cucumber/messages": "29.0.1",
92
+ "@cucumber/gherkin": "38.0.0",
93
+ "@cucumber/messages": "32.0.1",
94
+ "@modelcontextprotocol/sdk": "^1.26.0",
93
95
  "@xmldom/xmldom": "0.9.8",
94
96
  "acorn": "8.15.0",
95
- "ai": "^5.0.60",
97
+ "ai": "^6.0.43",
96
98
  "arrify": "3.0.0",
97
- "axios": "1.12.2",
99
+ "axios": "1.13.2",
98
100
  "chalk": "4.1.2",
99
101
  "cheerio": "^1.0.0",
100
- "chokidar": "^4.0.3",
101
- "commander": "11.1.0",
102
+ "chokidar": "^5.0.0",
103
+ "commander": "14.0.3",
102
104
  "cross-spawn": "7.0.6",
103
105
  "css-to-xpath": "0.1.0",
104
106
  "csstoxpath": "1.6.0",
105
- "envinfo": "7.20.0",
107
+ "envinfo": "7.21.0",
106
108
  "escape-string-regexp": "4.0.0",
107
109
  "figures": "3.2.0",
108
110
  "fn-args": "4.0.0",
109
- "fs-extra": "11.3.2",
111
+ "fs-extra": "11.3.3",
110
112
  "fuse.js": "^7.0.0",
111
- "glob": ">=9.0.0 <12",
113
+ "glob": ">=9.0.0 <14",
112
114
  "html-minifier-terser": "7.2.0",
113
115
  "inquirer": "^8.2.7",
114
116
  "invisi-data": "^1.0.0",
115
- "joi": "18.0.1",
117
+ "joi": "18.0.2",
116
118
  "js-beautify": "1.15.4",
117
119
  "lodash.clonedeep": "4.5.0",
118
120
  "lodash.merge": "4.6.2",
@@ -137,33 +139,33 @@
137
139
  "@apollo/server": "^5",
138
140
  "@codeceptjs/expect-helper": "^1.0.2",
139
141
  "@codeceptjs/mock-request": "0.3.1",
140
- "@eslint/eslintrc": "3.3.1",
141
- "@eslint/js": "9.36.0",
142
- "@faker-js/faker": "9.8.0",
143
- "@inquirer/testing": "^2.1.49",
142
+ "@eslint/eslintrc": "3.3.3",
143
+ "@eslint/js": "9.39.2",
144
+ "@faker-js/faker": "10.2.0",
145
+ "@inquirer/testing": "^3.0.3",
144
146
  "@pollyjs/adapter-puppeteer": "6.0.6",
145
147
  "@pollyjs/core": "6.0.6",
146
148
  "@testomatio/reporter": "^2.3.1",
147
- "@types/chai": "5.2.2",
149
+ "@types/chai": "5.2.3",
148
150
  "@types/inquirer": "9.0.9",
149
- "@types/node": "^24.9.2",
151
+ "@types/node": "^25.0.3",
150
152
  "@wdio/sauce-service": "9.12.5",
151
153
  "@wdio/selenium-standalone-service": "8.15.0",
152
- "@wdio/utils": "9.20.0",
154
+ "@wdio/utils": "9.23.3",
153
155
  "@xmldom/xmldom": "0.9.8",
154
156
  "bunosh": "latest",
155
- "chai": "^4.5.0",
156
- "chai-as-promised": "7.1.2",
157
+ "chai": "^6.2.1",
158
+ "chai-as-promised": "^8.0.2",
157
159
  "chai-subset": "1.6.0",
158
160
  "documentation": "14.0.3",
159
- "electron": "38.2.0",
161
+ "electron": "40.1.0",
160
162
  "eslint": "^9.36.0",
161
163
  "eslint-plugin-import": "2.32.0",
162
- "eslint-plugin-mocha": "11.1.0",
164
+ "eslint-plugin-mocha": "11.2.0",
163
165
  "expect": "30.2.0",
164
166
  "express": "^5.1.0",
165
- "globals": "16.4.0",
166
- "graphql": "16.11.0",
167
+ "globals": "17.3.0",
168
+ "graphql": "16.12.0",
167
169
  "graphql-tag": "^2.12.6",
168
170
  "husky": "9.1.7",
169
171
  "jsdoc": "^3.6.11",
@@ -172,25 +174,34 @@
172
174
  "mochawesome": "^7.1.3",
173
175
  "playwright": "1.55.1",
174
176
  "prettier": "^3.3.2",
175
- "puppeteer": "24.15.0",
177
+ "puppeteer": "24.36.0",
176
178
  "qrcode-terminal": "0.12.0",
177
179
  "rosie": "2.1.1",
178
180
  "runok": "^0.9.3",
179
181
  "semver": "7.7.3",
180
- "sinon": "21.0.0",
181
- "sinon-chai": "3.7.0",
182
+ "sinon": "21.0.1",
183
+ "sinon-chai": "^4.0.1",
182
184
  "ts-morph": "27.0.2",
183
185
  "ts-node": "10.9.2",
184
186
  "tsd": "^0.33.0",
185
187
  "tsd-jsdoc": "2.5.0",
186
- "typedoc": "0.28.13",
188
+ "tsx": "^4.19.2",
189
+ "typedoc": "0.28.16",
187
190
  "typedoc-plugin-markdown": "4.9.0",
188
- "typescript": "5.8.3",
191
+ "typescript": "5.9.3",
189
192
  "wdio-docker-service": "3.2.1",
190
- "webdriverio": "9.12.5",
193
+ "webdriverio": "9.23.0",
191
194
  "xml2js": "0.6.2",
192
195
  "xpath": "0.0.34"
193
196
  },
197
+ "peerDependencies": {
198
+ "tsx": "^4.0.0"
199
+ },
200
+ "peerDependenciesMeta": {
201
+ "tsx": {
202
+ "optional": true
203
+ }
204
+ },
194
205
  "engines": {
195
206
  "node": ">=16.0",
196
207
  "npm": ">=5.6.0"