2025-03-20 14:39:32 +00:00
|
|
|
import { tap, expect } from '@push.rocks/tapbundle';
|
|
|
|
import { XInvoice } from '../ts/classes.xinvoice.js';
|
|
|
|
import { type ExportFormat } from '../ts/interfaces.js';
|
2025-03-20 14:47:43 +00:00
|
|
|
import { PDFDocument, PDFName, PDFRawStream } from 'pdf-lib';
|
|
|
|
import * as pako from 'pako';
|
2025-03-20 14:39:32 +00:00
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// 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
|
2025-03-20 14:39:32 +00:00
|
|
|
const invoice = new XInvoice();
|
2025-03-20 14:47:43 +00:00
|
|
|
const uniqueId = `TEST-PDF-EXPORT-${Date.now()}`;
|
|
|
|
|
|
|
|
invoice.content.invoiceData.id = uniqueId;
|
2025-03-20 14:39:32 +00:00
|
|
|
invoice.content.invoiceData.billedBy.name = 'Test Seller';
|
|
|
|
invoice.content.invoiceData.billedTo.name = 'Test Buyer';
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// Add required address details
|
2025-03-20 14:39:32 +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';
|
|
|
|
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
// Add a test item
|
2025-03-20 14:39:32 +00:00
|
|
|
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();
|
2025-03-20 14:47:43 +00:00
|
|
|
pdfDoc.addPage().drawText('PDF Export Test');
|
2025-03-20 14:39:32 +00:00
|
|
|
const pdfBuffer = await pdfDoc.save();
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// Store original buffer size for comparison
|
|
|
|
const originalSize = pdfBuffer.byteLength;
|
|
|
|
console.log(`Original PDF size: ${originalSize} bytes`);
|
|
|
|
|
|
|
|
// Load the PDF into the invoice
|
2025-03-20 14:39:32 +00:00
|
|
|
invoice.pdf = {
|
2025-03-20 14:47:43 +00:00
|
|
|
name: 'test.pdf',
|
|
|
|
id: `test-${Date.now()}`,
|
2025-03-20 14:39:32 +00:00
|
|
|
metadata: {
|
2025-03-20 14:47:43 +00:00
|
|
|
textExtraction: 'PDF Export Test'
|
2025-03-20 14:39:32 +00:00
|
|
|
},
|
|
|
|
buffer: pdfBuffer
|
|
|
|
};
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// Test each format
|
2025-03-20 14:39:32 +00:00
|
|
|
const formats: ExportFormat[] = ['facturx', 'zugferd', 'xrechnung', 'ubl'];
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// 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('----------|----------|----------|------------');
|
|
|
|
|
2025-03-20 14:39:32 +00:00
|
|
|
for (const format of formats) {
|
2025-03-20 14:47:43 +00:00
|
|
|
// This tests the type safety of the parameter
|
2025-03-20 14:39:32 +00:00
|
|
|
const exportedPdf = await invoice.exportPdf(format);
|
2025-03-20 14:47:43 +00:00
|
|
|
const newSize = exportedPdf.buffer.byteLength;
|
|
|
|
const increase = newSize - originalSize;
|
|
|
|
const increasePercent = ((increase / originalSize) * 100).toFixed(1);
|
2025-03-20 14:39:32 +00:00
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// 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
|
2025-03-20 14:39:32 +00:00
|
|
|
expect(exportedPdf).toBeDefined();
|
|
|
|
expect(exportedPdf.buffer).toBeDefined();
|
2025-03-20 14:47:43 +00:00
|
|
|
expect(exportedPdf.buffer.byteLength).toBeGreaterThan(originalSize);
|
2025-03-20 14:39:32 +00:00
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// Check the PDF structure for embedded files
|
2025-03-20 14:39:32 +00:00
|
|
|
const pdfDoc = await PDFDocument.load(exportedPdf.buffer);
|
2025-03-20 14:47:43 +00:00
|
|
|
|
|
|
|
// Verify Names dictionary exists - required for embedded files
|
2025-03-20 14:39:32 +00:00
|
|
|
const namesDict = pdfDoc.catalog.lookup(PDFName.of('Names'));
|
|
|
|
expect(namesDict).toBeDefined();
|
2025-03-20 14:47:43 +00:00
|
|
|
|
|
|
|
// 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()}`);
|
|
|
|
}
|
|
|
|
}
|
2025-03-20 14:39:32 +00:00
|
|
|
}
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
console.log('\n✓ All formats successfully exported PDFs with embedded files');
|
2025-03-20 14:39:32 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// 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();
|
2025-03-20 14:47:43 +00:00
|
|
|
|
|
|
|
// 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';
|
2025-03-20 14:47:43 +00:00
|
|
|
|
|
|
|
// 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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
2025-04-03 13:26:27 +00:00
|
|
|
// Add test items with different unit types, quantities, and tax rates
|
|
|
|
const testItems = [
|
2025-03-20 14:47:43 +00:00
|
|
|
{
|
|
|
|
position: 1,
|
2025-04-03 13:26:27 +00:00
|
|
|
name: 'Special Product A',
|
2025-03-20 14:47:43 +00:00
|
|
|
unitType: 'piece',
|
|
|
|
unitQuantity: 2,
|
|
|
|
unitNetPrice: 99.95,
|
|
|
|
vatPercentage: 19
|
|
|
|
},
|
|
|
|
{
|
|
|
|
position: 2,
|
2025-04-03 13:26:27 +00:00
|
|
|
name: 'Premium Service B',
|
2025-03-20 14:47:43 +00:00
|
|
|
unitType: 'hour',
|
|
|
|
unitQuantity: 5,
|
|
|
|
unitNetPrice: 120.00,
|
|
|
|
vatPercentage: 7
|
|
|
|
},
|
|
|
|
{
|
|
|
|
position: 3,
|
2025-04-03 13:26:27 +00:00
|
|
|
name: 'Unique Item C',
|
2025-03-20 14:47:43 +00:00
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
}
|
|
|
|
|
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}`));
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// 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;
|
|
|
|
|
2025-03-20 14:47:43 +00:00
|
|
|
// Assign the PDF to the invoice
|
2025-04-03 13:26:27 +00:00
|
|
|
invoice.pdf = {
|
2025-03-20 14:47:43 +00:00
|
|
|
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`);
|
2025-03-20 14:47:43 +00:00
|
|
|
} else {
|
2025-04-03 13:26:27 +00:00
|
|
|
console.log(`✗ Item "${item.name}" not found in exported XML`);
|
2025-03-20 14:47:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
|
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-03-20 14:47:43 +00:00
|
|
|
}
|
|
|
|
|
2025-04-03 13:26:27 +00:00
|
|
|
console.log('\n✓ All formats produced XML with the expected structure');
|
2025-03-20 14:47:43 +00:00
|
|
|
});
|
|
|
|
|
2025-03-20 14:39:32 +00:00
|
|
|
// Start the tests
|
|
|
|
export default tap.start();
|