454 lines
16 KiB
TypeScript
454 lines
16 KiB
TypeScript
#!/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<string, { rows: any[]; units: Record<string, string> }> {
|
|
const idx = new Map<string, { rows: any[]; units: Record<string, string> }>();
|
|
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<string, string> = {};
|
|
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<string, string> } | 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();
|