import { describe, expect, it } from "vitest"; import { GERAET_FELDER, PERSON_FELDER, auswahlLesen, datumLesen, dezimalLesen, jaNeinLesen, normalisiereUeberschrift, spaltenZuordnen, wertLesen, zahlLesen, } from "@/lib/stammdaten/import-felder"; import { csvLesen, formatErkennen, textAusPuffer, vorschauErstellen } from "@/lib/stammdaten/import"; /** * Tests für den Import. * * Der Import ist die Stelle, an der eine Wehr das Vertrauen in die Software * gewinnt oder verliert: Wenn hier Spalten verrutschen, stehen später falsche * Seriennummern an falschen Geräten, und das merkt niemand, bis jemand am * Regal steht. * * Geprüft wird deshalb nicht nur der Normalfall, sondern vor allem das, was * in echten Tabellen tatsächlich vorkommt: deutsche Datumsformate, Semikolon * als Trennzeichen, Windows-1252, Excel-Seriennummern, Dubletten. */ describe("Datum lesen", () => { it("versteht das deutsche Format", () => { expect(datumLesen("02.09.2026").wert).toEqual(new Date(Date.UTC(2026, 8, 2, 12))); expect(datumLesen("2.9.2026").wert).toEqual(new Date(Date.UTC(2026, 8, 2, 12))); }); it("versteht zweistellige Jahre nach der üblichen Grenze", () => { expect(datumLesen("02.09.26").wert?.getUTCFullYear()).toBe(2026); expect(datumLesen("02.09.85").wert?.getUTCFullYear()).toBe(1985); }); it("versteht ISO", () => { expect(datumLesen("2026-09-02").wert).toEqual(new Date(Date.UTC(2026, 8, 2, 12))); }); it("versteht die Seriennummer, die Excel intern benutzt", () => { // 45000 Tage nach dem 30.12.1899 ist der 15.03.2023. const ergebnis = datumLesen("45000"); expect(ergebnis.ok).toBe(true); expect(ergebnis.wert?.getUTCFullYear()).toBe(2023); expect(ergebnis.wert?.getUTCMonth()).toBe(2); expect(ergebnis.wert?.getUTCDate()).toBe(15); }); it("weist Tage ab, die es nicht gibt", () => { expect(datumLesen("31.02.2026").ok).toBe(false); expect(datumLesen("32.01.2026").ok).toBe(false); expect(datumLesen("01.13.2026").ok).toBe(false); }); it("weist unplausible Jahre ab", () => { expect(datumLesen("01.01.1850").ok).toBe(false); expect(datumLesen("01.01.2200").ok).toBe(false); }); it("lässt leere Felder durch, ohne einen Wert zu setzen", () => { const ergebnis = datumLesen(" "); expect(ergebnis.ok).toBe(true); expect(ergebnis.wert).toBeUndefined(); }); it("meldet Unsinn mit einer verständlichen Meldung", () => { const ergebnis = datumLesen("demnächst"); expect(ergebnis.ok).toBe(false); expect(ergebnis.fehler).toContain("TT.MM.JJJJ"); }); }); describe("Zahlen lesen", () => { it("versteht Tausenderpunkte", () => { expect(zahlLesen("1.234").wert).toBe(1234); expect(zahlLesen("12").wert).toBe(12); }); it("versteht deutsche und englische Dezimalschreibweise", () => { expect(dezimalLesen("1.234,56").wert).toBeCloseTo(1234.56); expect(dezimalLesen("1,234.56").wert).toBeCloseTo(1234.56); expect(dezimalLesen("1234,56").wert).toBeCloseTo(1234.56); expect(dezimalLesen("1234.56").wert).toBeCloseTo(1234.56); }); it("verkraftet ein Eurozeichen", () => { expect(dezimalLesen("1.899,00 €").wert).toBeCloseTo(1899); }); it("weist Text ab", () => { expect(zahlLesen("etwa zwölf").ok).toBe(false); expect(dezimalLesen("teuer").ok).toBe(false); }); }); describe("Ja/Nein lesen", () => { it("versteht die Schreibweisen aus echten Tabellen", () => { for (const wahr of ["ja", "J", "x", "1", "WAHR", "true"]) { expect(jaNeinLesen(wahr).wert, wahr).toBe(true); } for (const falsch of ["nein", "N", "0", "falsch", "-"]) { expect(jaNeinLesen(falsch).wert, falsch).toBe(false); } }); it("weist Unklares ab, statt zu raten", () => { expect(jaNeinLesen("vielleicht").ok).toBe(false); }); }); describe("Auswahlwerte", () => { const auswahl = { einsatzbereit: "EINSATZBEREIT", "Prüfung fällig": "PRUEFUNG_FAELLIG" }; it("erkennt die Anzeigeform", () => { expect(auswahlLesen("einsatzbereit", auswahl).wert).toBe("EINSATZBEREIT"); }); it("erkennt auch den gespeicherten Wert", () => { expect(auswahlLesen("PRUEFUNG_FAELLIG", auswahl).wert).toBe("PRUEFUNG_FAELLIG"); }); it("ist bei Umlauten und Schreibweise nachsichtig", () => { expect(auswahlLesen("pruefung faellig", auswahl).wert).toBe("PRUEFUNG_FAELLIG"); expect(auswahlLesen(" Einsatzbereit ", auswahl).wert).toBe("EINSATZBEREIT"); }); it("nennt bei einem falschen Wert die erlaubten", () => { const ergebnis = auswahlLesen("kaputt", auswahl); expect(ergebnis.ok).toBe(false); expect(ergebnis.fehler).toContain("einsatzbereit"); }); }); describe("Überschriften normalisieren", () => { it("macht Umlaute und Sonderzeichen vergleichbar", () => { expect(normalisiereUeberschrift("Nächste Prüfung")).toBe("naechstepruefung"); expect(normalisiereUeberschrift("INVENTAR-NR.")).toBe("inventarnr"); expect(normalisiereUeberschrift("Größe (kg)")).toBe("groessekg"); }); }); describe("Spalten zuordnen", () => { it("erkennt die üblichen Überschriften von selbst", () => { const zuordnung = spaltenZuordnen( ["Inventarnummer", "Bezeichnung", "Hersteller", "Seriennummer"], GERAET_FELDER, ); expect(zuordnung["Inventarnummer"]).toBe("inventarnummer"); expect(zuordnung["Bezeichnung"]).toBe("bezeichnung"); expect(zuordnung["Hersteller"]).toBe("hersteller"); expect(zuordnung["Seriennummer"]).toBe("seriennummer"); }); it("erkennt auch abweichende Schreibweisen", () => { const zuordnung = spaltenZuordnen(["Inv.-Nr.", "Fabrikat", "Serien-Nr"], GERAET_FELDER); expect(zuordnung["Inv.-Nr."]).toBe("inventarnummer"); expect(zuordnung["Fabrikat"]).toBe("hersteller"); expect(zuordnung["Serien-Nr"]).toBe("seriennummer"); }); it("vergibt jedes Zielfeld höchstens einmal", () => { const zuordnung = spaltenZuordnen(["Name", "Bezeichnung"], GERAET_FELDER); const vergeben = Object.values(zuordnung).filter(Boolean); expect(new Set(vergeben).size).toBe(vergeben.length); }); it("lässt unbekannte Spalten leer, statt zu raten", () => { const zuordnung = spaltenZuordnen(["Lieblingsfarbe"], PERSON_FELDER); expect(zuordnung["Lieblingsfarbe"]).toBeNull(); }); }); describe("Werte gegen die Feldbeschreibung prüfen", () => { const inventarnummer = GERAET_FELDER.find((feld) => feld.schluessel === "inventarnummer")!; const bemerkung = GERAET_FELDER.find((feld) => feld.schluessel === "bemerkung")!; it("meldet ein fehlendes Pflichtfeld", () => { const ergebnis = wertLesen(inventarnummer, ""); expect(ergebnis.ok).toBe(false); expect(ergebnis.fehler).toContain("Pflichtfeld"); }); it("meldet zu lange Werte mit Zahlen", () => { const ergebnis = wertLesen(bemerkung, "x".repeat(1500)); expect(ergebnis.ok).toBe(false); expect(ergebnis.fehler).toContain("1500"); }); }); describe("CSV lesen", () => { it("erkennt das Semikolon, das Excel im Deutschen schreibt", () => { const tabelle = csvLesen("Inventarnummer;Bezeichnung\nINV-001;Pressluftatmer\n"); expect(tabelle.spalten).toEqual(["Inventarnummer", "Bezeichnung"]); expect(tabelle.zeilen).toHaveLength(1); expect(tabelle.zeilen[0]?.["Bezeichnung"]).toBe("Pressluftatmer"); }); it("kommt auch mit Komma zurecht", () => { const tabelle = csvLesen("Inventarnummer,Bezeichnung\nINV-001,Schlauch\n"); expect(tabelle.zeilen[0]?.["Bezeichnung"]).toBe("Schlauch"); }); it("überspringt leere Zeilen", () => { const tabelle = csvLesen("A;B\n1;2\n\n\n3;4\n"); expect(tabelle.zeilen).toHaveLength(2); }); }); describe("Zeichensatz erkennen", () => { it("entfernt eine UTF-8-BOM", () => { const puffer = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from("Prüfung", "utf8")]); expect(textAusPuffer(puffer)).toBe("Prüfung"); }); it("fällt bei Windows-1252 auf latin1 zurück", () => { // "Prüfung" in Windows-1252: das ü ist ein einzelnes Byte 0xFC. const puffer = Buffer.from([0x50, 0x72, 0xfc, 0x66, 0x75, 0x6e, 0x67]); expect(textAusPuffer(puffer)).toBe("Prüfung"); }); }); describe("Formaterkennung", () => { it("erkennt die unterstützten Endungen", () => { expect(formatErkennen("geraete.csv")).toBe("csv"); expect(formatErkennen("Geräte.XLSX")).toBe("xlsx"); expect(formatErkennen("export.txt")).toBe("csv"); }); it("gibt bei allem anderen null zurück", () => { expect(formatErkennen("bestand.pdf")).toBeNull(); expect(formatErkennen("ohne-endung")).toBeNull(); }); }); describe("Vorschau", () => { const csv = [ "Inventarnummer;Bezeichnung;Hersteller;Indienststellung;Preis", "INV-001;Pressluftatmer;Dräger;01.03.2019;1.899,00", "INV-002;Feuerwehrleine;Bauer;15.06.2021;249,50", ";Ohne Nummer;Unbekannt;01.01.2020;100", "INV-001;Doppelte Nummer;Dräger;01.01.2020;100", "INV-004;Schlauch;Ziegler;kein Datum;80", ].join("\n"); const vorschau = () => vorschauErstellen(csvLesen(csv), "geraet"); it("ordnet die Spalten von selbst zu", () => { const ergebnis = vorschau(); expect(ergebnis.zuordnung["Inventarnummer"]).toBe("inventarnummer"); expect(ergebnis.zuordnung["Preis"]).toBe("anschaffungspreis"); }); it("wandelt Werte in den richtigen Typ", () => { const erste = vorschau().zeilen[0]; expect(erste?.werte.inventarnummer).toBe("INV-001"); expect(erste?.werte.anschaffungspreis).toBeCloseTo(1899); expect(erste?.werte.indienststellung).toBeInstanceOf(Date); }); it("markiert die Zeile ohne Pflichtfeld als nicht übernehmbar", () => { const dritte = vorschau().zeilen[2]; expect(dritte?.uebernehmbar).toBe(false); expect(dritte?.fehler.some((f) => f.meldung.includes("Pflichtfeld"))).toBe(true); }); it("erkennt Dubletten innerhalb der Datei und nennt die erste Zeile", () => { const vierte = vorschau().zeilen[3]; expect(vierte?.uebernehmbar).toBe(false); expect(vierte?.fehler.some((f) => f.meldung.includes("Zeile 1"))).toBe(true); }); it("markiert ein unlesbares Datum, ohne die Zeile stillschweigend zu verwerfen", () => { const fuenfte = vorschau().zeilen[4]; expect(fuenfte?.uebernehmbar).toBe(false); expect(fuenfte?.fehler[0]?.feld).toBe("Indienststellung"); }); it("zählt übernehmbare und fehlerhafte Zeilen getrennt", () => { const ergebnis = vorschau(); expect(ergebnis.anzahlUebernehmbar).toBe(2); expect(ergebnis.anzahlFehlerhaft).toBe(3); expect(ergebnis.zeilen).toHaveLength(5); }); it("meldet Pflichtfelder, denen gar keine Spalte zugeordnet ist", () => { const ohneBezeichnung = vorschauErstellen(csvLesen("Inventarnummer\nINV-001\n"), "geraet"); expect(ohneBezeichnung.fehlendePflichtfelder).toContain("Bezeichnung"); expect(ohneBezeichnung.anzahlUebernehmbar).toBe(0); }); it("folgt einer von Hand geänderten Zuordnung", () => { const tabelle = csvLesen("Spalte A;Spalte B\nINV-9;Schlauch\n"); const ergebnis = vorschauErstellen(tabelle, "geraet", { "Spalte A": "inventarnummer", "Spalte B": "bezeichnung", }); expect(ergebnis.anzahlUebernehmbar).toBe(1); expect(ergebnis.zeilen[0]?.werte.inventarnummer).toBe("INV-9"); }); });