diff --git a/docs/plugins.md b/docs/plugins.md
index 0489bf097..19b335ada 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -817,7 +817,7 @@ Enable it manually on each run via `-p` option:
## reportData
-TypeScript: Explicitly type reportData arrays as any[] to avoid 'never' errors
+TypeScript: Explicitly type reportData arrays as any\[] to avoid 'never' errors
## retryFailedStep
diff --git a/lib/plugin/htmlReporter.js b/lib/plugin/htmlReporter.js
index 77f7d9133..70daf848a 100644
--- a/lib/plugin/htmlReporter.js
+++ b/lib/plugin/htmlReporter.js
@@ -1538,6 +1538,30 @@ module.exports = function (config) {
function escapeHtml(text) {
if (!text) return ''
+ // Convert non-string values to strings before escaping
+ if (typeof text !== 'string') {
+ // Handle arrays by recursively flattening and joining with commas
+ if (Array.isArray(text)) {
+ // Recursive helper to flatten deeply nested arrays with depth limit to prevent stack overflow
+ const flattenArray = (arr, depth = 0, maxDepth = 100) => {
+ if (depth >= maxDepth) {
+ // Safety limit reached, return string representation
+ return String(arr)
+ }
+ return arr
+ .map(item => {
+ if (Array.isArray(item)) {
+ return flattenArray(item, depth + 1, maxDepth)
+ }
+ return String(item)
+ })
+ .join(', ')
+ }
+ text = flattenArray(text)
+ } else {
+ text = String(text)
+ }
+ }
return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''')
}
@@ -1653,8 +1677,12 @@ module.exports = function (config) {
if (!systemInfo) return ''
const formatInfo = (key, value) => {
+ // Handle array values (e.g., ['Node', '22.14.0', 'path'])
if (Array.isArray(value) && value.length > 1) {
- return `
${key}: ${escapeHtml(value[1])}
`
+ // value[1] might be an array itself (e.g., edgeInfo: ['Edge', ['Chromium (140.0.3485.54)'], 'N/A'])
+ // escapeHtml now handles this, but we can also flatten it here for clarity
+ const displayValue = value[1]
+ return `${key}: ${escapeHtml(displayValue)}
`
} else if (typeof value === 'string' && value !== 'N/A' && value !== 'undefined') {
return `${key}: ${escapeHtml(value)}
`
}
diff --git a/test/unit/plugin/htmlReporter_test.js b/test/unit/plugin/htmlReporter_test.js
new file mode 100644
index 000000000..742d8f13d
--- /dev/null
+++ b/test/unit/plugin/htmlReporter_test.js
@@ -0,0 +1,190 @@
+const { expect } = require('chai')
+
+// Helper function to simulate the escapeHtml behavior from htmlReporter.js
+function escapeHtml(text) {
+ if (!text) return ''
+ // Convert non-string values to strings before escaping
+ if (typeof text !== 'string') {
+ // Handle arrays by recursively flattening and joining with commas
+ if (Array.isArray(text)) {
+ // Recursive helper to flatten deeply nested arrays with depth limit to prevent stack overflow
+ const flattenArray = (arr, depth = 0, maxDepth = 100) => {
+ if (depth >= maxDepth) {
+ // Safety limit reached, return string representation
+ return String(arr)
+ }
+ return arr
+ .map(item => {
+ if (Array.isArray(item)) {
+ return flattenArray(item, depth + 1, maxDepth)
+ }
+ return String(item)
+ })
+ .join(', ')
+ }
+ text = flattenArray(text)
+ } else {
+ text = String(text)
+ }
+ }
+ return text.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''')
+}
+
+describe('htmlReporter plugin', () => {
+ describe('escapeHtml function', () => {
+ it('should escape HTML special characters in strings', () => {
+ const result = escapeHtml('')
+ expect(result).to.include('<script>')
+ expect(result).to.include('"')
+ })
+
+ it('should handle string inputs correctly', () => {
+ const result = escapeHtml('Hello ')
+ expect(result).to.include('Hello <World>')
+ })
+
+ it('should handle array inputs by converting to string', () => {
+ const result = escapeHtml(['Item1', 'Item2', 'Item3'])
+ expect(result).to.include('Item1, Item2, Item3')
+ })
+
+ it('should handle nested arrays by flattening them', () => {
+ // This is the key test case from the issue
+ const result = escapeHtml(['Edge', ['Chromium (140.0.3485.54)'], 'N/A'])
+ expect(result).to.include('Edge')
+ expect(result).to.include('Chromium (140.0.3485.54)')
+ expect(result).to.include('N/A')
+ // Should not crash with "text.replace is not a function"
+ })
+
+ it('should handle deeply nested arrays', () => {
+ const result = escapeHtml(['Level1', ['Level2', ['Level3']], 'End'])
+ expect(result).to.include('Level1')
+ expect(result).to.include('Level2')
+ expect(result).to.include('Level3')
+ expect(result).to.include('End')
+ })
+
+ it('should handle null and undefined inputs', () => {
+ const resultNull = escapeHtml(null)
+ expect(resultNull).to.equal('')
+
+ const resultUndefined = escapeHtml(undefined)
+ expect(resultUndefined).to.equal('')
+ })
+
+ it('should handle empty strings', () => {
+ const result = escapeHtml('')
+ expect(result).to.equal('')
+ })
+
+ it('should handle numbers by converting to strings', () => {
+ const result = escapeHtml(42)
+ expect(result).to.include('42')
+ })
+
+ it('should handle objects by converting to strings', () => {
+ const result = escapeHtml({ key: 'value' })
+ expect(result).to.include('[object Object]')
+ })
+
+ it('should escape all HTML entities in arrays', () => {
+ const result = escapeHtml(['', '"quoted"', "it's", 'A&B'])
+ expect(result).to.include('<div>')
+ expect(result).to.include('"quoted"')
+ expect(result).to.include('it's')
+ expect(result).to.include('A&B')
+ })
+ })
+
+ describe('generateSystemInfoHtml function', () => {
+ it('should handle system info with nested arrays', () => {
+ // This tests the real-world scenario from the issue
+ const systemInfo = {
+ nodeInfo: ['Node', '22.14.0', '~\\AppData\\Local\\fnm_multishells\\19200_1763624547202\\node.EXE'],
+ osInfo: ['OS', 'Windows 10 10.0.19045'],
+ cpuInfo: ['CPU', '(12) x64 12th Gen Intel(R) Core(TM) i5-12500'],
+ chromeInfo: ['Chrome', '142.0.7444.163', 'N/A'],
+ edgeInfo: ['Edge', ['Chromium (140.0.3485.54)'], 'N/A'], // This is the problematic case
+ firefoxInfo: undefined,
+ safariInfo: ['Safari', 'N/A'],
+ playwrightBrowsers: 'chromium: 136.0.7103.25, firefox: 137.0, webkit: 18.4',
+ }
+
+ // Test that processing this system info doesn't crash
+ // We simulate the formatInfo function behavior
+ const formatValue = value => {
+ if (Array.isArray(value) && value.length > 1) {
+ const displayValue = value[1]
+ return escapeHtml(displayValue)
+ } else if (typeof value === 'string') {
+ return value
+ }
+ return ''
+ }
+
+ // Test each system info value
+ expect(formatValue(systemInfo.nodeInfo)).to.include('22.14.0')
+ expect(formatValue(systemInfo.osInfo)).to.include('Windows 10')
+ expect(formatValue(systemInfo.cpuInfo)).to.include('12th Gen')
+ expect(formatValue(systemInfo.chromeInfo)).to.include('142.0.7444.163')
+
+ // The critical test: edgeInfo with nested array should not crash
+ const edgeResult = formatValue(systemInfo.edgeInfo)
+ expect(edgeResult).to.include('Chromium')
+ expect(edgeResult).to.include('140.0.3485.54')
+
+ expect(formatValue(systemInfo.safariInfo)).to.equal('N/A')
+ })
+
+ it('should handle undefined values gracefully', () => {
+ const systemInfo = {
+ firefoxInfo: undefined,
+ }
+
+ const formatValue = value => {
+ if (Array.isArray(value) && value.length > 1) {
+ return 'has value'
+ }
+ return ''
+ }
+
+ expect(formatValue(systemInfo.firefoxInfo)).to.equal('')
+ })
+
+ it('should handle string values directly', () => {
+ const systemInfo = {
+ playwrightBrowsers: 'chromium: 136.0.7103.25, firefox: 137.0, webkit: 18.4',
+ }
+
+ const formatValue = value => {
+ if (typeof value === 'string') {
+ return value
+ }
+ return ''
+ }
+
+ expect(formatValue(systemInfo.playwrightBrowsers)).to.include('chromium')
+ expect(formatValue(systemInfo.playwrightBrowsers)).to.include('firefox')
+ expect(formatValue(systemInfo.playwrightBrowsers)).to.include('webkit')
+ })
+ })
+
+ describe('edge cases', () => {
+ it('should handle arrays with HTML content', () => {
+ const result = escapeHtml([''])
+ expect(result).to.include('<script>')
+ expect(result).to.include('alert("xss")')
+ expect(result).to.include('</script>')
+ })
+
+ it('should handle mixed array types', () => {
+ const result = escapeHtml(['String', 42, true, null, ['nested']])
+ expect(result).to.include('String')
+ expect(result).to.include('42')
+ expect(result).to.include('true')
+ expect(result).to.include('null')
+ expect(result).to.include('nested')
+ })
+ })
+})