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+
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

2029
describe('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
})

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-
function stubDisplay({ browser }: { browser?: string } = {}): () => void {
33+
function stubDisplay({ browser, browserArgs }: { browser?: string, browserArgs?: string } = {}): () => void {
3434
const platform = process.platform
3535
const env = { ...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
return listener
6263
}
6364

64-
async function withShortcuts(context: Partial<ShortcutContext> = {}) {
65+
async function withShortcuts(context: Partial<ShortcutContext> = {}, options: Parameters<typeof listen>[1] = {}) {
6566
const stdin = new PassThrough() as unknown as typeof process.stdin
6667
Object.assign(stdin, { isTTY: true })
6768
vi.spyOn(process, 'stdin', 'get').mockReturnValue(stdin)
6869

69-
const listener = await start({ showURL: false })
70+
const listener = await start({ 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+
const renderer = await render(() => 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
const renderer = await render(() => printQRCode('http://192.168.1.20:3000/', { showURL: true }))
123130
const lines = 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 } = await withShortcuts({}, { publicURL: 'https://example.com/' })
155+
156+
const renderer = await render(() => 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+
const restore = stubDisplay()
163+
const { press } = await withShortcuts()
164+
165+
const renderer = await render(() => 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 } = await withShortcuts()
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-
const restore = stubDisplay({ browser: 'false' })
210+
// A launcher that fails, without depending on a platform-specific binary.
211+
const restore = stubDisplay({ browser: process.execPath, browserArgs: '-e process.exit(1)' })
186212

187213
const renderer = await render(async ({ waitForOutput }) => {
188214
openBrowser('http://localhost:3000/')

0 commit comments

Comments
 (0)