codeceptjs 3.7.3 → 3.7.5-beta.1

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.
@@ -86,7 +86,7 @@ module.exports = function (config) {
86
86
  let fileName
87
87
 
88
88
  if (options.uniqueScreenshotNames && test) {
89
- fileName = `${testToFileName(test, _getUUID(test))}.failed.png`
89
+ fileName = `${testToFileName(test, { unique: true })}.failed.png`
90
90
  } else {
91
91
  fileName = `${testToFileName(test)}.failed.png`
92
92
  }
@@ -137,12 +137,4 @@ module.exports = function (config) {
137
137
  true,
138
138
  )
139
139
  })
140
-
141
- function _getUUID(test) {
142
- if (test.uid) {
143
- return test.uid
144
- }
145
-
146
- return Math.floor(new Date().getTime() / 1000)
147
- }
148
140
  }
package/lib/recorder.js CHANGED
@@ -379,6 +379,15 @@ module.exports = {
379
379
  toString() {
380
380
  return `Queue: ${currentQueue()}\n\nTasks: ${this.scheduled()}`
381
381
  },
382
+
383
+ /**
384
+ * Get current session ID
385
+ * @return {string|null}
386
+ * @inner
387
+ */
388
+ getCurrentSessionId() {
389
+ return sessionId
390
+ },
382
391
  }
383
392
 
384
393
  function getTimeoutPromise(timeoutMs, taskName) {
@@ -0,0 +1,323 @@
1
+ const express = require('express')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ /**
6
+ * Internal API test server to replace json-server dependency
7
+ * Provides REST API endpoints for testing CodeceptJS helpers
8
+ */
9
+ class TestServer {
10
+ constructor(config = {}) {
11
+ this.app = express()
12
+ this.server = null
13
+ this.port = config.port || 8010
14
+ this.host = config.host || 'localhost'
15
+ this.dbFile = config.dbFile || path.join(__dirname, '../test/data/rest/db.json')
16
+ this.lastModified = null
17
+ this.data = this.loadData()
18
+
19
+ this.setupMiddleware()
20
+ this.setupRoutes()
21
+ this.setupFileWatcher()
22
+ }
23
+
24
+ loadData() {
25
+ try {
26
+ const content = fs.readFileSync(this.dbFile, 'utf8')
27
+ const data = JSON.parse(content)
28
+ // Update lastModified time when loading data
29
+ if (fs.existsSync(this.dbFile)) {
30
+ this.lastModified = fs.statSync(this.dbFile).mtime
31
+ }
32
+ console.log('[Data Load] Loaded data from file:', JSON.stringify(data))
33
+ return data
34
+ } catch (err) {
35
+ console.warn(`[Data Load] Could not load data file ${this.dbFile}:`, err.message)
36
+ console.log('[Data Load] Using fallback default data')
37
+ return {
38
+ posts: [{ id: 1, title: 'json-server', author: 'davert' }],
39
+ user: { name: 'john', password: '123456' },
40
+ }
41
+ }
42
+ }
43
+
44
+ reloadData() {
45
+ console.log('[Reload] Reloading data from file...')
46
+ this.data = this.loadData()
47
+ console.log('[Reload] Data reloaded successfully')
48
+ return this.data
49
+ }
50
+
51
+ saveData() {
52
+ try {
53
+ fs.writeFileSync(this.dbFile, JSON.stringify(this.data, null, 2))
54
+ console.log('[Save] Data saved to file')
55
+ // Force update modification time to ensure auto-reload works
56
+ const now = new Date()
57
+ fs.utimesSync(this.dbFile, now, now)
58
+ this.lastModified = now
59
+ console.log('[Save] File modification time updated')
60
+ } catch (err) {
61
+ console.warn(`[Save] Could not save data file ${this.dbFile}:`, err.message)
62
+ }
63
+ }
64
+
65
+ setupMiddleware() {
66
+ // Parse JSON bodies
67
+ this.app.use(express.json())
68
+
69
+ // Parse URL-encoded bodies
70
+ this.app.use(express.urlencoded({ extended: true }))
71
+
72
+ // CORS support
73
+ this.app.use((req, res, next) => {
74
+ res.header('Access-Control-Allow-Origin', '*')
75
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
76
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Test')
77
+
78
+ if (req.method === 'OPTIONS') {
79
+ res.status(200).end()
80
+ return
81
+ }
82
+ next()
83
+ })
84
+
85
+ // Auto-reload middleware - check if file changed before each request
86
+ this.app.use((req, res, next) => {
87
+ try {
88
+ if (fs.existsSync(this.dbFile)) {
89
+ const stats = fs.statSync(this.dbFile)
90
+ if (!this.lastModified || stats.mtime > this.lastModified) {
91
+ console.log(`[Auto-reload] Database file changed (${this.dbFile}), reloading data...`)
92
+ console.log(`[Auto-reload] Old mtime: ${this.lastModified}, New mtime: ${stats.mtime}`)
93
+ this.reloadData()
94
+ this.lastModified = stats.mtime
95
+ console.log(`[Auto-reload] Data reloaded, user name is now: ${this.data.user?.name}`)
96
+ }
97
+ }
98
+ } catch (err) {
99
+ console.warn('[Auto-reload] Error checking file modification time:', err.message)
100
+ }
101
+ next()
102
+ })
103
+
104
+ // Logging middleware
105
+ this.app.use((req, res, next) => {
106
+ console.log(`${req.method} ${req.path}`)
107
+ next()
108
+ })
109
+ }
110
+
111
+ setupRoutes() {
112
+ // Reload endpoint (for testing)
113
+ this.app.post('/_reload', (req, res) => {
114
+ this.reloadData()
115
+ res.json({ message: 'Data reloaded', data: this.data })
116
+ })
117
+
118
+ // Headers endpoint (for header testing)
119
+ this.app.get('/headers', (req, res) => {
120
+ res.json(req.headers)
121
+ })
122
+
123
+ this.app.post('/headers', (req, res) => {
124
+ res.json(req.headers)
125
+ })
126
+
127
+ // User endpoints
128
+ this.app.get('/user', (req, res) => {
129
+ console.log(`[GET /user] Serving user data: ${JSON.stringify(this.data.user)}`)
130
+ res.json(this.data.user)
131
+ })
132
+
133
+ this.app.post('/user', (req, res) => {
134
+ this.data.user = { ...this.data.user, ...req.body }
135
+ this.saveData()
136
+ res.status(201).json(this.data.user)
137
+ })
138
+
139
+ this.app.patch('/user', (req, res) => {
140
+ this.data.user = { ...this.data.user, ...req.body }
141
+ this.saveData()
142
+ res.json(this.data.user)
143
+ })
144
+
145
+ this.app.put('/user', (req, res) => {
146
+ this.data.user = req.body
147
+ this.saveData()
148
+ res.json(this.data.user)
149
+ })
150
+
151
+ // Posts endpoints
152
+ this.app.get('/posts', (req, res) => {
153
+ res.json(this.data.posts)
154
+ })
155
+
156
+ this.app.get('/posts/:id', (req, res) => {
157
+ const id = parseInt(req.params.id)
158
+ const post = this.data.posts.find(p => p.id === id)
159
+
160
+ if (!post) {
161
+ // Return empty object instead of 404 for json-server compatibility
162
+ return res.json({})
163
+ }
164
+
165
+ res.json(post)
166
+ })
167
+
168
+ this.app.post('/posts', (req, res) => {
169
+ const newId = Math.max(...this.data.posts.map(p => p.id || 0)) + 1
170
+ const newPost = { id: newId, ...req.body }
171
+
172
+ this.data.posts.push(newPost)
173
+ this.saveData()
174
+ res.status(201).json(newPost)
175
+ })
176
+
177
+ this.app.put('/posts/:id', (req, res) => {
178
+ const id = parseInt(req.params.id)
179
+ const postIndex = this.data.posts.findIndex(p => p.id === id)
180
+
181
+ if (postIndex === -1) {
182
+ return res.status(404).json({ error: 'Post not found' })
183
+ }
184
+
185
+ this.data.posts[postIndex] = { id, ...req.body }
186
+ this.saveData()
187
+ res.json(this.data.posts[postIndex])
188
+ })
189
+
190
+ this.app.patch('/posts/:id', (req, res) => {
191
+ const id = parseInt(req.params.id)
192
+ const postIndex = this.data.posts.findIndex(p => p.id === id)
193
+
194
+ if (postIndex === -1) {
195
+ return res.status(404).json({ error: 'Post not found' })
196
+ }
197
+
198
+ this.data.posts[postIndex] = { ...this.data.posts[postIndex], ...req.body }
199
+ this.saveData()
200
+ res.json(this.data.posts[postIndex])
201
+ })
202
+
203
+ this.app.delete('/posts/:id', (req, res) => {
204
+ const id = parseInt(req.params.id)
205
+ const postIndex = this.data.posts.findIndex(p => p.id === id)
206
+
207
+ if (postIndex === -1) {
208
+ return res.status(404).json({ error: 'Post not found' })
209
+ }
210
+
211
+ const deletedPost = this.data.posts.splice(postIndex, 1)[0]
212
+ this.saveData()
213
+ res.json(deletedPost)
214
+ })
215
+
216
+ // File upload endpoint (basic implementation)
217
+ this.app.post('/upload', (req, res) => {
218
+ // Simple upload simulation - for more complex file uploads,
219
+ // multer would be needed but basic tests should work
220
+ res.json({
221
+ message: 'File upload endpoint available',
222
+ headers: req.headers,
223
+ body: req.body,
224
+ })
225
+ })
226
+
227
+ // Comments endpoints (for ApiDataFactory tests)
228
+ this.app.get('/comments', (req, res) => {
229
+ res.json(this.data.comments || [])
230
+ })
231
+
232
+ this.app.post('/comments', (req, res) => {
233
+ if (!this.data.comments) this.data.comments = []
234
+ const newId = Math.max(...this.data.comments.map(c => c.id || 0), 0) + 1
235
+ const newComment = { id: newId, ...req.body }
236
+
237
+ this.data.comments.push(newComment)
238
+ this.saveData()
239
+ res.status(201).json(newComment)
240
+ })
241
+
242
+ this.app.delete('/comments/:id', (req, res) => {
243
+ if (!this.data.comments) this.data.comments = []
244
+ const id = parseInt(req.params.id)
245
+ const commentIndex = this.data.comments.findIndex(c => c.id === id)
246
+
247
+ if (commentIndex === -1) {
248
+ return res.status(404).json({ error: 'Comment not found' })
249
+ }
250
+
251
+ const deletedComment = this.data.comments.splice(commentIndex, 1)[0]
252
+ this.saveData()
253
+ res.json(deletedComment)
254
+ })
255
+
256
+ // Generic catch-all for other endpoints
257
+ this.app.use((req, res) => {
258
+ res.status(404).json({ error: 'Endpoint not found' })
259
+ })
260
+ }
261
+
262
+ setupFileWatcher() {
263
+ if (fs.existsSync(this.dbFile)) {
264
+ fs.watchFile(this.dbFile, (current, previous) => {
265
+ if (current.mtime !== previous.mtime) {
266
+ console.log('Database file changed, reloading data...')
267
+ this.reloadData()
268
+ }
269
+ })
270
+ }
271
+ }
272
+
273
+ start() {
274
+ return new Promise((resolve, reject) => {
275
+ this.server = this.app.listen(this.port, this.host, err => {
276
+ if (err) {
277
+ reject(err)
278
+ } else {
279
+ console.log(`Test server running on http://${this.host}:${this.port}`)
280
+ resolve(this.server)
281
+ }
282
+ })
283
+ })
284
+ }
285
+
286
+ stop() {
287
+ return new Promise(resolve => {
288
+ if (this.server) {
289
+ this.server.close(() => {
290
+ console.log('Test server stopped')
291
+ resolve()
292
+ })
293
+ } else {
294
+ resolve()
295
+ }
296
+ })
297
+ }
298
+ }
299
+
300
+ module.exports = TestServer
301
+
302
+ // CLI usage
303
+ if (require.main === module) {
304
+ const config = {
305
+ port: process.env.PORT || 8010,
306
+ host: process.env.HOST || '0.0.0.0',
307
+ dbFile: process.argv[2] || path.join(__dirname, '../test/data/rest/db.json'),
308
+ }
309
+
310
+ const server = new TestServer(config)
311
+ server.start().catch(console.error)
312
+
313
+ // Graceful shutdown
314
+ process.on('SIGINT', () => {
315
+ console.log('\nShutting down test server...')
316
+ server.stop().then(() => process.exit(0))
317
+ })
318
+
319
+ process.on('SIGTERM', () => {
320
+ console.log('\nShutting down test server...')
321
+ server.stop().then(() => process.exit(0))
322
+ })
323
+ }
@@ -0,0 +1,53 @@
1
+ const { maskSensitiveData } = require('invisi-data')
2
+
3
+ /**
4
+ * Mask sensitive data utility for CodeceptJS
5
+ * Supports both boolean and object configuration formats
6
+ *
7
+ * @param {string} input - The string to mask
8
+ * @param {boolean|object} config - Masking configuration
9
+ * @returns {string} - Masked string
10
+ */
11
+ function maskData(input, config) {
12
+ if (!config) {
13
+ return input
14
+ }
15
+
16
+ // Handle boolean config (backward compatibility)
17
+ if (typeof config === 'boolean' && config === true) {
18
+ return maskSensitiveData(input)
19
+ }
20
+
21
+ // Handle object config with custom patterns
22
+ if (typeof config === 'object' && config.enabled === true) {
23
+ const customPatterns = config.patterns || []
24
+ return maskSensitiveData(input, customPatterns)
25
+ }
26
+
27
+ return input
28
+ }
29
+
30
+ /**
31
+ * Check if masking is enabled based on global configuration
32
+ *
33
+ * @returns {boolean|object} - Current masking configuration
34
+ */
35
+ function getMaskConfig() {
36
+ return global.maskSensitiveData || false
37
+ }
38
+
39
+ /**
40
+ * Check if data should be masked
41
+ *
42
+ * @returns {boolean} - True if masking is enabled
43
+ */
44
+ function shouldMaskData() {
45
+ const config = getMaskConfig()
46
+ return config === true || (typeof config === 'object' && config.enabled === true)
47
+ }
48
+
49
+ module.exports = {
50
+ maskData,
51
+ getMaskConfig,
52
+ shouldMaskData,
53
+ }
package/lib/utils.js CHANGED
@@ -6,6 +6,7 @@ const getFunctionArguments = require('fn-args')
6
6
  const deepClone = require('lodash.clonedeep')
7
7
  const { convertColorToRGBA, isColorProperty } = require('./colorUtils')
8
8
  const Fuse = require('fuse.js')
9
+ const { spawnSync } = require('child_process')
9
10
 
10
11
  function deepMerge(target, source) {
11
12
  const merge = require('lodash.merge')
@@ -191,8 +192,39 @@ module.exports.test = {
191
192
  submittedData(dataFile) {
192
193
  return function (key) {
193
194
  if (!fs.existsSync(dataFile)) {
194
- const waitTill = new Date(new Date().getTime() + 1 * 1000) // wait for one sec for file to be created
195
- while (waitTill > new Date()) {}
195
+ // Extended timeout for CI environments to handle slower processing
196
+ const waitTime = process.env.CI ? 60 * 1000 : 2 * 1000 // 60 seconds in CI, 2 seconds otherwise
197
+ let pollInterval = 100 // Start with 100ms polling interval
198
+ const maxPollInterval = 2000 // Max 2 second intervals
199
+ const startTime = new Date().getTime()
200
+
201
+ // Synchronous polling with exponential backoff to reduce CPU usage
202
+ while (new Date().getTime() - startTime < waitTime) {
203
+ if (fs.existsSync(dataFile)) {
204
+ break
205
+ }
206
+
207
+ // Use Node.js child_process.spawnSync with platform-specific sleep commands
208
+ // This avoids busy waiting and allows other processes to run
209
+ try {
210
+ if (os.platform() === 'win32') {
211
+ // Windows: use ping with precise timing (ping waits exactly the specified ms)
212
+ spawnSync('ping', ['-n', '1', '-w', pollInterval.toString(), '127.0.0.1'], { stdio: 'ignore' })
213
+ } else {
214
+ // Unix/Linux/macOS: use sleep with fractional seconds
215
+ spawnSync('sleep', [(pollInterval / 1000).toString()], { stdio: 'ignore' })
216
+ }
217
+ } catch (err) {
218
+ // If system commands fail, use a simple busy wait with minimal CPU usage
219
+ const end = new Date().getTime() + pollInterval
220
+ while (new Date().getTime() < end) {
221
+ // No-op loop - much lighter than previous approaches
222
+ }
223
+ }
224
+
225
+ // Exponential backoff: gradually increase polling interval to reduce resource usage
226
+ pollInterval = Math.min(pollInterval * 1.2, maxPollInterval)
227
+ }
196
228
  }
197
229
  if (!fs.existsSync(dataFile)) {
198
230
  throw new Error('Data file was not created in time')
@@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => {
7
7
  const { threadId } = workerObj;
8
8
  workerObj.on('message', (messageData) => {
9
9
  if (messageData.event === shareEvent) {
10
- share(messageData.data);
10
+ const Container = require('./container');
11
+ Container.share(messageData.data);
11
12
  }
12
13
  });
13
14
  workerObj.on('exit', () => {
package/lib/workers.js CHANGED
@@ -49,13 +49,14 @@ const populateGroups = numberOfWorkers => {
49
49
  return groups
50
50
  }
51
51
 
52
- const createWorker = workerObject => {
52
+ const createWorker = (workerObject, isPoolMode = false) => {
53
53
  const worker = new Worker(pathToWorker, {
54
54
  workerData: {
55
55
  options: simplifyObject(workerObject.options),
56
56
  tests: workerObject.tests,
57
57
  testRoot: workerObject.testRoot,
58
58
  workerIndex: workerObject.workerIndex + 1,
59
+ poolMode: isPoolMode,
59
60
  },
60
61
  })
61
62
  worker.on('error', err => output.error(`Worker Error: ${err.stack}`))
@@ -231,11 +232,17 @@ class Workers extends EventEmitter {
231
232
  super()
232
233
  this.setMaxListeners(50)
233
234
  this.codecept = initializeCodecept(config.testConfig, config.options)
235
+ this.options = config.options || {}
234
236
  this.errors = []
235
237
  this.numberOfWorkers = 0
236
238
  this.closedWorkers = 0
237
239
  this.workers = []
238
240
  this.testGroups = []
241
+ this.testPool = []
242
+ this.testPoolInitialized = false
243
+ this.isPoolMode = config.by === 'pool'
244
+ this.activeWorkers = new Map()
245
+ this.maxWorkers = numberOfWorkers // Track original worker count for pool mode
239
246
 
240
247
  createOutputDir(config.testConfig)
241
248
  if (numberOfWorkers) this._initWorkers(numberOfWorkers, config)
@@ -255,6 +262,7 @@ class Workers extends EventEmitter {
255
262
  *
256
263
  * - `suite`
257
264
  * - `test`
265
+ * - `pool`
258
266
  * - function(numberOfWorkers)
259
267
  *
260
268
  * This method can be overridden for a better split.
@@ -270,7 +278,11 @@ class Workers extends EventEmitter {
270
278
  this.testGroups.push(convertToMochaTests(testGroup))
271
279
  }
272
280
  } else if (typeof numberOfWorkers === 'number' && numberOfWorkers > 0) {
273
- this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
281
+ if (config.by === 'pool') {
282
+ this.createTestPool(numberOfWorkers)
283
+ } else {
284
+ this.testGroups = config.by === 'suite' ? this.createGroupsOfSuites(numberOfWorkers) : this.createGroupsOfTests(numberOfWorkers)
285
+ }
274
286
  }
275
287
  }
276
288
 
@@ -308,6 +320,85 @@ class Workers extends EventEmitter {
308
320
  return groups
309
321
  }
310
322
 
323
+ /**
324
+ * @param {Number} numberOfWorkers
325
+ */
326
+ createTestPool(numberOfWorkers) {
327
+ // For pool mode, create empty groups for each worker and initialize empty pool
328
+ // Test pool will be populated lazily when getNextTest() is first called
329
+ this.testPool = []
330
+ this.testPoolInitialized = false
331
+ this.testGroups = populateGroups(numberOfWorkers)
332
+ }
333
+
334
+ /**
335
+ * Initialize the test pool if not already done
336
+ * This is called lazily to avoid state pollution issues during construction
337
+ */
338
+ _initializeTestPool() {
339
+ if (this.testPoolInitialized) {
340
+ return
341
+ }
342
+
343
+ const files = this.codecept.testFiles
344
+ if (!files || files.length === 0) {
345
+ this.testPoolInitialized = true
346
+ return
347
+ }
348
+
349
+ try {
350
+ const mocha = Container.mocha()
351
+ mocha.files = files
352
+ mocha.loadFiles()
353
+
354
+ mocha.suite.eachTest(test => {
355
+ if (test) {
356
+ this.testPool.push(test.uid)
357
+ }
358
+ })
359
+ } catch (e) {
360
+ // If mocha loading fails due to state pollution, skip
361
+ }
362
+
363
+ // If no tests were found, fallback to using createGroupsOfTests approach
364
+ // This works around state pollution issues
365
+ if (this.testPool.length === 0 && files.length > 0) {
366
+ try {
367
+ const testGroups = this.createGroupsOfTests(2) // Use 2 as a default for fallback
368
+ for (const group of testGroups) {
369
+ this.testPool.push(...group)
370
+ }
371
+ } catch (e) {
372
+ // If createGroupsOfTests fails, fallback to simple file names
373
+ for (const file of files) {
374
+ this.testPool.push(`test_${file.replace(/[^a-zA-Z0-9]/g, '_')}`)
375
+ }
376
+ }
377
+ }
378
+
379
+ // Last resort fallback for unit tests - add dummy test UIDs
380
+ if (this.testPool.length === 0) {
381
+ for (let i = 0; i < Math.min(files.length, 5); i++) {
382
+ this.testPool.push(`dummy_test_${i}_${Date.now()}`)
383
+ }
384
+ }
385
+
386
+ this.testPoolInitialized = true
387
+ }
388
+
389
+ /**
390
+ * Gets the next test from the pool
391
+ * @returns {String|null} test uid or null if no tests available
392
+ */
393
+ getNextTest() {
394
+ // Initialize test pool lazily on first access
395
+ if (!this.testPoolInitialized) {
396
+ this._initializeTestPool()
397
+ }
398
+
399
+ return this.testPool.shift() || null
400
+ }
401
+
311
402
  /**
312
403
  * @param {Number} numberOfWorkers
313
404
  */
@@ -352,7 +443,7 @@ class Workers extends EventEmitter {
352
443
  process.env.RUNS_WITH_WORKERS = 'true'
353
444
  recorder.add('starting workers', () => {
354
445
  for (const worker of this.workers) {
355
- const workerThread = createWorker(worker)
446
+ const workerThread = createWorker(worker, this.isPoolMode)
356
447
  this._listenWorkerEvents(workerThread)
357
448
  }
358
449
  })
@@ -376,9 +467,27 @@ class Workers extends EventEmitter {
376
467
  }
377
468
 
378
469
  _listenWorkerEvents(worker) {
470
+ // Track worker thread for pool mode
471
+ if (this.isPoolMode) {
472
+ this.activeWorkers.set(worker, { available: true, workerIndex: null })
473
+ }
474
+
379
475
  worker.on('message', message => {
380
476
  output.process(message.workerIndex)
381
477
 
478
+ // Handle test requests for pool mode
479
+ if (message.type === 'REQUEST_TEST') {
480
+ if (this.isPoolMode) {
481
+ const nextTest = this.getNextTest()
482
+ if (nextTest) {
483
+ worker.postMessage({ type: 'TEST_ASSIGNED', test: nextTest })
484
+ } else {
485
+ worker.postMessage({ type: 'NO_MORE_TESTS' })
486
+ }
487
+ }
488
+ return
489
+ }
490
+
382
491
  // deal with events that are not test cycle related
383
492
  if (!message.event) {
384
493
  return this.emit('message', message)
@@ -387,11 +496,21 @@ class Workers extends EventEmitter {
387
496
  switch (message.event) {
388
497
  case event.all.result:
389
498
  // we ensure consistency of result by adding tests in the very end
390
- Container.result().addFailures(message.data.failures)
391
- Container.result().addStats(message.data.stats)
392
- message.data.tests.forEach(test => {
393
- Container.result().addTest(deserializeTest(test))
394
- })
499
+ // Check if message.data.stats is valid before adding
500
+ if (message.data.stats) {
501
+ Container.result().addStats(message.data.stats)
502
+ }
503
+
504
+ if (message.data.failures) {
505
+ Container.result().addFailures(message.data.failures)
506
+ }
507
+
508
+ if (message.data.tests) {
509
+ message.data.tests.forEach(test => {
510
+ Container.result().addTest(deserializeTest(test))
511
+ })
512
+ }
513
+
395
514
  break
396
515
  case event.suite.before:
397
516
  this.emit(event.suite.before, deserializeSuite(message.data))
@@ -438,7 +557,14 @@ class Workers extends EventEmitter {
438
557
 
439
558
  worker.on('exit', () => {
440
559
  this.closedWorkers += 1
441
- if (this.closedWorkers === this.numberOfWorkers) {
560
+
561
+ if (this.isPoolMode) {
562
+ // Pool mode: finish when all workers have exited and no more tests
563
+ if (this.closedWorkers === this.numberOfWorkers) {
564
+ this._finishRun()
565
+ }
566
+ } else if (this.closedWorkers === this.numberOfWorkers) {
567
+ // Regular mode: finish when all original workers have exited
442
568
  this._finishRun()
443
569
  }
444
570
  })