import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as einvoice from '../../../ts/index.js'; import * as plugins from '../../plugins.js'; import { CorpusLoader } from '../../helpers/corpus.loader.js'; import { PerformanceTracker } from '../../helpers/performance.tracker.js'; tap.test('PARSE-01: Basic XML structure parsing', async () => { const testCases = [ { name: 'Minimal invoice', xml: '\nTEST-001', expectedId: null // Generic invoice element not recognized }, { name: 'Invoice with namespaces', xml: ` TEST-002 `, expectedId: 'TEST-002' }, { name: 'XRechnung UBL invoice', xml: ` TEST-003 2024-01-01 Test Supplier Berlin 10115 DE Test Customer Munich 80331 DE 1 1 100.00 Test Product 100.00 119.00 `, expectedId: 'TEST-003' } ]; for (const testCase of testCases) { const { result, metric } = await PerformanceTracker.track( 'xml-parsing', async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(testCase.xml); return { success: true, id: invoice.id, hasFrom: !!invoice.from, hasTo: !!invoice.to, itemCount: invoice.items?.length || 0 }; } catch (error) { return { success: false, error: error.message }; } } ); console.log(`${testCase.name}: ${result.success ? '✓' : '✗'}`); if (testCase.expectedId !== null) { if (result.success) { expect(result.id).toEqual(testCase.expectedId); console.log(` ID: ${result.id}`); console.log(` Has supplier: ${result.hasFrom}`); console.log(` Has customer: ${result.hasTo}`); console.log(` Item count: ${result.itemCount}`); } else { console.log(` Error: ${result.error}`); } } console.log(` Parse time: ${metric.duration.toFixed(2)}ms`); } }); tap.test('PARSE-01: Character encoding handling', async () => { const encodingTests = [ { name: 'UTF-8 with special characters', xml: ` UTF8-TEST Special chars: äöü ñ € « » 中文 `, expectedNote: 'Special chars: äöü ñ € « » 中文' }, { name: 'ISO-8859-1 declaration', xml: ` ISO-TEST Latin-1 chars: àèìòù `, expectedNote: 'Latin-1 chars: àèìòù' } ]; for (const test of encodingTests) { const { result } = await PerformanceTracker.track( 'encoding-test', async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(test.xml); return { success: true, notes: invoice.notes, id: invoice.id }; } catch (error) { return { success: false, error: error.message }; } } ); console.log(`${test.name}: ${result.success ? '✓' : '✗'}`); if (result.success) { expect(result.notes).toBeDefined(); if (result.notes && result.notes.length > 0) { expect(result.notes[0]).toEqual(test.expectedNote); console.log(` Note preserved: ${result.notes[0]}`); } } } }); tap.test('PARSE-01: Namespace handling', async () => { const namespaceTests = [ { name: 'Multiple namespace declarations', xml: ` urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended NS-TEST-001 `, expectedFormat: einvoice.InvoiceFormat.FACTURX, expectedId: 'NS-TEST-001' }, { name: 'Default namespace', xml: ` DEFAULT-NS-TEST `, expectedFormat: einvoice.InvoiceFormat.UBL, expectedId: 'DEFAULT-NS-TEST' } ]; for (const test of namespaceTests) { const { result } = await PerformanceTracker.track( 'namespace-test', async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(test.xml); return { success: true, format: invoice.getFormat(), id: invoice.id }; } catch (error) { return { success: false, error: error.message }; } } ); console.log(`${test.name}: ${result.success ? '✓' : '✗'}`); if (result.success) { expect(result.format).toEqual(test.expectedFormat); expect(result.id).toEqual(test.expectedId); console.log(` Detected format: ${einvoice.InvoiceFormat[result.format]}`); console.log(` ID: ${result.id}`); } } }); tap.test('PARSE-01: Large XML file parsing', async () => { // Generate a large invoice with many line items const generateLargeInvoice = (lineCount: number): string => { const lines = []; for (let i = 1; i <= lineCount; i++) { lines.push(` ${i} ${i} ${(i * 10).toFixed(2)} Product ${i} Description for product ${i} with some additional text to make it larger 10.00 `); } return ` LARGE-INVOICE-${lineCount} 2024-01-01 Large Supplier Inc Berlin 10115 DE Large Customer Corp Munich 80331 DE ${lines.join('')} `; }; const sizes = [10, 100, 1000]; for (const size of sizes) { const xml = generateLargeInvoice(size); const xmlSize = Buffer.byteLength(xml, 'utf-8') / 1024; // KB const { result, metric } = await PerformanceTracker.track( `parse-${size}-lines`, async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(xml); return { success: true, itemCount: invoice.items?.length || 0, memoryUsed: metric?.memory?.used || 0 }; } catch (error) { return { success: false, error: error.message }; } } ); console.log(`Parse ${size} line items (${xmlSize.toFixed(1)}KB): ${result.success ? '✓' : '✗'}`); if (result.success) { expect(result.itemCount).toEqual(size); console.log(` Items parsed: ${result.itemCount}`); console.log(` Parse time: ${metric.duration.toFixed(2)}ms`); console.log(` Memory used: ${(metric.memory.used / 1024 / 1024).toFixed(2)}MB`); console.log(` Speed: ${(xmlSize / metric.duration * 1000).toFixed(2)}KB/s`); } } }); tap.test('PARSE-01: Real corpus file parsing', async () => { // Try to load some real files from the corpus const testFiles = [ { category: 'UBL_XMLRECHNUNG', file: 'XRECHNUNG_Einfach.ubl.xml' }, { category: 'CII_XMLRECHNUNG', file: 'XRECHNUNG_Einfach.cii.xml' }, { category: 'ZUGFERDV2_CORRECT', file: null } // Will use first available ]; for (const testFile of testFiles) { try { let xmlContent: string; if (testFile.file) { xmlContent = await CorpusLoader.loadTestFile(testFile.category, testFile.file); } else { const files = await CorpusLoader.getCorpusFiles(testFile.category); if (files.length > 0) { xmlContent = await CorpusLoader.loadTestFile(testFile.category, files[0]); } else { console.log(`No files found in category ${testFile.category}`); continue; } } const { result, metric } = await PerformanceTracker.track( 'corpus-parsing', async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(xmlContent); return { success: true, format: invoice.getFormat(), id: invoice.id, hasData: !!invoice.from && !!invoice.to && invoice.items?.length > 0 }; } catch (error) { return { success: false, error: error.message }; } } ); console.log(`${testFile.category}/${testFile.file || 'first-file'}: ${result.success ? '✓' : '✗'}`); if (result.success) { console.log(` Format: ${einvoice.InvoiceFormat[result.format]}`); console.log(` ID: ${result.id}`); console.log(` Has complete data: ${result.hasData}`); console.log(` Parse time: ${metric.duration.toFixed(2)}ms`); } else { console.log(` Error: ${result.error}`); } } catch (error) { console.log(`Failed to load ${testFile.category}/${testFile.file}: ${error.message}`); } } }); tap.test('PARSE-01: Error recovery', async () => { const errorCases = [ { name: 'Empty XML', xml: '', expectError: true }, { name: 'Invalid XML syntax', xml: 'TEST', expectError: true }, { name: 'Non-invoice XML', xml: 'test', expectError: true }, { name: 'Missing mandatory fields', xml: ` `, expectError: true } ]; for (const testCase of errorCases) { const { result } = await PerformanceTracker.track( 'error-recovery', async () => { const invoice = new einvoice.EInvoice(); try { await invoice.fromXmlString(testCase.xml); return { success: true }; } catch (error) { return { success: false, error: error.message, errorType: error.constructor.name }; } } ); console.log(`${testCase.name}: ${testCase.expectError ? (result.success ? '✗' : '✓') : (result.success ? '✓' : '✗')}`); if (testCase.expectError) { expect(result.success).toBeFalse(); console.log(` Error type: ${result.errorType}`); console.log(` Error message: ${result.error}`); } else { expect(result.success).toBeTrue(); } } }); tap.test('PARSE-01: Performance summary', async () => { const stats = PerformanceTracker.getStats('xml-parsing'); if (stats) { console.log('\nPerformance Summary:'); console.log(` Total parses: ${stats.count}`); console.log(` Average time: ${stats.avg.toFixed(2)}ms`); console.log(` Min time: ${stats.min.toFixed(2)}ms`); console.log(` Max time: ${stats.max.toFixed(2)}ms`); console.log(` P95 time: ${stats.p95.toFixed(2)}ms`); // Check against thresholds expect(stats.avg).toBeLessThan(50); // 50ms average for small files expect(stats.p95).toBeLessThan(100); // 100ms for 95th percentile } }); // Run the tests tap.start();