Readur/frontend/src/components/__tests__/BulkRetryModal.test.tsx

280 lines
8.6 KiB
TypeScript

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();
vi.mock('../../services/api', () => ({
documentService: {
bulkRetryOcr: mockBulkRetryOcr,
},
}));
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();
// Set up different responses for preview and execute
mockBulkRetryOcr
.mockResolvedValueOnce({
data: {
success: true,
queued_count: 0,
matched_count: 5,
documents: [],
estimated_total_time_minutes: 2.5,
message: 'Preview completed',
},
})
.mockResolvedValueOnce({
data: {
success: true,
queued_count: 5,
matched_count: 5,
documents: [],
estimated_total_time_minutes: 2.5,
message: 'Operation completed successfully',
},
});
render(<BulkRetryModal {...mockProps} />);
// First do a preview
const previewButton = screen.getByText('Preview');
await user.click(previewButton);
await waitFor(() => {
expect(screen.getByText('Preview Results')).toBeInTheDocument();
});
// Now execute the retry
const executeButton = screen.getByText('Retry 5 Documents');
await user.click(executeButton);
await waitFor(() => {
expect(mockBulkRetryOcr).toHaveBeenCalledTimes(2);
});
expect(mockBulkRetryOcr).toHaveBeenNthCalledWith(1,
expect.objectContaining({
mode: 'all',
preview_only: true,
})
);
expect(mockBulkRetryOcr).toHaveBeenNthCalledWith(2,
expect.objectContaining({
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 operation')).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 never resolve
mockBulkRetryOcr.mockImplementation(() => new Promise(() => {}));
render(<BulkRetryModal {...mockProps} />);
const previewButton = screen.getByText('Preview');
await user.click(previewButton);
// Should show loading state
await waitFor(() => {
expect(screen.getByRole('progressbar')).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();
});
});