This commit is contained in:
2026-01-14 17:16:09 +01:00
parent ca08b4820c
commit 2c41f5619b
55 changed files with 1202 additions and 758 deletions

View File

@@ -21,7 +21,13 @@ Font.register({
// Industrial/technical/restrained design - STYLEGUIDE.md compliant
const styles = StyleSheet.create({
page: {
padding: 72, // Large margins for engineering documentation feel
// Large margins for engineering documentation feel.
// Extra bottom padding reserves space for the fixed footer so content
// (esp. long descriptions) doesn't render underneath it.
paddingTop: 72,
paddingLeft: 72,
paddingRight: 72,
paddingBottom: 140,
fontFamily: 'Helvetica',
fontSize: 10,
color: '#1F2933', // Dark gray text
@@ -165,12 +171,15 @@ const styles = StyleSheet.create({
// Cross-section table - engineering specification style
table: {
marginTop: 16,
borderWidth: 1,
borderColor: '#E6E9ED',
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#E6E9ED',
borderBottom: '1px solid #E6E9ED',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableHeaderCell: {
@@ -183,9 +192,19 @@ const styles = StyleSheet.create({
letterSpacing: 0.3,
},
tableHeaderCellLast: {
borderRightWidth: 0,
},
tableHeaderCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRow: {
flexDirection: 'row',
borderBottom: '1px solid #F8F9FA',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
tableCell: {
@@ -195,6 +214,15 @@ const styles = StyleSheet.create({
color: '#1F2933',
},
tableCellLast: {
borderRightWidth: 0,
},
tableCellWithDivider: {
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
},
tableRowAlt: {
backgroundColor: '#F8F9FA',
},
@@ -209,7 +237,54 @@ const styles = StyleSheet.create({
specsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
// Technical data table (used for the metagrid)
specsTable: {
borderWidth: 1,
borderColor: '#E6E9ED',
},
specsTableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#E6E9ED',
},
specsTableRowLast: {
borderBottomWidth: 0,
},
specsTableLabelCell: {
flex: 3,
paddingVertical: 8,
paddingHorizontal: 8,
backgroundColor: '#F8F9FA',
borderRightWidth: 1,
borderRightColor: '#E6E9ED',
justifyContent: 'center',
},
specsTableValueCell: {
flex: 4,
paddingVertical: 8,
paddingHorizontal: 8,
justifyContent: 'center',
},
specsTableLabelText: {
fontSize: 9,
fontWeight: 700,
color: '#0E2A47',
textTransform: 'uppercase',
letterSpacing: 0.3,
lineHeight: 1.2,
},
specsTableValueText: {
fontSize: 10,
color: '#1F2933',
lineHeight: 1.4,
},
specColumn: {
@@ -391,13 +466,24 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
{product.attributes && product.attributes.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{labels.specifications}</Text>
<View style={styles.specsGrid}>
<View style={styles.specsTable}>
{product.attributes.map((attr, index) => (
<View key={index} style={styles.specItem}>
<Text style={styles.specLabel}>{attr.name}</Text>
<Text style={styles.specValue}>
{attr.options.join(', ')}
</Text>
<View
key={index}
style={[
styles.specsTableRow,
index === product.attributes.length - 1 &&
styles.specsTableRowLast,
]}
>
<View style={styles.specsTableLabelCell}>
<Text style={styles.specsTableLabelText}>{attr.name}</Text>
</View>
<View style={styles.specsTableValueCell}>
<Text style={styles.specsTableValueText}>
{attr.options.join(', ')}
</Text>
</View>
</View>
))}
</View>
@@ -419,7 +505,7 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
)}
{/* Minimal footer */}
<View style={styles.footer}>
<View style={styles.footer} fixed>
<Text style={styles.footerLeft}>
{labels.sku}: {product.sku}
</Text>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,15 @@
#!/usr/bin/env ts-node
/**
* Test to verify that products with multiple Excel row structures
* use the most complete data structure
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
function normalizeValue(value: string): string {
function normalizeValue(value) {
if (!value) return '';
return String(value)
.replace(/<[^>]*>/g, '')
@@ -16,14 +17,14 @@ function normalizeValue(value: string): string {
.trim();
}
function normalizeExcelKey(value: string): string {
function normalizeExcelKey(value) {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
function loadExcelRows(filePath: string): any[] {
function loadExcelRows(filePath) {
const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
const trimmed = out.trim();
const jsonStart = trimmed.indexOf('[');
@@ -36,125 +37,93 @@ function loadExcelRows(filePath: string): any[] {
}
}
// Simulate the Excel index building
const excelFiles = [
'data/source/high-voltage.xlsx',
'data/source/medium-voltage-KM.xlsx',
'data/source/low-voltage-KM.xlsx',
'data/source/solar-cables.xlsx',
];
describe('Excel: products with multiple row structures', () => {
it('uses the most complete structure (NA2XSFL2Y)', { timeout: 30_000 }, () => {
const excelFiles = [
'data/source/high-voltage.xlsx',
'data/source/medium-voltage-KM.xlsx',
'data/source/low-voltage-KM.xlsx',
'data/source/solar-cables.xlsx',
];
const idx = new Map<string, { rows: any[]; units: Record<string, string> }>();
const idx = new Map();
for (const file of 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 file of excelFiles) {
if (!fs.existsSync(file)) continue;
const rows = loadExcelRows(file);
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
const units = {};
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;
}
}
}
}
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;
const match = idx.get('NA2XSFL2Y');
expect(match, 'NA2XSFL2Y must exist in Excel index').toBeTruthy();
if (!match) return;
// Count different structures
const structures = {};
match.rows.forEach((r, i) => {
const keys = Object.keys(r)
.filter(k => k && k !== 'Part Number' && k !== 'Units')
.sort()
.join('|');
if (!structures[keys]) structures[keys] = [];
structures[keys].push(i);
});
const structureCounts = Object.keys(structures).map(key => ({
colCount: key.split('|').length,
rowCount: structures[key].length,
rows: structures[key],
}));
const mostColumns = Math.max(...structureCounts.map(s => s.colCount));
// Simulate findExcelRowsForProduct: choose the structure with the most columns.
const rows = match.rows;
let sample = rows.find(r => r && Object.keys(r).length > 0) || {};
let maxColumns = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
for (const r of rows) {
const cols = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
if (cols > maxColumns) {
sample = r;
maxColumns = cols;
}
}
}
}
// Test NA2XSFL2Y
const match = idx.get('NA2XSFL2Y');
if (!match) {
console.log('❌ FAIL: NA2XSFL2Y not found in Excel');
process.exit(1);
}
const sampleKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
const compatibleRows = rows.filter(r => {
const rKeys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
return JSON.stringify(rKeys) === JSON.stringify(sampleKeys);
});
console.log('Test: NA2XSFL2Y multiple row structures');
console.log('========================================');
console.log(`Total rows in index: ${match.rows.length}`);
// Count different structures
const structures: Record<string, number[]> = {};
match.rows.forEach((r, i) => {
const keys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort().join('|');
if (!structures[keys]) structures[keys] = [];
structures[keys].push(i);
// Expectations
expect(sampleKeys.length).toBe(mostColumns);
expect(compatibleRows.length).toBeGreaterThan(0);
for (const r of compatibleRows) {
const keys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units');
expect(keys.length).toBe(mostColumns);
}
});
});
const structureCounts = Object.keys(structures).map(key => ({
colCount: key.split('|').length,
rowCount: structures[key].length,
rows: structures[key]
}));
structureCounts.forEach((s, i) => {
console.log(` Structure ${i+1}: ${s.colCount} columns, ${s.rowCount} rows`);
});
const mostColumns = Math.max(...structureCounts.map(s => s.colCount));
console.log(`Most complete structure: ${mostColumns} columns`);
console.log('');
// Now test the fix: simulate findExcelRowsForProduct
const rows = match.rows;
// Find the row with most columns as sample
let sample = rows.find(r => r && Object.keys(r).length > 0) || {};
let maxColumns = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
for (const r of rows) {
const cols = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
if (cols > maxColumns) {
sample = r;
maxColumns = cols;
}
}
// Filter to only rows with the same column structure as sample
const sampleKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
const compatibleRows = rows.filter(r => {
const rKeys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
return JSON.stringify(rKeys) === JSON.stringify(sampleKeys);
});
console.log('After fix (findExcelRowsForProduct):');
console.log(` Filtered rows: ${compatibleRows.length}`);
console.log(` Sample columns: ${sampleKeys.length}`);
console.log(` All rows have same structure: ${compatibleRows.every(r => {
const keys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units');
return keys.length === sampleKeys.length;
})}`);
console.log('');
// Verify the fix
const firstFilteredRowKeys = Object.keys(compatibleRows[0]).filter(k => k && k !== 'Part Number' && k !== 'Units');
console.log('✅ PASS: Filtered rows use the most complete structure');
console.log(` All ${compatibleRows.length} rows have ${mostColumns} columns`);
console.log(` First row has ${firstFilteredRowKeys.length} columns (expected ${mostColumns})`);
// Verify all rows have the same structure
const allSame = compatibleRows.every(r => {
const keys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units');
return keys.length === mostColumns;
});
if (!allSame || firstFilteredRowKeys.length !== mostColumns) {
console.log('❌ FAIL: Verification failed');
throw new Error('Verification failed');
}
console.log('\nAll checks passed!');

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env ts-node
/**
* PDF Datasheet Generator Test Suite
* Validates that datasheets are generated correctly with all expected values
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
@@ -24,14 +25,14 @@ const TEST_CONFIG = {
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 {
function normalizeExcelKey(value) {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
function normalizeValue(value: string): string {
function normalizeValue(value) {
if (!value) return '';
return String(value)
.replace(/<[^>]*>/g, '')
@@ -39,7 +40,7 @@ function normalizeValue(value: string): string {
.trim();
}
function loadExcelRows(filePath: string): any[] {
function loadExcelRows(filePath) {
const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
const trimmed = out.trim();
const jsonStart = trimmed.indexOf('[');
@@ -52,14 +53,14 @@ function loadExcelRows(filePath: string): any[] {
}
}
function getExcelIndex(): Map<string, { rows: any[]; units: Record<string, string> }> {
const idx = new Map<string, { rows: any[]; units: Record<string, string> }>();
function getExcelIndex() {
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<string, string> = {};
const units = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === 'Part Number') continue;
@@ -85,14 +86,14 @@ function getExcelIndex(): Map<string, { rows: any[]; units: Record<string, strin
return idx;
}
function findExcelForProduct(product: any): { rows: any[]; units: Record<string, string> } | null {
function findExcelForProduct(product) {
const idx = getExcelIndex();
const candidates = [
product.name,
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
product.sku,
product.translationKey,
].filter(Boolean) as string[];
].filter(Boolean);
for (const c of candidates) {
const key = normalizeExcelKey(c);
@@ -102,97 +103,54 @@ function findExcelForProduct(product: any): { rows: any[]; units: Record<string,
return null;
}
// Test Suite
class PDFDatasheetTest {
private passed = 0;
private failed = 0;
private tests: Array<{ name: string; passed: boolean; error?: string }> = [];
describe('PDF Datasheet Generator (smoke)', () => {
it('excel source files exist', () => {
const missing = TEST_CONFIG.excelFiles.filter(f => !fs.existsSync(f));
expect(missing, `Missing Excel files: ${missing.join(', ')}`).toHaveLength(0);
});
constructor() {
console.log('🧪 PDF Datasheet Generator Test Suite\n');
}
it('products.json exists', () => {
expect(fs.existsSync(TEST_CONFIG.productsFile)).toBe(true);
});
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}` : ''}`);
}
}
it('pdf output directory exists', () => {
expect(fs.existsSync(TEST_CONFIG.outputDir)).toBe(true);
});
// 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(', ')}`
);
}
it(
'excel data is loadable',
() => {
const idx = getExcelIndex();
expect(idx.size).toBeGreaterThan(0);
},
30_000,
);
// 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 {
it(
'product NA2XS(FL)2Y has excel data',
() => {
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 product = products.find(p => p.id === 46773);
expect(product).toBeTruthy();
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'
);
}
expect(match).toBeTruthy();
expect(match?.rows?.length || 0).toBeGreaterThan(0);
},
30_000,
);
// Test 6: Verify Excel rows contain expected columns
testExcelColumnsPresent(): void {
it(
'excel contains required column families',
() => {
const products = JSON.parse(fs.readFileSync(TEST_CONFIG.productsFile, 'utf8'));
const product = products.find((p: any) => p.id === 46773);
const product = products.find(p => p.id === 46773);
const match = findExcelForProduct(product);
if (!match || match.rows.length === 0) {
this.assert(false, 'Excel columns present', 'No data available');
return;
}
expect(match).toBeTruthy();
if (!match) 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'));
@@ -201,253 +159,19 @@ class PDFDatasheetTest {
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 hasOD = 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;
const foundCount = [hasDI, hasRI, hasWi, hasIbl, hasIbe, hasIk, hasWm, hasRbv, hasOD, hasG].filter(Boolean).length;
expect(foundCount).toBeGreaterThanOrEqual(5);
},
30_000,
);
// 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 {
it('pdf naming convention is correct', () => {
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();
expect(pdfFiles.length).toBeGreaterThan(0);
expect(pdfFiles.every(f => namingPattern.test(f))).toBe(true);
});
});

File diff suppressed because one or more lines are too long