diff --git a/frontend/src/components/FailedDocumentViewer.tsx b/frontend/src/components/FailedDocumentViewer.tsx index 8b7b0fb..b49d3b8 100644 --- a/frontend/src/components/FailedDocumentViewer.tsx +++ b/frontend/src/components/FailedDocumentViewer.tsx @@ -91,7 +91,7 @@ const FailedDocumentViewer: React.FC = ({ }}> {documentUrl && ( <> - {mimeType.startsWith('image/') ? ( + {mimeType?.startsWith('image/') ? ( = ({ style={{ border: 'none', borderRadius: '4px' }} title={filename} /> - ) : mimeType.startsWith('text/') ? ( + ) : mimeType?.startsWith('text/') ? ( = ({ ) : ( - Cannot preview this file type ({mimeType}) + Cannot preview this file type ({mimeType || 'unknown'}) File: {filename} diff --git a/frontend/src/components/__tests__/FailedDocumentViewer.test.tsx b/frontend/src/components/__tests__/FailedDocumentViewer.test.tsx new file mode 100644 index 0000000..c5b4a06 --- /dev/null +++ b/frontend/src/components/__tests__/FailedDocumentViewer.test.tsx @@ -0,0 +1,493 @@ +import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; +import FailedDocumentViewer from '../FailedDocumentViewer'; +import { api } from '../../services/api'; + +// Mock the API +vi.mock('../../services/api', () => ({ + api: { + get: vi.fn(), + }, +})); + +const theme = createTheme(); + +const defaultProps = { + failedDocumentId: 'test-failed-doc-id', + filename: 'test-document.pdf', + mimeType: 'application/pdf', +}; + +const renderFailedDocumentViewer = (props = {}) => { + const combinedProps = { ...defaultProps, ...props }; + + return render( + + + + ); +}; + +// Mock Blob and URL.createObjectURL +const mockBlob = vi.fn(() => ({ + text: () => Promise.resolve('mock text content'), +})); +global.Blob = mockBlob as any; + +const mockCreateObjectURL = vi.fn(() => 'mock-object-url'); +const mockRevokeObjectURL = vi.fn(); +global.URL = { + createObjectURL: mockCreateObjectURL, + revokeObjectURL: mockRevokeObjectURL, +} as any; + +describe('FailedDocumentViewer', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('Loading State', () => { + test('should show loading spinner initially', () => { + // Mock API to never resolve + vi.mocked(api.get).mockImplementation(() => new Promise(() => {})); + + renderFailedDocumentViewer(); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + test('should show loading spinner with correct styling', () => { + vi.mocked(api.get).mockImplementation(() => new Promise(() => {})); + + renderFailedDocumentViewer(); + + const loadingContainer = screen.getByRole('progressbar').closest('div'); + expect(loadingContainer).toHaveStyle({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: '200px' + }); + }); + }); + + describe('Successful Document Loading', () => { + test('should load and display PDF document', async () => { + const mockResponse = { + data: new Blob(['mock pdf content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/documents/failed/test-failed-doc-id/view', { + responseType: 'blob' + }); + }); + + await waitFor(() => { + const iframe = screen.getByTitle('test-document.pdf'); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute('src', 'mock-object-url'); + expect(iframe).toHaveAttribute('width', '100%'); + expect(iframe).toHaveAttribute('height', '400px'); + }); + }); + + test('should load and display image document', async () => { + const mockResponse = { + data: new Blob(['mock image content'], { type: 'image/jpeg' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + filename: 'test-image.jpg', + mimeType: 'image/jpeg' + }); + + await waitFor(() => { + const image = screen.getByAltText('test-image.jpg'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'mock-object-url'); + expect(image).toHaveStyle({ + maxWidth: '100%', + maxHeight: '400px', + objectFit: 'contain', + }); + }); + }); + + test('should load and display text document', async () => { + const mockResponse = { + data: new Blob(['mock text content'], { type: 'text/plain' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + filename: 'test-file.txt', + mimeType: 'text/plain' + }); + + await waitFor(() => { + const iframe = screen.getByTitle('test-file.txt'); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute('src', 'mock-object-url'); + }); + }); + + test('should show unsupported file type message', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/unknown' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + filename: 'test-file.unknown', + mimeType: 'application/unknown' + }); + + await waitFor(() => { + expect(screen.getByText('Cannot preview this file type (application/unknown)')).toBeInTheDocument(); + expect(screen.getByText('File: test-file.unknown')).toBeInTheDocument(); + expect(screen.getByText('You can try downloading the file to view it locally.')).toBeInTheDocument(); + }); + }); + }); + + describe('Error Handling', () => { + test('should show 404 error when document not found', async () => { + const error = { + response: { status: 404 } + }; + vi.mocked(api.get).mockRejectedValueOnce(error); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(screen.getByText('Document file not found or has been deleted')).toBeInTheDocument(); + expect(screen.getByText('The original file may have been deleted or moved from storage.')).toBeInTheDocument(); + }); + }); + + test('should show generic error for other failures', async () => { + const error = new Error('Network error'); + vi.mocked(api.get).mockRejectedValueOnce(error); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(screen.getByText('Failed to load document for viewing')).toBeInTheDocument(); + expect(screen.getByText('The original file may have been deleted or moved from storage.')).toBeInTheDocument(); + }); + }); + + test('should handle API errors gracefully', async () => { + const error = { + response: { status: 500 } + }; + vi.mocked(api.get).mockRejectedValueOnce(error); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(screen.getByText('Failed to load document for viewing')).toBeInTheDocument(); + }); + }); + }); + + describe('Memory Management', () => { + test('should create object URL when loading document', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(mockCreateObjectURL).toHaveBeenCalled(); + }); + + // Should display the document + await waitFor(() => { + expect(screen.getByTitle(defaultProps.filename)).toBeInTheDocument(); + }); + }); + + test('should create new object URL when failedDocumentId changes', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValue(mockResponse); + + const { rerender } = renderFailedDocumentViewer(); + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/documents/failed/test-failed-doc-id/view', { + responseType: 'blob' + }); + }); + + // Change the failedDocumentId + rerender( + + + + ); + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/documents/failed/new-doc-id/view', { + responseType: 'blob' + }); + }); + + expect(api.get).toHaveBeenCalledTimes(2); + }); + }); + + describe('Document Types', () => { + test('should handle PDF documents correctly', async () => { + const mockResponse = { + data: new Blob(['mock pdf content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + mimeType: 'application/pdf' + }); + + await waitFor(() => { + const iframe = screen.getByTitle(defaultProps.filename); + expect(iframe).toBeInTheDocument(); + expect(iframe.tagName).toBe('IFRAME'); + }); + }); + + test('should handle various image types', async () => { + const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + + for (const mimeType of imageTypes) { + const mockResponse = { + data: new Blob(['mock image content'], { type: mimeType }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + const filename = `test.${mimeType.split('/')[1]}`; + renderFailedDocumentViewer({ + filename, + mimeType + }); + + await waitFor(() => { + const image = screen.getByAltText(filename); + expect(image).toBeInTheDocument(); + expect(image.tagName).toBe('IMG'); + }); + + // Clean up for next iteration + screen.getByAltText(filename).remove(); + } + }); + + test('should handle text documents', async () => { + const textTypes = ['text/plain', 'text/html', 'text/css']; + + for (const mimeType of textTypes) { + const mockResponse = { + data: new Blob(['mock text content'], { type: mimeType }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + const filename = `test.${mimeType.split('/')[1]}`; + renderFailedDocumentViewer({ + filename, + mimeType + }); + + await waitFor(() => { + const iframe = screen.getByTitle(filename); + expect(iframe).toBeInTheDocument(); + expect(iframe.tagName).toBe('IFRAME'); + }); + + // Clean up for next iteration + screen.getByTitle(filename).remove(); + } + }); + }); + + describe('Styling and Layout', () => { + test('should apply correct Paper styling', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + const paper = screen.getByTitle(defaultProps.filename).closest('.MuiPaper-root'); + expect(paper).toHaveClass('MuiPaper-root'); + }); + }); + + test('should center images properly', async () => { + const mockResponse = { + data: new Blob(['mock image content'], { type: 'image/jpeg' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + mimeType: 'image/jpeg' + }); + + await waitFor(() => { + const imageContainer = screen.getByAltText(defaultProps.filename).closest('div'); + expect(imageContainer).toHaveStyle({ + textAlign: 'center' + }); + }); + }); + }); + + describe('API Call Parameters', () => { + test('should call API with correct endpoint and parameters', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/documents/failed/test-failed-doc-id/view', { + responseType: 'blob' + }); + }); + }); + + test('should handle different document IDs correctly', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + failedDocumentId: 'different-doc-id' + }); + + await waitFor(() => { + expect(api.get).toHaveBeenCalledWith('/documents/failed/different-doc-id/view', { + responseType: 'blob' + }); + }); + }); + }); + + describe('Edge Cases', () => { + test('should handle empty blob response', async () => { + const mockResponse = { + data: new Blob([], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + // Should still create object URL and show iframe + expect(mockCreateObjectURL).toHaveBeenCalled(); + expect(screen.getByTitle(defaultProps.filename)).toBeInTheDocument(); + }); + }); + + test('should handle very long filenames', async () => { + const longFilename = 'a'.repeat(500) + '.pdf'; + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + filename: longFilename + }); + + await waitFor(() => { + expect(screen.getByTitle(longFilename)).toBeInTheDocument(); + }); + }); + + test('should handle special characters in filename', async () => { + const specialFilename = 'test file & "quotes" .pdf'; + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + filename: specialFilename + }); + + await waitFor(() => { + expect(screen.getByTitle(specialFilename)).toBeInTheDocument(); + }); + }); + + test('should handle undefined or null mimeType gracefully', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: '' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + mimeType: undefined as any + }); + + await waitFor(() => { + // Should show unsupported file type message + expect(screen.getByText(/Cannot preview this file type \(unknown\)/)).toBeInTheDocument(); + }); + }); + }); + + describe('Accessibility', () => { + test('should have proper ARIA attributes', async () => { + const mockResponse = { + data: new Blob(['mock content'], { type: 'application/pdf' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer(); + + await waitFor(() => { + const iframe = screen.getByTitle(defaultProps.filename); + expect(iframe).toHaveAttribute('title', defaultProps.filename); + }); + }); + + test('should have proper alt text for images', async () => { + const mockResponse = { + data: new Blob(['mock image content'], { type: 'image/jpeg' }), + }; + vi.mocked(api.get).mockResolvedValueOnce(mockResponse); + + renderFailedDocumentViewer({ + mimeType: 'image/jpeg' + }); + + await waitFor(() => { + const image = screen.getByAltText(defaultProps.filename); + expect(image).toBeInTheDocument(); + }); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/pages/DocumentManagementPage.tsx b/frontend/src/pages/DocumentManagementPage.tsx index 7c865db..b8d6a53 100644 --- a/frontend/src/pages/DocumentManagementPage.tsx +++ b/frontend/src/pages/DocumentManagementPage.tsx @@ -1020,11 +1020,11 @@ const DocumentManagementPage: React.FC = () => { {/* Show OCR confidence and word count for low confidence failures */} {(document.failure_reason === 'low_ocr_confidence' || document.ocr_failure_reason === 'low_ocr_confidence') && ( <> - + OCR Results: - - {document.ocr_confidence !== undefined && ( + + {document.ocr_confidence !== undefined && document.ocr_confidence !== null && ( } @@ -1959,28 +1959,28 @@ const DocumentManagementPage: React.FC = () => { Document Information - + Original Filename: {selectedDocument.original_filename} - + File Size: {formatFileSize(selectedDocument.file_size)} - + MIME Type: {selectedDocument.mime_type} - + Failure Category: { sx={{ mb: 2 }} /> - + Retry Count: {selectedDocument.retry_count} attempts - + Created: {format(new Date(selectedDocument.created_at), 'PPpp')} - + Last Updated: @@ -2013,13 +2013,13 @@ const DocumentManagementPage: React.FC = () => { Tags: - + {selectedDocument.tags.length > 0 ? ( selectedDocument.tags.map((tag) => ( )) ) : ( - No tags + No tags )} @@ -2031,7 +2031,7 @@ const DocumentManagementPage: React.FC = () => { Error Details - + Full Error Message: ({ + api: { + get: vi.fn(), + delete: vi.fn(), + }, + documentService: { + getFailedDocuments: vi.fn(), + getFailedOcrDocuments: vi.fn(), + getDuplicates: vi.fn(), + retryOcr: vi.fn(), + deleteLowConfidence: vi.fn(), + deleteFailedOcr: vi.fn(), + downloadFile: vi.fn(), + }, + queueService: { + requeueFailed: vi.fn(), + }, +})); + +const theme = createTheme(); + +const DocumentManagementPageWrapper = ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + ); +}; + +describe('DocumentManagementPage - Runtime Error Prevention', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('OCR Confidence Display - Null Safety', () => { + test('should handle null ocr_confidence without crashing', async () => { + const mockFailedDocument = { + id: 'test-doc-1', + filename: 'test.pdf', + original_filename: 'test.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'low_ocr_confidence', + failure_category: 'OCR Error', + retry_count: 0, + can_retry: true, + ocr_confidence: null, // This should not cause a crash + ocr_word_count: 10, + error_message: 'Low confidence OCR result', + }; + + // Mock the API service + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + // Wait for data to load + await waitFor(() => { + expect(screen.getByText('test.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should not show confidence chip since ocr_confidence is null + await waitFor(() => { + expect(screen.queryByText(/confidence/)).not.toBeInTheDocument(); + }); + + // But should show word count if available + if (mockFailedDocument.ocr_word_count) { + expect(screen.getByText(/10 words found/)).toBeInTheDocument(); + } + }); + + test('should handle undefined ocr_confidence without crashing', async () => { + const mockFailedDocument = { + id: 'test-doc-2', + filename: 'test2.pdf', + original_filename: 'test2.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'low_ocr_confidence', + failure_category: 'OCR Error', + retry_count: 0, + can_retry: true, + // ocr_confidence is undefined + ocr_word_count: undefined, + error_message: 'Low confidence OCR result', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test2.pdf')).toBeInTheDocument(); + }); + + // Should render without crashing + expect(screen.getByText('Document Management')).toBeInTheDocument(); + }); + + test('should properly display valid ocr_confidence values', async () => { + const mockFailedDocument = { + id: 'test-doc-3', + filename: 'test3.pdf', + original_filename: 'test3.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'low_ocr_confidence', + failure_category: 'OCR Error', + retry_count: 0, + can_retry: true, + ocr_confidence: 15.7, // Valid number + ocr_word_count: 42, + error_message: 'Low confidence OCR result', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test3.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should show confidence with proper formatting + await waitFor(() => { + expect(screen.getByText('15.7% confidence')).toBeInTheDocument(); + expect(screen.getByText('42 words found')).toBeInTheDocument(); + }); + }); + }); + + describe('HTML Structure Validation', () => { + test('should not nest block elements inside Typography components', async () => { + const mockFailedDocument = { + id: 'test-doc-4', + filename: 'test4.pdf', + original_filename: 'test4.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: ['tag1', 'tag2'], + failure_reason: 'low_ocr_confidence', + failure_category: 'OCR Error', + retry_count: 0, + can_retry: true, + ocr_confidence: 25.5, + ocr_word_count: 15, + error_message: 'Test error message', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test4.pdf')).toBeInTheDocument(); + }); + + // Click "View Details" to open the dialog + const viewButton = screen.getByLabelText(/view details/i) || screen.getByText(/view details/i); + fireEvent.click(viewButton); + + await waitFor(() => { + expect(screen.getByText('Document Details: test4.pdf')).toBeInTheDocument(); + }); + + // Check that tags are displayed correctly without HTML structure issues + expect(screen.getByText('tag1')).toBeInTheDocument(); + expect(screen.getByText('tag2')).toBeInTheDocument(); + + // Check that all sections render without throwing HTML validation errors + expect(screen.getByText('Original Filename:')).toBeInTheDocument(); + expect(screen.getByText('File Size:')).toBeInTheDocument(); + expect(screen.getByText('MIME Type:')).toBeInTheDocument(); + expect(screen.getByText('Full Error Message:')).toBeInTheDocument(); + }); + }); + + describe('Error Data Field Access', () => { + test('should handle missing error_message field gracefully', async () => { + const mockFailedDocument = { + id: 'test-doc-5', + filename: 'test5.pdf', + original_filename: 'test5.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'processing_error', + failure_category: 'Processing Error', + retry_count: 0, + can_retry: true, + // error_message is missing + // ocr_error is missing too + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test5.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should show fallback text + await waitFor(() => { + expect(screen.getByText('No error message available')).toBeInTheDocument(); + }); + }); + + test('should prioritize error_message over ocr_error', async () => { + const mockFailedDocument = { + id: 'test-doc-6', + filename: 'test6.pdf', + original_filename: 'test6.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'processing_error', + failure_category: 'Processing Error', + retry_count: 0, + can_retry: true, + error_message: 'New error message format', + ocr_error: 'Old OCR error format', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test6.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should show the new error_message format, not ocr_error + await waitFor(() => { + expect(screen.getByText('New error message format')).toBeInTheDocument(); + expect(screen.queryByText('Old OCR error format')).not.toBeInTheDocument(); + }); + }); + + test('should fallback to ocr_error when error_message is missing', async () => { + const mockFailedDocument = { + id: 'test-doc-7', + filename: 'test7.pdf', + original_filename: 'test7.pdf', + file_size: 1024, + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'ocr_error', + failure_category: 'OCR Error', + retry_count: 0, + can_retry: true, + ocr_error: 'OCR processing failed', + // error_message is missing + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test7.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should show the OCR error + await waitFor(() => { + expect(screen.getByText('OCR processing failed')).toBeInTheDocument(); + }); + }); + }); + + describe('Ignored Files Tab Functionality', () => { + test('should render ignored files tab without errors', async () => { + // Mock ignored files API responses + const { api } = await import('../../services/api'); + vi.mocked(api.get).mockImplementation((url) => { + if (url.includes('/ignored-files/stats')) { + return Promise.resolve({ + data: { + total_ignored_files: 5, + total_size_bytes: 1024000, + most_recent_ignored_at: '2024-01-01T00:00:00Z', + } + }); + } + if (url.includes('/ignored-files')) { + return Promise.resolve({ + data: { + ignored_files: [], + total: 0, + } + }); + } + return Promise.resolve({ data: {} }); + }); + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [], + pagination: { total: 0, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 0, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Document Management')).toBeInTheDocument(); + }); + + // Click on the Ignored Files tab + const ignoredFilesTab = screen.getByText(/Ignored Files/); + fireEvent.click(ignoredFilesTab); + + // Should render without errors + await waitFor(() => { + expect(screen.getByText('Ignored Files Management')).toBeInTheDocument(); + }); + }); + }); + + describe('Edge Cases and Boundary Conditions', () => { + test('should handle empty arrays and null values', async () => { + const mockFailedDocument = { + id: 'test-doc-8', + filename: 'test8.pdf', + original_filename: 'test8.pdf', + file_size: 0, // Edge case: zero file size + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], // Empty array + failure_reason: 'unknown', + failure_category: 'Unknown', + retry_count: 0, + can_retry: false, + ocr_confidence: 0, // Edge case: zero confidence + ocr_word_count: 0, // Edge case: zero words + error_message: '', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test8.pdf')).toBeInTheDocument(); + }); + + // Should render without crashing even with edge case values + expect(screen.getByText('Document Management')).toBeInTheDocument(); + }); + + test('should handle very large numbers without crashing', async () => { + const mockFailedDocument = { + id: 'test-doc-9', + filename: 'test9.pdf', + original_filename: 'test9.pdf', + file_size: Number.MAX_SAFE_INTEGER, // Very large number + mime_type: 'application/pdf', + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + tags: [], + failure_reason: 'low_ocr_confidence', + failure_category: 'OCR Error', + retry_count: 999, + can_retry: true, + ocr_confidence: 99.999999, // High precision number + ocr_word_count: 1000000, // Large word count + error_message: 'Test error', + }; + + const { documentService } = await import('../../services/api'); + vi.mocked(documentService.getFailedDocuments).mockResolvedValueOnce({ + data: { + documents: [mockFailedDocument], + pagination: { total: 1, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 1, by_reason: {}, by_stage: {} }, + }, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('test9.pdf')).toBeInTheDocument(); + }); + + // Expand the row to see details + const expandButton = screen.getByLabelText(/expand/i) || screen.getAllByRole('button')[0]; + fireEvent.click(expandButton); + + // Should handle large numbers gracefully + await waitFor(() => { + expect(screen.getByText('100.0% confidence')).toBeInTheDocument(); // Should be rounded properly + expect(screen.getByText('1000000 words found')).toBeInTheDocument(); + }); + }); + }); + + describe('Component Lifecycle and State Management', () => { + test('should handle rapid tab switching without errors', async () => { + const { documentService } = await import('../../services/api'); + const { api } = await import('../../services/api'); + + // Mock all necessary API calls + vi.mocked(documentService.getFailedDocuments).mockResolvedValue({ + data: { + documents: [], + pagination: { total: 0, limit: 25, offset: 0, total_pages: 1 }, + statistics: { total_failed: 0, by_reason: {}, by_stage: {} }, + }, + }); + + vi.mocked(documentService.getDuplicates).mockResolvedValue({ + data: { + duplicates: [], + pagination: { total: 0, limit: 25, offset: 0, has_more: false }, + statistics: { total_duplicate_groups: 0 }, + }, + }); + + vi.mocked(api.get).mockResolvedValue({ + data: { + ignored_files: [], + total: 0, + } + }); + + const user = userEvent.setup(); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Document Management')).toBeInTheDocument(); + }); + + // Rapidly switch between tabs + const tabs = screen.getAllByRole('tab'); + + for (let i = 0; i < tabs.length; i++) { + await user.click(tabs[i]); + // Wait a minimal amount to ensure state updates + await waitFor(() => { + expect(tabs[i]).toHaveAttribute('aria-selected', 'true'); + }); + } + + // Should not crash or throw errors + expect(screen.getByText('Document Management')).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file