@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
33
44vi . mock ( 'electron' , ( ) => import ( '@/test/electron-mock' ) )
55
6- import { BrowserWindow , Menu } from 'electron'
6+ import { BrowserWindow , Menu , nativeImage } from 'electron'
77import * as cdp from '@/main/browser-agent/cdp'
88import * as driverModule from '@/main/browser-agent/driver'
99import * as session from '@/main/browser-agent/session'
@@ -1175,6 +1175,15 @@ describe('credential protection', () => {
11751175 . mock . calls . filter ( ( [ called ] ) => called === method )
11761176 }
11771177
1178+ function mockScreenshotImage ( size : { width : number ; height : number } | null ) : void {
1179+ vi . mocked ( nativeImage . createFromBuffer ) . mockReturnValueOnce ( {
1180+ isEmpty : vi . fn ( ( ) => size === null ) ,
1181+ getSize : vi . fn ( ( ) => size ?? { width : 0 , height : 0 } ) ,
1182+ resize : vi . fn ( ( ) => ( { toJPEG : vi . fn ( ( ) => Buffer . from ( 'resized' ) ) } ) ) ,
1183+ toJPEG : vi . fn ( ( ) => Buffer . alloc ( 0 ) ) ,
1184+ } as unknown as ReturnType < typeof nativeImage . createFromBuffer > )
1185+ }
1186+
11781187 it ( 'refuses a keystroke while a password field holds focus' , async ( ) => {
11791188 const contents = await openPage ( )
11801189 respondWith ( contents , { activeElementSecrecy : 'secret' } )
@@ -2385,6 +2394,7 @@ describe('credential protection', () => {
23852394
23862395 it ( 'returns the screenshot scale for coordinate mapping' , async ( ) => {
23872396 const contents = await openPage ( )
2397+ mockScreenshotImage ( { width : 1024 , height : 512 } )
23882398 vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
23892399 if ( method === 'Page.getLayoutMetrics' ) {
23902400 return Promise . resolve ( {
@@ -2402,12 +2412,132 @@ describe('credential protection', () => {
24022412
24032413 expect ( result ) . toMatchObject ( {
24042414 ok : true ,
2405- result : { scale : 0.5 , viewport : { width : 2048 , height : 1024 } } ,
2415+ result : {
2416+ scale : 0.5 ,
2417+ viewport : {
2418+ url : 'https://example.com/login' ,
2419+ title : 'Example' ,
2420+ width : 2048 ,
2421+ height : 1024 ,
2422+ } ,
2423+ } ,
24062424 } )
24072425 expect (
24082426 vi
24092427 . mocked ( contents . executeJavaScript )
24102428 . mock . calls . some ( ( [ expression ] ) => isPageCall ( String ( expression ) , 'getViewportInfo' ) )
24112429 ) . toBe ( false )
24122430 } )
2431+
2432+ it ( 'uses the in-page CSS viewport when CDP exposes only deprecated device metrics' , async ( ) => {
2433+ const contents = await openPage ( )
2434+ mockScreenshotImage ( { width : 1024 , height : 512 } )
2435+ vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
2436+ if ( method === 'Page.getLayoutMetrics' ) {
2437+ return Promise . resolve ( { layoutViewport : { clientWidth : 2048 , clientHeight : 1024 } } )
2438+ }
2439+ if ( method === 'Page.captureScreenshot' ) {
2440+ return Promise . resolve ( { data : 'c2lt' } )
2441+ }
2442+ return Promise . resolve ( undefined )
2443+ } )
2444+ respondWith ( contents , {
2445+ getViewportInfo : {
2446+ url : 'https://example.com/login' ,
2447+ title : 'Example' ,
2448+ width : 1024 ,
2449+ height : 512 ,
2450+ } ,
2451+ } )
2452+
2453+ const result = await driver . executeTool ( 'chat-test' , 'browser_screenshot' , { } )
2454+
2455+ expect ( result ) . toMatchObject ( {
2456+ ok : true ,
2457+ result : {
2458+ scale : 1 ,
2459+ viewport : {
2460+ url : 'https://example.com/login' ,
2461+ title : 'Example' ,
2462+ width : 1024 ,
2463+ height : 512 ,
2464+ } ,
2465+ } ,
2466+ } )
2467+ if (
2468+ ! result . ok ||
2469+ typeof result . result !== 'object' ||
2470+ result . result === null ||
2471+ ! ( 'scale' in result . result ) ||
2472+ typeof result . result . scale !== 'number'
2473+ ) {
2474+ throw new Error ( 'browser_screenshot did not return a numeric coordinate scale' )
2475+ }
2476+ expect ( 1024 / result . result . scale ) . toBe ( 1024 )
2477+ expect (
2478+ vi
2479+ . mocked ( contents . executeJavaScript )
2480+ . mock . calls . some ( ( [ expression ] ) => isPageCall ( String ( expression ) , 'getViewportInfo' ) )
2481+ ) . toBe ( true )
2482+ } )
2483+
2484+ it ( 'rejects an undecodable screenshot instead of returning an unverified scale' , async ( ) => {
2485+ const contents = await openPage ( )
2486+ mockScreenshotImage ( null )
2487+ vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
2488+ if ( method === 'Page.getLayoutMetrics' ) {
2489+ return Promise . resolve ( {
2490+ cssLayoutViewport : { clientWidth : 2048 , clientHeight : 1024 } ,
2491+ } )
2492+ }
2493+ if ( method === 'Page.captureScreenshot' ) return Promise . resolve ( { data : 'c2lt' } )
2494+ return Promise . resolve ( undefined )
2495+ } )
2496+
2497+ const result = await driver . executeTool ( 'chat-test' , 'browser_screenshot' , { } )
2498+
2499+ expect ( result . ok ) . toBe ( false )
2500+ expect ( result . error ) . toMatch ( / v e r i f y t h e s c r e e n s h o t d i m e n s i o n s / )
2501+ } )
2502+
2503+ it ( 'rejects a screenshot when no CSS viewport can be established' , async ( ) => {
2504+ const contents = await openPage ( )
2505+ mockScreenshotImage ( { width : 1024 , height : 512 } )
2506+ vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
2507+ if ( method === 'Page.getLayoutMetrics' ) {
2508+ return Promise . resolve ( { layoutViewport : { clientWidth : 2048 , clientHeight : 1024 } } )
2509+ }
2510+ if ( method === 'Page.captureScreenshot' ) return Promise . resolve ( { data : 'c2lt' } )
2511+ return Promise . resolve ( undefined )
2512+ } )
2513+ respondWith ( contents , { getViewportInfo : null } )
2514+
2515+ const result = await driver . executeTool ( 'chat-test' , 'browser_screenshot' , { } )
2516+
2517+ expect ( result . ok ) . toBe ( false )
2518+ expect ( result . error ) . toMatch ( / v e r i f y t h e p a g e v i e w p o r t / )
2519+ } )
2520+
2521+ it ( 'rejects coordinate mapping when the viewport changes during capture' , async ( ) => {
2522+ const contents = await openPage ( )
2523+ mockScreenshotImage ( { width : 1024 , height : 256 } )
2524+ vi . mocked ( contents . debugger . sendCommand ) . mockImplementation ( ( method : string ) => {
2525+ if ( method === 'Page.getLayoutMetrics' ) return Promise . resolve ( { } )
2526+ if ( method === 'Page.captureScreenshot' ) return Promise . resolve ( { data : 'c2lt' } )
2527+ return Promise . resolve ( undefined )
2528+ } )
2529+ respondWith ( contents , {
2530+ getViewportInfo : {
2531+ url : 'https://example.com/login' ,
2532+ title : 'Example' ,
2533+ width : 1024 ,
2534+ height : 512 ,
2535+ } ,
2536+ } )
2537+
2538+ const result = await driver . executeTool ( 'chat-test' , 'browser_screenshot' , { } )
2539+
2540+ expect ( result . ok ) . toBe ( false )
2541+ expect ( result . error ) . toMatch ( / v i e w p o r t c h a n g e d w h i l e t h e s c r e e n s h o t w a s c a p t u r e d / )
2542+ } )
24132543} )
0 commit comments