Skip to content

Commit 391f02b

Browse files
committed
test: add wizard e2e tests
1 parent 44d1955 commit 391f02b

1 file changed

Lines changed: 257 additions & 0 deletions

File tree

test/wizard.test.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import type { Nuxt } from '@nuxt/schema'
2+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
// Mock consola before importing the wizard
8+
const mockPrompt = vi.fn()
9+
vi.mock('consola', () => ({
10+
consola: {
11+
prompt: (...args: unknown[]) => mockPrompt(...args),
12+
withTag: () => ({
13+
info: vi.fn(),
14+
warn: vi.fn(),
15+
error: vi.fn(),
16+
success: vi.fn(),
17+
box: vi.fn(),
18+
}),
19+
},
20+
}))
21+
22+
// Mock ofetch
23+
const mockFetch = vi.fn()
24+
vi.mock('ofetch', () => ({
25+
$fetch: (...args: unknown[]) => mockFetch(...args),
26+
}))
27+
28+
// Mock fs functions for ~/.shelve
29+
let mockShelveRc: Record<string, string> = {}
30+
vi.mock('node:os', async (importOriginal) => {
31+
const original = await importOriginal<typeof import('node:os')>()
32+
return {
33+
...original,
34+
homedir: () => '/mock-home',
35+
}
36+
})
37+
38+
vi.mock('node:fs', async (importOriginal) => {
39+
const original = await importOriginal<typeof import('node:fs')>()
40+
return {
41+
...original,
42+
existsSync: (path: string) => {
43+
if (path === '/mock-home/.shelve')
44+
return Object.keys(mockShelveRc).length > 0
45+
return original.existsSync(path)
46+
},
47+
readFileSync: (path: string, encoding?: string) => {
48+
if (path === '/mock-home/.shelve') {
49+
return Object.entries(mockShelveRc).map(([k, v]) => `${k}=${v}`).join('\n')
50+
}
51+
return original.readFileSync(path, encoding as BufferEncoding)
52+
},
53+
writeFileSync: (path: string, content: string) => {
54+
if (path === '/mock-home/.shelve') {
55+
mockShelveRc = {}
56+
for (const line of content.split('\n')) {
57+
const match = line.match(/^(\w+)=(.+)$/)
58+
if (match)
59+
mockShelveRc[match[1]] = match[2]
60+
}
61+
return
62+
}
63+
return original.writeFileSync(path, content)
64+
},
65+
}
66+
})
67+
68+
const { runShelveWizard } = await import('../src/wizard/shelve-setup')
69+
70+
describe('shelve wizard', () => {
71+
let testDir: string
72+
let nuxtConfig: string
73+
74+
beforeEach(() => {
75+
// Create temp directory with nuxt.config.ts
76+
testDir = join(tmpdir(), `wizard-test-${Date.now()}`)
77+
mkdirSync(testDir, { recursive: true })
78+
nuxtConfig = join(testDir, 'nuxt.config.ts')
79+
writeFileSync(nuxtConfig, `export default defineNuxtConfig({
80+
modules: ['nuxt-safe-runtime-config'],
81+
})`)
82+
83+
// Reset mocks
84+
mockPrompt.mockReset()
85+
mockFetch.mockReset()
86+
mockShelveRc = {}
87+
})
88+
89+
afterEach(() => {
90+
rmSync(testDir, { recursive: true, force: true })
91+
})
92+
93+
function createMockNuxt(overrides: Partial<Nuxt['options']> = {}): Nuxt {
94+
return {
95+
options: {
96+
rootDir: testDir,
97+
safeRuntimeConfig: {},
98+
...overrides,
99+
},
100+
} as unknown as Nuxt
101+
}
102+
103+
it('skips when user declines Shelve integration', async () => {
104+
mockPrompt.mockResolvedValueOnce(false) // Enable Shelve? No
105+
106+
await runShelveWizard(createMockNuxt())
107+
108+
expect(mockFetch).not.toHaveBeenCalled()
109+
})
110+
111+
it('skips when already configured', async () => {
112+
const nuxt = createMockNuxt({
113+
safeRuntimeConfig: { shelve: { project: 'existing', slug: 'team' } },
114+
} as any)
115+
116+
await runShelveWizard(nuxt)
117+
118+
expect(mockPrompt).not.toHaveBeenCalled()
119+
})
120+
121+
it('prompts for token when not logged in', async () => {
122+
mockPrompt
123+
.mockResolvedValueOnce(true) // Enable Shelve? Yes
124+
.mockResolvedValueOnce('test-token') // Enter token
125+
126+
mockFetch
127+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
128+
.mockResolvedValueOnce([{ slug: 'my-team', name: 'My Team' }]) // fetch teams
129+
.mockResolvedValueOnce([{ id: 1, name: 'my-project' }]) // fetch projects
130+
131+
await runShelveWizard(createMockNuxt())
132+
133+
expect(mockFetch).toHaveBeenCalledWith(
134+
'https://app.shelve.cloud/api/user/me',
135+
expect.objectContaining({ headers: { Cookie: 'authToken=test-token' } }),
136+
)
137+
})
138+
139+
it('uses existing token from ~/.shelve', async () => {
140+
mockShelveRc = { token: 'existing-token', username: 'saveduser', email: 'saved@example.com' }
141+
142+
mockPrompt.mockResolvedValueOnce(true) // Enable Shelve? Yes
143+
144+
mockFetch
145+
.mockResolvedValueOnce({ username: 'saveduser', email: 'saved@example.com' }) // validate existing token
146+
.mockResolvedValueOnce([{ slug: 'my-team', name: 'My Team' }]) // fetch teams
147+
.mockResolvedValueOnce([{ id: 1, name: 'my-project' }]) // fetch projects
148+
149+
await runShelveWizard(createMockNuxt())
150+
151+
// Should validate existing token, not prompt for new one
152+
expect(mockFetch).toHaveBeenCalledWith(
153+
'https://app.shelve.cloud/api/user/me',
154+
expect.objectContaining({ headers: { Cookie: 'authToken=existing-token' } }),
155+
)
156+
// Should not prompt for token
157+
expect(mockPrompt).toHaveBeenCalledTimes(1) // Only the "Enable Shelve?" prompt
158+
})
159+
160+
it('handles no teams found', async () => {
161+
mockPrompt
162+
.mockResolvedValueOnce(true) // Enable Shelve? Yes
163+
.mockResolvedValueOnce('test-token') // Enter token
164+
165+
mockFetch
166+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
167+
.mockResolvedValueOnce([]) // fetch teams - empty
168+
169+
await runShelveWizard(createMockNuxt())
170+
171+
// Should not proceed to project selection
172+
expect(mockFetch).toHaveBeenCalledTimes(2) // user/me + teams
173+
})
174+
175+
it('handles undefined teams from API', async () => {
176+
mockPrompt
177+
.mockResolvedValueOnce(true) // Enable Shelve? Yes
178+
.mockResolvedValueOnce('test-token') // Enter token
179+
180+
mockFetch
181+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
182+
.mockRejectedValueOnce(new Error('API error')) // fetch teams fails
183+
184+
await runShelveWizard(createMockNuxt())
185+
186+
// Should handle gracefully, not crash
187+
expect(mockFetch).toHaveBeenCalledTimes(2)
188+
})
189+
190+
it('auto-selects single team', async () => {
191+
mockShelveRc = { token: 'test-token' }
192+
193+
mockPrompt.mockResolvedValueOnce(true) // Enable Shelve? Yes
194+
195+
mockFetch
196+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
197+
.mockResolvedValueOnce([{ slug: 'only-team', name: 'Only Team' }]) // single team
198+
.mockResolvedValueOnce([{ id: 1, name: 'my-project' }]) // fetch projects
199+
200+
await runShelveWizard(createMockNuxt())
201+
202+
// Should not prompt for team selection when only one team
203+
expect(mockPrompt).toHaveBeenCalledTimes(1) // Only "Enable Shelve?" prompt
204+
// Should fetch projects for the auto-selected team
205+
expect(mockFetch).toHaveBeenCalledWith(
206+
'https://app.shelve.cloud/api/teams/only-team/projects',
207+
expect.anything(),
208+
)
209+
})
210+
211+
it('prompts for team selection when multiple teams', async () => {
212+
mockShelveRc = { token: 'test-token' }
213+
214+
mockPrompt
215+
.mockResolvedValueOnce(true) // Enable Shelve? Yes
216+
.mockResolvedValueOnce('team-b') // Select team
217+
218+
mockFetch
219+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
220+
.mockResolvedValueOnce([
221+
{ slug: 'team-a', name: 'Team A' },
222+
{ slug: 'team-b', name: 'Team B' },
223+
]) // multiple teams
224+
.mockResolvedValueOnce([{ id: 1, name: 'my-project' }]) // fetch projects
225+
226+
await runShelveWizard(createMockNuxt())
227+
228+
// Should prompt for team selection
229+
expect(mockPrompt).toHaveBeenCalledWith('Select team:', expect.objectContaining({
230+
type: 'select',
231+
options: expect.arrayContaining([
232+
expect.objectContaining({ label: 'Team A', value: 'team-a' }),
233+
expect.objectContaining({ label: 'Team B', value: 'team-b' }),
234+
]),
235+
}))
236+
})
237+
238+
it('updates nuxt.config.ts on successful setup', async () => {
239+
mockShelveRc = { token: 'test-token' }
240+
241+
mockPrompt.mockResolvedValueOnce(true) // Enable Shelve? Yes
242+
243+
mockFetch
244+
.mockResolvedValueOnce({ username: 'testuser', email: 'test@example.com' }) // validate token
245+
.mockResolvedValueOnce([{ slug: 'my-team', name: 'My Team' }]) // single team
246+
.mockResolvedValueOnce([{ id: 42, name: 'my-project' }]) // single project
247+
248+
await runShelveWizard(createMockNuxt())
249+
250+
// Check nuxt.config.ts was updated
251+
const updatedConfig = readFileSync(nuxtConfig, 'utf-8')
252+
expect(updatedConfig).toContain('safeRuntimeConfig')
253+
expect(updatedConfig).toContain('shelve')
254+
expect(updatedConfig).toContain('my-project')
255+
expect(updatedConfig).toContain('my-team')
256+
})
257+
})

0 commit comments

Comments
 (0)