@@ -11,11 +11,12 @@ import type {
1111 WebContents ,
1212 WebFrameMain ,
1313} from 'electron'
14- import { app , shell } from 'electron'
14+ import { app , webContents as electronWebContents , shell } from 'electron'
1515import {
1616 appLaunchNoticeForContents ,
1717 handleBrowserLogin ,
1818 handleSelectClientCertificate ,
19+ handleSelectPasskeyAccount ,
1920 httpAuthChallenge ,
2021 noteMainFrameNavigation ,
2122 pagePromptForContents ,
@@ -309,3 +310,90 @@ describe('app links', () => {
309310 expect ( shell . openExternal ) . toHaveBeenCalledExactlyOnceWith ( 'zoommtg://zoom.us/join?id=1' )
310311 } )
311312} )
313+
314+ describe ( 'accepting an answer' , ( ) => {
315+ const topFrame = { parent : null , frameTreeNodeId : 1 , origin : PAGE } as WebFrameMain
316+
317+ /** What a renderer could send over IPC without `allowed: true`, which the gesture gate keys on. */
318+ function withoutAllowed ( response : Record < string , unknown > ) : BrowserPagePromptResponse {
319+ // double-cast-allowed: an IPC payload is untyped; this is the shape a hostile renderer could send
320+ return response as unknown as BrowserPagePromptResponse
321+ }
322+
323+ it . each ( [
324+ [
325+ 'client-certificate' ,
326+ { kind : 'client-certificate' , choiceId : 'sha256/first' } ,
327+ ( contents : WebContents , callback : ( ) => void ) =>
328+ handleSelectClientCertificate (
329+ { preventDefault : ( ) => { } } ,
330+ contents ,
331+ 'https://mtls.corp' ,
332+ [ { fingerprint : 'sha256/first' , subjectName : 'Personal' } as Certificate ] ,
333+ callback
334+ ) ,
335+ ] ,
336+ [
337+ 'passkey-account' ,
338+ { kind : 'passkey-account' , choiceId : 'credential-1' } ,
339+ ( contents : WebContents , callback : ( ) => void ) => {
340+ vi . mocked ( electronWebContents . fromFrame ) . mockReturnValue ( contents )
341+ handleSelectPasskeyAccount (
342+ { } ,
343+ {
344+ relyingPartyId : 'meet.example' ,
345+ accounts : [ { credentialId : 'credential-1' , name : 'ada' , userHandle : 'u1' } ] ,
346+ frame : topFrame ,
347+ } ,
348+ callback
349+ )
350+ } ,
351+ ] ,
352+ [
353+ 'http-auth' ,
354+ { kind : 'http-auth' , username : 'ada' , password : 'pw' } ,
355+ ( contents : WebContents , callback : ( ) => void ) => {
356+ noteMainFrameNavigation ( contents , 'https://intranet.example/' )
357+ handleBrowserLogin (
358+ { preventDefault : ( ) => { } } ,
359+ contents ,
360+ {
361+ url : 'https://intranet.example/' ,
362+ isRequestForNavigation : true ,
363+ firstAuthAttempt : true ,
364+ } as AuthenticationResponseDetails ,
365+ { isProxy : false , scheme : 'basic' , host : 'intranet.example' , port : 443 } as AuthInfo ,
366+ callback
367+ )
368+ } ,
369+ ] ,
370+ ] ) ( 'sends nothing for a %s answer without allowed: true' , ( _kind , response , ask ) => {
371+ const contents = agentContents ( )
372+ const callback = vi . fn ( )
373+ ask ( contents , callback )
374+ answer ( contents , withoutAllowed ( response ) )
375+ expect ( callback ) . toHaveBeenCalledExactlyOnceWith ( )
376+ } )
377+
378+ it . each ( [
379+ [ 'an app prompt' , { kind : 'external-app' , alwaysAllow : true } , 'prompt' ] ,
380+ [ 'an app notice' , { kind : 'app-launch-notice' } , 'notice' ] ,
381+ ] ) ( 'opens nothing for %s answer without allowed: true' , async ( _label , response , slot ) => {
382+ app . getApplicationNameForProtocol = vi . fn ( ( ) => 'Zoom' )
383+ vi . mocked ( shell . openExternal ) . mockClear ( )
384+ context . userActivated = slot === 'prompt'
385+ const contents = agentContents ( )
386+ requestExternalLaunch ( contents , 'zoommtg://zoom.us/join' , { initiator : topFrame } )
387+ const requestId =
388+ slot === 'prompt'
389+ ? shownPrompt ( contents ) . requestId
390+ : appLaunchNoticeForContents ( contents ) ?. requestId
391+ respondToPagePrompt ( 'chat' , {
392+ action : 'respond-prompt' ,
393+ requestId,
394+ response : withoutAllowed ( response ) ,
395+ } )
396+ await vi . advanceTimersByTimeAsync ( 0 )
397+ expect ( shell . openExternal ) . not . toHaveBeenCalled ( )
398+ } )
399+ } )
0 commit comments