import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import fs from 'node:fs/promises'; import path from 'node:path'; import os from 'node:os'; import ExcelJS from 'exceljs'; import { PDFDocument, StandardFonts } from 'pdf-lib'; import { registerDocumentConverters } from '../../src/converters/document.js'; import { resolve } from '../../src/converters/registry.js'; import { detectInputMime } from '../../src/mime.js'; let tmpDir; let csvFixturePath; let xlsxFixturePath; beforeAll(async () => { registerDocumentConverters(); tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'converter-spreadsheet-')); csvFixturePath = path.join(tmpDir, 'fixture.csv'); await fs.writeFile(csvFixturePath, 'Name,Age\nAlice,30\nBob,25\n'); const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('Sheet1'); worksheet.addRows([ ['Name', 'Age'], ['Alice', 30], ['Bob', 25], ]); xlsxFixturePath = path.join(tmpDir, 'fixture.xlsx'); await workbook.xlsx.writeFile(xlsxFixturePath); }); afterAll(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); describe('spreadsheet converters', () => { it('converts CSV to XLSX', async () => { const outputPath = path.join(tmpDir, 'from-csv.xlsx'); const entry = resolve('csv', 'xlsx'); await entry.convert(csvFixturePath, outputPath); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(outputPath); const [worksheet] = workbook.worksheets; expect(worksheet.getRow(2).values.slice(1)).toEqual(['Alice', 30]); }); it('converts XLSX to CSV', async () => { const outputPath = path.join(tmpDir, 'from-xlsx.csv'); const entry = resolve('xlsx', 'csv'); await entry.convert(xlsxFixturePath, outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Name,Age'); expect(csv).toContain('Alice,30'); }); it('converts CSV to HTML as a table', async () => { const outputPath = path.join(tmpDir, 'from-csv.html'); const entry = resolve('csv', 'html'); await entry.convert(csvFixturePath, outputPath); const html = await fs.readFile(outputPath, 'utf8'); expect(html).toContain(''); expect(html).toContain(''); }); it('converts HTML table to CSV', async () => { const inputPath = path.join(tmpDir, 'fixture-table.html'); await fs.writeFile( inputPath, '
Alice
NameAge
Alice30
' ); const outputPath = path.join(tmpDir, 'from-html.csv'); const entry = resolve('html', 'csv'); await entry.convert(inputPath, outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Name,Age'); expect(csv).toContain('Alice,30'); }); it('converts CSV to a Markdown table', async () => { const outputPath = path.join(tmpDir, 'from-csv.md'); const entry = resolve('csv', 'md'); await entry.convert(csvFixturePath, outputPath); const markdown = await fs.readFile(outputPath, 'utf8'); expect(markdown).toContain('| Name | Age |'); expect(markdown).toContain('| Alice | 30 |'); }); it('converts a Markdown table to CSV', async () => { const inputPath = path.join(tmpDir, 'fixture-table.md'); await fs.writeFile(inputPath, '| Name | Age |\n| --- | --- |\n| Alice | 30 |\n'); const outputPath = path.join(tmpDir, 'from-md.csv'); const entry = resolve('md', 'csv'); await entry.convert(inputPath, outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Name,Age'); expect(csv).toContain('Alice,30'); }); it('converts CSV to tab-delimited TXT', async () => { const outputPath = path.join(tmpDir, 'from-csv.txt'); const entry = resolve('csv', 'txt'); await entry.convert(csvFixturePath, outputPath); const text = await fs.readFile(outputPath, 'utf8'); expect(text).toContain('Name\tAge'); expect(text).toContain('Alice\t30'); }); it('converts tab-delimited TXT to CSV', async () => { const inputPath = path.join(tmpDir, 'fixture.txt'); await fs.writeFile(inputPath, 'Name\tAge\nAlice\t30\n'); const outputPath = path.join(tmpDir, 'from-txt.csv'); const entry = resolve('txt', 'csv'); await entry.convert(inputPath, outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Name,Age'); expect(csv).toContain('Alice,30'); }); it('converts CSV to DOCX containing a table', async () => { const outputPath = path.join(tmpDir, 'from-csv.docx'); const entry = resolve('csv', 'docx'); await entry.convert(csvFixturePath, outputPath); const mammoth = await import('mammoth'); const result = await mammoth.default.convertToHtml({ path: outputPath }); expect(result.value).toContain('Alice'); expect(result.value).toContain(''); }); it('converts DOCX table to CSV', async () => { const outputPath = path.join(tmpDir, 'from-docx.csv'); const entry = resolve('docx', 'csv'); await entry.convert(path.join(tmpDir, 'from-csv.docx'), outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Alice'); }); it('converts CSV to a valid PDF', async () => { const outputPath = path.join(tmpDir, 'from-csv.pdf'); const entry = resolve('csv', 'pdf'); await entry.convert(csvFixturePath, outputPath); const detected = await detectInputMime(outputPath); expect(detected.mime).toBe('application/pdf'); }, 20000); it('converts XLSX to HTML as a table', async () => { const outputPath = path.join(tmpDir, 'from-xlsx.html'); const entry = resolve('xlsx', 'html'); await entry.convert(xlsxFixturePath, outputPath); const html = await fs.readFile(outputPath, 'utf8'); expect(html).toContain(''); }); it('converts HTML table to XLSX', async () => { const inputPath = path.join(tmpDir, 'fixture-table-2.html'); await fs.writeFile( inputPath, '
Bob
NameAge
Carol40
' ); const outputPath = path.join(tmpDir, 'from-html.xlsx'); const entry = resolve('html', 'xlsx'); await entry.convert(inputPath, outputPath); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.readFile(outputPath); const [worksheet] = workbook.worksheets; expect(worksheet.getRow(2).values.slice(1)).toEqual(['Carol', '40']); }); it('extracts PDF text into a CSV row (best-effort)', async () => { const pdfDoc = await PDFDocument.create(); const page = pdfDoc.addPage([600, 400]); const font = await pdfDoc.embedFont(StandardFonts.Helvetica); page.drawText('Extractable fixture text', { x: 50, y: 350, size: 24, font }); const bytes = await pdfDoc.save(); const inputPath = path.join(tmpDir, 'fixture.pdf'); await fs.writeFile(inputPath, bytes); const outputPath = path.join(tmpDir, 'from-pdf.csv'); const entry = resolve('pdf', 'csv'); await entry.convert(inputPath, outputPath); const csv = await fs.readFile(outputPath, 'utf8'); expect(csv).toContain('Extractable fixture text'); }); });