import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { FileUpload } from '@/components/files/FileUpload'; import { FileList } from '@/components/files/FileList'; import { FileGallery } from '@/components/files/FileGallery'; import { FilePreview } from '@/components/files/FilePreview'; import { formatFileSize, isImageMime, MAX_FILE_SIZE_MB, } from '@/lib/files'; import { apiFetch } from '@/lib/api'; // Mock the api module vi.mock('@/lib/api', () => ({ apiFetch: vi.fn(), API_BASE_URL: 'http://localhost:8000/api/v1', })); const mockedApiFetch = vi.mocked(apiFetch); // Mock fetch for file operations const mockFetch = vi.fn(); global.fetch = mockFetch as any; // Mock URL.createObjectURL and URL.revokeObjectURL global.URL.createObjectURL = vi.fn(() => 'blob:http://test/blob123'); global.URL.revokeObjectURL = vi.fn(); describe('FileUpload', () => { beforeEach(() => { vi.clearAllMocks(); mockFetch.mockResolvedValue({ ok: true, status: 201, json: async () => ({ id: 'file-1', vehicle_id: 'veh-1', original_filename: 'test.jpg', stored_filename: 'abc123.jpg', file_path: '/tmp/uploads/veh-1/abc123.jpg', mime_type: 'image/jpeg', file_size: 1024, thumbnail_path: '/tmp/uploads/veh-1/thumbnails/thumb_abc123.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }), }); }); it('renders drag-and-drop zone', () => { render(); expect(screen.getByTestId('dropzone')).toBeDefined(); expect(screen.getByTestId('file-input')).toBeDefined(); expect(screen.getByText(/Drag & drop files or click to browse/i)).toBeDefined(); }); it('shows supported file types and size limit', () => { render(); expect(screen.getByText(new RegExp(`max ${MAX_FILE_SIZE_MB}MB`, 'i'))).toBeDefined(); }); it('accepts file selection via input', async () => { render(); const input = screen.getByTestId('file-input') as HTMLInputElement; const file = new File(['test image data'], 'test.jpg', { type: 'image/jpeg' }); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(mockFetch).toHaveBeenCalled(); }); }); it('shows upload status after file selection', async () => { render(); const input = screen.getByTestId('file-input') as HTMLInputElement; const file = new File(['test image data'], 'test.jpg', { type: 'image/jpeg' }); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(screen.getByTestId('upload-statuses')).toBeDefined(); }); }); it('rejects unsupported file types', async () => { render(); const input = screen.getByTestId('file-input') as HTMLInputElement; const file = new File(['malware'], 'malware.exe', { type: 'application/x-msdownload' }); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(screen.getByText(/Unsupported file type/i)).toBeDefined(); }); }); it('rejects oversized files', async () => { render(); const input = screen.getByTestId('file-input') as HTMLInputElement; // Create a file larger than 20MB const largeContent = new Array(21 * 1024 * 1024).fill('x').join(''); const file = new File([largeContent], 'large.jpg', { type: 'image/jpeg' }); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(screen.getByText(/exceeds.*MB limit/i)).toBeDefined(); }); }); it('calls onUploadComplete on successful upload', async () => { const onUploadComplete = vi.fn(); render(); const input = screen.getByTestId('file-input') as HTMLInputElement; const file = new File(['test image data'], 'test.jpg', { type: 'image/jpeg' }); fireEvent.change(input, { target: { files: [file] } }); await waitFor(() => { expect(onUploadComplete).toHaveBeenCalled(); }); }); }); describe('FileList', () => { beforeEach(() => { vi.clearAllMocks(); mockedApiFetch.mockResolvedValue({ items: [ { id: 'file-1', vehicle_id: 'veh-1', original_filename: 'photo.jpg', stored_filename: 'abc123.jpg', file_path: '/tmp/uploads/veh-1/abc123.jpg', mime_type: 'image/jpeg', file_size: 102400, thumbnail_path: '/tmp/uploads/veh-1/thumbnails/thumb_abc123.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, { id: 'file-2', vehicle_id: 'veh-1', original_filename: 'document.pdf', stored_filename: 'def456.pdf', file_path: '/tmp/uploads/veh-1/def456.pdf', mime_type: 'application/pdf', file_size: 204800, thumbnail_path: null, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, ], total: 2, page: 1, page_size: 20, } as any); }); it('renders file list with items', async () => { render(); await waitFor(() => { expect(screen.getByTestId('file-list')).toBeDefined(); }); expect(screen.getByText('photo.jpg')).toBeDefined(); expect(screen.getByText('document.pdf')).toBeDefined(); }); it('shows thumbnails for image files', async () => { render(); await waitFor(() => { expect(screen.getByTestId('file-thumb-file-1')).toBeDefined(); }); }); it('shows file icon for non-image files', async () => { render(); await waitFor(() => { expect(screen.getByTestId('file-item-file-2')).toBeDefined(); }); // Non-image file should not have a thumbnail img expect(screen.queryByTestId('file-thumb-file-2')).toBeNull(); }); it('shows delete button for each file', async () => { render(); await waitFor(() => { expect(screen.getByTestId('delete-btn-file-1')).toBeDefined(); expect(screen.getByTestId('delete-btn-file-2')).toBeDefined(); }); }); it('shows download button for each file', async () => { render(); await waitFor(() => { expect(screen.getByTestId('download-btn-file-1')).toBeDefined(); expect(screen.getByTestId('download-btn-file-2')).toBeDefined(); }); }); it('opens delete confirmation dialog', async () => { render(); await waitFor(() => { expect(screen.getByTestId('delete-btn-file-1')).toBeDefined(); }); fireEvent.click(screen.getByTestId('delete-btn-file-1')); await waitFor(() => { expect(screen.getByTestId('delete-confirm')).toBeDefined(); expect(screen.getByTestId('confirm-delete-btn')).toBeDefined(); }); }); it('shows empty state when no files', async () => { mockedApiFetch.mockResolvedValueOnce({ items: [], total: 0, page: 1, page_size: 20, } as any); render(); await waitFor(() => { expect(screen.getByTestId('file-list-empty')).toBeDefined(); }); }); it('shows error state on API failure', async () => { mockedApiFetch.mockRejectedValueOnce({ error: { code: 'UNKNOWN', message: 'Network error' }, }); render(); await waitFor(() => { expect(screen.getByTestId('file-list-error')).toBeDefined(); }); }); }); describe('FileGallery', () => { beforeEach(() => { vi.clearAllMocks(); mockedApiFetch.mockResolvedValue({ items: [ { id: 'img-1', vehicle_id: 'veh-1', original_filename: 'photo1.jpg', stored_filename: 'abc123.jpg', file_path: '/tmp/uploads/veh-1/abc123.jpg', mime_type: 'image/jpeg', file_size: 102400, thumbnail_path: '/tmp/uploads/veh-1/thumbnails/thumb_abc123.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, { id: 'img-2', vehicle_id: 'veh-1', original_filename: 'photo2.png', stored_filename: 'def456.png', file_path: '/tmp/uploads/veh-1/def456.png', mime_type: 'image/png', file_size: 204800, thumbnail_path: '/tmp/uploads/veh-1/thumbnails/thumb_def456.png', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, ], total: 2, page: 1, page_size: 100, } as any); }); it('renders gallery with images', async () => { render(); await waitFor(() => { expect(screen.getByTestId('file-gallery')).toBeDefined(); }); expect(screen.getByTestId('gallery-item-img-1')).toBeDefined(); expect(screen.getByTestId('gallery-item-img-2')).toBeDefined(); }); it('shows empty state when no images', async () => { mockedApiFetch.mockResolvedValueOnce({ items: [], total: 0, page: 1, page_size: 100, } as any); render(); await waitFor(() => { expect(screen.getByTestId('gallery-empty')).toBeDefined(); }); }); it('filters out non-image files', async () => { mockedApiFetch.mockResolvedValueOnce({ items: [ { id: 'img-1', vehicle_id: 'veh-1', original_filename: 'photo.jpg', stored_filename: 'abc.jpg', file_path: '/tmp/abc.jpg', mime_type: 'image/jpeg', file_size: 1024, thumbnail_path: '/tmp/thumb.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, { id: 'doc-1', vehicle_id: 'veh-1', original_filename: 'doc.pdf', stored_filename: 'def.pdf', file_path: '/tmp/def.pdf', mime_type: 'application/pdf', file_size: 2048, thumbnail_path: null, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }, ], total: 2, page: 1, page_size: 100, } as any); render(); await waitFor(() => { expect(screen.getByTestId('gallery-item-img-1')).toBeDefined(); }); expect(screen.queryByTestId('gallery-item-doc-1')).toBeNull(); }); }); describe('FilePreview', () => { it('renders image preview for image files', () => { const file = { id: 'file-1', vehicle_id: 'veh-1', original_filename: 'photo.jpg', stored_filename: 'abc.jpg', file_path: '/tmp/abc.jpg', mime_type: 'image/jpeg', file_size: 102400, thumbnail_path: '/tmp/thumb.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; render( {}} />); expect(screen.getByTestId('file-preview')).toBeDefined(); expect(screen.getByTestId('preview-image')).toBeDefined(); }); it('renders placeholder for non-image files', () => { const file = { id: 'file-2', vehicle_id: 'veh-1', original_filename: 'doc.pdf', stored_filename: 'def.pdf', file_path: '/tmp/def.pdf', mime_type: 'application/pdf', file_size: 204800, thumbnail_path: null, created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; render( {}} />); expect(screen.getByTestId('file-preview')).toBeDefined(); expect(screen.queryByTestId('preview-image')).toBeNull(); expect(screen.getByText(/Preview not available/i)).toBeDefined(); }); it('shows file metadata', () => { const file = { id: 'file-1', vehicle_id: 'veh-1', original_filename: 'photo.jpg', stored_filename: 'abc.jpg', file_path: '/tmp/abc.jpg', mime_type: 'image/jpeg', file_size: 102400, thumbnail_path: '/tmp/thumb.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; render( {}} />); expect(screen.getByText('image/jpeg')).toBeDefined(); expect(screen.getByText('100.0 KB')).toBeDefined(); }); it('shows download button', () => { const file = { id: 'file-1', vehicle_id: 'veh-1', original_filename: 'photo.jpg', stored_filename: 'abc.jpg', file_path: '/tmp/abc.jpg', mime_type: 'image/jpeg', file_size: 1024, thumbnail_path: '/tmp/thumb.jpg', created_at: '2025-01-01T00:00:00Z', updated_at: '2025-01-01T00:00:00Z', }; render( {}} />); expect(screen.getByTestId('preview-download-btn')).toBeDefined(); }); it('returns null when file is null', () => { const { container } = render( {}} />); expect(container.firstChild).toBeNull(); }); }); describe('Utility functions', () => { it('formatFileSize formats bytes correctly', () => { expect(formatFileSize(0)).toBe('0 B'); expect(formatFileSize(512)).toBe('512 B'); expect(formatFileSize(1024)).toBe('1.0 KB'); expect(formatFileSize(1024 * 1024)).toBe('1.0 MB'); expect(formatFileSize(1024 * 1024 * 5)).toBe('5.0 MB'); }); it('isImageMime detects image MIME types', () => { expect(isImageMime('image/jpeg')).toBe(true); expect(isImageMime('image/png')).toBe(true); expect(isImageMime('image/webp')).toBe(true); expect(isImageMime('application/pdf')).toBe(false); expect(isImageMime('text/plain')).toBe(false); }); });