import { beforeEach, describe, expect, it, vi } from 'vitest' import { act, fireEvent, render, screen, within } from '@testing-library/react' import '@testing-library/jest-dom' import { Settings } from '../pages/Settings' import { useSettingsStore } from '../stores/settingsStore' import { useUIStore } from '../stores/uiStore' import { useUpdateStore } from '../stores/updateStore' import type { SavedProvider } from '../types/provider' import type { ProviderPreset } from '../types/providerPreset' const MOCK_DELETE_PROVIDER = vi.fn() const MOCK_GET_SETTINGS = vi.fn() const MOCK_UPDATE_SETTINGS = vi.fn() const desktopNotificationsMock = vi.hoisted(() => ({ getDesktopNotificationPermission: vi.fn(), requestDesktopNotificationPermission: vi.fn(), openDesktopNotificationSettings: vi.fn(), })) const providerStoreState = { providers: [] as SavedProvider[], activeId: null as string | null, hasLoadedProviders: true, presets: [] as ProviderPreset[], isLoading: false, isPresetsLoading: false, fetchProviders: vi.fn(), fetchPresets: vi.fn(), deleteProvider: MOCK_DELETE_PROVIDER, activateProvider: vi.fn(), activateOfficial: vi.fn(), testProvider: vi.fn(), createProvider: vi.fn(), updateProvider: vi.fn(), testConfig: vi.fn(), } vi.mock('../api/agents', () => ({ agentsApi: { list: vi.fn().mockResolvedValue({ activeAgents: [], allAgents: [] }), }, })) vi.mock('../stores/providerStore', () => ({ useProviderStore: () => providerStoreState, })) vi.mock('../api/providers', () => ({ providersApi: { getSettings: MOCK_GET_SETTINGS, updateSettings: MOCK_UPDATE_SETTINGS, }, })) vi.mock('../lib/desktopNotifications', () => desktopNotificationsMock) vi.mock('../components/settings/ClaudeOfficialLogin', () => ({ ClaudeOfficialLogin: () =>
, })) vi.mock('../pages/AdapterSettings', () => ({ AdapterSettings: () =>
Adapter Settings Mock
, })) vi.mock('../stores/agentStore', () => ({ useAgentStore: () => ({ activeAgents: [], allAgents: [], isLoading: false, error: null, selectedAgent: null, fetchAgents: vi.fn(), selectAgent: vi.fn(), }), })) vi.mock('../stores/skillStore', () => ({ useSkillStore: () => ({ skills: [], selectedSkill: null, isLoading: false, isDetailLoading: false, error: null, fetchSkills: vi.fn(), fetchSkillDetail: vi.fn(), clearSelection: vi.fn(), }), })) vi.mock('../components/chat/CodeViewer', () => ({ CodeViewer: ({ code }: { code: string }) =>
{code}
, })) describe('Settings > General tab', () => { beforeEach(() => { MOCK_DELETE_PROVIDER.mockReset() desktopNotificationsMock.getDesktopNotificationPermission.mockReset() desktopNotificationsMock.requestDesktopNotificationPermission.mockReset() desktopNotificationsMock.openDesktopNotificationSettings.mockReset() desktopNotificationsMock.getDesktopNotificationPermission.mockResolvedValue('default') desktopNotificationsMock.requestDesktopNotificationPermission.mockResolvedValue('granted') desktopNotificationsMock.openDesktopNotificationSettings.mockResolvedValue(true) MOCK_GET_SETTINGS.mockResolvedValue({}) MOCK_UPDATE_SETTINGS.mockResolvedValue({}) providerStoreState.providers = [] providerStoreState.activeId = null providerStoreState.hasLoadedProviders = true providerStoreState.presets = [] providerStoreState.isLoading = false providerStoreState.isPresetsLoading = false providerStoreState.fetchProviders = vi.fn() providerStoreState.fetchPresets = vi.fn() providerStoreState.activateProvider = vi.fn() providerStoreState.activateOfficial = vi.fn() providerStoreState.testProvider = vi.fn() providerStoreState.createProvider = vi.fn() providerStoreState.updateProvider = vi.fn() providerStoreState.testConfig = vi.fn() useSettingsStore.setState({ locale: 'en', thinkingEnabled: true, skipWebFetchPreflight: true, desktopNotificationsEnabled: true, webSearch: { mode: 'auto', tavilyApiKey: '', braveApiKey: '' }, setThinkingEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ thinkingEnabled: enabled }) }), setSkipWebFetchPreflight: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ skipWebFetchPreflight: enabled }) }), setDesktopNotificationsEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ desktopNotificationsEnabled: enabled }) }), setWebSearch: vi.fn().mockImplementation(async (webSearch) => { useSettingsStore.setState({ webSearch }) }), }) useUIStore.setState({ pendingSettingsTab: null }) useUpdateStore.setState({ status: 'idle', availableVersion: null, releaseNotes: null, progressPercent: 0, downloadedBytes: 0, totalBytes: null, error: null, checkedAt: null, shouldPrompt: false, initialize: vi.fn().mockResolvedValue(undefined), checkForUpdates: vi.fn().mockResolvedValue(null), installUpdate: vi.fn().mockResolvedValue(undefined), dismissPrompt: vi.fn(), }) }) it('shows WebFetch preflight toggle enabled by default', () => { render() fireEvent.click(screen.getByText('General')) const toggle = screen.getByLabelText('Skip WebFetch domain preflight') expect(toggle).toBeChecked() }) it('lets the user disable WebFetch preflight skipping', () => { render() fireEvent.click(screen.getByText('General')) const toggle = screen.getByLabelText('Skip WebFetch domain preflight') fireEvent.click(toggle) expect(useSettingsStore.getState().setSkipWebFetchPreflight).toHaveBeenCalledWith(false) }) it('lets the user disable thinking mode for new sessions', () => { render() fireEvent.click(screen.getByText('General')) const toggle = screen.getByLabelText('Enable thinking mode') expect(toggle).toBeChecked() fireEvent.click(toggle) expect(useSettingsStore.getState().setThinkingEnabled).toHaveBeenCalledWith(false) }) it('lets the user disable desktop system notifications', () => { render() fireEvent.click(screen.getByText('General')) const toggle = screen.getByLabelText('Enable system notifications') expect(toggle).toBeChecked() fireEvent.click(toggle) expect(useSettingsStore.getState().setDesktopNotificationsEnabled).toHaveBeenCalledWith(false) expect(desktopNotificationsMock.requestDesktopNotificationPermission).not.toHaveBeenCalled() }) it('requests native notification permission when desktop notifications are enabled', async () => { useSettingsStore.setState({ desktopNotificationsEnabled: false }) render() fireEvent.click(screen.getByText('General')) await act(async () => { fireEvent.click(screen.getByLabelText('Enable system notifications')) }) expect(useSettingsStore.getState().setDesktopNotificationsEnabled).toHaveBeenCalledWith(true) await vi.waitFor(() => { expect(desktopNotificationsMock.requestDesktopNotificationPermission).toHaveBeenCalledTimes(1) }) }) it('opens system settings when enabling notifications finds system denial', async () => { useSettingsStore.setState({ desktopNotificationsEnabled: false }) desktopNotificationsMock.requestDesktopNotificationPermission.mockResolvedValue('denied') render() fireEvent.click(screen.getByText('General')) await act(async () => { fireEvent.click(screen.getByLabelText('Enable system notifications')) }) await vi.waitFor(() => { expect(desktopNotificationsMock.openDesktopNotificationSettings).toHaveBeenCalledTimes(1) }) }) it('saves WebSearch fallback provider settings', () => { render() fireEvent.click(screen.getByText('General')) fireEvent.click(screen.getByRole('button', { name: 'Tavily' })) fireEvent.change(screen.getByLabelText('Tavily API key'), { target: { value: 'tvly-test-key' }, }) fireEvent.click(screen.getByRole('button', { name: 'Save' })) expect(useSettingsStore.getState().setWebSearch).toHaveBeenCalledWith({ mode: 'tavily', tavilyApiKey: 'tvly-test-key', braveApiKey: '', }) }) it('links to WebSearch provider API key dashboards', () => { render() fireEvent.click(screen.getByText('General')) expect(screen.getByRole('link', { name: 'Get Tavily API key' })).toHaveAttribute( 'href', 'https://app.tavily.com/home', ) expect(screen.getByRole('link', { name: 'Get Brave Search API key' })).toHaveAttribute( 'href', 'https://api-dashboard.search.brave.com/app/keys', ) }) it('keeps extension tabs available alongside the terminal tab', () => { render() expect(screen.queryByText('Install')).not.toBeInTheDocument() expect(screen.getByText('Terminal')).toBeInTheDocument() expect(screen.getByText('MCP')).toBeInTheDocument() expect(screen.getByText('Plugins')).toBeInTheDocument() }) }) describe('Settings > Providers tab', () => { beforeEach(() => { MOCK_DELETE_PROVIDER.mockReset() MOCK_GET_SETTINGS.mockResolvedValue({}) MOCK_UPDATE_SETTINGS.mockResolvedValue({}) providerStoreState.providers = [ { id: 'provider-1', name: 'MiniMax-M2.7-highspeed(openai)', presetId: 'custom', apiKey: '***', baseUrl: 'https://api.minimaxi.com', apiFormat: 'openai_chat', models: { main: 'MiniMax-M2.7-highspeed', haiku: '', sonnet: '', opus: '', }, notes: '', }, ] providerStoreState.activeId = null providerStoreState.hasLoadedProviders = true }) it('does not query official OAuth status before providers finish loading', () => { providerStoreState.providers = [] providerStoreState.activeId = null providerStoreState.hasLoadedProviders = false render() expect(screen.queryByTestId('claude-official-login')).not.toBeInTheDocument() }) it('shows official OAuth status only after official provider is confirmed active', () => { providerStoreState.providers = [] providerStoreState.activeId = null providerStoreState.hasLoadedProviders = true render() expect(screen.getByTestId('claude-official-login')).toBeInTheDocument() }) it('requires confirmation before deleting a provider', async () => { render() fireEvent.click(screen.getAllByText('Delete')[0]!) expect(MOCK_DELETE_PROVIDER).not.toHaveBeenCalled() expect(screen.getByRole('dialog')).toBeInTheDocument() expect(screen.getByText('Delete provider "MiniMax-M2.7-highspeed(openai)"? This cannot be undone.')).toBeInTheDocument() const dialog = screen.getByRole('dialog') fireEvent.click(within(dialog).getByRole('button', { name: 'Delete' })) expect(MOCK_DELETE_PROVIDER).toHaveBeenCalledWith('provider-1') }) it('uses the shared dropdown for API format in the provider form', () => { providerStoreState.presets = [ { id: 'custom', name: 'Custom', baseUrl: 'https://api.example.com/anthropic', apiFormat: 'anthropic', defaultModels: { main: 'custom-main', haiku: '', sonnet: '', opus: '', }, needsApiKey: true, websiteUrl: '', }, ] render() fireEvent.click(screen.getByRole('button', { name: /Add Provider/i })) const dialog = screen.getByRole('dialog') expect(within(dialog).queryByRole('combobox')).not.toBeInTheDocument() fireEvent.click(within(dialog).getByRole('button', { name: /Anthropic Messages \(native\)/i })) fireEvent.click(within(dialog).getByRole('button', { name: /OpenAI Responses API \(proxy\)/i })) expect(within(dialog).getByRole('button', { name: /OpenAI Responses API \(proxy\)/i })).toBeInTheDocument() expect(within(dialog).getByText('Requests will be translated via the local proxy')).toBeInTheDocument() }) it('hides the API key by default and reveals it from the eye button', () => { providerStoreState.presets = [ { id: 'custom', name: 'Custom', baseUrl: 'https://api.example.com/anthropic', apiFormat: 'anthropic', defaultModels: { main: 'custom-main', haiku: '', sonnet: '', opus: '', }, needsApiKey: true, websiteUrl: '', }, ] render() fireEvent.click(screen.getByRole('button', { name: /Add Provider/i })) const dialog = screen.getByRole('dialog') const apiKeyInput = within(dialog).getByPlaceholderText('sk-...') expect(apiKeyInput).toHaveAttribute('type', 'password') fireEvent.click(within(dialog).getByRole('button', { name: 'Show API Key' })) expect(apiKeyInput).toHaveAttribute('type', 'text') expect(within(dialog).getByRole('button', { name: 'Hide API Key' })).toBeInTheDocument() }) }) describe('Settings > About tab', () => { beforeEach(() => { useUIStore.setState({ pendingSettingsTab: 'about' }) useUpdateStore.setState({ status: 'available', availableVersion: '0.1.5', releaseNotes: '# Claude Code Haha v0.1.5\n\n- Fixed updater rendering\n- Added markdown support', progressPercent: 0, downloadedBytes: 0, totalBytes: null, error: null, checkedAt: null, shouldPrompt: true, initialize: vi.fn().mockResolvedValue(undefined), checkForUpdates: vi.fn().mockResolvedValue(null), installUpdate: vi.fn().mockResolvedValue(undefined), dismissPrompt: vi.fn(), }) }) it('renders release notes with markdown formatting', async () => { render() expect(await screen.findByRole('heading', { name: 'Claude Code Haha v0.1.5' })).toBeInTheDocument() expect(screen.getByText('Fixed updater rendering')).toBeInTheDocument() expect(screen.getByText('Added markdown support')).toBeInTheDocument() }) it('shows downloaded bytes instead of a fake zero percent when total size is unknown', async () => { useUpdateStore.setState({ status: 'downloading', availableVersion: '0.1.5', releaseNotes: '# Claude Code Haha v0.1.5', progressPercent: 0, downloadedBytes: 1536, totalBytes: null, error: null, checkedAt: null, shouldPrompt: true, initialize: vi.fn().mockResolvedValue(undefined), checkForUpdates: vi.fn().mockResolvedValue(null), installUpdate: vi.fn().mockResolvedValue(undefined), dismissPrompt: vi.fn(), }) render() expect(await screen.findByText('Downloading update... 1.5 KB downloaded')).toBeInTheDocument() expect(screen.queryByText('Downloading update... 0%')).not.toBeInTheDocument() }) })