Files
FireAdministration/tests/unit/wartung-bewertung.test.ts
T
Florian WildeandClaude Opus 5 e503aafc18 Phase 2: Wartungs- und Pruefmanagement
Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf,
Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche
Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung.

Elf neue Tabellen, alle mit organisationId und Row Level Security. Die
Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger
durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung
mit Verweis auf die alte, beide bleiben lesbar.

79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 12:07:11 +02:00

211 lines
6.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
beanstandetePunkte,
beantwortet,
dauerInMinuten,
ergebnisVorschlag,
geraetStatusNachPruefung,
messwertInOrdnung,
offenePflichtpunkte,
punktInOrdnung,
type Antwort,
type Punkt,
} from "@/lib/wartung/bewertung";
/**
* Bewertung einer Prüfung.
*
* Der Kern ist eine einzige fachliche Regel: ein durchgefallener Pflichtpunkt
* bedeutet "nicht bestanden", ein durchgefallener Kann-Punkt "bestanden mit
* Mängeln". Daran hängt, ob ein Gerät im Einsatz sein darf — deshalb steht das
* hier so ausführlich unter Test.
*/
const SICHTPRUEFUNG: Punkt = {
id: "p1",
text: "Trageeinrichtung ohne Beschädigung",
typ: "JA_NEIN",
pflicht: true,
};
const WARNDRUCK: Punkt = {
id: "p2",
text: "Ansprechdruck der Warneinrichtung",
typ: "MESSWERT",
pflicht: true,
minWert: 50,
maxWert: 60,
};
const BEUTEL: Punkt = {
id: "p3",
text: "Leinenbeutel unbeschädigt",
typ: "JA_NEIN",
pflicht: false,
};
const BEFUND: Punkt = {
id: "p4",
text: "Auffälligkeiten",
typ: "TEXT",
pflicht: false,
};
const FOTO: Punkt = { id: "p5", text: "Foto nach der Reinigung", typ: "FOTO", pflicht: false };
function antwort(punktId: string, teil: Partial<Antwort> = {}): Antwort {
return { punktId, ...teil };
}
describe("messwertInOrdnung", () => {
it("prüft gegen beide Grenzen", () => {
expect(messwertInOrdnung(55, 50, 60)).toBe(true);
expect(messwertInOrdnung(50, 50, 60)).toBe(true);
expect(messwertInOrdnung(60, 50, 60)).toBe(true);
expect(messwertInOrdnung(49.9, 50, 60)).toBe(false);
expect(messwertInOrdnung(61, 50, 60)).toBe(false);
});
it("behandelt eine fehlende Grenze als offen", () => {
expect(messwertInOrdnung(300, 270, null)).toBe(true);
expect(messwertInOrdnung(0.2, null, 0.5)).toBe(true);
expect(messwertInOrdnung(0.8, null, 0.5)).toBe(false);
});
it("wertet einen fehlenden Wert nicht als in Ordnung", () => {
expect(messwertInOrdnung(null, 50, 60)).toBe(false);
expect(messwertInOrdnung(undefined)).toBe(false);
expect(messwertInOrdnung(Number.NaN, 0, 10)).toBe(false);
});
});
describe("beantwortet", () => {
it("erkennt jede Antwortart", () => {
expect(beantwortet(SICHTPRUEFUNG, antwort("p1", { jaNein: false }))).toBe(true);
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: 0 }))).toBe(true);
expect(beantwortet(BEFUND, antwort("p4", { text: "ohne Befund" }))).toBe(true);
expect(beantwortet(FOTO, antwort("p5", { fotoDateiId: "abc" }))).toBe(true);
});
it("erkennt fehlende Antworten", () => {
expect(beantwortet(SICHTPRUEFUNG, undefined)).toBe(false);
expect(beantwortet(SICHTPRUEFUNG, antwort("p1"))).toBe(false);
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: null }))).toBe(false);
expect(beantwortet(BEFUND, antwort("p4", { text: " " }))).toBe(false);
});
it("wertet die Null als Antwort — sie ist ein Messwert wie jeder andere", () => {
expect(beantwortet(WARNDRUCK, antwort("p2", { zahlwert: 0 }))).toBe(true);
});
});
describe("punktInOrdnung", () => {
it("folgt bei Ja/Nein der Antwort", () => {
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: true }))).toBe(true);
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: false }))).toBe(false);
});
it("rechnet bei Messwerten den Bereich aus", () => {
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 55 }))).toBe(true);
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 45 }))).toBe(false);
});
it("lässt die ausdrückliche Angabe des Prüfers gewinnen", () => {
// Wert in der Toleranz, der Prüfer beanstandet trotzdem.
expect(punktInOrdnung(WARNDRUCK, antwort("p2", { zahlwert: 55, inOrdnung: false }))).toBe(
false,
);
// Und umgekehrt.
expect(punktInOrdnung(SICHTPRUEFUNG, antwort("p1", { jaNein: false, inOrdnung: true }))).toBe(
true,
);
});
});
describe("offenePflichtpunkte", () => {
it("nennt nur unbeantwortete Pflichtpunkte", () => {
const offen = offenePflichtpunkte(
[SICHTPRUEFUNG, WARNDRUCK, BEUTEL],
[antwort("p1", { jaNein: true })],
);
expect(offen.map((punkt) => punkt.id)).toEqual(["p2"]);
});
it("ist leer, wenn alle Pflichtpunkte beantwortet sind", () => {
const offen = offenePflichtpunkte(
[SICHTPRUEFUNG, WARNDRUCK, BEUTEL],
[antwort("p1", { jaNein: true }), antwort("p2", { zahlwert: 55 })],
);
expect(offen).toHaveLength(0);
});
});
describe("ergebnisVorschlag", () => {
const punkte = [SICHTPRUEFUNG, WARNDRUCK, BEUTEL, BEFUND];
it("bestanden, wenn nichts beanstandet ist", () => {
const antworten = [
antwort("p1", { jaNein: true }),
antwort("p2", { zahlwert: 55 }),
antwort("p3", { jaNein: true }),
];
expect(ergebnisVorschlag(punkte, antworten)).toBe("BESTANDEN");
});
it("mit Mängeln, wenn nur ein Kann-Punkt durchfällt", () => {
const antworten = [
antwort("p1", { jaNein: true }),
antwort("p2", { zahlwert: 55 }),
antwort("p3", { jaNein: false }),
];
expect(ergebnisVorschlag(punkte, antworten)).toBe("BESTANDEN_MIT_MAENGELN");
expect(beanstandetePunkte(punkte, antworten).map((punkt) => punkt.id)).toEqual(["p3"]);
});
it("nicht bestanden, sobald ein Pflichtpunkt durchfällt", () => {
const antworten = [
antwort("p1", { jaNein: true }),
// Messwert außerhalb der Toleranz — Pflichtpunkt.
antwort("p2", { zahlwert: 42 }),
antwort("p3", { jaNein: false }),
];
expect(ergebnisVorschlag(punkte, antworten)).toBe("NICHT_BESTANDEN");
});
it("wertet unbeantwortete Punkte nicht als Beanstandung", () => {
// Sonst wäre jede halb ausgefüllte Prüfung sofort "nicht bestanden".
expect(ergebnisVorschlag(punkte, [antwort("p1", { jaNein: true })])).toBe("BESTANDEN");
});
});
describe("geraetStatusNachPruefung", () => {
it("sperrt nur bei nicht bestanden", () => {
expect(geraetStatusNachPruefung("NICHT_BESTANDEN")).toBe("GESPERRT");
expect(geraetStatusNachPruefung("BESTANDEN_MIT_MAENGELN")).toBe("EINSATZBEREIT");
expect(geraetStatusNachPruefung("BESTANDEN")).toBe("EINSATZBEREIT");
});
});
describe("dauerInMinuten", () => {
it("rundet auf volle Minuten", () => {
const beginn = new Date("2026-06-15T09:00:00.000Z");
const ende = new Date("2026-06-15T09:24:40.000Z");
expect(dauerInMinuten(beginn, ende)).toBe(25);
});
it("gibt ohne Beginn nichts zurück", () => {
expect(dauerInMinuten(null, new Date())).toBeNull();
});
it("liefert keine negative Dauer, wenn die Uhr gesprungen ist", () => {
const beginn = new Date("2026-06-15T10:00:00.000Z");
const ende = new Date("2026-06-15T09:00:00.000Z");
expect(dauerInMinuten(beginn, ende)).toBeNull();
});
});