import { describe, expect, it } from "vitest"; import { Serienmuster } from "@prisma/client"; import { HOECHSTZAHL, serienlaenge, serientermine } from "@/lib/termin/serie"; /** * Serientermine. * * Geprüft wird gegen den Kalender, nicht gegen die Implementierung: „monatlich" * heißt derselbe Tag im nächsten Monat, und der 31. Januar landet im Februar * auf dem 28. — nicht im März, wie es passiert, wenn man stur Monate addiert. */ const D = (jahr: number, monat: number, tag: number, stunde = 19) => new Date(Date.UTC(jahr, monat - 1, tag, stunde, 0, 0)); describe("serientermine", () => { it("gibt bei KEINE genau den einen Termin zurück", () => { const termine = serientermine({ beginn: D(2026, 3, 10), muster: Serienmuster.KEINE, bis: D(2027, 3, 10), }); expect(termine).toHaveLength(1); expect(termine[0]).toEqual(D(2026, 3, 10)); }); it("legt wöchentlich immer denselben Wochentag", () => { const termine = serientermine({ beginn: D(2026, 3, 10), // Dienstag muster: Serienmuster.WOECHENTLICH, bis: D(2026, 4, 7), }); expect(termine).toEqual([ D(2026, 3, 10), D(2026, 3, 17), D(2026, 3, 24), D(2026, 3, 31), D(2026, 4, 7), ]); // Alle am selben Wochentag — das ist der Punkt einer Serie. for (const termin of termine) { expect(termin.getUTCDay()).toBe(D(2026, 3, 10).getUTCDay()); } }); it("überspringt bei ZWEIWOECHENTLICH jede zweite Woche", () => { const termine = serientermine({ beginn: D(2026, 3, 10), muster: Serienmuster.ZWEIWOECHENTLICH, bis: D(2026, 4, 30), }); expect(termine).toEqual([D(2026, 3, 10), D(2026, 3, 24), D(2026, 4, 7), D(2026, 4, 21)]); }); it("behält die Uhrzeit über die ganze Serie", () => { const termine = serientermine({ beginn: D(2026, 3, 10, 19), muster: Serienmuster.WOECHENTLICH, bis: D(2026, 4, 30), }); for (const termin of termine) { expect(termin.getUTCHours()).toBe(19); expect(termin.getUTCMinutes()).toBe(0); } }); it("rechnet monatlich auf dem Kalender, nicht in 30-Tage-Schritten", () => { const termine = serientermine({ beginn: D(2026, 1, 15), muster: Serienmuster.MONATLICH, bis: D(2026, 5, 20), }); expect(termine).toEqual([ D(2026, 1, 15), D(2026, 2, 15), D(2026, 3, 15), D(2026, 4, 15), D(2026, 5, 15), ]); }); /** * Der Fall, an dem jede naive Umsetzung scheitert: setMonth(+1) auf dem * 31. Januar ergibt den 3. März, weil der Februar keinen 31. hat. Ein * Mensch erwartet den letzten Februartag. */ it("schiebt den 31. nicht in den übernächsten Monat", () => { const termine = serientermine({ beginn: D(2026, 1, 31), muster: Serienmuster.MONATLICH, bis: D(2026, 4, 30), }); expect(termine).toEqual([ D(2026, 1, 31), D(2026, 2, 28), // kein 31. Februar D(2026, 3, 31), D(2026, 4, 30), // kein 31. April ]); }); it("kennt den 29. Februar im Schaltjahr", () => { const termine = serientermine({ beginn: D(2024, 2, 29), muster: Serienmuster.JAEHRLICH, bis: D(2028, 3, 1), }); expect(termine).toEqual([ D(2024, 2, 29), D(2025, 2, 28), // kein Schaltjahr D(2026, 2, 28), D(2027, 2, 28), D(2028, 2, 29), // wieder Schaltjahr ]); }); it("nimmt einen Termin am Endtag noch mit", () => { const termine = serientermine({ beginn: D(2026, 3, 10), muster: Serienmuster.WOECHENTLICH, bis: D(2026, 3, 24), }); expect(termine.at(-1)).toEqual(D(2026, 3, 24)); }); it("liefert bei einem Endtag vor dem Beginn nur den Beginn", () => { const termine = serientermine({ beginn: D(2026, 3, 10), muster: Serienmuster.WOECHENTLICH, bis: D(2026, 1, 1), }); expect(termine).toEqual([D(2026, 3, 10)]); }); /** * Eine Serie, die versehentlich bis 2099 läuft, darf nicht zehntausend * Zeilen anlegen. Die Oberfläche zeigt vorher an, dass gekappt wird. */ it("kappt bei der Höchstzahl", () => { const termine = serientermine({ beginn: D(2026, 1, 1), muster: Serienmuster.WOECHENTLICH, bis: D(2099, 1, 1), }); expect(termine).toHaveLength(HOECHSTZAHL); }); it("achtet eine niedrigere eigene Höchstzahl", () => { const termine = serientermine({ beginn: D(2026, 1, 1), muster: Serienmuster.WOECHENTLICH, bis: D(2099, 1, 1), hoechstzahl: 5, }); expect(termine).toHaveLength(5); }); it("lässt sich die Höchstzahl nicht hochsetzen", () => { const termine = serientermine({ beginn: D(2026, 1, 1), muster: Serienmuster.WOECHENTLICH, bis: D(2099, 1, 1), hoechstzahl: 10_000, }); expect(termine).toHaveLength(HOECHSTZAHL); }); }); describe("serienlaenge", () => { it("zählt dasselbe, was serientermine liefert", () => { const angaben = { beginn: D(2026, 3, 10), muster: Serienmuster.ZWEIWOECHENTLICH, bis: D(2026, 9, 10), }; expect(serienlaenge(angaben)).toBe(serientermine(angaben).length); }); });