@@ -27,6 +27,8 @@ interface BeforeActionEvent {
2727 class : string ;
2828 method : string ;
2929 params : Record < string , unknown > ;
30+ pageId ?: string ;
31+ beforeSnapshot ?: string ;
3032 stepId ?: string ;
3133 parentId ?: string ;
3234 stack ?: StackFrame [ ] ;
@@ -36,6 +38,7 @@ interface AfterActionEvent {
3638 type : 'after' ;
3739 callId : string ;
3840 endTime : number ;
41+ afterSnapshot ?: string ;
3942 error ?: { message : string ; stack ?: string } ;
4043 attachments ?: TraceAttachment [ ] ;
4144}
@@ -50,6 +53,30 @@ interface ScreencastFrameEvent {
5053 frameSwapWallTime ?: number ;
5154}
5255
56+ // NodeSnapshot uses Playwright's compact array format:
57+ // string → text node
58+ // [tagName] | [tagName, attrs, ...children] → element node
59+ type NodeSnapshot = string | unknown [ ] ;
60+
61+ interface FrameSnapshotEvent {
62+ type : 'frame-snapshot' ;
63+ snapshot : {
64+ snapshotName : string ;
65+ callId : string ;
66+ pageId : string ;
67+ frameId : string ;
68+ frameUrl : string ;
69+ timestamp : number ;
70+ wallTime : number ;
71+ collectionTime : number ;
72+ doctype : string ;
73+ html : NodeSnapshot ;
74+ resourceOverrides : Array < { url : string ; sha1 : string } > ;
75+ viewport : { width : number ; height : number } ;
76+ isMainFrame : boolean ;
77+ } ;
78+ }
79+
5380interface ErrorEvent {
5481 type : 'error' ;
5582 message : string ;
@@ -75,8 +102,11 @@ type TraceEvent =
75102 | BeforeActionEvent
76103 | AfterActionEvent
77104 | ScreencastFrameEvent
105+ | FrameSnapshotEvent
78106 | ErrorEvent ;
79107
108+ type ScreenshotCapture = { sha1 : string ; width : number ; height : number } ;
109+
80110// ─── Tracer ─────────────────────────────────────────────────────
81111
82112export class Tracer {
@@ -126,7 +156,7 @@ export class Tracer {
126156 return hash ;
127157 }
128158
129- private async captureScreenshot ( ) : Promise < { sha1 : string ; width : number ; height : number } | null > {
159+ private async captureScreenshot ( ) : Promise < ScreenshotCapture | null > {
130160 if ( ! this . driver ) {
131161 return null ;
132162 }
@@ -167,6 +197,47 @@ export class Tracer {
167197 return frames ;
168198 }
169199
200+ private pushScreencastFrame ( shot : ScreenshotCapture ) : void {
201+ this . events . push ( {
202+ type : 'screencast-frame' ,
203+ pageId : 'device@1' ,
204+ sha1 : shot . sha1 ,
205+ width : shot . width ,
206+ height : shot . height ,
207+ timestamp : this . monotonicTime ( ) ,
208+ frameSwapWallTime : Date . now ( ) ,
209+ } ) ;
210+ }
211+
212+ private pushFrameSnapshot ( snapshotName : string , callId : string , shot : ScreenshotCapture ) : void {
213+ this . events . push ( {
214+ type : 'frame-snapshot' ,
215+ snapshot : {
216+ snapshotName,
217+ callId,
218+ pageId : 'device@1' ,
219+ frameId : 'device@1' ,
220+ frameUrl : 'mobilewright://device' ,
221+ timestamp : this . monotonicTime ( ) ,
222+ wallTime : Date . now ( ) ,
223+ collectionTime : 0 ,
224+ doctype : 'html' ,
225+ html : [
226+ 'HTML' , { } ,
227+ [ 'HEAD' , { } ,
228+ [ 'STYLE' , { } , 'body{margin:0;padding:0;background:#000}img{width:100%;height:100%;object-fit:contain;display:block}' ] ,
229+ ] ,
230+ [ 'BODY' , { } ,
231+ [ 'IMG' , { src : 'screenshot.png' } ] ,
232+ ] ,
233+ ] ,
234+ resourceOverrides : [ { url : 'screenshot.png' , sha1 : shot . sha1 } ] ,
235+ viewport : { width : shot . width , height : shot . height } ,
236+ isMainFrame : true ,
237+ } ,
238+ } ) ;
239+ }
240+
170241 async wrapAction < T > (
171242 className : string ,
172243 method : string ,
@@ -176,21 +247,12 @@ export class Tracer {
176247 const callId = this . nextCallId ( ) ;
177248 const stack = this . captureStack ( ) ;
178249
179- // Before screenshot
180- const beforeScreenshot = await this . captureScreenshot ( ) ;
181- if ( beforeScreenshot ) {
182- this . events . push ( {
183- type : 'screencast-frame' ,
184- pageId : 'device@1' ,
185- sha1 : beforeScreenshot . sha1 ,
186- width : beforeScreenshot . width ,
187- height : beforeScreenshot . height ,
188- timestamp : this . monotonicTime ( ) ,
189- frameSwapWallTime : Date . now ( ) ,
190- } ) ;
250+ const beforeShot = await this . captureScreenshot ( ) ;
251+ if ( beforeShot ) {
252+ this . pushScreencastFrame ( beforeShot ) ;
253+ this . pushFrameSnapshot ( `before@${ callId } ` , callId , beforeShot ) ;
191254 }
192255
193- // Before event
194256 this . events . push ( {
195257 type : 'before' ,
196258 callId,
@@ -199,46 +261,32 @@ export class Tracer {
199261 method,
200262 params,
201263 stack,
264+ pageId : 'device@1' ,
265+ ...( beforeShot && { beforeSnapshot : `before@${ callId } ` } ) ,
202266 } ) ;
203267
204268 try {
205269 const result = await fn ( ) ;
206270
207- // After screenshot
208- const afterScreenshot = await this . captureScreenshot ( ) ;
209- if ( afterScreenshot ) {
210- this . events . push ( {
211- type : 'screencast-frame' ,
212- pageId : 'device@1' ,
213- sha1 : afterScreenshot . sha1 ,
214- width : afterScreenshot . width ,
215- height : afterScreenshot . height ,
216- timestamp : this . monotonicTime ( ) ,
217- frameSwapWallTime : Date . now ( ) ,
218- } ) ;
271+ const afterShot = await this . captureScreenshot ( ) ;
272+ if ( afterShot ) {
273+ this . pushScreencastFrame ( afterShot ) ;
274+ this . pushFrameSnapshot ( `after@${ callId } ` , callId , afterShot ) ;
219275 }
220276
221- // After event
222277 this . events . push ( {
223278 type : 'after' ,
224279 callId,
225280 endTime : this . monotonicTime ( ) ,
281+ ...( afterShot && { afterSnapshot : `after@${ callId } ` } ) ,
226282 } ) ;
227283
228284 return result ;
229285 } catch ( error ) {
230- // After screenshot on failure
231- const errorScreenshot = await this . captureScreenshot ( ) ;
232- if ( errorScreenshot ) {
233- this . events . push ( {
234- type : 'screencast-frame' ,
235- pageId : 'device@1' ,
236- sha1 : errorScreenshot . sha1 ,
237- width : errorScreenshot . width ,
238- height : errorScreenshot . height ,
239- timestamp : this . monotonicTime ( ) ,
240- frameSwapWallTime : Date . now ( ) ,
241- } ) ;
286+ const errorShot = await this . captureScreenshot ( ) ;
287+ if ( errorShot ) {
288+ this . pushScreencastFrame ( errorShot ) ;
289+ this . pushFrameSnapshot ( `after@${ callId } ` , callId , errorShot ) ;
242290 }
243291
244292 const err = error instanceof Error ? error : new Error ( String ( error ) ) ;
@@ -247,6 +295,7 @@ export class Tracer {
247295 type : 'after' ,
248296 callId,
249297 endTime : this . monotonicTime ( ) ,
298+ ...( errorShot && { afterSnapshot : `after@${ callId } ` } ) ,
250299 error : {
251300 message : err . message ,
252301 stack : err . stack ,
@@ -262,19 +311,14 @@ export class Tracer {
262311
263312 const zipFile = new yazl . ZipFile ( ) ;
264313
265- // Add trace events as NDJSON
266314 const traceContent = this . events . map ( e => JSON . stringify ( e ) ) . join ( '\n' ) ;
267315 zipFile . addBuffer ( Buffer . from ( traceContent ) , 'trace.trace' ) ;
268-
269- // Add empty network trace
270316 zipFile . addBuffer ( Buffer . from ( '' ) , 'trace.network' ) ;
271317
272- // Add screenshot resources
273318 for ( const [ sha1 , data ] of this . resources ) {
274319 zipFile . addBuffer ( data , `resources/${ sha1 } ` ) ;
275320 }
276321
277- // Write ZIP to disk
278322 await new Promise < void > ( ( resolve , reject ) => {
279323 zipFile . end ( undefined , ( ) => {
280324 const stream = createWriteStream ( outputPath ) ;
0 commit comments