Files
hermes-agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx
T
Teknium 9cc47b20cb feat(desktop): add 'choose provider later' skip to first-run onboarding (#39483)
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.
2026-06-04 19:40:54 -07:00

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()
})
})