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:
Claude 2026-01-28 18:52:35 +00:00
parent 48306e882d
commit cbacd3430c
No known key found for this signature in database
9 changed files with 6062 additions and 2 deletions

4333
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

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

View 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
View 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'] == '東京'

View 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');
});
});
});

View 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
View 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
View 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'
}
}
});