forked from jappel/leistungsbilanz-ts
248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { describe, it } from "node:test";
|
||
import {
|
||
assertPrintRevisionConsistency,
|
||
buildBoardHierarchy,
|
||
formatFeedingReference,
|
||
formatCircuitCable,
|
||
formatProtectionDevice,
|
||
summarizePrintBoard,
|
||
} from "../src/frontend/print/print-document-model.js";
|
||
import type {
|
||
CircuitTreeResponseDto,
|
||
DistributionBoardDto,
|
||
} from "../src/frontend/types.js";
|
||
|
||
function board(
|
||
id: string,
|
||
info: Partial<DistributionBoardDto> = {}
|
||
): DistributionBoardDto {
|
||
return {
|
||
id,
|
||
projectId: "project-1",
|
||
name: id,
|
||
floorId: null,
|
||
supplyType: "AV",
|
||
simultaneityFactor: 1,
|
||
boardNumber: null,
|
||
location: null,
|
||
fedFrom: null,
|
||
remark: null,
|
||
feedingDistributionBoardId: null,
|
||
feedingCircuitId: null,
|
||
...info,
|
||
};
|
||
}
|
||
|
||
function tree(
|
||
rows: Array<{
|
||
quantity: number;
|
||
powerPerUnit: number;
|
||
simultaneityFactor: number;
|
||
cosPhi?: number;
|
||
}>,
|
||
boardSimultaneityFactor = 1
|
||
): CircuitTreeResponseDto {
|
||
const deviceRows = rows.map((row, index) => ({
|
||
id: `row-${index}`,
|
||
sortOrder: index,
|
||
name: "Gerät",
|
||
displayName: "Gerät",
|
||
manualQuantity: row.quantity,
|
||
rowTotalPower: row.quantity * row.powerPerUnit * row.simultaneityFactor,
|
||
...row,
|
||
}));
|
||
const total = deviceRows.reduce((sum, row) => sum + row.rowTotalPower, 0);
|
||
return {
|
||
circuitListId: "list",
|
||
distributionBoardId: "board",
|
||
distributionBoardName: "Board",
|
||
currentRevision: 1,
|
||
singlePhaseVoltageV: 230,
|
||
threePhaseVoltageV: 400,
|
||
distributionBoardSimultaneityFactor: boardSimultaneityFactor,
|
||
distributionBoardTotalPower: total,
|
||
distributionBoardTotalPowerWithSimultaneityFactor:
|
||
total * boardSimultaneityFactor,
|
||
headerComponents: [],
|
||
footerComponents: [],
|
||
sections: [
|
||
{
|
||
id: "section",
|
||
key: "single_phase",
|
||
displayName: "1-phasig 1",
|
||
prefix: "-2F1.",
|
||
sortOrder: 1,
|
||
sectionTotalPower: total,
|
||
components: [],
|
||
circuits: [
|
||
{
|
||
id: "circuit",
|
||
circuitListId: "list",
|
||
sectionId: "section",
|
||
equipmentIdentifier: "-2F1.1",
|
||
sortOrder: 1,
|
||
isReserve: false,
|
||
circuitTotalPower: total,
|
||
circuitApparentPower: deviceRows.reduce(
|
||
(sum, row) => sum + row.rowTotalPower / (row.cosPhi || 1),
|
||
0
|
||
),
|
||
circuitCosPhi: 1,
|
||
deviceRows,
|
||
downstreamBoardLoads: [],
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
function summary(
|
||
id: string,
|
||
info: Partial<DistributionBoardDto>,
|
||
demandKw: number
|
||
) {
|
||
return summarizePrintBoard({
|
||
board: board(id, info),
|
||
circuitListId: id,
|
||
floorName: null,
|
||
tree: tree([{ quantity: 1, powerPerUnit: demandKw, simultaneityFactor: 1 }]),
|
||
});
|
||
}
|
||
|
||
describe("print document model", () => {
|
||
it("rejects mixed project revisions instead of printing mislabeled data", () => {
|
||
const summaries = [summary("uv", {}, 1)];
|
||
assert.doesNotThrow(() => assertPrintRevisionConsistency(1, summaries, 1));
|
||
assert.throws(() => assertPrintRevisionConsistency(0, summaries, 0), /neu laden/);
|
||
assert.throws(() => assertPrintRevisionConsistency(1, summaries, 2), /neu laden/);
|
||
assert.throws(() => assertPrintRevisionConsistency(1, [], 2), /neu laden/);
|
||
});
|
||
|
||
it("prints both cable type and cross-section without hiding either value", () => {
|
||
const circuit = tree([]).sections[0].circuits[0];
|
||
assert.equal(formatCircuitCable({ ...circuit, cableType: "NYM-J", cableCrossSection: "3x2,5" }), "NYM-J · 3x2,5");
|
||
assert.equal(formatCircuitCable({ ...circuit, cableType: "NYM-J" }), "NYM-J");
|
||
assert.equal(formatCircuitCable({ ...circuit, cableCrossSection: "3x2,5" }), "3x2,5");
|
||
assert.equal(formatCircuitCable(circuit), null);
|
||
});
|
||
|
||
it("does not derive hierarchy from the external free text", () => {
|
||
const summaries = [
|
||
summary("a", { boardNumber: "HV" }, 2),
|
||
summary("b", { name: "HV" }, 3),
|
||
summary("uv", { fedFrom: " hv " }, 1),
|
||
];
|
||
for (const ordered of [summaries, [...summaries].reverse()]) {
|
||
const entries = buildBoardHierarchy(ordered);
|
||
assert.ok(entries.every((entry) => entry.parentBoardId === null));
|
||
}
|
||
});
|
||
|
||
it("derives installed, device and demand power and the design current", () => {
|
||
const result = summarizePrintBoard({
|
||
board: board("uv"),
|
||
circuitListId: "uv",
|
||
floorName: "EG",
|
||
tree: tree(
|
||
[
|
||
{ quantity: 4, powerPerUnit: 2, simultaneityFactor: 0.5, cosPhi: 0.8 },
|
||
{ quantity: 1, powerPerUnit: 6, simultaneityFactor: 1 },
|
||
],
|
||
0.5
|
||
),
|
||
});
|
||
assert.equal(result.circuitCount, 1);
|
||
assert.equal(result.installedPowerKw, 14);
|
||
assert.equal(result.powerBeforeBoardSimultaneityKw, 10);
|
||
assert.equal(result.demandPowerKw, 5);
|
||
// S = (4 / 0.8 + 6 / 1) × 0.5 = 5.5 kVA → I = 5500 / (√3 · 400)
|
||
assert.ok(Math.abs(result.designCurrentA - 5500 / (Math.sqrt(3) * 400)) < 1e-9);
|
||
});
|
||
|
||
it("uses the selected one-phase feeder circuit for the design current", () => {
|
||
const inputTree = tree([
|
||
{ quantity: 1, powerPerUnit: 2.3, simultaneityFactor: 1, cosPhi: 1 },
|
||
]);
|
||
inputTree.feedingReference = {
|
||
distributionBoardId: "hv",
|
||
circuitListId: "hv-list",
|
||
distributionBoardName: "Hauptverteilung",
|
||
distributionBoardNumber: "NSHV",
|
||
circuitId: "out",
|
||
circuitEquipmentIdentifier: "-2F1.1",
|
||
circuitCategory: "single_phase",
|
||
};
|
||
const result = summarizePrintBoard({
|
||
board: board("uv"),
|
||
circuitListId: "uv",
|
||
floorName: null,
|
||
tree: inputTree,
|
||
});
|
||
assert.ok(Math.abs(result.designCurrentA - 10) < 1e-9);
|
||
});
|
||
|
||
it("nests boards by their logical feeding references", () => {
|
||
const entries = buildBoardHierarchy([
|
||
summary("hv", { boardNumber: "HV 0.1", fedFrom: "HAK" }, 10),
|
||
summary("uv-a", { boardNumber: "UV 1.1", feedingDistributionBoardId: "hv" }, 4),
|
||
summary("uv-b", { boardNumber: "UV 1.2", feedingDistributionBoardId: "uv-a" }, 2),
|
||
summary("uv-c", { name: "UV-C", fedFrom: "unbekannt" }, 1),
|
||
]);
|
||
assert.deepEqual(
|
||
entries.map((entry) => [
|
||
entry.summary.board.id,
|
||
entry.depth,
|
||
entry.parentBoardId,
|
||
]),
|
||
[
|
||
["hv", 0, null],
|
||
["uv-a", 1, "hv"],
|
||
["uv-b", 2, "uv-a"],
|
||
["uv-c", 0, null],
|
||
]
|
||
);
|
||
});
|
||
|
||
it("keeps malformed cyclic logical links visible at the top level", () => {
|
||
const entries = buildBoardHierarchy([
|
||
summary("a", { boardNumber: "A", feedingDistributionBoardId: "b" }, 1),
|
||
summary("b", { boardNumber: "B", feedingDistributionBoardId: "a" }, 1),
|
||
]);
|
||
assert.equal(entries.length, 2);
|
||
assert.ok(entries.some((entry) => entry.parentBoardId === null));
|
||
});
|
||
|
||
it("formats the resolved feeding board and circuit instead of free text", () => {
|
||
const value = summary("uv", { fedFrom: "Alttext" }, 1);
|
||
value.tree.feedingReference = {
|
||
distributionBoardId: "hv",
|
||
circuitListId: "hv-list",
|
||
distributionBoardName: "Hauptverteilung",
|
||
distributionBoardNumber: "NSHV",
|
||
circuitId: "out",
|
||
circuitEquipmentIdentifier: "-3F1.1",
|
||
circuitCategory: "three_phase",
|
||
};
|
||
assert.equal(formatFeedingReference(value), "NSHV-3F1.1");
|
||
});
|
||
|
||
it("formats protection devices for the plan table", () => {
|
||
assert.equal(
|
||
formatProtectionDevice({ type: "LS", ratedCurrentA: 16, tripCharacteristic: "B" }),
|
||
"LS B 16 A"
|
||
);
|
||
assert.equal(
|
||
formatProtectionDevice({
|
||
type: "FI_LS",
|
||
ratedCurrentA: 16,
|
||
tripCharacteristic: "B",
|
||
rcdType: "A",
|
||
ratedResidualCurrentMa: 30,
|
||
}),
|
||
"FI/LS B 16 A, 30 mA Typ A"
|
||
);
|
||
assert.equal(formatProtectionDevice(undefined), null);
|
||
});
|
||
});
|