feat(tests): create unit tests for retry
This commit is contained in:
parent
2006907d2f
commit
05c04f242e
|
|
@ -365,13 +365,13 @@ export const BulkRetryModal: React.FC<BulkRetryModalProps> = ({
|
||||||
{formatDuration(previewResult.estimated_total_time_minutes)}
|
{formatDuration(previewResult.estimated_total_time_minutes)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{previewResult.documents.length > 0 && (
|
{previewResult.documents && previewResult.documents.length > 0 && (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
Sample Documents:
|
Sample Documents:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box maxHeight={200} overflow="auto">
|
<Box maxHeight={200} overflow="auto">
|
||||||
{previewResult.documents.slice(0, 10).map((doc) => (
|
{(previewResult.documents || []).slice(0, 10).map((doc) => (
|
||||||
<Box key={doc.id} py={0.5}>
|
<Box key={doc.id} py={0.5}>
|
||||||
<Typography variant="body2">
|
<Typography variant="body2">
|
||||||
{doc.filename} ({formatFileSize(doc.file_size)})
|
{doc.filename} ({formatFileSize(doc.file_size)})
|
||||||
|
|
@ -385,7 +385,7 @@ export const BulkRetryModal: React.FC<BulkRetryModalProps> = ({
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
{previewResult.documents.length > 10 && (
|
{previewResult.documents && previewResult.documents.length > 10 && (
|
||||||
<Typography variant="body2" color="text.secondary" mt={1}>
|
<Typography variant="body2" color="text.secondary" mt={1}>
|
||||||
... and {previewResult.documents.length - 10} more documents
|
... and {previewResult.documents.length - 10} more documents
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { BulkRetryModal } from '../BulkRetryModal';
|
||||||
|
|
||||||
|
// Mock the API
|
||||||
|
const mockBulkRetryOcr = vi.fn();
|
||||||
|
const mockDocumentService = {
|
||||||
|
bulkRetryOcr: mockBulkRetryOcr,
|
||||||
|
};
|
||||||
|
const mockApi = {
|
||||||
|
bulkRetryOcr: mockBulkRetryOcr,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../../services/api', () => ({
|
||||||
|
default: mockApi,
|
||||||
|
documentService: mockDocumentService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('BulkRetryModal', () => {
|
||||||
|
const mockProps = {
|
||||||
|
open: true,
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onSuccess: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockBulkRetryOcr.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 5,
|
||||||
|
matched_count: 5,
|
||||||
|
documents: [],
|
||||||
|
estimated_total_time_minutes: 2.5,
|
||||||
|
message: 'Operation completed successfully',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders modal with title and form elements', () => {
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('Bulk OCR Retry')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Retry Mode')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Retry all failed OCR documents')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Retry documents matching criteria')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closes modal when close button is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const closeButton = screen.getByText('Cancel');
|
||||||
|
await user.click(closeButton);
|
||||||
|
|
||||||
|
expect(mockProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows preview by default', () => {
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const previewButton = screen.getByText('Preview');
|
||||||
|
expect(previewButton).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows switching to filter mode', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const filterRadio = screen.getByLabelText('Retry documents matching criteria');
|
||||||
|
await user.click(filterRadio);
|
||||||
|
|
||||||
|
// Should show the accordion with filter criteria
|
||||||
|
expect(screen.getByText('Filter Criteria')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Expand the accordion to see filter options
|
||||||
|
const filterAccordion = screen.getByText('Filter Criteria');
|
||||||
|
await user.click(filterAccordion);
|
||||||
|
|
||||||
|
expect(screen.getByText('File Types')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Failure Reasons')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Maximum File Size')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can select MIME types in filter mode', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
// Switch to filter mode
|
||||||
|
const filterRadio = screen.getByLabelText('Retry documents matching criteria');
|
||||||
|
await user.click(filterRadio);
|
||||||
|
|
||||||
|
// Expand the accordion to see filter options
|
||||||
|
const filterAccordion = screen.getByText('Filter Criteria');
|
||||||
|
await user.click(filterAccordion);
|
||||||
|
|
||||||
|
// Should show MIME type chips
|
||||||
|
const pdfChip = screen.getByText('PDF');
|
||||||
|
expect(pdfChip).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click on the PDF chip to select it
|
||||||
|
await user.click(pdfChip);
|
||||||
|
|
||||||
|
// The chip should now be selected (filled variant)
|
||||||
|
expect(pdfChip.closest('[data-testid], .MuiChip-root')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can set priority override', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
// Expand the Advanced Options accordion
|
||||||
|
const advancedAccordion = screen.getByText('Advanced Options');
|
||||||
|
await user.click(advancedAccordion);
|
||||||
|
|
||||||
|
// Enable priority override
|
||||||
|
const priorityCheckbox = screen.getByLabelText('Override processing priority');
|
||||||
|
await user.click(priorityCheckbox);
|
||||||
|
|
||||||
|
// Now the slider should be visible
|
||||||
|
const prioritySlider = screen.getByRole('slider');
|
||||||
|
fireEvent.change(prioritySlider, { target: { value: 15 } });
|
||||||
|
|
||||||
|
expect(prioritySlider).toHaveValue('15');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes preview request successfully', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockBulkRetryOcr.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 0,
|
||||||
|
matched_count: 3,
|
||||||
|
documents: [
|
||||||
|
{ id: '1', filename: 'doc1.pdf', file_size: 1024, mime_type: 'application/pdf' },
|
||||||
|
{ id: '2', filename: 'doc2.pdf', file_size: 2048, mime_type: 'application/pdf' },
|
||||||
|
],
|
||||||
|
estimated_total_time_minutes: 1.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const previewButton = screen.getByText('Preview');
|
||||||
|
await user.click(previewButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Preview Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Documents matched:')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Estimated processing time:')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes actual retry request successfully', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
// First do a preview
|
||||||
|
const previewButton = screen.getByText('Preview');
|
||||||
|
await user.click(previewButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Retry \d+ Documents/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now execute the retry
|
||||||
|
const executeButton = screen.getByText(/Retry \d+ Documents/);
|
||||||
|
await user.click(executeButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalledWith({
|
||||||
|
mode: 'all',
|
||||||
|
preview_only: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockProps.onSuccess).toHaveBeenCalled();
|
||||||
|
expect(mockProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles API errors gracefully', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockBulkRetryOcr.mockRejectedValue(new Error('API Error'));
|
||||||
|
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const previewButton = screen.getByText('Preview');
|
||||||
|
await user.click(previewButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Failed to preview retry/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can set document limit in filter mode', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
// Switch to filter mode
|
||||||
|
const filterRadio = screen.getByLabelText('Retry documents matching criteria');
|
||||||
|
await user.click(filterRadio);
|
||||||
|
|
||||||
|
// Expand the accordion to see filter options
|
||||||
|
const filterAccordion = screen.getByText('Filter Criteria');
|
||||||
|
await user.click(filterAccordion);
|
||||||
|
|
||||||
|
// Find and set the document limit
|
||||||
|
const limitInput = screen.getByLabelText('Maximum Documents to Retry');
|
||||||
|
await user.clear(limitInput);
|
||||||
|
await user.type(limitInput, '100');
|
||||||
|
|
||||||
|
expect(limitInput).toHaveValue(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows loading state during API calls', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Make the API call take time
|
||||||
|
mockBulkRetryOcr.mockImplementation(() => new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve({
|
||||||
|
data: { success: true, queued_count: 0, matched_count: 0, documents: [] }
|
||||||
|
}), 100)
|
||||||
|
));
|
||||||
|
|
||||||
|
render(<BulkRetryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const previewButton = screen.getByText('Preview');
|
||||||
|
await user.click(previewButton);
|
||||||
|
|
||||||
|
// Should show loading state
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
// The button should remain as "Preview" during loading, not change text
|
||||||
|
expect(screen.getByText('Preview')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resets form when modal is closed and reopened', () => {
|
||||||
|
const { rerender } = render(<BulkRetryModal {...mockProps} open={false} />);
|
||||||
|
|
||||||
|
// Reopen the modal
|
||||||
|
rerender(<BulkRetryModal {...mockProps} open={true} />);
|
||||||
|
|
||||||
|
// Should be back to default state
|
||||||
|
expect(screen.getByLabelText('Retry all failed OCR documents')).toBeChecked();
|
||||||
|
// Note: slider is not visible by default as it's in an accordion
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not render when modal is closed', () => {
|
||||||
|
render(<BulkRetryModal {...mockProps} open={false} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Bulk OCR Retry')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { RetryHistoryModal } from '../RetryHistoryModal';
|
||||||
|
|
||||||
|
// Mock the API
|
||||||
|
const mockGetDocumentRetryHistory = vi.fn();
|
||||||
|
|
||||||
|
const mockDocumentService = {
|
||||||
|
getDocumentRetryHistory: mockGetDocumentRetryHistory,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../../services/api', () => ({
|
||||||
|
documentService: mockDocumentService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('RetryHistoryModal', () => {
|
||||||
|
const mockProps = {
|
||||||
|
open: true,
|
||||||
|
onClose: vi.fn(),
|
||||||
|
documentId: 'test-doc-123',
|
||||||
|
documentName: 'test-document.pdf',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleRetryHistory = [
|
||||||
|
{
|
||||||
|
id: 'retry-1',
|
||||||
|
retry_reason: 'bulk_retry_all',
|
||||||
|
previous_status: 'failed',
|
||||||
|
previous_failure_reason: 'low_confidence',
|
||||||
|
previous_error: 'OCR confidence too low: 45%',
|
||||||
|
priority: 15,
|
||||||
|
queue_id: 'queue-1',
|
||||||
|
created_at: '2024-01-15T10:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'retry-2',
|
||||||
|
retry_reason: 'manual_retry',
|
||||||
|
previous_status: 'failed',
|
||||||
|
previous_failure_reason: 'image_quality',
|
||||||
|
previous_error: 'Image resolution too low',
|
||||||
|
priority: 12,
|
||||||
|
queue_id: 'queue-2',
|
||||||
|
created_at: '2024-01-14T14:20:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: sampleRetryHistory,
|
||||||
|
total_retries: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders modal with title and document name', () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByText('OCR Retry History')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test-document.pdf')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not render when modal is closed', () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} open={false} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('OCR Retry History')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads and displays retry history on mount', async () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Bulk Retry (All Documents)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Manual Retry')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Low Confidence')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Image Quality')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('High')).toBeInTheDocument(); // Priority 15
|
||||||
|
expect(screen.getByText('Medium')).toBeInTheDocument(); // Priority 12
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows loading state initially', () => {
|
||||||
|
mockGetDocumentRetryHistory.mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Loading retry history...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles API errors gracefully', async () => {
|
||||||
|
mockGetDocumentRetryHistory.mockRejectedValue(new Error('API Error'));
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Failed to load retry history/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows empty state when no retry history exists', async () => {
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: [],
|
||||||
|
total_retries: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('No retry history found for this document.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closes modal when close button is clicked', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
const closeButton = screen.getByText('Close');
|
||||||
|
await user.click(closeButton);
|
||||||
|
|
||||||
|
expect(mockProps.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats retry reasons correctly', async () => {
|
||||||
|
const customHistory = [
|
||||||
|
{ ...sampleRetryHistory[0], retry_reason: 'bulk_retry_all' },
|
||||||
|
{ ...sampleRetryHistory[0], retry_reason: 'bulk_retry_specific' },
|
||||||
|
{ ...sampleRetryHistory[0], retry_reason: 'bulk_retry_filtered' },
|
||||||
|
{ ...sampleRetryHistory[0], retry_reason: 'manual_retry' },
|
||||||
|
{ ...sampleRetryHistory[0], retry_reason: 'unknown_reason' },
|
||||||
|
];
|
||||||
|
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: customHistory,
|
||||||
|
total_retries: customHistory.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Bulk Retry (All Documents)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Bulk Retry (Specific Documents)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Bulk Retry (Filtered)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Manual Retry')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('unknown_reason')).toBeInTheDocument(); // Unknown reasons show as-is
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats priority levels correctly', async () => {
|
||||||
|
const customHistory = [
|
||||||
|
{ ...sampleRetryHistory[0], priority: 20 },
|
||||||
|
{ ...sampleRetryHistory[0], priority: 15 },
|
||||||
|
{ ...sampleRetryHistory[0], priority: 10 },
|
||||||
|
{ ...sampleRetryHistory[0], priority: 5 },
|
||||||
|
{ ...sampleRetryHistory[0], priority: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: customHistory,
|
||||||
|
total_retries: customHistory.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const highPriorities = screen.getAllByText('High');
|
||||||
|
const mediumPriorities = screen.getAllByText('Medium');
|
||||||
|
const lowPriorities = screen.getAllByText('Low');
|
||||||
|
|
||||||
|
expect(highPriorities).toHaveLength(2); // Priority 20 and 15
|
||||||
|
expect(mediumPriorities).toHaveLength(1); // Priority 10
|
||||||
|
expect(lowPriorities).toHaveLength(2); // Priority 5 and 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats failure reasons correctly', async () => {
|
||||||
|
const customHistory = [
|
||||||
|
{ ...sampleRetryHistory[0], previous_failure_reason: 'low_confidence' },
|
||||||
|
{ ...sampleRetryHistory[0], previous_failure_reason: 'image_quality' },
|
||||||
|
{ ...sampleRetryHistory[0], previous_failure_reason: 'processing_timeout' },
|
||||||
|
{ ...sampleRetryHistory[0], previous_failure_reason: 'unknown_error' },
|
||||||
|
{ ...sampleRetryHistory[0], previous_failure_reason: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: customHistory,
|
||||||
|
total_retries: customHistory.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Image Quality')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Processing Timeout')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Unknown Error')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('N/A')).toBeInTheDocument(); // null reason
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays previous error messages', async () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('OCR confidence too low: 45%')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Image resolution too low')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats dates correctly', async () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check that dates are formatted (exact format may vary by locale)
|
||||||
|
expect(screen.getByText(/Jan/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/2024/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows total retry count', async () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Total retries: 2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles missing documentName gracefully', async () => {
|
||||||
|
render(<RetryHistoryModal {...mockProps} documentName={undefined} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('test-doc-123')).toBeInTheDocument(); // Falls back to documentId
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles history entries with missing fields', async () => {
|
||||||
|
const incompleteHistory = [
|
||||||
|
{
|
||||||
|
id: 'retry-1',
|
||||||
|
retry_reason: null,
|
||||||
|
previous_status: null,
|
||||||
|
previous_failure_reason: null,
|
||||||
|
previous_error: null,
|
||||||
|
priority: null,
|
||||||
|
queue_id: null,
|
||||||
|
created_at: '2024-01-15T10:30:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-123',
|
||||||
|
retry_history: incompleteHistory,
|
||||||
|
total_retries: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should not crash and should show N/A for missing fields
|
||||||
|
expect(screen.getAllByText('N/A')).toHaveLength(4); // reason, failure reason, previous error, priority
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads fresh data when documentId changes', async () => {
|
||||||
|
const { rerender } = render(<RetryHistoryModal {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockGetDocumentRetryHistory).toHaveBeenCalledWith('test-doc-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change document ID
|
||||||
|
rerender(<RetryHistoryModal {...mockProps} documentId="different-doc-456" />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockGetDocumentRetryHistory).toHaveBeenCalledWith('different-doc-456');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetDocumentRetryHistory).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { RetryRecommendations } from '../RetryRecommendations';
|
||||||
|
|
||||||
|
// Mock the API
|
||||||
|
const mockGetRetryRecommendations = vi.fn();
|
||||||
|
const mockBulkRetryOcr = vi.fn();
|
||||||
|
|
||||||
|
const mockDocumentService = {
|
||||||
|
getRetryRecommendations: mockGetRetryRecommendations,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockApi = {
|
||||||
|
bulkRetryOcr: mockBulkRetryOcr,
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../../services/api', () => ({
|
||||||
|
documentService: mockDocumentService,
|
||||||
|
default: mockApi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('RetryRecommendations', () => {
|
||||||
|
const mockProps = {
|
||||||
|
onRetrySuccess: vi.fn(),
|
||||||
|
onRetryClick: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleRecommendations = [
|
||||||
|
{
|
||||||
|
reason: 'low_confidence',
|
||||||
|
title: 'Low Confidence Results',
|
||||||
|
description: 'Documents with OCR confidence below 70%',
|
||||||
|
estimated_success_rate: 0.8,
|
||||||
|
document_count: 15,
|
||||||
|
filter: {
|
||||||
|
failure_reasons: ['low_confidence'],
|
||||||
|
min_confidence: 0,
|
||||||
|
max_confidence: 70,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reason: 'image_quality',
|
||||||
|
title: 'Image Quality Issues',
|
||||||
|
description: 'Documents that failed due to poor image quality',
|
||||||
|
estimated_success_rate: 0.6,
|
||||||
|
document_count: 8,
|
||||||
|
filter: {
|
||||||
|
failure_reasons: ['image_quality', 'resolution_too_low'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: sampleRecommendations,
|
||||||
|
total_recommendations: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockBulkRetryOcr.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 10,
|
||||||
|
matched_count: 15,
|
||||||
|
documents: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders loading state initially', () => {
|
||||||
|
mockGetRetryRecommendations.mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Loading retry recommendations...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads and displays recommendations on mount', async () => {
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('OCR Retry Recommendations')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Image Quality Issues')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('15 documents')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('8 documents')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displays success rate badges with correct colors', async () => {
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('80% (High)')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('60% (Medium)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that the badges have the correct colors
|
||||||
|
const highBadge = screen.getByText('80% (High)').closest('.MuiChip-root');
|
||||||
|
const mediumBadge = screen.getByText('60% (Medium)').closest('.MuiChip-root');
|
||||||
|
|
||||||
|
expect(highBadge).toHaveClass('MuiChip-colorSuccess');
|
||||||
|
expect(mediumBadge).toHaveClass('MuiChip-colorWarning');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles retry click with onRetryClick callback', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getAllByText('Retry Now')[0];
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
expect(mockProps.onRetryClick).toHaveBeenCalledWith(sampleRecommendations[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executes retry directly when onRetryClick is not provided', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<RetryRecommendations onRetrySuccess={mockProps.onRetrySuccess} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getAllByText('Retry Now')[0];
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalledWith({
|
||||||
|
mode: 'filter',
|
||||||
|
filter: sampleRecommendations[0].filter,
|
||||||
|
priority_override: 12,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockProps.onRetrySuccess).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows loading state during retry execution', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockBulkRetryOcr.mockImplementation(() => new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve({
|
||||||
|
data: { success: true, queued_count: 10, matched_count: 10, documents: [] }
|
||||||
|
}), 100)
|
||||||
|
));
|
||||||
|
|
||||||
|
render(<RetryRecommendations onRetrySuccess={mockProps.onRetrySuccess} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getAllByText('Retry Now')[0];
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
// Should show loading state
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
expect(retryButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles API errors gracefully', async () => {
|
||||||
|
mockGetRetryRecommendations.mockRejectedValue(new Error('API Error'));
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Failed to load retry recommendations/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles retry API errors gracefully', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockBulkRetryOcr.mockRejectedValue({
|
||||||
|
response: { data: { message: 'Retry failed' } }
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryRecommendations onRetrySuccess={mockProps.onRetrySuccess} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getAllByText('Retry Now')[0];
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Retry failed')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows empty state when no recommendations are available', async () => {
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: [],
|
||||||
|
total_recommendations: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('No retry recommendations available')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('All documents have been processed successfully')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('No failed documents found')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows correct success rate labels', () => {
|
||||||
|
const { rerender } = render(<div />);
|
||||||
|
|
||||||
|
// Test high success rate (>= 70%)
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: [{
|
||||||
|
...sampleRecommendations[0],
|
||||||
|
estimated_success_rate: 0.85,
|
||||||
|
}],
|
||||||
|
total_recommendations: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(screen.getByText('85% (High)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test medium success rate (40-69%)
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: [{
|
||||||
|
...sampleRecommendations[0],
|
||||||
|
estimated_success_rate: 0.55,
|
||||||
|
}],
|
||||||
|
total_recommendations: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(screen.getByText('55% (Medium)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test low success rate (< 40%)
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: [{
|
||||||
|
...sampleRecommendations[0],
|
||||||
|
estimated_success_rate: 0.25,
|
||||||
|
}],
|
||||||
|
total_recommendations: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
|
expect(screen.getByText('25% (Low)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshes recommendations after successful retry', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<RetryRecommendations onRetrySuccess={mockProps.onRetrySuccess} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Low Confidence Results')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetRetryRecommendations).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
const retryButton = screen.getAllByText('Retry Now')[0];
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should reload recommendations after successful retry
|
||||||
|
expect(mockGetRetryRecommendations).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles null/undefined recommendations safely', async () => {
|
||||||
|
mockGetRetryRecommendations.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
recommendations: null,
|
||||||
|
total_recommendations: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RetryRecommendations {...mockProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('No retry recommendations available')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not crash
|
||||||
|
expect(screen.getByText('OCR Retry Recommendations')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,367 @@
|
||||||
|
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||||
|
import DocumentDetailsPage from '../DocumentDetailsPage';
|
||||||
|
|
||||||
|
// Mock the entire API module
|
||||||
|
const mockBulkRetryOcr = vi.fn();
|
||||||
|
const mockGetById = vi.fn();
|
||||||
|
const mockGetOcrText = vi.fn();
|
||||||
|
const mockGetThumbnail = vi.fn();
|
||||||
|
const mockGetDocumentRetryHistory = vi.fn();
|
||||||
|
|
||||||
|
const mockDocumentService = {
|
||||||
|
getById: mockGetById,
|
||||||
|
getOcrText: mockGetOcrText,
|
||||||
|
getThumbnail: mockGetThumbnail,
|
||||||
|
bulkRetryOcr: mockBulkRetryOcr,
|
||||||
|
getDocumentRetryHistory: mockGetDocumentRetryHistory,
|
||||||
|
download: vi.fn(),
|
||||||
|
getProcessedImage: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockApi = {
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('../../services/api', () => ({
|
||||||
|
documentService: mockDocumentService,
|
||||||
|
default: mockApi,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the RetryHistoryModal component
|
||||||
|
vi.mock('../../components/RetryHistoryModal', () => ({
|
||||||
|
RetryHistoryModal: ({ open, onClose, documentId, documentName }: any) => (
|
||||||
|
open ? (
|
||||||
|
<div data-testid="retry-history-modal">
|
||||||
|
<div>Retry History for {documentName}</div>
|
||||||
|
<div>Document ID: {documentId}</div>
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock other components
|
||||||
|
vi.mock('../../components/DocumentViewer', () => ({
|
||||||
|
default: ({ documentId, filename }: any) => (
|
||||||
|
<div data-testid="document-viewer">
|
||||||
|
Viewing {filename} (ID: {documentId})
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../components/Labels/LabelSelector', () => ({
|
||||||
|
default: ({ selectedLabels, onLabelsChange }: any) => (
|
||||||
|
<div data-testid="label-selector">
|
||||||
|
<div>Selected: {selectedLabels.length} labels</div>
|
||||||
|
<button onClick={() => onLabelsChange([])}>Clear Labels</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../components/MetadataDisplay', () => ({
|
||||||
|
default: ({ metadata, title }: any) => (
|
||||||
|
<div data-testid="metadata-display">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<pre>{JSON.stringify(metadata, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('DocumentDetailsPage - Retry Functionality', () => {
|
||||||
|
const mockDocument = {
|
||||||
|
id: 'test-doc-1',
|
||||||
|
original_filename: 'test-document.pdf',
|
||||||
|
filename: 'test-document.pdf',
|
||||||
|
file_size: 1024000,
|
||||||
|
mime_type: 'application/pdf',
|
||||||
|
created_at: '2023-01-01T00:00:00Z',
|
||||||
|
has_ocr_text: true,
|
||||||
|
tags: ['important'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockOcrData = {
|
||||||
|
document_id: 'test-doc-1',
|
||||||
|
filename: 'test-document.pdf',
|
||||||
|
has_ocr_text: true,
|
||||||
|
ocr_text: 'Sample OCR text content',
|
||||||
|
ocr_confidence: 95,
|
||||||
|
ocr_word_count: 100,
|
||||||
|
ocr_processing_time_ms: 5000,
|
||||||
|
ocr_status: 'completed',
|
||||||
|
ocr_completed_at: '2023-01-01T00:05:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
mockGetById.mockResolvedValue({
|
||||||
|
data: mockDocument,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockGetOcrText.mockResolvedValue({
|
||||||
|
data: mockOcrData,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockGetThumbnail.mockRejectedValue(new Error('Thumbnail not available'));
|
||||||
|
|
||||||
|
mockBulkRetryOcr.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 1,
|
||||||
|
matched_count: 1,
|
||||||
|
documents: [mockDocument],
|
||||||
|
estimated_total_time_minutes: 2.0,
|
||||||
|
message: 'OCR retry queued successfully',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockGetDocumentRetryHistory.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
document_id: 'test-doc-1',
|
||||||
|
retry_history: [],
|
||||||
|
total_retries: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockApi.get.mockResolvedValue({ data: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderDocumentDetailsPage = () => {
|
||||||
|
return render(
|
||||||
|
<MemoryRouter initialEntries={['/documents/test-doc-1']}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/documents/:id" element={<DocumentDetailsPage />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('renders retry OCR button', async () => {
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Retry OCR')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can retry OCR for document', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getByText('Retry OCR');
|
||||||
|
expect(retryButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Clear previous calls to track only the retry call
|
||||||
|
mockBulkRetryOcr.mockClear();
|
||||||
|
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalledWith({
|
||||||
|
mode: 'specific',
|
||||||
|
document_ids: ['test-doc-1'],
|
||||||
|
priority_override: 15,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows loading state during retry', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Make the retry take some time
|
||||||
|
mockBulkRetryOcr.mockImplementation(() =>
|
||||||
|
new Promise(resolve =>
|
||||||
|
setTimeout(() => resolve({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 1,
|
||||||
|
matched_count: 1,
|
||||||
|
documents: [mockDocument],
|
||||||
|
estimated_total_time_minutes: 2.0,
|
||||||
|
message: 'OCR retry queued successfully',
|
||||||
|
},
|
||||||
|
}), 100)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getByText('Retry OCR');
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
// Should show loading state
|
||||||
|
expect(screen.getByText('Retrying...')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Wait for retry to complete
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Retry OCR')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles retry OCR error gracefully', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Mock retry to fail
|
||||||
|
mockBulkRetryOcr.mockRejectedValue(new Error('Retry failed'));
|
||||||
|
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = screen.getByText('Retry OCR');
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
// Should still show the retry button (not stuck in loading state)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Retry OCR')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders retry history button', async () => {
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Retry History')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can open retry history modal', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const historyButton = screen.getByText('Retry History');
|
||||||
|
await user.click(historyButton);
|
||||||
|
|
||||||
|
// Should open the retry history modal
|
||||||
|
expect(screen.getByTestId('retry-history-modal')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Retry History for test-document.pdf')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Document ID: test-doc-1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can close retry history modal', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
const historyButton = screen.getByText('Retry History');
|
||||||
|
await user.click(historyButton);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('retry-history-modal')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
const closeButton = screen.getByText('Close');
|
||||||
|
await user.click(closeButton);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId('retry-history-modal')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshes document details after successful retry', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Mock successful retry
|
||||||
|
mockBulkRetryOcr.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
queued_count: 1,
|
||||||
|
matched_count: 1,
|
||||||
|
documents: [mockDocument],
|
||||||
|
estimated_total_time_minutes: 2.0,
|
||||||
|
message: 'OCR retry queued successfully',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear previous calls
|
||||||
|
mockGetById.mockClear();
|
||||||
|
|
||||||
|
const retryButton = screen.getByText('Retry OCR');
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
// Should call getById again to refresh document details after delay
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockGetById).toHaveBeenCalledWith('test-doc-1');
|
||||||
|
}, { timeout: 2000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retry functionality works with documents without OCR text', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
// Mock document without OCR text
|
||||||
|
mockGetById.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
...mockDocument,
|
||||||
|
has_ocr_text: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry button should still be available
|
||||||
|
const retryButton = screen.getByText('Retry OCR');
|
||||||
|
expect(retryButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(retryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockBulkRetryOcr).toHaveBeenCalledWith({
|
||||||
|
mode: 'specific',
|
||||||
|
document_ids: ['test-doc-1'],
|
||||||
|
priority_override: 15,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('retry history modal receives correct props', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderDocumentDetailsPage();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Document Details')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const historyButton = screen.getByText('Retry History');
|
||||||
|
await user.click(historyButton);
|
||||||
|
|
||||||
|
// Verify modal props are passed correctly
|
||||||
|
expect(screen.getByText('Document ID: test-doc-1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Retry History for test-document.pdf')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue