import { describe, expect, it } from "vitest"; import { toCsv } from "@/lib/export"; import { baueHonestlyZeilen, honestlySpalten, orgKette, ouUeberschrift } from "@/lib/honestly"; import { buildOrgMaps, type OrgUnit } from "@/lib/org"; // Die Datei geht an eine fremde Plattform, die sie über Spaltennamen // zuordnet. Ein Fehler darin zeigt sich nicht hier, sondern dort — als // Befragung, die an die falschen Leute geht oder nach der falschen Einheit // auswertet. Deshalb wird hier die Form festgehalten, nicht nur der Inhalt. const units: OrgUnit[] = [ { id: "g", org_number: "1", name: "Manner AG", parent_id: null, unit_type: "Gesellschaft" }, { id: "b", org_number: "2", name: "Produktion", parent_id: "g", unit_type: "Bereich" }, { id: "a", org_number: "3", name: "Waffel", parent_id: "b", unit_type: "Abteilung" }, { id: "t", org_number: "4", name: "Linie 1", parent_id: "a", unit_type: "Team" }, { id: "v", org_number: "5", name: "Vertrieb", parent_id: "g", unit_type: "Bereich" }, ]; const maps = buildOrgMaps(units, []); const standorte = new Map([["wien", "Wien"], ["wolkersdorf", "Wolkersdorf"]]); function person(teil: Partial[0][number]> = {}) { return { personnel_number: 4711, // Die private Adresse, wie sie auf der Zeile aus der Datenbank steht — // sie darf im Export nirgends auftauchen. email: "anna.privat@example.test", company_email: "a.berger@firma.test", first_name: "Anna", last_name: "Berger", location_id: "wien", org_unit_id: "t", ...teil, }; } describe("orgKette", () => { it("fängt bei der eigenen Einheit an und endet ganz oben", () => { // OU ist die Einheit der Person, OU+1 die darüber — so ist es bestellt. // ancestorsOf liefert es andersherum; ohne das Umdrehen stünde die // Gesellschaft in der Spalte „OU". expect(orgKette(maps, "t")).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]); }); it("ist leer ohne Einheit", () => { expect(orgKette(maps, null)).toEqual([]); }); }); describe("Spalten", () => { it("stehen in der vorgegebenen Reihenfolge", () => { expect(honestlySpalten(3).map((s) => s.header)).toEqual([ "Personalnummer", "Email", "Firstname", "Last Name", "Language", "Location", "OU", "OU+1", "OU+2", "Role", ]); }); it("heissen OU, OU+1, OU+2 …", () => { expect([0, 1, 2, 5].map(ouUeberschrift)).toEqual(["OU", "OU+1", "OU+2", "OU+5"]); }); it("behalten mindestens eine OU-Spalte", () => { // Eine Datei, deren Spalten je nach Inhalt verschwinden, bricht die // Zuordnung, die auf der anderen Seite eingerichtet wurde. expect(honestlySpalten(0).filter((s) => s.header.startsWith("OU"))).toHaveLength(1); }); }); describe("baueHonestlyZeilen", () => { it("richtet die Zahl der OU-Spalten nach der tiefsten Kette", () => { const { tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person({ org_unit_id: "t" })], maps, standorte); expect(tiefe).toBe(4); }); it("füllt kürzere Ketten hinten mit Leerzellen auf", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: "v" }), person()], maps, standorte); const spalten = honestlySpalten(tiefe); const werte = (i: number) => spalten.map((s) => s.get(zeilen[i])); // Vertrieb hängt direkt unter der Gesellschaft: zwei Einheiten, zwei leere. expect(werte(0).slice(6, 10)).toEqual(["Vertrieb", "Manner AG", "", ""]); expect(werte(1).slice(6, 10)).toEqual(["Linie 1", "Waffel", "Produktion", "Manner AG"]); }); it("setzt Sprache und Rolle fest", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte); const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])])); expect(zeile.Language).toBe("de"); expect(zeile.Role).toBe("Respondee"); }); it("übernimmt die Personendaten unverändert", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte); const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])])); expect(zeile).toMatchObject({ Personalnummer: 4711, Firstname: "Anna", "Last Name": "Berger", Location: "Wien", }); }); it("nimmt die Firmenadresse in die E-Mail-Spalte", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte); const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])])); expect(zeile.Email).toBe("a.berger@firma.test"); }); it("lässt die Spalte leer, wenn keine Firmenadresse hinterlegt ist", () => { // Eine fehlende Dienstadresse ist eine fehlende Angabe — und keine // Ausrede, ersatzweise die private zu nehmen. const { zeilen, tiefe } = baueHonestlyZeilen([person({ company_email: null })], maps, standorte); const zeile = Object.fromEntries(honestlySpalten(tiefe).map((s) => [s.header, s.get(zeilen[0])])); expect(zeile.Email).toBe(""); }); it("trägt die private Adresse in keine Spalte ein", () => { // Nicht nur „Email" prüfen: rutschte sie in eine andere Spalte, fiele es // dort erst in Honestly auf. const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte); const csv = toCsv(zeilen, honestlySpalten(tiefe)); expect(csv).not.toContain("anna.privat@example.test"); expect(csv).toContain("a.berger@firma.test"); }); it("nimmt auch dann nicht die private Adresse, wenn keine dienstliche da ist", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person({ company_email: null })], maps, standorte); expect(toCsv(zeilen, honestlySpalten(tiefe))).not.toContain("anna.privat@example.test"); }); it("behält die Spalte Email, auch solange sie leer ist", () => { // Die Zuordnung in Honestly wird einmal eingerichtet. Kommt die // Firmenadresse später dazu, soll sich an der Datei nur der Inhalt ändern. expect(honestlySpalten(1).map((s) => s.header)).toContain("Email"); }); it("gibt Personen ohne Einheit eine leere Kette, aber eine Zeile", () => { // Wer nicht zugeordnet ist, soll trotzdem befragt werden. const { zeilen, tiefe } = baueHonestlyZeilen([person({ org_unit_id: null })], maps, standorte); expect(zeilen).toHaveLength(1); expect(tiefe).toBe(1); expect(honestlySpalten(tiefe).find((s) => s.header === "OU")?.get(zeilen[0])).toBe(""); }); }); describe("als CSV", () => { it("schreibt die Kopfzeile genau so, wie Honestly sie zuordnet", () => { const { zeilen, tiefe } = baueHonestlyZeilen([person()], maps, standorte); const kopf = toCsv(zeilen, honestlySpalten(tiefe)).replace(/^/, "").split(/\r?\n/)[0]; expect(kopf).toContain("Personalnummer"); expect(kopf).toContain("Last Name"); expect(kopf).toMatch(/OU\+3.*Role/); }); });