codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.9.esm-aria
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 +46 -3
- package/bin/codecept.js +9 -0
- package/bin/test-server.js +64 -0
- package/docs/webapi/click.mustache +5 -1
- package/lib/ai.js +66 -102
- package/lib/codecept.js +99 -24
- package/lib/command/generate.js +33 -1
- package/lib/command/init.js +7 -3
- package/lib/command/run-workers.js +31 -2
- package/lib/command/run.js +15 -0
- package/lib/command/workers/runTests.js +331 -58
- package/lib/config.js +16 -5
- package/lib/container.js +15 -13
- package/lib/effects.js +1 -1
- package/lib/element/WebElement.js +327 -0
- package/lib/event.js +10 -1
- package/lib/helper/AI.js +11 -11
- package/lib/helper/ApiDataFactory.js +34 -6
- package/lib/helper/Appium.js +156 -42
- package/lib/helper/GraphQL.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +48 -40
- package/lib/helper/Mochawesome.js +24 -2
- package/lib/helper/Playwright.js +841 -153
- package/lib/helper/Puppeteer.js +263 -67
- package/lib/helper/REST.js +21 -0
- package/lib/helper/WebDriver.js +105 -16
- package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
- package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
- package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
- package/lib/helper/network/actions.js +8 -6
- package/lib/listener/config.js +11 -3
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/globalTimeout.js +19 -4
- package/lib/listener/helpers.js +8 -2
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +12 -0
- package/lib/mocha/asyncWrapper.js +13 -3
- package/lib/mocha/cli.js +1 -1
- package/lib/mocha/factory.js +3 -0
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/test.js +6 -0
- package/lib/mocha/ui.js +13 -0
- package/lib/output.js +62 -18
- package/lib/plugin/coverage.js +16 -3
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/retryFailedStep.js +1 -0
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/recorder.js +28 -3
- package/lib/result.js +100 -23
- package/lib/retryCoordinator.js +207 -0
- package/lib/step/base.js +1 -1
- package/lib/step/comment.js +2 -2
- package/lib/step/meta.js +1 -1
- package/lib/template/heal.js +1 -1
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +87 -6
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +179 -23
- package/package.json +59 -47
- package/typings/index.d.ts +19 -7
- package/typings/promiseBasedTypes.d.ts +5534 -3764
- package/typings/types.d.ts +5789 -3775
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from '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.readOnly = config.readOnly || false
|
|
17
|
+
this.lastModified = null
|
|
18
|
+
this.data = this.loadData()
|
|
19
|
+
|
|
20
|
+
this.setupMiddleware()
|
|
21
|
+
this.setupRoutes()
|
|
22
|
+
this.setupFileWatcher()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loadData() {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(this.dbFile, 'utf8')
|
|
28
|
+
const data = JSON.parse(content)
|
|
29
|
+
// Update lastModified time when loading data
|
|
30
|
+
if (fs.existsSync(this.dbFile)) {
|
|
31
|
+
this.lastModified = fs.statSync(this.dbFile).mtime
|
|
32
|
+
}
|
|
33
|
+
console.log('[Data Load] Loaded data from file:', JSON.stringify(data))
|
|
34
|
+
return data
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`[Data Load] Could not load data file ${this.dbFile}:`, err.message)
|
|
37
|
+
console.log('[Data Load] Using fallback default data')
|
|
38
|
+
return {
|
|
39
|
+
posts: [{ id: 1, title: 'json-server', author: 'davert' }],
|
|
40
|
+
user: { name: 'john', password: '123456' },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
reloadData() {
|
|
46
|
+
console.log('[Reload] Reloading data from file...')
|
|
47
|
+
this.data = this.loadData()
|
|
48
|
+
console.log('[Reload] Data reloaded successfully')
|
|
49
|
+
return this.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
saveData() {
|
|
53
|
+
if (this.readOnly) {
|
|
54
|
+
console.log('[Save] Skipping save - running in read-only mode')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(this.dbFile, JSON.stringify(this.data, null, 2))
|
|
59
|
+
console.log('[Save] Data saved to file')
|
|
60
|
+
// Force update modification time to ensure auto-reload works
|
|
61
|
+
const now = new Date()
|
|
62
|
+
fs.utimesSync(this.dbFile, now, now)
|
|
63
|
+
this.lastModified = now
|
|
64
|
+
console.log('[Save] File modification time updated')
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn(`[Save] Could not save data file ${this.dbFile}:`, err.message)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setupMiddleware() {
|
|
71
|
+
// Parse JSON bodies
|
|
72
|
+
this.app.use(express.json())
|
|
73
|
+
|
|
74
|
+
// Parse URL-encoded bodies
|
|
75
|
+
this.app.use(express.urlencoded({ extended: true }))
|
|
76
|
+
|
|
77
|
+
// CORS support
|
|
78
|
+
this.app.use((req, res, next) => {
|
|
79
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
80
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
81
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Test')
|
|
82
|
+
|
|
83
|
+
if (req.method === 'OPTIONS') {
|
|
84
|
+
res.status(200).end()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
next()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Auto-reload middleware - check if file changed before each request
|
|
91
|
+
this.app.use((req, res, next) => {
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(this.dbFile)) {
|
|
94
|
+
const stats = fs.statSync(this.dbFile)
|
|
95
|
+
if (!this.lastModified || stats.mtime > this.lastModified) {
|
|
96
|
+
console.log(`[Auto-reload] Database file changed (${this.dbFile}), reloading data...`)
|
|
97
|
+
console.log(`[Auto-reload] Old mtime: ${this.lastModified}, New mtime: ${stats.mtime}`)
|
|
98
|
+
this.reloadData()
|
|
99
|
+
this.lastModified = stats.mtime
|
|
100
|
+
console.log(`[Auto-reload] Data reloaded, user name is now: ${this.data.user?.name}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.warn('[Auto-reload] Error checking file modification time:', err.message)
|
|
105
|
+
}
|
|
106
|
+
next()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Logging middleware
|
|
110
|
+
this.app.use((req, res, next) => {
|
|
111
|
+
console.log(`${req.method} ${req.path}`)
|
|
112
|
+
next()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setupRoutes() {
|
|
117
|
+
// Reload endpoint (for testing)
|
|
118
|
+
this.app.post('/_reload', (req, res) => {
|
|
119
|
+
this.reloadData()
|
|
120
|
+
res.json({ message: 'Data reloaded', data: this.data })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Headers endpoint (for header testing)
|
|
124
|
+
this.app.get('/headers', (req, res) => {
|
|
125
|
+
res.json(req.headers)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
this.app.post('/headers', (req, res) => {
|
|
129
|
+
res.json(req.headers)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// User endpoints
|
|
133
|
+
this.app.get('/user', (req, res) => {
|
|
134
|
+
console.log(`[GET /user] Serving user data: ${JSON.stringify(this.data.user)}`)
|
|
135
|
+
res.json(this.data.user)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
this.app.post('/user', (req, res) => {
|
|
139
|
+
this.data.user = { ...this.data.user, ...req.body }
|
|
140
|
+
this.saveData()
|
|
141
|
+
res.status(201).json(this.data.user)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
this.app.patch('/user', (req, res) => {
|
|
145
|
+
this.data.user = { ...this.data.user, ...req.body }
|
|
146
|
+
this.saveData()
|
|
147
|
+
res.json(this.data.user)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
this.app.put('/user', (req, res) => {
|
|
151
|
+
this.data.user = req.body
|
|
152
|
+
this.saveData()
|
|
153
|
+
res.json(this.data.user)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Posts endpoints
|
|
157
|
+
this.app.get('/posts', (req, res) => {
|
|
158
|
+
res.json(this.data.posts)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
this.app.get('/posts/:id', (req, res) => {
|
|
162
|
+
const id = parseInt(req.params.id)
|
|
163
|
+
const post = this.data.posts.find(p => p.id === id)
|
|
164
|
+
|
|
165
|
+
if (!post) {
|
|
166
|
+
// Return empty object instead of 404 for json-server compatibility
|
|
167
|
+
return res.json({})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
res.json(post)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
this.app.post('/posts', (req, res) => {
|
|
174
|
+
const newId = Math.max(...this.data.posts.map(p => p.id || 0)) + 1
|
|
175
|
+
const newPost = { id: newId, ...req.body }
|
|
176
|
+
|
|
177
|
+
this.data.posts.push(newPost)
|
|
178
|
+
this.saveData()
|
|
179
|
+
res.status(201).json(newPost)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
this.app.put('/posts/:id', (req, res) => {
|
|
183
|
+
const id = parseInt(req.params.id)
|
|
184
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
185
|
+
|
|
186
|
+
if (postIndex === -1) {
|
|
187
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.data.posts[postIndex] = { id, ...req.body }
|
|
191
|
+
this.saveData()
|
|
192
|
+
res.json(this.data.posts[postIndex])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
this.app.patch('/posts/:id', (req, res) => {
|
|
196
|
+
const id = parseInt(req.params.id)
|
|
197
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
198
|
+
|
|
199
|
+
if (postIndex === -1) {
|
|
200
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.data.posts[postIndex] = { ...this.data.posts[postIndex], ...req.body }
|
|
204
|
+
this.saveData()
|
|
205
|
+
res.json(this.data.posts[postIndex])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
this.app.delete('/posts/:id', (req, res) => {
|
|
209
|
+
const id = parseInt(req.params.id)
|
|
210
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
211
|
+
|
|
212
|
+
if (postIndex === -1) {
|
|
213
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const deletedPost = this.data.posts.splice(postIndex, 1)[0]
|
|
217
|
+
this.saveData()
|
|
218
|
+
res.json(deletedPost)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// File upload endpoint (basic implementation)
|
|
222
|
+
this.app.post('/upload', (req, res) => {
|
|
223
|
+
// Simple upload simulation - for more complex file uploads,
|
|
224
|
+
// multer would be needed but basic tests should work
|
|
225
|
+
res.json({
|
|
226
|
+
message: 'File upload endpoint available',
|
|
227
|
+
headers: req.headers,
|
|
228
|
+
body: req.body,
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Comments endpoints (for ApiDataFactory tests)
|
|
233
|
+
this.app.get('/comments', (req, res) => {
|
|
234
|
+
res.json(this.data.comments || [])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
this.app.post('/comments', (req, res) => {
|
|
238
|
+
if (!this.data.comments) this.data.comments = []
|
|
239
|
+
const newId = Math.max(...this.data.comments.map(c => c.id || 0), 0) + 1
|
|
240
|
+
const newComment = { id: newId, ...req.body }
|
|
241
|
+
|
|
242
|
+
this.data.comments.push(newComment)
|
|
243
|
+
this.saveData()
|
|
244
|
+
res.status(201).json(newComment)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
this.app.delete('/comments/:id', (req, res) => {
|
|
248
|
+
if (!this.data.comments) this.data.comments = []
|
|
249
|
+
const id = parseInt(req.params.id)
|
|
250
|
+
const commentIndex = this.data.comments.findIndex(c => c.id === id)
|
|
251
|
+
|
|
252
|
+
if (commentIndex === -1) {
|
|
253
|
+
return res.status(404).json({ error: 'Comment not found' })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const deletedComment = this.data.comments.splice(commentIndex, 1)[0]
|
|
257
|
+
this.saveData()
|
|
258
|
+
res.json(deletedComment)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Generic catch-all for other endpoints
|
|
262
|
+
this.app.use((req, res) => {
|
|
263
|
+
res.status(404).json({ error: 'Endpoint not found' })
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setupFileWatcher() {
|
|
268
|
+
if (fs.existsSync(this.dbFile)) {
|
|
269
|
+
fs.watchFile(this.dbFile, (current, previous) => {
|
|
270
|
+
if (current.mtime !== previous.mtime) {
|
|
271
|
+
console.log('Database file changed, reloading data...')
|
|
272
|
+
this.reloadData()
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
start() {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
this.server = this.app.listen(this.port, this.host, err => {
|
|
281
|
+
if (err) {
|
|
282
|
+
reject(err)
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`Test server running on http://${this.host}:${this.port}`)
|
|
285
|
+
resolve(this.server)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
stop() {
|
|
292
|
+
return new Promise(resolve => {
|
|
293
|
+
if (this.server) {
|
|
294
|
+
this.server.close(() => {
|
|
295
|
+
console.log('Test server stopped')
|
|
296
|
+
resolve()
|
|
297
|
+
})
|
|
298
|
+
} else {
|
|
299
|
+
resolve()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default TestServer
|
|
306
|
+
|
|
307
|
+
// CLI usage - Import meta for ESM
|
|
308
|
+
import { fileURLToPath } from 'url'
|
|
309
|
+
import { dirname } from 'path'
|
|
310
|
+
|
|
311
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
312
|
+
const __dirname = dirname(__filename)
|
|
313
|
+
|
|
314
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
315
|
+
const config = {
|
|
316
|
+
port: process.env.PORT || 8010,
|
|
317
|
+
host: process.env.HOST || '0.0.0.0',
|
|
318
|
+
dbFile: process.argv[2] || path.join(__dirname, '../test/data/rest/db.json'),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const server = new TestServer(config)
|
|
322
|
+
server.start().catch(console.error)
|
|
323
|
+
|
|
324
|
+
// Graceful shutdown
|
|
325
|
+
process.on('SIGINT', () => {
|
|
326
|
+
console.log('\nShutting down test server...')
|
|
327
|
+
server.stop().then(() => process.exit(0))
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
process.on('SIGTERM', () => {
|
|
331
|
+
console.log('\nShutting down test server...')
|
|
332
|
+
server.stop().then(() => process.exit(0))
|
|
333
|
+
})
|
|
334
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { maskSensitiveData } from '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
|
+
export 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
|
+
export 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
|
+
export function shouldMaskData() {
|
|
45
|
+
const config = getMaskConfig()
|
|
46
|
+
return config === true || (typeof config === 'object' && config.enabled === true)
|
|
47
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import os from 'os'
|
|
3
3
|
import path from 'path'
|
|
4
|
+
import { createRequire } from 'module'
|
|
4
5
|
import chalk from 'chalk'
|
|
5
6
|
import getFunctionArguments from 'fn-args'
|
|
6
7
|
import deepClone from 'lodash.clonedeep'
|
|
@@ -9,8 +10,7 @@ import { convertColorToRGBA, isColorProperty } from './colorUtils.js'
|
|
|
9
10
|
import Fuse from 'fuse.js'
|
|
10
11
|
import crypto from 'crypto'
|
|
11
12
|
import jsBeautify from 'js-beautify'
|
|
12
|
-
import
|
|
13
|
-
import { createRequire } from 'module'
|
|
13
|
+
import { spawnSync } from 'child_process'
|
|
14
14
|
|
|
15
15
|
function deepMerge(target, source) {
|
|
16
16
|
return merge(target, source)
|
|
@@ -195,8 +195,39 @@ export const test = {
|
|
|
195
195
|
submittedData(dataFile) {
|
|
196
196
|
return function (key) {
|
|
197
197
|
if (!fs.existsSync(dataFile)) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// Extended timeout for CI environments to handle slower processing
|
|
199
|
+
const waitTime = process.env.CI ? 60 * 1000 : 2 * 1000 // 60 seconds in CI, 2 seconds otherwise
|
|
200
|
+
let pollInterval = 100 // Start with 100ms polling interval
|
|
201
|
+
const maxPollInterval = 2000 // Max 2 second intervals
|
|
202
|
+
const startTime = new Date().getTime()
|
|
203
|
+
|
|
204
|
+
// Synchronous polling with exponential backoff to reduce CPU usage
|
|
205
|
+
while (new Date().getTime() - startTime < waitTime) {
|
|
206
|
+
if (fs.existsSync(dataFile)) {
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Use Node.js child_process.spawnSync with platform-specific sleep commands
|
|
211
|
+
// This avoids busy waiting and allows other processes to run
|
|
212
|
+
try {
|
|
213
|
+
if (os.platform() === 'win32') {
|
|
214
|
+
// Windows: use ping with precise timing (ping waits exactly the specified ms)
|
|
215
|
+
spawnSync('ping', ['-n', '1', '-w', pollInterval.toString(), '127.0.0.1'], { stdio: 'ignore' })
|
|
216
|
+
} else {
|
|
217
|
+
// Unix/Linux/macOS: use sleep with fractional seconds
|
|
218
|
+
spawnSync('sleep', [(pollInterval / 1000).toString()], { stdio: 'ignore' })
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// If system commands fail, use a simple busy wait with minimal CPU usage
|
|
222
|
+
const end = new Date().getTime() + pollInterval
|
|
223
|
+
while (new Date().getTime() < end) {
|
|
224
|
+
// No-op loop - much lighter than previous approaches
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Exponential backoff: gradually increase polling interval to reduce resource usage
|
|
229
|
+
pollInterval = Math.min(pollInterval * 1.2, maxPollInterval)
|
|
230
|
+
}
|
|
200
231
|
}
|
|
201
232
|
if (!fs.existsSync(dataFile)) {
|
|
202
233
|
throw new Error('Data file was not created in time')
|
|
@@ -447,8 +478,12 @@ export const isNotSet = function (obj) {
|
|
|
447
478
|
return false
|
|
448
479
|
}
|
|
449
480
|
|
|
450
|
-
export const emptyFolder =
|
|
451
|
-
|
|
481
|
+
export const emptyFolder = directoryPath => {
|
|
482
|
+
// Do not throw on non-existent directory, since it may be created later
|
|
483
|
+
if (!fs.existsSync(directoryPath)) return
|
|
484
|
+
for (const file of fs.readdirSync(directoryPath)) {
|
|
485
|
+
fs.rmSync(path.join(directoryPath, file), { recursive: true, force: true })
|
|
486
|
+
}
|
|
452
487
|
}
|
|
453
488
|
|
|
454
489
|
export const printObjectProperties = obj => {
|
|
@@ -547,6 +582,52 @@ export const humanizeString = function (string) {
|
|
|
547
582
|
return _result.join(' ').trim()
|
|
548
583
|
}
|
|
549
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Creates a circular-safe replacer function for JSON.stringify
|
|
587
|
+
* @param {string[]} keysToSkip - Keys to skip during serialization to break circular references
|
|
588
|
+
* @returns {Function} Replacer function for JSON.stringify
|
|
589
|
+
*/
|
|
590
|
+
function createCircularSafeReplacer(keysToSkip = []) {
|
|
591
|
+
const seen = new WeakSet()
|
|
592
|
+
const defaultSkipKeys = ['parent', 'tests', 'suite', 'root', 'runner', 'ctx']
|
|
593
|
+
const skipKeys = new Set([...defaultSkipKeys, ...keysToSkip])
|
|
594
|
+
|
|
595
|
+
return function (key, value) {
|
|
596
|
+
// Skip specific keys that commonly cause circular references
|
|
597
|
+
if (key && skipKeys.has(key)) {
|
|
598
|
+
return undefined
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (value === null || typeof value !== 'object') {
|
|
602
|
+
return value
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Handle circular references
|
|
606
|
+
if (seen.has(value)) {
|
|
607
|
+
return `[Circular Reference to ${value.constructor?.name || 'Object'}]`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
seen.add(value)
|
|
611
|
+
return value
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Safely stringify an object, handling circular references
|
|
617
|
+
* @param {any} obj - Object to stringify
|
|
618
|
+
* @param {string[]} keysToSkip - Additional keys to skip during serialization
|
|
619
|
+
* @param {number} space - Number of spaces for indentation (default: 0)
|
|
620
|
+
* @returns {string} JSON string representation
|
|
621
|
+
*/
|
|
622
|
+
export const safeStringify = function (obj, keysToSkip = [], space = 0) {
|
|
623
|
+
try {
|
|
624
|
+
return JSON.stringify(obj, createCircularSafeReplacer(keysToSkip), space)
|
|
625
|
+
} catch (error) {
|
|
626
|
+
// Fallback for any remaining edge cases
|
|
627
|
+
return JSON.stringify({ error: `Failed to serialize: ${error.message}` }, null, space)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
550
631
|
export const serializeError = function (error) {
|
|
551
632
|
if (error) {
|
|
552
633
|
const { stack, uncaught, message, actual, expected } = error
|
package/lib/workerStorage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isMainThread, parentPort } from 'worker_threads'
|
|
2
|
+
import Container from './container.js'
|
|
2
3
|
|
|
3
4
|
const workerObjects = {}
|
|
4
5
|
const shareEvent = 'share'
|
|
@@ -7,7 +8,7 @@ const invokeWorkerListeners = workerObj => {
|
|
|
7
8
|
const { threadId } = workerObj
|
|
8
9
|
workerObj.on('message', messageData => {
|
|
9
10
|
if (messageData.event === shareEvent) {
|
|
10
|
-
share(messageData.data)
|
|
11
|
+
Container.share(messageData.data)
|
|
11
12
|
}
|
|
12
13
|
})
|
|
13
14
|
workerObj.on('exit', () => {
|