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")); }); });