@@ -625,7 +625,7 @@ describe('executeTool', () => {
625625 } )
626626 }
627627 if ( isPageCall ( expression , 'focusElementForTyping' ) ) {
628- return Promise . resolve ( { focused : true , kind : 'input' } )
628+ return Promise . resolve ( { focused : true , kind : 'input' , x : 24 , y : 48 } )
629629 }
630630 if ( isPageCall ( expression , 'activeElementSecrecy' ) ) return Promise . resolve ( 'safe' )
631631 if ( isPageCall ( expression , 'readActiveElementState' ) ) return Promise . resolve ( { } )
@@ -869,11 +869,17 @@ describe('credential protection', () => {
869869
870870 it ( 'aborts a type when focus moves to a password field before the insert' , async ( ) => {
871871 const contents = await openPage ( )
872- // The element passed the guard, then the page advanced focus — what a
873- // login form does between the username and password steps.
874- respondWith ( contents , {
875- focusElementForTyping : { focused : true , kind : 'input' } ,
876- activeElementSecrecy : 'secret' ,
872+ let focusReads = 0
873+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
874+ if ( isPageCall ( expression , 'focusElementForTyping' ) ) {
875+ focusReads ++
876+ return Promise . resolve (
877+ focusReads === 1 ? { focused : true , kind : 'input' , x : 24 , y : 48 } : { error : 'password' }
878+ )
879+ }
880+ if ( isPageCall ( expression , 'readActiveElementState' ) ) return Promise . resolve ( { } )
881+ if ( isPageCall ( expression , 'readPageActionState' ) ) return Promise . resolve ( { } )
882+ return Promise . resolve ( undefined )
877883 } )
878884
879885 const result = await driver . executeTool ( 'chat-test' , 'browser_type' , {
@@ -886,16 +892,15 @@ describe('credential protection', () => {
886892 expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 0 )
887893 } )
888894
889- it ( 'aborts when a surface check moves focus to a password field at the final guard' , async ( ) => {
895+ it ( 'aborts when the suggestions surface steals focus at the final guard' , async ( ) => {
890896 const contents = await openPage ( )
891- let secrecyReads = 0
897+ let focusReads = 0
892898 vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
893899 if ( isPageCall ( expression , 'focusElementForTyping' ) ) {
894- return Promise . resolve ( { focused : true , kind : 'input' } )
895- }
896- if ( isPageCall ( expression , 'activeElementSecrecy' ) ) {
897- secrecyReads ++
898- return Promise . resolve ( secrecyReads === 1 ? 'safe' : 'secret' )
900+ focusReads ++
901+ return Promise . resolve (
902+ focusReads === 1 ? { focused : true , kind : 'input' , x : 24 , y : 48 } : { error : 'different' }
903+ )
899904 }
900905 if ( isPageCall ( expression , 'clickElement' ) ) {
901906 return Promise . resolve ( { dispatched : false , x : 24 , y : 48 , element : 'Test' } )
@@ -921,16 +926,16 @@ describe('credential protection', () => {
921926 text : 'hunter2' ,
922927 } )
923928
924- expect ( secrecyReads ) . toBe ( 2 )
929+ expect ( focusReads ) . toBe ( 2 )
925930 expect ( result . ok ) . toBe ( false )
926- 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 / )
931+ expect ( result . error ) . toMatch ( / d i f f e r e n t f i e l d t o o k f o c u s / )
927932 expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 0 )
928933 } )
929934
930935 it ( 'warns when acknowledged text produces no observable field change' , async ( ) => {
931936 const contents = await openPage ( )
932937 respondWith ( contents , {
933- focusElementForTyping : { focused : true , kind : 'input' } ,
938+ focusElementForTyping : { focused : true , kind : 'input' , x : 24 , y : 48 } ,
934939 activeElementSecrecy : 'safe' ,
935940 readActiveElementState : { activeElement : 'input' , valueLength : 7 } ,
936941 } )
@@ -950,6 +955,42 @@ describe('credential protection', () => {
950955 expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 1 )
951956 } )
952957
958+ it ( 'types through a focused combobox suggestions popup without pointer probing' , async ( ) => {
959+ const contents = await openPage ( )
960+ respondWith ( contents , {
961+ focusElementForTyping : {
962+ focused : true ,
963+ kind : 'input' ,
964+ x : 24 ,
965+ y : 48 ,
966+ coveredByRelatedPopup : true ,
967+ } ,
968+ readActiveElementState : { activeElement : 'input' , valueLength : 0 } ,
969+ readPageActionState : {
970+ url : 'https://example.com/login' ,
971+ title : 'Compose' ,
972+ focus : 'input:combobox:::To:' ,
973+ mutationRevision : 0 ,
974+ dialogs : [ ] ,
975+ popups : [ 'Contact list' ] ,
976+ scroll : [ 0 ] ,
977+ } ,
978+ } )
979+
980+ const result = await driver . executeTool ( 'chat-test' , 'browser_type' , {
981+ elementId : 0 ,
982+ text : 'Mondu' ,
983+ } )
984+
985+ expect ( result ) . toMatchObject ( { ok : true , result : { dispatched : true , trusted : true } } )
986+ expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 1 )
987+ expect (
988+ vi
989+ . mocked ( contents . executeJavaScript )
990+ . mock . calls . some ( ( [ expression ] ) => isPageCall ( String ( expression ) , 'clickElement' ) )
991+ ) . toBe ( false )
992+ } )
993+
953994 it ( 'confirms typing only after the field readback changes' , async ( ) => {
954995 const contents = await openPage ( )
955996 let inserted = false
@@ -960,7 +1001,7 @@ describe('credential protection', () => {
9601001 } )
9611002 vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
9621003 if ( isPageCall ( expression , 'focusElementForTyping' ) ) {
963- return Promise . resolve ( { focused : true , kind : 'input' } )
1004+ return Promise . resolve ( { focused : true , kind : 'input' , x : 24 , y : 48 } )
9641005 }
9651006 if ( isPageCall ( expression , 'activeElementSecrecy' ) ) return Promise . resolve ( 'safe' )
9661007 if ( isPageCall ( expression , 'clickElement' ) ) {
@@ -1031,6 +1072,22 @@ describe('credential protection', () => {
10311072 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 / )
10321073 } )
10331074
1075+ it ( 'guides typing through owned suggestions without dispatching a pointer click' , async ( ) => {
1076+ const contents = await openPage ( )
1077+ respondWith ( contents , {
1078+ clickElement : { error : 'suggestions-open' , blocker : 'Contact list' } ,
1079+ } )
1080+
1081+ const result = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
1082+
1083+ expect ( result ) . toMatchObject ( {
1084+ ok : false ,
1085+ error : expect . stringContaining ( 'Use browser_type on the same element' ) ,
1086+ } )
1087+ expect ( result . error ) . toContain ( 'do not dismiss the popup' )
1088+ expect ( cdpCalls ( contents , 'Input.dispatchMouseEvent' ) ) . toHaveLength ( 0 )
1089+ } )
1090+
10341091 it ( 'uses trusted CDP mouse input for element clicks' , async ( ) => {
10351092 const contents = await openPage ( )
10361093 respondWith ( contents , {
@@ -1126,10 +1183,52 @@ describe('credential protection', () => {
11261183 } )
11271184 } )
11281185
1186+ it ( 'confirms a panel close when the clicked target semantically disappears' , async ( ) => {
1187+ const contents = await openPage ( )
1188+ let actionReads = 0
1189+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
1190+ if ( isPageCall ( expression , 'clickElement' ) ) {
1191+ return Promise . resolve ( { dispatched : false , x : 24 , y : 48 , element : 'Close thread' } )
1192+ }
1193+ if ( isPageCall ( expression , 'readActiveElementState' ) ) return Promise . resolve ( { } )
1194+ if ( isPageCall ( expression , 'readPageActionState' ) ) {
1195+ actionReads ++
1196+ return Promise . resolve ( {
1197+ url : 'https://example.com/thread' ,
1198+ title : 'Thread' ,
1199+ focus : 'body' ,
1200+ mutationRevision : actionReads === 1 ? 0 : 2 ,
1201+ dialogs : [ ] ,
1202+ popups : [ ] ,
1203+ scroll : [ 0 ] ,
1204+ targetState :
1205+ actionReads === 1
1206+ ? { present : true , rendered : true }
1207+ : { present : false , rendered : false } ,
1208+ } )
1209+ }
1210+ return Promise . resolve ( undefined )
1211+ } )
1212+
1213+ const result = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
1214+
1215+ expect ( result ) . toMatchObject ( {
1216+ ok : true ,
1217+ result : {
1218+ effectObserved : true ,
1219+ possibleEffectObserved : true ,
1220+ effect : { targetChanged : true } ,
1221+ } ,
1222+ } )
1223+ expect ( result ) . not . toMatchObject ( {
1224+ result : { note : expect . stringContaining ( 'background DOM/title churn' ) } ,
1225+ } )
1226+ } )
1227+
11291228 it ( 'reports failed submit dispatch separately from a completed text write' , async ( ) => {
11301229 const contents = await openPage ( )
11311230 respondWith ( contents , {
1132- focusElementForTyping : { focused : true , kind : 'input' } ,
1231+ focusElementForTyping : { focused : true , kind : 'input' , x : 24 , y : 48 } ,
11331232 activeElementSecrecy : 'safe' ,
11341233 readActiveElementState : { activeElement : 'input' , valueLength : 5 } ,
11351234 readPageActionState : {
@@ -1172,7 +1271,7 @@ describe('credential protection', () => {
11721271 it ( 'does not retry text when Chromium loses the insert acknowledgement' , async ( ) => {
11731272 const contents = await openPage ( )
11741273 respondWith ( contents , {
1175- focusElementForTyping : { focused : true , kind : 'input' } ,
1274+ focusElementForTyping : { focused : true , kind : 'input' , x : 24 , y : 48 } ,
11761275 activeElementSecrecy : 'safe' ,
11771276 readActiveElementState : { activeElement : 'input' , valueLength : 5 } ,
11781277 } )
0 commit comments