@@ -107,6 +107,8 @@ function makeSession(frame?: ReturnType<typeof makeFrame>) {
107107
108108function makeContext ( onClose ?: ( context : BrowserContext ) => void ) {
109109 const events = new NodeEvents . EventEmitter ( ) ;
110+ const nativeFileChooser = vi . fn ( ) ;
111+ const setChooserFiles = vi . fn ( async ( _files : unknown ) => { } ) ;
110112 const sessions : ReturnType < typeof makeSession > [ ] = [ ] ;
111113 let url = "about:blank" ;
112114 let viewport = { width : 1280 , height : 800 } ;
@@ -117,6 +119,19 @@ function makeContext(onClose?: (context: BrowserContext) => void) {
117119 once : ( name : string , callback : ( ...args : unknown [ ] ) => void ) => events . once ( name , callback ) ,
118120 off : ( name : string , callback : ( ...args : unknown [ ] ) => void ) => events . off ( name , callback ) ,
119121 emit : ( name : string , ...args : unknown [ ] ) => events . emit ( name , ...args ) ,
122+ nativeFileChooser,
123+ setChooserFiles,
124+ openFileChooser : ( ) => {
125+ if (
126+ ! events . emit ( "filechooser" , {
127+ isMultiple : ( ) => false ,
128+ element : ( ) => ( { getAttribute : async ( ) => ".csv" } ) ,
129+ setFiles : setChooserFiles ,
130+ } )
131+ ) {
132+ nativeFileChooser ( ) ;
133+ }
134+ } ,
120135 emitAsync : ( name : string , ...args : unknown [ ] ) =>
121136 Promise . all ( events . listeners ( name ) . map ( ( listener ) => listener ( ...args ) ) ) ,
122137 setDefaultTimeout : vi . fn ( ) ,
@@ -1395,6 +1410,75 @@ it.live("drives the desktop's own page for a tab the desktop renders", () =>
13951410 ) . pipe ( Effect . provide ( layer ) ) ,
13961411) ;
13971412
1413+ it . live ( "desktop file pickers stay native outside agent actions and remote control" , ( ) =>
1414+ Effect . scoped (
1415+ Effect . gen ( function * ( ) {
1416+ const browser = yield * ServerBrowser . ServerBrowser ;
1417+ const broker = yield * Broker . PreviewAutomationBroker ;
1418+ const manager = yield * Manager . PreviewManager ;
1419+ yield * Effect . yieldNow ;
1420+ desktopRendersNext = true ;
1421+ const opened = yield * manager . open ( { threadId : scope . thread . threadId , runtime : "server" } ) ;
1422+ const tabId = PreviewTabId . make ( opened . tabId ) ;
1423+ const observer = yield * browser . attachViewer ( viewerInput ( tabId , false ) ) ;
1424+ const page = desktopConnections [ 0 ] ! . context . page ;
1425+ page . openFileChooser ( ) ;
1426+ expect ( page . nativeFileChooser ) . toHaveBeenCalledTimes ( 1 ) ;
1427+
1428+ const status = ( ) =>
1429+ broker . invoke < PreviewAutomationStatus > ( { scope, tabId, operation : "status" , input : { } } ) ;
1430+ expect ( ( yield * status ( ) ) . fileChooser ) . toBeNull ( ) ;
1431+ const remote = yield * browser . attachViewer ( viewerInput ( tabId , true ) ) ;
1432+ page . openFileChooser ( ) ;
1433+ let offered = yield * Queue . take ( remote . output ) ;
1434+ while ( offered . _tag !== "fileChooser" ) offered = yield * Queue . take ( remote . output ) ;
1435+ const file = { name : "a.csv" , mimeType : "text/csv" , buffer : Buffer . from ( "a,b" ) } ;
1436+ expect (
1437+ yield * browser . answerFileChooser ( {
1438+ threadId : scope . thread . threadId ,
1439+ tabId,
1440+ chooserId : offered . id ,
1441+ files : [ file ] ,
1442+ } ) ,
1443+ ) . toBe ( true ) ;
1444+ expect ( page . setChooserFiles ) . toHaveBeenLastCalledWith ( [ file ] ) ;
1445+ expect ( page . nativeFileChooser ) . toHaveBeenCalledTimes ( 1 ) ;
1446+
1447+ yield * remote . input ( { type : "releaseControl" } ) ;
1448+ page . openFileChooser ( ) ;
1449+ expect ( page . nativeFileChooser ) . toHaveBeenCalledTimes ( 2 ) ;
1450+
1451+ page . locator . mockReturnValue ( {
1452+ scrollIntoViewIfNeeded : async ( ) => { } ,
1453+ boundingBox : async ( ) => null ,
1454+ click : async ( ) => page . openFileChooser ( ) ,
1455+ } as never ) ;
1456+ yield * broker . invoke ( { scope, tabId, operation : "click" , input : { locator : "input" } } ) ;
1457+ expect ( ( yield * status ( ) ) . fileChooser ) . toEqual ( { multiple : false , accept : ".csv" } ) ;
1458+ yield * broker . invoke ( { scope, tabId, operation : "upload" , input : { paths : [ "/tmp/a.csv" ] } } ) ;
1459+ expect ( page . setChooserFiles ) . toHaveBeenLastCalledWith ( [ "/tmp/a.csv" ] , expect . anything ( ) ) ;
1460+ expect ( ( yield * status ( ) ) . fileChooser ) . toBeNull ( ) ;
1461+ page . openFileChooser ( ) ;
1462+ expect ( page . nativeFileChooser ) . toHaveBeenCalledTimes ( 3 ) ;
1463+ page . locator . mockReturnValue ( {
1464+ scrollIntoViewIfNeeded : async ( ) => {
1465+ throw new Error ( "input disappeared" ) ;
1466+ } ,
1467+ } as never ) ;
1468+ expect (
1469+ yield * Effect . flip (
1470+ broker . invoke < void > ( { scope, tabId, operation : "click" , input : { locator : "input" } } ) ,
1471+ ) ,
1472+ ) . toMatchObject ( { _tag : "PreviewAutomationExecutionError" } ) ;
1473+ page . openFileChooser ( ) ;
1474+ expect ( page . nativeFileChooser ) . toHaveBeenCalledTimes ( 4 ) ;
1475+ expect (
1476+ ( yield * Queue . clear ( observer . output ) ) . some ( ( item ) => item . _tag === "fileChooser" ) ,
1477+ ) . toBe ( false ) ;
1478+ } ) ,
1479+ ) . pipe ( Effect . provide ( layer ) ) ,
1480+ ) ;
1481+
13981482it . live ( "a desktop page the desktop takes back reconnects instead of closing" , ( ) =>
13991483 Effect . scoped (
14001484 Effect . gen ( function * ( ) {
0 commit comments