Files
web-cad/frontend/tests/bomExport.test.ts
T

163 lines
5.9 KiB
TypeScript

/**
* Task CAD-9 - Stueckliste (BOM) fuer Traversen.
*
* buildBOM: gruppiert Traversen + Boxcorner nach Typ + Mass
* (Laenge x Breite), zaehlt Stueckzahlen, summiert Gesamt-meter.
* exportBOMCsv: saubere CSV mit Kopfzeile fuer Excel-Import.
* Dialog zeigt Tabelle + CSV-Download.
*/
import { describe, it, expect } from 'vitest';
import { buildBOM, exportBOMCsv, type BOMRow } from '../src/services/bomService';
import type { CADElement } from '../src/types/cad.types';
function truss(id: string, x: number, y: number, length: number, rotation: number, width = 29): CADElement {
return {
id, type: 'truss', layerId: 'l', x, y, width: length, height: width,
properties: { length, rotation, trussWidth: width, truss: true },
} as unknown as CADElement;
}
function corner(id: string, x: number, y: number, armLength: number, rotation: number, width = 29): CADElement {
return {
id, type: 'boxcorner', layerId: 'l', x, y, width: 80, height: 80,
properties: { armLength, rotation, trussWidth: width, boxcorner: true },
} as unknown as CADElement;
}
function chair(id: string): CADElement {
return {
id, type: 'chair', layerId: 'l', x: 0, y: 0, width: 45, height: 45,
properties: {},
} as unknown as CADElement;
}
// ─── buildBOM: Gruppierung ──────────────────────────────────
describe('CAD-9: buildBOM (Gruppierung + Zaehlung)', () => {
it('zwei identische Traversen (200x29) ergeben EINE Position mit Anzahl 2', () => {
const els = [
truss('t1', 0, 0, 200, 0),
truss('t2', 100, 100, 200, 90),
];
const bom = buildBOM(els);
const rows = bom.rows.filter((r) => r.type === 'Traverse');
expect(rows).toHaveLength(1);
expect(rows[0].count).toBe(2);
});
it('unterschiedliche Laengen ergetrennte Positionen', () => {
const els = [
truss('t1', 0, 0, 200, 0),
truss('t2', 0, 0, 100, 0),
truss('t3', 0, 0, 300, 0),
];
const bom = buildBOM(els);
const trussRows = bom.rows.filter((r) => r.type === 'Traverse');
expect(trussRows).toHaveLength(3);
const counts = trussRows.map((r) => r.count);
expect(counts).toEqual([1, 1, 1]);
});
it('unterschiedliche Breiten = getrennte Positionen (100x29 vs 100x40)', () => {
const els = [
truss('t1', 0, 0, 100, 0, 29),
truss('t2', 0, 0, 100, 0, 40),
];
const bom = buildBOM(els);
expect(bom.rows.filter((r) => r.type === 'Traverse')).toHaveLength(2);
});
it('Boxcorner als eigener Typ mit armLength als Laenge', () => {
const els = [
corner('c1', 0, 0, 50, 0),
corner('c2', 100, 100, 50, 90),
corner('c3', 200, 200, 75, 0),
];
const bom = buildBOM(els);
const cornerRows = bom.rows.filter((r) => r.type === 'Eck-Traverse');
expect(cornerRows).toHaveLength(2); // 50cm x2 + 75cm x1
const r50 = cornerRows.find((r) => r.length === 50)!;
expect(r50.count).toBe(2);
});
it('Nicht-Traversen (chair etc.) werden ignoriert', () => {
const els = [chair('s1'), truss('t1', 0, 0, 200, 0)];
const bom = buildBOM(els);
expect(bom.rows.every((r) => r.type !== 'chair')).toBe(true);
});
it('leere Zeichnung: leere Stueckliste ohne Crash', () => {
const bom = buildBOM([]);
expect(bom.rows).toHaveLength(0);
expect(bom.totalLengthM).toBe(0);
});
it('Gesamtlaenge: 200+100+300cm Traversen + 2x50cm Ecken = 7.00m', () => {
const els = [
truss('t1', 0, 0, 200, 0),
truss('t2', 0, 0, 100, 0),
truss('t3', 0, 0, 300, 0),
corner('c1', 0, 0, 50, 0),
corner('c2', 0, 0, 50, 90),
];
const bom = buildBOM(els);
// Traversen 6.00m + 2 Ecken je 2 Arme = 8.00m
expect(bom.totalLengthM).toBeCloseTo(8.0, 2);
});
it('Boxcorner zaehlt BEIDE Arme in die Gesamtlaenge (50er-Ecke = 1.00m)', () => {
const els = [corner('c1', 0, 0, 50, 0)];
const bom = buildBOM(els);
expect(bom.totalLengthM).toBeCloseTo(1.0, 2);
});
it('Positionen sind sortiert: Traversen vor Ecken, dann nach Laenge absteigend', () => {
const els = [
corner('c1', 0, 0, 50, 0),
truss('t1', 0, 0, 100, 0),
truss('t2', 0, 0, 300, 0),
];
const bom = buildBOM(els);
const types = bom.rows.map((r) => r.type);
// Traversen zuerst (nach Laenge absteigend), dann Ecken
expect(types.indexOf('Traverse')).toBeLessThan(types.indexOf('Eck-Traverse'));
const trussLens = bom.rows.filter((r) => r.type === 'Traverse').map((r) => r.length);
expect(trussLens[0]).toBeGreaterThanOrEqual(trussLens[trussLens.length - 1]);
});
});
// ─── exportBOMCsv ─────────────────────────────────────────
describe('CAD-9: exportBOMCsv', () => {
it('CSV hat Kopfzeile und Zeilen pro Position mit Semikolon/ Komma', () => {
const els = [truss('t1', 0, 0, 200, 0), truss('t2', 0, 0, 200, 90)];
const bom = buildBOM(els);
const csv = exportBOMCsv(bom);
const lines = csv.split('\n');
expect(lines[0]).toContain('Pos');
expect(lines[0]).toContain('Typ');
expect(lines[0]).toContain('Anzahl');
expect(lines).toHaveLength(2 + 1); // Header + 1 Position + ggf. Summenzeile
});
it('CSV enthaelt Laenge und Anzahl der Position', () => {
const els = [truss('t1', 0, 0, 200, 0), truss('t2', 0, 0, 200, 0)];
const csv = exportBOMCsv(buildBOM(els));
expect(csv).toContain('200');
expect(csv).toContain('2');
});
it('CSV: Semikolon-Separatoren (Excel-DE-freundlich) oder Komma + konsistent', () => {
const csv = exportBOMCsv(buildBOM([truss('t1', 0, 0, 200, 0)]));
// Konsistenz: Jede Datenzeile hat dieselbe Anzahl Separatoren
const lines = csv.trim().split('\n');
const sepCounts = lines.map((l) => (l.match(/;/g) || []).length);
expect(new Set(sepCounts).size).toBe(1);
});
it('leere Stueckliste: nur Kopfzeile', () => {
const csv = exportBOMCsv(buildBOM([]));
expect(csv.trim().split('\n')).toHaveLength(1);
});
});