codeceptjs 3.7.5-beta.15 → 3.7.5-beta.16

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/bin/codecept.js CHANGED
@@ -296,29 +296,6 @@ program
296
296
 
297
297
  .action(require('../lib/command/run-rerun'))
298
298
 
299
- program
300
- .command('run-failed-tests')
301
- .description('Re-run tests that failed in the previous test run')
302
- .option(commandFlags.config.flag, commandFlags.config.description)
303
- .option(commandFlags.profile.flag, commandFlags.profile.description)
304
- .option(commandFlags.verbose.flag, commandFlags.verbose.description)
305
- .option(commandFlags.debug.flag, commandFlags.debug.description)
306
- .option(commandFlags.steps.flag, commandFlags.steps.description)
307
- .option('-o, --override [value]', 'override current config options')
308
- .option('-f, --file [path]', 'path to failed tests file (default: ./failed-tests.json)')
309
- .option('-g, --grep <pattern>', 'only run failed tests matching <pattern>')
310
- .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
311
- .option('--features', 'run only *.feature files and skip tests')
312
- .option('--tests', 'run only JS test files and skip features')
313
- .option('--colors', 'force enabling of colors')
314
- .option('--no-colors', 'force disabling of colors')
315
- .option('-R, --reporter <name>', 'specify the reporter to use')
316
- .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
317
- .option('--workers <number>', 'run failed tests in parallel using specified number of workers')
318
- .option('--suites', 'parallel execution of suites not single tests (when using --workers)')
319
- .option('--by <strategy>', 'test distribution strategy when using --workers: "test", "suite", or "pool"')
320
- .action(errorHandler(require('../lib/command/run-failed-tests')))
321
-
322
299
  program.on('command:*', cmd => {
323
300
  console.log(`\nUnknown command ${cmd}\n`)
324
301
  program.outputHelp()
@@ -642,6 +642,9 @@ class Playwright extends Helper {
642
642
  async _after() {
643
643
  if (!this.isRunning) return
644
644
 
645
+ // Clear popup state to prevent leakage between tests
646
+ popupStore.clear()
647
+
645
648
  if (this.isElectron) {
646
649
  this.browser.close()
647
650
  this.electronSessions.forEach(session => session.close())
@@ -330,6 +330,9 @@ class Puppeteer extends Helper {
330
330
  async _after() {
331
331
  if (!this.isRunning) return
332
332
 
333
+ // Clear popup state to prevent leakage between tests
334
+ popupStore.clear()
335
+
333
336
  // close other sessions
334
337
  const contexts = this.browser.browserContexts()
335
338
  const defaultCtx = contexts.shift()
@@ -0,0 +1,110 @@
1
+ const event = require('../event')
2
+ const output = require('../output')
3
+ const Config = require('../config')
4
+ const { isNotSet } = require('../utils')
5
+
6
+ const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
+
8
+ /**
9
+ * Priority levels for retry mechanisms (higher number = higher priority)
10
+ * This ensures consistent behavior when multiple retry mechanisms are active
11
+ */
12
+ const RETRY_PRIORITIES = {
13
+ MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
14
+ STEP_PLUGIN: 50, // retryFailedStep plugin
15
+ SCENARIO_CONFIG: 30, // Global scenario retry config
16
+ FEATURE_CONFIG: 20, // Global feature retry config
17
+ HOOK_CONFIG: 10, // Hook retry config - lowest priority
18
+ }
19
+
20
+ /**
21
+ * Enhanced global retry mechanism that coordinates with other retry types
22
+ */
23
+ module.exports = function () {
24
+ event.dispatcher.on(event.suite.before, suite => {
25
+ let retryConfig = Config.get('retry')
26
+ if (!retryConfig) return
27
+
28
+ if (Number.isInteger(+retryConfig)) {
29
+ // is number - apply as feature-level retry
30
+ const retryNum = +retryConfig
31
+ output.log(`[Global Retry] Feature retries: ${retryNum}`)
32
+
33
+ // Only set if not already set by higher priority mechanism
34
+ if (isNotSet(suite.retries())) {
35
+ suite.retries(retryNum)
36
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
37
+ }
38
+ return
39
+ }
40
+
41
+ if (!Array.isArray(retryConfig)) {
42
+ retryConfig = [retryConfig]
43
+ }
44
+
45
+ for (const config of retryConfig) {
46
+ if (config.grep) {
47
+ if (!suite.title.includes(config.grep)) continue
48
+ }
49
+
50
+ // Handle hook retries with priority awareness
51
+ hooks
52
+ .filter(hook => !!config[hook])
53
+ .forEach(hook => {
54
+ const retryKey = `retry${hook}`
55
+ if (isNotSet(suite.opts[retryKey])) {
56
+ suite.opts[retryKey] = config[hook]
57
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
58
+ }
59
+ })
60
+
61
+ // Handle feature-level retries
62
+ if (config.Feature) {
63
+ if (isNotSet(suite.retries()) || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
64
+ suite.retries(config.Feature)
65
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
66
+ output.log(`[Global Retry] Feature retries: ${config.Feature}`)
67
+ }
68
+ }
69
+ }
70
+ })
71
+
72
+ event.dispatcher.on(event.test.before, test => {
73
+ let retryConfig = Config.get('retry')
74
+ if (!retryConfig) return
75
+
76
+ if (Number.isInteger(+retryConfig)) {
77
+ // Only set if not already set by higher priority mechanism
78
+ if (test.retries() === -1) {
79
+ test.retries(retryConfig)
80
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
81
+ output.log(`[Global Retry] Scenario retries: ${retryConfig}`)
82
+ }
83
+ return
84
+ }
85
+
86
+ if (!Array.isArray(retryConfig)) {
87
+ retryConfig = [retryConfig]
88
+ }
89
+
90
+ retryConfig = retryConfig.filter(config => !!config.Scenario)
91
+
92
+ for (const config of retryConfig) {
93
+ if (config.grep) {
94
+ if (!test.fullTitle().includes(config.grep)) continue
95
+ }
96
+
97
+ if (config.Scenario) {
98
+ // Respect priority system
99
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
100
+ test.retries(config.Scenario)
101
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
102
+ output.log(`[Global Retry] Scenario retries: ${config.Scenario}`)
103
+ }
104
+ }
105
+ }
106
+ })
107
+ }
108
+
109
+ // Export priority constants for use by other retry mechanisms
110
+ module.exports.RETRY_PRIORITIES = RETRY_PRIORITIES
package/lib/mocha/test.js CHANGED
@@ -121,7 +121,6 @@ function serializeTest(test, error = null) {
121
121
  }
122
122
 
123
123
  return {
124
- file: test.file ? relativeDir(test.file) : undefined,
125
124
  opts: test.opts || {},
126
125
  tags: test.tags || [],
127
126
  uid: test.uid,
@@ -0,0 +1,99 @@
1
+ const event = require('../event')
2
+ const recorder = require('../recorder')
3
+ const store = require('../store')
4
+ const output = require('../output')
5
+ const { RETRY_PRIORITIES } = require('../retryCoordinator')
6
+
7
+ const defaultConfig = {
8
+ retries: 3,
9
+ defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
10
+ factor: 1.5,
11
+ ignoredSteps: [],
12
+ }
13
+
14
+ /**
15
+ * Enhanced retryFailedStep plugin that coordinates with other retry mechanisms
16
+ *
17
+ * This plugin provides step-level retries and coordinates with global retry settings
18
+ * to avoid conflicts and provide predictable behavior.
19
+ */
20
+ module.exports = config => {
21
+ config = Object.assign({}, defaultConfig, config)
22
+ config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
23
+ const customWhen = config.when
24
+
25
+ let enableRetry = false
26
+
27
+ const when = err => {
28
+ if (!enableRetry) return false
29
+ if (store.debugMode) return false
30
+ if (!store.autoRetries) return false
31
+ if (customWhen) return customWhen(err)
32
+ return true
33
+ }
34
+ config.when = when
35
+
36
+ event.dispatcher.on(event.step.started, step => {
37
+ // if a step is ignored - return
38
+ for (const ignored of config.ignoredSteps) {
39
+ if (step.name === ignored) return
40
+ if (ignored instanceof RegExp) {
41
+ if (step.name.match(ignored)) return
42
+ } else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
43
+ }
44
+ enableRetry = true // enable retry for a step
45
+ })
46
+
47
+ event.dispatcher.on(event.step.finished, () => {
48
+ enableRetry = false
49
+ })
50
+
51
+ event.dispatcher.on(event.test.before, test => {
52
+ // pass disableRetryFailedStep is a preferred way to disable retries
53
+ // test.disableRetryFailedStep is used for backward compatibility
54
+ if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
55
+ store.autoRetries = false
56
+ output.log(`[Step Retry] Disabled for test: ${test.title}`)
57
+ return // disable retry when a test is not active
58
+ }
59
+
60
+ // Check if step retries should be disabled due to higher priority scenario retries
61
+ const scenarioRetries = test.retries()
62
+ const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
63
+ const scenarioPriority = test.opts.retryPriority || 0
64
+
65
+ if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
66
+ // Scenario retries are configured with higher or equal priority
67
+ // Option 1: Disable step retries (conservative approach)
68
+ store.autoRetries = false
69
+ output.log(`[Step Retry] Deferred to scenario retries (${scenarioRetries} retries)`)
70
+ return
71
+
72
+ // Option 2: Reduce step retries to avoid excessive total retries
73
+ // const reducedStepRetries = Math.max(1, Math.floor(config.retries / scenarioRetries))
74
+ // config.retries = reducedStepRetries
75
+ // output.log(`[Step Retry] Reduced to ${reducedStepRetries} retries due to scenario retries (${scenarioRetries})`)
76
+ }
77
+
78
+ // this option is used to set the retries inside _before() block of helpers
79
+ store.autoRetries = true
80
+ test.opts.conditionalRetries = config.retries
81
+ test.opts.stepRetryPriority = stepRetryPriority
82
+
83
+ recorder.retry(config)
84
+
85
+ output.log(`[Step Retry] Enabled with ${config.retries} retries for test: ${test.title}`)
86
+ })
87
+
88
+ // Add coordination info for debugging
89
+ event.dispatcher.on(event.test.finished, test => {
90
+ if (test.state === 'passed' && test.opts.conditionalRetries && store.autoRetries) {
91
+ const stepRetries = test.opts.conditionalRetries || 0
92
+ const scenarioRetries = test.retries() || 0
93
+
94
+ if (stepRetries > 0 && scenarioRetries > 0) {
95
+ output.log(`[Retry Coordination] Test used both step retries (${stepRetries}) and scenario retries (${scenarioRetries})`)
96
+ }
97
+ }
98
+ })
99
+ }
package/lib/recorder.js CHANGED
@@ -11,6 +11,7 @@ let running = false
11
11
  let errFn
12
12
  let queueId = 0
13
13
  let sessionId = null
14
+ let sessionStack = [] // Stack to support nested sessions
14
15
  let asyncErr = null
15
16
  let ignoredErrs = []
16
17
 
@@ -89,6 +90,7 @@ module.exports = {
89
90
  if (promise && running) this.catch()
90
91
  queueId++
91
92
  sessionId = null
93
+ sessionStack = [] // Clear the session stack
92
94
  asyncErr = null
93
95
  log(`${currentQueue()} Starting recording promises`)
94
96
  promise = Promise.resolve()
@@ -123,8 +125,13 @@ module.exports = {
123
125
  */
124
126
  start(name) {
125
127
  if (sessionId) {
126
- debug(`${currentQueue()}Session already started as ${sessionId}`)
127
- this.restore(sessionId)
128
+ debug(`${currentQueue()}Session already started as ${sessionId}, nesting session ${name}`)
129
+ // Push current session to stack instead of restoring it
130
+ sessionStack.push({
131
+ id: sessionId,
132
+ promise: promise,
133
+ running: this.running,
134
+ })
128
135
  }
129
136
  debug(`${currentQueue()}Starting <${name}> session`)
130
137
  tasks.push('--->')
@@ -142,9 +149,18 @@ module.exports = {
142
149
  tasks.push('<---')
143
150
  debug(`${currentQueue()}Finalize <${name}> session`)
144
151
  this.running = false
145
- sessionId = null
146
152
  this.catch(errFn)
147
153
  promise = promise.then(() => oldPromises.pop())
154
+
155
+ // Restore parent session from stack if available
156
+ if (sessionStack.length > 0) {
157
+ const parentSession = sessionStack.pop()
158
+ sessionId = parentSession.id
159
+ this.running = parentSession.running
160
+ debug(`${currentQueue()}Restored parent session <${sessionId}>`)
161
+ } else {
162
+ sessionId = null
163
+ }
148
164
  },
149
165
 
150
166
  /**
@@ -0,0 +1,207 @@
1
+ const output = require('./output')
2
+
3
+ /**
4
+ * Retry Coordinator - Central coordination for all retry mechanisms
5
+ *
6
+ * This module provides:
7
+ * 1. Priority-based retry coordination
8
+ * 2. Unified configuration validation
9
+ * 3. Consolidated retry reporting
10
+ * 4. Conflict detection and resolution
11
+ */
12
+
13
+ /**
14
+ * Priority levels for retry mechanisms (higher number = higher priority)
15
+ */
16
+ const RETRY_PRIORITIES = {
17
+ MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
18
+ STEP_PLUGIN: 50, // retryFailedStep plugin
19
+ SCENARIO_CONFIG: 30, // Global scenario retry config
20
+ FEATURE_CONFIG: 20, // Global feature retry config
21
+ HOOK_CONFIG: 10, // Hook retry config - lowest priority
22
+ }
23
+
24
+ /**
25
+ * Retry mechanism types
26
+ */
27
+ const RETRY_TYPES = {
28
+ MANUAL_STEP: 'manual-step',
29
+ STEP_PLUGIN: 'step-plugin',
30
+ SCENARIO: 'scenario',
31
+ FEATURE: 'feature',
32
+ HOOK: 'hook',
33
+ }
34
+
35
+ /**
36
+ * Global retry coordination state
37
+ */
38
+ let retryState = {
39
+ activeTest: null,
40
+ activeSuite: null,
41
+ retryHistory: [],
42
+ conflicts: [],
43
+ }
44
+
45
+ /**
46
+ * Registers a retry mechanism for coordination
47
+ * @param {string} type - Type of retry mechanism
48
+ * @param {Object} config - Retry configuration
49
+ * @param {Object} target - Target object (test, suite, etc.)
50
+ * @param {number} priority - Priority level
51
+ */
52
+ function registerRetry(type, config, target, priority = 0) {
53
+ const retryInfo = {
54
+ type,
55
+ config,
56
+ target,
57
+ priority,
58
+ timestamp: Date.now(),
59
+ }
60
+
61
+ // Detect conflicts
62
+ const existingRetries = retryState.retryHistory.filter(r => r.target === target && r.type !== type && r.priority !== priority)
63
+
64
+ if (existingRetries.length > 0) {
65
+ const conflict = {
66
+ newRetry: retryInfo,
67
+ existingRetries: existingRetries,
68
+ resolved: false,
69
+ }
70
+ retryState.conflicts.push(conflict)
71
+ handleRetryConflict(conflict)
72
+ }
73
+
74
+ retryState.retryHistory.push(retryInfo)
75
+
76
+ output.log(`[Retry Coordinator] Registered ${type} retry (priority: ${priority})`)
77
+ }
78
+
79
+ /**
80
+ * Handles conflicts between retry mechanisms
81
+ * @param {Object} conflict - Conflict information
82
+ */
83
+ function handleRetryConflict(conflict) {
84
+ const { newRetry, existingRetries } = conflict
85
+
86
+ // Find highest priority retry
87
+ const allRetries = [newRetry, ...existingRetries]
88
+ const highestPriority = Math.max(...allRetries.map(r => r.priority))
89
+ const winningRetry = allRetries.find(r => r.priority === highestPriority)
90
+
91
+ // Log the conflict resolution
92
+ output.log(`[Retry Coordinator] Conflict detected:`)
93
+ allRetries.forEach(retry => {
94
+ const status = retry === winningRetry ? 'ACTIVE' : 'DEFERRED'
95
+ output.log(` - ${retry.type} (priority: ${retry.priority}) [${status}]`)
96
+ })
97
+
98
+ conflict.resolved = true
99
+ conflict.winner = winningRetry
100
+ }
101
+
102
+ /**
103
+ * Gets the effective retry configuration for a target
104
+ * @param {Object} target - Target object (test, suite, etc.)
105
+ * @returns {Object} Effective retry configuration
106
+ */
107
+ function getEffectiveRetryConfig(target) {
108
+ const targetRetries = retryState.retryHistory.filter(r => r.target === target)
109
+
110
+ if (targetRetries.length === 0) {
111
+ return { type: 'none', retries: 0 }
112
+ }
113
+
114
+ // Find highest priority retry
115
+ const highestPriority = Math.max(...targetRetries.map(r => r.priority))
116
+ const effectiveRetry = targetRetries.find(r => r.priority === highestPriority)
117
+
118
+ return {
119
+ type: effectiveRetry.type,
120
+ retries: effectiveRetry.config.retries || effectiveRetry.config,
121
+ config: effectiveRetry.config,
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Generates a retry summary report
127
+ * @returns {Object} Retry summary
128
+ */
129
+ function generateRetrySummary() {
130
+ const summary = {
131
+ totalRetryMechanisms: retryState.retryHistory.length,
132
+ conflicts: retryState.conflicts.length,
133
+ byType: {},
134
+ recommendations: [],
135
+ }
136
+
137
+ // Count by type
138
+ retryState.retryHistory.forEach(retry => {
139
+ summary.byType[retry.type] = (summary.byType[retry.type] || 0) + 1
140
+ })
141
+
142
+ // Generate recommendations
143
+ if (summary.conflicts > 0) {
144
+ summary.recommendations.push('Consider consolidating retry configurations to avoid conflicts')
145
+ }
146
+
147
+ if (summary.byType[RETRY_TYPES.STEP_PLUGIN] && summary.byType[RETRY_TYPES.SCENARIO]) {
148
+ summary.recommendations.push('Step-level and scenario-level retries are both active - consider using only one approach')
149
+ }
150
+
151
+ return summary
152
+ }
153
+
154
+ /**
155
+ * Resets the retry coordination state (useful for testing)
156
+ */
157
+ function reset() {
158
+ retryState = {
159
+ activeTest: null,
160
+ activeSuite: null,
161
+ retryHistory: [],
162
+ conflicts: [],
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Validates retry configuration for common issues
168
+ * @param {Object} config - Configuration object
169
+ * @returns {Array} Array of validation warnings
170
+ */
171
+ function validateConfig(config) {
172
+ const warnings = []
173
+
174
+ if (!config) return warnings
175
+
176
+ // Check for potential configuration conflicts
177
+ if (config.retry && config.plugins && config.plugins.retryFailedStep) {
178
+ const globalRetries = typeof config.retry === 'number' ? config.retry : config.retry.Scenario || config.retry.Feature
179
+ const stepRetries = config.plugins.retryFailedStep.retries || 3
180
+
181
+ if (globalRetries && stepRetries) {
182
+ warnings.push(`Both global retries (${globalRetries}) and step retries (${stepRetries}) are configured - total executions could be ${globalRetries * (stepRetries + 1)}`)
183
+ }
184
+ }
185
+
186
+ // Check for excessive retry counts
187
+ if (config.retry) {
188
+ const retryValues = typeof config.retry === 'number' ? [config.retry] : Object.values(config.retry)
189
+ const maxRetries = Math.max(...retryValues.filter(v => typeof v === 'number'))
190
+
191
+ if (maxRetries > 5) {
192
+ warnings.push(`High retry count detected (${maxRetries}) - consider investigating test stability instead`)
193
+ }
194
+ }
195
+
196
+ return warnings
197
+ }
198
+
199
+ module.exports = {
200
+ RETRY_PRIORITIES,
201
+ RETRY_TYPES,
202
+ registerRetry,
203
+ getEffectiveRetryConfig,
204
+ generateRetrySummary,
205
+ validateConfig,
206
+ reset,
207
+ }
package/lib/utils.js CHANGED
@@ -203,7 +203,7 @@ module.exports.test = {
203
203
  if (fs.existsSync(dataFile)) {
204
204
  break
205
205
  }
206
-
206
+
207
207
  // Use Node.js child_process.spawnSync with platform-specific sleep commands
208
208
  // This avoids busy waiting and allows other processes to run
209
209
  try {
@@ -221,7 +221,7 @@ module.exports.test = {
221
221
  // No-op loop - much lighter than previous approaches
222
222
  }
223
223
  }
224
-
224
+
225
225
  // Exponential backoff: gradually increase polling interval to reduce resource usage
226
226
  pollInterval = Math.min(pollInterval * 1.2, maxPollInterval)
227
227
  }
@@ -576,6 +576,52 @@ module.exports.humanizeString = function (string) {
576
576
  return _result.join(' ').trim()
577
577
  }
578
578
 
579
+ /**
580
+ * Creates a circular-safe replacer function for JSON.stringify
581
+ * @param {string[]} keysToSkip - Keys to skip during serialization to break circular references
582
+ * @returns {Function} Replacer function for JSON.stringify
583
+ */
584
+ function createCircularSafeReplacer(keysToSkip = []) {
585
+ const seen = new WeakSet()
586
+ const defaultSkipKeys = ['parent', 'tests', 'suite', 'root', 'runner', 'ctx']
587
+ const skipKeys = new Set([...defaultSkipKeys, ...keysToSkip])
588
+
589
+ return function (key, value) {
590
+ // Skip specific keys that commonly cause circular references
591
+ if (key && skipKeys.has(key)) {
592
+ return undefined
593
+ }
594
+
595
+ if (value === null || typeof value !== 'object') {
596
+ return value
597
+ }
598
+
599
+ // Handle circular references
600
+ if (seen.has(value)) {
601
+ return `[Circular Reference to ${value.constructor?.name || 'Object'}]`
602
+ }
603
+
604
+ seen.add(value)
605
+ return value
606
+ }
607
+ }
608
+
609
+ /**
610
+ * Safely stringify an object, handling circular references
611
+ * @param {any} obj - Object to stringify
612
+ * @param {string[]} keysToSkip - Additional keys to skip during serialization
613
+ * @param {number} space - Number of spaces for indentation (default: 0)
614
+ * @returns {string} JSON string representation
615
+ */
616
+ module.exports.safeStringify = function (obj, keysToSkip = [], space = 0) {
617
+ try {
618
+ return JSON.stringify(obj, createCircularSafeReplacer(keysToSkip), space)
619
+ } catch (error) {
620
+ // Fallback for any remaining edge cases
621
+ return JSON.stringify({ error: `Failed to serialize: ${error.message}` }, null, space)
622
+ }
623
+ }
624
+
579
625
  module.exports.serializeError = function (error) {
580
626
  if (error) {
581
627
  const { stack, uncaught, message, actual, expected } = error
package/lib/workers.js CHANGED
@@ -310,28 +310,11 @@ class Workers extends EventEmitter {
310
310
  const groups = populateGroups(numberOfWorkers)
311
311
  let groupCounter = 0
312
312
 
313
- // If specific tests are provided (e.g., from run-failed-tests), only include those
314
- const targetTests = this.options && this.options.tests
315
- const exactTestTitles = this.options && this.options.exactTestTitles
316
-
317
313
  mocha.suite.eachTest(test => {
314
+ const i = groupCounter % groups.length
318
315
  if (test) {
319
- let shouldInclude = true
320
-
321
- // If we have specific target tests by UID, only include matching UIDs
322
- if (targetTests && targetTests.length > 0) {
323
- shouldInclude = targetTests.includes(test.uid)
324
- }
325
- // If we have exact test titles, only include tests with exact matching titles
326
- else if (exactTestTitles && exactTestTitles.length > 0) {
327
- shouldInclude = exactTestTitles.some(title => title === test.title)
328
- }
329
-
330
- if (shouldInclude) {
331
- const i = groupCounter % groups.length
332
- groups[i].push(test.uid)
333
- groupCounter++
334
- }
316
+ groups[i].push(test.uid)
317
+ groupCounter++
335
318
  }
336
319
  })
337
320
  return groups
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.7.5-beta.15",
3
+ "version": "3.7.5-beta.16",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -157,7 +157,7 @@
157
157
  "jsdoc-typeof-plugin": "1.0.0",
158
158
  "json-server": "0.17.4",
159
159
  "mochawesome": "^7.1.3",
160
- "playwright": "1.54.1",
160
+ "playwright": "1.55.0",
161
161
  "prettier": "^3.3.2",
162
162
  "puppeteer": "24.15.0",
163
163
  "qrcode-terminal": "0.12.0",
@@ -1,277 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
- const { getConfig, printError, getTestRoot, createOutputDir } = require('./utils')
4
- const Config = require('../config')
5
- const store = require('../store')
6
- const Codecept = require('../codecept')
7
- const output = require('../output')
8
- const Workers = require('../workers')
9
- const { tryOrDefault } = require('../utils')
10
-
11
- module.exports = async function (options) {
12
- // registering options globally to use in config
13
- if (options.profile) {
14
- process.env.profile = options.profile
15
- }
16
- if (options.verbose || options.debug) store.debugMode = true
17
-
18
- const configFile = options.config
19
- let config = getConfig(configFile)
20
-
21
- if (options.override) {
22
- config = Config.append(JSON.parse(options.override))
23
- }
24
-
25
- const testRoot = getTestRoot(configFile)
26
- createOutputDir(config, testRoot)
27
-
28
- // Determine failed tests file path - respect CodeceptJS output directory
29
- const failedTestsFile = options.file || 'failed-tests.json'
30
- const failedTestsPath = path.isAbsolute(failedTestsFile)
31
- ? failedTestsFile
32
- : path.resolve(global.output_dir || './output', failedTestsFile)
33
-
34
- // Check if failed tests file exists
35
- if (!fs.existsSync(failedTestsPath)) {
36
- output.error(`Failed tests file not found: ${failedTestsPath}`)
37
- output.print('Run tests first to generate a failed tests file, or specify a different file with --file option')
38
- process.exitCode = 1
39
- return
40
- }
41
-
42
- let failedTestsData
43
- try {
44
- const fileContent = fs.readFileSync(failedTestsPath, 'utf8')
45
- failedTestsData = JSON.parse(fileContent)
46
- } catch (error) {
47
- output.error(`Failed to read or parse failed tests file: ${error.message}`)
48
- process.exitCode = 1
49
- return
50
- }
51
-
52
- if (!failedTestsData.tests || failedTestsData.tests.length === 0) {
53
- output.print('No failed tests found in the file')
54
- return
55
- }
56
-
57
- output.print(`Found ${failedTestsData.tests.length} failed tests from ${failedTestsData.timestamp}`)
58
-
59
- // Debug: Show what's in the failed tests data
60
- if (options.verbose) {
61
- output.print('\nFailed tests data structure:')
62
- failedTestsData.tests.forEach((test, index) => {
63
- output.print(` ${index + 1}. Title: "${test.title}", UID: "${test.uid}", File: "${test.file}"`)
64
- })
65
- output.print('')
66
- }
67
-
68
- // Build test patterns from failed tests
69
- const testPatterns = []
70
- const testsByFile = new Map()
71
-
72
- // Group tests by file for more efficient execution
73
- failedTestsData.tests.forEach(test => {
74
- if (test.file) {
75
- if (!testsByFile.has(test.file)) {
76
- testsByFile.set(test.file, [])
77
- }
78
- testsByFile.get(test.file).push(test)
79
- }
80
- })
81
-
82
- // Build precise test selection from failed tests
83
- if (testsByFile.size > 0) {
84
- // Use file paths for loading tests
85
- for (const [file, tests] of testsByFile) {
86
- testPatterns.push(file)
87
- }
88
-
89
- // Create a map of exact test titles to target for filtering
90
- const failedTestTitles = new Set(failedTestsData.tests.map(test => test.title).filter(Boolean))
91
-
92
- if (failedTestTitles.size > 0) {
93
- output.print(`Targeting ${failedTestTitles.size} specific failed tests by exact title matching`)
94
- options.exactTestTitles = Array.from(failedTestTitles)
95
- } else {
96
- output.print('No specific test titles found, running all tests in files')
97
- }
98
- } else {
99
- // Fallback: use test titles with exact match grep
100
- const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
101
- if (testTitles.length > 0) {
102
- const grepPattern = testTitles.map(title => `^${title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`).join('|')
103
- options.grep = grepPattern
104
- output.print(`Targeting failed tests by title pattern (no file info available)`)
105
- }
106
- }
107
-
108
- // Check if user wants to run with workers
109
- if (options.workers) {
110
- await runWithWorkers(config, options, testPatterns, failedTestsData)
111
- } else {
112
- await runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot)
113
- }
114
- }
115
-
116
- async function runWithWorkers(config, options, testPatterns, failedTestsData) {
117
- const numberOfWorkers = parseInt(options.workers, 10)
118
- const overrideConfigs = tryOrDefault(() => JSON.parse(options.override || '{}'), {})
119
-
120
- // Determine test split strategy
121
- let by = 'test' // default for failed tests
122
- if (options.by) {
123
- by = options.by
124
- } else if (options.suites) {
125
- by = 'suite'
126
- }
127
-
128
- // Validate the by option
129
- const validStrategies = ['test', 'suite', 'pool']
130
- if (!validStrategies.includes(by)) {
131
- throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
132
- }
133
-
134
- const workerConfig = {
135
- by,
136
- testConfig: options.config,
137
- options,
138
- selectedRuns: undefined,
139
- }
140
-
141
- // If we have specific test UIDs, override the worker test selection
142
- if (options.tests && options.tests.length > 0) {
143
- workerConfig.by = 'test' // Force test-level distribution for precise targeting
144
- output.print(`Using precise test UID targeting for ${options.tests.length} failed tests`)
145
- }
146
-
147
- output.print(`CodeceptJS v${require('../codecept').version()}`)
148
- output.print(`Re-running ${failedTestsData.tests.length} failed tests in ${output.styles.bold(numberOfWorkers)} workers...`)
149
- output.print()
150
- store.hasWorkers = true
151
-
152
- const workers = new Workers(numberOfWorkers, workerConfig)
153
- workers.overrideConfig(overrideConfigs)
154
-
155
- // Set up event listeners for worker output
156
- workers.on('test.failed', test => {
157
- output.test.failed(test)
158
- })
159
-
160
- workers.on('test.passed', test => {
161
- output.test.passed(test)
162
- })
163
-
164
- workers.on('test.skipped', test => {
165
- output.test.skipped(test)
166
- })
167
-
168
- workers.on('all.result', result => {
169
- workers.printResults()
170
- })
171
-
172
- try {
173
- if (options.verbose || options.debug) store.debugMode = true
174
-
175
- if (options.verbose) {
176
- output.print('\nFailed tests to re-run with workers:')
177
- failedTestsData.tests.forEach((test, index) => {
178
- output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
179
- if (test.error && test.error.message) {
180
- output.print(` Error: ${test.error.message}`)
181
- }
182
- })
183
- output.print('')
184
-
185
- const { getMachineInfo } = require('./info')
186
- await getMachineInfo()
187
- }
188
-
189
- await workers.bootstrapAll()
190
- await workers.run()
191
- } catch (err) {
192
- printError(err)
193
- process.exitCode = 1
194
- } finally {
195
- await workers.teardownAll()
196
- }
197
- }
198
-
199
- async function runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot) {
200
- const codecept = new Codecept(config, options)
201
-
202
- try {
203
- codecept.init(testRoot)
204
- await codecept.bootstrap()
205
-
206
- // Load tests - if we have specific patterns, use them, otherwise load all and filter with grep
207
- if (testPatterns.length > 0) {
208
- codecept.loadTests(testPatterns.join(' '))
209
- } else {
210
- codecept.loadTests()
211
- }
212
-
213
- // If we have specific test titles, use exact matching
214
- if (options.exactTestTitles && options.exactTestTitles.length > 0) {
215
- // Store the exact failed tests globally for runtime verification
216
- global.__EXACT_FAILED_TESTS__ = failedTestsData.tests.map(test => ({
217
- fullTitle: test.fullTitle,
218
- stableId: test.stableId,
219
- title: test.title,
220
- file: test.file
221
- }))
222
-
223
- // Override the test loading mechanism to filter tests after loading
224
- const Container = require('../container')
225
- const mocha = Container.mocha()
226
-
227
- // Override Mocha's loadFiles to filter tests after loading
228
- const originalLoadFiles = mocha.loadFiles.bind(mocha)
229
- mocha.loadFiles = function() {
230
- const result = originalLoadFiles()
231
-
232
- // Filter the suite after files are loaded to ensure only exact matches run
233
- const filterSuite = (suite) => {
234
- // Filter direct tests in this suite
235
- suite.tests = suite.tests.filter(test => {
236
- const testFullTitle = test.fullTitle()
237
- return global.__EXACT_FAILED_TESTS__.some(failedTest =>
238
- failedTest.fullTitle === testFullTitle
239
- )
240
- })
241
-
242
- // Recursively filter child suites
243
- suite.suites.forEach(childSuite => filterSuite(childSuite))
244
-
245
- // Remove empty child suites
246
- suite.suites = suite.suites.filter(childSuite =>
247
- childSuite.tests.length > 0 || childSuite.suites.length > 0
248
- )
249
- }
250
-
251
- filterSuite(this.suite)
252
- return result
253
- }
254
-
255
- output.print(`Filtered to ${failedTestsData.tests.length} specific failed tests using exact matching`)
256
- }
257
-
258
- // Display information about what we're running
259
- if (options.verbose) {
260
- output.print('\nFailed tests to re-run:')
261
- failedTestsData.tests.forEach((test, index) => {
262
- output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
263
- if (test.error && test.error.message) {
264
- output.print(` Error: ${test.error.message}`)
265
- }
266
- })
267
- output.print('')
268
- }
269
-
270
- await codecept.run()
271
- } catch (err) {
272
- printError(err)
273
- process.exitCode = 1
274
- } finally {
275
- await codecept.teardown()
276
- }
277
- }
@@ -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
- }