import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { OCRUpload } from '@/components/ocr/OCRUpload'; import { OCRResults } from '@/components/ocr/OCRResults'; import { OCRDetail } from '@/components/ocr/OCRDetail'; import type { OCRResultResponse } from '@/lib/ocr'; // Mock fetch globally const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; // Mock localStorage const localStorageMock = (() => { let store: Record = {}; return { getItem: (key: string) => store[key] || null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { delete store[key]; }, clear: () => { store = {}; }, }; })(); Object.defineProperty(window, 'localStorage', { value: localStorageMock }); // Mock apiFetch to avoid auth header complexity for list/get tests vi.mock('@/lib/api', () => ({ apiFetch: vi.fn(), API_BASE_URL: 'http://localhost:8000/api/v1', })); const { apiFetch } = await import('@/lib/api'); beforeEach(() => { vi.clearAllMocks(); localStorageMock.clear(); }); const mockOCRResult: OCRResultResponse = { id: 'result-123', vehicle_id: 'vehicle-456', file_path: '/tmp/uploads/scan.png', file_name: 'scan.png', mime_type: 'image/png', status: 'completed', raw_text: '{"brand": "Mercedes"}', structured_data: { brand: 'Mercedes', model: 'Actros', vin: 'WDB9066351L123456', first_registration: '15.03.2020', mileage: 120000, power_kw: 350, fuel_type: 'Diesel', }, confidence_score: 0.92, error_message: null, created_at: '2026-07-16T10:00:00Z', updated_at: '2026-07-16T10:01:00Z', }; describe('OCRUpload', () => { it('renders drag-and-drop zone', () => { render(); expect(screen.getByTestId('ocr-dropzone')).toBeInTheDocument(); expect(screen.getByText('Drag and drop scan image here')).toBeInTheDocument(); }); it('shows file input on click', () => { render(); const input = screen.getByTestId('ocr-file-input'); expect(input).toHaveAttribute('type', 'file'); expect(input).toHaveAttribute('accept', 'image/*'); }); it('shows error for non-image file', async () => { render(); const input = screen.getByTestId('ocr-file-input') as HTMLInputElement; const file = new File(['data'], 'doc.pdf', { type: 'application/pdf' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('ocr-upload-error')).toBeInTheDocument(); expect(screen.getByText('Only image files are allowed')).toBeInTheDocument(); }); }); it('uploads image file successfully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ message: 'OCR processing queued', ocr_result_id: 'new-result-id', status: 'pending', }), }); let uploadedId = ''; render( { uploadedId = id; }} />); const input = screen.getByTestId('ocr-file-input') as HTMLInputElement; const file = new File(['image-data'], 'scan.png', { type: 'image/png' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('ocr-upload-success')).toBeInTheDocument(); expect(uploadedId).toBe('new-result-id'); }); expect(mockFetch).toHaveBeenCalledTimes(1); const call = mockFetch.mock.calls[0]; expect(call[0]).toContain('/ocr/upload'); expect(call[1].method).toBe('POST'); }); it('shows error on upload failure', async () => { mockFetch.mockResolvedValueOnce({ ok: false, json: async () => ({ error: { code: 'INVALID_MIME_TYPE', message: 'Invalid MIME type' }, }), }); render(); const input = screen.getByTestId('ocr-file-input') as HTMLInputElement; const file = new File(['image-data'], 'scan.png', { type: 'image/png' }); Object.defineProperty(input, 'files', { value: [file], writable: false }); fireEvent.change(input); await waitFor(() => { expect(screen.getByTestId('ocr-upload-error')).toBeInTheDocument(); expect(screen.getByText('Invalid MIME type')).toBeInTheDocument(); }); }); }); describe('OCRResults', () => { it('renders results list', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ items: [mockOCRResult], total: 1, page: 1, page_size: 20, }); render(); await waitFor(() => { expect(screen.getByText('scan.png')).toBeInTheDocument(); expect(screen.getByText('completed')).toBeInTheDocument(); }); }); it('shows loading state', async () => { vi.mocked(apiFetch).mockImplementationOnce( () => new Promise(resolve => setTimeout(() => resolve({ items: [], total: 0, page: 1, page_size: 20, }), 100)) ); render(); expect(screen.getByTestId('ocr-results-loading')).toBeInTheDocument(); }); it('shows error on fetch failure', async () => { vi.mocked(apiFetch).mockRejectedValueOnce({ error: { code: 'UNKNOWN', message: 'Network error' }, }); render(); await waitFor(() => { expect(screen.getByTestId('ocr-results-error')).toBeInTheDocument(); expect(screen.getByText('Network error')).toBeInTheDocument(); }); }); it('calls onSelectResult when row is clicked', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ items: [mockOCRResult], total: 1, page: 1, page_size: 20, }); const onSelect = vi.fn(); render(); await waitFor(() => { expect(screen.getByTestId('ocr-row-result-123')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('ocr-row-result-123')); expect(onSelect).toHaveBeenCalledWith(mockOCRResult); }); }); describe('OCRDetail', () => { it('renders side-by-side original and extracted data', () => { render(); expect(screen.getByTestId('ocr-original-scan')).toBeInTheDocument(); expect(screen.getByTestId('ocr-extracted-data')).toBeInTheDocument(); expect(screen.getByTestId('ocr-field-brand')).toHaveTextContent('Mercedes'); expect(screen.getByTestId('ocr-field-model')).toHaveTextContent('Actros'); expect(screen.getByTestId('ocr-field-vin')).toHaveTextContent('WDB9066351L123456'); expect(screen.getByTestId('ocr-field-mileage')).toHaveTextContent('120000 km'); expect(screen.getByTestId('ocr-field-power_kw')).toHaveTextContent('350 kW'); expect(screen.getByTestId('ocr-field-fuel_type')).toHaveTextContent('Diesel'); }); it('shows apply button when status is completed and vehicle linked', () => { render(); expect(screen.getByTestId('ocr-apply-button')).toBeInTheDocument(); }); it('hides apply button when no vehicle linked', () => { const noVehicleResult = { ...mockOCRResult, vehicle_id: null }; render(); expect(screen.queryByTestId('ocr-apply-button')).not.toBeInTheDocument(); }); it('hides apply button when status is pending', () => { const pendingResult = { ...mockOCRResult, status: 'pending' as const }; render(); expect(screen.queryByTestId('ocr-apply-button')).not.toBeInTheDocument(); }); it('sends POST apply request on button click', async () => { vi.mocked(apiFetch).mockResolvedValueOnce({ message: 'OCR data applied to vehicle', ocr_result_id: 'result-123', vehicle_id: 'vehicle-456', updated_fields: ['make', 'model', 'mileage_km'], }); render(); fireEvent.click(screen.getByTestId('ocr-apply-button')); await waitFor(() => { expect(screen.getByTestId('ocr-apply-success')).toBeInTheDocument(); expect(screen.getByText(/Applied 3 fields/)).toBeInTheDocument(); }); expect(apiFetch).toHaveBeenCalledWith('/ocr/results/result-123/apply', { method: 'POST', }); }); it('shows error on apply failure', async () => { vi.mocked(apiFetch).mockRejectedValueOnce({ error: { code: 'APPLY_FAILED', message: 'Vehicle not found' }, }); render(); fireEvent.click(screen.getByTestId('ocr-apply-button')); await waitFor(() => { expect(screen.getByTestId('ocr-apply-error')).toBeInTheDocument(); expect(screen.getByText('Vehicle not found')).toBeInTheDocument(); }); }); it('shows raw text when available', () => { render(); expect(screen.getByTestId('ocr-raw-text')).toBeInTheDocument(); expect(screen.getByTestId('ocr-raw-text')).toHaveTextContent('Mercedes'); }); it('shows error message when status is failed', () => { const failedResult: OCRResultResponse = { ...mockOCRResult, status: 'failed', error_message: 'OpenRouter API unavailable', structured_data: null, }; render(); expect(screen.getByText(/OpenRouter API unavailable/)).toBeInTheDocument(); }); it('shows no data message when structured_data is null', () => { const noDataResult: OCRResultResponse = { ...mockOCRResult, structured_data: null, }; render(); expect(screen.getByText('No structured data available yet.')).toBeInTheDocument(); }); });