@@ -1732,4 +1732,146 @@ describe('credential protection', () => {
17321732 'Element ids are not valid in this tab. Call browser_snapshot and use an id from that result.' ,
17331733 } )
17341734 } )
1735+
1736+ /** Fires every instrumentation listener registered for a WebContents event. */
1737+ function emitContentsEvent (
1738+ contents : Awaited < ReturnType < typeof openPage > > ,
1739+ event : string ,
1740+ ...args : unknown [ ]
1741+ ) : void {
1742+ for ( const [ name , listener ] of vi . mocked ( contents . on ) . mock . calls ) {
1743+ if ( name === event ) ( listener as ( ...listenerArgs : unknown [ ] ) => void ) ( { } , ...args )
1744+ }
1745+ }
1746+
1747+ it ( 'keeps element ids across a same-document (SPA) navigation' , async ( ) => {
1748+ const contents = await openPage ( )
1749+ respondWith ( contents , { } )
1750+
1751+ emitContentsEvent ( contents , 'did-navigate-in-page' )
1752+ const result = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
1753+
1754+ expect ( result ) . toMatchObject ( { ok : true , result : { dispatched : true } } )
1755+ } )
1756+
1757+ it ( 'still invalidates element ids on a cross-document navigation' , async ( ) => {
1758+ const contents = await openPage ( )
1759+ respondWith ( contents , { } )
1760+
1761+ emitContentsEvent ( contents , 'did-navigate' )
1762+ const result = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
1763+
1764+ expect ( result ) . toEqual ( {
1765+ ok : false ,
1766+ error :
1767+ 'Element ids are not valid in this tab. Call browser_snapshot and use an id from that result.' ,
1768+ } )
1769+ } )
1770+
1771+ it ( 'tolerates same-document URL churn during a keypress' , async ( ) => {
1772+ const contents = await openPage ( )
1773+ let urlReads = 0
1774+ vi . mocked ( contents . getURL ) . mockImplementation ( ( ) =>
1775+ ++ urlReads === 1 ? 'https://example.com/channel-a' : 'https://example.com/channel-b'
1776+ )
1777+ respondWith ( contents , {
1778+ activeElementSecrecy : 'safe' ,
1779+ readActiveElementState : { } ,
1780+ readPageActionState : {
1781+ url : 'https://example.com/channel-a' ,
1782+ title : 'Example' ,
1783+ focus : 'body' ,
1784+ mutationRevision : 0 ,
1785+ dialogs : [ ] ,
1786+ scroll : [ 0 ] ,
1787+ } ,
1788+ } )
1789+
1790+ const result = await driver . executeTool ( 'chat-test' , 'browser_press_key' , { key : 'Escape' } )
1791+
1792+ expect ( result . ok ) . toBe ( true )
1793+ expect ( cdpCalls ( contents , 'Input.dispatchKeyEvent' ) . length ) . toBeGreaterThan ( 0 )
1794+ } )
1795+
1796+ it ( 'aborts a keypress when a cross-document navigation lands mid-flight' , async ( ) => {
1797+ const contents = await openPage ( )
1798+ let navigated = false
1799+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
1800+ if ( isPageCall ( expression , 'activeElementSecrecy' ) ) return Promise . resolve ( 'safe' )
1801+ if ( isPageCall ( expression , 'readActiveElementState' ) ) return Promise . resolve ( { } )
1802+ if ( isPageCall ( expression , 'readPageActionState' ) ) {
1803+ if ( ! navigated ) {
1804+ navigated = true
1805+ emitContentsEvent ( contents , 'did-navigate' )
1806+ }
1807+ return Promise . resolve ( {
1808+ url : 'https://example.com/login' ,
1809+ title : 'Example' ,
1810+ focus : 'body' ,
1811+ mutationRevision : 0 ,
1812+ dialogs : [ ] ,
1813+ scroll : [ 0 ] ,
1814+ } )
1815+ }
1816+ return Promise . resolve ( undefined )
1817+ } )
1818+
1819+ const result = await driver . executeTool ( 'chat-test' , 'browser_press_key' , { key : 'Escape' } )
1820+
1821+ expect ( result . ok ) . toBe ( false )
1822+ expect ( result . error ) . toMatch ( / a c t i v e t a b o r p a g e c h a n g e d / )
1823+ expect ( cdpCalls ( contents , 'Input.dispatchKeyEvent' ) ) . toHaveLength ( 0 )
1824+ } )
1825+
1826+ it ( 'waits for a late-mounting editor before typing' , async ( ) => {
1827+ const contents = await openPage ( )
1828+ let focusReads = 0
1829+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
1830+ if ( isPageCall ( expression , 'focusElementForTyping' ) ) {
1831+ focusReads ++
1832+ return Promise . resolve (
1833+ focusReads === 1
1834+ ? { error : 'not-editable' }
1835+ : { focused : true , kind : 'contenteditable' , x : 24 , y : 48 }
1836+ )
1837+ }
1838+ if ( isPageCall ( expression , 'activeElementSecrecy' ) ) return Promise . resolve ( 'safe' )
1839+ if ( isPageCall ( expression , 'readActiveElementState' ) ) {
1840+ return Promise . resolve ( { activeElement : 'div' , valueLength : 5 } )
1841+ }
1842+ return Promise . resolve ( undefined )
1843+ } )
1844+
1845+ const result = await driver . executeTool ( 'chat-test' , 'browser_type' , {
1846+ elementId : 0 ,
1847+ text : 'hello' ,
1848+ } )
1849+
1850+ expect ( result . ok ) . toBe ( true )
1851+ expect ( focusReads ) . toBeGreaterThan ( 1 )
1852+ expect ( cdpCalls ( contents , 'Input.insertText' ) ) . toHaveLength ( 1 )
1853+ } )
1854+
1855+ it ( 'reprobes a transiently stale click target before giving up' , async ( ) => {
1856+ const contents = await openPage ( )
1857+ let clickReads = 0
1858+ vi . mocked ( contents . executeJavaScript ) . mockImplementation ( ( expression : string ) => {
1859+ if ( isPageCall ( expression , 'clickElement' ) ) {
1860+ clickReads ++
1861+ return Promise . resolve (
1862+ clickReads === 1
1863+ ? { error : 'stale' }
1864+ : { dispatched : false , x : 24 , y : 48 , element : 'Channel row' }
1865+ )
1866+ }
1867+ if ( isPageCall ( expression , 'readActiveElementState' ) ) return Promise . resolve ( { } )
1868+ if ( isPageCall ( expression , 'readPageActionState' ) ) return Promise . resolve ( { } )
1869+ return Promise . resolve ( undefined )
1870+ } )
1871+
1872+ const result = await driver . executeTool ( 'chat-test' , 'browser_click' , { elementId : 0 } )
1873+
1874+ expect ( result ) . toMatchObject ( { ok : true , result : { dispatched : true } } )
1875+ expect ( clickReads ) . toBeGreaterThan ( 1 )
1876+ } )
17351877} )
0 commit comments