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