import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { SettingsPanel } from './SettingsPanel' import type { Settings } from '../types' // Mock the tauri/mock-tauri calls used by GitHubSection const mockInvokeFn = vi.fn() vi.mock('@tauri-apps/api/core', () => ({ invoke: (...args: unknown[]) => mockInvokeFn(...args), })) vi.mock('../mock-tauri', () => ({ isTauri: () => false, mockInvoke: (...args: unknown[]) => mockInvokeFn(...args), })) const emptySettings: Settings = { anthropic_key: null, openai_key: null, google_key: null, github_token: null, github_username: null, } const populatedSettings: Settings = { anthropic_key: 'sk-ant-api03-test123', openai_key: 'sk-openai-test456', google_key: null, github_token: null, github_username: null, } describe('SettingsPanel', () => { const onSave = vi.fn() const onClose = vi.fn() beforeEach(() => { vi.clearAllMocks() }) it('renders nothing when not open', () => { const { container } = render( ) expect(container.innerHTML).toBe('') }) it('renders modal when open', () => { render( ) expect(screen.getByText('Settings')).toBeInTheDocument() expect(screen.getByText('AI Provider Keys')).toBeInTheDocument() expect(screen.getByText(/stored locally/)).toBeInTheDocument() }) it('shows three key fields with labels', () => { render( ) expect(screen.getByText('Anthropic')).toBeInTheDocument() expect(screen.getByText('OpenAI')).toBeInTheDocument() expect(screen.getByText('Google AI')).toBeInTheDocument() }) it('populates fields from settings', () => { render( ) const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement const openaiInput = screen.getByTestId('settings-key-openai') as HTMLInputElement const googleInput = screen.getByTestId('settings-key-google-ai') as HTMLInputElement expect(anthropicInput.value).toBe('sk-ant-api03-test123') expect(openaiInput.value).toBe('sk-openai-test456') expect(googleInput.value).toBe('') }) it('calls onSave with trimmed keys on save', () => { render( ) const anthropicInput = screen.getByTestId('settings-key-anthropic') fireEvent.change(anthropicInput, { target: { value: ' sk-ant-test ' } }) fireEvent.click(screen.getByTestId('settings-save')) expect(onSave).toHaveBeenCalledWith({ anthropic_key: 'sk-ant-test', openai_key: null, google_key: null, github_token: null, github_username: null, }) expect(onClose).toHaveBeenCalled() }) it('converts empty/whitespace keys to null', () => { render( ) // Clear the anthropic key field const anthropicInput = screen.getByTestId('settings-key-anthropic') fireEvent.change(anthropicInput, { target: { value: ' ' } }) fireEvent.click(screen.getByTestId('settings-save')) expect(onSave).toHaveBeenCalledWith({ anthropic_key: null, openai_key: 'sk-openai-test456', google_key: null, github_token: null, github_username: null, }) }) it('calls onClose when Cancel is clicked', () => { render( ) fireEvent.click(screen.getByText('Cancel')) expect(onClose).toHaveBeenCalled() }) it('calls onClose when close button is clicked', () => { render( ) fireEvent.click(screen.getByTitle('Close settings')) expect(onClose).toHaveBeenCalled() }) it('calls onClose on Escape key', () => { render( ) fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Escape' }) expect(onClose).toHaveBeenCalled() }) it('saves on Cmd+Enter', () => { render( ) const anthropicInput = screen.getByTestId('settings-key-anthropic') fireEvent.change(anthropicInput, { target: { value: 'sk-ant-test' } }) fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Enter', metaKey: true }) expect(onSave).toHaveBeenCalledWith({ anthropic_key: 'sk-ant-test', openai_key: null, google_key: null, github_token: null, github_username: null, }) }) it('calls onClose when clicking backdrop', () => { render( ) fireEvent.click(screen.getByTestId('settings-panel')) expect(onClose).toHaveBeenCalled() }) it('clears a key field when X button is clicked', () => { render( ) const clearBtn = screen.getByTestId('clear-anthropic') fireEvent.click(clearBtn) const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement expect(anthropicInput.value).toBe('') }) it('shows keyboard shortcut hint in footer', () => { render( ) expect(screen.getByText(/to open settings/)).toBeInTheDocument() }) it('resets fields when reopened with different settings', () => { const { rerender } = render( ) // Verify initial state const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement expect(anthropicInput.value).toBe('sk-ant-api03-test123') // Close and reopen with different settings rerender( ) const newSettings: Settings = { ...emptySettings, anthropic_key: 'new-key' } rerender( ) const updatedInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement expect(updatedInput.value).toBe('new-key') }) describe('GitHub OAuth section', () => { it('shows Login with GitHub button when not connected', () => { render( ) expect(screen.getByTestId('github-login')).toBeInTheDocument() expect(screen.getByText('Login with GitHub')).toBeInTheDocument() }) it('does not show GitHub token input field', () => { render( ) expect(screen.queryByTestId('settings-key-github-token')).not.toBeInTheDocument() expect(screen.queryByPlaceholderText('ghp_... or gho_...')).not.toBeInTheDocument() }) it('shows connected state with username when GitHub is connected', () => { const connectedSettings: Settings = { ...emptySettings, github_token: 'gho_test_token', github_username: 'lucaong', } render( ) expect(screen.getByTestId('github-connected')).toBeInTheDocument() expect(screen.getByText('lucaong')).toBeInTheDocument() expect(screen.getByText('Connected')).toBeInTheDocument() expect(screen.getByTestId('github-disconnect')).toBeInTheDocument() }) it('clears GitHub connection on disconnect', () => { const connectedSettings: Settings = { ...emptySettings, github_token: 'gho_test_token', github_username: 'lucaong', } render( ) fireEvent.click(screen.getByTestId('github-disconnect')) // onSave should be called with cleared GitHub fields expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ github_token: null, github_username: null, })) }) it('shows waiting state with user code during OAuth flow', async () => { mockInvokeFn.mockImplementation(async (cmd: string) => { if (cmd === 'github_device_flow_start') { return { device_code: 'test_device_code', user_code: 'TEST-1234', verification_uri: 'https://github.com/login/device', expires_in: 900, interval: 5, } } if (cmd === 'github_device_flow_poll') { return { status: 'pending', access_token: null, error: 'authorization_pending' } } return null }) // Mock window.open const windowOpen = vi.spyOn(window, 'open').mockImplementation(() => null) render( ) fireEvent.click(screen.getByTestId('github-login')) await waitFor(() => { expect(screen.getByTestId('github-waiting')).toBeInTheDocument() expect(screen.getByTestId('github-user-code')).toHaveTextContent('TEST-1234') }) expect(windowOpen).toHaveBeenCalledWith('https://github.com/login/device', '_blank') windowOpen.mockRestore() }) it('shows GitHub section description about connecting', () => { render( ) expect(screen.getByText(/Connect your GitHub account/)).toBeInTheDocument() }) }) })