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

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