import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { ChatInterface } from '@/components/copilot/ChatInterface'; import { MessageList } from '@/components/copilot/MessageList'; import { ChatInput } from '@/components/copilot/ChatInput'; import { ActionPreview } from '@/components/copilot/ActionPreview'; import { ChatHistory } from '@/components/copilot/ChatHistory'; vi.mock('@/lib/copilot', () => ({ sendChatMessage: vi.fn(), executeAction: vi.fn(), getChatHistory: vi.fn(), sendVoiceMessage: vi.fn(), fileToBase64: vi.fn(), })); import { sendChatMessage, executeAction, getChatHistory } from '@/lib/copilot'; import type { ActionItem, ChatMessage } from '@/lib/copilot'; describe('ChatInput', () => { it('renders input and send button', () => { render(); expect(screen.getByTestId('chat-input')).toBeInTheDocument(); expect(screen.getByTestId('send-button')).toBeInTheDocument(); }); it('calls onSend when send button clicked', () => { const onSend = vi.fn(); render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Test message' } }); fireEvent.click(screen.getByTestId('send-button')); expect(onSend).toHaveBeenCalledWith('Test message'); }); it('clears input after send', () => { render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Test' } }); fireEvent.click(screen.getByTestId('send-button')); expect(input.value).toBe(''); }); it('does not send empty messages', () => { const onSend = vi.fn(); render(); fireEvent.click(screen.getByTestId('send-button')); expect(onSend).not.toHaveBeenCalled(); }); it('is disabled when disabled prop is true', () => { render(); expect(screen.getByTestId('send-button')).toBeDisabled(); }); }); describe('MessageList', () => { it('shows placeholder when no messages', () => { render(); expect(screen.getByText('Starte eine Konversation mit dem KI-Copilot...')).toBeInTheDocument(); }); it('renders user and assistant messages', () => { const messages: ChatMessage[] = [ { id: '1', session_id: 's1', role: 'user', content: 'Hallo', created_at: '2025-01-01T00:00:00Z' }, { id: '2', session_id: 's1', role: 'assistant', content: 'Hi there!', created_at: '2025-01-01T00:00:01Z' }, ]; render(); expect(screen.getByText('Hallo')).toBeInTheDocument(); expect(screen.getByText('Hi there!')).toBeInTheDocument(); }); it('shows loading indicator', () => { render(); expect(screen.getByTestId('loading-indicator')).toBeInTheDocument(); }); it('displays action proposals in assistant messages', () => { const messages: ChatMessage[] = [ { id: '1', session_id: 's1', role: 'assistant', content: 'Ich suche LKWs.', actions: [{ type: 'search_vehicles', params: { type: 'lkw' } }], created_at: '2025-01-01T00:00:00Z', }, ]; render(); expect(screen.getByText('Vorgeschlagene Aktionen:')).toBeInTheDocument(); expect(screen.getByText('search_vehicles')).toBeInTheDocument(); }); }); describe('ActionPreview', () => { const mockActions: ActionItem[] = [ { type: 'search_vehicles', params: { type: 'lkw' } }, ]; it('renders action preview with confirm and dismiss buttons', () => { render(); expect(screen.getByTestId('action-preview')).toBeInTheDocument(); expect(screen.getByTestId('confirm-action-0')).toBeInTheDocument(); expect(screen.getByTestId('dismiss-action-0')).toBeInTheDocument(); }); it('calls onConfirm when confirm button clicked', () => { const onConfirm = vi.fn(); render(); fireEvent.click(screen.getByTestId('confirm-action-0')); expect(onConfirm).toHaveBeenCalledWith(mockActions[0]); }); it('calls onDismiss when dismiss button clicked', () => { const onDismiss = vi.fn(); render(); fireEvent.click(screen.getByTestId('dismiss-action-0')); expect(onDismiss).toHaveBeenCalledWith(mockActions[0]); }); it('returns null when no actions', () => { const { container } = render(); expect(container.firstChild).toBeNull(); }); it('shows action label for known action types', () => { render(); expect(screen.getByText('Fahrzeuge durchsuchen')).toBeInTheDocument(); }); }); describe('ChatHistory', () => { beforeEach(() => { vi.clearAllMocks(); }); it('renders history sidebar with title', async () => { vi.mocked(getChatHistory).mockResolvedValue({ items: [], total: 0, page: 1, page_size: 100 }); render(); expect(screen.getByText('Konversationen')).toBeInTheDocument(); }); it('shows empty state when no sessions', async () => { vi.mocked(getChatHistory).mockResolvedValue({ items: [], total: 0, page: 1, page_size: 100 }); render(); await waitFor(() => { expect(screen.getByTestId('history-empty')).toBeInTheDocument(); }); }); it('displays sessions grouped by session_id', async () => { const messages: ChatMessage[] = [ { id: '1', session_id: 's1', role: 'user', content: 'Erste Frage', created_at: '2025-01-01T00:00:00Z' }, { id: '2', session_id: 's1', role: 'assistant', content: 'Antwort', created_at: '2025-01-01T00:00:01Z' }, { id: '3', session_id: 's2', role: 'user', content: 'Zweite Frage', created_at: '2025-01-02T00:00:00Z' }, ]; vi.mocked(getChatHistory).mockResolvedValue({ items: messages, total: 3, page: 1, page_size: 100 }); render(); await waitFor(() => { expect(screen.getByText('Erste Frage')).toBeInTheDocument(); expect(screen.getByText('Zweite Frage')).toBeInTheDocument(); }); }); it('calls onSelectSession when session clicked', async () => { const messages: ChatMessage[] = [ { id: '1', session_id: 's1', role: 'user', content: 'Test', created_at: '2025-01-01T00:00:00Z' }, ]; vi.mocked(getChatHistory).mockResolvedValue({ items: messages, total: 1, page: 1, page_size: 100 }); const onSelectSession = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('Test')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('history-session-s1')); expect(onSelectSession).toHaveBeenCalledWith('s1'); }); }); describe('ChatInterface', () => { beforeEach(() => { vi.clearAllMocks(); }); it('renders chat interface with input and message list', () => { render(); expect(screen.getByTestId('chat-input')).toBeInTheDocument(); expect(screen.getByTestId('message-list')).toBeInTheDocument(); expect(screen.getByTestId('voice-button')).toBeInTheDocument(); }); it('sends message and displays response', async () => { vi.mocked(sendChatMessage).mockResolvedValue({ response: 'Ich helfe dir!', actions: [], session_id: 'test-session', message_id: 'msg-1', }); render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Hallo' } }); fireEvent.click(screen.getByTestId('send-button')); await waitFor(() => { expect(screen.getByText('Ich helfe dir!')).toBeInTheDocument(); }); expect(sendChatMessage).toHaveBeenCalledWith('Hallo', undefined); }); it('shows action preview when response has actions', async () => { vi.mocked(sendChatMessage).mockResolvedValue({ response: 'Ich suche LKWs.', actions: [{ type: 'search_vehicles', params: { type: 'lkw' } }], session_id: 'test-session', message_id: 'msg-1', }); render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Zeige LKWs' } }); fireEvent.click(screen.getByTestId('send-button')); await waitFor(() => { expect(screen.getByTestId('action-preview')).toBeInTheDocument(); }); }); it('executes action when confirm button clicked', async () => { vi.mocked(sendChatMessage).mockResolvedValue({ response: 'Ich suche LKWs.', actions: [{ type: 'search_vehicles', params: { type: 'lkw' } }], session_id: 'test-session', message_id: 'msg-1', }); vi.mocked(executeAction).mockResolvedValue({ action: 'search_vehicles', result: { items: [], total: 0, page: 1, page_size: 20 }, success: true, }); render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Zeige LKWs' } }); fireEvent.click(screen.getByTestId('send-button')); await waitFor(() => { expect(screen.getByTestId('action-preview')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('confirm-action-0')); await waitFor(() => { expect(executeAction).toHaveBeenCalled(); }); }); it('shows error on API failure', async () => { vi.mocked(sendChatMessage).mockRejectedValue(new Error('Network error')); render(); const input = screen.getByTestId('chat-input') as HTMLTextAreaElement; fireEvent.change(input, { target: { value: 'Test' } }); fireEvent.click(screen.getByTestId('send-button')); await waitFor(() => { expect(screen.getByTestId('chat-error')).toBeInTheDocument(); }); }); it('toggles history sidebar', async () => { vi.mocked(getChatHistory).mockResolvedValue({ items: [], total: 0, page: 1, page_size: 100 }); render(); expect(screen.queryByTestId('chat-history')).not.toBeInTheDocument(); fireEvent.click(screen.getByTestId('toggle-history')); await waitFor(() => { expect(screen.getByTestId('chat-history')).toBeInTheDocument(); }); }); });