diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js
index 97e7d0d0f..cc4c510d1 100644
--- a/lib/plugin/htmlReporter.js
+++ b/lib/plugin/htmlReporter.js
@@ -1,3 +1,7 @@
+// @ts-nocheck
+// TypeScript: Import Node.js types for process, fs, path, etc.
+///
+
const fs = require('fs')
const path = require('path')
const mkdirp = require('mkdirp')
@@ -11,7 +15,7 @@ const output = require('../output')
const Codecept = require('../codecept')
const defaultConfig = {
- output: global.output_dir || './output',
+ output: typeof global !== 'undefined' && global.output_dir ? global.output_dir : './output',
reportFileName: 'report.html',
includeArtifacts: true,
showSteps: true,
@@ -60,6 +64,9 @@ const defaultConfig = {
*/
module.exports = function (config) {
const options = { ...defaultConfig, ...config }
+ /**
+ * TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors
+ */
let reportData = {
stats: {},
tests: [],
@@ -82,8 +89,8 @@ module.exports = function (config) {
// Track overall test execution
event.dispatcher.on(event.all.before, () => {
- reportData.startTime = new Date()
- output.plugin('htmlReporter', 'Starting HTML report generation...')
+ reportData.startTime = new Date().toISOString()
+ output.print('HTML Reporter: Starting HTML report generation...')
})
// Track test start to initialize steps and hooks collection
@@ -138,10 +145,30 @@ module.exports = function (config) {
if (step.htmlReporterStartTime) {
step.duration = Date.now() - step.htmlReporterStartTime
}
+
+ // Serialize args immediately to preserve them through worker serialization
+ let serializedArgs = []
+ if (step.args && Array.isArray(step.args)) {
+ serializedArgs = step.args.map(arg => {
+ try {
+ // Try to convert to JSON-friendly format
+ if (typeof arg === 'string') return arg
+ if (typeof arg === 'number') return arg
+ if (typeof arg === 'boolean') return arg
+ if (arg === null || arg === undefined) return arg
+ // For objects, try to serialize them
+ return JSON.parse(JSON.stringify(arg))
+ } catch (e) {
+ // If serialization fails, convert to string
+ return String(arg)
+ }
+ })
+ }
+
currentTestSteps.push({
name: step.name,
actor: step.actor,
- args: step.args || [],
+ args: serializedArgs,
status: step.failed ? 'failed' : 'success',
duration: step.duration || 0,
})
@@ -211,19 +238,25 @@ module.exports = function (config) {
// Debug logging
output.debug(`HTML Reporter: Test finished - ${test.title}, State: ${test.state}, Retries: ${retryAttempts}`)
- // Detect if this is a BDD/Gherkin test
- const isBddTest = isBddGherkinTest(test, currentSuite)
+ // Detect if this is a BDD/Gherkin test - use test.parent directly instead of currentSuite
+ const suite = test.parent || test.suite || currentSuite
+ const isBddTest = isBddGherkinTest(test, suite)
const steps = isBddTest ? currentBddSteps : currentTestSteps
- const featureInfo = isBddTest ? getBddFeatureInfo(test, currentSuite) : null
+ const featureInfo = isBddTest ? getBddFeatureInfo(test, suite) : null
// Check if this test already exists in reportData.tests (from a previous retry)
const existingTestIndex = reportData.tests.findIndex(t => t.id === testId)
- const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex].state === 'failed'
+ const hasFailedBefore = existingTestIndex >= 0 && reportData.tests[existingTestIndex] && reportData.tests[existingTestIndex].state === 'failed'
const currentlyFailed = test.state === 'failed'
// Debug artifacts collection (but don't process them yet - screenshots may not be ready)
output.debug(`HTML Reporter: Test ${test.title} artifacts at test.finished: ${JSON.stringify(test.artifacts)}`)
+ // Extract parent/suite title before serialization (for worker mode)
+ // This ensures the feature name is preserved when test data is JSON stringified
+ const parentTitle = test.parent?.title || test.suite?.title || (suite && suite.title) || null
+ const suiteTitle = test.suite?.title || (suite && suite.title) || null
+
const testData = {
...test,
id: testId,
@@ -239,11 +272,14 @@ module.exports = function (config) {
uid: test.uid,
isBdd: isBddTest,
feature: featureInfo,
+ // Store parent/suite titles as simple strings for worker mode serialization
+ parentTitle: parentTitle,
+ suiteTitle: suiteTitle,
}
if (existingTestIndex >= 0) {
// Update existing test with final result (including failed state)
- reportData.tests[existingTestIndex] = testData
+ if (existingTestIndex >= 0) reportData.tests[existingTestIndex] = testData
output.debug(`HTML Reporter: Updated existing test - ${test.title}, Final state: ${test.state}`)
} else {
// Add new test
@@ -293,9 +329,9 @@ module.exports = function (config) {
})
// Generate final report
- event.dispatcher.on(event.all.result, result => {
- reportData.endTime = new Date()
- reportData.duration = reportData.endTime - reportData.startTime
+ event.dispatcher.on(event.all.result, async result => {
+ reportData.endTime = new Date().toISOString()
+ reportData.duration = new Date(reportData.endTime).getTime() - new Date(reportData.startTime).getTime()
// Process artifacts now that all async tasks (including screenshots) are complete
output.debug(`HTML Reporter: Processing artifacts for ${reportData.tests.length} tests after all async tasks complete`)
@@ -345,7 +381,11 @@ module.exports = function (config) {
.filter(t => t.state === 'failed')
.map(t => {
const testName = t.title || 'Unknown Test'
- const featureName = t.parent?.title || 'Unknown Feature'
+ // Try to get feature name from BDD, preserved titles (worker mode), or direct access
+ let featureName = t.feature?.name || t.parentTitle || t.suiteTitle || t.parent?.title || t.suite?.title || 'Unknown Feature'
+ if (featureName === 'Unknown Feature' && t.suite && t.suite.feature && t.suite.feature.name) {
+ featureName = t.suite.feature.name
+ }
if (t.err) {
const errorMessage = t.err.message || t.err.toString() || 'Test failed'
@@ -410,15 +450,20 @@ module.exports = function (config) {
// Always overwrite the file with the latest complete data from this worker
// This prevents double-counting when the event is triggered multiple times
fs.writeFileSync(jsonPath, safeJsonStringify(reportData))
- output.print(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`)
+ output.debug(`HTML Reporter: Generated worker JSON results: ${jsonFileName}`)
} catch (error) {
- output.print(`HTML Reporter: Failed to write worker JSON: ${error.message}`)
+ output.debug(`HTML Reporter: Failed to write worker JSON: ${error.message}`)
}
return
}
// Single process mode - generate report normally
- generateHtmlReport(reportData, options)
+ try {
+ await generateHtmlReport(reportData, options)
+ } catch (error) {
+ output.print(`Failed to generate HTML report: ${error.message}`)
+ output.debug(`HTML Reporter error stack: ${error.stack}`)
+ }
// Export stats if configured
if (options.exportStats) {
@@ -542,8 +587,8 @@ module.exports = function (config) {
const testName = replicateTestToFileName(originalTestName)
const featureName = replicateTestToFileName(originalFeatureName)
- output.print(`HTML Reporter: Original test title: "${originalTestName}"`)
- output.print(`HTML Reporter: CodeceptJS filename: "${testName}"`)
+ output.debug(`HTML Reporter: Original test title: "${originalTestName}"`)
+ output.debug(`HTML Reporter: CodeceptJS filename: "${testName}"`)
// Generate possible screenshot names based on CodeceptJS patterns
const possibleNames = [
@@ -567,19 +612,19 @@ module.exports = function (config) {
'failure.jpg',
]
- output.print(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`)
+ output.debug(`HTML Reporter: Checking ${possibleNames.length} possible screenshot names for "${testName}"`)
// Search for screenshots in possible directories
for (const dir of possibleDirs) {
- output.print(`HTML Reporter: Checking directory: ${dir}`)
+ output.debug(`HTML Reporter: Checking directory: ${dir}`)
if (!fs.existsSync(dir)) {
- output.print(`HTML Reporter: Directory does not exist: ${dir}`)
+ output.debug(`HTML Reporter: Directory does not exist: ${dir}`)
continue
}
try {
const files = fs.readdirSync(dir)
- output.print(`HTML Reporter: Found ${files.length} files in ${dir}`)
+ output.debug(`HTML Reporter: Found ${files.length} files in ${dir}`)
// Look for exact matches first
for (const name of possibleNames) {
@@ -587,7 +632,7 @@ module.exports = function (config) {
const fullPath = path.join(dir, name)
if (!screenshots.includes(fullPath)) {
screenshots.push(fullPath)
- output.print(`HTML Reporter: Found screenshot: ${fullPath}`)
+ output.debug(`HTML Reporter: Found screenshot: ${fullPath}`)
}
}
}
@@ -618,16 +663,16 @@ module.exports = function (config) {
const fullPath = path.join(dir, file)
if (!screenshots.includes(fullPath)) {
screenshots.push(fullPath)
- output.print(`HTML Reporter: Found related screenshot: ${fullPath}`)
+ output.debug(`HTML Reporter: Found related screenshot: ${fullPath}`)
}
}
} catch (error) {
// Ignore directory read errors
- output.print(`HTML Reporter: Could not read directory ${dir}: ${error.message}`)
+ output.debug(`HTML Reporter: Could not read directory ${dir}: ${error.message}`)
}
}
} catch (error) {
- output.print(`HTML Reporter: Error collecting screenshots: ${error.message}`)
+ output.debug(`HTML Reporter: Error collecting screenshots: ${error.message}`)
}
return screenshots
@@ -654,7 +699,7 @@ module.exports = function (config) {
const statsPath = path.resolve(reportDir, config.exportStatsPath)
const exportData = {
- timestamp: data.endTime.toISOString(),
+ timestamp: data.endTime, // Already an ISO string
duration: data.duration,
stats: data.stats,
retries: data.retries,
@@ -698,7 +743,7 @@ module.exports = function (config) {
// Add current run to history
history.unshift({
- timestamp: data.endTime.toISOString(),
+ timestamp: data.endTime, // Already an ISO string
duration: data.duration,
stats: data.stats,
retries: data.retries.length,
@@ -725,11 +770,11 @@ module.exports = function (config) {
const jsonFiles = fs.readdirSync(reportDir).filter(file => file.startsWith('worker-') && file.endsWith('-results.json'))
if (jsonFiles.length === 0) {
- output.print('HTML Reporter: No worker JSON results found to consolidate')
+ output.debug('HTML Reporter: No worker JSON results found to consolidate')
return
}
- output.print(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`)
+ output.debug(`HTML Reporter: Found ${jsonFiles.length} worker JSON files to consolidate`)
// Initialize consolidated data structure
const consolidatedData = {
@@ -821,9 +866,9 @@ module.exports = function (config) {
saveTestHistory(consolidatedData, config)
}
- output.print(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`)
+ output.debug(`HTML Reporter: Successfully consolidated ${jsonFiles.length} worker reports`)
} catch (error) {
- output.print(`HTML Reporter: Failed to consolidate worker reports: ${error.message}`)
+ output.debug(`HTML Reporter: Failed to consolidate worker reports: ${error.message}`)
}
}
@@ -844,7 +889,7 @@ module.exports = function (config) {
// Add current run to history for chart display (before saving to file)
const currentRun = {
- timestamp: data.endTime.toISOString(),
+ timestamp: data.endTime, // Already an ISO string
duration: data.duration,
stats: data.stats,
retries: data.retries.length,
@@ -863,7 +908,7 @@ module.exports = function (config) {
const html = template(getHtmlTemplate(), {
title: `CodeceptJS Test Report v${Codecept.version()}`,
- timestamp: data.endTime.toISOString(),
+ timestamp: data.endTime, // Already an ISO string
duration: formatDuration(data.duration),
stats: JSON.stringify(data.stats),
history: JSON.stringify(history),
@@ -929,7 +974,9 @@ module.exports = function (config) {
return tests
.map(test => {
const statusClass = test.state || 'unknown'
- const feature = test.isBdd && test.feature ? test.feature.name : test.parent?.title || 'Unknown Feature'
+ // Use preserved parent/suite titles (for worker mode) or fallback to direct access
+ const feature = test.isBdd && test.feature ? test.feature.name : test.parentTitle || test.suiteTitle || test.parent?.title || test.suite?.title || 'Unknown Feature'
+ // Always try to show steps if available, even for unknown feature
const steps = config.showSteps && test.steps ? (test.isBdd ? generateBddStepsHtml(test.steps) : generateStepsHtml(test.steps)) : ''
const featureDetails = test.isBdd && test.feature ? generateBddFeatureHtml(test.feature) : ''
const hooks = test.hooks && test.hooks.length > 0 ? generateHooksHtml(test.hooks) : ''
@@ -1135,19 +1182,19 @@ module.exports = function (config) {
function generateArtifactsHtml(artifacts, isFailedTest = false) {
if (!artifacts || artifacts.length === 0) {
- output.print(`HTML Reporter: No artifacts found for test`)
+ output.debug(`HTML Reporter: No artifacts found for test`)
return ''
}
- output.print(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`)
- output.print(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`)
+ output.debug(`HTML Reporter: Processing ${artifacts.length} artifacts, isFailedTest: ${isFailedTest}`)
+ output.debug(`HTML Reporter: Artifacts: ${JSON.stringify(artifacts)}`)
// Separate screenshots from other artifacts
const screenshots = []
const otherArtifacts = []
artifacts.forEach(artifact => {
- output.print(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`)
+ output.debug(`HTML Reporter: Processing artifact: ${artifact} (type: ${typeof artifact})`)
// Handle different artifact formats
let artifactPath = artifact
@@ -1162,14 +1209,14 @@ module.exports = function (config) {
// Check if it's a screenshot file
if (typeof artifactPath === 'string' && artifactPath.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i)) {
screenshots.push(artifactPath)
- output.print(`HTML Reporter: Found screenshot: ${artifactPath}`)
+ output.debug(`HTML Reporter: Found screenshot: ${artifactPath}`)
} else {
otherArtifacts.push(artifact)
- output.print(`HTML Reporter: Found other artifact: ${artifact}`)
+ output.debug(`HTML Reporter: Found other artifact: ${artifact}`)
}
})
- output.print(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`)
+ output.debug(`HTML Reporter: Found ${screenshots.length} screenshots and ${otherArtifacts.length} other artifacts`)
let artifactsHtml = ''
@@ -1198,7 +1245,7 @@ module.exports = function (config) {
}
}
- output.print(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
+ output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
return `
@@ -1242,7 +1289,7 @@ module.exports = function (config) {
}
}
- output.print(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
+ output.debug(`HTML Reporter: Screenshot ${filename} -> ${relativePath}`)
return `

`
})
.join('')
@@ -1556,7 +1603,7 @@ module.exports = function (config) {
@@ -2457,6 +2504,10 @@ body {
function getJsScripts() {
return `
+// Go to Top button
+function scrollToTop() {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+}
function toggleTestDetails(testId) {
const details = document.getElementById('details-' + testId);
if (details.style.display === 'none' || details.style.display === '') {
@@ -2939,9 +2990,26 @@ function drawHistoryChart() {
// Initialize charts and filters
document.addEventListener('DOMContentLoaded', function() {
- // Draw charts
- drawPieChart();
- drawHistoryChart();
+ // Draw charts
+ drawPieChart();
+ drawHistoryChart();
+ // Add Go to Top button
+ const goTopBtn = document.createElement('button');
+ goTopBtn.innerText = '↑ Top';
+ goTopBtn.id = 'goTopBtn';
+ goTopBtn.style.position = 'fixed';
+ goTopBtn.style.bottom = '30px';
+ goTopBtn.style.right = '30px';
+ goTopBtn.style.zIndex = '9999';
+ goTopBtn.style.padding = '12px 18px';
+ goTopBtn.style.borderRadius = '50%';
+ goTopBtn.style.background = '#27ae60';
+ goTopBtn.style.color = '#fff';
+ goTopBtn.style.fontSize = '20px';
+ goTopBtn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)';
+ goTopBtn.style.cursor = 'pointer';
+ goTopBtn.onclick = scrollToTop;
+ document.body.appendChild(goTopBtn);
// Set up filter event listeners
document.getElementById('statusFilter').addEventListener('change', applyFilters);
diff --git a/package.json b/package.json
index 7414556bd..45134ebb4 100644
--- a/package.json
+++ b/package.json
@@ -141,7 +141,7 @@
"@pollyjs/core": "6.0.6",
"@types/chai": "5.2.2",
"@types/inquirer": "9.0.9",
- "@types/node": "^24.6.0",
+ "@types/node": "^24.6.2",
"@wdio/sauce-service": "9.12.5",
"@wdio/selenium-standalone-service": "8.15.0",
"@wdio/utils": "9.19.2",
diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js
new file mode 100644
index 000000000..f2ee21035
--- /dev/null
+++ b/test/data/sandbox/configs/html-reporter-plugin/codecept-with-retries.conf.js
@@ -0,0 +1,27 @@
+const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure')
+
+setHeadlessWhen(process.env.HEADLESS)
+setWindowSize(1600, 1200)
+
+exports.config = {
+ tests: './retry_test.js',
+ output: './output',
+ helpers: {
+ FileSystem: {},
+ },
+ plugins: {
+ htmlReporter: {
+ enabled: true,
+ output: './output',
+ reportFileName: 'retry-report.html',
+ includeArtifacts: true,
+ showSteps: true,
+ showRetries: true,
+ },
+ retryFailedStep: {
+ enabled: true,
+ retries: 2,
+ },
+ },
+ name: 'html-reporter-plugin retry tests',
+}
diff --git a/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js b/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js
new file mode 100644
index 000000000..9b5043f08
--- /dev/null
+++ b/test/data/sandbox/configs/html-reporter-plugin/codecept-workers.conf.js
@@ -0,0 +1,34 @@
+const { setHeadlessWhen, setWindowSize } = require('@codeceptjs/configure')
+
+setHeadlessWhen(process.env.HEADLESS)
+setWindowSize(1600, 1200)
+
+exports.config = {
+ tests: './*_test.js',
+ output: './output',
+ helpers: {
+ FileSystem: {},
+ },
+ plugins: {
+ htmlReporter: {
+ enabled: true,
+ output: './output',
+ reportFileName: 'worker-report.html',
+ includeArtifacts: true,
+ showSteps: true,
+ showSkipped: true,
+ showMetadata: true,
+ showTags: true,
+ showRetries: true,
+ exportStats: false,
+ keepHistory: false,
+ },
+ },
+ multiple: {
+ parallel: {
+ chunks: 2,
+ browsers: ['chrome', 'firefox'],
+ },
+ },
+ name: 'html-reporter-plugin worker tests',
+}
diff --git a/test/data/sandbox/configs/html-reporter-plugin/edge-cases_test.js b/test/data/sandbox/configs/html-reporter-plugin/edge-cases_test.js
new file mode 100644
index 000000000..8c0633302
--- /dev/null
+++ b/test/data/sandbox/configs/html-reporter-plugin/edge-cases_test.js
@@ -0,0 +1,42 @@
+Feature('HTML Reporter Edge Cases')
+
+Scenario('test with special characters <>&"\'', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('package.json')
+})
+
+Scenario('test with very long name that should be handled properly without breaking the layout or causing any rendering issues in the HTML report', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('codecept.conf.js')
+})
+
+Scenario('test with unicode characters 测试 🎉 ñoño', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('package.json')
+})
+
+Scenario('@tag1 @tag2 @critical test with multiple tags', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('codecept.conf.js')
+})
+
+Scenario('test with metadata', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('package.json')
+}).tag('@smoke').tag('@regression')
+
+Scenario('test that takes longer to execute', async ({ I }) => {
+ I.amInPath('.')
+ await new Promise(resolve => setTimeout(resolve, 500))
+ I.seeFile('package.json')
+})
+
+Scenario('test with nested error', ({ I }) => {
+ I.amInPath('.')
+ try {
+ throw new Error('Nested error with tags & special chars')
+ } catch (e) {
+ // This will fail
+ I.seeFile('non-existent-file-with-error.txt')
+ }
+})
diff --git a/test/data/sandbox/configs/html-reporter-plugin/retry_test.js b/test/data/sandbox/configs/html-reporter-plugin/retry_test.js
new file mode 100644
index 000000000..1b8631e9e
--- /dev/null
+++ b/test/data/sandbox/configs/html-reporter-plugin/retry_test.js
@@ -0,0 +1,23 @@
+Feature('HTML Reporter Retry Test')
+
+let attemptCounter = 0
+
+Scenario('test that fails first time then passes', ({ I }) => {
+ attemptCounter++
+ I.amInPath('.')
+ if (attemptCounter === 1) {
+ I.seeFile('this-file-does-not-exist.txt') // Will fail first time
+ } else {
+ I.seeFile('package.json') // Will pass on retry
+ }
+})
+
+Scenario('test that always fails even with retries', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('this-will-never-exist.txt')
+})
+
+Scenario('test that passes without retries', ({ I }) => {
+ I.amInPath('.')
+ I.seeFile('codecept.conf.js')
+})
diff --git a/test/runner/html-reporter-plugin_test.js b/test/runner/html-reporter-plugin_test.js
index 56a7a7ef8..03f50beff 100644
--- a/test/runner/html-reporter-plugin_test.js
+++ b/test/runner/html-reporter-plugin_test.js
@@ -166,4 +166,444 @@ describe('CodeceptJS html-reporter-plugin', function () {
done()
})
})
+
+ it('should display correct feature names in worker mode', done => {
+ // Test for the "Unknown Feature" fix when running with workers
+ const runCmd = `${codecept_run} --config ${codecept_dir}/configs/html-reporter-plugin/codecept-workers.conf.js`
+
+ exec(runCmd, (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'worker-report.html')
+ expect(fs.existsSync(reportFile)).toBe(true)
+
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Should NOT contain "Unknown Feature" - all tests should have proper feature names
+ const unknownFeatureMatches = reportContent.match(/data-feature="Unknown Feature"/g)
+ expect(unknownFeatureMatches).toBe(null)
+
+ // Should contain the actual feature names from test suites
+ expect(reportContent).toContain('data-feature="HTML Reporter Test"')
+ expect(reportContent).toContain('data-feature="HTML Reporter Edge Cases"')
+ expect(reportContent).toContain('data-feature="HTML Reporter Retry Test"')
+
+ // Verify all tests have non-empty feature names
+ const featureMatches = reportContent.match(/data-feature="([^"]*)"/g)
+ expect(featureMatches).not.toBe(null)
+ expect(featureMatches.length).toBeGreaterThan(0)
+
+ // Check that no feature is empty or just whitespace
+ featureMatches.forEach(match => {
+ const featureName = match.match(/data-feature="([^"]*)"/)[1]
+ expect(featureName.trim().length).toBeGreaterThan(0)
+ expect(featureName).not.toBe('Unknown Feature')
+ })
+
+ done()
+ })
+ })
+
+ it('should preserve step details for all tests including worker runs', done => {
+ const runCmd = `${codecept_run} --config ${codecept_dir}/configs/html-reporter-plugin/codecept-workers.conf.js`
+
+ exec(runCmd, (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'worker-report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // Check that steps section exists and is populated
+ expect(reportContent).toContain('steps-section')
+ expect(reportContent).toContain('step-item')
+ expect(reportContent).toContain('amInPath')
+ expect(reportContent).toContain('seeFile')
+
+ // CRITICAL: Steps should include ARGUMENTS (the main fix)
+ // Before fix: I.amInPath() - missing argument
+ // After fix: I.amInPath(".") - with argument
+ expect(reportContent).toContain('I.amInPath(".")')
+ expect(reportContent).toContain('I.seeFile("package.json")')
+ expect(reportContent).toContain('I.seeFile("codecept.conf.js")')
+
+ // Verify steps have the complete step-title with arguments
+ const stepTitleMatches = reportContent.match(/
([^<]+)<\/span>/g)
+ expect(stepTitleMatches).not.toBe(null)
+ expect(stepTitleMatches.length).toBeGreaterThan(0)
+
+ // Check that at least some steps have arguments (parentheses with content)
+ const stepsWithArgs = stepTitleMatches.filter(
+ match => match.includes('(') && match.includes(')') && !match.match(/\(\s*\)/), // Not empty parens
+ )
+ expect(stepsWithArgs.length).toBeGreaterThan(0)
+
+ done()
+ })
+ })
+
+ it('should render high-resolution test history chart', done => {
+ exec(config_run_config('codecept-with-history.conf.js'), (err, stdout) => {
+ debug(stdout)
+
+ const reportFile = path.join(`${codecept_dir}/configs/html-reporter-plugin`, 'output', 'report.html')
+ const reportContent = fs.readFileSync(reportFile, 'utf8')
+
+ // CRITICAL: Check for increased canvas resolution from 800x300 to 1600x600
+ expect(reportContent).toMatch(/