@@ -81,6 +81,8 @@ export default function (config) {
8181 let currentTest = null
8282 let testStartTime
8383 let currentUrl = null
84+ let testFailed = false
85+ let firstFailedStepSaved = false
8486
8587 const reportDir = config . output ? path . resolve ( global . codecept_dir , config . output ) : defaultConfig . output
8688
@@ -111,6 +113,8 @@ export default function (config) {
111113 . slice ( 0 , 8 )
112114 dir = path . join ( reportDir , `trace_${ testTitle } _${ uniqueHash } ` )
113115 mkdirp . sync ( dir )
116+ deleteDir ( dir )
117+ mkdirp . sync ( dir )
114118 stepNum = 0
115119 error = null
116120 steps = [ ]
@@ -119,16 +123,88 @@ export default function (config) {
119123 currentTest = test
120124 testStartTime = Date . now ( )
121125 currentUrl = null
126+ testFailed = false
127+ firstFailedStepSaved = false
122128 } )
123129
124- event . dispatcher . on ( event . step . after , step => {
130+ event . dispatcher . on ( event . step . after , async step => {
125131 if ( ! currentTest ) return
126- recorder . add ( 'save ai trace step' , async ( ) => persistStep ( step ) , true )
132+ if ( step . status === 'failed' ) {
133+ testFailed = true
134+ }
135+ if ( step . status === 'queued' && testFailed ) {
136+ output . debug ( `aiTrace: Skipping queued step "${ step . toString ( ) } " - testFailed: ${ testFailed } ` )
137+ return
138+ }
139+ if ( step . status === 'failed' && firstFailedStepSaved ) {
140+ output . debug ( `aiTrace: Skipping failed step "${ step . toString ( ) } " - already handled by step.failed event` )
141+ return
142+ }
143+ recorder . add ( `save artifacts for step ${ step . toString ( ) } ` , async ( ) => {
144+ try {
145+ await persistStep ( step )
146+ } catch ( err ) {
147+ output . debug ( `aiTrace: Error saving step: ${ err . message } ` )
148+ }
149+ } , true )
127150 } )
128151
129- event . dispatcher . on ( event . step . failed , step => {
152+ event . dispatcher . on ( event . step . failed , async step => {
130153 if ( ! currentTest ) return
131- recorder . add ( 'save ai trace failed step' , async ( ) => persistStep ( step ) , true )
154+ if ( step . status === 'queued' && testFailed ) {
155+ output . debug ( `aiTrace: Skipping queued failed step "${ step . toString ( ) } " - testFailed: ${ testFailed } ` )
156+ return
157+ }
158+ if ( firstFailedStepSaved ) {
159+ output . debug ( `aiTrace: Skipping subsequent failed step "${ step . toString ( ) } " - already saved first failed step` )
160+ return
161+ }
162+
163+ const stepKey = step . toString ( )
164+ if ( savedSteps . has ( stepKey ) ) {
165+ const existingStep = steps . find ( s => s . step === stepKey )
166+ if ( ! existingStep ) {
167+ output . debug ( `aiTrace: Step "${ stepKey } " marked as saved but not found in steps array` )
168+ return
169+ }
170+ existingStep . status = 'failed'
171+
172+ try {
173+ await captureArtifactsForStep ( step , existingStep , existingStep . prefix )
174+ } catch ( err ) {
175+ output . debug ( `aiTrace: Error updating failed step: ${ err . message } ` )
176+ }
177+ } else {
178+ if ( stepNum === - 1 ) return
179+ if ( isStepIgnored ( step ) ) return
180+ if ( step . metaStep && step . metaStep . name === 'BeforeSuite' ) return
181+
182+ const stepPrefix = generateStepPrefix ( step , stepNum )
183+ stepNum ++
184+
185+ const stepData = {
186+ step : stepKey ,
187+ status : 'failed' ,
188+ prefix : stepPrefix ,
189+ artifacts : { } ,
190+ meta : { } ,
191+ debugOutput : [ ] ,
192+ }
193+
194+ if ( step . startTime && step . endTime ) {
195+ stepData . meta . duration = ( ( step . endTime - step . startTime ) / 1000 ) . toFixed ( 2 ) + 's'
196+ }
197+
198+ savedSteps . add ( stepKey )
199+ steps . push ( stepData )
200+ firstFailedStepSaved = true
201+
202+ try {
203+ await captureArtifactsForStep ( step , stepData , stepPrefix )
204+ } catch ( err ) {
205+ output . debug ( `aiTrace: Error capturing failed step artifacts: ${ err . message } ` )
206+ }
207+ }
132208 } )
133209
134210 event . dispatcher . on ( event . test . passed , test => {
@@ -152,16 +228,19 @@ export default function (config) {
152228 if ( step . metaStep && step . metaStep . name === 'BeforeSuite' ) return
153229
154230 const stepKey = step . toString ( )
231+
155232 if ( savedSteps . has ( stepKey ) ) {
156233 const existingStep = steps . find ( s => s . step === stepKey )
157234 if ( existingStep && step . status === 'failed' ) {
158235 existingStep . status = 'failed'
236+ step . artifacts = { }
237+ await captureArtifactsForStep ( step , existingStep , existingStep . prefix )
159238 }
160239 return
161240 }
162241 savedSteps . add ( stepKey )
163242
164- const stepPrefix = ` ${ String ( stepNum ) . padStart ( 4 , '0' ) } `
243+ const stepPrefix = generateStepPrefix ( step , stepNum )
165244 stepNum ++
166245
167246 const stepData = {
@@ -182,28 +261,44 @@ export default function (config) {
182261 debugOutput = [ ]
183262 }
184263
264+ await captureArtifactsForStep ( step , stepData , stepPrefix )
265+ steps . push ( stepData )
266+ }
267+
268+ async function captureArtifactsForStep ( step , stepData , stepPrefix ) {
269+ if ( ! step . artifacts ) {
270+ step . artifacts = { }
271+ }
272+
273+ let browserAvailable = true
274+
185275 try {
186- if ( helper . grabCurrentUrl ) {
187- try {
276+ try {
277+ if ( helper . grabCurrentUrl ) {
188278 const url = await helper . grabCurrentUrl ( )
189279 stepData . meta . url = url
190280 currentUrl = url
191- } catch ( err ) {
192- // Ignore URL capture errors
193281 }
282+ } catch ( err ) {
283+ browserAvailable = false
284+ output . debug ( `aiTrace: Browser unavailable, partial artifact capture: ${ err . message } ` )
194285 }
195286
196- // Save screenshot
197287 if ( ! step . artifacts ?. screenshot ) {
198- const screenshotFile = `${ stepPrefix } _screenshot.png`
199- await helper . saveScreenshot ( path . join ( dir , screenshotFile ) , config . fullPageScreenshots )
200- stepData . artifacts . screenshot = screenshotFile
201- } else {
202- stepData . artifacts . screenshot = step . artifacts . screenshot
288+ try {
289+ const screenshotFile = `${ stepPrefix } _screenshot.png`
290+ const screenshotPath = path . join ( dir , screenshotFile )
291+ await helper . saveScreenshot ( screenshotPath , config . fullPageScreenshots )
292+
293+ stepData . artifacts . screenshot = screenshotFile
294+ step . artifacts . screenshot = screenshotPath
295+ } catch ( err ) {
296+ output . debug ( `aiTrace: Could not save screenshot: ${ err . message } ` )
297+ }
203298 }
204299
205300 // Save HTML
206- if ( config . captureHTML && helper . grabSource ) {
301+ if ( config . captureHTML && helper . grabSource && browserAvailable ) {
207302 if ( ! step . artifacts ?. html ) {
208303 try {
209304 const html = await helper . grabSource ( )
@@ -219,7 +314,7 @@ export default function (config) {
219314 }
220315
221316 // Save ARIA snapshot
222- if ( config . captureARIA && helper . grabAriaSnapshot ) {
317+ if ( config . captureARIA && helper . grabAriaSnapshot && browserAvailable ) {
223318 try {
224319 const aria = await helper . grabAriaSnapshot ( )
225320 const ariaFile = `${ stepPrefix } _aria.txt`
@@ -231,7 +326,7 @@ export default function (config) {
231326 }
232327
233328 // Save browser logs
234- if ( config . captureBrowserLogs && helper . grabBrowserLogs ) {
329+ if ( config . captureBrowserLogs && helper . grabBrowserLogs && browserAvailable ) {
235330 try {
236331 const logs = await helper . grabBrowserLogs ( )
237332 const logsFile = `${ stepPrefix } _console.json`
@@ -245,8 +340,6 @@ export default function (config) {
245340 } catch ( err ) {
246341 output . plugin ( `aiTrace: Can't save step artifacts: ${ err } ` )
247342 }
248-
249- steps . push ( stepData )
250343 }
251344
252345 function persist ( test , status ) {
@@ -281,7 +374,9 @@ export default function (config) {
281374 }
282375
283376 steps . forEach ( ( stepData , index ) => {
284- markdown += `${ stepData . step } \n`
377+ const stepAnchor = clearString ( stepData . step ) . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_' ) . slice ( 0 , 50 )
378+ markdown += `### Step ${ index + 1 } : ${ stepData . step } \n`
379+ markdown += `<a id="${ stepAnchor } "></a>\n`
285380
286381 if ( stepData . meta . duration ) {
287382 markdown += ` > duration: ${ stepData . meta . duration } \n`
@@ -343,5 +438,15 @@ export default function (config) {
343438 }
344439 return false
345440 }
346- }
347441
442+ function generateStepPrefix ( step , index ) {
443+ const stepName = step . toString ( )
444+ const cleanedName = clearString ( stepName )
445+ . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, '_' )
446+ . replace ( / _ { 2 , } / g, '_' )
447+ . slice ( 0 , 80 )
448+ . trim ( )
449+
450+ return `${ String ( index ) . padStart ( 4 , '0' ) } _${ cleanedName } `
451+ }
452+ }
0 commit comments