Skip to content

Commit e12e139

Browse files
committed
test: cover upgrade preconditions, install failures and the pnpm catalog path
1 parent f8338be commit e12e139

1 file changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import{mkdtemp,readFile,rm,writeFile}from'node:fs/promises'
2+
import{tmpdir}from'node:os'
3+
importprocessfrom'node:process'
4+
5+
import{join}from'pathe'
6+
import{afterEach,beforeEach,describe,expect,it,vi}from'vitest'
7+
8+
const{ answers, progress, detectPackageManager, getNuxtVersion, resolveRegistryVersion, runDedupe, runInstall }=vi.hoisted(()=>({
9+
answers: {select: []asunknown[]},
10+
progress: []asstring[],
11+
detectPackageManager: vi.fn(),
12+
getNuxtVersion: vi.fn(),
13+
resolveRegistryVersion: vi.fn(),
14+
runDedupe: vi.fn(),
15+
runInstall: vi.fn(),
16+
}))
17+
18+
vi.mock('@clack/prompts',async(importOriginal)=>{
19+
constoriginal=awaitimportOriginal<typeofimport('@clack/prompts')>()
20+
return{
21+
...original,
22+
select: vi.fn(()=>Promise.resolve(answers.select.length ? answers.select.shift() : 'skip')),
23+
spinner: vi.fn(()=>{
24+
constrecord=(message?: string)=>void(message&&progress.push(message))
25+
return{start: record,stop: record,error: record,message: record}
26+
}),
27+
}
28+
})
29+
30+
vi.mock('nypm',asyncimportOriginal=>({
31+
...awaitimportOriginal<typeofimport('nypm')>(),
32+
detectPackageManager,
33+
}))
34+
35+
vi.mock('../../../src/utils/install',asyncimportOriginal=>({
36+
...awaitimportOriginal<typeofimport('../../../src/utils/install')>(),
37+
runDedupe,
38+
runInstall,
39+
}))
40+
41+
vi.mock('../../../src/utils/versions',asyncimportOriginal=>({
42+
...awaitimportOriginal<typeofimport('../../../src/utils/versions')>(),
43+
getNuxtVersion,
44+
resolveRegistryVersion,
45+
}))
46+
47+
vi.mock('../../../src/utils/kit',()=>({
48+
loadKit: ()=>Promise.resolve({loadNuxtConfig: ()=>Promise.resolve({buildDir: '.nuxt'})}),
49+
tryResolveNuxt: ()=>undefined,
50+
}))
51+
52+
const{runCommandDef}=awaitimport('../../../src/run-command')
53+
const{render,screen}=awaitimport('../../utils/terminal')
54+
constupgrade=awaitimport('../../../src/commands/upgrade').then(r=>r.default)
55+
56+
classExitErrorextendsError{
57+
constructor(readonlycode: number |undefined){
58+
super(`process.exit(${code})`)
59+
}
60+
}
61+
62+
letcwd: string
63+
64+
asyncfunctionrunUpgrade(argv: string[]=[]): Promise<{output: string,exitCode: number|undefined}>{
65+
letexitCode: number|undefined
66+
constexit=vi.spyOn(process,'exit').mockImplementation((code)=>{
67+
exitCode=codeasnumber|undefined
68+
thrownewExitError(exitCode)
69+
})
70+
71+
constrenderer=awaitrender(async()=>{
72+
awaitrunCommandDef(upgrade,[`--cwd=${cwd}`, ...argv]).catch((error)=>{
73+
if(!(errorinstanceofExitError)){
74+
throwerror
75+
}
76+
})
77+
})
78+
79+
exit.mockRestore()
80+
return{output: [...progress,renderer.frames.join('\n'),screen(renderer)].join('\n'),exitCode}
81+
}
82+
83+
asyncfunctionwriteProject(files: Record<string,string>): Promise<void>{
84+
for(const[name,contents]ofObject.entries(files)){
85+
awaitwriteFile(join(cwd,name),contents)
86+
}
87+
}
88+
89+
constinstalled={success: true,output: '',command: 'pnpm install',ignoredBuilds: []}
90+
91+
beforeEach(async()=>{
92+
cwd=awaitmkdtemp(join(tmpdir(),'nuxt-upgrade-'))
93+
answers.select.length=0
94+
progress.length=0
95+
vi.clearAllMocks()
96+
detectPackageManager.mockResolvedValue({name: 'pnpm',command: 'pnpm',lockFile: ['pnpm-lock.yaml']})
97+
getNuxtVersion.mockResolvedValue('4.0.0')
98+
resolveRegistryVersion.mockResolvedValue('4.1.0')
99+
runInstall.mockResolvedValue(installed)
100+
runDedupe.mockResolvedValue(installed)
101+
awaitwriteProject({
102+
'package.json': JSON.stringify({name: 'app',dependencies: {nuxt: '^4.0.0'}}),
103+
'pnpm-lock.yaml': '',
104+
})
105+
})
106+
107+
afterEach(async()=>{
108+
awaitrm(cwd,{recursive: true,force: true})
109+
vi.restoreAllMocks()
110+
})
111+
112+
describe('upgrade preconditions',()=>{
113+
it('should explain how to fix an undetectable package manager',async()=>{
114+
detectPackageManager.mockResolvedValue(undefined)
115+
116+
const{ output, exitCode }=awaitrunUpgrade()
117+
118+
expect(exitCode).toBe(1)
119+
expect(output).toContain('Unable to determine the package manager')
120+
expect(output).toContain('packageManager')
121+
expect(runInstall).not.toHaveBeenCalled()
122+
})
123+
124+
it('should carry on without a lock file but say it is missing',async()=>{
125+
awaitrm(join(cwd,'pnpm-lock.yaml'))
126+
127+
const{ output }=awaitrunUpgrade()
128+
129+
expect(output).toContain('Unable to find a pnpm lock file')
130+
expect(runInstall).toHaveBeenCalledTimes(1)
131+
})
132+
})
133+
134+
describe('upgrade install',()=>{
135+
it('should install the requested channel',async()=>{
136+
awaitrunUpgrade(['--channel=v3'])
137+
138+
expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: ['nuxt@3']}))
139+
})
140+
141+
it('should upgrade the core packages the project depends on',async()=>{
142+
awaitwriteProject({'package.json': JSON.stringify({name: 'app',devDependencies: {'nuxt': '^4.0.0','@nuxt/kit': '^4.0.0'}})})
143+
144+
awaitrunUpgrade()
145+
146+
expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({
147+
dependencies: ['nuxt@latest','@nuxt/kit@latest'],
148+
dev: true,
149+
}))
150+
})
151+
152+
it('should stop when the install fails',async()=>{
153+
runInstall.mockResolvedValue({ ...installed,success: false,error: 'pnpm ERR! offline',output: 'pnpm ERR! offline'})
154+
155+
const{ output, exitCode }=awaitrunUpgrade()
156+
157+
expect(exitCode).toBe(1)
158+
expect(output).toContain('pnpm ERR! offline')
159+
expect(runDedupe).not.toHaveBeenCalled()
160+
})
161+
162+
it('should dedupe when asked to',async()=>{
163+
awaitrunUpgrade(['--dedupe'])
164+
165+
expect(runDedupe).toHaveBeenCalledWith(expect.objectContaining({recreateLockfile: false}))
166+
})
167+
168+
it('should recreate the lockfile when forced, and say how to undo it',async()=>{
169+
const{ output }=awaitrunUpgrade(['--force'])
170+
171+
expect(runDedupe).toHaveBeenCalledWith(expect.objectContaining({recreateLockfile: true}))
172+
expect(output).toContain('--no-force')
173+
})
174+
175+
it('should report an upgrade that changed the version',async()=>{
176+
getNuxtVersion.mockResolvedValueOnce('4.0.0').mockResolvedValueOnce('4.1.0')
177+
178+
const{ output }=awaitrunUpgrade()
179+
180+
expect(output).toContain('Successfully upgraded Nuxt')
181+
expect(output).toContain('4.1.0')
182+
})
183+
184+
it('should say when the project was already up to date',async()=>{
185+
const{ output }=awaitrunUpgrade()
186+
187+
expect(output).toContain('already using the latest version')
188+
})
189+
})
190+
191+
describe('upgrade with a pnpm catalog',()=>{
192+
constworkspace='packages:\n - .\ncatalog:\n nuxt: ^4.0.0\n'
193+
194+
beforeEach(async()=>{
195+
awaitwriteProject({
196+
'package.json': JSON.stringify({name: 'app',dependencies: {nuxt: 'catalog:'}}),
197+
'pnpm-workspace.yaml': workspace,
198+
})
199+
})
200+
201+
it('should update the catalog entry rather than pinning it in package.json',async()=>{
202+
const{ output }=awaitrunUpgrade()
203+
204+
expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toContain('nuxt: ^4.1.0')
205+
expect(JSON.parse(awaitreadFile(join(cwd,'package.json'),'utf8')).dependencies.nuxt).toBe('catalog:')
206+
expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: []}))
207+
expect(output).toContain('Catalog entries updated')
208+
})
209+
210+
it('should leave the catalog alone for a package manager without catalogs',async()=>{
211+
detectPackageManager.mockResolvedValue({name: 'npm',command: 'npm',lockFile: ['package-lock.json']})
212+
awaitwriteProject({'package-lock.json': '{}'})
213+
214+
awaitrunUpgrade()
215+
216+
expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toBe(workspace)
217+
expect(runInstall).toHaveBeenCalledWith(expect.objectContaining({dependencies: ['nuxt@latest']}))
218+
})
219+
220+
it('should stop when nuxt cannot be resolved for the catalog',async()=>{
221+
resolveRegistryVersion.mockResolvedValue(undefined)
222+
223+
const{ output, exitCode }=awaitrunUpgrade()
224+
225+
expect(exitCode).toBe(1)
226+
expect(output).toContain('Unable to resolve a latest stable version')
227+
expect(output).toContain('DEBUG=nuxi')
228+
expect(output).toContain('Unable to upgrade')
229+
expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toBe(workspace)
230+
expect(runInstall).not.toHaveBeenCalled()
231+
})
232+
233+
it('should stop when the workspace file cannot be rewritten',async()=>{
234+
awaitwriteProject({'pnpm-workspace.yaml': 'catalog:\n nuxt: *missing-anchor\n'})
235+
236+
const{ output, exitCode }=awaitrunUpgrade()
237+
238+
expect(exitCode).toBe(1)
239+
expect(output).toContain('pnpm-workspace.yaml')
240+
expect(runInstall).not.toHaveBeenCalled()
241+
})
242+
243+
it('should tell the user the catalog moved on when the install then fails',async()=>{
244+
runInstall.mockResolvedValue({ ...installed,success: false,error: 'pnpm ERR! offline'})
245+
246+
const{ output, exitCode }=awaitrunUpgrade()
247+
248+
expect(exitCode).toBe(1)
249+
expect(output).toContain('catalog entries were already updated')
250+
})
251+
252+
it('should keep the range operator the catalog entry used',async()=>{
253+
awaitwriteProject({'pnpm-workspace.yaml': 'catalog:\n nuxt: 4.0.0\n'})
254+
255+
awaitrunUpgrade()
256+
257+
expect(awaitreadFile(join(cwd,'pnpm-workspace.yaml'),'utf8')).toContain('nuxt: 4.1.0')
258+
})
259+
})

0 commit comments

Comments
 (0)