import { expect, tap } from '@git.zone/tstest/tapbundle'; import { EInvoice } from '../../../ts/index.js'; tap.test('ENC-05: Special Characters - should handle special XML characters correctly', async () => { console.log('Testing special character handling in XML content...\n'); // Test 1: Unicode special characters const testUnicodeSpecialChars = async () => { const einvoice = new EInvoice(); einvoice.id = 'UNICODE-SPECIAL-TEST'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; // Test various special Unicode characters const specialChars = { mathematical: '∑∏∆∇∂∞≠≤≥±∓×÷√∝∴∵∠∟⊥∥∦', currency: '€£¥₹₽₩₪₨₫₡₢₣₤₥₦₧₨₩₪₫', symbols: '™®©℗℠⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞', arrows: '←→↑↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥', punctuation: '‚„"«»‹›§¶†‡•‰‱′″‴‵‶‷‸‼⁇⁈⁉⁊⁋⁌⁍⁎⁏' }; einvoice.subject = `Unicode test: ${specialChars.mathematical.substring(0, 10)}`; einvoice.notes = [ `Math: ${specialChars.mathematical}`, `Currency: ${specialChars.currency}`, `Symbols: ${specialChars.symbols}`, `Arrows: ${specialChars.arrows}`, `Punctuation: ${specialChars.punctuation}` ]; einvoice.from = { type: 'company', name: 'Special Characters Inc ™', description: 'Company with special symbols: ®©', address: { streetName: 'Unicode Street ←→', houseNumber: '∞', postalCode: '12345', city: 'Symbol City ≤≥', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'Special Registry ™' } }; einvoice.to = { type: 'company', name: 'Customer Ltd ©', description: 'Customer with currency: €£¥', address: { streetName: 'Currency Ave', houseNumber: '€1', postalCode: '54321', city: 'Money City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE987654321', registrationId: 'HRB 54321', registrationName: 'Customer Registry' } }; einvoice.items = [{ position: 1, name: 'Product with symbols: ∑∏∆', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; const xmlString = await einvoice.toXmlString('ubl'); // Check if special characters are preserved or properly encoded const mathPreserved = specialChars.mathematical.split('').filter(char => xmlString.includes(char) || xmlString.includes(`&#${char.charCodeAt(0)};`) || xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`) ).length; const currencyPreserved = specialChars.currency.split('').filter(char => xmlString.includes(char) || xmlString.includes(`&#${char.charCodeAt(0)};`) || xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`) ).length; const symbolsPreserved = specialChars.symbols.split('').filter(char => xmlString.includes(char) || xmlString.includes(`&#${char.charCodeAt(0)};`) || xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`) ).length; return { mathPreserved, currencyPreserved, symbolsPreserved, totalMath: specialChars.mathematical.length, totalCurrency: specialChars.currency.length, totalSymbols: specialChars.symbols.length, xmlString }; }; const unicodeResult = await testUnicodeSpecialChars(); console.log('Test 1 - Unicode special characters:'); console.log(` Mathematical symbols: ${unicodeResult.mathPreserved}/${unicodeResult.totalMath} preserved`); console.log(` Currency symbols: ${unicodeResult.currencyPreserved}/${unicodeResult.totalCurrency} preserved`); console.log(` Other symbols: ${unicodeResult.symbolsPreserved}/${unicodeResult.totalSymbols} preserved`); // Test 2: Control characters and whitespace const testControlCharacters = async () => { const einvoice = new EInvoice(); einvoice.id = 'CONTROL-CHARS-TEST'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; // Test various whitespace and control characters einvoice.subject = 'Control chars test:\ttab\nnewline\rcarriage return'; einvoice.notes = [ 'Tab separated:\tvalue1\tvalue2\tvalue3', 'Line break:\nSecond line\nThird line', 'Mixed whitespace: spaces \t tabs \r\n mixed' ]; einvoice.from = { type: 'company', name: 'Control\tCharacters\nCompany', description: 'Company\twith\ncontrol\rcharacters', address: { streetName: 'Control Street', houseNumber: '1', postalCode: '12345', city: 'Test City', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'Registry' } }; einvoice.to = { type: 'company', name: 'Customer', description: 'Normal customer', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE987654321', registrationId: 'HRB 54321', registrationName: 'Customer Registry' } }; einvoice.items = [{ position: 1, name: 'Product\twith\ncontrol\rchars', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; const xmlString = await einvoice.toXmlString('ubl'); // Check how control characters are handled const hasTabHandling = xmlString.includes(' ') || xmlString.includes(' ') || xmlString.includes('\t') || !xmlString.includes('Control\tCharacters'); const hasNewlineHandling = xmlString.includes(' ') || xmlString.includes(' ') || xmlString.includes('\n') || !xmlString.includes('Characters\nCompany'); const hasCarriageReturnHandling = xmlString.includes(' ') || xmlString.includes(' ') || xmlString.includes('\r') || !xmlString.includes('control\rcharacters'); return { hasTabHandling, hasNewlineHandling, hasCarriageReturnHandling, xmlString }; }; const controlResult = await testControlCharacters(); console.log('\nTest 2 - Control characters and whitespace:'); console.log(` Tab handling: ${controlResult.hasTabHandling ? 'Yes' : 'No'}`); console.log(` Newline handling: ${controlResult.hasNewlineHandling ? 'Yes' : 'No'}`); console.log(` Carriage return handling: ${controlResult.hasCarriageReturnHandling ? 'Yes' : 'No'}`); // Test 3: Emojis and extended Unicode const testEmojisAndExtended = async () => { const einvoice = new EInvoice(); einvoice.id = 'EMOJI-TEST'; einvoice.date = Date.now(); einvoice.currency = 'EUR'; // Test emojis and extended Unicode const emojis = '😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁☹😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹👺👻👽👾🤖😺😸😹😻😼😽🙀😿😾🙈🙉🙊💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤'; einvoice.subject = `Emoji test: ${emojis.substring(0, 20)}`; einvoice.notes = [ `Faces: ${emojis.substring(0, 50)}`, `Hearts: 💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍`, `Objects: 💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤` ]; einvoice.from = { type: 'company', name: 'Emoji Company 😊', description: 'Company with emojis 🏢', address: { streetName: 'Happy Street 😃', houseNumber: '1️⃣', postalCode: '12345', city: 'Emoji City 🌆', country: 'DE' }, status: 'active', foundedDate: { year: 2020, month: 1, day: 1 }, registrationDetails: { vatId: 'DE123456789', registrationId: 'HRB 12345', registrationName: 'Registry 📝' } }; einvoice.to = { type: 'company', name: 'Customer 🛍️', description: 'Shopping customer', address: { streetName: 'Customer Street', houseNumber: '2', postalCode: '54321', city: 'Customer City', country: 'DE' }, status: 'active', foundedDate: { year: 2019, month: 1, day: 1 }, registrationDetails: { vatId: 'DE987654321', registrationId: 'HRB 54321', registrationName: 'Customer Registry' } }; einvoice.items = [{ position: 1, name: 'Emoji Product 📦', unitType: 'C62', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; const xmlString = await einvoice.toXmlString('ubl'); // Check if emojis are preserved or encoded const emojiCount = emojis.split('').filter(char => { const codePoint = char.codePointAt(0); return codePoint && codePoint > 0xFFFF; // Emojis are typically above the BMP }).length; const preservedEmojis = emojis.split('').filter(char => { const codePoint = char.codePointAt(0); if (!codePoint || codePoint <= 0xFFFF) return false; return xmlString.includes(char) || xmlString.includes(`&#${codePoint};`) || xmlString.includes(`&#x${codePoint.toString(16)};`); }).length; return { emojiCount, preservedEmojis, preservationRate: emojiCount > 0 ? (preservedEmojis / emojiCount) * 100 : 0 }; }; const emojiResult = await testEmojisAndExtended(); console.log('\nTest 3 - Emojis and extended Unicode:'); console.log(` Emoji preservation: ${emojiResult.preservedEmojis}/${emojiResult.emojiCount} (${emojiResult.preservationRate.toFixed(1)}%)`); // Test 4: XML predefined entities in content const testXmlPredefinedEntities = async () => { const xmlWithEntities = ` ENTITIES-TEST 2025-01-25 Entities: & < > " ' 380 EUR Entity & Company <Special> Street Entity City 12345 DE Customer "Quotes" Customer Street Customer City 54321 DE 1 1 100.00 Product 'Apostrophe' `; try { const invoice = await EInvoice.fromXml(xmlWithEntities); const supplierName = invoice.from?.name || ''; const customerName = invoice.to?.name || ''; const itemName = invoice.items?.[0]?.name || ''; const entitiesDecoded = supplierName.includes('Entity & Company') && customerName.includes('Customer "Quotes"') && itemName.includes("Product 'Apostrophe'"); return { success: invoice.id === 'ENTITIES-TEST', entitiesDecoded, supplierName, customerName, itemName }; } catch (error) { return { success: false, error: error.message }; } }; const entitiesResult = await testXmlPredefinedEntities(); console.log('\nTest 4 - XML predefined entities:'); console.log(` Invoice parsed: ${entitiesResult.success ? 'Yes' : 'No'}`); console.log(` Entities decoded: ${entitiesResult.entitiesDecoded ? 'Yes' : 'No'}`); if (entitiesResult.error) { console.log(` Error: ${entitiesResult.error}`); } // Summary console.log('\n=== Special Characters Test Summary ==='); const unicodeScore = (unicodeResult.mathPreserved + unicodeResult.currencyPreserved + unicodeResult.symbolsPreserved) / (unicodeResult.totalMath + unicodeResult.totalCurrency + unicodeResult.totalSymbols) * 100; console.log(`Unicode symbols: ${unicodeScore.toFixed(1)}% preserved`); console.log(`Control characters: ${controlResult.hasTabHandling && controlResult.hasNewlineHandling ? 'Handled' : 'Issues'}`); console.log(`Emojis: ${emojiResult.preservationRate.toFixed(1)}% preserved`); console.log(`XML entities: ${entitiesResult.success && entitiesResult.entitiesDecoded ? 'Working' : 'Issues'}`); // Tests pass if basic functionality works expect(unicodeScore).toBeGreaterThan(50); // At least 50% of Unicode symbols preserved expect(entitiesResult.success).toEqual(true); expect(entitiesResult.entitiesDecoded).toEqual(true); console.log('\n✓ Special characters test completed'); }); tap.start();