import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ApiKeyModal } from '../../components/ApiKeyModal'; describe('ApiKeyModal', () => { const mockOnSave = vi.fn(); const mockOnClose = vi.fn(); beforeEach(() => { vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('visibility', () => { it('should not render when isOpen is false', () => { render(); expect(screen.queryByText('Gemini API Key')).not.toBeInTheDocument(); }); it('should render when isOpen is true', () => { render(); expect(screen.getByText('Gemini API Key')).toBeInTheDocument(); }); }); describe('form elements', () => { it('should render the API key input field', () => { render(); expect(screen.getByPlaceholderText('AIza...')).toBeInTheDocument(); }); it('should render the submit button', () => { render(); expect(screen.getByRole('button', { name: 'Speichern' })).toBeInTheDocument(); }); it('should render link to Google AI Studio', () => { render(); const link = screen.getByRole('link', { name: /API Key bei Google AI Studio holen/i }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute('href', 'https://aistudio.google.com/apikey'); expect(link).toHaveAttribute('target', '_blank'); }); it('should render privacy notice', () => { render(); expect(screen.getByText(/nur lokal in deinem Browser gespeichert/i)).toBeInTheDocument(); }); }); describe('close button', () => { it('should not render close button when onClose is not provided', () => { render(); // The close button should not be present const buttons = screen.getAllByRole('button'); expect(buttons).toHaveLength(1); // Only save button }); it('should render close button when onClose is provided', () => { render(); const buttons = screen.getAllByRole('button'); expect(buttons).toHaveLength(2); // Save button and close button }); it('should call onClose when close button is clicked', async () => { const user = userEvent.setup(); render(); const buttons = screen.getAllByRole('button'); const closeButton = buttons.find(btn => btn !== screen.getByText('Speichern')); await user.click(closeButton!); expect(mockOnClose).toHaveBeenCalledTimes(1); }); }); describe('currentKey prop', () => { it('should pre-fill input with currentKey', () => { render(); const input = screen.getByPlaceholderText('AIza...') as HTMLInputElement; expect(input.value).toBe('AIexisting123'); }); it('should leave input empty when currentKey is not provided', () => { render(); const input = screen.getByPlaceholderText('AIza...') as HTMLInputElement; expect(input.value).toBe(''); }); }); describe('form validation', () => { it('should show error when submitting empty key', async () => { const user = userEvent.setup(); render(); await user.click(screen.getByText('Speichern')); expect(screen.getByText('Bitte gib einen API Key ein')).toBeInTheDocument(); expect(mockOnSave).not.toHaveBeenCalled(); }); it('should show error when submitting whitespace-only key', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); await user.type(input, ' '); await user.click(screen.getByText('Speichern')); expect(screen.getByText('Bitte gib einen API Key ein')).toBeInTheDocument(); expect(mockOnSave).not.toHaveBeenCalled(); }); it('should show error when key does not start with AI', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); await user.type(input, 'invalid_key'); await user.click(screen.getByText('Speichern')); expect(screen.getByText('Der Key sollte mit "AI" beginnen')).toBeInTheDocument(); expect(mockOnSave).not.toHaveBeenCalled(); }); it('should accept key that starts with AI', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); await user.type(input, 'AIvalidkey123'); await user.click(screen.getByText('Speichern')); expect(mockOnSave).toHaveBeenCalledWith('AIvalidkey123'); }); it('should clear error when user types', async () => { const user = userEvent.setup(); render(); // First, trigger an error await user.click(screen.getByText('Speichern')); expect(screen.getByText('Bitte gib einen API Key ein')).toBeInTheDocument(); // Now type something const input = screen.getByPlaceholderText('AIza...'); await user.type(input, 'A'); // Error should be cleared expect(screen.queryByText('Bitte gib einen API Key ein')).not.toBeInTheDocument(); }); }); describe('form submission', () => { it('should call onSave with trimmed key on valid submit', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); // Type key with leading/trailing spaces await user.clear(input); await user.type(input, 'AIkey123'); await user.click(screen.getByText('Speichern')); // The component should trim the input expect(mockOnSave).toHaveBeenCalledWith('AIkey123'); }); it('should submit on Enter key press', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); await user.type(input, 'AIkey123'); await user.keyboard('{Enter}'); expect(mockOnSave).toHaveBeenCalledWith('AIkey123'); }); it('should prevent form default submission', async () => { const user = userEvent.setup(); render(); const input = screen.getByPlaceholderText('AIza...'); await user.type(input, 'AIkey123'); const form = input.closest('form'); const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); const preventDefaultSpy = vi.spyOn(submitEvent, 'preventDefault'); fireEvent(form!, submitEvent); expect(preventDefaultSpy).toHaveBeenCalled(); }); }); describe('input type', () => { it('should have password type for security', () => { render(); const input = screen.getByPlaceholderText('AIza...'); expect(input).toHaveAttribute('type', 'password'); }); }); describe('accessibility', () => { it('should have proper label for input', () => { render(); expect(screen.getByText('API Key')).toBeInTheDocument(); }); it('should have autofocus on input', () => { render(); const input = screen.getByPlaceholderText('AIza...'); // In the DOM, React's autoFocus prop becomes autofocus attribute (lowercase) // But jsdom doesn't actually focus, so we check the document.activeElement or just verify the component renders expect(input).toBeInTheDocument(); }); }); });