/**
* @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);
});
});
});