leistungsbilanz-ts/tests/circuit-grid-model.test.ts
Julian Appel 7222398628 Typecheck tests against current types
tsx runs tests without type checking and tsconfig.json covers only src,
so 71 type errors had accumulated unnoticed. tsconfig.tests.json now
checks every test and helper, and npm run typecheck includes it.

Fixtures gain the fields the DTOs have grown since they were written
(manualQuantity, circuit apparent power/cos phi, downstream board loads,
section totals, building, cover and board info fields). Repository
lookups that return "row ?? null" now declare the null in their type,
which their callers already handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 13:17:45 +01:00

248 lines
8.3 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
allColumns,
buildCircuitEditPatch,
buildDeviceRowEditPatch,
formatValue,
formatCellValue,
getCircuitSectionLabel,
getProjectColumnLayoutStorageKey,
isGridEditorControlTarget,
getBlockSortValue,
getCellKind,
getCircuitValue,
getDeviceValue,
parseStoredColumnLayout,
parseNumeric,
} from "../src/frontend/utils/circuit-grid-model.js";
import type { CircuitTreeCircuitDto, CircuitTreeDeviceRowDto } from "../src/frontend/types.js";
const device: CircuitTreeDeviceRowDto = {
id: "row-1",
sortOrder: 10,
name: "Technical light",
displayName: "Office light",
phaseType: "single_phase",
roomNumberSnapshot: "1.01",
roomNameSnapshot: "Office",
quantity: 2,
manualQuantity: 2,
powerPerUnit: 0.05,
simultaneityFactor: 0.8,
rowTotalPower: 0.08,
remark: "Device remark",
};
const circuit: CircuitTreeCircuitDto = {
id: "circuit-1",
circuitListId: "list-1",
sectionId: "section-1",
equipmentIdentifier: "-1F1",
displayName: "Lighting circuit",
sortOrder: 10,
protectionDevice: {
type: "LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
},
cableType: "NYM-J",
cableCrossSection: "1.5 mm²",
cableLength: 20,
voltage: 230,
controlRequirement: "DALI",
isReserve: false,
remark: "Circuit remark",
circuitTotalPower: 0.08,
circuitApparentPower: 0.08,
circuitCosPhi: 1,
deviceRows: [device],
downstreamBoardLoads: [],
};
describe("circuit grid model", () => {
it("does not route clicks from active editor controls back to the grid cell", () => {
const inputTarget = {
closest: (selector: string) =>
selector === "input, select, textarea" ? {} : null,
};
const cellTarget = { closest: () => null };
assert.equal(isGridEditorControlTarget(inputTarget), true);
assert.equal(isGridEditorControlTarget(cellTarget), false);
assert.equal(isGridEditorControlTarget(null), false);
});
it("keeps the equipment identifier locked as the first column", () => {
assert.equal(allColumns[0].key, "equipmentIdentifier");
assert.equal(allColumns[0].label, "SK-Nr.");
assert.equal(
allColumns[0].fullLabel,
"Stromkreisnummer / Betriebsmittelkennzeichen"
);
assert.equal(allColumns[0].locked, true);
});
it("stores valid column layouts under a project-specific key", () => {
assert.equal(
getProjectColumnLayoutStorageKey("project-1"),
"circuitTreeEditor.columnLayout.v3.project-1"
);
const layout = parseStoredColumnLayout(
JSON.stringify({
order: ["displayName", "equipmentIdentifier"],
visible: [
"displayName",
"equipmentIdentifier",
"unknown",
],
})
);
assert.ok(layout);
assert.equal(layout.order[0], "equipmentIdentifier");
assert.deepEqual(layout.visible, [
"displayName",
"equipmentIdentifier",
]);
assert.equal(parseStoredColumnLayout("{invalid"), null);
});
it("shows stable circuit sections with German labels", () => {
assert.equal(
getCircuitSectionLabel({
key: "lighting",
displayName: "Lighting",
category: "lighting",
}),
"Lighting"
);
assert.equal(
getCircuitSectionLabel({
key: "lighting",
displayName: "Lighting",
}),
"Licht"
);
assert.equal(
getCircuitSectionLabel({
key: "custom",
displayName: "Sonderbereich",
}),
"Sonderbereich"
);
});
it("maps shared fields to the correct level for every row shape", () => {
assert.equal(getCellKind("circuitCompact", "displayName"), "deviceField");
assert.equal(getCellKind("circuitCompact", "equipmentIdentifier"), "circuitField");
assert.equal(getCellKind("circuitSummary", "displayName"), "circuitField");
assert.equal(getCellKind("deviceRow", "displayName"), "deviceField");
assert.equal(getCellKind("reserveCircuit", "displayName"), "circuitField");
assert.equal(getCellKind("reserveCircuit", "remark"), "circuitField");
assert.equal(getCellKind("reserveCircuit", "powerPerUnit"), "deviceField");
assert.equal(getCellKind("placeholder", "displayName"), "deviceField");
assert.equal(getCellKind("deviceRow", "equipmentIdentifier"), "readonly");
assert.equal(getCellKind("circuitSummary", "controlRequirement"), "circuitField");
assert.equal(getCellKind("deviceRow", "controlRequirement"), "readonly");
assert.equal(getCellKind("circuitCompact", "rowTotalPower"), "computed");
assert.equal(getCellKind("circuitCompact", "protectionType"), "computed");
});
it("projects circuit and device values without mixing ownership", () => {
assert.equal(getDeviceValue(device, "roomSummary"), "1.01 Office");
assert.equal(getDeviceValue(device, "rowTotalPower"), 0.08);
assert.equal(getCircuitValue(circuit, "protectionSummary"), "LS · 16 A · B");
assert.equal(
getCircuitValue(
{
...circuit,
protectionDevice: {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
},
},
"protectionSummary"
),
"FI/LS · 16 A · B · Typ A · 30 mA"
);
assert.equal(getCircuitValue(circuit, "voltage"), 230);
assert.equal(getCircuitValue(circuit, "controlRequirement"), "DALI");
assert.equal(getCircuitValue(circuit, "cableSummary"), "NYM-J, 1.5 mm², 20 m");
});
it("formats displayed numeric values without changing their stored precision", () => {
assert.equal(formatValue(0.123456, "powerPerUnit"), "0,123");
assert.equal(formatValue(0.87654, "simultaneityFactor"), "0,88");
assert.equal(formatValue(230.4, "voltage"), "230");
assert.equal(formatValue(16, "protectionRatedCurrent"), "16");
assert.equal(formatValue("single_phase", "phaseType"), "1-phasig");
assert.equal(formatValue("three_phase", "phaseType"), "3-phasig");
assert.equal(formatCellValue(2, "quantity"), "2 St.");
assert.equal(formatCellValue(0.123456, "powerPerUnit"), "0,123 kW");
assert.equal(formatCellValue(1.25, "rowTotalPower"), "1,25 kW");
assert.equal(formatCellValue(undefined, "circuitTotalPower"), "-");
});
it("uses circuit display values for block sorting and falls back to the first device", () => {
assert.equal(getBlockSortValue(circuit, "displayName"), "Lighting circuit");
assert.equal(getBlockSortValue({ ...circuit, displayName: undefined }, "displayName"), "Office light");
assert.equal(getBlockSortValue(circuit, "quantity"), 2);
});
it("parses numeric drafts and rejects invalid values", () => {
assert.equal(parseNumeric("quantity", " 1500 "), 1500);
assert.equal(parseNumeric("powerPerUnit", " 1,25 "), 1.25);
assert.equal(parseNumeric("quantity", ""), undefined);
assert.throws(() => parseNumeric("quantity", "two"), /Ungültiger Zahlenwert/);
});
it("rejects a dot instead of silently misreading it as a thousands separator", () => {
// "1.500" typed with German thousands-separator intent (meaning 1500)
// must never silently become 1.5 — it must fail loudly instead.
assert.throws(
() => parseNumeric("cableLength", "1.500"),
/Dezimalstellen mit Komma eingeben/
);
assert.throws(
() => parseNumeric("powerPerUnit", "2.5"),
/Dezimalstellen mit Komma eingeben/
);
});
it("builds nullable circuit command patches from grid drafts", () => {
assert.deepEqual(buildCircuitEditPatch("voltage", ""), {
voltage: null,
});
assert.deepEqual(
buildCircuitEditPatch("controlRequirement", " DALI "),
{ controlRequirement: "DALI" }
);
assert.deepEqual(buildCircuitEditPatch("isReserve", "ja"), {
isReserve: true,
});
});
it("builds device command patches with explicit clearing semantics", () => {
assert.deepEqual(buildDeviceRowEditPatch("roomSummary", ""), {
roomNumberSnapshot: null,
roomNameSnapshot: null,
});
assert.deepEqual(
buildDeviceRowEditPatch("roomSummary", "1.01 Büro"),
{
roomNumberSnapshot: "1.01",
roomNameSnapshot: "Büro",
}
);
assert.deepEqual(buildDeviceRowEditPatch("technicalName", " Leuchte "), {
name: "Leuchte",
});
assert.throws(
() => buildDeviceRowEditPatch("quantity", ""),
/darf nicht leer sein/
);
});
});