@@ -27,6 +27,23 @@ const FORM = `<!doctype html><html><head><title>Form fixture</title></head><body
2727 </div>
2828</body></html>`
2929
30+ const CLICK_FIXTURE = `<!doctype html><title>Click fixture</title>
31+ <style>body{margin:0;height:2400px}button{position:absolute;left:100px;top:calc(100vh - 100px);width:200px;height:40px}</style>
32+ <button id="target" role="option" onclick="document.body.dataset.clicks = Number(document.body.dataset.clicks) + 1">Choose option</button>
33+ <script>
34+ document.body.dataset.clicks = '0'; document.body.dataset.scrolls = '0';
35+ const mode = new URLSearchParams(location.search).get('mode');
36+ if (mode === 'sticky') {
37+ document.getElementById('target').style.top = '1010px';
38+ document.body.insertAdjacentHTML('beforeend', '<div style="position:fixed;inset:0 0 auto;height:80px;background:white;z-index:2">Sticky header</div>');
39+ scrollTo(0,1000);
40+ }
41+ addEventListener('scroll', () => {
42+ document.body.dataset.scrolls = Number(document.body.dataset.scrolls) + 1;
43+ if (mode === 'menu' && document.body.dataset.armed === 'true') document.getElementById('target')?.remove();
44+ });
45+ </script>`
46+
3047test . describe ( 'browser tools' , ( ) => {
3148 const calls = new Map <
3249 string ,
@@ -56,9 +73,11 @@ test.describe('browser tools', () => {
5673 }
5774 response . writeHead ( 200 , { 'Content-Type' : 'text/html' } )
5875 response . end (
59- path === '/form'
60- ? FORM
61- : '<!doctype html><title>Sim fixture</title><h1>Browser tools fixture</h1>'
76+ path === '/click'
77+ ? CLICK_FIXTURE
78+ : path === '/form'
79+ ? FORM
80+ : '<!doctype html><title>Sim fixture</title><h1>Browser tools fixture</h1>'
6281 )
6382 } )
6483 await new Promise < void > ( ( resolve ) => server . listen ( 0 , '127.0.0.1' , resolve ) )
@@ -78,15 +97,21 @@ test.describe('browser tools', () => {
7897 } ,
7998 } )
8099 window = await app . firstWindow ( )
100+ await app . evaluate ( ( { BrowserWindow } ) =>
101+ BrowserWindow . getAllWindows ( ) [ 0 ] . webContents . setBackgroundThrottling ( false )
102+ )
81103 await expect ( window . getByRole ( 'heading' ) ) . toHaveText ( 'Browser tools fixture' )
82104 await window . evaluate ( async ( scope ) => {
83105 const api = ( globalThis as typeof globalThis & { simDesktop : SimDesktopApi } ) . simDesktop
84106 await api . browserAgent . activateScope ( scope )
85- api . browserAgent . setPanelBounds (
86- { x : 0 , y : 80 , width : innerWidth , height : innerHeight - 80 } ,
87- null ,
88- scope
89- )
107+ const updateBounds = ( ) =>
108+ api . browserAgent . setPanelBounds (
109+ { x : 0 , y : 80 , width : innerWidth , height : innerHeight - 80 } ,
110+ null ,
111+ scope
112+ )
113+ updateBounds ( )
114+ setInterval ( updateBounds , 200 )
90115 } , SCOPE )
91116 } )
92117
@@ -143,6 +168,148 @@ test.describe('browser tools', () => {
143168 } , origin )
144169 }
145170
171+ for ( const mode of [ 'menu' , 'sticky' ] ) {
172+ test ( `clicks a ${ mode } target without losing its identity` , async ( ) => {
173+ const opened = await execute ( 'browser_open_url' , { url : `${ origin } /click?mode=${ mode } ` } )
174+ expect ( opened . ok , opened . error ) . toBe ( true )
175+ await app . evaluate (
176+ async ( { webContents } , { origin, mode } ) => {
177+ const contents = webContents
178+ . getAllWebContents ( )
179+ . find ( ( wc ) => wc . getURL ( ) . startsWith ( `${ origin } /click` ) )
180+ if ( ! contents ) throw new Error ( 'Missing click fixture' )
181+ await contents . executeJavaScript ( `
182+ history.scrollRestoration = 'manual';
183+ document.getElementById('target').style.top = ${ mode === 'sticky' ? '1010' : 'innerHeight - 100' } + 'px';
184+ scrollTo(0, ${ mode === 'sticky' ? '1000' : '0' } );
185+ new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
186+ document.body.dataset.scrolls = '0'; document.body.dataset.armed = 'true'; resolve();
187+ })))
188+ ` )
189+ } ,
190+ { origin, mode }
191+ )
192+ const snapshot = await execute ( 'browser_snapshot' , { } )
193+ expect ( snapshot . ok , snapshot . error ) . toBe ( true )
194+ const outline = ( snapshot . result as { outline : string } ) . outline
195+ const line = outline . split ( '\n' ) . find ( ( line ) => line . includes ( '"Choose option"' ) )
196+ const match = line ?. match ( / \[ r e f = ( \d + ) \] / )
197+ if ( ! match ) throw new Error ( `Missing target: ${ outline } ` )
198+ const result = await execute ( 'browser_click' , { elementId : Number ( match [ 1 ] ) } )
199+ expect ( result . ok , result . error ) . toBe ( true )
200+ const state = await app . evaluate ( async ( { webContents } , origin ) => {
201+ const contents = webContents
202+ . getAllWebContents ( )
203+ . find ( ( wc ) => wc . getURL ( ) . startsWith ( `${ origin } /click` ) )
204+ if ( ! contents ) throw new Error ( 'Missing click fixture' )
205+ return contents . executeJavaScript (
206+ '({clicks:document.body.dataset.clicks,scrolls:document.body.dataset.scrolls,scrollY})'
207+ )
208+ } , origin )
209+ expect ( state . clicks ) . toBe ( '1' )
210+ if ( mode === 'menu' ) expect ( state ) . toMatchObject ( { scrolls : '0' , scrollY : 0 } )
211+ else expect ( state . scrollY ) . toBeLessThan ( 1000 )
212+ } )
213+ }
214+
215+ for ( const mode of [ 'visible' , 'hidden' , 'minimized' ] ) {
216+ test ( `captures a ${ mode } window without changing its state` , async ( ) => {
217+ test . skip (
218+ mode === 'minimized' && process . platform !== 'darwin' ,
219+ 'Requires a window manager with minimize events'
220+ )
221+ await openForm ( )
222+ await app . evaluate ( async ( { BrowserWindow } , mode ) => {
223+ const win = BrowserWindow . getAllWindows ( ) [ 0 ]
224+ win . blur ( )
225+ if ( mode === 'hidden' ) win . hide ( )
226+ if ( mode === 'minimized' ) {
227+ const minimized = new Promise < void > ( ( resolve ) => win . once ( 'minimize' , ( ) => resolve ( ) ) )
228+ win . minimize ( )
229+ await minimized
230+ }
231+ } , mode )
232+ const state = ( ) =>
233+ app . evaluate ( async ( { BrowserWindow, webContents } , origin ) => {
234+ const win = BrowserWindow . getAllWindows ( ) [ 0 ]
235+ const contents = webContents
236+ . getAllWebContents ( )
237+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
238+ if ( ! contents ) throw new Error ( 'Missing screenshot fixture' )
239+ return {
240+ visible : win . isVisible ( ) ,
241+ minimized : win . isMinimized ( ) ,
242+ bounds : win . getBounds ( ) ,
243+ focused : BrowserWindow . getFocusedWindow ( ) ?. id ?? null ,
244+ page : await contents . executeJavaScript (
245+ '({width:innerWidth,height:innerHeight,scrollX,scrollY,html:document.body.innerHTML,focus:document.activeElement?.id})'
246+ ) ,
247+ }
248+ } , origin )
249+ const before = await state ( )
250+ for ( let i = 0 ; i < 3 ; i ++ ) {
251+ const response = await execute ( 'browser_screenshot' , { } )
252+ expect ( response . ok , response . error ) . toBe ( true )
253+ const shot = response . result as {
254+ dataUrl : string
255+ scale : number
256+ viewport : { width : number ; height : number }
257+ }
258+ expect ( shot . dataUrl . length ) . toBeGreaterThan ( 1000 )
259+ expect ( shot . viewport . width ) . toBeGreaterThan ( 0 )
260+ expect ( shot . viewport . height ) . toBeGreaterThan ( 0 )
261+ const image = await app . evaluate ( ( { nativeImage } , dataUrl ) => {
262+ const image = nativeImage . createFromDataURL ( dataUrl )
263+ return { empty : image . isEmpty ( ) , ...image . getSize ( ) }
264+ } , shot . dataUrl )
265+ expect ( image ) . toEqual ( {
266+ empty : false ,
267+ width : Math . round ( shot . viewport . width * shot . scale ) ,
268+ height : Math . round ( shot . viewport . height * shot . scale ) ,
269+ } )
270+ expect ( await state ( ) ) . toEqual ( before )
271+ }
272+ } )
273+ }
274+
275+ test ( 'captures fresh pixels after resizing and repainting the viewport' , async ( ) => {
276+ await openForm ( )
277+ const viewportWidth = ( ) =>
278+ app . evaluate ( async ( { webContents } , origin ) => {
279+ const contents = webContents
280+ . getAllWebContents ( )
281+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
282+ return contents ?. executeJavaScript ( 'innerWidth' )
283+ } , origin )
284+ const beforeWidth = await viewportWidth ( )
285+ await app . evaluate ( ( { BrowserWindow } ) => BrowserWindow . getAllWindows ( ) [ 0 ] . setSize ( 1280 , 900 ) )
286+ await expect . poll ( viewportWidth ) . not . toBe ( beforeWidth )
287+ for ( const color of [ 'red' , 'blue' ] ) {
288+ await app . evaluate (
289+ async ( { webContents } , { origin, color } ) => {
290+ const contents = webContents
291+ . getAllWebContents ( )
292+ . find ( ( wc ) => wc . getURL ( ) === `${ origin } /form` )
293+ if ( ! contents ) throw new Error ( 'Missing screenshot fixture' )
294+ await contents . executeJavaScript (
295+ `document.body.style.background = ${ JSON . stringify ( color ) } ; void 0`
296+ )
297+ } ,
298+ { origin, color }
299+ )
300+ const response = await execute ( 'browser_screenshot' , { } )
301+ expect ( response . ok , response . error ) . toBe ( true )
302+ const shot = response . result as { dataUrl : string }
303+ const pixel = await app . evaluate ( ( { nativeImage } , dataUrl ) => {
304+ const image = nativeImage . createFromDataURL ( dataUrl )
305+ return Array . from ( image . toBitmap ( ) . subarray ( 0 , 4 ) )
306+ } , shot . dataUrl )
307+ const dominant = pixel [ color === 'red' ? 2 : 0 ]
308+ const other = pixel [ color === 'red' ? 0 : 2 ]
309+ expect ( dominant - other , `${ color } : ${ pixel } ` ) . toBeGreaterThan ( 150 )
310+ }
311+ } )
312+
146313 test ( 'opens with references, fills in order, and scrolls a horizontal pane' , async ( ) => {
147314 const ref = await openForm ( )
148315 const fill = await execute ( 'browser_fill_form' , {
0 commit comments