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.
- package/README.md +39 -27
- package/bin/codecept.js +2 -2
- package/bin/mcp-server.js +610 -0
- package/docs/webapi/appendField.mustache +5 -0
- package/docs/webapi/attachFile.mustache +12 -0
- package/docs/webapi/checkOption.mustache +1 -1
- package/docs/webapi/clearField.mustache +5 -0
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/dontSeeElement.mustache +4 -0
- package/docs/webapi/dontSeeInField.mustache +5 -0
- package/docs/webapi/fillField.mustache +5 -0
- package/docs/webapi/moveCursorTo.mustache +5 -1
- package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
- package/docs/webapi/seeElement.mustache +4 -0
- package/docs/webapi/seeInField.mustache +5 -0
- package/docs/webapi/selectOption.mustache +5 -0
- package/docs/webapi/uncheckOption.mustache +1 -1
- package/lib/actor.js +12 -8
- package/lib/codecept.js +51 -18
- package/lib/command/definitions.js +14 -7
- package/lib/command/init.js +2 -4
- package/lib/command/run-workers.js +13 -2
- package/lib/command/workers/runTests.js +121 -9
- package/lib/config.js +24 -33
- package/lib/container.js +177 -28
- package/lib/element/WebElement.js +81 -2
- package/lib/els.js +12 -6
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/GraphQL.js +6 -4
- package/lib/helper/JSONResponse.js +3 -4
- package/lib/helper/Playwright.js +339 -505
- package/lib/helper/Puppeteer.js +324 -89
- package/lib/helper/REST.js +15 -9
- package/lib/helper/WebDriver.js +311 -81
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/html.js +14 -1
- package/lib/listener/config.js +11 -3
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +2 -14
- package/lib/locator.js +32 -0
- package/lib/mocha/cli.js +16 -0
- package/lib/mocha/factory.js +7 -27
- package/lib/mocha/gherkin.js +4 -4
- package/lib/mocha/test.js +4 -2
- package/lib/output.js +2 -2
- package/lib/plugin/aiTrace.js +464 -0
- package/lib/plugin/auth.js +2 -1
- package/lib/plugin/retryFailedStep.js +28 -19
- package/lib/plugin/stepByStepReport.js +5 -1
- package/lib/step/base.js +14 -1
- package/lib/step/config.js +15 -2
- package/lib/step/meta.js +18 -1
- package/lib/step/record.js +9 -1
- package/lib/utils/loaderCheck.js +162 -0
- package/lib/utils/typescript.js +449 -0
- package/lib/utils.js +48 -0
- package/lib/workers.js +163 -54
- package/package.json +43 -32
- package/typings/index.d.ts +120 -4
- package/lib/helper/extras/PlaywrightLocator.js +0 -110
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -11011
- 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',
|
|
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',
|
|
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 =
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
284
|
-
this.numberOfWorkersRequested = numberOfWorkers
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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": "
|
|
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": "
|
|
92
|
-
"@cucumber/messages": "
|
|
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": "^
|
|
97
|
+
"ai": "^6.0.43",
|
|
96
98
|
"arrify": "3.0.0",
|
|
97
|
-
"axios": "1.
|
|
99
|
+
"axios": "1.13.2",
|
|
98
100
|
"chalk": "4.1.2",
|
|
99
101
|
"cheerio": "^1.0.0",
|
|
100
|
-
"chokidar": "^
|
|
101
|
-
"commander": "
|
|
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.
|
|
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.
|
|
111
|
+
"fs-extra": "11.3.3",
|
|
110
112
|
"fuse.js": "^7.0.0",
|
|
111
|
-
"glob": ">=9.0.0 <
|
|
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.
|
|
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.
|
|
141
|
-
"@eslint/js": "9.
|
|
142
|
-
"@faker-js/faker": "
|
|
143
|
-
"@inquirer/testing": "^
|
|
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.
|
|
149
|
+
"@types/chai": "5.2.3",
|
|
148
150
|
"@types/inquirer": "9.0.9",
|
|
149
|
-
"@types/node": "^
|
|
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.
|
|
154
|
+
"@wdio/utils": "9.23.3",
|
|
153
155
|
"@xmldom/xmldom": "0.9.8",
|
|
154
156
|
"bunosh": "latest",
|
|
155
|
-
"chai": "^
|
|
156
|
-
"chai-as-promised": "
|
|
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": "
|
|
161
|
+
"electron": "40.1.0",
|
|
160
162
|
"eslint": "^9.36.0",
|
|
161
163
|
"eslint-plugin-import": "2.32.0",
|
|
162
|
-
"eslint-plugin-mocha": "11.
|
|
164
|
+
"eslint-plugin-mocha": "11.2.0",
|
|
163
165
|
"expect": "30.2.0",
|
|
164
166
|
"express": "^5.1.0",
|
|
165
|
-
"globals": "
|
|
166
|
-
"graphql": "16.
|
|
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.
|
|
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.
|
|
181
|
-
"sinon-chai": "
|
|
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
|
-
"
|
|
188
|
+
"tsx": "^4.19.2",
|
|
189
|
+
"typedoc": "0.28.16",
|
|
187
190
|
"typedoc-plugin-markdown": "4.9.0",
|
|
188
|
-
"typescript": "5.
|
|
191
|
+
"typescript": "5.9.3",
|
|
189
192
|
"wdio-docker-service": "3.2.1",
|
|
190
|
-
"webdriverio": "9.
|
|
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"
|