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>
271 lines
7.9 KiB
TypeScript
271 lines
7.9 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { describe, it } from "node:test";
|
||
import {
|
||
buildCostGroupFilterOptions,
|
||
buildFloorFilterOptions,
|
||
buildSupplyTypeFilterOptions,
|
||
emptyDistributionBoardFilters,
|
||
emptyProjectDeviceFilters,
|
||
filterDistributionBoards,
|
||
filterProjectDevices,
|
||
isDistributionBoardFilterActive,
|
||
isProjectDeviceFilterActive,
|
||
retainAvailableFilterValues,
|
||
toggleFilterValue,
|
||
withoutCostGroupFilterId,
|
||
withoutFloorFilterId,
|
||
withoutSupplyTypeFilterId,
|
||
} from "../src/frontend/utils/overview-filters.js";
|
||
import type {
|
||
DistributionBoardDto,
|
||
FloorDto,
|
||
ProjectDeviceDto,
|
||
} from "../src/frontend/types.js";
|
||
|
||
type TestBoard = Pick<
|
||
DistributionBoardDto,
|
||
"id" | "name" | "floorId" | "supplyType"
|
||
>;
|
||
|
||
function board(
|
||
id: string,
|
||
name: string,
|
||
floorId: string | null,
|
||
supplyType: DistributionBoardDto["supplyType"]
|
||
): TestBoard {
|
||
return { id, name, floorId, supplyType };
|
||
}
|
||
|
||
function device(
|
||
id: string,
|
||
overrides: Partial<ProjectDeviceDto>
|
||
): ProjectDeviceDto {
|
||
return {
|
||
id,
|
||
projectId: "project-1",
|
||
name: `device-${id}`,
|
||
displayName: `Gerät ${id}`,
|
||
phaseType: "single_phase",
|
||
connectionKind: null,
|
||
costGroup: null,
|
||
category: "single_phase",
|
||
quantity: 1,
|
||
powerPerUnit: 1,
|
||
simultaneityFactor: 1,
|
||
totalPower: 1,
|
||
cosPhi: null,
|
||
remark: null,
|
||
voltageV: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
const floors: FloorDto[] = [
|
||
{ id: "floor-og", projectId: "project-1", name: "1. OG", sortOrder: 2, buildingId: null },
|
||
{ id: "floor-eg", projectId: "project-1", name: "EG", sortOrder: 1, buildingId: null },
|
||
];
|
||
const floorNameById = new Map(floors.map((floor) => [floor.id, floor.name]));
|
||
|
||
const boards = [
|
||
board("b1", "UV-EG-01", "floor-eg", "AV"),
|
||
board("b2", "UV-OG-01", "floor-og", "SV"),
|
||
board("b3", "HV", null, "AV"),
|
||
board("b4", "SiBe-Zentrale", null, null),
|
||
];
|
||
|
||
describe("overview filters: distribution boards", () => {
|
||
it("returns every board without an active filter", () => {
|
||
assert.equal(isDistributionBoardFilterActive(emptyDistributionBoardFilters), false);
|
||
assert.deepEqual(
|
||
filterDistributionBoards(boards, emptyDistributionBoardFilters, floorNameById),
|
||
boards
|
||
);
|
||
});
|
||
|
||
it("also searches board number and location when the boards carry them", () => {
|
||
const withInfo = [
|
||
{ ...boards[0]!, boardNumber: "UV 0.1", location: "Raum 003 Technik" },
|
||
{ ...boards[1]!, boardNumber: null, location: null },
|
||
];
|
||
assert.deepEqual(
|
||
filterDistributionBoards(
|
||
withInfo,
|
||
{ ...emptyDistributionBoardFilters, query: "uv 0.1" },
|
||
floorNameById
|
||
).map((entry) => entry.id),
|
||
[withInfo[0]!.id]
|
||
);
|
||
assert.deepEqual(
|
||
filterDistributionBoards(
|
||
withInfo,
|
||
{ ...emptyDistributionBoardFilters, query: "technik" },
|
||
floorNameById
|
||
).map((entry) => entry.id),
|
||
[withInfo[0]!.id]
|
||
);
|
||
});
|
||
|
||
it("searches board and floor names case-insensitively and ignores blank queries", () => {
|
||
const byFloorName = filterDistributionBoards(
|
||
boards,
|
||
{ ...emptyDistributionBoardFilters, query: " og " },
|
||
floorNameById
|
||
);
|
||
assert.deepEqual(byFloorName.map((entry) => entry.id), ["b2"]);
|
||
const byBoardName = filterDistributionBoards(
|
||
boards,
|
||
{ ...emptyDistributionBoardFilters, query: "sibe" },
|
||
floorNameById
|
||
);
|
||
assert.deepEqual(byBoardName.map((entry) => entry.id), ["b4"]);
|
||
assert.equal(
|
||
isDistributionBoardFilterActive({ ...emptyDistributionBoardFilters, query: " " }),
|
||
false
|
||
);
|
||
});
|
||
|
||
it("filters floors with the dedicated option for boards without floor", () => {
|
||
const result = filterDistributionBoards(
|
||
boards,
|
||
{ ...emptyDistributionBoardFilters, floorIds: ["floor-eg", withoutFloorFilterId] },
|
||
floorNameById
|
||
);
|
||
assert.deepEqual(result.map((entry) => entry.id), ["b1", "b3", "b4"]);
|
||
});
|
||
|
||
it("combines supply type with the other criteria", () => {
|
||
const filters = {
|
||
query: "",
|
||
floorIds: [withoutFloorFilterId],
|
||
supplyTypes: ["AV"],
|
||
};
|
||
assert.equal(isDistributionBoardFilterActive(filters), true);
|
||
assert.deepEqual(
|
||
filterDistributionBoards(boards, filters, floorNameById).map((entry) => entry.id),
|
||
["b3"]
|
||
);
|
||
assert.deepEqual(
|
||
filterDistributionBoards(
|
||
boards,
|
||
{ ...emptyDistributionBoardFilters, supplyTypes: [withoutSupplyTypeFilterId] },
|
||
floorNameById
|
||
).map((entry) => entry.id),
|
||
["b4"]
|
||
);
|
||
});
|
||
|
||
it("offers floors in configured order followed by 'Ohne Etage'", () => {
|
||
assert.deepEqual(buildFloorFilterOptions(floors), [
|
||
{ id: "floor-eg", label: "EG" },
|
||
{ id: "floor-og", label: "1. OG" },
|
||
{ id: withoutFloorFilterId, label: "Ohne Etage" },
|
||
]);
|
||
});
|
||
|
||
it("offers enabled and used supply types in catalog order", () => {
|
||
const options = buildSupplyTypeFilterOptions(
|
||
["AV", "SV", "EV", "USV"],
|
||
["USV", "AV"],
|
||
boards,
|
||
{ AV: "AV – Allgemein", SV: "SV – Sicherheit" }
|
||
);
|
||
assert.deepEqual(options, [
|
||
{ id: "AV", label: "AV – Allgemein" },
|
||
{ id: "SV", label: "SV – Sicherheit" },
|
||
{ id: "USV", label: "USV" },
|
||
{ id: withoutSupplyTypeFilterId, label: "Nicht festgelegt" },
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe("overview filters: project devices", () => {
|
||
const devices = [
|
||
device("d1", {
|
||
displayName: "Steckdose Büro",
|
||
costGroup: "444",
|
||
connectionKind: "Steckdose",
|
||
}),
|
||
device("d2", {
|
||
displayName: "Durchlauferhitzer",
|
||
phaseType: "three_phase",
|
||
category: "three_phase",
|
||
costGroup: " 445 ",
|
||
}),
|
||
device("d3", {
|
||
displayName: "Downlight",
|
||
category: "lighting",
|
||
costGroup: " ",
|
||
}),
|
||
device("d4", { displayName: "Kaffeemaschine", costGroup: null }),
|
||
];
|
||
|
||
it("searches names, connection kind, cost group and the German category label", () => {
|
||
assert.deepEqual(
|
||
filterProjectDevices(devices, { ...emptyProjectDeviceFilters, query: "STECK" }).map(
|
||
(entry) => entry.id
|
||
),
|
||
["d1"]
|
||
);
|
||
assert.deepEqual(
|
||
filterProjectDevices(
|
||
devices,
|
||
{ ...emptyProjectDeviceFilters, query: "beleucht" },
|
||
{ lighting: "Beleuchtung" }
|
||
).map((entry) => entry.id),
|
||
["d3"]
|
||
);
|
||
});
|
||
|
||
it("filters category, phase type and cost group together", () => {
|
||
const filters = {
|
||
query: "",
|
||
categories: ["single_phase", "three_phase"],
|
||
phaseTypes: ["three_phase"],
|
||
costGroups: ["445"],
|
||
};
|
||
assert.equal(isProjectDeviceFilterActive(filters), true);
|
||
assert.deepEqual(
|
||
filterProjectDevices(devices, filters).map((entry) => entry.id),
|
||
["d2"]
|
||
);
|
||
});
|
||
|
||
it("treats blank cost groups as 'Ohne Kostengruppe'", () => {
|
||
assert.deepEqual(
|
||
filterProjectDevices(devices, {
|
||
...emptyProjectDeviceFilters,
|
||
costGroups: [withoutCostGroupFilterId],
|
||
}).map((entry) => entry.id),
|
||
["d3", "d4"]
|
||
);
|
||
assert.deepEqual(buildCostGroupFilterOptions(devices), [
|
||
{ id: "444", label: "444" },
|
||
{ id: "445", label: "445" },
|
||
{ id: withoutCostGroupFilterId, label: "Ohne Kostengruppe" },
|
||
]);
|
||
});
|
||
|
||
it("returns an empty list when the filters hide every device", () => {
|
||
assert.deepEqual(
|
||
filterProjectDevices(devices, { ...emptyProjectDeviceFilters, query: "gibt es nicht" }),
|
||
[]
|
||
);
|
||
assert.equal(isProjectDeviceFilterActive(emptyProjectDeviceFilters), false);
|
||
});
|
||
});
|
||
|
||
describe("overview filters: selection helpers", () => {
|
||
it("toggles values without mutating the selection", () => {
|
||
const selected = ["a"];
|
||
assert.deepEqual(toggleFilterValue(selected, "b"), ["a", "b"]);
|
||
assert.deepEqual(toggleFilterValue(selected, "a"), []);
|
||
assert.deepEqual(selected, ["a"]);
|
||
});
|
||
|
||
it("drops selected values that are no longer offered", () => {
|
||
assert.deepEqual(
|
||
retainAvailableFilterValues(["floor-eg", "deleted-floor"], buildFloorFilterOptions(floors)),
|
||
["floor-eg"]
|
||
);
|
||
});
|
||
});
|