leistungsbilanz-ts/tests/circuit-grid-projection.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

380 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildVisibleGridRowsWithStructure,
filterAndSortCircuitSections,
getDistinctFilterValues,
} from "../src/frontend/utils/circuit-grid-projection.js";
import type {
CircuitTreeCircuitDto,
CircuitTreeDeviceRowDto,
CircuitTreeSectionDto,
} from "../src/frontend/types.js";
import "./circuit-structure-projection.test.js";
function makeDevice(
id: string,
displayName: string,
roomNumberSnapshot: string,
sortOrder: number
): CircuitTreeDeviceRowDto {
return {
id,
sortOrder,
name: displayName,
displayName,
roomNumberSnapshot,
quantity: 1,
manualQuantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
rowTotalPower: 0.1,
};
}
function makeCircuit(
id: string,
equipmentIdentifier: string,
displayName: string,
deviceRows: CircuitTreeDeviceRowDto[],
sortOrder: number
): CircuitTreeCircuitDto {
return {
id,
circuitListId: "list-1",
sectionId: "section-1",
equipmentIdentifier,
displayName,
sortOrder,
isReserve: deviceRows.length === 0,
circuitTotalPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
circuitApparentPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
circuitCosPhi: 1,
deviceRows,
downstreamBoardLoads: [],
};
}
const multiDeviceCircuit = makeCircuit(
"circuit-multi",
"-1F2",
"Office lighting",
[makeDevice("device-office", "Office light", "1.01", 10), makeDevice("device-store", "Store light", "1.02", 20)],
20
);
const compactCircuit = makeCircuit(
"circuit-compact",
"-1F1",
"Corridor lighting",
[makeDevice("device-corridor", "Corridor light", "1.03", 10)],
10
);
const reserveCircuit = makeCircuit("circuit-reserve", "-1F3", "Reserve", [], 30);
const sections: CircuitTreeSectionDto[] = [
{
id: "section-1",
key: "lighting",
displayName: "Lighting",
prefix: "-1F",
sortOrder: 10,
sectionTotalPower: 0.3,
components: [],
circuits: [compactCircuit, multiDeviceCircuit, reserveCircuit],
},
];
describe("circuit grid projection", () => {
it("keeps empty sections and their free placeholders when no filter is active", () => {
const emptySections: CircuitTreeSectionDto[] = [
{
id: "empty-lighting",
key: "lighting",
displayName: "Lighting",
prefix: "-1F",
sortOrder: 10,
sectionTotalPower: 0,
components: [],
circuits: [],
},
{
id: "empty-single-phase",
key: "single_phase",
displayName: "Single-phase circuits",
prefix: "-2F",
sortOrder: 20,
sectionTotalPower: 0,
components: [],
circuits: [],
},
];
const projectedSections = filterAndSortCircuitSections(emptySections, {}, null);
const rows = buildVisibleGridRowsWithStructure(projectedSections, { headerComponents: [], footerComponents: [] });
assert.equal(projectedSections.length, 2);
assert.deepEqual(
rows.map((row) => row.rowKey),
[
"section:empty-lighting",
"placeholder:empty-lighting",
"section:empty-single-phase",
"placeholder:empty-single-phase",
]
);
});
it("keeps every device row in a circuit block when one device matches a filter", () => {
const result = filterAndSortCircuitSections(sections, { roomNumberSnapshot: ["1.01"] }, null);
assert.deepEqual(result.map((section) => section.circuits.map((circuit) => circuit.id)), [["circuit-multi"]]);
assert.deepEqual(result[0].circuits[0].deviceRows.map((row) => row.id), ["device-office", "device-store"]);
});
it("combines column filters across the complete circuit block", () => {
const result = filterAndSortCircuitSections(
sections,
{ equipmentIdentifier: ["-1F2"], roomNumberSnapshot: ["1.02"] },
null
);
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), ["circuit-multi"]);
});
it("treats missing device values as missing without matching populated circuits", () => {
const result = filterAndSortCircuitSections(sections, { roomNumberSnapshot: ["-"] }, null);
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), ["circuit-reserve"]);
});
it("sorts complete circuit blocks without changing device row order", () => {
const result = filterAndSortCircuitSections(sections, {}, { key: "equipmentIdentifier", direction: "desc" });
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), [
"circuit-reserve",
"circuit-multi",
"circuit-compact",
]);
assert.deepEqual(result[0].circuits[1].deviceRows.map((row) => row.id), ["device-office", "device-store"]);
});
it("builds the compact, grouped, reserve and placeholder row shapes", () => {
const rows = buildVisibleGridRowsWithStructure(sections, { headerComponents: [], footerComponents: [] });
assert.deepEqual(rows.map((row) => row.rowType), [
"section",
"circuitCompact",
"circuitSummary",
"deviceRow",
"deviceRow",
"reserveCircuit",
"placeholder",
]);
assert.equal(rows.at(-1)?.rowKey, "placeholder:section-1");
assert.equal(
rows.at(-1)?.cells.find((cell) => cell.cellKey === "equipmentIdentifier")?.value,
"-frei-"
);
const circuitSummary = rows.find((row) => row.rowType === "circuitSummary");
assert.equal(
circuitSummary?.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
multiDeviceCircuit.circuitTotalPower
);
assert.equal(
circuitSummary?.cells.some((cell) => cell.cellKey === "circuitTotalPower"),
false
);
});
it("renders one linked downstream board as one compact editable circuit row", () => {
const feeder = {
...reserveCircuit,
displayName: undefined,
isReserve: false,
circuitTotalPower: 4,
circuitApparentPower: 5,
circuitCosPhi: 0.8,
downstreamBoardLoads: [{
distributionBoardId: "board-child",
name: "Unterverteilung",
boardNumber: "UV-02",
activePowerKw: 4,
apparentPowerKva: 5,
cosPhi: 0.8,
}],
};
const rows = buildVisibleGridRowsWithStructure(
[{ ...sections[0], circuits: [feeder] }],
{ headerComponents: [], footerComponents: [] }
);
assert.deepEqual(rows.map((row) => row.rowType), [
"section",
"circuitSummary",
"placeholder",
]);
const loadRow = rows[1];
assert.equal(
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.value,
"UV-02"
);
assert.equal(
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.editValue,
""
);
assert.equal(
loadRow.cells.find((cell) => cell.cellKey === "rowTotalPower")?.value,
4
);
assert.equal(
loadRow.cells.find((cell) => cell.cellKey === "displayName")?.kind,
"circuitField"
);
});
it("keeps separate read-only rows for several linked downstream boards", () => {
const feeder = {
...reserveCircuit,
isReserve: false,
circuitTotalPower: 6,
circuitApparentPower: 7,
circuitCosPhi: 6 / 7,
downstreamBoardLoads: [
{
distributionBoardId: "board-a",
name: "Unterverteilung A",
boardNumber: "UV-A",
activePowerKw: 4,
apparentPowerKva: 5,
cosPhi: 0.8,
},
{
distributionBoardId: "board-b",
name: "Unterverteilung B",
boardNumber: "UV-B",
activePowerKw: 2,
apparentPowerKva: 2,
cosPhi: 1,
},
],
};
const rows = buildVisibleGridRowsWithStructure(
[{ ...sections[0], circuits: [feeder] }],
{ headerComponents: [], footerComponents: [] }
);
assert.deepEqual(rows.map((row) => row.rowType), [
"section",
"circuitSummary",
"downstreamBoardLoad",
"downstreamBoardLoad",
"placeholder",
]);
assert.equal(rows[2].cells.every((cell) => !cell.editable), true);
});
it("shows circuit protection once on each visible circuit row", () => {
const protectedSections = sections.map((section) => ({
...section,
circuits: section.circuits.map((circuit) => ({
...circuit,
protectionDevice: {
type: "FI_LS" as const,
ratedCurrentA: 16,
tripCharacteristic: "B" as const,
rcdType: "A" as const,
ratedResidualCurrentMa: 30,
},
})),
}));
const rows = buildVisibleGridRowsWithStructure(protectedSections, { headerComponents: [], footerComponents: [] });
const protectionValue = (rowType: string) =>
rows
.find((row) => row.rowType === rowType)
?.cells.find((cell) => cell.cellKey === "protectionSummary")
?.value;
assert.equal(
protectionValue("circuitCompact"),
"FI/LS · 16 A · B · Typ A · 30 mA"
);
assert.equal(
protectionValue("circuitSummary"),
"FI/LS · 16 A · B · Typ A · 30 mA"
);
assert.equal(
protectionValue("reserveCircuit"),
"FI/LS · 16 A · B · Typ A · 30 mA"
);
assert.equal(protectionValue("deviceRow"), undefined);
});
it("normalizes structural component rows around complete circuit groups", () => {
const component = {
circuitListId: "list-1",
equipmentIdentifier: "-Q0",
name: "Hauptschalter",
role: "main_switch" as const,
placement: "header" as const,
sortOrder: 10,
};
const groupedSections = sections.map((section) => ({
...section,
components: [
{
...component,
id: "group-rcd",
sectionId: section.id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection" as const,
placement: "group" as const,
},
],
}));
const rows = buildVisibleGridRowsWithStructure(groupedSections, {
headerComponents: [{ ...component, id: "main" }],
footerComponents: [
{
...component,
id: "actor",
equipmentIdentifier: "-K1",
name: "KNX Aktor",
role: "auxiliary",
placement: "footer",
},
],
});
assert.deepEqual(
rows.map((row) => row.rowType),
[
"headerComponent",
"section",
"groupComponent",
"circuitCompact",
"circuitSummary",
"deviceRow",
"deviceRow",
"reserveCircuit",
"placeholder",
"footerComponent",
]
);
assert.equal(
rows.find((row) => row.rowType === "groupComponent")?.cells.some(
(cell) => cell.editable
),
false
);
});
it("collects filter options from both circuit and device values", () => {
const values = getDistinctFilterValues(sections, [
{ key: "displayName", label: "Display name" },
{ key: "roomNumberSnapshot", label: "Room number" },
]);
assert.ok(values.displayName.includes("Office lighting"));
assert.ok(values.displayName.includes("Store light"));
assert.ok(values.roomNumberSnapshot.includes("1.03"));
});
});