@@ -96,7 +96,7 @@ test.describe('browser tools', () => {
9696
9797 test . beforeEach ( async ( ) => {
9898 app = await electron . launch ( {
99- args : [ '.' ] ,
99+ args : [ process . env . SIM_DESKTOP_E2E_MAIN ?? '.' ] ,
100100 cwd : DESKTOP_DIR ,
101101 env : {
102102 ...process . env ,
@@ -105,9 +105,15 @@ test.describe('browser tools', () => {
105105 } ,
106106 } )
107107 window = await app . firstWindow ( )
108- await app . evaluate ( ( { BrowserWindow } ) =>
109- BrowserWindow . getAllWindows ( ) [ 0 ] . webContents . setBackgroundThrottling ( false )
110- )
108+ await app . evaluate ( ( { app, BrowserWindow } ) => {
109+ const host = BrowserWindow . getAllWindows ( ) [ 0 ]
110+ host . webContents . setBackgroundThrottling ( false )
111+ app . focus ( { steal : true } )
112+ host . focus ( )
113+ } )
114+ await expect
115+ . poll ( ( ) => app . evaluate ( ( { BrowserWindow } ) => BrowserWindow . getAllWindows ( ) [ 0 ] . isFocused ( ) ) )
116+ . toBe ( true )
111117 await expect ( window . getByRole ( 'heading' ) ) . toHaveText ( 'Browser tools fixture' )
112118 await window . evaluate ( async ( scope ) => {
113119 const api = ( globalThis as typeof globalThis & { simDesktop : SimDesktopApi } ) . simDesktop
@@ -303,6 +309,180 @@ test.describe('browser tools', () => {
303309 } )
304310 }
305311
312+ test ( 'recovers a permanently pending native capture without losing the page' , async ( ) => {
313+ await openForm ( )
314+ const before = await app . evaluate ( async ( { webContents } , origin ) => {
315+ const contents = webContents
316+ . getAllWebContents ( )
317+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
318+ if ( ! contents ) throw new Error ( 'Missing capture fixture' )
319+ await contents . executeJavaScript ( `
320+ document.getElementById('name').value = 'Unsaved work';
321+ document.getElementById('name').focus();
322+ ` )
323+ contents . capturePage = ( ) => new Promise ( ( ) => { } )
324+ return { id : contents . id , url : contents . getURL ( ) }
325+ } , origin )
326+ for ( const [ color , dominantChannel ] of [
327+ [ 'rgb(240, 20, 30)' , 0 ] ,
328+ [ 'rgb(30, 40, 230)' , 2 ] ,
329+ [ 'rgb(20, 220, 50)' , 1 ] ,
330+ ] as const ) {
331+ await app . evaluate (
332+ async ( { webContents } , { id, color } ) => {
333+ const contents = webContents . fromId ( id )
334+ if ( ! contents ) throw new Error ( 'Capture fixture was replaced' )
335+ await contents . executeJavaScript ( `
336+ document.body.style.background = ${ JSON . stringify ( color ) } ;
337+ new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
338+ ` )
339+ } ,
340+ { id : before . id , color }
341+ )
342+ const response = await execute ( 'browser_screenshot' , { } )
343+ expect ( response . ok , response . error ) . toBe ( true )
344+ const shot = response . result as { dataUrl : string }
345+ const pixel = await app . evaluate ( ( { nativeImage } , dataUrl ) => {
346+ const bitmap = nativeImage . createFromDataURL ( dataUrl ) . toBitmap ( )
347+ return [ bitmap [ 2 ] , bitmap [ 1 ] , bitmap [ 0 ] ]
348+ } , shot . dataUrl )
349+ expect ( pixel [ dominantChannel ] ) . toBeGreaterThan ( 180 )
350+ for ( let channel = 0 ; channel < 3 ; channel ++ ) {
351+ if ( channel !== dominantChannel )
352+ expect ( pixel [ dominantChannel ] - pixel [ channel ] ) . toBeGreaterThan ( 80 )
353+ }
354+ }
355+ const after = await app . evaluate ( async ( { webContents } , id ) => {
356+ const contents = webContents . fromId ( id )
357+ if ( ! contents ) throw new Error ( 'Capture fixture was replaced' )
358+ return {
359+ id : contents . id ,
360+ url : contents . getURL ( ) ,
361+ page : await contents . executeJavaScript (
362+ `({value:document.getElementById('name').value,focus:document.activeElement.id})`
363+ ) ,
364+ }
365+ } , before . id )
366+ expect ( after ) . toEqual ( { ...before , page : { value : 'Unsaved work' , focus : 'name' } } )
367+ } )
368+
369+ test ( 'maps a fractional narrow crop back to its actual viewport position' , async ( ) => {
370+ await openForm ( )
371+ const target = await app . evaluate ( async ( { webContents } , origin ) => {
372+ const contents = webContents
373+ . getAllWebContents ( )
374+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
375+ if ( ! contents ) throw new Error ( 'Missing crop fixture' )
376+ return contents . executeJavaScript ( `
377+ const button = document.createElement('button');
378+ button.textContent = 'Narrow target';
379+ button.style.cssText = 'position:absolute;left:20.1px;top:60.1px;width:1.1px;height:100px;padding:0;border:0;overflow:hidden';
380+ button.onclick = () => { document.body.dataset.cropClicks = Number(document.body.dataset.cropClicks || 0) + 1 };
381+ document.body.append(button);
382+ const rect = button.getBoundingClientRect();
383+ ({x:rect.x,y:rect.y,width:rect.width,height:rect.height,devicePixelRatio});
384+ ` ) as Promise < {
385+ x : number
386+ y : number
387+ width : number
388+ height : number
389+ devicePixelRatio : number
390+ } >
391+ } , origin )
392+ const snapshot = await execute ( 'browser_snapshot' , { } )
393+ expect ( snapshot . ok , snapshot . error ) . toBe ( true )
394+ const line = ( snapshot . result as { outline : string } ) . outline
395+ . split ( '\n' )
396+ . find ( ( line ) => line . includes ( '"Narrow target"' ) )
397+ const match = line ?. match ( / \[ r e f = ( \d + ) \] / )
398+ if ( ! match ) throw new Error ( 'Missing narrow target reference' )
399+ const response = await execute ( 'browser_screenshot' , { elementId : Number ( match [ 1 ] ) } )
400+ expect ( response . ok , response . error ) . toBe ( true )
401+ const shot = response . result as {
402+ imageSize : { width : number ; height : number }
403+ clip : { x : number ; y : number ; width : number ; height : number }
404+ scale : number
405+ }
406+ expect ( shot . clip . x ) . toBeLessThanOrEqual ( target . x )
407+ expect ( shot . clip . y ) . toBeLessThanOrEqual ( target . y )
408+ expect ( shot . clip . x + shot . clip . width ) . toBeGreaterThanOrEqual ( target . x + target . width )
409+ expect ( shot . clip . y + shot . clip . height ) . toBeGreaterThanOrEqual ( target . y + target . height )
410+ expect ( target . x - shot . clip . x ) . toBeLessThan ( 1 / target . devicePixelRatio )
411+ expect ( target . y - shot . clip . y ) . toBeLessThan ( 1 / target . devicePixelRatio )
412+ expect ( shot . scale ) . toBeCloseTo ( shot . imageSize . width / shot . clip . width )
413+ const clicked = await execute ( 'browser_click_at' , {
414+ x : shot . clip . x + shot . clip . width / 2 ,
415+ y : shot . clip . y + shot . clip . height / 2 ,
416+ } )
417+ expect ( clicked . ok , clicked . error ) . toBe ( true )
418+ const count = await app . evaluate ( async ( { webContents } , origin ) => {
419+ const contents = webContents
420+ . getAllWebContents ( )
421+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
422+ return contents ?. executeJavaScript ( 'document.body.dataset.cropClicks' )
423+ } , origin )
424+ expect ( count ) . toBe ( '1' )
425+ } )
426+
427+ for ( const mode of [ 'hidden' , 'minimized' ] ) {
428+ test ( `recovers a stalled capture after restoring a ${ mode } window` , async ( ) => {
429+ test . skip ( mode === 'minimized' && process . platform !== 'darwin' , 'Requires minimize events' )
430+ await openForm ( )
431+ await app . evaluate (
432+ async ( { BrowserWindow, webContents } , { origin, mode } ) => {
433+ const contents = webContents
434+ . getAllWebContents ( )
435+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
436+ if ( ! contents ) throw new Error ( 'Missing capture fixture' )
437+ contents . capturePage = ( ) => new Promise ( ( ) => { } )
438+ await contents . executeJavaScript ( "document.getElementById('name').value = 'Unsaved work'" )
439+ const win = BrowserWindow . getAllWindows ( ) [ 0 ]
440+ win . blur ( )
441+ if ( mode === 'hidden' ) win . hide ( )
442+ else {
443+ const minimized = new Promise < void > ( ( resolve ) => win . once ( 'minimize' , resolve ) )
444+ win . minimize ( )
445+ await minimized
446+ }
447+ } ,
448+ { origin, mode }
449+ )
450+ const state = ( ) =>
451+ app . evaluate ( async ( { BrowserWindow, webContents } , origin ) => {
452+ const win = BrowserWindow . getAllWindows ( ) [ 0 ]
453+ const contents = webContents
454+ . getAllWebContents ( )
455+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
456+ if ( ! contents ) throw new Error ( 'Missing capture fixture' )
457+ return {
458+ id : contents . id ,
459+ visible : win . isVisible ( ) ,
460+ minimized : win . isMinimized ( ) ,
461+ focused : BrowserWindow . getFocusedWindow ( ) ?. id ?? null ,
462+ bounds : win . getBounds ( ) ,
463+ value : await contents . executeJavaScript ( "document.getElementById('name').value" ) ,
464+ }
465+ } , origin )
466+ const before = await state ( )
467+ const start = Date . now ( )
468+ const hiddenCapture = await execute ( 'browser_screenshot' , { } )
469+ expect ( Date . now ( ) - start ) . toBeLessThan ( 12_000 )
470+ if ( ! hiddenCapture . ok )
471+ expect ( hiddenCapture . error ) . toContain ( 'Screenshot frame capture timed out' )
472+ expect ( await state ( ) ) . toEqual ( before )
473+ await app . evaluate ( ( { BrowserWindow } , mode ) => {
474+ const win = BrowserWindow . getAllWindows ( ) [ 0 ]
475+ if ( mode === 'minimized' ) win . restore ( )
476+ else win . showInactive ( )
477+ } , mode )
478+ for ( let attempt = 0 ; attempt < 2 ; attempt ++ ) {
479+ const response = await execute ( 'browser_screenshot' , { } )
480+ expect ( response . ok , response . error ) . toBe ( true )
481+ }
482+ expect ( await state ( ) ) . toMatchObject ( { id : before . id , value : 'Unsaved work' } )
483+ } )
484+ }
485+
306486 for ( const mode of [ 'visible' , 'hidden' , 'minimized' ] ) {
307487 test ( `captures a ${ mode } window without changing its state` , async ( ) => {
308488 test . skip (
@@ -383,7 +563,8 @@ test.describe('browser tools', () => {
383563 . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
384564 if ( ! contents ) throw new Error ( 'Missing screenshot fixture' )
385565 await contents . executeJavaScript (
386- `document.body.style.background = ${ JSON . stringify ( color ) } ; void 0`
566+ `document.body.style.background = ${ JSON . stringify ( color ) } ;
567+ new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))`
387568 )
388569 } ,
389570 { origin, color }
0 commit comments