Skip to content

Commit 25c401f

Browse files
authored
fix: prevent Data() screenshot filename collisions with uniqueScreenshotNames (#5299)
1 parent f217ab8 commit 25c401f

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

lib/mocha/test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,16 @@ function cloneTest(test) {
153153
function testToFileName(test, { suffix = '', unique = false } = {}) {
154154
let fileName = test.title
155155

156-
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
157-
if (suffix) fileName = `${fileName}_${suffix}`
158156
// remove tags with empty string (disable for now)
159157
// fileName = fileName.replace(/\@\w+/g, '')
160158
fileName = fileName.slice(0, 100)
161159
if (fileName.indexOf('{') !== -1) {
162160
fileName = fileName.substr(0, fileName.indexOf('{') - 3).trim()
163161
}
162+
163+
// Apply unique suffix AFTER removing data part to ensure uniqueness
164+
if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime())}`
165+
if (suffix) fileName = `${fileName}_${suffix}`
164166
if (test.ctx && test.ctx.test && test.ctx.test.type === 'hook') fileName = clearString(`${test.title}_${test.ctx.test.title}`)
165167
// TODO: add suite title to file name
166168
// if (test.parent && test.parent.title) {

test/unit/plugin/screenshotOnFail_test.js

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('screenshotOnFail', () => {
7272
await recorder.promise()
7373
expect(screenshotSaved.called).is.ok
7474
const fileName = screenshotSaved.getCall(0).args[0]
75-
const regexpFileName = /test1_[0-9]{10}.failed.png/
75+
const regexpFileName = /test1_[0-9]{13}.failed.png/
7676
expect(fileName.match(regexpFileName).length).is.equal(1)
7777
})
7878

@@ -84,7 +84,7 @@ describe('screenshotOnFail', () => {
8484
await recorder.promise()
8585
expect(screenshotSaved.called).is.ok
8686
const fileName = screenshotSaved.getCall(0).args[0]
87-
const regexpFileName = /test1_[0-9]{10}.failed.png/
87+
const regexpFileName = /test1_[0-9]{13}.failed.png/
8888
expect(fileName.match(regexpFileName).length).is.equal(1)
8989
})
9090

@@ -139,5 +139,99 @@ describe('screenshotOnFail', () => {
139139
const screenshotFileName = screenshotSaved.getCall(0).args[0]
140140
expect(spy.getCall(0).args[1]).to.equal(screenshotFileName)
141141
})
142+
143+
describe('Data() scenarios', () => {
144+
let savedFilenames = []
145+
146+
beforeEach(() => {
147+
savedFilenames = []
148+
149+
// Override screenshotSaved to capture filenames
150+
screenshotSaved = sinon.stub().callsFake(filename => {
151+
savedFilenames.push(filename)
152+
return Promise.resolve()
153+
})
154+
155+
container.clear({
156+
WebDriver: {
157+
options: {},
158+
saveScreenshot: screenshotSaved,
159+
},
160+
})
161+
})
162+
163+
it('should generate unique screenshot names for Data() iterations with uniqueScreenshotNames: true', async () => {
164+
screenshotOnFail({ uniqueScreenshotNames: true })
165+
166+
// Simulate Data() test scenario - same test title, different data
167+
const dataScenario1 = createTest('test something | {"nr":"1","url":"http://codecept.io"}')
168+
const dataScenario2 = createTest('test something | {"nr":"2","url":"http://playwright.dev"}')
169+
170+
// Both tests don't have uid (typical for Data() scenarios)
171+
dataScenario1.uid = null
172+
dataScenario2.uid = null
173+
174+
// Use fake timers to control timing but allow small progression
175+
const clock = sinon.useFakeTimers(1731340123000)
176+
177+
// Emit first failure
178+
event.dispatcher.emit(event.test.failed, dataScenario1)
179+
await recorder.promise()
180+
181+
// Advance time slightly (simulate quick succession like Data() iterations)
182+
clock.tick(100) // 100ms later
183+
184+
// Emit second failure
185+
event.dispatcher.emit(event.test.failed, dataScenario2)
186+
await recorder.promise()
187+
188+
clock.restore()
189+
190+
// Verify both screenshots were attempted
191+
expect(screenshotSaved.callCount).to.equal(2)
192+
193+
// Get the generated filenames
194+
const filename1 = savedFilenames[0]
195+
const filename2 = savedFilenames[1]
196+
197+
// Verify filenames are different (no collision)
198+
expect(filename1).to.not.equal(filename2, `Screenshot filenames should be unique for Data() iterations. Got: ${filename1} and ${filename2}`)
199+
200+
// Verify both contain the base test name (without data part)
201+
expect(filename1).to.include('test_something')
202+
expect(filename2).to.include('test_something')
203+
204+
// Verify both have unique suffixes (timestamp-based)
205+
expect(filename1).to.match(/test_something_[0-9]{13}\.failed\.png/)
206+
expect(filename2).to.match(/test_something_[0-9]{13}\.failed\.png/)
207+
})
208+
209+
it('should generate same filename for Data() iterations with uniqueScreenshotNames: false', async () => {
210+
screenshotOnFail({ uniqueScreenshotNames: false })
211+
212+
// Same scenario but without unique names
213+
const dataScenario1 = createTest('test something | {"nr":"1","url":"http://codecept.io"}')
214+
const dataScenario2 = createTest('test something | {"nr":"2","url":"http://playwright.dev"}')
215+
216+
// Emit failures
217+
event.dispatcher.emit(event.test.failed, dataScenario1)
218+
await recorder.promise()
219+
220+
event.dispatcher.emit(event.test.failed, dataScenario2)
221+
await recorder.promise()
222+
223+
// Verify both screenshots were attempted
224+
expect(screenshotSaved.callCount).to.equal(2)
225+
226+
// Get the generated filenames
227+
const filename1 = savedFilenames[0]
228+
const filename2 = savedFilenames[1]
229+
230+
// With uniqueScreenshotNames: false, both should have the same base name
231+
expect(filename1).to.equal('test_something.failed.png')
232+
expect(filename2).to.equal('test_something.failed.png')
233+
})
234+
})
235+
142236
// TODO: write more tests for different options
143237
})

0 commit comments

Comments
 (0)