@@ -2,8 +2,9 @@ import { describe, expect, it, vi } from 'vitest'
22
33vi . mock ( 'electron' , ( ) => import ( '@/test/electron-mock' ) )
44
5- import { WebContentsView , type WebFrameMain } from 'electron'
5+ import { nativeImage , type WebContents , WebContentsView , type WebFrameMain } from 'electron'
66import {
7+ captureScreenshot ,
78 clickAt ,
89 ensureInstrumented ,
910 evaluateInIsolatedFrame ,
@@ -482,3 +483,89 @@ describe('browser-agent CDP theme', () => {
482483 } )
483484 } )
484485} )
486+
487+ /**
488+ * The browser panel shows a LIVE view, so a capture must not perturb the page.
489+ * Chromium serves `clip` by applying device-emulation params to the widget and
490+ * syncing visual properties, which the user sees as the page rescaling and
491+ * snapping back. Resolution is bounded on the returned image instead.
492+ */
493+ describe ( 'browser-agent screenshot capture' , ( ) => {
494+ function captureFixture ( imageSize : { width : number ; height : number } | null ) {
495+ const contents = new WebContentsView ( ) . webContents
496+ vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
497+ if ( method === 'Page.getLayoutMetrics' ) {
498+ return Promise . resolve ( { cssLayoutViewport : { clientWidth : 2048 , clientHeight : 1024 } } )
499+ }
500+ if ( method === 'Page.captureScreenshot' ) return Promise . resolve ( { data : 'c2lt' } )
501+ return Promise . resolve ( undefined )
502+ } )
503+ const resized = {
504+ toJPEG : vi . fn ( ( ) => Buffer . from ( 'resized' ) ) ,
505+ }
506+ // Shared module-level mock: without this, a later fixture reads the
507+ // earlier test's decoded image.
508+ vi . mocked ( nativeImage . createFromBuffer ) . mockReset ( )
509+ vi . mocked ( nativeImage . createFromBuffer ) . mockReturnValue ( {
510+ isEmpty : vi . fn ( ( ) => imageSize === null ) ,
511+ getSize : vi . fn ( ( ) => imageSize ?? { width : 0 , height : 0 } ) ,
512+ resize : vi . fn ( ( ) => resized ) ,
513+ toJPEG : vi . fn ( ( ) => Buffer . alloc ( 0 ) ) ,
514+ } as unknown as ReturnType < typeof nativeImage . createFromBuffer > )
515+ return { contents, resized }
516+ }
517+
518+ function screenshotParams ( contents : WebContents ) : Record < string , unknown > {
519+ const call = vi
520+ . mocked ( contents . debugger . sendCommand )
521+ . mock . calls . find ( ( [ method ] ) => method === 'Page.captureScreenshot' )
522+ if ( ! call ) throw new Error ( 'no capture was requested' )
523+ return call [ 1 ] as Record < string , unknown >
524+ }
525+
526+ it ( 'never sends a clip, which would emulate the live page for the capture' , async ( ) => {
527+ const { contents } = captureFixture ( { width : 4096 , height : 2048 } )
528+
529+ await captureScreenshot ( contents )
530+
531+ expect ( screenshotParams ( contents ) ) . not . toHaveProperty ( 'clip' )
532+ } )
533+
534+ /**
535+ * A 2048px CSS viewport bounded to 1024px is scale 0.5, and the capture
536+ * arrives at device resolution (4096px on a 2x display). The resize is what
537+ * lands the image on the CSS-relative size the coordinate contract
538+ * (cssX = imageX / scale) assumes.
539+ */
540+ it ( 'downscales the returned image to the CSS-relative size' , async ( ) => {
541+ const { contents, resized } = captureFixture ( { width : 4096 , height : 2048 } )
542+
543+ const shot = await captureScreenshot ( contents )
544+
545+ const image = vi . mocked ( nativeImage . createFromBuffer ) . mock . results [ 0 ] . value
546+ expect ( image . resize ) . toHaveBeenCalledWith ( { width : 1024 , height : 512 , quality : 'good' } )
547+ expect ( resized . toJPEG ) . toHaveBeenCalled ( )
548+ expect ( shot ) . toEqual ( {
549+ dataUrl : `data:image/jpeg;base64,${ Buffer . from ( 'resized' ) . toString ( 'base64' ) } ` ,
550+ scale : 0.5 ,
551+ } )
552+ } )
553+
554+ it ( 'skips the re-encode when the capture already matches the target size' , async ( ) => {
555+ const { contents } = captureFixture ( { width : 1024 , height : 512 } )
556+
557+ const shot = await captureScreenshot ( contents )
558+
559+ const image = vi . mocked ( nativeImage . createFromBuffer ) . mock . results [ 0 ] . value
560+ expect ( image . resize ) . not . toHaveBeenCalled ( )
561+ expect ( shot ) . toEqual ( { dataUrl : 'data:image/jpeg;base64,c2lt' , scale : 0.5 } )
562+ } )
563+
564+ it ( 'returns the raw capture when the image cannot be decoded' , async ( ) => {
565+ const { contents } = captureFixture ( null )
566+
567+ const shot = await captureScreenshot ( contents )
568+
569+ expect ( shot ) . toEqual ( { dataUrl : 'data:image/jpeg;base64,c2lt' , scale : 0.5 } )
570+ } )
571+ } )
0 commit comments