codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.8.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.
Files changed (68) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +105 -16
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +58 -47
  66. package/typings/index.d.ts +19 -7
  67. package/typings/promiseBasedTypes.d.ts +5525 -3759
  68. package/typings/types.d.ts +5791 -3781
@@ -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 childProcess from 'child_process'
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
- const waitTill = new Date(new Date().getTime() + 1 * 1000) // wait for one sec for file to be created
199
- while (waitTill > new Date()) {}
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 = async directoryPath => {
451
- childProcess.execSync(`rm -rf ${directoryPath}/*`)
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
@@ -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', () => {