@@ -38,6 +38,10 @@ const COMPONENT = 'wdio-devtools-browser'
3838export class DevtoolsBrowser extends Element {
3939 #vdom = document . createDocumentFragment ( )
4040 #activeUrl?: string
41+ #resizeTimer?: number
42+ #boundResize = ( ) => this . #debouncedResize( )
43+ #checkpoints = new Map < number , DocumentFragment > ( )
44+ #checkpointStride = 50
4145
4246 @consume ( { context : metadataContext , subscribe : true } )
4347 metadata : Metadata | undefined = undefined
@@ -84,13 +88,20 @@ export class DevtoolsBrowser extends Element {
8488
8589 async connectedCallback ( ) {
8690 super . connectedCallback ( )
87- window . addEventListener ( 'resize' , this . #setIframeSize . bind ( this ) )
88- window . addEventListener ( 'window-drag' , this . #setIframeSize . bind ( this ) )
91+ window . addEventListener ( 'resize' , this . #boundResize )
92+ window . addEventListener ( 'window-drag' , this . #boundResize )
8993 window . addEventListener ( 'app-mutation-highlight' , this . #highlightMutation. bind ( this ) )
9094 window . addEventListener ( 'app-mutation-select' , ( ev ) => this . #renderBrowserState( ev . detail ) )
9195 await this . updateComplete
9296 }
9397
98+ #debouncedResize( ) {
99+ if ( this . #resizeTimer) {
100+ window . clearTimeout ( this . #resizeTimer)
101+ }
102+ this . #resizeTimer = window . setTimeout ( ( ) => this . #setIframeSize( ) , 80 )
103+ }
104+
94105 #setIframeSize ( ) {
95106 const metadata = this . metadata
96107 if ( ! this . section || ! this . iframe || ! this . header || ! metadata ) {
@@ -155,19 +166,10 @@ export class DevtoolsBrowser extends Element {
155166 }
156167
157168 async #handleMutation ( mutation : TraceMutation ) {
158- if ( ! this . iframe ) {
159- await this . updateComplete
160- }
161-
162- if ( mutation . type === 'attributes' ) {
163- return this . #handleAttributeMutation( mutation )
164- }
165- if ( mutation . type === 'childList' ) {
166- return this . #handleChildListMutation( mutation )
167- }
168- if ( mutation . type === 'characterData' ) {
169- return this . #handleCharacterDataMutation( mutation )
170- }
169+ if ( ! this . iframe ) await this . updateComplete
170+ if ( mutation . type === 'attributes' ) return this . #handleAttributeMutation( mutation )
171+ if ( mutation . type === 'childList' ) return this . #handleChildListMutation( mutation )
172+ if ( mutation . type === 'characterData' ) return this . #handleCharacterDataMutation( mutation )
171173 }
172174
173175 #handleCharacterDataMutation ( mutation : TraceMutation ) {
@@ -180,16 +182,17 @@ export class DevtoolsBrowser extends Element {
180182 }
181183
182184 #handleAttributeMutation ( mutation : TraceMutation ) {
183- if ( ! mutation . attributeName || ! mutation . attributeValue ) {
185+ if ( ! mutation . attributeName ) {
184186 return
185187 }
186-
187188 const el = this . #queryElement( mutation . target ! )
188- if ( ! el ) {
189- return
190- }
189+ if ( ! el ) return
191190
192- el . setAttribute ( mutation . attributeName , mutation . attributeValue || '' )
191+ if ( mutation . attributeValue === undefined || mutation . attributeValue === null ) {
192+ el . removeAttribute ( mutation . attributeName )
193+ } else {
194+ el . setAttribute ( mutation . attributeName , mutation . attributeValue )
195+ }
193196 }
194197
195198 #handleChildListMutation ( mutation : TraceMutation ) {
@@ -259,45 +262,64 @@ export class DevtoolsBrowser extends Element {
259262
260263 async #renderBrowserState ( mutationEntry ?: TraceMutation ) {
261264 const mutations = this . mutations
262- if ( ! mutations || ! mutations . length ) {
263- return
265+ if ( ! mutations ?. length ) return
266+
267+ const targetIndex = mutationEntry ? mutations . indexOf ( mutationEntry ) : 0
268+ if ( targetIndex < 0 ) return
269+
270+ // locate nearest checkpoint (<= targetIndex)
271+ const checkpointIndices = [ ...this . #checkpoints. keys ( ) ] . sort ( ( a , b ) => a - b )
272+ const nearest = checkpointIndices . filter ( i => i <= targetIndex ) . pop ( )
273+
274+ if ( nearest !== undefined ) {
275+ // start from checkpoint clone
276+ this . #vdom = this . #checkpoints. get ( nearest ) ! . cloneNode ( true ) as DocumentFragment
277+ } else {
278+ this . #vdom = document . createDocumentFragment ( )
264279 }
265280
266- const mutationIndex = mutationEntry
267- ? mutations . indexOf ( mutationEntry )
268- : 0
269- this . #vdom = document . createDocumentFragment ( )
270- const rootIndex = mutations
271- . map ( ( m , i ) => [
272- // is document loaded
273- m . addedNodes . length === 1 && Boolean ( m . url ) ,
274- // index
275- i
276- ] as const )
277- . filter ( ( [ isDocLoaded , docLoadedIndex ] ) => isDocLoaded && docLoadedIndex <= mutationIndex )
278- . map ( ( [ , i ] ) => i )
279- . pop ( ) || 0
281+ // find root after checkpoint (initial full doc mutation)
282+ const startIndex = nearest !== undefined ? nearest + 1 : 0
283+ let rootIndex = startIndex
284+ for ( let i = startIndex ; i <= targetIndex ; i ++ ) {
285+ const m = mutations [ i ]
286+ if ( m . addedNodes . length === 1 && Boolean ( m . url ) ) rootIndex = i
287+ }
288+ if ( rootIndex !== startIndex ) {
289+ this . #vdom = document . createDocumentFragment ( )
290+ }
280291
281292 this . #activeUrl = mutations [ rootIndex ] . url || this . metadata ?. url || 'unknown'
282- for ( let i = rootIndex ; i <= mutationIndex ; i ++ ) {
283- await this . #handleMutation( mutations [ i ] ) . catch (
284- ( err ) => console . warn ( `Failed to render mutation: ${ err . message } ` ) )
293+
294+ for ( let i = rootIndex ; i <= targetIndex ; i ++ ) {
295+ try {
296+ await this . #handleMutation( mutations [ i ] )
297+ // create checkpoint
298+ if ( i % this . #checkpointStride === 0 && ! this . #checkpoints. has ( i ) ) {
299+ this . #checkpoints. set ( i , this . #vdom. cloneNode ( true ) as DocumentFragment )
300+ }
301+ } catch ( err : any ) {
302+ console . warn ( `Failed to render mutation ${ i } : ${ err ?. message } ` )
303+ }
285304 }
286305
287- /**
288- * scroll changed element into view
289- */
290- const mutation = mutations [ mutationIndex ]
306+ const mutation = mutations [ targetIndex ]
291307 if ( mutation . target ) {
292308 const el = this . #queryElement( mutation . target )
293- if ( el ) {
294- el . scrollIntoView ( { block : 'center' , inline : 'center' } )
295- }
309+ el ?. scrollIntoView ( { block : 'center' , inline : 'center' } )
296310 }
297311
298312 this . requestUpdate ( )
299313 }
300314
315+ /**
316+ * Public API: jump to mutation index
317+ */
318+ goToMutation ( index : number ) {
319+ const m = this . mutations [ index ]
320+ if ( m ) this . #renderBrowserState( m )
321+ }
322+
301323 render ( ) {
302324 /**
303325 * render a browser state if it hasn't before
0 commit comments