@@ -61,10 +61,34 @@ describe('openInBrowser', () => {
6161
6262 it ( 'spawns detached, ignores stdio, and unrefs the child' , ( ) => {
6363 const unref = vi . fn ( ) ;
64- spawnMock . mockReturnValue ( { unref } ) ;
64+ spawnMock . mockReturnValue ( { unref, on : vi . fn ( ) } ) ;
6565 openInBrowser ( url , { platform : 'darwin' } ) ;
6666 expect ( spawnMock ) . toHaveBeenCalledWith ( 'open' , [ url ] , { detached : true , stdio : 'ignore' } ) ;
6767 expect ( unref ) . toHaveBeenCalledTimes ( 1 ) ;
6868 } ) ;
69+
70+ it ( "handles the child's async 'error' (missing binary) with a stderr hint, not a crash" , ( ) => {
71+ // spawn() reports ENOENT asynchronously on the child; an unhandled
72+ // 'error' event would crash the CLI. The default spawner must register
73+ // a listener that degrades to the manual-open hint.
74+ const listeners = new Map < string , ( err : Error ) => void > ( ) ;
75+ spawnMock . mockReturnValue ( {
76+ unref : vi . fn ( ) ,
77+ on : ( event : string , listener : ( err : Error ) => void ) => {
78+ listeners . set ( event , listener ) ;
79+ } ,
80+ } ) ;
81+ const stderrSpy = vi . spyOn ( process . stderr , 'write' ) . mockReturnValue ( true ) ;
82+ try {
83+ openInBrowser ( url , { platform : 'linux' } ) ;
84+ const onError = listeners . get ( 'error' ) ;
85+ expect ( onError ) . toBeDefined ( ) ;
86+ // Firing the listener must not throw and must print the hint.
87+ expect ( ( ) => onError ! ( new Error ( 'spawn xdg-open ENOENT' ) ) ) . not . toThrow ( ) ;
88+ expect ( String ( stderrSpy . mock . calls . at ( - 1 ) ?. [ 0 ] ) ) . toContain ( 'could not launch a browser' ) ;
89+ } finally {
90+ stderrSpy . mockRestore ( ) ;
91+ }
92+ } ) ;
6993 } ) ;
7094} ) ;
0 commit comments