Files
leistungsbilanz-ts/tests/circuit-group-numbering.test.ts
T
2026-07-31 07:58:42 +02:00

658 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
formatGroupedCircuitIdentifier,
formatGroupRcdIdentifier,
formatGroupUpstreamProtectionIdentifier,
getNextCircuitGroupNumber,
getNextGroupedCircuitIdentifier,
parseGroupedEquipmentIdentifier,
} from "../src/domain/services/circuit-group-numbering.js";
import "./circuit-group-structure-project-command.repository.test.js";
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
import "./circuit-group-renumber-project-command.repository.test.js";
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
import "./circuit-group-move-project-command.repository.test.js";
import "./circuit-group-subtree-snapshot.test.js";
import "./circuit-group-subtree-project-command.repository.test.js";
import {
buildCircuitGroupMovePlan,
buildCircuitGroupRenumberPlan,
buildCircuitGroupReorderAssignments,
buildNewCircuitGroupSnapshot,
canMoveCircuitToGroup,
canRenumberCircuitGroups,
canDeleteCircuitGroup,
renameCircuitGroupSnapshot,
} from "../src/frontend/utils/circuit-group-editing.js";
describe("circuit group numbering", () => {
it("builds the next group without filling gaps or renumbering existing groups", () => {
const sections = [
groupSection("lighting-1", "lighting", 1, 10),
groupSection("single-1", "single_phase", 1, 20),
groupSection("lighting-3", "lighting", 3, 15),
];
const snapshot = buildNewCircuitGroupSnapshot({
id: "lighting-4",
circuitListId: "list-1",
category: "lighting",
sections,
});
assert.deepEqual(snapshot, {
id: "lighting-4",
circuitListId: "list-1",
key: "lighting_4",
displayName: "Beleuchtung 4",
prefix: "-1F4.",
sortOrder: 17.5,
category: "lighting",
groupNumber: 4,
});
assert.equal(
renameCircuitGroupSnapshot(snapshot, " Beleuchtung Nord ").displayName,
"Beleuchtung Nord"
);
assert.equal(canDeleteCircuitGroup(sections[0]), true);
assert.equal(
canDeleteCircuitGroup({
...sections[0],
components: [
{
id: "component-1",
circuitListId: "list-1",
sectionId: sections[0].id,
equipmentIdentifier: "-1Q1.0",
name: "Gruppen-FI",
role: "group_residual_current_protection",
placement: "group",
sortOrder: 10,
},
],
}),
false
);
});
it("reorders complete group lists only between category peers", () => {
const sections = [
groupSection("lighting-1", "lighting", 1, 10),
groupSection("lighting-2", "lighting", 2, 15),
groupSection("single-1", "single_phase", 1, 20),
];
assert.deepEqual(
buildCircuitGroupReorderAssignments(
sections,
"lighting-1",
1
),
[
{
groupId: "lighting-1",
expectedSortOrder: 10,
targetSortOrder: 15,
},
{
groupId: "lighting-2",
expectedSortOrder: 15,
targetSortOrder: 10,
},
{
groupId: "single-1",
expectedSortOrder: 20,
targetSortOrder: 20,
},
]
);
assert.equal(
buildCircuitGroupReorderAssignments(
sections,
"lighting-1",
-1
),
null
);
assert.equal(
buildCircuitGroupReorderAssignments(
sections,
"single-1",
1
),
null
);
});
it("renumbers a complete category by group order and preserves circuit suffixes", () => {
const sections = [
{
...groupSection("lighting-3", "lighting", 3, 10),
prefix: "-1F3.",
circuits: [
{
id: "circuit-7",
circuitListId: "list-1",
sectionId: "lighting-3",
equipmentIdentifier: "-1F3.7",
sortOrder: 10,
isReserve: false,
circuitTotalPower: 0,
deviceRows: [],
},
],
components: [
{
id: "rcd-3",
circuitListId: "list-1",
sectionId: "lighting-3",
equipmentIdentifier: "-1Q3.0",
name: "Gruppen-FI",
role: "group_residual_current_protection" as const,
placement: "group" as const,
sortOrder: 10,
},
],
},
{
...groupSection("lighting-1", "lighting", 1, 20),
prefix: "-1F1.",
},
groupSection("single-1", "single_phase", 1, 30),
];
assert.deepEqual(buildCircuitGroupRenumberPlan(sections, "lighting"), {
groups: [
{
groupId: "lighting-3",
category: "lighting",
expectedGroupNumber: 3,
targetGroupNumber: 1,
expectedPrefix: "-1F3.",
targetPrefix: "-1F1.",
circuits: [
{
circuitId: "circuit-7",
expectedEquipmentIdentifier: "-1F3.7",
targetEquipmentIdentifier: "-1F1.7",
},
],
components: [
{
componentId: "rcd-3",
expectedEquipmentIdentifier: "-1Q3.0",
targetEquipmentIdentifier: "-1Q1.0",
},
],
},
{
groupId: "lighting-1",
category: "lighting",
expectedGroupNumber: 1,
targetGroupNumber: 2,
expectedPrefix: "-1F1.",
targetPrefix: "-1F2.",
circuits: [],
components: [],
},
],
});
assert.equal(
buildCircuitGroupRenumberPlan(
[
groupSection("lighting-1", "lighting", 1, 10),
groupSection("lighting-2", "lighting", 2, 20),
],
"lighting"
),
null
);
assert.equal(
canRenumberCircuitGroups(sections, "lighting"),
true
);
assert.equal(
canRenumberCircuitGroups(
[
groupSection("lighting-1", "lighting", 1, 10),
groupSection("lighting-2", "lighting", 2, 20),
],
"lighting"
),
false
);
});
it("plans a same-category circuit drop with the next target BMK", () => {
const source = {
...groupSection("lighting-1", "lighting", 1, 10),
circuits: [
{
id: "circuit-source",
circuitListId: "list-1",
sectionId: "lighting-1",
equipmentIdentifier: "-1F1.5",
sortOrder: 10,
isReserve: false,
circuitTotalPower: 0,
deviceRows: [],
},
],
};
const target = {
...groupSection("lighting-2", "lighting", 2, 20),
circuits: [
{
id: "circuit-target",
circuitListId: "list-1",
sectionId: "lighting-2",
equipmentIdentifier: "-1F2.3",
sortOrder: 20,
isReserve: false,
circuitTotalPower: 0,
deviceRows: [],
},
],
};
assert.equal(
canMoveCircuitToGroup(
[source, target],
"circuit-source",
"lighting-2"
),
true
);
assert.deepEqual(
buildCircuitGroupMovePlan({
sections: [source, target],
circuitId: "circuit-source",
targetSectionId: "lighting-2",
placement: {
kind: "before",
targetCircuitId: "circuit-target",
},
}),
{
circuitId: "circuit-source",
circuitListId: "list-1",
expectedSectionId: "lighting-1",
targetSectionId: "lighting-2",
expectedEquipmentIdentifier: "-1F1.5",
targetEquipmentIdentifier: "-1F2.4",
expectedSortOrder: 10,
targetSortOrder: 10,
}
);
assert.equal(
canMoveCircuitToGroup(
[
source,
groupSection("single-1", "single_phase", 1, 20),
],
"circuit-source",
"single-1"
),
false
);
});
it("formats the agreed identifiers including the leading hyphen", () => {
assert.equal(
formatGroupUpstreamProtectionIdentifier("lighting", 1),
"-1F1.0"
);
assert.equal(formatGroupRcdIdentifier("lighting", 1), "-1Q1.0");
assert.equal(
formatGroupedCircuitIdentifier("lighting", 1, 2),
"-1F1.2"
);
assert.equal(
formatGroupedCircuitIdentifier("single_phase", 3, 7),
"-2F3.7"
);
assert.equal(
formatGroupedCircuitIdentifier("three_phase", 2, 1),
"-3F2.1"
);
});
it("parses circuit and group component identifiers", () => {
assert.deepEqual(parseGroupedEquipmentIdentifier("-1F2.4"), {
category: "lighting",
groupNumber: 2,
kind: "circuit",
circuitNumber: 4,
});
assert.deepEqual(parseGroupedEquipmentIdentifier("-2F3.0"), {
category: "single_phase",
groupNumber: 3,
kind: "group_upstream_protection",
circuitNumber: null,
});
assert.deepEqual(parseGroupedEquipmentIdentifier("-3Q1.0"), {
category: "three_phase",
groupNumber: 1,
kind: "group_rcd",
circuitNumber: null,
});
});
it("rejects legacy, malformed and unsupported identifiers", () => {
for (const identifier of [
"-1F1",
"1F1.1",
"-1Q1.2",
"-4F1.1",
"-1F0.1",
"-1F1.01",
]) {
assert.equal(parseGroupedEquipmentIdentifier(identifier), null);
}
});
it("uses the highest target-group suffix plus one without filling gaps", () => {
assert.equal(
getNextGroupedCircuitIdentifier("lighting", 2, [
"-1F2.1",
"-1F2.3",
"-1F2.7",
"-1F1.12",
"-2F2.9",
"-1F2.0",
"-1Q2.0",
"-1F8",
]),
"-1F2.8"
);
});
it("uses the highest group number in the selected category plus one", () => {
assert.equal(
getNextCircuitGroupNumber("lighting", [
{ category: "lighting", groupNumber: 1 },
{ category: "lighting", groupNumber: 4 },
{ category: "single_phase", groupNumber: 9 },
]),
5
);
});
it("rejects non-positive group and circuit numbers", () => {
assert.throws(
() => formatGroupedCircuitIdentifier("lighting", 0, 1),
RangeError
);
assert.throws(
() => formatGroupedCircuitIdentifier("lighting", 1, 0),
RangeError
);
});
});
function groupSection(
id: string,
category: "lighting" | "single_phase" | "three_phase",
groupNumber: number,
sortOrder: number
) {
return {
id,
key: id,
displayName: id,
prefix: `-${category === "lighting" ? 1 : category === "single_phase" ? 2 : 3}F${groupNumber}.`,
sortOrder,
category,
groupNumber,
sectionTotalPower: 0,
components: [],
circuits: [],
};
}
describe("circuit group renumber planning", () => {
const groups = [
{
id: "lighting-1",
category: "lighting" as const,
groupNumber: 1,
prefix: "-1F1.",
circuits: [
{ id: "circuit-1", equipmentIdentifier: "-1F1.1" },
{ id: "circuit-2", equipmentIdentifier: "-1F1.7" },
],
components: [
{
id: "fuse-1",
role: "group_upstream_protection" as const,
equipmentIdentifier: "-1F1.0",
},
{
id: "rcd-1",
role: "group_residual_current_protection" as const,
equipmentIdentifier: "-1Q1.0",
},
],
},
{
id: "lighting-2",
category: "lighting" as const,
groupNumber: 2,
prefix: "-1F2.",
circuits: [
{ id: "circuit-3", equipmentIdentifier: "-1F2.4" },
],
components: [],
},
{
id: "single-1",
category: "single_phase" as const,
groupNumber: 1,
prefix: "-2F1.",
circuits: [],
components: [],
},
];
it("preserves circuit suffixes and maps optional group components", () => {
const plan = createCircuitGroupRenumberPlan(groups, [
{ groupId: "lighting-1", targetGroupNumber: 3 },
]);
assert.deepEqual(plan.groups[0], {
groupId: "lighting-1",
category: "lighting",
expectedGroupNumber: 1,
targetGroupNumber: 3,
expectedPrefix: "-1F1.",
targetPrefix: "-1F3.",
circuits: [
{
circuitId: "circuit-1",
expectedEquipmentIdentifier: "-1F1.1",
targetEquipmentIdentifier: "-1F3.1",
},
{
circuitId: "circuit-2",
expectedEquipmentIdentifier: "-1F1.7",
targetEquipmentIdentifier: "-1F3.7",
},
],
components: [
{
componentId: "fuse-1",
expectedEquipmentIdentifier: "-1F1.0",
targetEquipmentIdentifier: "-1F3.0",
},
{
componentId: "rcd-1",
expectedEquipmentIdentifier: "-1Q1.0",
targetEquipmentIdentifier: "-1Q3.0",
},
],
});
});
it("allows a number swap when every affected group participates", () => {
const plan = createCircuitGroupRenumberPlan(groups, [
{ groupId: "lighting-1", targetGroupNumber: 2 },
{ groupId: "lighting-2", targetGroupNumber: 1 },
]);
assert.deepEqual(
plan.groups.map((group) => [
group.expectedGroupNumber,
group.targetGroupNumber,
]),
[
[1, 2],
[2, 1],
]
);
});
it("rejects collisions, no-op requests and mismatched current BMKs", () => {
assert.throws(
() =>
createCircuitGroupRenumberPlan(groups, [
{ groupId: "lighting-1", targetGroupNumber: 2 },
]),
/unchanged group/
);
assert.throws(
() =>
createCircuitGroupRenumberPlan(groups, [
{ groupId: "lighting-1", targetGroupNumber: 1 },
]),
/must change/
);
assert.throws(
() =>
createCircuitGroupRenumberPlan(
[
{
...groups[0],
circuits: [
{
id: "circuit-1",
equipmentIdentifier: "-1F9.1",
},
],
},
],
[{ groupId: "lighting-1", targetGroupNumber: 3 }]
),
/Circuit BMK does not match/
);
});
});
describe("circuit group move planning", () => {
const sourceGroup = {
id: "lighting-1",
category: "lighting" as const,
groupNumber: 1,
prefix: "-1F1.",
};
const targetGroup = {
id: "lighting-2",
category: "lighting" as const,
groupNumber: 2,
prefix: "-1F2.",
};
it("uses the highest target suffix plus one and stores both positions", () => {
assert.deepEqual(
createCircuitGroupMovePlan({
circuit: {
id: "circuit-1",
circuitListId: "list-1",
sectionId: "lighting-1",
equipmentIdentifier: "-1F1.7",
sortOrder: 20,
},
sourceGroup,
targetGroup,
targetCircuitEquipmentIdentifiers: [
"-1F2.1",
"-1F2.4",
"-1F2.9",
],
targetSortOrder: 40,
}),
{
circuitId: "circuit-1",
circuitListId: "list-1",
expectedSectionId: "lighting-1",
targetSectionId: "lighting-2",
expectedEquipmentIdentifier: "-1F1.7",
targetEquipmentIdentifier: "-1F2.10",
expectedSortOrder: 20,
targetSortOrder: 40,
}
);
});
it("does not fill target gaps", () => {
assert.equal(
createCircuitGroupMovePlan({
circuit: {
id: "circuit-1",
circuitListId: "list-1",
sectionId: "lighting-1",
equipmentIdentifier: "-1F1.2",
sortOrder: 10,
},
sourceGroup,
targetGroup,
targetCircuitEquipmentIdentifiers: ["-1F2.1", "-1F2.3"],
targetSortOrder: 20,
}).targetEquipmentIdentifier,
"-1F2.4"
);
});
it("rejects cross-category, same-group and mismatched source moves", () => {
const base = {
circuit: {
id: "circuit-1",
circuitListId: "list-1",
sectionId: "lighting-1",
equipmentIdentifier: "-1F1.1",
sortOrder: 10,
},
sourceGroup,
targetGroup,
targetCircuitEquipmentIdentifiers: [] as string[],
targetSortOrder: 10,
};
assert.throws(
() =>
createCircuitGroupMovePlan({
...base,
targetGroup: {
id: "single-1",
category: "single_phase",
groupNumber: 1,
prefix: "-2F1.",
},
}),
/same category/
);
assert.throws(
() =>
createCircuitGroupMovePlan({
...base,
targetGroup: sourceGroup,
}),
/different groups/
);
assert.throws(
() =>
createCircuitGroupMovePlan({
...base,
circuit: {
...base.circuit,
equipmentIdentifier: "-1F9.1",
},
}),
/does not match/
);
});
});