@@ -15,7 +15,16 @@ const { copyURL, openBrowser, printQRCode } = vi.hoisted(() => ({
1515
1616vi . mock ( '../../src/dev/listen' , ( ) => ( { copyURL, openBrowser, printQRCode } ) )
1717
18- vi . mock ( 'std-env' , ( ) => ( { isCI : false , isTest : false } ) )
18+ const environment = vi . hoisted ( ( ) => ( { isCI : false , isTest : false } ) )
19+
20+ vi . mock ( 'std-env' , ( ) => ( {
21+ get isCI ( ) {
22+ return environment . isCI
23+ } ,
24+ get isTest ( ) {
25+ return environment . isTest
26+ } ,
27+ } ) )
1928
2029describe ( 'setupShortcuts' , ( ) => {
2130 const restores : Array < ( ) => void > = [ ]
@@ -24,7 +33,9 @@ describe('setupShortcuts', () => {
2433 for ( const restore of restores . splice ( 0 ) ) {
2534 restore ( )
2635 }
36+ Object . assign ( environment , { isCI : false , isTest : false } )
2737 vi . restoreAllMocks ( )
38+ vi . clearAllMocks ( )
2839 } )
2940
3041 function setup ( context : Partial < ShortcutContext > = { } , { isTTY = true } = { } ) {
@@ -35,8 +46,7 @@ describe('setupShortcuts', () => {
3546 Object . defineProperty ( process , 'stdin' , { value : stdin , configurable : true } )
3647 restores . push ( ( ) => Object . defineProperty ( process , 'stdin' , { value : original , configurable : true } ) )
3748
38- const logs : string [ ] = [ ]
39- vi . spyOn ( console , 'log' ) . mockImplementation ( message => void logs . push ( String ( message ) ) )
49+ vi . spyOn ( console , 'log' ) . mockImplementation ( ( ) => { } )
4050
4151 const listener = {
4252 url : 'http://localhost:3000/' ,
@@ -53,45 +63,29 @@ describe('setupShortcuts', () => {
5363
5464 setupShortcuts ( resolved )
5565
56- const press = async ( input : string ) => {
57- stdin . write ( `${ input } \n` )
58- await new Promise ( resolve => setImmediate ( resolve ) )
66+ return {
67+ context : resolved ,
68+ listener,
69+ stdin,
70+ press : async ( input : string ) => {
71+ stdin . write ( `${ input } \n` )
72+ await new Promise ( resolve => setImmediate ( resolve ) )
73+ } ,
5974 }
60-
61- return { context : resolved , listener, logs, press, stdin }
6275 }
6376
64- it ( 'should do nothing when stdin is not a TTY' , ( ) => {
65- const { logs, stdin } = setup ( { } , { isTTY : false } )
66-
67- expect ( logs ) . toHaveLength ( 0 )
68- expect ( stdin . listenerCount ( 'line' ) ) . toBe ( 0 )
69- } )
70-
71- it ( 'should hint at the help shortcut once ready' , ( ) => {
72- const { logs } = setup ( )
77+ it ( 'should not read stdin when it is not a TTY' , ( ) => {
78+ const { stdin } = setup ( { } , { isTTY : false } )
7379
74- expect ( logs . join ( '\n ') ) . toContain ( 'h + enter' )
80+ expect ( stdin . listenerCount ( 'data ') ) . toBe ( 0 )
7581 } )
7682
77- it ( 'should open the browser and show urls' , async ( ) => {
78- const { listener, press } = setup ( )
79-
80- await press ( 'o' )
81- await vi . waitFor ( ( ) => expect ( openBrowser ) . toHaveBeenCalledWith ( 'http://localhost:3000/' ) )
82-
83- await press ( 'urls' )
84- expect ( listener . showURLs ) . toHaveBeenCalled ( )
85- } )
86-
87- it ( 'should reprint the urls after clearing the console' , async ( ) => {
88- const { listener, press } = setup ( )
89- const write = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true )
90-
91- await press ( 'clear' )
83+ it ( 'should not read stdin in CI or under test' , ( ) => {
84+ environment . isCI = true
85+ expect ( setup ( ) . stdin . listenerCount ( 'data' ) ) . toBe ( 0 )
9286
93- expect ( write ) . toHaveBeenCalledWith ( '\u001B[2J\u001B[3J\u001B[H' )
94- expect ( listener . showURLs ) . toHaveBeenCalled ( )
87+ Object . assign ( environment , { isCI : false , isTest : true } )
88+ expect ( setup ( ) . stdin . listenerCount ( 'data' ) ) . toBe ( 0 )
9589 } )
9690
9791 it ( 'should distinguish `q` from `qr`' , async ( ) => {
@@ -108,10 +102,11 @@ describe('setupShortcuts', () => {
108102 expect ( exit ) . toHaveBeenCalled ( )
109103 } )
110104
111- it ( 'should prefer a network url for sharing ' , async ( ) => {
105+ it ( 'should share the most reachable url ' , async ( ) => {
112106 const listener = {
113107 url : 'http://localhost:3000/' ,
114108 qrURL : 'http://192.168.1.20:3000/' ,
109+ publicURL : 'https://example.com/' ,
115110 getURLs : ( ) => [ ] ,
116111 showURLs : vi . fn ( ) ,
117112 } as unknown as Listener
@@ -121,22 +116,79 @@ describe('setupShortcuts', () => {
121116 await vi . waitFor ( ( ) => expect ( copyURL ) . toHaveBeenCalledWith ( 'http://192.168.1.20:3000/' ) )
122117 } )
123118
119+ it ( 'should fall back to a network url when sharing' , async ( ) => {
120+ const listener = {
121+ url : 'http://localhost:3000/' ,
122+ getURLs : ( ) => [
123+ { url : 'http://localhost:3000/' , type : 'local' } ,
124+ { url : 'http://192.168.1.20:3000/' , type : 'network' } ,
125+ ] ,
126+ showURLs : vi . fn ( ) ,
127+ } as unknown as Listener
128+ const { press } = setup ( { listener } )
129+
130+ await press ( 'copy' )
131+ await vi . waitFor ( ( ) => expect ( copyURL ) . toHaveBeenCalledWith ( 'http://192.168.1.20:3000/' ) )
132+ } )
133+
124134 it ( 'should restart when a restart handler is available' , async ( ) => {
125135 const restart = vi . fn ( )
126- const { press, logs } = setup ( { restart } )
136+ const { press } = setup ( { restart } )
127137
128138 await press ( 'r' )
129- expect ( restart ) . toHaveBeenCalled ( )
139+ await vi . waitFor ( ( ) => expect ( restart ) . toHaveBeenCalled ( ) )
140+ } )
130141
131- await press ( 'h' )
132- expect ( logs . join ( '\n' ) ) . toContain ( 'restart the dev server' )
142+ it ( 'should ignore the restart shortcut when no handler is available' , async ( ) => {
143+ const { press, listener } = setup ( )
144+
145+ await press ( 'r' )
146+
147+ expect ( listener . showURLs ) . not . toHaveBeenCalled ( )
148+ expect ( openBrowser ) . not . toHaveBeenCalled ( )
149+ } )
150+
151+ it ( 'should report a failure to quit and exit non-zero' , async ( ) => {
152+ const exitCode = process . exitCode
153+ restores . push ( ( ) => {
154+ process . exitCode = exitCode
155+ } )
156+
157+ const close = vi . fn ( ) . mockRejectedValue ( new Error ( 'could not close' ) )
158+ const exit = vi . spyOn ( process , 'exit' ) . mockImplementation ( ( ) => undefined as never )
159+ const error = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
160+ const { press } = setup ( { close } )
161+
162+ await press ( 'q' )
163+ await vi . waitFor ( ( ) => expect ( exit ) . toHaveBeenCalled ( ) )
164+
165+ expect ( error ) . toHaveBeenCalledWith ( expect . objectContaining ( { message : 'could not close' } ) )
166+ expect ( process . exitCode ) . toBe ( 1 )
133167 } )
134168
135- it ( 'should hide the restart shortcut when unavailable' , async ( ) => {
136- const { press, logs } = setup ( )
169+ it ( 'should report a failing shortcut without exiting' , async ( ) => {
170+ const listener = {
171+ url : 'http://localhost:3000/' ,
172+ getURLs : ( ) => [ ] ,
173+ showURLs : vi . fn ( ( ) => {
174+ throw new Error ( 'boom' )
175+ } ) ,
176+ } as unknown as Listener
177+ const error = vi . spyOn ( console , 'error' ) . mockImplementation ( ( ) => { } )
178+ const { press } = setup ( { listener } )
179+
180+ await press ( 'urls' )
181+
182+ await vi . waitFor ( ( ) => expect ( error ) . toHaveBeenCalledWith ( expect . objectContaining ( { message : 'boom' } ) ) )
183+ } )
184+
185+ it ( 'should ignore unknown input' , async ( ) => {
186+ const { press, listener } = setup ( )
187+
188+ await press ( 'nonsense' )
137189
138- await press ( 'h' )
139- expect ( logs . join ( '\n' ) ) . not . toContain ( 'restart the dev server' )
140- expect ( logs . join ( '\n' ) ) . toContain ( 'quit' )
190+ expect ( listener . showURLs ) . not . toHaveBeenCalled ( )
191+ expect ( openBrowser ) . not . toHaveBeenCalled ( )
192+ expect ( copyURL ) . not . toHaveBeenCalled ( )
141193 } )
142194} )
0 commit comments