Files
leistungsbilanz-ts/tests/circuit-grid-projection.test.ts
T

287 lines
8.2 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildVisibleGridRows,
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,
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),
deviceRows,
};
}
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,
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,
circuits: [],
},
{
id: "empty-single-phase",
key: "single_phase",
displayName: "Single-phase circuits",
prefix: "-2F",
sortOrder: 20,
circuits: [],
},
];
const projectedSections = filterAndSortCircuitSections(emptySections, {}, null);
const rows = buildVisibleGridRows(projectedSections);
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 = buildVisibleGridRows(sections);
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("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 = buildVisibleGridRows(protectedSections);
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"));
});
});