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
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue