import { test, describe } from "node:test"; import assert from "node:assert/strict"; import { getStartOfWeek, getDayName, isSameDay, formatDateToISO, parseCalendarDate, formatHour, getTimeSlots, getHourFromSlot, getWeekNumber, getCWReferenceDate, getSelectedDay, } from "../../src/lib/weekly/date-utils.ts"; /** * Unit-Tests der Datums- und Zeitraster-Helfer. * * Läuft ohne Browser, ohne Datenbank, ohne laufende App: `npm run test:unit`. * * Die Erwartungen sind bewusst gegen unabhängig überprüfbare Fakten formuliert * (Kalenderdaten, ISO-8601-Regeln) und nicht gegen die Implementierung — ein Test, * der nur nachspricht, was der Code tut, findet keine Fehler. */ describe("getWeekNumber (ISO 8601)", () => { // Die Wochenzahl wird prominent in der Kopfzeile angezeigt ("KW 34 | August 2026"), // ein Fehler hier fällt sofort jedem Nutzer auf. test("Donnerstag 01.01.2026 liegt in KW 1", () => { assert.equal(getWeekNumber(new Date(2026, 0, 1)), 1); }); test("Samstag 22.08.2026 liegt in KW 34", () => { assert.equal(getWeekNumber(new Date(2026, 7, 22)), 34); }); test("Mittwoch 02.09.2026 liegt in KW 36", () => { assert.equal(getWeekNumber(new Date(2026, 8, 2)), 36); }); test("Montag 01.01.2024 liegt in KW 1", () => { assert.equal(getWeekNumber(new Date(2024, 0, 1)), 1); }); test("Sonntag 01.01.2023 gehört noch zu KW 52 des Vorjahres", () => { // ISO 8601: Die Woche gehört dem Jahr, in dem ihr Donnerstag liegt. assert.equal(getWeekNumber(new Date(2023, 0, 1)), 52); }); test("Freitag 01.01.2021 gehört zu KW 53 des Vorjahres", () => { assert.equal(getWeekNumber(new Date(2021, 0, 1)), 53); }); }); describe("getStartOfWeek", () => { test("Wochenstart Montag: Donnerstag 03.09.2026 → Montag 31.08.2026", () => { const r = getStartOfWeek(new Date(2026, 8, 3), 1); assert.equal(formatDateToISO(r), "2026-08-31"); }); test("Wochenstart Montag: Sonntag gehört zur ablaufenden Woche", () => { // Der klassische Stolperstein — Sonntag ist Tag 0, liegt aber am Wochenende. const r = getStartOfWeek(new Date(2026, 8, 6), 1); assert.equal(formatDateToISO(r), "2026-08-31"); }); test("Wochenstart Sonntag: Sonntag ist bereits der Wochenstart", () => { const r = getStartOfWeek(new Date(2026, 8, 6), 0); assert.equal(formatDateToISO(r), "2026-09-06"); }); test("Ein Tag, der bereits der Wochenstart ist, bleibt unverändert", () => { const r = getStartOfWeek(new Date(2026, 7, 31), 1); assert.equal(formatDateToISO(r), "2026-08-31"); }); test("verändert das übergebene Datum nicht", () => { const input = new Date(2026, 8, 3); getStartOfWeek(input, 1); assert.equal(formatDateToISO(input), "2026-09-03"); }); }); describe("formatDateToISO", () => { test("formatiert mit führenden Nullen", () => { assert.equal(formatDateToISO(new Date(2026, 0, 5)), "2026-01-05"); }); test("nutzt die lokale Zeit, nicht UTC", () => { // Kern der Sache: toISOString() würde bei einem späten Abendtermin in einer // Zeitzone östlich von UTC auf den Folgetag rutschen. Genau daraus entstehen // Termine, die im Kalender einen Tag daneben landen. const spaeterAbend = new Date(2026, 8, 7, 23, 30, 0); assert.equal(formatDateToISO(spaeterAbend), "2026-09-07"); }); test("früher Morgen bleibt am selben Tag", () => { assert.equal(formatDateToISO(new Date(2026, 8, 7, 0, 30, 0)), "2026-09-07"); }); }); describe("parseCalendarDate", () => { test("reines Datum wird als lokale Mitternacht gelesen", () => { const d = parseCalendarDate("2026-09-07"); assert.equal(d.getFullYear(), 2026); assert.equal(d.getMonth(), 8); assert.equal(d.getDate(), 7); assert.equal(d.getHours(), 0); }); test("Round-Trip mit formatDateToISO ist verlustfrei", () => { // Diese beiden Funktionen sind die Brücke zwischen Datenbank und Anzeige. // Wenn sie auseinanderlaufen, verschieben sich Aufgaben um einen Tag. for (const s of ["2026-01-01", "2026-06-15", "2026-12-31", "2026-02-28"]) { assert.equal(formatDateToISO(parseCalendarDate(s)), s); } }); }); describe("getTimeSlots", () => { test("30-Minuten-Raster von 8 bis 10 Uhr", () => { assert.deepEqual(getTimeSlots(30, 8, 10), ["08:00", "08:30", "09:00", "09:30"]); }); test("15-Minuten-Raster ergibt vier Einträge pro Stunde", () => { assert.deepEqual(getTimeSlots(15, 9, 10), ["09:00", "09:15", "09:30", "09:45"]); }); test("volle Stunden über den ganzen Tag ergeben 24 Einträge", () => { const slots = getTimeSlots(60, 0, 24); assert.equal(slots.length, 24); assert.equal(slots[0], "00:00"); assert.equal(slots[23], "23:00"); }); test("Endstunde ist exklusiv", () => { assert.equal(getTimeSlots(60, 8, 9).length, 1); }); test("leeres Ergebnis, wenn Ende nicht nach Start liegt", () => { assert.deepEqual(getTimeSlots(30, 10, 10), []); }); }); describe("formatHour", () => { test("24h-Format, kurz, ohne Minuten", () => { assert.equal(formatHour(9, 0, "short", "24h"), "9"); }); test("24h-Format mit Minuten wird zweistellig", () => { assert.equal(formatHour(9, 30, "short", "24h"), "09:30"); }); test("12h-Format: Nachmittag", () => { assert.equal(formatHour(13, 0, "short", "12h"), "1 PM"); }); test("12h-Format: Mitternacht ist 12 AM, nicht 0 AM", () => { assert.equal(formatHour(0, 0, "short", "12h"), "12 AM"); }); test("12h-Format: Mittag ist 12 PM", () => { assert.equal(formatHour(12, 0, "short", "12h"), "12 PM"); }); }); describe("getHourFromSlot", () => { test("liest die Stunde aus dem Slot", () => { assert.equal(getHourFromSlot("08:30"), 8); assert.equal(getHourFromSlot("23:45"), 23); }); test("führende Null wird nicht als Oktalzahl gelesen", () => { assert.equal(getHourFromSlot("09:00"), 9); }); }); describe("isSameDay", () => { test("gleicher Tag, verschiedene Uhrzeit", () => { assert.equal(isSameDay(new Date(2026, 8, 7, 8, 0), new Date(2026, 8, 7, 22, 0)), true); }); test("aufeinanderfolgende Tage sind nicht gleich", () => { assert.equal(isSameDay(new Date(2026, 8, 7), new Date(2026, 8, 8)), false); }); test("gleicher Tag im anderen Jahr ist nicht gleich", () => { assert.equal(isSameDay(new Date(2026, 8, 7), new Date(2025, 8, 7)), false); }); }); describe("getCWReferenceDate", () => { test("liefert den Montag der Woche des ersten sichtbaren Tages", () => { const days = [new Date(2026, 8, 3), new Date(2026, 8, 4)]; // Do, Fr assert.equal(formatDateToISO(getCWReferenceDate(days)), "2026-08-31"); }); test("Sonntag gehört zur ablaufenden Woche, nicht zur kommenden", () => { const days = [new Date(2026, 8, 6)]; // Sonntag assert.equal(formatDateToISO(getCWReferenceDate(days)), "2026-08-31"); }); test("leere Liste liefert ein Datum statt zu werfen", () => { assert.ok(getCWReferenceDate([]) instanceof Date); }); }); describe("getSelectedDay", () => { test("ohne ausdrückliche Auswahl wird ein Tag aus der Liste geliefert", () => { const days = [new Date(2026, 8, 7), new Date(2026, 8, 8)]; assert.ok(days.some((d) => isSameDay(d, getSelectedDay(days)))); }); test("eine ausdrückliche Auswahl aus der Liste wird übernommen", () => { const days = [new Date(2026, 8, 7), new Date(2026, 8, 8)]; assert.equal(formatDateToISO(getSelectedDay(days, days[1])), "2026-09-08"); }); }); describe("getDayName", () => { test("deutscher Wochentag, lang", () => { assert.equal(getDayName(new Date(2026, 8, 7), "de-DE"), "Montag"); }); test("eigene Namen werden nach Wochentag zugeordnet", () => { const namen = "So,Mo,Di,Mi,Do,Fr,Sa"; assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", "custom", namen, 0), "Mo"); }); test("eigene Namen mit falscher Anzahl fallen auf das Gebietsschema zurück", () => { assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", "custom", "nur,drei,Namen"), "Montag"); }); test("Großschreibung ist einstellbar", () => { assert.equal(getDayName(new Date(2026, 8, 7), "de-DE", undefined, undefined, 0, "uppercase"), "MONTAG"); }); });