feat(vorlagen): Editor, Liste und Versionsgeschichte (#24)
This commit was merged in pull request #24.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
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,
|
||||
} 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> = {}): 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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user