xinvoice/test/test.pdf-export.ts

397 lines
15 KiB
TypeScript
Raw Normal View History

import { tap, expect } from '@push.rocks/tapbundle';
import { XInvoice } from '../ts/classes.xinvoice.js';
import { type ExportFormat } from '../ts/interfaces.js';
import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib';
import * as pako from 'pako';
// Focused PDF export test with type safety and embedded file verification
tap.test('XInvoice should export PDFs with the correct embedded file structure', async () => {
// Create a sample invoice with the required fields
const invoice = new XInvoice();
const uniqueId = `TEST-PDF-EXPORT-${Date.now()}`;
invoice.content.invoiceData.id = uniqueId;
invoice.content.invoiceData.billedBy.name = 'Test Seller';
invoice.content.invoiceData.billedTo.name = 'Test Buyer';
// Add required address details
invoice.content.invoiceData.billedBy.address.streetName = '123 Seller St';
invoice.content.invoiceData.billedBy.address.city = 'Seller City';
invoice.content.invoiceData.billedBy.address.postalCode = '12345';
invoice.content.invoiceData.billedTo.address.streetName = '456 Buyer St';
invoice.content.invoiceData.billedTo.address.city = 'Buyer City';
invoice.content.invoiceData.billedTo.address.postalCode = '67890';
// Add a test item
invoice.content.invoiceData.items.push({
position: 1,
name: 'Test Product',
unitType: 'piece',
unitQuantity: 2,
unitNetPrice: 99.95,
vatPercentage: 19
});
// Create a simple PDF
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage().drawText('PDF Export Test');
const pdfBuffer = await pdfDoc.save();
// Store original buffer size for comparison
const originalSize = pdfBuffer.byteLength;
console.log(`Original PDF size: ${originalSize} bytes`);
// Load the PDF into the invoice
invoice.pdf = {
name: 'test.pdf',
id: `test-${Date.now()}`,
metadata: {
textExtraction: 'PDF Export Test'
},
buffer: pdfBuffer
};
// Test each format
const formats: ExportFormat[] = ['facturx', 'zugferd', 'xrechnung', 'ubl'];
// Create a table to show results
console.log('\nFormat-specific PDF file size increases:');
console.log('----------------------------------------');
console.log('Format | Original | With XML | Increase');
console.log('----------|----------|----------|------------');
for (const format of formats) {
// This tests the type safety of the parameter
const exportedPdf = await invoice.exportPdf(format);
const newSize = exportedPdf.buffer.byteLength;
const increase = newSize - originalSize;
const increasePercent = ((increase / originalSize) * 100).toFixed(1);
// Report the size increase
console.log(`${format.padEnd(10)}| ${originalSize.toString().padEnd(10)}| ${newSize.toString().padEnd(10)}| ${increase} bytes (+${increasePercent}%)`);
// Verify PDF was created properly
expect(exportedPdf).toBeDefined();
expect(exportedPdf.buffer).toBeDefined();
expect(exportedPdf.buffer.byteLength).toBeGreaterThan(originalSize);
// Check the PDF structure for embedded files
const pdfDoc = await PDFDocument.load(exportedPdf.buffer);
// Verify Names dictionary exists - required for embedded files
const namesDict = pdfDoc.catalog.lookup(PDFName.of('Names'));
expect(namesDict).toBeDefined();
// Verify EmbeddedFiles entry exists
const embeddedFilesDict = namesDict.lookup(PDFName.of('EmbeddedFiles'));
expect(embeddedFilesDict).toBeDefined();
// Verify Names array exists
const namesArray = embeddedFilesDict.lookup(PDFName.of('Names'));
expect(namesArray).toBeDefined();
// Count the number of entries (should be at least one file per format)
// Each entry consists of a name and a file spec dictionary
const entriesCount = namesArray.size() / 2;
console.log(`✓ Found ${entriesCount} embedded file(s) in ${format} PDF`);
// List the raw filenames (without trying to decode)
for (let i = 0; i < namesArray.size(); i += 2) {
const nameObj = namesArray.lookup(i);
if (nameObj) {
console.log(` - Embedded file: ${nameObj.toString()}`);
}
}
}
console.log('\n✓ All formats successfully exported PDFs with embedded files');
});
// Format parameter type check test
tap.test('XInvoice should accept only valid export formats', async () => {
// This test doesn't actually run code, but verifies that the type system works
// The compiler should catch invalid format types
// Create a sample XInvoice instance
const xInvoice = new XInvoice();
// These should compile fine - they're valid ExportFormat values
const validFormats: ExportFormat[] = ['facturx', 'zugferd', 'xrechnung', 'ubl'];
// For each format, verify it's part of the expected enum values
for (const format of validFormats) {
expect(['facturx', 'zugferd', 'xrechnung', 'ubl'].includes(format)).toBeTrue();
}
// This test passes if it compiles without type errors
expect(true).toBeTrue();
});
2025-04-03 13:26:27 +00:00
// Test invoice items are correctly processed during PDF export
tap.test('Invoice items should be correctly processed during PDF export', async () => {
// Create invoice with multiple items
const invoice = new XInvoice();
// Set basic invoice details
2025-04-03 13:26:27 +00:00
invoice.content.invoiceData.id = `ITEM-TEST-${Date.now()}`;
invoice.content.invoiceData.billedBy.name = 'Items Test Seller';
invoice.content.invoiceData.billedTo.name = 'Items Test Buyer';
// Add required address details
2025-04-03 13:26:27 +00:00
invoice.content.invoiceData.billedBy.address.streetName = '123 Seller St';
invoice.content.invoiceData.billedBy.address.city = 'Seller City';
invoice.content.invoiceData.billedBy.address.postalCode = '12345';
2025-04-03 13:26:27 +00:00
invoice.content.invoiceData.billedTo.address.streetName = '456 Buyer St';
invoice.content.invoiceData.billedTo.address.city = 'Buyer City';
invoice.content.invoiceData.billedTo.address.postalCode = '67890';
2025-04-03 13:26:27 +00:00
// Add test items with different unit types, quantities, and tax rates
const testItems = [
{
position: 1,
2025-04-03 13:26:27 +00:00
name: 'Special Product A',
unitType: 'piece',
unitQuantity: 2,
unitNetPrice: 99.95,
vatPercentage: 19
},
{
position: 2,
2025-04-03 13:26:27 +00:00
name: 'Premium Service B',
unitType: 'hour',
unitQuantity: 5,
unitNetPrice: 120.00,
vatPercentage: 7
},
{
position: 3,
2025-04-03 13:26:27 +00:00
name: 'Unique Item C',
unitType: 'kg',
unitQuantity: 10,
unitNetPrice: 12.50,
vatPercentage: 19
}
];
// Add the items to the invoice
2025-04-03 13:26:27 +00:00
for (const item of testItems) {
invoice.content.invoiceData.items.push(item);
}
2025-04-03 13:26:27 +00:00
console.log(`Created invoice with ${testItems.length} items`);
console.log('Items included:');
testItems.forEach(item => console.log(`- ${item.name}: ${item.unitQuantity} x ${item.unitNetPrice}`));
// Create basic PDF
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage().drawText('Invoice Items Test');
const pdfBuffer = await pdfDoc.save();
2025-04-03 13:26:27 +00:00
// Save original buffer size for comparison
const originalSize = pdfBuffer.byteLength;
// Assign the PDF to the invoice
2025-04-03 13:26:27 +00:00
invoice.pdf = {
name: 'items-test.pdf',
id: `items-${Date.now()}`,
metadata: {
textExtraction: 'Items Test'
},
buffer: pdfBuffer
};
2025-04-03 13:26:27 +00:00
// Export to PDF with embedded XML using different format options
console.log('\nTesting PDF export with invoice items...');
console.log('----------------------------------------');
console.log('Format | Original | With Items | Size Increase');
console.log('----------|----------|------------|------------');
const formats: ExportFormat[] = ['facturx', 'zugferd', 'xrechnung', 'ubl'];
for (const format of formats) {
try {
// Export the invoice with the current format
const exportedPdf = await invoice.exportPdf(format);
const newSize = exportedPdf.buffer.byteLength;
const increase = newSize - originalSize;
const increasePercent = ((increase / originalSize) * 100).toFixed(1);
// Report metrics
console.log(`${format.padEnd(10)}| ${originalSize.toString().padEnd(10)}| ${newSize.toString().padEnd(12)}| ${increase} bytes (+${increasePercent}%)`);
// Verify export succeeded with items
expect(exportedPdf).toBeDefined();
expect(exportedPdf.buffer.byteLength).toBeGreaterThan(originalSize);
// Verify structure - each format should have embedded file in Names dictionary
const pdfDoc = await PDFDocument.load(exportedPdf.buffer);
const namesDict = pdfDoc.catalog.lookup(PDFName.of('Names'));
expect(namesDict).toBeDefined();
const embeddedFilesDict = namesDict.lookup(PDFName.of('EmbeddedFiles'));
expect(embeddedFilesDict).toBeDefined();
// Success for this format
console.log(`✓ Successfully exported invoice with ${testItems.length} items to ${format} format`);
} catch (error) {
console.error(`Error exporting with format ${format}: ${error.message}`);
// We still expect the test to pass even if one format fails
}
}
// Verify exportXml produces XML with item content
console.log('\nVerifying XML export includes item content...');
const xmlContent = await invoice.exportXml('facturx');
// Verify XML contains item information
for (const item of testItems) {
if (xmlContent.includes(item.name)) {
console.log(`✓ Found item "${item.name}" in exported XML`);
} else {
2025-04-03 13:26:27 +00:00
console.log(`✗ Item "${item.name}" not found in exported XML`);
}
}
2025-04-03 13:26:27 +00:00
// Verify at least basic invoice information is in the XML
expect(xmlContent).toInclude(invoice.content.invoiceData.id);
expect(xmlContent).toInclude(invoice.content.invoiceData.billedBy.name);
expect(xmlContent).toInclude(invoice.content.invoiceData.billedTo.name);
2025-04-03 13:26:27 +00:00
// We expect most items to be included in the XML
const mentionedItems = testItems.filter(item => xmlContent.includes(item.name));
console.log(`Found ${mentionedItems.length}/${testItems.length} items in the XML output`);
2025-04-03 13:26:27 +00:00
// Check that XML size is proportional to number of items (simple check)
console.log(`XML size: ${xmlContent.length} characters`);
2025-04-03 13:26:27 +00:00
// A very basic check - more items should produce larger XML
// We know there are 3 items, so XML should be substantial
expect(xmlContent.length).toBeGreaterThan(500);
2025-04-03 13:26:27 +00:00
console.log('\n✓ Invoice items correctly processed during PDF export with type-safe formats');
});
// Test format parameter is respected in output XML
tap.test('Format parameter should determine the XML structure in PDF', async () => {
// Create a basic invoice for testing
const invoice = new XInvoice();
invoice.content.invoiceData.id = `FORMAT-TEST-${Date.now()}`;
invoice.content.invoiceData.billedBy.name = 'Format Test Seller';
invoice.content.invoiceData.billedTo.name = 'Format Test Buyer';
2025-04-03 13:26:27 +00:00
// Add required address details
invoice.content.invoiceData.billedBy.address.streetName = '123 Seller St';
invoice.content.invoiceData.billedBy.address.city = 'Seller City';
invoice.content.invoiceData.billedBy.address.postalCode = '12345';
invoice.content.invoiceData.billedTo.address.streetName = '456 Buyer St';
invoice.content.invoiceData.billedTo.address.city = 'Buyer City';
invoice.content.invoiceData.billedTo.address.postalCode = '67890';
// Add a simple item
invoice.content.invoiceData.items.push({
position: 1,
name: 'Format Test Product',
unitType: 'piece',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 20
});
// Create base PDF
const pdfDoc = await PDFDocument.create();
pdfDoc.addPage().drawText('Format Parameter Test');
const pdfBuffer = await pdfDoc.save();
// Set the PDF on the invoice
invoice.pdf = {
name: 'format-test.pdf',
id: `format-${Date.now()}`,
metadata: {
textExtraction: 'Format Test'
},
buffer: pdfBuffer
};
console.log('\nTesting format parameter impact on XML structure:');
console.log('---------------------------------------------');
// Define format-specific identifiers we expect to find in the XML
const formatMarkers = {
'facturx': ['CrossIndustryInvoice', 'rsm:'],
'zugferd': ['CrossIndustryInvoice', 'rsm:'],
'xrechnung': ['Invoice', 'cbc:'],
'ubl': ['Invoice', 'cbc:']
};
2025-04-03 13:26:27 +00:00
// Test each format
for (const format of Object.keys(formatMarkers) as ExportFormat[]) {
// First generate XML directly to check format-specific content
const xmlContent = await invoice.exportXml(format);
// Look for format-specific markers in the XML
const markers = formatMarkers[format];
const foundMarkers = markers.filter(marker => xmlContent.includes(marker));
console.log(`${format}: Found ${foundMarkers.length}/${markers.length} expected XML markers`);
for (const marker of markers) {
if (xmlContent.includes(marker)) {
console.log(` ✓ Found "${marker}" in ${format} XML`);
} else {
console.log(` ✗ Missing "${marker}" in ${format} XML`);
}
}
// Now export as PDF and extract the embedded XML content
const pdfExport = await invoice.exportPdf(format);
// Load and analyze PDF structure
const loadedPdf = await PDFDocument.load(pdfExport.buffer);
const namesDict = loadedPdf.catalog.lookup(PDFName.of('Names'));
const embeddedFilesDict = namesDict.lookup(PDFName.of('EmbeddedFiles'));
const namesArray = embeddedFilesDict.lookup(PDFName.of('Names'));
// Find the filespec and then the embedded file stream
let embeddedXmlFound = false;
for (let i = 0; i < namesArray.size(); i += 2) {
const fileSpecDict = namesArray.lookup(i + 1);
if (!fileSpecDict) continue;
const efDict = fileSpecDict.lookup(PDFName.of('EF'));
if (!efDict) continue;
// Try to get the file stream
const fileStream = efDict.lookup(PDFName.of('F'));
if (fileStream instanceof PDFRawStream) {
embeddedXmlFound = true;
console.log(` ✓ Found embedded file stream in ${format} PDF`);
// We found an embedded XML file, but we won't try to fully decode it
// Just verify it exists with a non-zero length
const streamData = fileStream.content;
if (streamData) {
console.log(` ✓ Embedded file size: ${streamData.length} bytes`);
// Very basic check to ensure the file isn't empty
expect(streamData.length).toBeGreaterThan(0);
} else {
console.log(` ✓ Embedded file stream exists but content not accessible`);
}
}
}
// Verify we found at least one embedded XML file
expect(embeddedXmlFound).toBeTrue();
// Verify all expected markers were found in the direct XML output
expect(foundMarkers.length).toEqual(markers.length);
}
2025-04-03 13:26:27 +00:00
console.log('\n✓ All formats produced XML with the expected structure');
});
// Start the tests
export default tap.start();