import { describe, expect, it } from "vitest"; import { BEISPIEL_PASSWORT, SCHLUESSEL_REGEL, abschnittNummer, alsEditorAufbau, alsStruktur, beispielwerte, doppelteSchluessel, konfigFuerTyp, mitBeispielwerten, neueKennung, schluesselFehler, titelOhneNummer, umsortieren, verwendetePlatzhalter, type EditorAufbau, type EditorVariable, bausteineAnreichern, neuerBausteinAbschnitt, } from "@/lib/editor-aufbau"; import { EINGEBAUTE_PLATZHALTER } from "@/lib/platzhalter"; import { StrukturSchema } from "@/lib/vorlagen-format"; /** Die reine Logik des Vorlagen-Editors (#010). Fuer Komponenten gibt es keine DOM-Umgebung. */ function variable(teil: Partial = {}): EditorVariable { return { kennung: neueKennung("variable"), schluessel: "vorname", beschriftung: "Vorname", typ: "TEXT", optionen: [], pflicht: false, ...teil, }; } describe("umsortieren", () => { it("schiebt einen Eintrag nach unten und laesst die Vorlage unberuehrt", () => { const liste = ["a", "b", "c"]; expect(umsortieren(liste, 0, 2)).toEqual(["b", "c", "a"]); expect(liste).toEqual(["a", "b", "c"]); }); it("schiebt einen Eintrag nach oben", () => { expect(umsortieren(["a", "b", "c"], 2, 1)).toEqual(["a", "c", "b"]); }); it("gibt bei gleicher Position und ausserhalb der Liste dieselbe Liste zurueck", () => { const liste = ["a", "b"]; expect(umsortieren(liste, 1, 1)).toBe(liste); expect(umsortieren(liste, 0, 2)).toBe(liste); expect(umsortieren(liste, -1, 0)).toBe(liste); }); }); describe("Abschnittsnummer", () => { it("nummeriert zweistellig ab eins", () => { expect(abschnittNummer(0)).toBe("01"); expect(abschnittNummer(9)).toBe("10"); }); it("entfernt eine Nummer, die mit Mittelpunkt im Titel steht", () => { expect(titelOhneNummer("01 · Vorbereitung")).toBe("Vorbereitung"); expect(titelOhneNummer("2·Konto")).toBe("Konto"); }); it("laesst eine Servicezeit in Ruhe — „24 · 7“ ist keine Nummerierung", () => { // Gefunden im Review: die Regel griff auch, wenn hinter dem Mittelpunkt eine // Ziffer stand. Aus „24 · 7 Bereitschaft“ wurde „7 Bereitschaft“, und beim // naechsten Speichern stand das dauerhaft in der Datenbank. expect(titelOhneNummer("24 · 7 Bereitschaft")).toBe("24 · 7 Bereitschaft"); expect(titelOhneNummer("08 · 17 Servicezeit")).toBe("08 · 17 Servicezeit"); }); it("laesst jeden anderen Trenner in Ruhe — „2. Etage“ ist eine Etage", () => { expect(titelOhneNummer("2. Etage prüfen")).toBe("2. Etage prüfen"); expect(titelOhneNummer("02) Konto")).toBe("02) Konto"); expect(titelOhneNummer("3 - Abschluss")).toBe("3 - Abschluss"); expect(titelOhneNummer("10 Jahre Aufbewahrung")).toBe("10 Jahre Aufbewahrung"); expect(titelOhneNummer("Vorbereitung")).toBe("Vorbereitung"); }); }); describe("konfig beim Typwechsel", () => { it("laesst fallen, was dem alten Typ gehoert", () => { const konfig = konfigFuerTyp({ shell: "bash", befehl: "echo {{hostname}}" }, "HAKEN"); expect(konfig).toEqual({}); }); it("behaelt, was der neue Typ braucht", () => { expect(konfigFuerTyp({ optionen: ["a", "b"], min: 1 }, "AUSWAHL")).toEqual({ optionen: ["a", "b"], }); expect(konfigFuerTyp({ min: 1, max: 9, einheit: "GB", mehrzeilig: true }, "ZAHL")).toEqual({ min: 1, max: 9, einheit: "GB", }); }); it("behaelt unbekannte Schluessel, damit ein Import aus einer spaeteren Fassung nichts verliert", () => { expect(konfigFuerTyp({ befehl: "echo", aktionsname: "ad-anlegen" }, "HAKEN")).toEqual({ aktionsname: "ad-anlegen", }); }); }); describe("Zustand und Struktur", () => { const struktur = StrukturSchema.parse({ variablen: [ { schluessel: "vorname", beschriftung: "Vorname", pflicht: true }, { schluessel: "raum", beschriftung: "Raum", typ: "AUSWAHL", optionen: ["EG", "OG"] }, ], abschnitte: [ { titel: "01 · Vorbereitung", schritte: [ { titel: "Gerät auspacken", pflicht: true }, { titel: "Konto anlegen", typ: "BEFEHL", konfig: { shell: "powershell", befehl: "New-ADUser {{vorname}}" }, dokuUrl: "https://intern.example/ad", }, ], }, ], }); it("uebernimmt die Struktur und gibt sie unveraendert zurueck — ohne die Nummer im Titel", () => { const aufbau = alsEditorAufbau(struktur); expect(aufbau.abschnitte[0]?.titel).toBe("Vorbereitung"); expect(aufbau.abschnitte[0]?.schritte).toHaveLength(2); const zurueck = alsStruktur(aufbau); expect(zurueck.abschnitte[0]?.titel).toBe("Vorbereitung"); expect(zurueck.abschnitte[0]?.schritte[1]?.konfig).toEqual({ shell: "powershell", befehl: "New-ADUser {{vorname}}", }); // Das Schema muss den Rueckweg annehmen, sonst scheitert erst das Speichern. expect(StrukturSchema.safeParse(zurueck).success).toBe(true); }); it("laesst leere Textfelder weg, statt sie als leere Zeichenkette zu schicken", () => { const aufbau: EditorAufbau = { variablen: [], abschnitte: [ { kennung: "a", titel: "Vorbereitung", schritte: [ { kennung: "s", titel: "Gerät auspacken", hilfetext: " ", typ: "HAKEN", pflicht: false, konfig: {}, dokuUrl: " https://intern.example/x ", }, ], }, ], }; const schritt = alsStruktur(aufbau).abschnitte[0]?.schritte[0]; expect(schritt?.hilfetext).toBeUndefined(); expect(schritt?.dokuUrl).toBe("https://intern.example/x"); }); it("schickt Optionen nur fuer eine Auswahl-Variable mit", () => { const aufbau: EditorAufbau = { variablen: [variable({ typ: "TEXT", optionen: ["alt", "vergessen"] })], abschnitte: [], }; expect(alsStruktur(aufbau).variablen[0]?.optionen).toEqual([]); }); it("findet die verwendeten Platzhalter", () => { expect([...verwendetePlatzhalter(alsEditorAufbau(struktur))]).toEqual(["vorname"]); }); }); describe("Variablenschluessel", () => { it("nimmt gueltige Schluessel an", () => { for (const schluessel of ["vorname", "ad_gruppe", "raum2"]) { expect(schluesselFehler(schluessel), schluessel).toBeUndefined(); } }); it("lehnt ab, was der Platzhalter nicht wiedererkennen koennte", () => { for (const schluessel of ["Vorname", "mit-strich", "2te_wahl", ""]) { expect(schluesselFehler(schluessel), schluessel).toBeDefined(); } }); it("nennt die Regel als Satz, damit der Hinweis am Feld stehen kann", () => { expect(SCHLUESSEL_REGEL).toContain("Kleinbuchstaben"); }); it("findet doppelte Schluessel und ignoriert leere", () => { expect( doppelteSchluessel([ variable({ schluessel: "vorname" }), variable({ schluessel: "vorname" }), variable({ schluessel: "" }), variable({ schluessel: "" }), ]), ).toEqual(["vorname"]); }); }); describe("Beispielwerte der Vorschau", () => { const werte = beispielwerte([], "24.09.2026"); it("hat fuer jeden eingebauten Platzhalter einen Wert", () => { for (const platzhalter of EINGEBAUTE_PLATZHALTER) { expect(werte[platzhalter.schluessel], platzhalter.schluessel).toBeTruthy(); } }); it("erzeugt fuer das generierte Passwort nie ein echtes, sondern immer denselben Beispieltext", () => { expect(werte["generiertes_passwort"]).toBe(BEISPIEL_PASSWORT); expect(beispielwerte([], "24.09.2026")["generiertes_passwort"]).toBe(BEISPIEL_PASSWORT); }); it("nimmt das Datum von aussen, damit Server und Browser dasselbe rendern", () => { expect(werte["datum"]).toBe("24.09.2026"); }); it("zieht fuer einen Schluessel, den es als Variable gibt, dessen Beispielwert vor", () => { const eigene = beispielwerte( [variable({ schluessel: "abteilung", typ: "AUSWAHL", optionen: ["IT", "Einkauf"] })], "24.09.2026", ); expect(eigene["abteilung"]).toBe("IT"); }); it("ersetzt Platzhalter auch mit Leerzeichen in den Klammern", () => { expect(mitBeispielwerten("Hallo {{ vorname }} ({{sam}})", werte)).toBe("Hallo Anna (a.beispiel)"); }); it("laesst einen unbekannten Platzhalter stehen, so wie es der Lauf auch tut", () => { expect(mitBeispielwerten("{{gibtsnicht}}", werte)).toBe("{{gibtsnicht}}"); }); it("setzt einen Wert mit $& woertlich ein", () => { // Gefunden im Review: `String.replace` deutet im Ersatztext `$&`, `` $` ``, // `$'` und `$1` als Rueckverweise. Ein Auswahlwert „Kosten $& Nutzen“ setzte // den gefundenen Platzhalter wieder ein. In #012 baut derselbe Weg einen // PowerShell-Befehl zusammen — dort waere es ein verfaelschter Befehl. for (const wert of ["Kosten $& Nutzen", "vor $` danach", "A $' B", "Gruppe $1"]) { expect(mitBeispielwerten("Raum: {{raum}}", { raum: wert }), wert).toBe(`Raum: ${wert}`); } }); }); describe("Bausteine im Editor-Aufbau", () => { const BAUSTEIN = { bausteinVersionId: "bv-1", name: "Datensicherung prüfen", titel: "Datensicherung", version: 3, anzahlSchritte: 2, }; it("schickt die Platzhalter-Schritte eines Baustein-Abschnitts nicht mit", () => { /* * Der Fehler, den erst das Durchklicken des gebauten Stands gezeigt hat * (#030): die Schritte im Zustand sind nur da, damit die Anzahl in der Karte * stimmt — sie haben keinen Titel. Mitgeschickt wies `SchrittSchema` den * ganzen Entwurf ab, und das Speichern scheiterte. */ const abschnitt = neuerBausteinAbschnitt(BAUSTEIN); expect(abschnitt.schritte).toHaveLength(2); expect(abschnitt.schritte.every((schritt) => schritt.titel === "")).toBe(true); const struktur = alsStruktur({ variablen: [], abschnitte: [abschnitt] }); expect(struktur.abschnitte[0]?.schritte).toEqual([]); expect(struktur.abschnitte[0]?.bausteinVersionId).toBe("bv-1"); // Und das Ergebnis muss durch das Schema gehen – genau das tat es vorher nicht. expect(() => StrukturSchema.parse(struktur)).not.toThrow(); }); it("nimmt den Verweis mit und gibt ihn zurück", () => { const struktur = StrukturSchema.parse({ variablen: [], abschnitte: [ { titel: "Eigen", schritte: [{ titel: "Eins" }] }, { titel: "x", bausteinVersionId: "bv-1", schritte: [] }, ], }); const aufbau = alsEditorAufbau(struktur); expect(aufbau.abschnitte[0]?.baustein).toBeUndefined(); expect(aufbau.abschnitte[1]?.baustein?.versionId).toBe("bv-1"); expect(alsStruktur(aufbau).abschnitte[1]?.bausteinVersionId).toBe("bv-1"); }); it("trägt Name und Fassung nach und benennt einen Verweis, der nicht mehr gilt", () => { const struktur = StrukturSchema.parse({ variablen: [], abschnitte: [ { titel: "a", bausteinVersionId: "bv-1", schritte: [] }, { titel: "b", bausteinVersionId: "bv-weg", schritte: [] }, ], }); const aufbau = bausteineAnreichern(alsEditorAufbau(struktur), [BAUSTEIN]); expect(aufbau.abschnitte[0]?.baustein).toEqual({ versionId: "bv-1", name: "Datensicherung prüfen", version: 3, }); // Ein Verweis, dessen Baustein nicht mehr einsetzbar ist, behält seine // Kennung – verschwinden darf er nicht, sonst wäre er beim nächsten // Speichern still weg. expect(aufbau.abschnitte[1]?.baustein).toEqual({ versionId: "bv-weg", name: "(nicht mehr verfügbar)", version: 0, }); expect(alsStruktur(aufbau).abschnitte[1]?.bausteinVersionId).toBe("bv-weg"); }); });