codeceptjs 4.0.1-beta.5 → 4.0.1-beta.8
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 +12 -8
- package/lib/container.js +29 -16
- package/lib/helper/GraphQL.js +2 -0
- package/lib/helper/Playwright.js +1 -0
- package/lib/helper/REST.js +13 -8
- package/lib/workers.js +36 -41
- package/package.json +1 -1
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.
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
211
|
-
container.
|
|
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
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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:
|
|
553
|
+
value: value,
|
|
541
554
|
}
|
|
542
555
|
},
|
|
543
556
|
get(target, key) {
|
package/lib/helper/GraphQL.js
CHANGED
package/lib/helper/Playwright.js
CHANGED
|
@@ -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'
|
package/lib/helper/REST.js
CHANGED
|
@@ -94,7 +94,9 @@ const config = {}
|
|
|
94
94
|
class REST extends Helper {
|
|
95
95
|
constructor(config) {
|
|
96
96
|
super(config)
|
|
97
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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))
|
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',
|
|
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,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 =
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
284
|
-
this.numberOfWorkersRequested = numberOfWorkers
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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) {
|