Skip to content

Commit 701762f

Browse files
Copilotkobenguyent
andcommitted
Changes before error encountered
Co-authored-by: kobenguyent <[email protected]>
1 parent d0c9119 commit 701762f

File tree

14 files changed

+395
-9
lines changed

14 files changed

+395
-9
lines changed

bin/codecept.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ program
166166
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
167167
.option('--shuffle', 'Shuffle the order in which test files run')
168168
.option('--shard <index/total>', 'run only a fraction of tests (e.g., --shard 1/4)')
169+
.option('--save-failed-tests [path]', 'save failed tests to JSON file (default: failed-tests.json)')
170+
.option('--failed-tests <path>', 'run only tests from failed tests JSON file')
169171

170172
// mocha options
171173
.option('--colors', 'force enabling of colors')
@@ -207,6 +209,8 @@ program
207209
.option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
208210
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
209211
.option('-R, --reporter <name>', 'specify the reporter to use')
212+
.option('--save-failed-tests [path]', 'save failed tests to JSON file (default: failed-tests.json)')
213+
.option('--failed-tests <path>', 'run only tests from failed tests JSON file')
210214
.action(errorHandler(require('../lib/command/run-workers')))
211215

212216
program

docs/plugins.md

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,14 @@ I.see('new title', 'h1');
440440

441441
* `config` &#x20;
442442

443+
## consolidateWorkerJsonResults
444+
445+
Consolidates JSON reports from multiple workers into a single HTML report
446+
447+
### Parameters
448+
449+
* `config` &#x20;
450+
443451
## coverage
444452

445453
Dumps code coverage from Playwright/Puppeteer after every test.
@@ -651,6 +659,17 @@ const eachElement = codeceptjs.container.plugins('eachElement');
651659
652660
Returns **([Promise][9]\<any> | [undefined][10])**&#x20;
653661
662+
## enhancedRetryFailedStep
663+
664+
Enhanced retryFailedStep plugin that coordinates with other retry mechanisms
665+
666+
This plugin provides step-level retries and coordinates with global retry settings
667+
to avoid conflicts and provide predictable behavior.
668+
669+
### Parameters
670+
671+
* `config` &#x20;
672+
654673
## fakerTransform
655674
656675
Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests
@@ -719,10 +738,10 @@ HTML Reporter Plugin for CodeceptJS
719738
720739
Generates comprehensive HTML reports showing:
721740
722-
- Test statistics
723-
- Feature/Scenario details
724-
- Individual step results
725-
- Test artifacts (screenshots, etc.)
741+
* Test statistics
742+
* Feature/Scenario details
743+
* Individual step results
744+
* Test artifacts (screenshots, etc.)
726745
727746
## Configuration
728747
@@ -749,7 +768,7 @@ Generates comprehensive HTML reports showing:
749768
750769
### Parameters
751770
752-
- `config` &#x20;
771+
* `config` &#x20;
753772
754773
## pageInfo
755774
@@ -862,6 +881,14 @@ Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
862881
863882
* `config` &#x20;
864883
884+
## safeJsonStringify
885+
886+
Safely serialize data to JSON, handling circular references
887+
888+
### Parameters
889+
890+
* `data` &#x20;
891+
865892
## screenshotOnFail
866893
867894
Creates screenshot on failure. Screenshot is saved into `output` directory.

lib/codecept.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,33 @@ class Codecept {
192192
}
193193
}
194194

195+
/**
196+
* Filter tests to only include failed tests from a failed tests data
197+
*
198+
* @param {Array<object>} failedTests - Array of failed test objects with uid, title, file, etc.
199+
*/
200+
filterByFailedTests(failedTests) {
201+
if (!failedTests || failedTests.length === 0) {
202+
this.testFiles = []
203+
return
204+
}
205+
206+
// Extract unique file paths from failed tests
207+
const failedTestFiles = [...new Set(failedTests.map(test => test.file).filter(Boolean))]
208+
209+
// Filter testFiles to only include files that contain failed tests
210+
this.testFiles = this.testFiles.filter(file => {
211+
const normalizedFile = fsPath.resolve(file)
212+
return failedTestFiles.some(failedFile => {
213+
const normalizedFailedFile = fsPath.resolve(failedFile)
214+
return normalizedFile === normalizedFailedFile
215+
})
216+
})
217+
218+
// Store failed test info for filtering during test execution
219+
this.failedTestsFilter = failedTests
220+
}
221+
195222
/**
196223
* Apply sharding to test files based on shard configuration
197224
*
@@ -246,6 +273,13 @@ class Codecept {
246273
}
247274
mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test)
248275
}
276+
277+
// Apply failed test filtering if specified
278+
if (this.failedTestsFilter && this.failedTestsFilter.length > 0) {
279+
mocha.loadFiles()
280+
this._filterMochaTestsByFailedTests(mocha, this.failedTestsFilter)
281+
}
282+
249283
const done = () => {
250284
event.emit(event.all.result, container.result())
251285
event.emit(event.all.after, this)
@@ -262,6 +296,67 @@ class Codecept {
262296
})
263297
}
264298

299+
/**
300+
* Filter Mocha tests to only include failed tests
301+
*
302+
* @private
303+
* @param {Mocha} mocha - Mocha instance
304+
* @param {Array<object>} failedTests - Array of failed test objects
305+
*/
306+
_filterMochaTestsByFailedTests(mocha, failedTests) {
307+
const failedTestUids = new Set(failedTests.map(t => t.uid).filter(Boolean))
308+
const failedTestTitles = new Set(failedTests.map(t => t.title).filter(Boolean))
309+
const failedTestFullTitles = new Set(failedTests.map(t => t.fullTitle).filter(Boolean))
310+
311+
// Remove tests that are not in the failed tests list
312+
for (const suite of mocha.suite.suites) {
313+
suite.tests = suite.tests.filter(test => {
314+
return failedTestUids.has(test.uid) || failedTestTitles.has(test.title) || failedTestFullTitles.has(test.fullTitle())
315+
})
316+
317+
// Remove nested suites' tests as well
318+
this._filterNestedSuites(suite, failedTestUids, failedTestTitles, failedTestFullTitles)
319+
}
320+
321+
// Clean up empty suites
322+
mocha.suite.suites = mocha.suite.suites.filter(suite => this._hasTests(suite))
323+
}
324+
325+
/**
326+
* Recursively filter nested suites
327+
*
328+
* @private
329+
* @param {Suite} suite - Mocha suite
330+
* @param {Set<string>} failedTestUids - Set of failed test UIDs
331+
* @param {Set<string>} failedTestTitles - Set of failed test titles
332+
* @param {Set<string>} failedTestFullTitles - Set of failed test full titles
333+
*/
334+
_filterNestedSuites(suite, failedTestUids, failedTestTitles, failedTestFullTitles) {
335+
for (const childSuite of suite.suites || []) {
336+
childSuite.tests = childSuite.tests.filter(test => {
337+
return failedTestUids.has(test.uid) || failedTestTitles.has(test.title) || failedTestFullTitles.has(test.fullTitle())
338+
})
339+
340+
this._filterNestedSuites(childSuite, failedTestUids, failedTestTitles, failedTestFullTitles)
341+
}
342+
343+
// Remove empty child suites
344+
suite.suites = suite.suites.filter(childSuite => this._hasTests(childSuite))
345+
}
346+
347+
/**
348+
* Check if suite or any nested suite has tests
349+
*
350+
* @private
351+
* @param {Suite} suite - Mocha suite
352+
* @returns {boolean}
353+
*/
354+
_hasTests(suite) {
355+
if (suite.tests && suite.tests.length > 0) return true
356+
if (suite.suites && suite.suites.some(childSuite => this._hasTests(childSuite))) return true
357+
return false
358+
}
359+
265360
static version() {
266361
return JSON.parse(readFileSync(`${__dirname}/../package.json`, 'utf8')).version
267362
}

lib/command/run-workers.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const output = require('../output')
44
const store = require('../store')
55
const event = require('../event')
66
const Workers = require('../workers')
7+
const fs = require('fs')
8+
const path = require('path')
9+
const container = require('../container')
710

811
module.exports = async function (workerCount, selectedRuns, options) {
912
process.env.profile = options.profile
@@ -27,11 +30,28 @@ module.exports = async function (workerCount, selectedRuns, options) {
2730
throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
2831
}
2932
delete options.parent
33+
34+
// Handle failed tests loading
35+
let failedTestsData = null
36+
if (options.failedTests) {
37+
const failedTestsFile = path.isAbsolute(options.failedTests) ? options.failedTests : path.resolve(options.failedTests)
38+
39+
if (!fs.existsSync(failedTestsFile)) {
40+
throw new Error(`Failed tests file not found: ${failedTestsFile}`)
41+
}
42+
43+
failedTestsData = JSON.parse(fs.readFileSync(failedTestsFile, 'utf8'))
44+
if (!failedTestsData.tests || !Array.isArray(failedTestsData.tests)) {
45+
throw new Error(`Invalid failed tests file format: ${failedTestsFile}`)
46+
}
47+
}
48+
3049
const config = {
3150
by,
3251
testConfig,
3352
options,
3453
selectedRuns,
54+
failedTestsData, // Pass failed tests data to workers
3555
}
3656

3757
const numberOfWorkers = parseInt(workerCount, 10)
@@ -69,6 +89,16 @@ module.exports = async function (workerCount, selectedRuns, options) {
6989
}
7090
await workers.bootstrapAll()
7191
await workers.run()
92+
93+
// Save failed tests if requested
94+
if (options.saveFailedTests !== undefined) {
95+
const result = container.result()
96+
if (result.failedTests.length > 0) {
97+
const fileName = typeof options.saveFailedTests === 'string' ? options.saveFailedTests : 'failed-tests.json'
98+
result.saveFailedTests(fileName)
99+
console.log(`Failed tests saved to: ${fileName}`)
100+
}
101+
}
72102
} catch (err) {
73103
output.error(err)
74104
process.exit(1)

lib/command/run.js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ const { getConfig, printError, getTestRoot, createOutputDir } = require('./utils
22
const Config = require('../config')
33
const store = require('../store')
44
const Codecept = require('../codecept')
5+
const fs = require('fs')
6+
const path = require('path')
7+
const container = require('../container')
58

69
module.exports = async function (test, options) {
710
// registering options globally to use in config
@@ -28,7 +31,26 @@ module.exports = async function (test, options) {
2831
try {
2932
codecept.init(testRoot)
3033
await codecept.bootstrap()
31-
codecept.loadTests(test)
34+
35+
// Handle failed tests file loading
36+
if (options.failedTests) {
37+
const failedTestsFile = path.isAbsolute(options.failedTests) ? options.failedTests : path.join(testRoot, options.failedTests)
38+
39+
if (!fs.existsSync(failedTestsFile)) {
40+
throw new Error(`Failed tests file not found: ${failedTestsFile}`)
41+
}
42+
43+
const failedTestsData = JSON.parse(fs.readFileSync(failedTestsFile, 'utf8'))
44+
if (!failedTestsData.tests || !Array.isArray(failedTestsData.tests)) {
45+
throw new Error(`Invalid failed tests file format: ${failedTestsFile}`)
46+
}
47+
48+
// Load all tests first, then filter to only failed ones
49+
codecept.loadTests(test)
50+
codecept.filterByFailedTests(failedTestsData.tests)
51+
} else {
52+
codecept.loadTests(test)
53+
}
3254

3355
if (options.verbose) {
3456
global.debugMode = true
@@ -37,6 +59,16 @@ module.exports = async function (test, options) {
3759
}
3860

3961
await codecept.run()
62+
63+
// Save failed tests if requested
64+
if (options.saveFailedTests !== undefined) {
65+
const result = container.result()
66+
if (result.failedTests.length > 0) {
67+
const fileName = typeof options.saveFailedTests === 'string' ? options.saveFailedTests : 'failed-tests.json'
68+
result.saveFailedTests(fileName)
69+
console.log(`Failed tests saved to: ${fileName}`)
70+
}
71+
}
4072
} catch (err) {
4173
printError(err)
4274
process.exitCode = 1

lib/command/workers/runTests.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const stderr = ''
2020
// Requiring of Codecept need to be after tty.getWindowSize is available.
2121
const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')
2222

23-
const { options, tests, testRoot, workerIndex, poolMode } = workerData
23+
const { options, tests, testRoot, workerIndex, poolMode, failedTestsData } = workerData
2424

2525
// hide worker output
2626
if (!options.debug && !options.verbose)
@@ -38,6 +38,12 @@ const config = deepMerge(getConfig(options.config || testRoot), overrideConfigs)
3838
const codecept = new Codecept(config, options)
3939
codecept.init(testRoot)
4040
codecept.loadTests()
41+
42+
// Apply failed tests filtering if provided
43+
if (failedTestsData && failedTestsData.tests) {
44+
codecept.filterByFailedTests(failedTestsData.tests)
45+
}
46+
4147
const mocha = container.mocha()
4248

4349
if (poolMode) {

lib/result.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,28 @@ class Result {
138138
fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(this.simplify(), null, 2))
139139
}
140140

141+
/**
142+
* Save failed tests to JSON file
143+
*
144+
* @param {string} fileName
145+
*/
146+
saveFailedTests(fileName) {
147+
if (!fileName) fileName = 'failed-tests.json'
148+
const failedTests = this.failedTests.map(test => ({
149+
uid: test.uid,
150+
title: test.title,
151+
fullTitle: test.fullTitle ? test.fullTitle() : test.title,
152+
file: test.file,
153+
parent: test.parent ? { title: test.parent.title } : null,
154+
}))
155+
const failedTestsData = {
156+
timestamp: new Date().toISOString(),
157+
count: failedTests.length,
158+
tests: failedTests,
159+
}
160+
fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(failedTestsData, null, 2))
161+
}
162+
141163
/**
142164
* Add stats to result
143165
*

0 commit comments

Comments
 (0)