@@ -11,7 +11,8 @@ import {
1111 mutationContext ,
1212 type TraceMutation ,
1313 metadataContext ,
14- type Metadata
14+ type Metadata ,
15+ commandContext
1516} from '../../controller/DataManager.js'
1617
1718import '~icons/mdi/world.js'
@@ -20,15 +21,16 @@ import '../placeholder.js'
2021const MUTATION_SELECTOR = '__mutation-highlight__'
2122
2223function transform ( node : any ) : VNode < { } > {
23- if ( typeof node !== 'object' ) {
24+ if ( typeof node !== 'object' || node === null ) {
25+ // Plain string/number text node — return as-is for Preact to render as text.
2426 return node as VNode < { } >
2527 }
2628
27- const { children, ...props } = node . props
29+ const { children, ...props } = node . props ?? { }
2830 /**
2931 * ToDo(Christian): fix way we collect data on added nodes in script
3032 */
31- if ( ! node . type && children . type ) {
33+ if ( ! node . type && children ? .type ) {
3234 return transform ( children )
3335 }
3436
@@ -44,13 +46,18 @@ const COMPONENT = 'wdio-devtools-browser'
4446export class DevtoolsBrowser extends Element {
4547 #vdom = document . createDocumentFragment ( )
4648 #activeUrl?: string
49+ /** Base64 PNG of the screenshot for the currently selected command, or null. */
50+ #screenshotData: string | null = null
4751
4852 @consume ( { context : metadataContext , subscribe : true } )
4953 metadata : Metadata | undefined = undefined
5054
5155 @consume ( { context : mutationContext , subscribe : true } )
5256 mutations : TraceMutation [ ] = [ ]
5357
58+ @consume ( { context : commandContext , subscribe : true } )
59+ commands : CommandLog [ ] = [ ]
60+
5461 static styles = [
5562 ...Element . styles ,
5663 css `
@@ -112,6 +119,31 @@ export class DevtoolsBrowser extends Element {
112119 border-radius: 0 0 0.5rem 0.5rem;
113120 min-height: 0;
114121 }
122+
123+ .screenshot-overlay {
124+ position: absolute;
125+ inset: 0;
126+ background: #111;
127+ display: flex;
128+ align-items: flex-start;
129+ justify-content: center;
130+ border-radius: 0 0 0.5rem 0.5rem;
131+ overflow: hidden;
132+ }
133+
134+ .screenshot-overlay img {
135+ max-width: 100%;
136+ height: auto;
137+ display: block;
138+ }
139+
140+ .iframe-wrapper {
141+ position: relative;
142+ flex: 1;
143+ min-height: 0;
144+ display: flex;
145+ flex-direction: column;
146+ }
115147 `
116148 ]
117149
@@ -148,9 +180,16 @@ export class DevtoolsBrowser extends Element {
148180 return
149181 }
150182
183+ // viewport may not be serialized yet (race between metadata message and
184+ // first resize event), or may arrive without dimensions — fall back to
185+ // sensible defaults so we never throw.
186+ const viewportWidth = ( metadata . viewport as any ) ?. width || 1280
187+ const viewportHeight = ( metadata . viewport as any ) ?. height || 800
188+ if ( ! viewportWidth || ! viewportHeight ) {
189+ return
190+ }
191+
151192 this . iframe . removeAttribute ( 'style' )
152- const viewportWidth = metadata . viewport . width
153- const viewportHeight = metadata . viewport . height
154193 const frameSize = this . getBoundingClientRect ( )
155194 const headerSize = this . header . getBoundingClientRect ( )
156195
@@ -180,21 +219,13 @@ export class DevtoolsBrowser extends Element {
180219 async #renderCommandScreenshot( command ?: CommandLog ) {
181220 const screenshot = command ?. screenshot
182221 if ( ! screenshot ) {
222+ // Clicking a command that has no screenshot clears any previous overlay.
223+ this . #screenshotData = null
224+ this . requestUpdate ( )
183225 return
184226 }
185-
186- if ( ! this . iframe ) {
187- await this . updateComplete
188- }
189- if ( ! this . iframe ) {
190- return
191- }
192-
193- this . iframe . srcdoc = `
194- <body style="margin:0;background:#111;display:flex;justify-content:center;align-items:flex-start;">
195- <img src="data:image/png;base64,${ screenshot } " style="max-width:100%;height:auto;display:block;" />
196- </body>
197- `
227+ this . #screenshotData = screenshot
228+ this . requestUpdate ( )
198229 }
199230
200231 async #renderNewDocument( doc : SimplifiedVNode , baseUrl : string ) {
@@ -270,7 +301,11 @@ export class DevtoolsBrowser extends Element {
270301
271302 #handleChildListMutation( mutation : TraceMutation ) {
272303 if ( mutation . addedNodes . length === 1 && ! mutation . target ) {
273- const baseUrl = this . metadata ?. url || 'unknown'
304+ // Prefer the URL embedded in the mutation itself (set by the injected script
305+ // at capture time), then fall back to the already-resolved active URL, and
306+ // finally to the context metadata URL. This avoids a race where metadata
307+ // arrives after the first childList mutation fires #renderNewDocument.
308+ const baseUrl = mutation . url || this . #activeUrl || this . metadata ?. url || 'unknown'
274309 this . #renderNewDocument(
275310 mutation . addedNodes [ 0 ] as SimplifiedVNode ,
276311 baseUrl
@@ -389,6 +424,15 @@ export class DevtoolsBrowser extends Element {
389424 this . requestUpdate ( )
390425 }
391426
427+ /** Latest screenshot from any command — auto-updates the preview as tests run. */
428+ get #latestAutoScreenshot( ) : string | null {
429+ if ( ! this . commands ?. length ) return null
430+ for ( let i = this . commands . length - 1 ; i >= 0 ; i -- ) {
431+ if ( this . commands [ i ] . screenshot ) return this . commands [ i ] . screenshot !
432+ }
433+ return null
434+ }
435+
392436 render ( ) {
393437 /**
394438 * render a browser state if it hasn't before
@@ -398,6 +442,12 @@ export class DevtoolsBrowser extends Element {
398442 this . #renderBrowserState( )
399443 }
400444
445+ const hasMutations = this . mutations && this . mutations . length
446+ // Explicit user selection takes priority; fall back to latest auto-screenshot
447+ // so the preview always shows the most recently executed command's state
448+ // (important for Nightwatch mode where there are no DOM mutations).
449+ const displayScreenshot = this . #screenshotData ?? this . #latestAutoScreenshot
450+
401451 return html `
402452 < section
403453 class ="w-full h-full bg-sideBarBackground rounded-lg border-2 border-panelBorder shadow-xl "
@@ -417,11 +467,26 @@ export class DevtoolsBrowser extends Element {
417467 < span class ="truncate "> ${ this . #activeUrl} </ span >
418468 </ div >
419469 </ header >
420- ${ this . mutations && this . mutations . length
421- ? html `< iframe class ="origin-top-left "> </ iframe > `
422- : html `< wdio-devtools-placeholder
423- style ="height: 100% "
424- > </ wdio-devtools-placeholder > ` }
470+ ${ hasMutations
471+ ? html `
472+ < div class ="iframe-wrapper ">
473+ < iframe class ="origin-top-left "> </ iframe >
474+ ${ displayScreenshot
475+ ? html `< div class ="screenshot-overlay ">
476+ < img src ="data:image/png;base64, ${ displayScreenshot } " />
477+ </ div > `
478+ : '' }
479+ </ div > `
480+ : displayScreenshot
481+ ? html `
482+ < div class ="iframe-wrapper ">
483+ < div class ="screenshot-overlay " style ="position:relative;flex:1;min-height:0; ">
484+ < img src ="data:image/png;base64, ${ displayScreenshot } " />
485+ </ div >
486+ </ div > `
487+ : html `< wdio-devtools-placeholder
488+ style ="height: 100% "
489+ > </ wdio-devtools-placeholder > ` }
425490 </ section >
426491 `
427492 }
0 commit comments