feat: add intelligent import for CSV, Excel, and PDF files

Add IntelligentImportAugmentation with support for:
- CSV: auto-detection of encoding, delimiters, and field types
- Excel: multi-sheet extraction with metadata preservation
- PDF: text extraction, table detection, and metadata extraction

Features:
- Automatic format detection from file extension or content
- Intelligent type inference (string, number, boolean, date)
- Seamless integration with neural entity extraction
- Production-ready with 69 comprehensive tests

Dependencies added:
- xlsx@^0.18.5 for Excel parsing
- pdfjs-dist@^4.0.379 for PDF parsing
- csv-parse@^6.1.0 for CSV parsing
- chardet@^2.0.0 for encoding detection

Documentation:
- Updated README with import examples
- Updated API_REFERENCE with comprehensive import() docs
- Updated import-anything.md guide
- Added working example: import-excel-pdf-csv.ts
- Updated augmentations README
This commit is contained in:
David Snelling 2025-10-01 16:51:03 -07:00
parent aaf8e0f411
commit 814cbb48ee
33 changed files with 4664 additions and 28 deletions

View file

@ -0,0 +1,281 @@
/**
* CSV Handler Tests
* Comprehensive tests for CSV parsing, encoding detection, and type inference
*/
import { describe, it, expect, beforeAll } from 'vitest'
import { CSVHandler } from '../../../src/augmentations/intelligentImport/handlers/csvHandler.js'
import { promises as fs } from 'fs'
import * as path from 'path'
describe('CSVHandler', () => {
let handler: CSVHandler
const fixturesPath = path.join(process.cwd(), 'tests/fixtures/import')
beforeAll(() => {
handler = new CSVHandler()
})
describe('canHandle', () => {
it('should handle .csv extension', () => {
expect(handler.canHandle({ filename: 'data.csv' })).toBe(true)
})
it('should handle .tsv extension', () => {
expect(handler.canHandle({ filename: 'data.tsv' })).toBe(true)
})
it('should handle .txt extension', () => {
expect(handler.canHandle({ filename: 'data.txt' })).toBe(true)
})
it('should handle CSV content (comma)', () => {
const content = 'name,age,email\nJohn,30,john@example.com'
expect(handler.canHandle(content)).toBe(true)
})
it('should handle CSV content (semicolon)', () => {
const content = 'name;age;email\nJohn;30;john@example.com'
expect(handler.canHandle(content)).toBe(true)
})
it('should not handle non-CSV content', () => {
const content = 'This is just plain text without any structure'
expect(handler.canHandle(content)).toBe(false)
})
it('should not handle .xlsx extension', () => {
expect(handler.canHandle({ filename: 'data.xlsx' })).toBe(false)
})
})
describe('process - simple CSV', () => {
it('should parse simple comma-delimited CSV', async () => {
const filePath = path.join(fixturesPath, 'simple.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.csv' })
expect(result.format).toBe('csv')
expect(result.data).toHaveLength(4)
expect(result.data[0]).toEqual({
name: 'John Doe',
age: 30,
email: 'john@example.com',
active: true
})
expect(result.metadata.rowCount).toBe(4)
expect(result.metadata.fields).toEqual(['name', 'age', 'email', 'active'])
expect(result.metadata.delimiter).toBe(',')
expect(result.metadata.hasHeaders).toBe(true)
})
it('should infer correct types', async () => {
const filePath = path.join(fixturesPath, 'simple.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.csv' })
expect(typeof result.data[0].name).toBe('string')
expect(typeof result.data[0].age).toBe('number')
expect(typeof result.data[0].active).toBe('boolean')
})
})
describe('process - delimiter detection', () => {
it('should detect semicolon delimiter', async () => {
const filePath = path.join(fixturesPath, 'semicolon.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'semicolon.csv' })
expect(result.metadata.delimiter).toBe(';')
expect(result.data).toHaveLength(4)
expect(result.data[0]).toEqual({
product: 'Laptop',
price: 999.99,
quantity: 10,
inStock: true
})
})
it('should detect tab delimiter', async () => {
const filePath = path.join(fixturesPath, 'tab-delimited.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'tab-delimited.csv' })
expect(result.metadata.delimiter).toBe('\t')
expect(result.data).toHaveLength(4)
expect(result.data[0].city).toBe('Tokyo')
expect(result.data[0].population).toBe(13960000)
})
it('should use specified delimiter when provided', async () => {
const content = 'name|age|email\nJohn|30|john@example.com'
const result = await handler.process(content, {
csvDelimiter: '|',
filename: 'pipe.csv'
})
expect(result.data).toHaveLength(1)
expect(result.data[0].name).toBe('John')
expect(result.data[0].age).toBe(30)
})
})
describe('process - type inference', () => {
it('should infer integer, float, date, and boolean types', async () => {
const filePath = path.join(fixturesPath, 'types.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'types.csv' })
expect(result.metadata.types).toBeDefined()
expect(result.metadata.types.id).toBe('integer')
expect(result.metadata.types.name).toBe('string')
// Score is 95 (integer), not 95.0, so it's detected as integer
expect(['integer', 'float']).toContain(result.metadata.types.score)
expect(result.metadata.types.active).toBe('boolean')
// Check actual values
expect(result.data[0].id).toBe(1)
expect([95, 95.0]).toContain(result.data[0].score) // Can be integer or float
expect(result.data[0].active).toBe(true)
expect(result.data[1].score).toBe(87.5) // This one is definitely a float
})
it('should handle null values', async () => {
const content = 'name,age,email\nJohn,30,john@example.com\nJane,,jane@example.com\nBob,45,'
const result = await handler.process(content, { filename: 'nulls.csv' })
expect(result.data[1].age).toBeNull()
expect(result.data[2].email).toBeNull()
})
})
describe('process - encoding detection', () => {
it('should detect UTF-8 encoding', async () => {
const content = 'name,description\nProduct 1,Description with émojis 🎉'
const result = await handler.process(content, { filename: 'utf8.csv' })
// Encoding can be 'UTF-8', 'utf-8', or 'utf8' depending on detection
expect(result.metadata.encoding.toLowerCase()).toContain('utf')
expect(result.data[0].description).toContain('émojis')
expect(result.data[0].description).toContain('🎉')
})
it('should use specified encoding when provided', async () => {
const content = 'name,age\nJohn,30'
const result = await handler.process(content, {
encoding: 'ascii',
filename: 'ascii.csv'
})
expect(result.metadata.encoding).toBe('ascii')
})
})
describe('process - options', () => {
it('should respect csvHeaders=false option', async () => {
const content = 'John,30,john@example.com\nJane,25,jane@example.com'
const result = await handler.process(content, {
csvHeaders: false,
filename: 'no-headers.csv'
})
expect(result.metadata.hasHeaders).toBe(false)
// csv-parse will generate column names like '0', '1', '2'
expect(Object.keys(result.data[0])).toHaveLength(3)
})
it('should respect maxRows option', async () => {
const filePath = path.join(fixturesPath, 'simple.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
maxRows: 2,
filename: 'simple.csv'
})
expect(result.data).toHaveLength(2)
expect(result.metadata.rowCount).toBe(2)
})
})
describe('process - edge cases', () => {
it('should handle empty CSV', async () => {
const content = ''
const result = await handler.process(content, { filename: 'empty.csv' })
expect(result.data).toHaveLength(0)
expect(result.metadata.rowCount).toBe(0)
expect(result.metadata.fields).toEqual([])
})
it('should handle CSV with only headers', async () => {
const content = 'name,age,email'
const result = await handler.process(content, { filename: 'headers-only.csv' })
expect(result.data).toHaveLength(0)
expect(result.metadata.rowCount).toBe(0)
})
it('should handle CSV with single row', async () => {
const content = 'name,age,email\nJohn,30,john@example.com'
const result = await handler.process(content, { filename: 'single-row.csv' })
expect(result.data).toHaveLength(1)
expect(result.data[0].name).toBe('John')
})
it('should handle CSV with varying column counts', async () => {
const content = 'name,age,email\nJohn,30,john@example.com\nJane,25\nBob,45,bob@example.com,extra'
const result = await handler.process(content, { filename: 'varying.csv' })
// csv-parse with relax_column_count should handle this
expect(result.data).toHaveLength(3)
})
it('should trim whitespace', async () => {
const content = 'name, age , email \n John , 30 , john@example.com '
const result = await handler.process(content, { filename: 'whitespace.csv' })
expect(result.data[0].name).toBe('John')
expect(result.data[0].age).toBe(30)
expect(result.data[0].email).toBe('john@example.com')
})
})
describe('process - performance', () => {
it('should measure processing time', async () => {
const filePath = path.join(fixturesPath, 'simple.csv')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.csv' })
expect(result.metadata.processingTime).toBeGreaterThanOrEqual(0)
expect(typeof result.metadata.processingTime).toBe('number')
})
})
describe('process - error handling', () => {
it('should throw error for malformed CSV', async () => {
const content = 'name,age,email\nJohn,30,"unclosed quote'
await expect(
handler.process(content, { filename: 'malformed.csv' })
).rejects.toThrow('CSV parsing failed')
})
})
})

View file

@ -0,0 +1,269 @@
/**
* Excel Handler Tests
* Comprehensive tests for Excel parsing, multi-sheet extraction, and type inference
*/
import { describe, it, expect, beforeAll } from 'vitest'
import { ExcelHandler } from '../../../src/augmentations/intelligentImport/handlers/excelHandler.js'
import { promises as fs } from 'fs'
import * as path from 'path'
describe('ExcelHandler', () => {
let handler: ExcelHandler
const fixturesPath = path.join(process.cwd(), 'tests/fixtures/import')
beforeAll(() => {
handler = new ExcelHandler()
})
describe('canHandle', () => {
it('should handle .xlsx extension', () => {
expect(handler.canHandle({ filename: 'data.xlsx' })).toBe(true)
})
it('should handle .xls extension', () => {
expect(handler.canHandle({ filename: 'data.xls' })).toBe(true)
})
it('should handle .xlsb extension', () => {
expect(handler.canHandle({ filename: 'data.xlsb' })).toBe(true)
})
it('should handle .xlsm extension', () => {
expect(handler.canHandle({ filename: 'data.xlsm' })).toBe(true)
})
it('should not handle .csv extension', () => {
expect(handler.canHandle({ filename: 'data.csv' })).toBe(false)
})
it('should not handle .pdf extension', () => {
expect(handler.canHandle({ filename: 'data.pdf' })).toBe(false)
})
})
describe('process - simple Excel', () => {
it('should parse simple single-sheet Excel file', async () => {
const filePath = path.join(fixturesPath, 'simple.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.xlsx' })
expect(result.format).toBe('excel')
expect(result.data).toHaveLength(4)
expect(result.data[0]).toMatchObject({
Name: 'Alice Johnson',
Age: 28,
Department: 'Engineering',
Salary: 95000,
Active: true
})
expect(result.metadata.rowCount).toBe(4)
expect(result.metadata.sheetCount).toBe(1)
expect(result.metadata.sheets).toEqual(['Employees'])
})
it('should infer correct types from Excel data', async () => {
const filePath = path.join(fixturesPath, 'simple.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.xlsx' })
expect(result.metadata.types).toBeDefined()
expect(result.metadata.types.Name).toBe('string')
expect(result.metadata.types.Age).toBe('integer')
expect(['integer', 'float']).toContain(result.metadata.types.Salary)
expect(result.metadata.types.Active).toBe('boolean')
})
})
describe('process - multi-sheet Excel', () => {
it('should extract data from all sheets by default', async () => {
const filePath = path.join(fixturesPath, 'multi-sheet.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'multi-sheet.xlsx' })
expect(result.metadata.sheetCount).toBe(3)
expect(result.metadata.sheets).toEqual(['Products', 'Orders', 'Customers'])
// Total rows from all sheets
const totalRows = 4 + 3 + 3 // Products + Orders + Customers
expect(result.data).toHaveLength(totalRows)
// Check _sheet field is added
expect(result.data[0]._sheet).toBe('Products')
})
it('should filter data by sheet name', async () => {
const filePath = path.join(fixturesPath, 'multi-sheet.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
filename: 'multi-sheet.xlsx',
excelSheets: ['Products']
})
expect(result.metadata.sheetCount).toBe(1)
expect(result.metadata.sheets).toEqual(['Products'])
expect(result.data).toHaveLength(4)
expect(result.data.every(row => row._sheet === 'Products')).toBe(true)
})
it('should extract data from multiple specified sheets', async () => {
const filePath = path.join(fixturesPath, 'multi-sheet.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
filename: 'multi-sheet.xlsx',
excelSheets: ['Products', 'Customers']
})
expect(result.metadata.sheetCount).toBe(2)
expect(result.metadata.sheets).toEqual(['Products', 'Customers'])
expect(result.data).toHaveLength(7) // 4 + 3
const sheets = new Set(result.data.map(row => row._sheet))
expect(sheets.has('Products')).toBe(true)
expect(sheets.has('Customers')).toBe(true)
expect(sheets.has('Orders')).toBe(false)
})
it('should include sheet metadata for each sheet', async () => {
const filePath = path.join(fixturesPath, 'multi-sheet.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'multi-sheet.xlsx' })
expect(result.metadata.sheetMetadata).toBeDefined()
expect(result.metadata.sheetMetadata.Products).toMatchObject({
rowCount: 4,
columnCount: 5
})
expect(result.metadata.sheetMetadata.Products.headers).toEqual([
'ID', 'Product', 'Price', 'Stock', 'Category'
])
})
})
describe('process - type inference', () => {
it('should infer integer, float, date, and boolean types', async () => {
const filePath = path.join(fixturesPath, 'types.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'types.xlsx' })
expect(result.metadata.types).toBeDefined()
expect(result.metadata.types.ID).toBe('integer')
expect(result.metadata.types.Name).toBe('string')
expect(['integer', 'float']).toContain(result.metadata.types.Score)
expect(['integer', 'float']).toContain(result.metadata.types.Percentage)
expect(result.metadata.types.Active).toBe('boolean')
// Check actual values
expect(result.data[0].ID).toBe(1)
expect(result.data[0].Active).toBe(true)
expect(typeof result.data[0].Score).toBe('number')
})
it('should handle null/empty values', async () => {
const filePath = path.join(fixturesPath, 'empty.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'empty.xlsx' })
expect(result.data).toHaveLength(0)
expect(result.metadata.rowCount).toBe(0)
})
})
describe('process - workbook metadata', () => {
it('should extract workbook information', async () => {
const filePath = path.join(fixturesPath, 'multi-sheet.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'multi-sheet.xlsx' })
expect(result.metadata.workbookInfo).toBeDefined()
expect(result.metadata.workbookInfo.sheetNames).toEqual([
'Products', 'Orders', 'Customers'
])
})
})
describe('process - edge cases', () => {
it('should handle Excel file with only headers', async () => {
const filePath = path.join(fixturesPath, 'empty.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'empty.xlsx' })
expect(result.data).toHaveLength(0)
expect(result.metadata.sheetCount).toBe(1)
})
it('should skip non-existent sheets when filtering', async () => {
const filePath = path.join(fixturesPath, 'simple.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
filename: 'simple.xlsx',
excelSheets: ['Employees', 'NonExistent']
})
expect(result.metadata.sheets).toEqual(['Employees'])
expect(result.data).toHaveLength(4)
})
})
describe('process - performance', () => {
it('should measure processing time', async () => {
const filePath = path.join(fixturesPath, 'simple.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.xlsx' })
expect(result.metadata.processingTime).toBeGreaterThanOrEqual(0)
expect(typeof result.metadata.processingTime).toBe('number')
})
})
describe('process - field sanitization', () => {
it('should sanitize column names', async () => {
const filePath = path.join(fixturesPath, 'simple.xlsx')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.xlsx' })
// All field names should be valid identifiers
const fields = Object.keys(result.data[0]).filter(k => k !== '_sheet')
for (const field of fields) {
expect(field).toMatch(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
}
})
})
describe('process - error handling', () => {
it('should handle corrupted Excel file gracefully', async () => {
// XLSX library is very forgiving, so we test that it returns empty data
// rather than crashing
const invalidData = Buffer.from('This is not an Excel file')
const result = await handler.process(invalidData, { filename: 'invalid.xlsx' })
// Should return empty data instead of crashing
expect(result.data).toHaveLength(0)
expect(result.metadata.rowCount).toBe(0)
})
it('should handle binary garbage', async () => {
const garbage = Buffer.from(new Uint8Array(256).fill(255))
const result = await handler.process(garbage, { filename: 'garbage.xlsx' })
// Should not crash
expect(result).toBeDefined()
expect(result.format).toBe('excel')
})
})
})

View file

@ -0,0 +1,292 @@
/**
* PDF Handler Tests
* Comprehensive tests for PDF text extraction, table detection, and metadata
*/
import { describe, it, expect, beforeAll } from 'vitest'
import { PDFHandler } from '../../../src/augmentations/intelligentImport/handlers/pdfHandler.js'
import { promises as fs } from 'fs'
import * as path from 'path'
describe('PDFHandler', () => {
let handler: PDFHandler
const fixturesPath = path.join(process.cwd(), 'tests/fixtures/import')
beforeAll(() => {
handler = new PDFHandler()
})
describe('canHandle', () => {
it('should handle .pdf extension', () => {
expect(handler.canHandle({ filename: 'document.pdf' })).toBe(true)
})
it('should handle PDF magic bytes', () => {
const pdfBuffer = Buffer.from('%PDF-1.4\n')
expect(handler.canHandle(pdfBuffer)).toBe(true)
})
it('should not handle .xlsx extension', () => {
expect(handler.canHandle({ filename: 'data.xlsx' })).toBe(false)
})
it('should not handle .csv extension', () => {
expect(handler.canHandle({ filename: 'data.csv' })).toBe(false)
})
it('should not handle non-PDF buffer', () => {
const buffer = Buffer.from('This is not a PDF')
expect(handler.canHandle(buffer)).toBe(false)
})
})
describe('process - simple text PDF', () => {
it('should extract text from simple PDF', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.format).toBe('pdf')
expect(result.data.length).toBeGreaterThan(0)
// Should have extracted paragraphs
const paragraphs = result.data.filter(item => item._type === 'paragraph')
expect(paragraphs.length).toBeGreaterThan(0)
// Check that text was extracted
const hasText = paragraphs.some(p => p.text && p.text.length > 0)
expect(hasText).toBe(true)
})
it('should include page numbers', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
// All items should have _page field
expect(result.data.every(item => typeof item._page === 'number')).toBe(true)
expect(result.data.every(item => item._page >= 1)).toBe(true)
})
it('should count pages correctly', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.metadata.pageCount).toBe(1)
})
})
describe('process - multi-page PDF', () => {
it('should extract text from all pages', async () => {
const filePath = path.join(fixturesPath, 'multi-page.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'multi-page.pdf' })
expect(result.metadata.pageCount).toBe(3)
// Should have content from multiple pages
const pages = new Set(result.data.map(item => item._page))
expect(pages.size).toBeGreaterThan(1)
expect(pages.has(1)).toBe(true)
expect(pages.has(2)).toBe(true)
expect(pages.has(3)).toBe(true)
})
it('should preserve page order', async () => {
const filePath = path.join(fixturesPath, 'multi-page.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'multi-page.pdf' })
// Page numbers should be in order
let lastPage = 0
let pageChanged = false
for (const item of result.data) {
if (item._page !== lastPage && lastPage > 0) {
pageChanged = true
}
expect(item._page).toBeGreaterThanOrEqual(lastPage)
lastPage = item._page
}
expect(pageChanged).toBe(true) // Should have moved through pages
})
})
describe('process - table detection', () => {
it('should detect tables in PDF', async () => {
const filePath = path.join(fixturesPath, 'table.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'table.pdf' })
// Should have detected at least one table
expect(result.metadata.tableCount).toBeGreaterThan(0)
})
it('should extract table rows when tables detected', async () => {
const filePath = path.join(fixturesPath, 'table.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
filename: 'table.pdf',
pdfExtractTables: true
})
// Should have table_row items
const tableRows = result.data.filter(item => item._type === 'table_row')
expect(tableRows.length).toBeGreaterThan(0)
// Table rows should have structured data
if (tableRows.length > 0) {
const firstRow = tableRows[0]
const fields = Object.keys(firstRow).filter(k => !k.startsWith('_'))
expect(fields.length).toBeGreaterThan(0)
}
})
it('should skip table extraction when disabled', async () => {
const filePath = path.join(fixturesPath, 'table.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, {
filename: 'table.pdf',
pdfExtractTables: false
})
// Should not have detected tables
expect(result.metadata.tableCount).toBe(0)
// Should not have table_row items
const tableRows = result.data.filter(item => item._type === 'table_row')
expect(tableRows.length).toBe(0)
})
})
describe('process - metadata extraction', () => {
it('should extract PDF metadata', async () => {
const filePath = path.join(fixturesPath, 'metadata.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'metadata.pdf' })
expect(result.metadata.pdfMetadata).toBeDefined()
expect(result.metadata.pdfMetadata.title).toBe('Test Document')
expect(result.metadata.pdfMetadata.author).toBe('Test Author')
expect(result.metadata.pdfMetadata.creator).toBe('Brainy Test Suite')
})
it('should handle PDFs without metadata gracefully', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.metadata.pdfMetadata).toBeDefined()
// Some fields may be null
expect(result.metadata.pdfMetadata).toHaveProperty('title')
expect(result.metadata.pdfMetadata).toHaveProperty('author')
})
})
describe('process - text statistics', () => {
it('should track total text length', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.metadata.textLength).toBeGreaterThan(0)
expect(typeof result.metadata.textLength).toBe('number')
})
it('should count extracted items', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.metadata.rowCount).toBe(result.data.length)
expect(result.metadata.rowCount).toBeGreaterThan(0)
})
})
describe('process - edge cases', () => {
it('should handle empty PDF', async () => {
const filePath = path.join(fixturesPath, 'empty.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'empty.pdf' })
expect(result.format).toBe('pdf')
expect(result.metadata.pageCount).toBe(1)
// Empty PDF may have 0 or minimal content
expect(result.data).toBeDefined()
})
it('should measure processing time', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
expect(result.metadata.processingTime).toBeGreaterThan(0)
expect(typeof result.metadata.processingTime).toBe('number')
})
})
describe('process - data structure', () => {
it('should include type indicators', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
// All items should have _type field
expect(result.data.every(item => '_type' in item)).toBe(true)
// Types should be valid
const validTypes = ['paragraph', 'table_row']
expect(result.data.every(item => validTypes.includes(item._type))).toBe(true)
})
it('should include index for paragraphs', async () => {
const filePath = path.join(fixturesPath, 'simple.pdf')
const buffer = await fs.readFile(filePath)
const result = await handler.process(buffer, { filename: 'simple.pdf' })
const paragraphs = result.data.filter(item => item._type === 'paragraph')
// Paragraphs should have _index field
expect(paragraphs.every(p => typeof p._index === 'number')).toBe(true)
})
})
describe('process - error handling', () => {
it('should throw error for invalid PDF', async () => {
const invalidData = Buffer.from('This is not a PDF file')
await expect(
handler.process(invalidData, { filename: 'invalid.pdf' })
).rejects.toThrow('PDF parsing failed')
})
it('should throw error for corrupted PDF', async () => {
const corruptedPDF = Buffer.concat([
Buffer.from('%PDF-1.4\n'),
Buffer.from('corrupted data that is not valid PDF structure')
])
await expect(
handler.process(corruptedPDF, { filename: 'corrupted.pdf' })
).rejects.toThrow('PDF parsing failed')
})
})
})