#!/usr/bin/env ts-node /** * PDF Datasheet Generator Test Suite * Validates that datasheets are generated correctly with all expected values */ import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; // Test configuration const TEST_CONFIG = { productsFile: path.join(process.cwd(), 'data/processed/products.json'), excelFiles: [ path.join(process.cwd(), 'data/source/high-voltage.xlsx'), path.join(process.cwd(), 'data/source/medium-voltage-KM.xlsx'), path.join(process.cwd(), 'data/source/low-voltage-KM.xlsx'), path.join(process.cwd(), 'data/source/solar-cables.xlsx'), ], outputDir: path.join(process.cwd(), 'public/datasheets'), }; // Expected table headers (13 columns as specified) const EXPECTED_HEADERS = ['DI', 'RI', 'Wi', 'Ibl', 'Ibe', 'Ik', 'Wm', 'Rbv', 'Ø', 'Fzv', 'Al', 'Cu', 'G']; // Helper functions (copied from generate-pdf-datasheets.ts for testing) function normalizeExcelKey(value: string): string { return String(value || '') .toUpperCase() .replace(/-\d+$/g, '') .replace(/[^A-Z0-9]+/g, ''); } function normalizeValue(value: string): string { if (!value) return ''; return String(value) .replace(/<[^>]*>/g, '') .replace(/\s+/g, ' ') .trim(); } function loadExcelRows(filePath: string): any[] { const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }); const trimmed = out.trim(); const jsonStart = trimmed.indexOf('['); if (jsonStart < 0) return []; const jsonText = trimmed.slice(jsonStart); try { return JSON.parse(jsonText); } catch { return []; } } function getExcelIndex(): Map }> { const idx = new Map }>(); for (const file of TEST_CONFIG.excelFiles) { if (!fs.existsSync(file)) continue; const rows = loadExcelRows(file); const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null; const units: Record = {}; if (unitsRow) { for (const [k, v] of Object.entries(unitsRow)) { if (k === 'Part Number') continue; const unit = normalizeValue(String(v ?? '')); if (unit) units[k] = unit; } } for (const r of rows) { const pn = r?.['Part Number']; if (!pn || pn === 'Units') continue; const key = normalizeExcelKey(String(pn)); if (!key) continue; const cur = idx.get(key); if (!cur) { idx.set(key, { rows: [r], units }); } else { cur.rows.push(r); if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units; } } } return idx; } function findExcelForProduct(product: any): { rows: any[]; units: Record } | null { const idx = getExcelIndex(); const candidates = [ product.name, product.slug ? product.slug.replace(/-\d+$/g, '') : '', product.sku, product.translationKey, ].filter(Boolean) as string[]; for (const c of candidates) { const key = normalizeExcelKey(c); const match = idx.get(key); if (match && match.rows.length) return match; } return null; } // Test Suite class PDFDatasheetTest { private passed = 0; private failed = 0; private tests: Array<{ name: string; passed: boolean; error?: string }> = []; constructor() { console.log('🧪 PDF Datasheet Generator Test Suite\n'); } private assert(condition: boolean, name: string, error?: string): void { if (condition) { this.passed++; this.tests.push({ name, passed: true }); console.log(`✅ ${name}`); } else { this.failed++; this.tests.push({ name, passed: false, error }); console.log(`❌ ${name}${error ? ` - ${error}` : ''}`); } } // Test 1: Check if Excel files exist testExcelFilesExist(): void { const allExist = TEST_CONFIG.excelFiles.every(file => fs.existsSync(file)); this.assert( allExist, 'Excel source files exist', `Missing: ${TEST_CONFIG.excelFiles.filter(f => !fs.existsSync(f)).join(', ')}` ); } // Test 2: Check if products.json exists testProductsFileExists(): void { this.assert( fs.existsSync(TEST_CONFIG.productsFile), 'Products JSON file exists' ); } // Test 3: Check if PDF output directory exists testOutputDirectoryExists(): void { this.assert( fs.existsSync(TEST_CONFIG.outputDir), 'PDF output directory exists' ); } // Test 4: Verify Excel data can be loaded testExcelDataLoadable(): void { try { const idx = getExcelIndex(); this.assert(idx.size > 0, 'Excel data loaded successfully', `Found ${idx.size} products`); } catch (error) { this.assert(false, 'Excel data loaded successfully', String(error)); } } // Test 5: Check specific product (NA2XS(FL)2Y) has Excel data testSpecificProductHasExcelData(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); if (!product) { this.assert(false, 'Product NA2XS(FL)2Y exists in products.json'); return; } const match = findExcelForProduct(product); this.assert( match !== null && match.rows.length > 0, 'Product NA2XS(FL)2Y has Excel data', match ? `Found ${match.rows.length} rows` : 'No Excel match found' ); } // Test 6: Verify Excel rows contain expected columns testExcelColumnsPresent(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match || match.rows.length === 0) { this.assert(false, 'Excel columns present', 'No data available'); return; } const sampleRow = match.rows[0]; const excelKeys = Object.keys(sampleRow).map(k => k.toLowerCase()); // Check for key columns that map to our 13 headers (flexible matching for actual Excel names) const hasDI = excelKeys.some(k => k.includes('diameter over insulation') || k.includes('insulation diameter')); const hasRI = excelKeys.some(k => k.includes('dc resistance') || k.includes('resistance conductor') || k.includes('leiterwiderstand')); const hasWi = excelKeys.some(k => k.includes('nominal insulation thickness') || k.includes('insulation thickness')); const hasIbl = excelKeys.some(k => k.includes('current ratings in air') || k.includes('strombelastbarkeit luft')); const hasIbe = excelKeys.some(k => k.includes('current ratings in ground') || k.includes('strombelastbarkeit erdreich')); const hasIk = excelKeys.some(k => k.includes('shortcircuit current') || k.includes('kurzschlussstrom')); const hasWm = excelKeys.some(k => k.includes('sheath thickness') || k.includes('manteldicke')); const hasRbv = excelKeys.some(k => k.includes('bending radius') || k.includes('biegeradius')); const hasØ = excelKeys.some(k => k.includes('outer diameter') || k.includes('außen') || k.includes('durchmesser')); const hasG = excelKeys.some(k => k.includes('weight') || k.includes('gewicht')); const foundCount = [hasDI, hasRI, hasWi, hasIbl, hasIbe, hasIk, hasWm, hasRbv, hasØ, hasG].filter(Boolean).length; // At least 5 of the 10 required columns should be present this.assert( foundCount >= 5, 'Excel contains required columns', `Found ${foundCount}/10 key columns (minimum 5 required)` ); } // Test 7: Verify PDF files were generated testPDFsGenerated(): void { const pdfFiles = fs.readdirSync(TEST_CONFIG.outputDir).filter(f => f.endsWith('.pdf')); this.assert( pdfFiles.length === 50, 'All 50 PDFs generated', `Found ${pdfFiles.length} PDFs` ); } // Test 8: Check PDF file sizes are reasonable testPDFFileSizes(): void { const pdfFiles = fs.readdirSync(TEST_CONFIG.outputDir).filter(f => f.endsWith('.pdf')); const sizes = pdfFiles.map(f => { const stat = fs.statSync(path.join(TEST_CONFIG.outputDir, f)); return stat.size; }); const avgSize = sizes.reduce((a, b) => a + b, 0) / sizes.length; const minSize = Math.min(...sizes); const maxSize = Math.max(...sizes); this.assert( avgSize > 50000 && avgSize < 500000, 'PDF file sizes are reasonable', `Avg: ${(avgSize / 1024).toFixed(1)}KB, Min: ${(minSize / 1024).toFixed(1)}KB, Max: ${(maxSize / 1024).toFixed(1)}KB` ); } // Test 9: Verify product NA2XS(FL)2Y has voltage-specific data testVoltageGrouping(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match || match.rows.length === 0) { this.assert(false, 'Voltage grouping works', 'No Excel data'); return; } // Check if rows have voltage information const hasVoltage = match.rows.some(r => { const v = r['Rated voltage'] || r['Voltage rating'] || r['Spannung'] || r['Nennspannung']; return v !== undefined && v !== 'Units'; }); this.assert( hasVoltage, 'Voltage grouping data present', 'Rows contain voltage ratings' ); } // Test 10: Verify all required units are present testUnitsPresent(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match) { this.assert(false, 'Units mapping present', 'No Excel data'); return; } const requiredUnits = ['mm', 'Ohm/km', 'A', 'kA', 'N', 'kg/km']; const foundUnits = requiredUnits.filter(u => Object.values(match.units).some(unit => unit.toLowerCase().includes(u.toLowerCase())) ); this.assert( foundUnits.length >= 4, 'Required units present', `Found ${foundUnits.length}/${requiredUnits.length} units` ); } // Test 11: Check if technical data extraction works testTechnicalDataExtraction(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match || match.rows.length === 0) { this.assert(false, 'Technical data extraction', 'No Excel data'); return; } // Check for constant values (technical data) const constantKeys = ['Conductor', 'Insulation', 'Sheath', 'Norm']; const hasConstantData = constantKeys.some(key => { const values = match.rows.map(r => normalizeValue(String(r?.[key] ?? ''))).filter(Boolean); const unique = Array.from(new Set(values.map(v => v.toLowerCase()))); return unique.length === 1 && values.length > 0; }); this.assert( hasConstantData, 'Technical data extraction works', 'Found constant values for conductor/insulation/sheath' ); } // Test 12: Verify table structure for sample product testTableStructure(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match || match.rows.length === 0) { this.assert(false, 'Table structure valid', 'No Excel data'); return; } // Check cross-section column exists (actual name in Excel) const excelKeys = Object.keys(match.rows[0]).map(k => k.toLowerCase()); const hasCrossSection = excelKeys.some(k => k.includes('number of cores and cross-section') || k.includes('querschnitt') || k.includes('ross section') || k.includes('cross-section') ); this.assert( hasCrossSection, 'Cross-section column present', 'Required for table structure' ); } // Test 13: Verify PDF naming convention testPDFNaming(): void { const pdfFiles = fs.readdirSync(TEST_CONFIG.outputDir).filter(f => f.endsWith('.pdf')); const namingPattern = /^[a-z0-9-]+-(en|de)\.pdf$/; const allValid = pdfFiles.every(f => namingPattern.test(f)); const sampleNames = pdfFiles.slice(0, 5); this.assert( allValid, 'PDF naming convention correct', `Examples: ${sampleNames.join(', ')}` ); } // Test 14: Check if both EN and DE versions exist for sample products testBothLanguages(): void { const pdfFiles = fs.readdirSync(TEST_CONFIG.outputDir).filter(f => f.endsWith('.pdf')); const enFiles = pdfFiles.filter(f => f.endsWith('-en.pdf')); const deFiles = pdfFiles.filter(f => f.endsWith('-de.pdf')); this.assert( enFiles.length === 25 && deFiles.length === 25, 'Both EN and DE versions generated', `EN: ${enFiles.length}, DE: ${deFiles.length}` ); } // Test 15: Verify Excel to header mapping testHeaderMapping(): void { const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8')); const product = products.find((p: any) => p.id === 46773); const match = findExcelForProduct(product); if (!match || match.rows.length === 0) { this.assert(false, 'Header mapping correct', 'No Excel data'); return; } const sampleRow = match.rows[0]; const excelKeys = Object.keys(sampleRow).map(k => k.toLowerCase()); // Check for actual Excel column names that map to our 13 headers (flexible matching) const checks = { 'diameter over insulation': excelKeys.some(k => k.includes('diameter over insulation') || k.includes('insulation diameter')), 'dc resistance': excelKeys.some(k => k.includes('dc resistance') || k.includes('resistance conductor') || k.includes('leiterwiderstand')), 'insulation thickness': excelKeys.some(k => k.includes('nominal insulation thickness') || k.includes('insulation thickness')), 'current ratings in air, trefoil': excelKeys.some(k => k.includes('current ratings in air') || k.includes('strombelastbarkeit luft')), 'current ratings in ground, trefoil': excelKeys.some(k => k.includes('current ratings in ground') || k.includes('strombelastbarkeit erdreich')), 'conductor shortcircuit current': excelKeys.some(k => k.includes('shortcircuit current') || k.includes('kurzschlussstrom')), 'sheath thickness': excelKeys.some(k => k.includes('sheath thickness') || k.includes('manteldicke')), 'bending radius': excelKeys.some(k => k.includes('bending radius') || k.includes('biegeradius')), 'outer diameter': excelKeys.some(k => k.includes('outer diameter') || k.includes('außen') || k.includes('durchmesser')), 'weight': excelKeys.some(k => k.includes('weight') || k.includes('gewicht')), }; const foundCount = Object.values(checks).filter(Boolean).length; // At least 5 of the 10 mappings should work this.assert( foundCount >= 5, 'Header mapping works', `Mapped ${foundCount}/10 Excel columns to our headers (minimum 5 required)` ); } // Run all tests runAll(): void { console.log('Running tests...\n'); this.testExcelFilesExist(); this.testProductsFileExists(); this.testOutputDirectoryExists(); this.testExcelDataLoadable(); this.testSpecificProductHasExcelData(); this.testExcelColumnsPresent(); this.testPDFsGenerated(); this.testPDFFileSizes(); this.testVoltageGrouping(); this.testUnitsPresent(); this.testTechnicalDataExtraction(); this.testTableStructure(); this.testPDFNaming(); this.testBothLanguages(); this.testHeaderMapping(); console.log('\n' + '='.repeat(60)); console.log(`RESULTS: ${this.passed} passed, ${this.failed} failed`); console.log('='.repeat(60)); if (this.failed > 0) { console.log('\n❌ Failed tests:'); this.tests.filter(t => !t.passed).forEach(t => { console.log(` - ${t.name}${t.error ? `: ${t.error}` : ''}`); }); // Don't call process.exit in test environment return; } else { console.log('\n✅ All tests passed!'); // Don't call process.exit in test environment return; } } } // Run tests const testSuite = new PDFDatasheetTest(); testSuite.runAll();