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
4333
package-lock.json
generated
Normal file
4333
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
|
@ -6,7 +6,11 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-dom": "^19.2.4",
|
||||
|
|
@ -20,6 +24,12 @@
|
|||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
"vite": "^6.2.0",
|
||||
"vitest": "^3.0.0",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.0",
|
||||
"@testing-library/user-event": "^14.5.0",
|
||||
"jsdom": "^26.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
344
tests/fill_pdf_test.py
Normal file
344
tests/fill_pdf_test.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for fill_pdf.py - PDF Form Filler utility
|
||||
|
||||
Run with: pytest tests/fill_pdf_test.py -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch, mock_open
|
||||
|
||||
import pytest
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from fill_pdf import extract_fields, fill_pdf, main
|
||||
|
||||
|
||||
class TestExtractFields:
|
||||
"""Tests for the extract_fields function"""
|
||||
|
||||
def test_extract_fields_returns_empty_for_no_fields(self):
|
||||
"""Should return empty list when PDF has no form fields"""
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.return_value.get_fields.return_value = None
|
||||
|
||||
result = extract_fields('test.pdf')
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_extract_fields_returns_field_info(self):
|
||||
"""Should return list of field info dicts"""
|
||||
mock_fields = {
|
||||
'txtName': {'/FT': '/Tx', '/V': 'John'},
|
||||
'txtDate': {'/FT': '/Tx', '/V': '2025-01-28'}
|
||||
}
|
||||
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.return_value.get_fields.return_value = mock_fields
|
||||
|
||||
result = extract_fields('test.pdf')
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]['field_id'] == 'txtName'
|
||||
assert result[0]['type'] == '/Tx'
|
||||
assert result[0]['value'] == 'John'
|
||||
|
||||
def test_extract_fields_handles_missing_type(self):
|
||||
"""Should handle fields without /FT type"""
|
||||
mock_fields = {
|
||||
'field1': {'/V': 'value1'} # No /FT
|
||||
}
|
||||
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.return_value.get_fields.return_value = mock_fields
|
||||
|
||||
result = extract_fields('test.pdf')
|
||||
|
||||
assert result[0]['type'] == ''
|
||||
|
||||
def test_extract_fields_handles_missing_value(self):
|
||||
"""Should handle fields without /V value"""
|
||||
mock_fields = {
|
||||
'field1': {'/FT': '/Tx'} # No /V
|
||||
}
|
||||
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.return_value.get_fields.return_value = mock_fields
|
||||
|
||||
result = extract_fields('test.pdf')
|
||||
|
||||
assert result[0]['value'] == ''
|
||||
|
||||
def test_extract_fields_raises_on_invalid_pdf(self):
|
||||
"""Should raise exception for invalid PDF file"""
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.side_effect = Exception('Invalid PDF')
|
||||
|
||||
with pytest.raises(Exception, match='Invalid PDF'):
|
||||
extract_fields('invalid.pdf')
|
||||
|
||||
|
||||
class TestFillPdf:
|
||||
"""Tests for the fill_pdf function"""
|
||||
|
||||
def test_fill_pdf_writes_output_file(self):
|
||||
"""Should create output PDF file"""
|
||||
with patch('fill_pdf.PdfReader') as mock_reader, \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()) as mock_file:
|
||||
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
mock_writer_instance.pages = [MagicMock()]
|
||||
|
||||
fill_pdf('input.pdf', {'field1': 'value1'}, 'output.pdf')
|
||||
|
||||
mock_file.assert_called_once_with('output.pdf', 'wb')
|
||||
mock_writer_instance.write.assert_called_once()
|
||||
|
||||
def test_fill_pdf_appends_reader_to_writer(self):
|
||||
"""Should append input PDF to writer"""
|
||||
with patch('fill_pdf.PdfReader') as mock_reader, \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()):
|
||||
|
||||
mock_reader_instance = MagicMock()
|
||||
mock_reader.return_value = mock_reader_instance
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
mock_writer_instance.pages = [MagicMock()]
|
||||
|
||||
fill_pdf('input.pdf', {}, 'output.pdf')
|
||||
|
||||
mock_writer_instance.append.assert_called_once_with(mock_reader_instance)
|
||||
|
||||
def test_fill_pdf_updates_all_pages(self):
|
||||
"""Should update form fields on all pages"""
|
||||
with patch('fill_pdf.PdfReader'), \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()):
|
||||
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
# Simulate 3 pages
|
||||
mock_pages = [MagicMock(), MagicMock(), MagicMock()]
|
||||
mock_writer_instance.pages = mock_pages
|
||||
|
||||
field_values = {'field1': 'value1'}
|
||||
fill_pdf('input.pdf', field_values, 'output.pdf')
|
||||
|
||||
assert mock_writer_instance.update_page_form_field_values.call_count == 3
|
||||
|
||||
def test_fill_pdf_passes_field_values(self):
|
||||
"""Should pass correct field values to update method"""
|
||||
with patch('fill_pdf.PdfReader'), \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()):
|
||||
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
mock_page = MagicMock()
|
||||
mock_writer_instance.pages = [mock_page]
|
||||
|
||||
field_values = {'txtName': 'John Doe', 'txtDate': '2025-01-28'}
|
||||
fill_pdf('input.pdf', field_values, 'output.pdf')
|
||||
|
||||
mock_writer_instance.update_page_form_field_values.assert_called_with(
|
||||
mock_page,
|
||||
field_values
|
||||
)
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Tests for the main CLI function"""
|
||||
|
||||
def test_main_extraction_mode(self, capsys):
|
||||
"""Should extract and print fields in --extract mode"""
|
||||
test_fields = [{'field_id': 'test', 'type': '/Tx', 'value': ''}]
|
||||
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', '--extract', 'input.pdf']), \
|
||||
patch('fill_pdf.extract_fields', return_value=test_fields) as mock_extract:
|
||||
|
||||
main()
|
||||
|
||||
mock_extract.assert_called_once_with('input.pdf')
|
||||
captured = capsys.readouterr()
|
||||
output = json.loads(captured.out)
|
||||
assert output == test_fields
|
||||
|
||||
def test_main_fill_mode_with_dict_json(self, capsys):
|
||||
"""Should fill PDF with dict-format JSON"""
|
||||
json_data = {'field1': 'value1', 'field2': 'value2'}
|
||||
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', 'in.pdf', 'values.json', 'out.pdf']), \
|
||||
patch('builtins.open', mock_open(read_data=json.dumps(json_data))), \
|
||||
patch('fill_pdf.fill_pdf') as mock_fill:
|
||||
|
||||
main()
|
||||
|
||||
mock_fill.assert_called_once_with('in.pdf', json_data, 'out.pdf')
|
||||
captured = capsys.readouterr()
|
||||
assert 'erfolgreich' in captured.out
|
||||
|
||||
def test_main_fill_mode_with_list_json(self, capsys):
|
||||
"""Should convert list-format JSON to dict and fill PDF"""
|
||||
json_list = [
|
||||
{'field_id': 'field1', 'value': 'value1'},
|
||||
{'field_id': 'field2', 'value': 'value2'}
|
||||
]
|
||||
expected_dict = {'field1': 'value1', 'field2': 'value2'}
|
||||
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', 'in.pdf', 'values.json', 'out.pdf']), \
|
||||
patch('builtins.open', mock_open(read_data=json.dumps(json_list))), \
|
||||
patch('fill_pdf.fill_pdf') as mock_fill:
|
||||
|
||||
main()
|
||||
|
||||
mock_fill.assert_called_once_with('in.pdf', expected_dict, 'out.pdf')
|
||||
|
||||
def test_main_shows_usage_on_wrong_args(self, capsys):
|
||||
"""Should print usage and exit with code 1 on wrong arguments"""
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', 'only_one_arg']):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
captured = capsys.readouterr()
|
||||
assert 'Usage:' in captured.out
|
||||
|
||||
def test_main_shows_usage_on_no_args(self, capsys):
|
||||
"""Should print usage when no arguments provided"""
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py']):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests using real temporary files"""
|
||||
|
||||
def test_fill_pdf_with_real_temporary_files(self):
|
||||
"""Integration test with actual file operations"""
|
||||
# This test requires pypdf to be installed
|
||||
# Skip if not available
|
||||
pytest.importorskip('pypdf')
|
||||
|
||||
from pypdf import PdfWriter
|
||||
|
||||
# Create a simple PDF with form fields
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
input_path = os.path.join(tmpdir, 'input.pdf')
|
||||
output_path = os.path.join(tmpdir, 'output.pdf')
|
||||
|
||||
# Create minimal test PDF
|
||||
writer = PdfWriter()
|
||||
writer.add_blank_page(width=612, height=792)
|
||||
with open(input_path, 'wb') as f:
|
||||
writer.write(f)
|
||||
|
||||
# PDFs without AcroForm will raise an error when trying to fill
|
||||
# This is expected behavior from pypdf
|
||||
from pypdf.errors import PyPdfError
|
||||
with pytest.raises(PyPdfError):
|
||||
fill_pdf(input_path, {}, output_path)
|
||||
|
||||
def test_extract_fields_with_real_pdf(self):
|
||||
"""Integration test for field extraction"""
|
||||
pytest.importorskip('pypdf')
|
||||
|
||||
from pypdf import PdfWriter
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
pdf_path = os.path.join(tmpdir, 'test.pdf')
|
||||
|
||||
# Create PDF without form fields
|
||||
writer = PdfWriter()
|
||||
writer.add_blank_page(width=612, height=792)
|
||||
with open(pdf_path, 'wb') as f:
|
||||
writer.write(f)
|
||||
|
||||
result = extract_fields(pdf_path)
|
||||
|
||||
assert result == []
|
||||
|
||||
|
||||
class TestEdgeCases:
|
||||
"""Edge case tests"""
|
||||
|
||||
def test_extract_fields_with_empty_string_value(self):
|
||||
"""Should handle fields with empty string values"""
|
||||
mock_fields = {
|
||||
'emptyField': {'/FT': '/Tx', '/V': ''}
|
||||
}
|
||||
|
||||
with patch('fill_pdf.PdfReader') as mock_reader:
|
||||
mock_reader.return_value.get_fields.return_value = mock_fields
|
||||
|
||||
result = extract_fields('test.pdf')
|
||||
|
||||
assert result[0]['value'] == ''
|
||||
|
||||
def test_fill_pdf_with_empty_dict(self):
|
||||
"""Should handle empty field values dict"""
|
||||
with patch('fill_pdf.PdfReader'), \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()):
|
||||
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
mock_writer_instance.pages = [MagicMock()]
|
||||
|
||||
# Should not raise
|
||||
fill_pdf('input.pdf', {}, 'output.pdf')
|
||||
|
||||
mock_writer_instance.update_page_form_field_values.assert_called_once()
|
||||
|
||||
def test_main_with_unicode_filename(self, capsys):
|
||||
"""Should handle unicode characters in filenames"""
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', '--extract', 'über.pdf']), \
|
||||
patch('fill_pdf.extract_fields', return_value=[]) as mock_extract:
|
||||
|
||||
main()
|
||||
|
||||
mock_extract.assert_called_once_with('über.pdf')
|
||||
|
||||
def test_fill_pdf_with_special_characters_in_values(self):
|
||||
"""Should handle special characters in field values"""
|
||||
with patch('fill_pdf.PdfReader'), \
|
||||
patch('fill_pdf.PdfWriter') as mock_writer, \
|
||||
patch('builtins.open', mock_open()):
|
||||
|
||||
mock_writer_instance = MagicMock()
|
||||
mock_writer.return_value = mock_writer_instance
|
||||
mock_writer_instance.pages = [MagicMock()]
|
||||
|
||||
special_values = {
|
||||
'field1': 'Müller, François & José',
|
||||
'field2': '日本語テスト',
|
||||
'field3': '<script>alert("xss")</script>'
|
||||
}
|
||||
|
||||
# Should not raise
|
||||
fill_pdf('input.pdf', special_values, 'output.pdf')
|
||||
|
||||
def test_main_with_json_encoding_utf8(self, capsys):
|
||||
"""Should handle UTF-8 encoded JSON files"""
|
||||
json_data = {'name': 'Müller', 'city': '東京'}
|
||||
|
||||
with patch.object(sys, 'argv', ['fill_pdf.py', 'in.pdf', 'values.json', 'out.pdf']), \
|
||||
patch('builtins.open', mock_open(read_data=json.dumps(json_data, ensure_ascii=False))), \
|
||||
patch('fill_pdf.fill_pdf') as mock_fill:
|
||||
|
||||
main()
|
||||
|
||||
mock_fill.assert_called_once()
|
||||
call_args = mock_fill.call_args[0]
|
||||
assert call_args[1]['name'] == 'Müller'
|
||||
assert call_args[1]['city'] == '東京'
|
||||
312
tests/services/geminiService.test.ts
Normal file
312
tests/services/geminiService.test.ts
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { FileData, FormResponse } from '../../types';
|
||||
import { PdfFieldInfo } from '../../services/pdfService';
|
||||
|
||||
// Create a mock function that can be referenced in the mock
|
||||
const mockGenerateContent = vi.fn();
|
||||
|
||||
// Mock the @google/genai module
|
||||
vi.mock('@google/genai', async () => {
|
||||
return {
|
||||
GoogleGenAI: vi.fn().mockImplementation(() => ({
|
||||
models: {
|
||||
generateContent: mockGenerateContent
|
||||
}
|
||||
})),
|
||||
Type: {
|
||||
OBJECT: 'OBJECT',
|
||||
STRING: 'STRING',
|
||||
ARRAY: 'ARRAY',
|
||||
INTEGER: 'INTEGER'
|
||||
},
|
||||
Schema: {}
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocking
|
||||
const { processDocuments } = await import('../../services/geminiService');
|
||||
|
||||
describe('geminiService', () => {
|
||||
const mockBlankForm: FileData = {
|
||||
file: new File([''], 'form.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'base64FormContent',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
const mockSourceDocument: FileData = {
|
||||
file: new File([''], 'source.pdf', { type: 'application/pdf' }),
|
||||
previewUrl: null,
|
||||
base64: 'base64SourceContent',
|
||||
type: 'application/pdf'
|
||||
};
|
||||
|
||||
const mockPdfFields: PdfFieldInfo[] = [
|
||||
{ name: 'firstName', type: 'PDFTextField' },
|
||||
{ name: 'lastName', type: 'PDFTextField' }
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('processDocuments', () => {
|
||||
it('should successfully process documents and return FormResponse', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Processed medical letter',
|
||||
fields: [
|
||||
{
|
||||
key: 'firstName',
|
||||
label: 'First Name',
|
||||
value: 'John',
|
||||
validation: { status: 'VALID' }
|
||||
},
|
||||
{
|
||||
key: 'lastName',
|
||||
label: 'Last Name',
|
||||
value: 'Doe',
|
||||
validation: { status: 'VALID' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
expect(result).toEqual(mockResponse);
|
||||
expect(mockGenerateContent).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should include field names in prompt when pdfFields are provided', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Test',
|
||||
fields: []
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
const callArgs = mockGenerateContent.mock.calls[0][0];
|
||||
expect(callArgs.config.systemInstruction).toContain('FILLABLE PDF');
|
||||
expect(callArgs.config.systemInstruction).toContain('firstName');
|
||||
expect(callArgs.config.systemInstruction).toContain('lastName');
|
||||
});
|
||||
|
||||
it('should use visual mode when no pdfFields are provided', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Test',
|
||||
fields: []
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
await processDocuments(mockBlankForm, mockSourceDocument, []);
|
||||
|
||||
const callArgs = mockGenerateContent.mock.calls[0][0];
|
||||
expect(callArgs.config.systemInstruction).toContain('VISUAL FILLING');
|
||||
expect(callArgs.config.systemInstruction).toContain('0 to 1000');
|
||||
});
|
||||
|
||||
it('should throw error when API returns no response', async () => {
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: null
|
||||
});
|
||||
|
||||
await expect(
|
||||
processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields)
|
||||
).rejects.toThrow('No response from Gemini');
|
||||
});
|
||||
|
||||
it('should throw error when API returns empty string', async () => {
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: ''
|
||||
});
|
||||
|
||||
await expect(
|
||||
processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw error on API failure', async () => {
|
||||
const apiError = new Error('API rate limit exceeded');
|
||||
mockGenerateContent.mockRejectedValue(apiError);
|
||||
|
||||
await expect(
|
||||
processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields)
|
||||
).rejects.toThrow('API rate limit exceeded');
|
||||
});
|
||||
|
||||
it('should throw error on invalid JSON response', async () => {
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: 'not valid json'
|
||||
});
|
||||
|
||||
await expect(
|
||||
processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle fields with validation warnings', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Processed with warnings',
|
||||
fields: [
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
value: '28-01-2025',
|
||||
validation: {
|
||||
status: 'WARNING',
|
||||
message: 'Date format might be incorrect',
|
||||
suggestion: '28.01.2025'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
expect(result.fields[0].validation?.status).toBe('WARNING');
|
||||
expect(result.fields[0].validation?.suggestion).toBe('28.01.2025');
|
||||
});
|
||||
|
||||
it('should handle fields with validation errors', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Processed with errors',
|
||||
fields: [
|
||||
{
|
||||
key: 'required_field',
|
||||
label: 'Required Field',
|
||||
value: '',
|
||||
validation: {
|
||||
status: 'INVALID',
|
||||
message: 'This field is required but was not found in source'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
expect(result.fields[0].validation?.status).toBe('INVALID');
|
||||
});
|
||||
|
||||
it('should handle visual mode with coordinates', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Visual mode response',
|
||||
fields: [
|
||||
{
|
||||
label: 'Name',
|
||||
value: 'John Doe',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: {
|
||||
pageIndex: 0,
|
||||
x: 150,
|
||||
y: 200
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, []);
|
||||
|
||||
expect(result.fields[0].coordinates).toEqual({
|
||||
pageIndex: 0,
|
||||
x: 150,
|
||||
y: 200
|
||||
});
|
||||
});
|
||||
|
||||
it('should include sourceContext in response', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Test',
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
value: 'John Doe',
|
||||
sourceContext: 'Patient: John Doe, DOB: 01.01.1990',
|
||||
validation: { status: 'VALID' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
expect(result.fields[0].sourceContext).toBe('Patient: John Doe, DOB: 01.01.1990');
|
||||
});
|
||||
|
||||
it('should pass correct MIME types in request', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'Test',
|
||||
fields: []
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const imageSource: FileData = {
|
||||
...mockSourceDocument,
|
||||
type: 'image/png'
|
||||
};
|
||||
|
||||
await processDocuments(mockBlankForm, imageSource, []);
|
||||
|
||||
const callArgs = mockGenerateContent.mock.calls[0][0];
|
||||
expect(callArgs.contents.parts[0].inlineData.mimeType).toBe('application/pdf');
|
||||
expect(callArgs.contents.parts[2].inlineData.mimeType).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should handle network timeout', async () => {
|
||||
mockGenerateContent.mockRejectedValue(new Error('Network timeout'));
|
||||
|
||||
await expect(
|
||||
processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields)
|
||||
).rejects.toThrow('Network timeout');
|
||||
});
|
||||
|
||||
it('should handle empty fields array response', async () => {
|
||||
const mockResponse: FormResponse = {
|
||||
summary: 'No fields found',
|
||||
fields: []
|
||||
};
|
||||
|
||||
mockGenerateContent.mockResolvedValue({
|
||||
text: JSON.stringify(mockResponse)
|
||||
});
|
||||
|
||||
const result = await processDocuments(mockBlankForm, mockSourceDocument, mockPdfFields);
|
||||
|
||||
expect(result.fields).toEqual([]);
|
||||
expect(result.summary).toBe('No fields found');
|
||||
});
|
||||
});
|
||||
});
|
||||
316
tests/services/pdfService.test.ts
Normal file
316
tests/services/pdfService.test.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getPdfFields, createFilledPdf } from '../../services/pdfService';
|
||||
import { ExtractedField } from '../../types';
|
||||
import { PDFDocument, PDFTextField, PDFCheckBox } from 'pdf-lib';
|
||||
|
||||
// Helper to create a minimal valid PDF base64 for testing
|
||||
async function createTestPdfBase64(withForm = false, fieldNames: string[] = []): Promise<string> {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const page = pdfDoc.addPage([612, 792]); // Letter size
|
||||
|
||||
if (withForm) {
|
||||
const form = pdfDoc.getForm();
|
||||
fieldNames.forEach((name, index) => {
|
||||
const textField = form.createTextField(name);
|
||||
textField.addToPage(page, { x: 50, y: 700 - (index * 30), width: 200, height: 20 });
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfBytes).toString('base64');
|
||||
}
|
||||
|
||||
async function createTestPdfWithCheckbox(): Promise<string> {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const page = pdfDoc.addPage([612, 792]);
|
||||
const form = pdfDoc.getForm();
|
||||
|
||||
const checkbox = form.createCheckBox('testCheckbox');
|
||||
checkbox.addToPage(page, { x: 50, y: 700, width: 20, height: 20 });
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfBytes).toString('base64');
|
||||
}
|
||||
|
||||
describe('pdfService', () => {
|
||||
describe('getPdfFields', () => {
|
||||
it('should extract fields from a fillable PDF', async () => {
|
||||
const base64 = await createTestPdfBase64(true, ['firstName', 'lastName', 'email']);
|
||||
|
||||
const fields = await getPdfFields(base64);
|
||||
|
||||
expect(fields).toHaveLength(3);
|
||||
expect(fields[0].name).toBe('firstName');
|
||||
expect(fields[1].name).toBe('lastName');
|
||||
expect(fields[2].name).toBe('email');
|
||||
expect(fields[0].type).toBe('PDFTextField');
|
||||
});
|
||||
|
||||
it('should return empty array for PDF without form fields', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
|
||||
const fields = await getPdfFields(base64);
|
||||
|
||||
expect(fields).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid/corrupted PDF', async () => {
|
||||
const invalidBase64 = Buffer.from('not a valid pdf').toString('base64');
|
||||
|
||||
const fields = await getPdfFields(invalidBase64);
|
||||
|
||||
expect(fields).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for empty string input', async () => {
|
||||
const fields = await getPdfFields('');
|
||||
|
||||
expect(fields).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect checkbox fields correctly', async () => {
|
||||
const base64 = await createTestPdfWithCheckbox();
|
||||
|
||||
const fields = await getPdfFields(base64);
|
||||
|
||||
expect(fields).toHaveLength(1);
|
||||
expect(fields[0].name).toBe('testCheckbox');
|
||||
expect(fields[0].type).toBe('PDFCheckBox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFilledPdf', () => {
|
||||
describe('fillable PDF mode (isFillable=true)', () => {
|
||||
it('should fill text fields in a fillable PDF', async () => {
|
||||
const base64 = await createTestPdfBase64(true, ['name', 'city']);
|
||||
const fields: ExtractedField[] = [
|
||||
{ key: 'name', label: 'Name', value: 'John Doe', validation: { status: 'VALID' } },
|
||||
{ key: 'city', label: 'City', value: 'Berlin', validation: { status: 'VALID' } }
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify the filled values by loading the result
|
||||
const filledDoc = await PDFDocument.load(result);
|
||||
const form = filledDoc.getForm();
|
||||
const nameField = form.getTextField('name');
|
||||
const cityField = form.getTextField('city');
|
||||
|
||||
expect(nameField.getText()).toBe('John Doe');
|
||||
expect(cityField.getText()).toBe('Berlin');
|
||||
});
|
||||
|
||||
it('should handle checkbox fields with true/yes values', async () => {
|
||||
const base64 = await createTestPdfWithCheckbox();
|
||||
const fields: ExtractedField[] = [
|
||||
{ key: 'testCheckbox', label: 'Test', value: 'true', validation: { status: 'VALID' } }
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
|
||||
const filledDoc = await PDFDocument.load(result);
|
||||
const form = filledDoc.getForm();
|
||||
const checkbox = form.getCheckBox('testCheckbox');
|
||||
|
||||
expect(checkbox.isChecked()).toBe(true);
|
||||
});
|
||||
|
||||
it('should uncheck checkbox fields with false/no values', async () => {
|
||||
const base64 = await createTestPdfWithCheckbox();
|
||||
const fields: ExtractedField[] = [
|
||||
{ key: 'testCheckbox', label: 'Test', value: 'false', validation: { status: 'VALID' } }
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
|
||||
const filledDoc = await PDFDocument.load(result);
|
||||
const form = filledDoc.getForm();
|
||||
const checkbox = form.getCheckBox('testCheckbox');
|
||||
|
||||
expect(checkbox.isChecked()).toBe(false);
|
||||
});
|
||||
|
||||
it('should skip fields without a key', async () => {
|
||||
const base64 = await createTestPdfBase64(true, ['name']);
|
||||
const fields: ExtractedField[] = [
|
||||
{ label: 'Name', value: 'John Doe', validation: { status: 'VALID' } } // no key
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should skip non-existent fields gracefully', async () => {
|
||||
const base64 = await createTestPdfBase64(true, ['name']);
|
||||
const fields: ExtractedField[] = [
|
||||
{ key: 'nonexistent', label: 'Non-existent', value: 'test', validation: { status: 'VALID' } }
|
||||
];
|
||||
|
||||
// Should not throw
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should handle empty field values', async () => {
|
||||
const base64 = await createTestPdfBase64(true, ['name']);
|
||||
const fields: ExtractedField[] = [
|
||||
{ key: 'name', label: 'Name', value: '', validation: { status: 'VALID' } }
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, true);
|
||||
|
||||
const filledDoc = await PDFDocument.load(result);
|
||||
const form = filledDoc.getForm();
|
||||
const nameField = form.getTextField('name');
|
||||
|
||||
// pdf-lib returns undefined for empty text fields, not empty string
|
||||
expect(nameField.getText()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('visual overlay mode (isFillable=false)', () => {
|
||||
it('should draw text at specified coordinates', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Name',
|
||||
value: 'John Doe',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 100, y: 100 }
|
||||
}
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should skip fields without coordinates', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{ label: 'Name', value: 'John Doe', validation: { status: 'VALID' } } // no coordinates
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should skip fields without values', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Name',
|
||||
value: '',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 100, y: 100 }
|
||||
}
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should skip fields with invalid page index', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Name',
|
||||
value: 'John Doe',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 99, x: 100, y: 100 } // invalid page
|
||||
}
|
||||
];
|
||||
|
||||
// Should not throw
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should skip fields with negative page index', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Name',
|
||||
value: 'John Doe',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: -1, x: 100, y: 100 }
|
||||
}
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should convert 0-1000 coordinates to PDF points correctly', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Corner',
|
||||
value: 'Test',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 0, y: 0 } // top-left
|
||||
},
|
||||
{
|
||||
label: 'Bottom Right',
|
||||
value: 'Test2',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 1000, y: 1000 } // bottom-right
|
||||
}
|
||||
];
|
||||
|
||||
// Should handle edge coordinates without error
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
|
||||
it('should handle multiple fields on the same page', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [
|
||||
{
|
||||
label: 'Field1',
|
||||
value: 'Value1',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 100, y: 100 }
|
||||
},
|
||||
{
|
||||
label: 'Field2',
|
||||
value: 'Value2',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 100, y: 200 }
|
||||
},
|
||||
{
|
||||
label: 'Field3',
|
||||
value: 'Value3',
|
||||
validation: { status: 'VALID' },
|
||||
coordinates: { pageIndex: 0, x: 100, y: 300 }
|
||||
}
|
||||
];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should throw error for invalid base64 input', async () => {
|
||||
const fields: ExtractedField[] = [];
|
||||
|
||||
await expect(createFilledPdf('invalid-base64', fields, true)).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle empty fields array', async () => {
|
||||
const base64 = await createTestPdfBase64(false);
|
||||
const fields: ExtractedField[] = [];
|
||||
|
||||
const result = await createFilledPdf(base64, fields, false);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
21
tests/setup.ts
Normal file
21
tests/setup.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Mock URL.createObjectURL and URL.revokeObjectURL
|
||||
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
|
||||
global.URL.revokeObjectURL = vi.fn();
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
23
vitest.config.ts
Normal file
23
vitest.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
include: ['tests/**/*.test.{ts,tsx}'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
include: ['services/**/*.ts', 'components/**/*.tsx'],
|
||||
exclude: ['**/node_modules/**', '**/tests/**']
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/home/user/Rentenversicherer'
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue