')
+ 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')
+ })
+ })
+})
diff --git a/test/unit/plugin/screenshotOnFail_test.js b/test/unit/plugin/screenshotOnFail_test.js
index af9b364b0..43a502c2f 100644
--- a/test/unit/plugin/screenshotOnFail_test.js
+++ b/test/unit/plugin/screenshotOnFail_test.js
@@ -22,6 +22,10 @@ describe('screenshotOnFail', () => {
})
})
+ afterEach(() => {
+ event.dispatcher.removeAllListeners(event.test.failed)
+ })
+
it('should remove the . at the end of test title', async () => {
screenshotOnFail({})
event.dispatcher.emit(event.test.failed, createTest('test title.'))
@@ -68,7 +72,8 @@ describe('screenshotOnFail', () => {
await recorder.promise()
expect(screenshotSaved.called).is.ok
const fileName = screenshotSaved.getCall(0).args[0]
- const regexpFileName = /test1_[0-9]{10}.failed.png/
+ const regexpFileName = /test1_[0-9]{13}.failed.png/
+
expect(fileName.match(regexpFileName).length).is.equal(1)
})
@@ -123,5 +128,103 @@ describe('screenshotOnFail', () => {
const screenshotFileName = screenshotSaved.getCall(0).args[0]
expect(spy.getCall(0).args[1]).to.equal(screenshotFileName)
})
+
+ describe('Data() scenarios', () => {
+ let savedFilenames = []
+
+ beforeEach(() => {
+ savedFilenames = []
+
+ // Override screenshotSaved to capture filenames
+ screenshotSaved = sinon.stub().callsFake(filename => {
+ savedFilenames.push(filename)
+ return Promise.resolve()
+ })
+
+ container.clear({
+ WebDriver: {
+ options: {},
+ saveScreenshot: screenshotSaved,
+ },
+ })
+ })
+
+ afterEach(() => {
+ event.dispatcher.removeAllListeners(event.test.failed)
+ })
+
+ it('should generate unique screenshot names for Data() iterations with uniqueScreenshotNames: true', async () => {
+ screenshotOnFail({ uniqueScreenshotNames: true })
+
+ // Simulate Data() test scenario - same test title, different data
+ const dataScenario1 = createTest('test something | {"nr":"1","url":"http://codecept.io"}')
+ const dataScenario2 = createTest('test something | {"nr":"2","url":"http://playwright.dev"}')
+
+ // Both tests don't have uid (typical for Data() scenarios)
+ dataScenario1.uid = null
+ dataScenario2.uid = null
+
+ // Use fake timers to control timing but allow small progression
+ const clock = sinon.useFakeTimers(1731340123000)
+
+ // Emit first failure
+ event.dispatcher.emit(event.test.failed, dataScenario1)
+ await recorder.promise()
+
+ // Advance time slightly (simulate quick succession like Data() iterations)
+ clock.tick(100) // 100ms later
+
+ // Emit second failure
+ event.dispatcher.emit(event.test.failed, dataScenario2)
+ await recorder.promise()
+
+ clock.restore()
+
+ // Verify both screenshots were attempted
+ expect(screenshotSaved.callCount).to.equal(2)
+
+ // Get the generated filenames
+ const filename1 = savedFilenames[0]
+ const filename2 = savedFilenames[1]
+
+ // Verify filenames are different (no collision)
+ expect(filename1).to.not.equal(filename2, `Screenshot filenames should be unique for Data() iterations. Got: ${filename1} and ${filename2}`)
+
+ // Verify both contain the base test name (without data part)
+ expect(filename1).to.include('test_something')
+ expect(filename2).to.include('test_something')
+
+ // Verify both have unique suffixes (timestamp-based)
+ expect(filename1).to.match(/test_something_[0-9]{13}\.failed\.png/)
+ expect(filename2).to.match(/test_something_[0-9]{13}\.failed\.png/)
+ })
+
+ it('should generate same filename for Data() iterations with uniqueScreenshotNames: false', async () => {
+ screenshotOnFail({ uniqueScreenshotNames: false })
+
+ // Same scenario but without unique names
+ const dataScenario1 = createTest('test something | {"nr":"1","url":"http://codecept.io"}')
+ const dataScenario2 = createTest('test something | {"nr":"2","url":"http://playwright.dev"}')
+
+ // Emit failures
+ event.dispatcher.emit(event.test.failed, dataScenario1)
+ await recorder.promise()
+
+ event.dispatcher.emit(event.test.failed, dataScenario2)
+ await recorder.promise()
+
+ // Verify both screenshots were attempted
+ expect(screenshotSaved.callCount).to.equal(2)
+
+ // Get the generated filenames
+ const filename1 = savedFilenames[0]
+ const filename2 = savedFilenames[1]
+
+ // With uniqueScreenshotNames: false, both should have the same base name
+ expect(filename1).to.equal('test_something.failed.png')
+ expect(filename2).to.equal('test_something.failed.png')
+ })
+ })
+
// TODO: write more tests for different options
})