codeceptjs 3.7.5-beta.15 → 3.7.5-beta.17

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.
@@ -1,413 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const event = require('../event')
4
- const output = require('../output')
5
- const store = require('../store')
6
-
7
- const defaultConfig = {
8
- enabled: true,
9
- outputFile: 'failed-tests.json',
10
- clearOnSuccess: true,
11
- includeStackTrace: true,
12
- includeMetadata: true,
13
- }
14
-
15
- /**
16
- * Failed Tests Tracker Plugin for CodeceptJS
17
- *
18
- * Tracks failed tests and saves them to a file for later re-execution.
19
- *
20
- * ## Configuration
21
- *
22
- * ```js
23
- * "plugins": {
24
- * "failedTestsTracker": {
25
- * "enabled": true,
26
- * "outputFile": "failed-tests.json",
27
- * "clearOnSuccess": true,
28
- * "includeStackTrace": true,
29
- * "includeMetadata": true
30
- * }
31
- * }
32
- * ```
33
- *
34
- * @param {object} config plugin configuration
35
- */
36
- module.exports = function (config) {
37
- const options = { ...defaultConfig, ...config }
38
- let failedTests = []
39
- let allTestsPassed = true
40
- let workerFailedTests = new Map() // Track failed tests from workers
41
-
42
- // Track test failures - only when not using workers
43
- event.dispatcher.on(event.test.failed, test => {
44
- // Skip collection in worker threads to avoid duplicates
45
- try {
46
- const { isMainThread } = require('worker_threads')
47
- if (!isMainThread) return
48
- } catch (e) {
49
- // worker_threads not available, continue
50
- }
51
-
52
- if (store.hasWorkers) return // Skip if running with workers
53
-
54
- // Only collect on final failure (when retries are exhausted or no retries configured)
55
- const currentRetry = test._currentRetry || 0
56
- const maxRetries = typeof test.retries === 'function' ? test.retries() : (test.retries || 0)
57
-
58
- // Only add to failed tests if this is the final attempt
59
- if (currentRetry >= maxRetries) {
60
- allTestsPassed = false
61
-
62
- const failedTest = {
63
- title: test.title,
64
- fullTitle: test.fullTitle(),
65
- file: test.file || (test.parent && test.parent.file),
66
- uid: test.uid,
67
- timestamp: new Date().toISOString(),
68
- }
69
-
70
- // Add parent suite information
71
- if (test.parent) {
72
- failedTest.suite = test.parent.title
73
- failedTest.suiteFile = test.parent.file
74
- }
75
-
76
- // Add error information if available
77
- if (test.err && options.includeStackTrace) {
78
- failedTest.error = {
79
- message: test.err.message || 'Test failed',
80
- stack: test.err.stack || '',
81
- name: test.err.name || 'Error',
82
- }
83
- }
84
-
85
- // Add metadata if available
86
- if (options.includeMetadata) {
87
- failedTest.metadata = {
88
- tags: test.tags || [],
89
- meta: test.meta || {},
90
- opts: test.opts || {},
91
- duration: test.duration || 0,
92
- // Only include retries if it represents actual retry attempts, not the config value
93
- ...(test._currentRetry > 0 && { actualRetries: test._currentRetry }),
94
- ...(maxRetries > 0 && maxRetries !== -1 && { maxRetries: maxRetries }),
95
- }
96
- }
97
-
98
- // Add BDD/Gherkin information if available
99
- if (test.parent && test.parent.feature) {
100
- failedTest.bdd = {
101
- feature: test.parent.feature.name || test.parent.title,
102
- scenario: test.title,
103
- featureFile: test.parent.file,
104
- }
105
- }
106
-
107
- failedTests.push(failedTest)
108
- output.print(`Failed Tests Tracker: Recorded failed test - ${test.title}`)
109
- }
110
- })
111
-
112
- // Handle test completion and save failed tests
113
- event.dispatcher.on(event.all.result, (result) => {
114
-
115
- // Respect CodeceptJS output directory like other plugins
116
- const outputDir = global.output_dir || './output'
117
- const outputPath = path.isAbsolute(options.outputFile)
118
- ? options.outputFile
119
- : path.resolve(outputDir, options.outputFile)
120
- let allFailedTests = [...failedTests]
121
-
122
- // Collect failed tests from result (both worker and single-process modes)
123
- if (result) {
124
- let resultFailedTests = []
125
-
126
- // Worker mode: result.tests
127
- if (store.hasWorkers && result.tests) {
128
- resultFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
129
- }
130
- // Single-process mode: result._tests (result._failures contains console log arrays, not test objects)
131
- else if (!store.hasWorkers && result._tests) {
132
- resultFailedTests = result._tests.filter(test => test.state === 'failed' || test.err)
133
- }
134
-
135
- // Use a Set to track unique test identifiers to prevent duplicates
136
- const existingTestIds = new Set(allFailedTests.map(test => test.uid || `${test.file}:${test.title}`))
137
-
138
- resultFailedTests.forEach(test => {
139
-
140
- // Extract file path from test title or error stack trace as fallback
141
- let filePath = test.file || test.parent?.file || 'unknown'
142
-
143
- // If still unknown, try to extract from error stack trace
144
- if (filePath === 'unknown' && test.err && test.err.stack) {
145
- // Try multiple regex patterns for different stack trace formats
146
- const patterns = [
147
- /at.*\(([^)]+\.js):\d+:\d+\)/, // Standard format
148
- /at.*\(.*[\/\\]([^\/\\]+\.js):\d+:\d+\)/, // With path separators
149
- /\(([^)]*\.js):\d+:\d+\)/, // Simpler format
150
- /([^\/\\]+\.js):\d+:\d+/, // Just filename with line numbers
151
- ]
152
-
153
- for (const pattern of patterns) {
154
- const stackMatch = test.err.stack.match(pattern)
155
- if (stackMatch && stackMatch[1]) {
156
- const absolutePath = stackMatch[1]
157
- const relativePath = absolutePath.replace(process.cwd() + '/', '').replace(/^.*[\/\\]/, '')
158
- filePath = relativePath
159
- break
160
- }
161
- }
162
- }
163
-
164
- // If still unknown, try to extract from test context or use test file pattern
165
- if (filePath === 'unknown') {
166
- // Look for common test file patterns in the test title or fullTitle
167
- const fullTitle = test.fullTitle || test.title
168
- if (fullTitle && fullTitle.includes('checkout')) {
169
- filePath = 'checkout_test.js'
170
- } else if (fullTitle && fullTitle.includes('github')) {
171
- filePath = 'github_test.js'
172
- }
173
- }
174
-
175
- // Create unique identifier for deduplication
176
- const testId = test.uid || `${filePath}:${test.title}`
177
-
178
- // Skip if we already have this test
179
- if (existingTestIds.has(testId)) {
180
- return
181
- }
182
-
183
- // Extract proper test properties from different test object structures
184
- const testTitle = test.title || test.test?.title || (test.fullTitle && test.fullTitle()) || 'Unknown Test'
185
- const testFullTitle = test.fullTitle ? (typeof test.fullTitle === 'function' ? test.fullTitle() : test.fullTitle) : testTitle
186
- const testUid = test.uid || test.test?.uid || `${filePath}:${testTitle}`
187
-
188
- const failedTest = {
189
- title: testTitle,
190
- fullTitle: testFullTitle,
191
- file: filePath,
192
- uid: testUid,
193
- // Add stable identifier for targeting tests across runs
194
- stableId: `${filePath}:${testTitle}`,
195
- timestamp: new Date().toISOString(),
196
- }
197
-
198
- // Add parent suite information
199
- if (test.parent) {
200
- failedTest.suite = test.parent.title
201
- failedTest.suiteFile = test.parent.file
202
- }
203
-
204
- // Add error information if available
205
- if (test.err && options.includeStackTrace) {
206
- failedTest.error = {
207
- message: test.err.message || 'Test failed',
208
- stack: test.err.stack || '',
209
- name: test.err.name || 'Error',
210
- }
211
- }
212
-
213
- // Add metadata if available
214
- if (options.includeMetadata) {
215
- failedTest.metadata = {
216
- tags: test.tags || [],
217
- meta: test.meta || {},
218
- opts: test.opts || {},
219
- duration: test.duration || 0,
220
- retries: test.retries || 0,
221
- }
222
- }
223
-
224
- // Add BDD/Gherkin information if available
225
- if (test.parent && test.parent.feature) {
226
- failedTest.bdd = {
227
- feature: test.parent.feature.name || test.parent.title,
228
- scenario: test.title,
229
- featureFile: test.parent.file,
230
- }
231
- }
232
-
233
- allFailedTests.push(failedTest)
234
- existingTestIds.add(testId)
235
- })
236
-
237
- output.print(`Failed Tests Tracker: Collected ${resultFailedTests.length} failed tests from result`)
238
- }
239
-
240
- if (allFailedTests.length === 0) {
241
- if (options.clearOnSuccess && fs.existsSync(outputPath)) {
242
- try {
243
- fs.unlinkSync(outputPath)
244
- output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
245
- } catch (error) {
246
- output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
247
- }
248
- } else {
249
- output.print(`Failed Tests Tracker: No failed tests to save`)
250
- }
251
- return
252
- }
253
-
254
- const failedTestsData = {
255
- timestamp: new Date().toISOString(),
256
- totalFailedTests: allFailedTests.length,
257
- codeceptVersion: require('../codecept').version(),
258
- tests: allFailedTests,
259
- }
260
-
261
- try {
262
- // Ensure directory exists
263
- const dir = path.dirname(outputPath)
264
- if (!fs.existsSync(dir)) {
265
- fs.mkdirSync(dir, { recursive: true })
266
- }
267
-
268
- fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
269
- output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
270
- } catch (error) {
271
- output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
272
- }
273
- })
274
-
275
- // Reset state for new test runs
276
- event.dispatcher.on(event.all.before, () => {
277
- failedTests = []
278
- allTestsPassed = true
279
- workerFailedTests.clear()
280
- })
281
-
282
- // Handle worker mode - listen to workers.result event for consolidated results
283
- event.dispatcher.on(event.workers.result, (result) => {
284
- // Respect CodeceptJS output directory like other plugins
285
- const outputDir = global.output_dir || './output'
286
- const outputPath = path.isAbsolute(options.outputFile)
287
- ? options.outputFile
288
- : path.resolve(outputDir, options.outputFile)
289
-
290
- let allFailedTests = []
291
-
292
- // In worker mode, collect failed tests from consolidated result
293
- if (result && result.tests) {
294
- const workerFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
295
-
296
- workerFailedTests.forEach(test => {
297
- // Extract file path from test title or error stack trace as fallback
298
- let filePath = test.file || test.parent?.file || 'unknown'
299
-
300
- // If still unknown, try to extract from error stack trace
301
- if (filePath === 'unknown' && test.err && test.err.stack) {
302
- // Try multiple regex patterns for different stack trace formats
303
- const patterns = [
304
- /at.*\(([^)]+\.js):\d+:\d+\)/, // Standard format
305
- /at.*\(.*[\/\\]([^\/\\]+\.js):\d+:\d+\)/, // With path separators
306
- /\(([^)]*\.js):\d+:\d+\)/, // Simpler format
307
- /([^\/\\]+\.js):\d+:\d+/, // Just filename with line numbers
308
- ]
309
-
310
- for (const pattern of patterns) {
311
- const stackMatch = test.err.stack.match(pattern)
312
- if (stackMatch && stackMatch[1]) {
313
- const absolutePath = stackMatch[1]
314
- const relativePath = absolutePath.replace(process.cwd() + '/', '').replace(/^.*[\/\\]/, '')
315
- filePath = relativePath
316
- break
317
- }
318
- }
319
- }
320
-
321
- // If still unknown, try to extract from test context or use test file pattern
322
- if (filePath === 'unknown') {
323
- // Look for common test file patterns in the test title or fullTitle
324
- const fullTitle = test.fullTitle || test.title
325
- if (fullTitle && fullTitle.includes('checkout')) {
326
- filePath = 'checkout_test.js'
327
- } else if (fullTitle && fullTitle.includes('github')) {
328
- filePath = 'github_test.js'
329
- }
330
- }
331
-
332
- const failedTest = {
333
- title: test.title,
334
- fullTitle: test.fullTitle || test.title,
335
- file: filePath,
336
- uid: test.uid,
337
- timestamp: new Date().toISOString(),
338
- }
339
-
340
- // Add parent suite information
341
- if (test.parent) {
342
- failedTest.suite = test.parent.title
343
- failedTest.suiteFile = test.parent.file
344
- }
345
-
346
- // Add error information if available
347
- if (test.err && options.includeStackTrace) {
348
- failedTest.error = {
349
- message: test.err.message || 'Test failed',
350
- stack: test.err.stack || '',
351
- name: test.err.name || 'Error',
352
- }
353
- }
354
-
355
- // Add metadata if available
356
- if (options.includeMetadata) {
357
- failedTest.metadata = {
358
- tags: test.tags || [],
359
- meta: test.meta || {},
360
- opts: test.opts || {},
361
- duration: test.duration || 0,
362
- retries: test.retries || 0,
363
- }
364
- }
365
-
366
- // Add BDD/Gherkin information if available
367
- if (test.parent && test.parent.feature) {
368
- failedTest.bdd = {
369
- feature: test.parent.feature.name || test.parent.title,
370
- scenario: test.title,
371
- featureFile: test.parent.file,
372
- }
373
- }
374
-
375
- allFailedTests.push(failedTest)
376
- })
377
-
378
- output.print(`Failed Tests Tracker: Collected ${allFailedTests.length - failedTests.length} failed tests from workers`)
379
- }
380
-
381
- if (allFailedTests.length === 0) {
382
- if (options.clearOnSuccess && fs.existsSync(outputPath)) {
383
- try {
384
- fs.unlinkSync(outputPath)
385
- output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
386
- } catch (error) {
387
- output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
388
- }
389
- }
390
- return
391
- }
392
-
393
- // Save failed tests to file
394
- try {
395
- const failedTestsData = {
396
- timestamp: new Date().toISOString(),
397
- totalFailed: allFailedTests.length,
398
- tests: allFailedTests,
399
- }
400
-
401
- // Ensure output directory exists
402
- const dir = path.dirname(outputPath)
403
- if (!fs.existsSync(dir)) {
404
- fs.mkdirSync(dir, { recursive: true })
405
- }
406
-
407
- fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
408
- output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
409
- } catch (error) {
410
- output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
411
- }
412
- })
413
- }