Skip to content

Commit 0e780ba

Browse files
committed
test: cover shortcut error paths
1 parent 1754688 commit 0e780ba

2 files changed

Lines changed: 127 additions & 49 deletions

File tree

‎packages/nuxt-cli/test/unit/shortcuts.spec.ts‎

Lines changed: 97 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ const { copyURL, openBrowser, printQRCode } = vi.hoisted(() => ({
1515

1616
vi.mock('../../src/dev/listen',()=>({ copyURL, openBrowser, printQRCode }))
1717

18-
vi.mock('std-env',()=>({isCI: false,isTest: false}))
18+
constenvironment=vi.hoisted(()=>({isCI: false,isTest: false}))
19+
20+
vi.mock('std-env',()=>({
21+
getisCI(){
22+
returnenvironment.isCI
23+
},
24+
getisTest(){
25+
returnenvironment.isTest
26+
},
27+
}))
1928

2029
describe('setupShortcuts',()=>{
2130
constrestores: Array<()=>void>=[]
@@ -24,7 +33,9 @@ describe('setupShortcuts', () => {
2433
for(constrestoreofrestores.splice(0)){
2534
restore()
2635
}
36+
Object.assign(environment,{isCI: false,isTest: false})
2737
vi.restoreAllMocks()
38+
vi.clearAllMocks()
2839
})
2940

3041
functionsetup(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-
constlogs: string[]=[]
39-
vi.spyOn(console,'log').mockImplementation(message=>voidlogs.push(String(message)))
49+
vi.spyOn(console,'log').mockImplementation(()=>{})
4050

4151
constlistener={
4252
url: 'http://localhost:3000/',
@@ -53,45 +63,29 @@ describe('setupShortcuts', () => {
5363

5464
setupShortcuts(resolved)
5565

56-
constpress=async(input: string)=>{
57-
stdin.write(`${input}\n`)
58-
awaitnewPromise(resolve=>setImmediate(resolve))
66+
return{
67+
context: resolved,
68+
listener,
69+
stdin,
70+
press: async(input: string)=>{
71+
stdin.write(`${input}\n`)
72+
awaitnewPromise(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-
awaitpress('o')
81-
awaitvi.waitFor(()=>expect(openBrowser).toHaveBeenCalledWith('http://localhost:3000/'))
82-
83-
awaitpress('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-
constwrite=vi.spyOn(process.stdout,'write').mockImplementation(()=>true)
90-
91-
awaitpress('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
constlistener={
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
}asunknownasListener
@@ -121,22 +116,79 @@ describe('setupShortcuts', () => {
121116
awaitvi.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+
constlistener={
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+
}asunknownasListener
128+
const{ press }=setup({ listener })
129+
130+
awaitpress('copy')
131+
awaitvi.waitFor(()=>expect(copyURL).toHaveBeenCalledWith('http://192.168.1.20:3000/'))
132+
})
133+
124134
it('should restart when a restart handler is available',async()=>{
125135
constrestart=vi.fn()
126-
const{ press, logs}=setup({ restart })
136+
const{ press }=setup({ restart })
127137

128138
awaitpress('r')
129-
expect(restart).toHaveBeenCalled()
139+
awaitvi.waitFor(()=>expect(restart).toHaveBeenCalled())
140+
})
130141

131-
awaitpress('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+
awaitpress('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+
constexitCode=process.exitCode
153+
restores.push(()=>{
154+
process.exitCode=exitCode
155+
})
156+
157+
constclose=vi.fn().mockRejectedValue(newError('could not close'))
158+
constexit=vi.spyOn(process,'exit').mockImplementation(()=>undefinedasnever)
159+
consterror=vi.spyOn(console,'error').mockImplementation(()=>{})
160+
const{ press }=setup({ close })
161+
162+
awaitpress('q')
163+
awaitvi.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+
constlistener={
171+
url: 'http://localhost:3000/',
172+
getURLs: ()=>[],
173+
showURLs: vi.fn(()=>{
174+
thrownewError('boom')
175+
}),
176+
}asunknownasListener
177+
consterror=vi.spyOn(console,'error').mockImplementation(()=>{})
178+
const{ press }=setup({ listener })
179+
180+
awaitpress('urls')
181+
182+
awaitvi.waitFor(()=>expect(error).toHaveBeenCalledWith(expect.objectContaining({message: 'boom'})))
183+
})
184+
185+
it('should ignore unknown input',async()=>{
186+
const{ press, listener }=setup()
187+
188+
awaitpress('nonsense')
137189

138-
awaitpress('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
})

‎packages/nuxt-cli/test/unit/terminal-output.spec.ts‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function screen(renderer: Renderer): string {
3030
}
3131

3232
/** Pretend to be a Linux desktop running `browser`, or a headless one. */
33-
functionstubDisplay({ browser}: {browser?: string}={}): ()=>void{
33+
functionstubDisplay({ browser, browserArgs }: {browser?: string,browserArgs?: string}={}): ()=>void{
3434
constplatform=process.platform
3535
constenv={ ...process.env}
3636
Object.defineProperty(process,'platform',{value: 'linux',configurable: true})
@@ -40,6 +40,7 @@ function stubDisplay({ browser }: { browser?: string } = {}): () => void {
4040
WAYLAND_DISPLAY: undefined,
4141
WSL_DISTRO_NAME: undefined,
4242
BROWSER: browser,
43+
BROWSER_ARGS: browserArgs,
4344
}
4445
return()=>{
4546
Object.defineProperty(process,'platform',{value: platform,configurable: true})
@@ -61,12 +62,12 @@ describe('dev server terminal output', () => {
6162
returnlistener
6263
}
6364

64-
asyncfunctionwithShortcuts(context: Partial<ShortcutContext>={}){
65+
asyncfunctionwithShortcuts(context: Partial<ShortcutContext>={},options: Parameters<typeoflisten>[1]={}){
6566
conststdin=newPassThrough()asunknownastypeofprocess.stdin
6667
Object.assign(stdin,{isTTY: true})
6768
vi.spyOn(process,'stdin','get').mockReturnValue(stdin)
6869

69-
constlistener=awaitstart({showURL: false})
70+
constlistener=awaitstart({showURL: false, ...options})
7071
setupShortcuts({
7172
listener,
7273
close: async()=>{},
@@ -118,6 +119,12 @@ describe('dev server terminal output', () => {
118119
})
119120

120121
describe('shortcuts',()=>{
122+
it('should hint at the help shortcut once the server is ready',async()=>{
123+
constrenderer=awaitrender(()=>withShortcuts())
124+
125+
expect(screen(renderer)).toContain('press h + enter to see available shortcuts')
126+
})
127+
121128
it('should caption a standalone qr code with its url',async()=>{
122129
constrenderer=awaitrender(()=>printQRCode('http://192.168.1.20:3000/',{showURL: true}))
123130
constlines=screen(renderer).split('\n').filter(Boolean)
@@ -143,6 +150,24 @@ describe('dev server terminal output', () => {
143150
expect(renderer.frames.at(-1)).not.toContain('noise from a previous build')
144151
})
145152

153+
it('should point a requested qr code at the public url',async()=>{
154+
const{ press }=awaitwithShortcuts({},{publicURL: 'https://example.com/'})
155+
156+
constrenderer=awaitrender(()=>press('qr'))
157+
158+
expect(screen(renderer).split('\n').filter(Boolean).at(-1)).toContain('https://example.com/')
159+
})
160+
161+
it('should say so when there is no clipboard to copy to',async()=>{
162+
constrestore=stubDisplay()
163+
const{ press }=awaitwithShortcuts()
164+
165+
constrenderer=awaitrender(()=>press('copy'))
166+
restore()
167+
168+
expect(screen(renderer)).toContain('No clipboard is available in this environment.')
169+
})
170+
146171
it('should list the available shortcuts',async()=>{
147172
const{ press }=awaitwithShortcuts()
148173

@@ -182,7 +207,8 @@ describe('dev server terminal output', () => {
182207
})
183208

184209
it('should say so when the browser launcher exits with an error',async()=>{
185-
constrestore=stubDisplay({browser: 'false'})
210+
// A launcher that fails, without depending on a platform-specific binary.
211+
constrestore=stubDisplay({browser: process.execPath,browserArgs: '-e process.exit(1)'})
186212

187213
constrenderer=awaitrender(async({ waitForOutput })=>{
188214
openBrowser('http://localhost:3000/')

0 commit comments

Comments
 (0)