feat: Add comprehensive test suite for all services and components
- Set up Vitest with testing-library for React component tests - Add 20 tests for pdfService (field extraction, PDF filling, visual overlay) - Add 14 tests for geminiService with mocked API responses - Add 17 tests for FileUpload component (drag-drop, file selection, preview) - Add 28 tests for ReviewPanel component (rendering, editing, filtering) - Add 21 Python tests for fill_pdf.py (extraction, filling, CLI) Total: 100 tests covering critical functionality https://claude.ai/code/session_01Wi3BtYKgQu6v4zbydtG6Sy
This commit is contained in:
parent
48306e882d
commit
cbacd3430c
9 changed files with 6062 additions and 2 deletions
331
tests/components/FileUpload.test.tsx
Normal file
331
tests/components/FileUpload.test.tsx
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { FileUpload } from '../../components/FileUpload';
|
||||
import { FileData } from '../../types';
|
||||
|
||||
describe('FileUpload', () => {
|
||||
const defaultProps = {
|
||||
label: 'Upload Document',
|
||||
description: 'PDF or Image files accepted',
|
||||
accept: '.pdf,image/*',
|
||||
onFileSelect: vi.fn(),
|
||||
selectedFile: null as FileData | null
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render upload area when no file is selected', () => {
|
||||
render(<FileUpload {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Upload Document')).toBeInTheDocument();
|
||||
expect(screen.getByText('PDF or Image files accepted')).toBeInTheDocument();
|
||||
expect(screen.getByText('Click to upload or drag and drop')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render file info when a file is selected', () => {
|
||||
const mockFile: FileData = {
|
||||
file: new File(['content'], 'document.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'base64content',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
Object.defineProperty(mockFile.file, 'size', { value: 1048576 }); // 1MB
|
||||
|
||||
render(<FileUpload {...defaultProps} selectedFile={mockFile} />);
|
||||
|
||||
expect(screen.getByText('document.pdf')).toBeInTheDocument();
|
||||
expect(screen.getByText('1.00 MB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show image preview when selected file is an image', () => {
|
||||
const mockFile: FileData = {
|
||||
file: new File([''], 'photo.png', { type: 'image/png' }),
|
||||
previewUrl: 'data:image/png;base64,abc123',
|
||||
base64: 'abc123',
|
||||
type: 'image/png'
|
||||
};
|
||||
|
||||
render(<FileUpload {...defaultProps} selectedFile={mockFile} />);
|
||||
|
||||
const img = screen.getByAltText('Preview');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('src', 'data:image/png;base64,abc123');
|
||||
});
|
||||
|
||||
it('should show file icon when selected file is a PDF', () => {
|
||||
const mockFile: FileData = {
|
||||
file: new File([''], 'document.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'pdfcontent',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
render(<FileUpload {...defaultProps} selectedFile={mockFile} />);
|
||||
|
||||
// Should not show image preview
|
||||
expect(screen.queryByAltText('Preview')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('file selection via input', () => {
|
||||
it('should call onFileSelect when a file is selected via input', async () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
// Mock FileReader
|
||||
const mockFileReader = {
|
||||
readAsDataURL: vi.fn(),
|
||||
result: 'data:application/pdf;base64,dGVzdCBjb250ZW50',
|
||||
onload: null as ((ev: ProgressEvent<FileReader>) => void) | null
|
||||
};
|
||||
|
||||
vi.spyOn(global, 'FileReader').mockImplementation(() => {
|
||||
return mockFileReader as unknown as FileReader;
|
||||
});
|
||||
|
||||
fireEvent.change(input, { target: { files: [file] } });
|
||||
|
||||
// Trigger the onload callback
|
||||
mockFileReader.onload?.({} as ProgressEvent<FileReader>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFileSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
file: expect.any(File),
|
||||
base64: 'dGVzdCBjb250ZW50', // base64 content without prefix
|
||||
type: 'application/pdf'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call onFileSelect when no files are selected', () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
fireEvent.change(input, { target: { files: [] } });
|
||||
|
||||
expect(onFileSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag and drop', () => {
|
||||
it('should highlight on drag over', () => {
|
||||
render(<FileUpload {...defaultProps} />);
|
||||
|
||||
const dropZone = screen.getByText('Click to upload or drag and drop').parentElement?.parentElement?.parentElement;
|
||||
|
||||
fireEvent.dragOver(dropZone!, { preventDefault: vi.fn() });
|
||||
|
||||
// Check for the dragging class (indigo border)
|
||||
expect(dropZone).toHaveClass('border-indigo-500');
|
||||
});
|
||||
|
||||
it('should remove highlight on drag leave', () => {
|
||||
render(<FileUpload {...defaultProps} />);
|
||||
|
||||
const dropZone = screen.getByText('Click to upload or drag and drop').parentElement?.parentElement?.parentElement;
|
||||
|
||||
fireEvent.dragOver(dropZone!, { preventDefault: vi.fn() });
|
||||
fireEvent.dragLeave(dropZone!);
|
||||
|
||||
expect(dropZone).not.toHaveClass('border-indigo-500');
|
||||
});
|
||||
|
||||
it('should handle file drop', async () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const file = new File(['dropped content'], 'dropped.pdf', { type: 'application/pdf' });
|
||||
const dropZone = screen.getByText('Click to upload or drag and drop').parentElement?.parentElement?.parentElement;
|
||||
|
||||
// Mock FileReader
|
||||
const mockFileReader = {
|
||||
readAsDataURL: vi.fn(),
|
||||
result: 'data:application/pdf;base64,ZHJvcHBlZCBjb250ZW50',
|
||||
onload: null as ((ev: ProgressEvent<FileReader>) => void) | null
|
||||
};
|
||||
|
||||
vi.spyOn(global, 'FileReader').mockImplementation(() => {
|
||||
return mockFileReader as unknown as FileReader;
|
||||
});
|
||||
|
||||
fireEvent.drop(dropZone!, {
|
||||
preventDefault: vi.fn(),
|
||||
dataTransfer: { files: [file] }
|
||||
});
|
||||
|
||||
// Trigger the onload callback
|
||||
mockFileReader.onload?.({} as ProgressEvent<FileReader>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFileSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
file: expect.any(File),
|
||||
base64: 'ZHJvcHBlZCBjb250ZW50',
|
||||
type: 'application/pdf'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not process drop if no files in dataTransfer', () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const dropZone = screen.getByText('Click to upload or drag and drop').parentElement?.parentElement?.parentElement;
|
||||
|
||||
fireEvent.drop(dropZone!, {
|
||||
preventDefault: vi.fn(),
|
||||
dataTransfer: { files: [] }
|
||||
});
|
||||
|
||||
expect(onFileSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('file clearing', () => {
|
||||
it('should call onFileSelect with null when clear button is clicked', async () => {
|
||||
const onFileSelect = vi.fn();
|
||||
const mockFile: FileData = {
|
||||
file: new File([''], 'document.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'content',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} selectedFile={mockFile} />);
|
||||
|
||||
// Find the clear button (X icon button)
|
||||
const clearButton = screen.getByRole('button');
|
||||
await userEvent.click(clearButton);
|
||||
|
||||
expect(onFileSelect).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('click to upload', () => {
|
||||
it('should open file dialog when upload area is clicked', () => {
|
||||
render(<FileUpload {...defaultProps} />);
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const clickSpy = vi.spyOn(input, 'click');
|
||||
|
||||
const uploadArea = screen.getByText('Click to upload or drag and drop').parentElement?.parentElement?.parentElement;
|
||||
fireEvent.click(uploadArea!);
|
||||
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('file preview generation', () => {
|
||||
it('should generate preview URL for image files', async () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const imageFile = new File(['image'], 'photo.png', { type: 'image/png' });
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
const mockFileReader = {
|
||||
readAsDataURL: vi.fn(),
|
||||
result: 'data:image/png;base64,aW1hZ2U=',
|
||||
onload: null as ((ev: ProgressEvent<FileReader>) => void) | null
|
||||
};
|
||||
|
||||
vi.spyOn(global, 'FileReader').mockImplementation(() => {
|
||||
return mockFileReader as unknown as FileReader;
|
||||
});
|
||||
|
||||
fireEvent.change(input, { target: { files: [imageFile] } });
|
||||
mockFileReader.onload?.({} as ProgressEvent<FileReader>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFileSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
previewUrl: 'data:image/png;base64,aW1hZ2U=',
|
||||
type: 'image/png'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set previewUrl to null for PDF files', async () => {
|
||||
const onFileSelect = vi.fn();
|
||||
render(<FileUpload {...defaultProps} onFileSelect={onFileSelect} />);
|
||||
|
||||
const pdfFile = new File(['pdf'], 'doc.pdf', { type: 'application/pdf' });
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
|
||||
const mockFileReader = {
|
||||
readAsDataURL: vi.fn(),
|
||||
result: 'data:application/pdf;base64,cGRm',
|
||||
onload: null as ((ev: ProgressEvent<FileReader>) => void) | null
|
||||
};
|
||||
|
||||
vi.spyOn(global, 'FileReader').mockImplementation(() => {
|
||||
return mockFileReader as unknown as FileReader;
|
||||
});
|
||||
|
||||
fireEvent.change(input, { target: { files: [pdfFile] } });
|
||||
mockFileReader.onload?.({} as ProgressEvent<FileReader>);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onFileSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
previewUrl: null,
|
||||
type: 'application/pdf'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('accept attribute', () => {
|
||||
it('should pass accept attribute to input element', () => {
|
||||
render(<FileUpload {...defaultProps} accept=".pdf,.docx" />);
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
expect(input.accept).toBe('.pdf,.docx');
|
||||
});
|
||||
});
|
||||
|
||||
describe('file size display', () => {
|
||||
it('should display correct file size in MB', () => {
|
||||
const mockFile: FileData = {
|
||||
file: new File([''], 'large.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'content',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
// 5.5 MB
|
||||
Object.defineProperty(mockFile.file, 'size', { value: 5767168 });
|
||||
|
||||
render(<FileUpload {...defaultProps} selectedFile={mockFile} />);
|
||||
|
||||
expect(screen.getByText('5.50 MB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display small file sizes correctly', () => {
|
||||
const mockFile: FileData = {
|
||||
file: new File([''], 'small.txt', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'content',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
// 50 KB
|
||||
Object.defineProperty(mockFile.file, 'size', { value: 51200 });
|
||||
|
||||
render(<FileUpload {...defaultProps} selectedFile={mockFile} />);
|
||||
|
||||
expect(screen.getByText('0.05 MB')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
370
tests/components/ReviewPanel.test.tsx
Normal file
370
tests/components/ReviewPanel.test.tsx
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ReviewPanel } from '../../components/ReviewPanel';
|
||||
import { ExtractedField, FileData } from '../../types';
|
||||
|
||||
// Mock pdfService
|
||||
vi.mock('../../services/pdfService', () => ({
|
||||
createFilledPdf: vi.fn().mockResolvedValue(new Uint8Array([0, 1, 2, 3]))
|
||||
}));
|
||||
|
||||
// Mock jspdf
|
||||
vi.mock('jspdf', () => ({
|
||||
jsPDF: vi.fn().mockImplementation(() => ({
|
||||
text: vi.fn(),
|
||||
save: vi.fn()
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('ReviewPanel', () => {
|
||||
const mockFields: ExtractedField[] = [
|
||||
{
|
||||
key: 'firstName',
|
||||
label: 'First Name',
|
||||
value: 'John',
|
||||
validation: { status: 'VALID' },
|
||||
isVerified: false
|
||||
},
|
||||
{
|
||||
key: 'lastName',
|
||||
label: 'Last Name',
|
||||
value: 'Doe',
|
||||
validation: { status: 'WARNING', message: 'Name might be incomplete', suggestion: 'Doe Jr.' },
|
||||
isVerified: false
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
value: '2025-01-28',
|
||||
validation: { status: 'INVALID', message: 'Invalid date format' },
|
||||
isVerified: false
|
||||
}
|
||||
];
|
||||
|
||||
const mockFormFile: FileData = {
|
||||
file: new File([''], 'form.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'formbase64',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
const mockSourceFile: FileData = {
|
||||
file: new File([''], 'source.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'sourcebase64',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
fields: mockFields,
|
||||
formFile: mockFormFile,
|
||||
sourceFile: mockSourceFile,
|
||||
summary: 'Processed medical document',
|
||||
isFillablePdf: true,
|
||||
onReset: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render summary text', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('Processed medical document')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all fields', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('First Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Last Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all field values', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const values = inputs.map(input => (input as HTMLInputElement).value);
|
||||
expect(values).toContain('John');
|
||||
expect(values).toContain('Doe');
|
||||
expect(values).toContain('2025-01-28');
|
||||
});
|
||||
|
||||
it('should display warning messages for fields with warnings', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('Name might be incomplete')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display error messages for invalid fields', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('Invalid date format')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show suggestion button when available', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText(/Accept Fix: "Doe Jr\."/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show verification progress', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('0 / 3 Verified')).toBeInTheDocument();
|
||||
expect(screen.getByText('0 of 3 fields verified')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Visual Overlay Mode indicator when not fillable', () => {
|
||||
render(<ReviewPanel {...defaultProps} isFillablePdf={false} />);
|
||||
expect(screen.getAllByText('Visual Overlay Mode').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not show Visual Overlay Mode indicator when fillable', () => {
|
||||
render(<ReviewPanel {...defaultProps} isFillablePdf={true} />);
|
||||
expect(screen.queryByText('Visual Overlay Mode')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('field sorting', () => {
|
||||
it('should sort attention-needed fields before valid fields', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
// The last input should be the VALID field (John)
|
||||
expect(inputs[inputs.length - 1]).toHaveValue('John');
|
||||
});
|
||||
|
||||
it('should sort verified fields after unverified', () => {
|
||||
const fieldsWithVerified: ExtractedField[] = [
|
||||
{ key: 'a', label: 'A', value: 'val1', validation: { status: 'VALID' }, isVerified: true },
|
||||
{ key: 'b', label: 'B', value: 'val2', validation: { status: 'VALID' }, isVerified: false }
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={fieldsWithVerified} />);
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
// Unverified first
|
||||
expect(inputs[0]).toHaveValue('val2');
|
||||
expect(inputs[1]).toHaveValue('val1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('field editing', () => {
|
||||
it('should update field value when edited', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const johnInput = inputs.find(input => (input as HTMLInputElement).value === 'John');
|
||||
|
||||
await user.clear(johnInput!);
|
||||
await user.type(johnInput!, 'Jane');
|
||||
|
||||
expect(johnInput).toHaveValue('Jane');
|
||||
});
|
||||
|
||||
it('should auto-verify field when manually edited', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const doeInput = inputs.find(input => (input as HTMLInputElement).value === 'Doe');
|
||||
|
||||
await user.clear(doeInput!);
|
||||
await user.type(doeInput!, 'Smith');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('VERIFIED').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('verification toggle', () => {
|
||||
it('should toggle verification status when checkbox clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const verifyButtons = screen.getAllByTitle('Mark as verified');
|
||||
await user.click(verifyButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1 / 3 Verified')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update progress when field is verified', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const verifyButtons = screen.getAllByTitle('Mark as verified');
|
||||
await user.click(verifyButtons[0]);
|
||||
await user.click(verifyButtons[1]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('2 / 3 Verified')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('suggestion application', () => {
|
||||
it('should apply suggestion when Accept Fix button is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const acceptButton = screen.getByText(/Accept Fix: "Doe Jr\."/);
|
||||
await user.click(acceptButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const inputs = screen.getAllByRole('textbox');
|
||||
const updatedInput = inputs.find(input => (input as HTMLInputElement).value === 'Doe Jr.');
|
||||
expect(updatedInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filtering', () => {
|
||||
it('should show all fields when ALL filter is selected', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('First Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Last Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should filter to only attention-needed fields when ATTENTION filter is selected', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
|
||||
const attentionButton = screen.getByText(/Needs Review/);
|
||||
await user.click(attentionButton);
|
||||
|
||||
expect(screen.getByText('Last Name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Date')).toBeInTheDocument();
|
||||
expect(screen.queryByText('First Name')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show correct count in filter buttons', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('All Fields (3)')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Needs Review \(2\)/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset functionality', () => {
|
||||
it('should call onReset when Start Over button is clicked', async () => {
|
||||
const onReset = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<ReviewPanel {...defaultProps} onReset={onReset} />);
|
||||
|
||||
const startOverButton = screen.getByText('Start Over');
|
||||
await user.click(startOverButton);
|
||||
|
||||
expect(onReset).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('preview generation', () => {
|
||||
it('should use image preview URL when form is not PDF', async () => {
|
||||
const imageFormFile: FileData = {
|
||||
file: new File([''], 'form.png', { type: 'image/png' }),
|
||||
previewUrl: 'data:image/png;base64,imagedata',
|
||||
base64: 'imagedata',
|
||||
type: 'image/png'
|
||||
};
|
||||
|
||||
render(<ReviewPanel {...defaultProps} formFile={imageFormFile} />);
|
||||
|
||||
// Wait for the useEffect to set the preview URL
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText('Form Document')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('source context', () => {
|
||||
it('should display source context when available and field not verified', () => {
|
||||
const fieldsWithContext: ExtractedField[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
value: 'John',
|
||||
sourceContext: 'Patient name: John Smith',
|
||||
validation: { status: 'WARNING', message: 'Check name' },
|
||||
isVerified: false
|
||||
}
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={fieldsWithContext} />);
|
||||
expect(screen.getByText(/"Patient name: John Smith"/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide source context when field is verified', async () => {
|
||||
const user = userEvent.setup();
|
||||
const fieldsWithContext: ExtractedField[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
value: 'John',
|
||||
sourceContext: 'Patient name: John Smith',
|
||||
validation: { status: 'WARNING', message: 'Check name' },
|
||||
isVerified: false
|
||||
}
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={fieldsWithContext} />);
|
||||
|
||||
const verifyButton = screen.getByTitle('Mark as verified');
|
||||
await user.click(verifyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/"Patient name: John Smith"/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('should not show Needs Review button when all fields are valid', () => {
|
||||
const allValidFields: ExtractedField[] = [
|
||||
{ key: 'a', label: 'A', value: 'val1', validation: { status: 'VALID' }, isVerified: true }
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={allValidFields} />);
|
||||
expect(screen.queryByText(/Needs Review/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('progress indicator', () => {
|
||||
it('should show Ready to Download when all fields verified', () => {
|
||||
const allVerifiedFields: ExtractedField[] = [
|
||||
{ key: 'a', label: 'A', value: 'val1', validation: { status: 'VALID' }, isVerified: true }
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={allVerifiedFields} />);
|
||||
expect(screen.getByText('Ready to Download')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show Review in progress when not all fields verified', () => {
|
||||
render(<ReviewPanel {...defaultProps} />);
|
||||
expect(screen.getByText('Review in progress')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('field key fallback', () => {
|
||||
it('should display key as label fallback when label is missing', () => {
|
||||
const fieldsWithoutLabel: ExtractedField[] = [
|
||||
{ key: 'fieldKey', label: '', value: 'test', validation: { status: 'VALID' }, isVerified: false }
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={fieldsWithoutLabel} />);
|
||||
expect(screen.getByText('fieldKey')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display Unknown Field when both key and label are missing', () => {
|
||||
const fieldsWithoutBoth: ExtractedField[] = [
|
||||
{ label: '', value: 'test', validation: { status: 'VALID' }, isVerified: false }
|
||||
];
|
||||
|
||||
render(<ReviewPanel {...defaultProps} fields={fieldsWithoutBoth} />);
|
||||
expect(screen.getByText('Unknown Field')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue