@@ -27,6 +27,8 @@ interface MockView {
2727 isDestroyed : ReturnType < typeof vi . fn >
2828 setBackgroundThrottling : ReturnType < typeof vi . fn >
2929 capturePage : ReturnType < typeof vi . fn >
30+ findInPage : ReturnType < typeof vi . fn >
31+ stopFindInPage : ReturnType < typeof vi . fn >
3032 }
3133 setBackgroundColor : ReturnType < typeof vi . fn >
3234 setBounds : ReturnType < typeof vi . fn >
@@ -116,6 +118,7 @@ describe('browser-agent session', () => {
116118 )
117119 expect ( session . browserShortcutForInput ( { ...input , key : 't' } , 'darwin' ) ) . toBe ( 'new-tab' )
118120 expect ( session . browserShortcutForInput ( { ...input , key : 'w' } , 'darwin' ) ) . toBe ( 'close-tab' )
121+ expect ( session . browserShortcutForInput ( { ...input , key : 'f' } , 'darwin' ) ) . toBe ( 'find' )
119122 expect (
120123 session . browserShortcutForInput ( { ...input , key : 't' , shift : true } , 'darwin' )
121124 ) . toBeNull ( )
@@ -169,6 +172,166 @@ describe('browser-agent session', () => {
169172 expect ( win . webContents . send ) . toHaveBeenLastCalledWith ( 'browser-agent:focus-omnibox' , 'clear' )
170173 } )
171174
175+ it ( 'opens the renderer find bar when the page takes Mod+F' , ( ) => {
176+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
177+ const tab = session . requireTab ( )
178+ const contents = ( tab . view as unknown as MockView ) . webContents
179+ const beforeInput = contents . on . mock . calls . find (
180+ ( [ eventName ] ) => eventName === 'before-input-event'
181+ ) ?. [ 1 ] as
182+ | ( ( event : { preventDefault : ( ) => void } , input : Record < string , unknown > ) => void )
183+ | undefined
184+ const event = { preventDefault : vi . fn ( ) }
185+
186+ beforeInput ?.( event , {
187+ type : 'keyDown' ,
188+ key : 'f' ,
189+ isAutoRepeat : false ,
190+ isComposing : false ,
191+ shift : false ,
192+ control : process . platform !== 'darwin' ,
193+ alt : false ,
194+ meta : process . platform === 'darwin' ,
195+ } )
196+
197+ // The page never sees it — otherwise a site's own Mod+F wins over find.
198+ expect ( event . preventDefault ) . toHaveBeenCalled ( )
199+ expect ( win . webContents . send ) . toHaveBeenLastCalledWith ( 'browser-agent:open-find' )
200+ } )
201+
202+ it ( 'restarts the search while typing and steps without restarting on next/previous' , ( ) => {
203+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
204+ const tab = session . requireTab ( )
205+ const contents = ( tab . view as unknown as MockView ) . webContents
206+
207+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
208+ expect ( contents . findInPage ) . toHaveBeenLastCalledWith ( 'needle' , {
209+ forward : true ,
210+ findNext : false ,
211+ } )
212+
213+ session . findInActiveTab ( { query : 'needle' , findNext : true , forward : false } )
214+ expect ( contents . findInPage ) . toHaveBeenLastCalledWith ( 'needle' , {
215+ forward : false ,
216+ findNext : true ,
217+ } )
218+
219+ // Clearing the box is a stop, not a search for the empty string — and the
220+ // bar has to survive it, or deleting the last character closes the bar the
221+ // user is still typing in.
222+ vi . mocked ( win . webContents . send ) . mockClear ( )
223+ session . findInActiveTab ( { query : '' , findNext : false , forward : true } )
224+ expect ( contents . stopFindInPage ) . toHaveBeenCalledWith ( 'clearSelection' )
225+ expect ( contents . findInPage ) . toHaveBeenCalledTimes ( 2 )
226+ expect ( win . webContents . send ) . not . toHaveBeenCalledWith ( 'browser-agent:close-find' )
227+ } )
228+
229+ it ( 'forwards match counts only for the tab the find is running on' , ( ) => {
230+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
231+ const first = session . requireTab ( )
232+ const second = session . addTab ( )
233+ const firstContents = ( first . view as unknown as MockView ) . webContents
234+ const secondContents = ( second . view as unknown as MockView ) . webContents
235+ const foundOn = ( contents : MockView [ 'webContents' ] ) =>
236+ contents . on . mock . calls . find ( ( [ eventName ] ) => eventName === 'found-in-page' ) ?. [ 1 ] as
237+ | ( ( event : unknown , result : Record < string , unknown > ) => void )
238+ | undefined
239+
240+ session . switchTab ( first . id )
241+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
242+ foundOn ( firstContents ) ?.( { } , { activeMatchOrdinal : 2 , matches : 7 , finalUpdate : true } )
243+ expect ( win . webContents . send ) . toHaveBeenLastCalledWith ( 'browser-agent:find-result' , {
244+ activeMatchOrdinal : 2 ,
245+ matches : 7 ,
246+ final : true ,
247+ } )
248+
249+ // A late result from a tab that is not being searched would relabel the bar
250+ // with counts for a page the user is not looking at.
251+ vi . mocked ( win . webContents . send ) . mockClear ( )
252+ foundOn ( secondContents ) ?.( { } , { activeMatchOrdinal : 1 , matches : 3 , finalUpdate : true } )
253+ expect ( win . webContents . send ) . not . toHaveBeenCalledWith (
254+ 'browser-agent:find-result' ,
255+ expect . anything ( )
256+ )
257+ } )
258+
259+ it ( 'drops the find when its page navigates away, but not on a same-document change' , ( ) => {
260+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
261+ const tab = session . requireTab ( )
262+ const contents = ( tab . view as unknown as MockView ) . webContents
263+ const navigate = contents . on . mock . calls . find (
264+ ( [ eventName ] ) => eventName === 'did-start-navigation'
265+ ) ?. [ 1 ] as ( ( details : Record < string , unknown > ) => void ) | undefined
266+
267+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
268+ vi . mocked ( win . webContents . send ) . mockClear ( )
269+
270+ // A pushState route change keeps the document the matches live in.
271+ navigate ?.( { isMainFrame : true , isSameDocument : true } )
272+ expect ( win . webContents . send ) . not . toHaveBeenCalledWith ( 'browser-agent:close-find' )
273+ // A subframe load likewise leaves the main document alone.
274+ navigate ?.( { isMainFrame : false , isSameDocument : false } )
275+ expect ( win . webContents . send ) . not . toHaveBeenCalledWith ( 'browser-agent:close-find' )
276+
277+ navigate ?.( { isMainFrame : true , isSameDocument : false } )
278+ expect ( contents . stopFindInPage ) . toHaveBeenCalledWith ( 'clearSelection' )
279+ expect ( win . webContents . send ) . toHaveBeenCalledWith ( 'browser-agent:close-find' )
280+ } )
281+
282+ it ( 'drops the find when the user switches to another tab' , ( ) => {
283+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
284+ const first = session . requireTab ( )
285+ const second = session . addTab ( )
286+ const firstContents = ( first . view as unknown as MockView ) . webContents
287+
288+ session . switchTab ( first . id )
289+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
290+ vi . mocked ( win . webContents . send ) . mockClear ( )
291+
292+ session . switchTab ( second . id )
293+ expect ( firstContents . stopFindInPage ) . toHaveBeenCalledWith ( 'clearSelection' )
294+ expect ( win . webContents . send ) . toHaveBeenCalledWith ( 'browser-agent:close-find' )
295+ } )
296+
297+ it ( 'returns focus to the page only when the user dismissed the bar' , ( ) => {
298+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
299+ const tab = session . requireTab ( )
300+ const contents = ( tab . view as unknown as MockView ) . webContents
301+
302+ // Panel teardown: the bar unmounts under a user who has already moved on,
303+ // so pulling focus back into the browser would drag them back to it.
304+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
305+ contents . focus . mockClear ( )
306+ session . stopFindInActiveTab ( false )
307+ expect ( contents . focus ) . not . toHaveBeenCalled ( )
308+
309+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
310+ contents . focus . mockClear ( )
311+ session . stopFindInActiveTab ( true )
312+ expect ( contents . focus ) . toHaveBeenCalled ( )
313+ } )
314+
315+ it ( 'returns focus to the page even when no search was running' , ( ) => {
316+ panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
317+ const tab = session . requireTab ( )
318+ const contents = ( tab . view as unknown as MockView ) . webContents
319+
320+ // Opened and closed without typing. Focus still has to leave the bar: it is
321+ // unmounting, and <body> cannot receive the Mod+F that reopens it.
322+ contents . focus . mockClear ( )
323+ session . stopFindInActiveTab ( true )
324+ expect ( contents . focus ) . toHaveBeenCalled ( )
325+
326+ // Same once the box is emptied — clearing the query ends the search, so
327+ // dismissing afterwards has no searched tab to key focus off either.
328+ session . findInActiveTab ( { query : 'needle' , findNext : false , forward : true } )
329+ session . findInActiveTab ( { query : '' , findNext : false , forward : true } )
330+ contents . focus . mockClear ( )
331+ session . stopFindInActiveTab ( true )
332+ expect ( contents . focus ) . toHaveBeenCalled ( )
333+ } )
334+
172335 it ( 'closes only the native browser tab targeted by the application menu accelerator' , ( ) => {
173336 panel . setPanelBounds ( { x : 100 , y : 50 , width : 800 , height : 600 } )
174337 const first = session . requireTab ( )
0 commit comments