feat(client): fix faileddocumentviewer
This commit is contained in:
parent
5f10a8b82c
commit
bf98aaab85
|
|
@ -91,7 +91,7 @@ const FailedDocumentViewer: React.FC<FailedDocumentViewerProps> = ({
|
||||||
}}>
|
}}>
|
||||||
{documentUrl && (
|
{documentUrl && (
|
||||||
<>
|
<>
|
||||||
{mimeType.startsWith('image/') ? (
|
{mimeType?.startsWith('image/') ? (
|
||||||
<Box sx={{ textAlign: 'center' }}>
|
<Box sx={{ textAlign: 'center' }}>
|
||||||
<img
|
<img
|
||||||
src={documentUrl}
|
src={documentUrl}
|
||||||
|
|
@ -111,7 +111,7 @@ const FailedDocumentViewer: React.FC<FailedDocumentViewerProps> = ({
|
||||||
style={{ border: 'none', borderRadius: '4px' }}
|
style={{ border: 'none', borderRadius: '4px' }}
|
||||||
title={filename}
|
title={filename}
|
||||||
/>
|
/>
|
||||||
) : mimeType.startsWith('text/') ? (
|
) : mimeType?.startsWith('text/') ? (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
|
@ -133,7 +133,7 @@ const FailedDocumentViewer: React.FC<FailedDocumentViewerProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography variant="body1" color="text.secondary">
|
||||||
Cannot preview this file type ({mimeType})
|
Cannot preview this file type ({mimeType || 'unknown'})
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
File: {filename}
|
File: {filename}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<FailedDocumentViewer {...combinedProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<FailedDocumentViewer
|
||||||
|
failedDocumentId="new-doc-id"
|
||||||
|
filename="test-document.pdf"
|
||||||
|
mimeType="application/pdf"
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
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" <brackets>.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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1020,11 +1020,11 @@ const DocumentManagementPage: React.FC = () => {
|
||||||
{/* Show OCR confidence and word count for low confidence failures */}
|
{/* Show OCR confidence and word count for low confidence failures */}
|
||||||
{(document.failure_reason === 'low_ocr_confidence' || document.ocr_failure_reason === 'low_ocr_confidence') && (
|
{(document.failure_reason === 'low_ocr_confidence' || document.ocr_failure_reason === 'low_ocr_confidence') && (
|
||||||
<>
|
<>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>OCR Results:</strong>
|
<strong>OCR Results:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ mb: 1, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
<Box component="div" sx={{ mb: 1, display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
{document.ocr_confidence !== undefined && (
|
{document.ocr_confidence !== undefined && document.ocr_confidence !== null && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
icon={<WarningIcon />}
|
icon={<WarningIcon />}
|
||||||
|
|
@ -1959,28 +1959,28 @@ const DocumentManagementPage: React.FC = () => {
|
||||||
Document Information
|
Document Information
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>Original Filename:</strong>
|
<strong>Original Filename:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
{selectedDocument.original_filename}
|
{selectedDocument.original_filename}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>File Size:</strong>
|
<strong>File Size:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
{formatFileSize(selectedDocument.file_size)}
|
{formatFileSize(selectedDocument.file_size)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>MIME Type:</strong>
|
<strong>MIME Type:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
{selectedDocument.mime_type}
|
{selectedDocument.mime_type}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>Failure Category:</strong>
|
<strong>Failure Category:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
|
|
@ -1989,21 +1989,21 @@ const DocumentManagementPage: React.FC = () => {
|
||||||
sx={{ mb: 2 }}
|
sx={{ mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
<Typography variant="body2" color="text.secondary" component="div" sx={{ mt: 2 }}>
|
||||||
<strong>Retry Count:</strong>
|
<strong>Retry Count:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
{selectedDocument.retry_count} attempts
|
{selectedDocument.retry_count} attempts
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>Created:</strong>
|
<strong>Created:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||||
{format(new Date(selectedDocument.created_at), 'PPpp')}
|
{format(new Date(selectedDocument.created_at), 'PPpp')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary" component="div">
|
||||||
<strong>Last Updated:</strong>
|
<strong>Last Updated:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
|
|
@ -2013,13 +2013,13 @@ const DocumentManagementPage: React.FC = () => {
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
|
||||||
<strong>Tags:</strong>
|
<strong>Tags:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ mb: 2 }}>
|
<Box component="div" sx={{ mb: 2 }}>
|
||||||
{selectedDocument.tags.length > 0 ? (
|
{selectedDocument.tags.length > 0 ? (
|
||||||
selectedDocument.tags.map((tag) => (
|
selectedDocument.tags.map((tag) => (
|
||||||
<Chip key={tag} label={tag} size="small" sx={{ mr: 1, mb: 1 }} />
|
<Chip key={tag} label={tag} size="small" sx={{ mr: 1, mb: 1 }} />
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" color="text.secondary">No tags</Typography>
|
<Typography variant="body2" color="text.secondary" component="span">No tags</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -2031,7 +2031,7 @@ const DocumentManagementPage: React.FC = () => {
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
Error Details
|
Error Details
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
<Typography variant="body2" color="text.secondary" component="div" sx={{ mb: 1 }}>
|
||||||
<strong>Full Error Message:</strong>
|
<strong>Full Error Message:</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paper sx={{
|
<Paper sx={{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,600 @@
|
||||||
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import DocumentManagementPage from '../DocumentManagementPage';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
// Mock API with comprehensive responses
|
||||||
|
vi.mock('../../services/api', () => ({
|
||||||
|
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 (
|
||||||
|
<BrowserRouter>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<DocumentManagementPageWrapper>
|
||||||
|
<DocumentManagementPage />
|
||||||
|
</DocumentManagementPageWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue