feat(tests): create unit tests for retry

This commit is contained in:
perf3ct 2025-07-02 02:13:58 +00:00
parent 2006907d2f
commit 05c04f242e
5 changed files with 1227 additions and 3 deletions

View File

@ -365,13 +365,13 @@ export const BulkRetryModal: React.FC<BulkRetryModalProps> = ({
{formatDuration(previewResult.estimated_total_time_minutes)}
</Typography>
</Box>
{previewResult.documents.length > 0 && (
{previewResult.documents && previewResult.documents.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Sample Documents:
</Typography>
<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}>
<Typography variant="body2">
{doc.filename} ({formatFileSize(doc.file_size)})
@ -385,7 +385,7 @@ export const BulkRetryModal: React.FC<BulkRetryModalProps> = ({
</Typography>
</Box>
))}
{previewResult.documents.length > 10 && (
{previewResult.documents && previewResult.documents.length > 10 && (
<Typography variant="body2" color="text.secondary" mt={1}>
... and {previewResult.documents.length - 10} more documents
</Typography>

View File

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

View File

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

View File

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

View File

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