wip
This commit is contained in:
@@ -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
@@ -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!');
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user