241 lines
10 KiB
TypeScript
241 lines
10 KiB
TypeScript
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(<ChatInput onSend={vi.fn()} />);
|
|
expect(screen.getByTestId('chat-input')).toBeInTheDocument();
|
|
expect(screen.getByTestId('send-button')).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onSend when send button clicked', () => {
|
|
const onSend = vi.fn();
|
|
render(<ChatInput onSend={onSend} />);
|
|
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(<ChatInput onSend={vi.fn()} />);
|
|
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(<ChatInput onSend={onSend} />);
|
|
fireEvent.click(screen.getByTestId('send-button'));
|
|
expect(onSend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('is disabled when disabled prop is true', () => {
|
|
render(<ChatInput onSend={vi.fn()} disabled />);
|
|
expect(screen.getByTestId('send-button')).toBeDisabled();
|
|
});
|
|
});
|
|
|
|
describe('MessageList', () => {
|
|
it('shows placeholder when no messages', () => {
|
|
render(<MessageList messages={[]} />);
|
|
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(<MessageList messages={messages} />);
|
|
expect(screen.getByText('Hallo')).toBeInTheDocument();
|
|
expect(screen.getByText('Hi there!')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows loading indicator', () => {
|
|
render(<MessageList messages={[]} loading />);
|
|
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(<MessageList messages={messages} />);
|
|
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(<ActionPreview actions={mockActions} onConfirm={vi.fn()} onDismiss={vi.fn()} />);
|
|
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(<ActionPreview actions={mockActions} onConfirm={onConfirm} onDismiss={vi.fn()} />);
|
|
fireEvent.click(screen.getByTestId('confirm-action-0'));
|
|
expect(onConfirm).toHaveBeenCalledWith(mockActions[0]);
|
|
});
|
|
|
|
it('calls onDismiss when dismiss button clicked', () => {
|
|
const onDismiss = vi.fn();
|
|
render(<ActionPreview actions={mockActions} onConfirm={vi.fn()} onDismiss={onDismiss} />);
|
|
fireEvent.click(screen.getByTestId('dismiss-action-0'));
|
|
expect(onDismiss).toHaveBeenCalledWith(mockActions[0]);
|
|
});
|
|
|
|
it('returns null when no actions', () => {
|
|
const { container } = render(<ActionPreview actions={[]} onConfirm={vi.fn()} onDismiss={vi.fn()} />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it('shows action label for known action types', () => {
|
|
render(<ActionPreview actions={mockActions} onConfirm={vi.fn()} onDismiss={vi.fn()} />);
|
|
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(<ChatHistory onSelectSession={vi.fn()} onClose={vi.fn()} />);
|
|
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(<ChatHistory onSelectSession={vi.fn()} onClose={vi.fn()} />);
|
|
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(<ChatHistory onSelectSession={vi.fn()} onClose={vi.fn()} />);
|
|
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(<ChatHistory onSelectSession={onSelectSession} onClose={vi.fn()} />);
|
|
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(<ChatInterface />);
|
|
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(<ChatInterface />);
|
|
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(<ChatInterface />);
|
|
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(<ChatInterface />);
|
|
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(<ChatInterface />);
|
|
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(<ChatInterface />);
|
|
expect(screen.queryByTestId('chat-history')).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByTestId('toggle-history'));
|
|
await waitFor(() => { expect(screen.getByTestId('chat-history')).toBeInTheDocument(); });
|
|
});
|
|
});
|