import { describe, expect, it } from "vitest"; import { BedingungSchema, type Bedingung, bedingungErfuellt, bedingungLesen, bedingungText, geltendeTeile, umgebungBauen, zeigtNachVorne, } from "@/lib/bedingungen"; /** * Bedingte Abschnitte und Schritte: die reine Logik (#039). * * Ohne Datenbank, weil hier alles entschieden wird, was zählt — und weil die * Sonderfälle nur so vollständig durchgehen. Dass die Auswertung auch im Dienst, * in der Abarbeitung und im Druck greift, steht in `tests/bedingte-laeufe.test.ts`. * * **Die wichtigste Zusage dieses Meilensteins:** eine Bedingung, die sich nicht * entscheiden lässt, blendet **nichts** aus. Ein Abschnitt zu viel ist sichtbar * und korrigierbar; ein fehlender fällt niemandem auf, und das ist bei einer * Checkliste der teure Fall. */ const leer = umgebungBauen({}, []); describe("Die Form einer Bedingung", () => { it("nimmt die beiden Arten an", () => { expect( BedingungSchema.safeParse({ art: "variable", schluessel: "konto_typ", operator: "ist", wert: "AD" }) .success, ).toBe(true); expect( BedingungSchema.safeParse({ art: "schritt", schrittId: "s1", zustaende: ["ERLEDIGT"] }).success, ).toBe(true); }); it("verlangt bei „ist“ einen Wert", () => { /* * Ohne Wert wäre „gilt, wenn konto_typ gleich “ ist“ eine Bedingung, die * niemand lesen kann — und sie träfe auf einen leeren Wert zu, also genau * dann, wenn nichts angegeben wurde. Das ist nicht gemeint. */ for (const operator of ["ist", "istNicht"]) { expect( BedingungSchema.safeParse({ art: "variable", schluessel: "k", operator }).success, operator, ).toBe(false); expect( BedingungSchema.safeParse({ art: "variable", schluessel: "k", operator, wert: " " }).success, `${operator} mit Leerraum`, ).toBe(false); } // `gesetzt` und `leer` brauchen keinen. expect(BedingungSchema.safeParse({ art: "variable", schluessel: "k", operator: "gesetzt" }).success).toBe( true, ); }); it("verlangt mindestens einen Zustand", () => { // Eine leere Liste wäre eine Bedingung, die nie zutrifft — also ein // Abschnitt, der für immer unsichtbar bleibt. expect(BedingungSchema.safeParse({ art: "schritt", schrittId: "s1", zustaende: [] }).success).toBe(false); }); it("weist eine unbekannte Art und einen unbekannten Zustand ab", () => { expect(BedingungSchema.safeParse({ art: "geraet", typ: "PC" }).success).toBe(false); expect( BedingungSchema.safeParse({ art: "schritt", schrittId: "s1", zustaende: ["VIELLEICHT"] }).success, ).toBe(false); }); it("liest beim Lesen nachsichtig: was nicht passt, ist keine Bedingung", () => { /* * Hier zu werfen hieße, dass eine einzige unlesbare Zeile eine ganze Vorlage * unbenutzbar macht. Die Richtung, in die der Fehler dann fällt, ist die * harmlose: ohne Bedingung wird angezeigt. */ expect(bedingungLesen(null)).toBeNull(); expect(bedingungLesen(undefined)).toBeNull(); expect(bedingungLesen({})).toBeNull(); expect(bedingungLesen("Unsinn")).toBeNull(); expect(bedingungLesen({ art: "variable" })).toBeNull(); expect(bedingungLesen({ art: "variable", schluessel: "k", operator: "ist", wert: "AD" })).toEqual({ art: "variable", schluessel: "k", operator: "ist", wert: "AD", }); }); }); describe("Eine Bedingung auf einer Variable", () => { const umgebung = umgebungBauen({ konto_typ: "AD", standort: "", anzahl: 3, aktiv: true }, []); it("vergleicht ohne Rücksicht auf Groß- und Kleinschreibung und Ränder", () => { // Die Werte kommen aus einem Auswahlfeld oder einem Formular; „AD“ ist // derselbe Wert wie „ad“ und wie „ AD “. const bedingung = (wert: string): Bedingung => ({ art: "variable", schluessel: "konto_typ", operator: "ist", wert, }); expect(bedingungErfuellt(bedingung("AD"), umgebung)).toBe(true); expect(bedingungErfuellt(bedingung("ad"), umgebung)).toBe(true); expect(bedingungErfuellt(bedingung(" AD "), umgebung)).toBe(true); expect(bedingungErfuellt(bedingung("ENTRA"), umgebung)).toBe(false); }); it("liest Zahlen und Wahrheitswerte als Text", () => { // `Lauf.variablen` ist ein Json; eine ZAHL-Variable kommt als Zahl zurück. expect( bedingungErfuellt({ art: "variable", schluessel: "anzahl", operator: "ist", wert: "3" }, umgebung), ).toBe(true); expect( bedingungErfuellt({ art: "variable", schluessel: "aktiv", operator: "ist", wert: "true" }, umgebung), ).toBe(true); }); it("unterscheidet gesetzt von leer", () => { expect( bedingungErfuellt({ art: "variable", schluessel: "konto_typ", operator: "gesetzt" }, umgebung), ).toBe(true); // Ein leerer Text zählt als nicht gesetzt — so kommt ein unausgefülltes // Formularfeld an. expect( bedingungErfuellt({ art: "variable", schluessel: "standort", operator: "gesetzt" }, umgebung), ).toBe(false); expect(bedingungErfuellt({ art: "variable", schluessel: "standort", operator: "leer" }, umgebung)).toBe( true, ); expect(bedingungErfuellt({ art: "variable", schluessel: "gibtsnicht", operator: "leer" }, umgebung)).toBe( true, ); }); it("ist bei einer nicht gesetzten Variable nicht entscheidbar", () => { /* * **Die Zusage dieses Meilensteins.** Ein Lauf, bei dem niemand `konto_typ` * angegeben hat, soll beide Konto-Abschnitte zeigen: auszublenden hieße, * eine Antwort zu erfinden, die niemand gegeben hat. * * `undefined` und nicht `false` — der Unterschied entscheidet in * `geltendeTeile` darüber, ob ausgeblendet wird. */ expect( bedingungErfuellt({ art: "variable", schluessel: "gibtsnicht", operator: "ist", wert: "AD" }, leer), ).toBeUndefined(); expect( bedingungErfuellt( { art: "variable", schluessel: "standort", operator: "istNicht", wert: "Nord" }, umgebung, ), ).toBeUndefined(); }); it("kehrt istNicht wirklich um", () => { expect( bedingungErfuellt( { art: "variable", schluessel: "konto_typ", operator: "istNicht", wert: "ENTRA" }, umgebung, ), ).toBe(true); expect( bedingungErfuellt( { art: "variable", schluessel: "konto_typ", operator: "istNicht", wert: "AD" }, umgebung, ), ).toBe(false); }); }); describe("Eine Bedingung auf einem Schritt", () => { const umgebung = umgebungBauen({}, [ { vorlagenSchrittId: "s1", zustand: "ERLEDIGT" }, { vorlagenSchrittId: "s2", zustand: "OFFEN" }, { vorlagenSchrittId: "s3", zustand: "NICHT_ZUTREFFEND" }, ]); it("trifft zu, wenn der Zustand in der Liste steht", () => { expect(bedingungErfuellt({ art: "schritt", schrittId: "s1", zustaende: ["ERLEDIGT"] }, umgebung)).toBe( true, ); expect( bedingungErfuellt( { art: "schritt", schrittId: "s3", zustaende: ["ERLEDIGT", "NICHT_ZUTREFFEND"] }, umgebung, ), ).toBe(true); expect(bedingungErfuellt({ art: "schritt", schrittId: "s2", zustaende: ["ERLEDIGT"] }, umgebung)).toBe( false, ); }); it("liest OFFEN als Antwort und nicht als Lücke", () => { /* * **Der Unterschied zur Variable**, und er ist wichtig: jedes * Schrittergebnis hat einen Zustand. `OFFEN` heißt „noch nicht erfüllt“ und * nicht „weiß nicht“. Ein Abschnitt, der erst nach einem erledigten Schritt * erscheint, ist der Sinn der Sache — wäre `OFFEN` nicht entscheidbar, wäre * er von Anfang an sichtbar. */ expect(bedingungErfuellt({ art: "schritt", schrittId: "s2", zustaende: ["ERLEDIGT"] }, umgebung)).toBe( false, ); expect(bedingungErfuellt({ art: "schritt", schrittId: "s2", zustaende: ["OFFEN"] }, umgebung)).toBe(true); }); it("ist bei einem Schritt, den es in diesem Lauf nicht gibt, nicht entscheidbar", () => { // Etwa ein Schritt, den #031 ausgefiltert hat, oder ein Verweis aus einer // anderen Version. Dann gibt es nichts zu entscheiden – und nichts // auszublenden. expect( bedingungErfuellt({ art: "schritt", schrittId: "gibtsnicht", zustaende: ["ERLEDIGT"] }, umgebung), ).toBeUndefined(); }); }); describe("Welche Teile gelten", () => { it("nimmt Teile ohne Bedingung immer", () => { expect( geltendeTeile( [ { id: "a", bedingung: null }, { id: "b", bedingung: {} }, ], leer, ), ).toEqual(["a", "b"]); }); it("blendet aus, was nachweislich nicht zutrifft", () => { const umgebung = umgebungBauen({ konto_typ: "AD" }, []); const teile = [ { id: "ad", bedingung: { art: "variable", schluessel: "konto_typ", operator: "ist", wert: "AD" } }, { id: "entra", bedingung: { art: "variable", schluessel: "konto_typ", operator: "ist", wert: "ENTRA" }, }, { id: "immer", bedingung: null }, ]; expect(geltendeTeile(teile, umgebung)).toEqual(["ad", "immer"]); }); it("blendet bei nicht entscheidbarer Bedingung nichts aus", () => { /* * Der echte Fall: ein Lauf ohne `konto_typ`. Beide Konto-Abschnitte * erscheinen, und jemand setzt den falschen auf NICHT_ZUTREFFEND — genau so, * wie es vor diesem Meilenstein für **jeden** Lauf war. Eine Verschlechterung * ist das also nicht; eine erfundene Antwort wäre eine. */ const teile = [ { id: "ad", bedingung: { art: "variable", schluessel: "konto_typ", operator: "ist", wert: "AD" } }, { id: "entra", bedingung: { art: "variable", schluessel: "konto_typ", operator: "ist", wert: "ENTRA" }, }, ]; expect(geltendeTeile(teile, leer)).toEqual(["ad", "entra"]); }); it("behält die Reihenfolge der Eingabe", () => { // Die Reihenfolge ist die der Version; sie darf hier nicht durcheinander // geraten, sonst stimmt die Abschnittsnummerierung nicht. const teile = [ { id: "eins", bedingung: null }, { id: "zwei", bedingung: { art: "variable", schluessel: "x", operator: "gesetzt" } }, { id: "drei", bedingung: null }, ]; expect(geltendeTeile(teile, umgebungBauen({ x: "ja" }, []))).toEqual(["eins", "zwei", "drei"]); }); }); describe("Hilfen für den Editor", () => { const reihenfolge = ["s1", "s2", "s3", "s4"]; it("erkennt eine Bedingung, die nach vorne zeigt", () => { /* * Fast immer ein Versehen: der bedingte Abschnitt wäre zunächst unsichtbar * und erschiene erst, nachdem jemand weiter unten etwas abgehakt hat — also * rückwärts. */ expect( zeigtNachVorne({ art: "schritt", schrittId: "s4", zustaende: ["ERLEDIGT"] }, reihenfolge, "s2"), ).toBe(true); expect( zeigtNachVorne({ art: "schritt", schrittId: "s1", zustaende: ["ERLEDIGT"] }, reihenfolge, "s2"), ).toBe(false); }); it("warnt nicht, wo es nichts zu warnen gibt", () => { expect( zeigtNachVorne({ art: "variable", schluessel: "k", operator: "gesetzt" }, reihenfolge, "s2"), ).toBeUndefined(); expect( zeigtNachVorne({ art: "schritt", schrittId: "fremd", zustaende: ["ERLEDIGT"] }, reihenfolge, "s2"), ).toBeUndefined(); }); it("schreibt eine Bedingung als Satz", () => { const namen = (kennung: string) => ({ konto_typ: "Kontotyp", s1: "Benutzer anlegen" })[kennung] ?? kennung; expect( bedingungText({ art: "variable", schluessel: "konto_typ", operator: "ist", wert: "AD" }, namen), ).toBe("gilt, wenn „Kontotyp“ gleich „AD“ ist"); expect(bedingungText({ art: "variable", schluessel: "konto_typ", operator: "leer" }, namen)).toBe( "gilt, wenn „Kontotyp“ leer ist", ); expect( bedingungText({ art: "schritt", schrittId: "s1", zustaende: ["ERLEDIGT", "NICHT_ZUTREFFEND"] }, namen), ).toBe("gilt, wenn „Benutzer anlegen“ erledigt oder nicht zutreffend ist"); }); }); describe("Die Umgebung", () => { it("verträgt ein Json, das kein Objekt ist", () => { // `Lauf.variablen` ist ein Json-Feld und kann alles enthalten. for (const roh of [null, undefined, "Text", 42, ["a"]]) { expect(umgebungBauen(roh, []).variablen).toEqual({}); } }); it("übergeht einen Zustand, den das Enum nicht kennt", () => { // Eine Zeile aus einer späteren Fassung soll die Auswertung nicht anhalten. const umgebung = umgebungBauen({}, [ { vorlagenSchrittId: "s1", zustand: "ERLEDIGT" }, { vorlagenSchrittId: "s2", zustand: "WARTET_AUF_AKTION" }, ]); expect(umgebung.zustaende.get("s1")).toBe("ERLEDIGT"); expect(umgebung.zustaende.has("s2")).toBe(false); }); });