The first-run provider picker was a hard gate — the only way out was connecting a provider. Add an 'I'll choose a provider later' link that dismisses the overlay and persists the skip to localStorage so it never re-nags on subsequent launches. Users connect a provider any time from Settings -> Providers (manual onboarding already bypasses the skip gate). - onboarding.ts: firstRunSkipped state seeded from localStorage (hermes-onboarding-skipped-v1) + dismissFirstRunOnboarding() action; completeDesktopOnboarding clears the flag once a provider connects. - overlay: skip gate (firstRunSkipped && !manual returns null); ChooseLaterLink rendered in both the OAuth picker footer and the API-key fallback, first-run only. - tests: skip persists + hidden in manual mode; full-state fixtures updated.
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
|
|
import { $desktopOnboarding, type DesktopOnboardingState, type OnboardingContext } from '@/store/onboarding'
|
|
import type { OAuthProvider } from '@/types/hermes'
|
|
|
|
import { Picker } from './desktop-onboarding-overlay'
|
|
|
|
function provider(id: string, name = id): OAuthProvider {
|
|
return {
|
|
cli_command: `hermes login ${id}`,
|
|
docs_url: `https://example.com/${id}`,
|
|
flow: 'pkce',
|
|
id,
|
|
name,
|
|
status: { logged_in: false }
|
|
}
|
|
}
|
|
|
|
function setProviders(providers: OAuthProvider[]) {
|
|
$desktopOnboarding.set({
|
|
configured: false,
|
|
flow: { status: 'idle' },
|
|
mode: 'oauth',
|
|
providers,
|
|
reason: null,
|
|
requested: false,
|
|
firstRunSkipped: false,
|
|
manual: false
|
|
} satisfies DesktopOnboardingState)
|
|
}
|
|
|
|
const ctx: OnboardingContext = { requestGateway: async () => undefined as never }
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
|
|
try {
|
|
window.localStorage.clear()
|
|
} catch {
|
|
// jsdom localStorage should always be present; ignore if not.
|
|
}
|
|
|
|
$desktopOnboarding.set({
|
|
configured: null,
|
|
flow: { status: 'idle' },
|
|
mode: 'oauth',
|
|
providers: null,
|
|
reason: null,
|
|
requested: false,
|
|
firstRunSkipped: false,
|
|
manual: false
|
|
})
|
|
})
|
|
|
|
describe('onboarding Picker', () => {
|
|
it('features Nous Portal and hides other providers behind a disclosure', () => {
|
|
setProviders([provider('anthropic', 'Anthropic Claude'), provider('nous', 'Nous Portal')])
|
|
render(<Picker ctx={ctx} />)
|
|
|
|
expect(screen.getByText('Nous Portal')).toBeTruthy()
|
|
expect(screen.getByText('Recommended')).toBeTruthy()
|
|
expect(screen.queryByText('Anthropic API Key')).toBeNull()
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Other providers' }))
|
|
|
|
expect(screen.getByText('Anthropic API Key')).toBeTruthy()
|
|
expect(screen.getByRole('button', { name: 'Collapse' })).toBeTruthy()
|
|
})
|
|
|
|
it('shows every provider directly when Nous Portal is absent', () => {
|
|
setProviders([provider('anthropic', 'Anthropic Claude'), provider('openai-codex', 'OpenAI Codex / ChatGPT')])
|
|
render(<Picker ctx={ctx} />)
|
|
|
|
expect(screen.getByText('Anthropic API Key')).toBeTruthy()
|
|
expect(screen.getByText('OpenAI OAuth (ChatGPT)')).toBeTruthy()
|
|
expect(screen.queryByText('Other sign-in options')).toBeNull()
|
|
expect(screen.queryByText('Recommended')).toBeNull()
|
|
})
|
|
|
|
it('offers "choose later" on first run and persists the skip', () => {
|
|
setProviders([provider('nous', 'Nous Portal')])
|
|
render(<Picker ctx={ctx} />)
|
|
|
|
const skip = screen.getByRole('button', { name: "I'll choose a provider later" })
|
|
|
|
fireEvent.click(skip)
|
|
|
|
expect($desktopOnboarding.get().firstRunSkipped).toBe(true)
|
|
expect(window.localStorage.getItem('hermes-onboarding-skipped-v1')).toBe('1')
|
|
})
|
|
|
|
it('hides "choose later" in manual (add-provider) mode', () => {
|
|
setProviders([provider('nous', 'Nous Portal')])
|
|
$desktopOnboarding.set({ ...$desktopOnboarding.get(), manual: true })
|
|
render(<Picker ctx={ctx} />)
|
|
|
|
expect(screen.queryByRole('button', { name: "I'll choose a provider later" })).toBeNull()
|
|
})
|
|
})
|