/** * @ai-summary Unit tests for DocumentsMobileScreen component * @ai-context Tests mobile UI with mocked hooks and navigation */ import { render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { DocumentsMobileScreen } from './DocumentsMobileScreen'; import { useDocumentsList } from '../hooks/useDocuments'; import { useUploadWithProgress } from '../hooks/useUploadWithProgress'; import { useNavigate } from 'react-router-dom'; import type { DocumentRecord } from '../types/documents.types'; // Mock dependencies jest.mock('../hooks/useDocuments'); jest.mock('../hooks/useUploadWithProgress'); jest.mock('react-router-dom'); const mockUseDocumentsList = jest.mocked(useDocumentsList); const mockUseUploadWithProgress = jest.mocked(useUploadWithProgress); const mockUseNavigate = jest.mocked(useNavigate); describe('DocumentsMobileScreen', () => { const mockNavigate = jest.fn(); const mockUploadMutate = jest.fn(); const mockDocuments: DocumentRecord[] = [ { id: 'doc-1', user_id: 'user-1', vehicle_id: 'vehicle-1', document_type: 'insurance', title: 'Car Insurance', created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }, { id: 'doc-2', user_id: 'user-1', vehicle_id: 'vehicle-2', document_type: 'registration', title: 'Vehicle Registration', created_at: '2024-01-02T00:00:00Z', updated_at: '2024-01-02T00:00:00Z', }, ]; beforeEach(() => { jest.clearAllMocks(); mockUseNavigate.mockReturnValue(mockNavigate); mockUseUploadWithProgress.mockReturnValue({ mutate: mockUploadMutate, isPending: false, progress: 0, isSuccess: false, isError: false, error: null, resetProgress: jest.fn(), data: undefined, variables: undefined, isIdle: true, status: 'idle', mutateAsync: jest.fn(), reset: jest.fn(), } as any); mockUseDocumentsList.mockReturnValue({ data: mockDocuments, isLoading: false, error: null, refetch: jest.fn(), isError: false, isPending: false, isSuccess: true, status: 'success', dataUpdatedAt: 0, errorUpdatedAt: 0, failureCount: 0, failureReason: null, errorUpdateCount: 0, isFetched: true, isFetchedAfterMount: true, isFetching: false, isInitialLoading: false, isPlaceholderData: false, isPaused: false, isRefetching: false, isRefetchError: false, isLoadingError: false, isStale: false, } as any); }); describe('Document List Display', () => { it('should render documents list', () => { render(); expect(screen.getByText('Documents')).toBeInTheDocument(); expect(screen.getByText('Car Insurance')).toBeInTheDocument(); expect(screen.getByText('Vehicle Registration')).toBeInTheDocument(); }); it('should display document metadata', () => { render(); // Check document types and vehicle IDs are displayed expect(screen.getByText(/insurance/)).toBeInTheDocument(); expect(screen.getByText(/registration/)).toBeInTheDocument(); expect(screen.getByText(/vehicle-1/)).toBeInTheDocument(); expect(screen.getByText(/vehicle-2/)).toBeInTheDocument(); }); it('should truncate long vehicle IDs', () => { const longVehicleId = 'very-long-vehicle-id-that-should-be-truncated'; const documentsWithLongId = [ { ...mockDocuments[0], vehicle_id: longVehicleId, }, ]; mockUseDocumentsList.mockReturnValue({ data: documentsWithLongId, isLoading: false, error: null, refetch: jest.fn(), } as any); render(); // Should show truncated version expect(screen.getByText(/very-lon\.\.\./)).toBeInTheDocument(); expect(screen.queryByText(longVehicleId)).not.toBeInTheDocument(); }); }); describe('Loading States', () => { it('should show loading message', () => { mockUseDocumentsList.mockReturnValue({ data: undefined, isLoading: true, error: null, refetch: jest.fn(), } as any); render(); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); it('should show error message', () => { mockUseDocumentsList.mockReturnValue({ data: undefined, isLoading: false, error: new Error('Failed to load'), refetch: jest.fn(), } as any); render(); expect(screen.getByText('Failed to load documents')).toBeInTheDocument(); }); it('should handle empty documents list', () => { mockUseDocumentsList.mockReturnValue({ data: [], isLoading: false, error: null, refetch: jest.fn(), } as any); render(); expect(screen.getByText('Documents')).toBeInTheDocument(); // Should not crash with empty list }); }); describe('Navigation', () => { it('should navigate to document detail when Open is clicked', async () => { const user = userEvent.setup(); render(); const openButtons = screen.getAllByText('Open'); await user.click(openButtons[0]); expect(mockNavigate).toHaveBeenCalledWith('/garage/documents/doc-1'); }); it('should navigate to correct document for each Open button', async () => { const user = userEvent.setup(); render(); const openButtons = screen.getAllByText('Open'); await user.click(openButtons[0]); expect(mockNavigate).toHaveBeenCalledWith('/garage/documents/doc-1'); await user.click(openButtons[1]); expect(mockNavigate).toHaveBeenCalledWith('/garage/documents/doc-2'); }); }); describe('File Upload', () => { let mockFileInput: HTMLInputElement; beforeEach(() => { // Mock file input element mockFileInput = document.createElement('input'); mockFileInput.type = 'file'; mockFileInput.click = jest.fn(); jest.spyOn(document, 'createElement').mockReturnValue(mockFileInput as any); }); it('should trigger file upload when Upload button is clicked', async () => { const user = userEvent.setup(); render(); const uploadButtons = screen.getAllByText('Upload'); await user.click(uploadButtons[0]); // Should clear and click the hidden file input expect(mockFileInput.value).toBe(''); expect(mockFileInput.click).toHaveBeenCalled(); }); it('should set correct document ID when upload button is clicked', async () => { const user = userEvent.setup(); render(); const uploadButtons = screen.getAllByText('Upload'); await user.click(uploadButtons[1]); // Click second document's upload // Verify the component tracks the current document ID // This is tested indirectly through the file change handler }); it('should handle file selection and upload', async () => { render(); const uploadButtons = screen.getAllByText('Upload'); fireEvent.click(uploadButtons[0]); // Simulate file selection const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); const fileInput = screen.getByRole('textbox', { hidden: true }) || document.querySelector('input[type="file"]') as HTMLInputElement; if (fileInput) { Object.defineProperty(fileInput, 'files', { value: [file], writable: false, }); fireEvent.change(fileInput); expect(mockUploadMutate).toHaveBeenCalledWith(file); } }); it('should show upload progress during upload', () => { mockUseUploadWithProgress.mockReturnValue({ mutate: mockUploadMutate, isPending: true, progress: 45, isSuccess: false, isError: false, error: null, resetProgress: jest.fn(), } as any); render(); expect(screen.getByText('45%')).toBeInTheDocument(); }); it('should show progress only for the uploading document', async () => { const user = userEvent.setup(); // Mock upload in progress for first document mockUseUploadWithProgress.mockImplementation((docId: string) => ({ mutate: mockUploadMutate, isPending: docId === 'doc-1', progress: docId === 'doc-1' ? 75 : 0, isSuccess: false, isError: false, error: null, resetProgress: jest.fn(), } as any)); render(); // Click upload for first document const uploadButtons = screen.getAllByText('Upload'); await user.click(uploadButtons[0]); // Should show progress for first document only expect(screen.getByText('75%')).toBeInTheDocument(); // Should not show progress for other documents const progressElements = screen.getAllByText(/\d+%/); expect(progressElements).toHaveLength(1); }); }); describe('File Input Configuration', () => { it('should configure file input with correct accept types', () => { render(); const fileInput = document.querySelector('input[type="file"]'); expect(fileInput).toBeTruthy(); expect(fileInput!).toHaveAttribute('accept', 'image/*,application/pdf'); }); it('should hide file input from UI', () => { render(); const fileInput = document.querySelector('input[type="file"]'); expect(fileInput).toBeTruthy(); expect(fileInput!).toHaveClass('hidden'); }); }); describe('Document Cards Layout', () => { it('should render documents in individual cards', () => { render(); // Each document should be in its own bordered container const documentCards = screen.getAllByRole('generic').filter(el => el.className.includes('border') && el.className.includes('rounded-xl') ); expect(documentCards.length).toBeGreaterThan(0); }); it('should display action buttons for each document', () => { render(); const openButtons = screen.getAllByText('Open'); const uploadButtons = screen.getAllByText('Upload'); expect(openButtons).toHaveLength(mockDocuments.length); expect(uploadButtons).toHaveLength(mockDocuments.length); }); }); describe('Error Handling', () => { it('should handle missing vehicle_id gracefully', () => { const documentsWithMissingVehicle = [ { ...mockDocuments[0], vehicle_id: null as any, }, ]; mockUseDocumentsList.mockReturnValue({ data: documentsWithMissingVehicle, isLoading: false, error: null, refetch: jest.fn(), } as any); render(); // Should show placeholder for missing vehicle ID expect(screen.getByText('—')).toBeInTheDocument(); }); it('should handle upload errors gracefully', () => { mockUseUploadWithProgress.mockReturnValue({ mutate: mockUploadMutate, isPending: false, progress: 0, isSuccess: false, isError: true, error: new Error('Upload failed'), resetProgress: jest.fn(), } as any); render(); // Component should still render without crashing expect(screen.getByText('Documents')).toBeInTheDocument(); }); }); describe('Accessibility', () => { it('should have proper heading structure', () => { render(); const heading = screen.getByRole('heading', { name: 'Documents' }); expect(heading).toBeInTheDocument(); expect(heading.tagName).toBe('H2'); }); it('should have accessible buttons', () => { render(); const openButtons = screen.getAllByRole('button', { name: 'Open' }); const uploadButtons = screen.getAllByRole('button', { name: 'Upload' }); expect(openButtons).toHaveLength(mockDocuments.length); expect(uploadButtons).toHaveLength(mockDocuments.length); }); }); });