@@ -1088,6 +1088,22 @@ describe('executeTool', () => {
10881088 } )
10891089} )
10901090
1091+ describe ( 'browserToolWatchdogMs' , ( ) => {
1092+ it . each ( [
1093+ [ 'number' , 30_000 , 35_000 ] ,
1094+ [ 'numeric string' , '30000' , 35_000 ] ,
1095+ [ 'absent' , undefined , 15_000 ] ,
1096+ [ 'non-numeric' , 'soon' , 15_000 ] ,
1097+ [ 'zero' , 0 , 15_000 ] ,
1098+ [ 'negative' , - 5_000 , 15_000 ] ,
1099+ [ 'above the wait clamp' , 500_000 , 125_000 ] ,
1100+ ] ) ( 'normalizes browser_wait_for timeout (%s)' , ( _label , timeoutMs , expected ) => {
1101+ const params = timeoutMs === undefined ? { } : { timeoutMs }
1102+
1103+ expect ( driverModule . browserToolWatchdogMs ( 'browser_wait_for' , params ) ) . toBe ( expected )
1104+ } )
1105+ } )
1106+
10911107/**
10921108 * Trusted CDP input never enters the page, so a focused credential field can
10931109 * only be ruled out in the driver. These cover that seam; the page-side
@@ -1167,6 +1183,8 @@ describe('credential protection', () => {
11671183
11681184 expect ( result . ok ) . toBe ( false )
11691185 expect ( result . error ) . toMatch ( / R e f u s i n g t o a c t o n a p a s s w o r d f i e l d / )
1186+ expect ( result . error ) . toMatch ( / v i s i b l e b r o w s e r / )
1187+ expect ( result . error ) . not . toContain ( 'browser_request_takeover' )
11701188 expect ( cdpCalls ( contents , 'Input.dispatchKeyEvent' ) ) . toHaveLength ( 0 )
11711189 } )
11721190
@@ -1532,6 +1550,24 @@ describe('credential protection', () => {
15321550 } )
15331551 } )
15341552
1553+ it ( 'rejects an unsupported browser_scroll direction instead of treating it as down' , async ( ) => {
1554+ const contents = await openPage ( )
1555+
1556+ const result = await driver . executeTool ( 'chat-test' , 'browser_scroll' , {
1557+ direction : 'sideways' ,
1558+ } )
1559+
1560+ expect ( result ) . toMatchObject ( {
1561+ ok : false ,
1562+ error : 'Scroll direction must be "up" or "down".' ,
1563+ } )
1564+ expect (
1565+ vi
1566+ . mocked ( contents . executeJavaScript )
1567+ . mock . calls . some ( ( [ expression ] ) => isPageCall ( String ( expression ) , 'scrollPage' ) )
1568+ ) . toBe ( false )
1569+ } )
1570+
15351571 it ( 'confirms a click when the requested target changes semantic state' , async ( ) => {
15361572 const contents = await openPage ( )
15371573 let actionReads = 0
@@ -2179,6 +2215,83 @@ describe('credential protection', () => {
21792215 expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 1 )
21802216 } )
21812217
2218+ it ( 'reports top-page effects observed after inserting text in a child frame' , async ( ) => {
2219+ const contents = await openPage ( )
2220+ const mainFrame = {
2221+ frameTreeNodeId : 1 ,
2222+ detached : false ,
2223+ isDestroyed : vi . fn ( ( ) => false ) ,
2224+ origin : 'https://example.com' ,
2225+ parent : null ,
2226+ framesInSubtree : [ ] as unknown [ ] ,
2227+ }
2228+ const childFrame = {
2229+ frameTreeNodeId : 2 ,
2230+ detached : false ,
2231+ isDestroyed : vi . fn ( ( ) => false ) ,
2232+ origin : 'https://mail-widget.example' ,
2233+ parent : mainFrame ,
2234+ url : 'https://mail-widget.example/compose' ,
2235+ }
2236+ mainFrame . framesInSubtree = [ mainFrame , childFrame ]
2237+ Object . defineProperty ( contents , 'mainFrame' , { configurable : true , value : mainFrame } )
2238+ Object . defineProperty ( contents , 'focusedFrame' , { configurable : true , value : childFrame } )
2239+ let topPageReads = 0
2240+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
2241+ if ( isPageCall ( expression , 'readPageActionState' ) ) {
2242+ topPageReads ++
2243+ return Promise . resolve ( {
2244+ url :
2245+ topPageReads === 1 ? 'https://example.com/compose' : 'https://example.com/message/sent' ,
2246+ title : 'Mail' ,
2247+ focus : 'iframe' ,
2248+ mutationRevision : topPageReads ,
2249+ dialogs : [ ] ,
2250+ scroll : [ 0 ] ,
2251+ } )
2252+ }
2253+ return Promise . resolve ( undefined )
2254+ } )
2255+ const isolatedFrameEval = vi
2256+ . spyOn ( cdp , 'evaluateInIsolatedFrame' )
2257+ . mockImplementation ( ( _contents , _frame , expression ) => {
2258+ if ( isPageCall ( expression , 'activeElementSecrecy' ) ) return Promise . resolve ( 'safe' )
2259+ if ( isPageCall ( expression , 'describeFocusedEditable' ) ) {
2260+ return Promise . resolve ( { editable : true , kind : 'input' } )
2261+ }
2262+ if ( isPageCall ( expression , 'readActiveElementState' ) ) {
2263+ return Promise . resolve ( { activeElement : 'input' , valueLength : 4 } )
2264+ }
2265+ if ( isPageCall ( expression , 'readPageActionState' ) ) {
2266+ return Promise . resolve ( {
2267+ url : 'https://mail-widget.example/compose' ,
2268+ title : 'Compose' ,
2269+ focus : 'input' ,
2270+ mutationRevision : 0 ,
2271+ dialogs : [ ] ,
2272+ scroll : [ 0 ] ,
2273+ } )
2274+ }
2275+ return Promise . resolve ( undefined )
2276+ } )
2277+
2278+ try {
2279+ const result = await driver . executeTool ( 'chat-test' , 'browser_insert_text' , { text : 'sent' } )
2280+
2281+ expect ( result . ok , result . error ) . toBe ( true )
2282+ expect ( result ) . toMatchObject ( {
2283+ ok : true ,
2284+ result : {
2285+ effectObserved : true ,
2286+ possibleEffectObserved : true ,
2287+ effect : { urlChanged : true } ,
2288+ } ,
2289+ } )
2290+ } finally {
2291+ isolatedFrameEval . mockRestore ( )
2292+ }
2293+ } )
2294+
21822295 it ( 'refuses insertion when nothing editable holds focus' , async ( ) => {
21832296 const contents = await openPage ( )
21842297 respondWith ( contents , {
@@ -2287,6 +2400,14 @@ describe('credential protection', () => {
22872400
22882401 const result = await driver . executeTool ( 'chat-test' , 'browser_screenshot' , { } )
22892402
2290- expect ( result ) . toMatchObject ( { ok : true , result : { scale : 0.5 } } )
2403+ expect ( result ) . toMatchObject ( {
2404+ ok : true ,
2405+ result : { scale : 0.5 , viewport : { width : 2048 , height : 1024 } } ,
2406+ } )
2407+ expect (
2408+ vi
2409+ . mocked ( contents . executeJavaScript )
2410+ . mock . calls . some ( ( [ expression ] ) => isPageCall ( String ( expression ) , 'getViewportInfo' ) )
2411+ ) . toBe ( false )
22912412 } )
22922413} )
0 commit comments