import { describe, expect, it } from 'vitest' import { aufgabeSchema, aufwandSumme, faelligkeitEinordnen, faelligkeitText, fortschrittBerechnen, istAbgeschlossen, istOffen, istUeberfaellig, moeglicheUebergaenge, nachDringlichkeit, projektSchema, schnellerfassungSchema, uebergangErlaubt, type SortierbareAufgabe, } from './aufgabe' // Dienstag, 17. März 2026, 10:00 Uhr Berliner Zeit. const jetzt = new Date('2026-03-17T09:00:00Z') describe('istOffen und istAbgeschlossen', () => { it('trennt die beiden Gruppen vollständig', () => { for (const status of ['OFFEN', 'GEPLANT', 'WARTET'] as const) { expect(istOffen(status)).toBe(true) expect(istAbgeschlossen(status)).toBe(false) } for (const status of ['ERLEDIGT', 'VERWORFEN'] as const) { expect(istOffen(status)).toBe(false) expect(istAbgeschlossen(status)).toBe(true) } }) }) describe('uebergangErlaubt', () => { it('lässt den Wechsel auf sich selbst zu', () => { expect(uebergangErlaubt('OFFEN', 'OFFEN')).toBe(true) }) it('lässt aus offenen Zuständen alles zu', () => { expect(uebergangErlaubt('OFFEN', 'ERLEDIGT')).toBe(true) expect(uebergangErlaubt('GEPLANT', 'WARTET')).toBe(true) expect(uebergangErlaubt('WARTET', 'VERWORFEN')).toBe(true) }) it('führt aus abgeschlossenen Zuständen nur zurück nach OFFEN', () => { expect(uebergangErlaubt('ERLEDIGT', 'OFFEN')).toBe(true) expect(uebergangErlaubt('ERLEDIGT', 'GEPLANT')).toBe(false) expect(uebergangErlaubt('VERWORFEN', 'OFFEN')).toBe(true) expect(uebergangErlaubt('VERWORFEN', 'ERLEDIGT')).toBe(false) }) it('nennt für einen abgeschlossenen Zustand genau einen Ausweg', () => { expect(moeglicheUebergaenge('ERLEDIGT')).toEqual(['OFFEN']) }) }) describe('faelligkeitEinordnen', () => { it('erkennt fehlenden Termin', () => { expect(faelligkeitEinordnen(null, jetzt)).toEqual({ art: 'ohne' }) expect(faelligkeitEinordnen(undefined, jetzt)).toEqual({ art: 'ohne' }) }) it('erkennt heute, auch spät am Abend', () => { expect(faelligkeitEinordnen(new Date('2026-03-17T22:00:00Z'), jetzt)).toEqual({ art: 'heute', }) }) it('rechnet in Kalendertagen, nicht in 24-Stunden-Schritten', () => { // 23:30 UTC am 17. ist in Berlin bereits der 18. um 00:30. expect(faelligkeitEinordnen(new Date('2026-03-17T23:30:00Z'), jetzt)).toEqual({ art: 'morgen', }) }) it('erkennt morgen', () => { expect(faelligkeitEinordnen(new Date('2026-03-18T09:00:00Z'), jetzt)).toEqual({ art: 'morgen', }) }) it('fasst die nächste Woche zusammen', () => { expect(faelligkeitEinordnen(new Date('2026-03-22T09:00:00Z'), jetzt)).toEqual({ art: 'diese-woche', tage: 5, }) expect(faelligkeitEinordnen(new Date('2026-03-24T09:00:00Z'), jetzt)).toEqual({ art: 'diese-woche', tage: 7, }) }) it('schiebt alles darüber nach später', () => { expect(faelligkeitEinordnen(new Date('2026-03-25T09:00:00Z'), jetzt)).toEqual({ art: 'spaeter', tage: 8, }) }) it('zählt Überfälligkeit in Tagen', () => { expect(faelligkeitEinordnen(new Date('2026-03-16T09:00:00Z'), jetzt)).toEqual({ art: 'ueberfaellig', tage: 1, }) expect(faelligkeitEinordnen(new Date('2026-03-10T09:00:00Z'), jetzt)).toEqual({ art: 'ueberfaellig', tage: 7, }) }) }) describe('faelligkeitText', () => { it('schreibt jeden Fall auf Deutsch', () => { expect(faelligkeitText({ art: 'ohne' })).toBe('ohne Termin') expect(faelligkeitText({ art: 'heute' })).toBe('heute fällig') expect(faelligkeitText({ art: 'morgen' })).toBe('morgen fällig') expect(faelligkeitText({ art: 'diese-woche', tage: 3 })).toBe('in 3 Tagen fällig') expect(faelligkeitText({ art: 'spaeter', tage: 20 })).toBe('in 20 Tagen fällig') }) it('sagt bei einem Tag Rückstand "seit gestern"', () => { expect(faelligkeitText({ art: 'ueberfaellig', tage: 1 })).toBe( 'seit gestern überfällig', ) expect(faelligkeitText({ art: 'ueberfaellig', tage: 4 })).toBe( 'seit 4 Tagen überfällig', ) }) }) describe('istUeberfaellig', () => { it('erkennt eine offene Aufgabe von gestern', () => { expect(istUeberfaellig(new Date('2026-03-16T09:00:00Z'), 'OFFEN', jetzt)).toBe(true) }) it('zählt den heutigen Tag nicht als überfällig', () => { expect(istUeberfaellig(new Date('2026-03-17T06:00:00Z'), 'OFFEN', jetzt)).toBe(false) }) it('lässt Erledigtes und Verworfenes in Ruhe', () => { const gestern = new Date('2026-03-16T09:00:00Z') expect(istUeberfaellig(gestern, 'ERLEDIGT', jetzt)).toBe(false) expect(istUeberfaellig(gestern, 'VERWORFEN', jetzt)).toBe(false) }) it('ist ohne Termin nie überfällig', () => { expect(istUeberfaellig(null, 'OFFEN', jetzt)).toBe(false) }) }) describe('nachDringlichkeit', () => { function auf(teil: Partial): SortierbareAufgabe { return { status: 'OFFEN', prioritaet: 3, faelligAm: null, titel: 'Titel', ...teil, } } it('stellt Offenes vor Abgeschlossenes', () => { expect( nachDringlichkeit(auf({ status: 'ERLEDIGT' }), auf({ status: 'OFFEN' })), ).toBeGreaterThan(0) }) it('stellt den früheren Termin nach vorn', () => { const frueh = auf({ faelligAm: new Date('2026-03-18T00:00:00Z') }) const spaet = auf({ faelligAm: new Date('2026-03-20T00:00:00Z') }) expect(nachDringlichkeit(frueh, spaet)).toBeLessThan(0) }) it('stellt Terminiertes vor Unterminiertes', () => { const mitTermin = auf({ faelligAm: new Date('2026-04-01T00:00:00Z') }) const ohneTermin = auf({ faelligAm: null }) expect(nachDringlichkeit(mitTermin, ohneTermin)).toBeLessThan(0) }) it('entscheidet bei gleichem Termin über die Priorität', () => { const termin = new Date('2026-03-18T00:00:00Z') expect( nachDringlichkeit( auf({ faelligAm: termin, prioritaet: 1 }), auf({ faelligAm: termin, prioritaet: 4 }), ), ).toBeLessThan(0) }) it('sortiert zuletzt nach dem Titel, mit deutscher Sortierung', () => { expect( nachDringlichkeit(auf({ titel: 'Äpfel' }), auf({ titel: 'Banane' })), ).toBeLessThan(0) }) it('ergibt eine stabile Gesamtreihenfolge', () => { const liste: SortierbareAufgabe[] = [ auf({ titel: 'Erledigt', status: 'ERLEDIGT' }), auf({ titel: 'Ohne Termin' }), auf({ titel: 'Übermorgen', faelligAm: new Date('2026-03-19T00:00:00Z') }), auf({ titel: 'Morgen', faelligAm: new Date('2026-03-18T00:00:00Z') }), ] expect([...liste].sort(nachDringlichkeit).map((a) => a.titel)).toEqual([ 'Morgen', 'Übermorgen', 'Ohne Termin', 'Erledigt', ]) }) }) describe('fortschrittBerechnen', () => { it('ist bei keiner Aufgabe null Prozent', () => { expect(fortschrittBerechnen([])).toEqual({ gesamt: 0, erledigt: 0, offen: 0, prozent: 0, }) }) it('rechnet Erledigtes gegen die Gesamtzahl', () => { expect( fortschrittBerechnen([ { status: 'ERLEDIGT' }, { status: 'ERLEDIGT' }, { status: 'OFFEN' }, { status: 'GEPLANT' }, ]), ).toEqual({ gesamt: 4, erledigt: 2, offen: 2, prozent: 50 }) }) it('lässt Verworfenes ganz aus der Rechnung', () => { // Sonst sähe ein Projekt fertig aus, weil die Hälfte gestrichen wurde. expect( fortschrittBerechnen([ { status: 'ERLEDIGT' }, { status: 'VERWORFEN' }, { status: 'OFFEN' }, ]), ).toEqual({ gesamt: 2, erledigt: 1, offen: 1, prozent: 50 }) }) it('rundet auf ganze Prozent', () => { expect( fortschrittBerechnen([ { status: 'ERLEDIGT' }, { status: 'OFFEN' }, { status: 'OFFEN' }, ]).prozent, ).toBe(33) }) }) describe('aufwandSumme', () => { it('summiert und zählt Ungeschätztes als null', () => { expect( aufwandSumme([ { aufwandMinuten: 30 }, { aufwandMinuten: null }, { aufwandMinuten: 45 }, ]), ).toBe(75) }) it('ist bei leerer Liste null', () => { expect(aufwandSumme([])).toBe(0) }) }) describe('aufgabeSchema', () => { const gueltig = { titel: 'Angebot schreiben', notiz: '', typ: 'AUFGABE', status: 'OFFEN', prioritaet: '2', aufwandMinuten: '90', faelligAm: '2026-03-20', bereichId: '', projektId: '', } it('nimmt ein vollständiges Formular an', () => { const ergebnis = aufgabeSchema.parse(gueltig) expect(ergebnis.titel).toBe('Angebot schreiben') expect(ergebnis.prioritaet).toBe(2) expect(ergebnis.aufwandMinuten).toBe(90) expect(ergebnis.faelligAm).toBeInstanceOf(Date) }) it('macht aus leeren Feldern null statt leerer Zeichenketten', () => { const ergebnis = aufgabeSchema.parse(gueltig) expect(ergebnis.notiz).toBeNull() expect(ergebnis.bereichId).toBeNull() expect(ergebnis.projektId).toBeNull() }) it('lässt Aufwand und Termin weg, wenn nichts angegeben ist', () => { const ergebnis = aufgabeSchema.parse({ ...gueltig, aufwandMinuten: '', faelligAm: '', }) expect(ergebnis.aufwandMinuten).toBeNull() expect(ergebnis.faelligAm).toBeNull() }) it('weist einen zu kurzen Titel ab', () => { expect(aufgabeSchema.safeParse({ ...gueltig, titel: 'A' }).success).toBe(false) }) it('hält die Priorität zwischen 1 und 5', () => { expect(aufgabeSchema.safeParse({ ...gueltig, prioritaet: '0' }).success).toBe(false) expect(aufgabeSchema.safeParse({ ...gueltig, prioritaet: '6' }).success).toBe(false) expect(aufgabeSchema.safeParse({ ...gueltig, prioritaet: '1' }).success).toBe(true) expect(aufgabeSchema.safeParse({ ...gueltig, prioritaet: '5' }).success).toBe(true) }) it('weist einen Aufwand über acht Stunden ab', () => { // Genau acht Stunden sind noch erlaubt, eine Minute mehr nicht. expect(aufgabeSchema.safeParse({ ...gueltig, aufwandMinuten: '480' }).success).toBe( true, ) expect(aufgabeSchema.safeParse({ ...gueltig, aufwandMinuten: '481' }).success).toBe( false, ) }) it('weist einen Aufwand von null oder weniger ab', () => { expect(aufgabeSchema.safeParse({ ...gueltig, aufwandMinuten: '0' }).success).toBe( false, ) }) it('weist ein unsinniges Datum ab', () => { expect(aufgabeSchema.safeParse({ ...gueltig, faelligAm: 'übermorgen' }).success).toBe( false, ) }) }) describe('schnellerfassungSchema', () => { it('kommt mit einem Titel aus', () => { expect(schnellerfassungSchema.parse({ titel: 'Rückruf Meier' })).toEqual({ titel: 'Rückruf Meier', typ: 'AUFGABE', }) }) it('kennt auch Ideen', () => { expect( schnellerfassungSchema.parse({ titel: 'Blog aufsetzen', typ: 'IDEE' }).typ, ).toBe('IDEE') }) it('lässt keine anderen Typen zu', () => { expect( schnellerfassungSchema.safeParse({ titel: 'Etwas', typ: 'MEILENSTEIN' }).success, ).toBe(false) }) }) describe('projektSchema', () => { it('nimmt ein Projekt an', () => { const ergebnis = projektSchema.parse({ name: 'Website neu', beschreibung: '', status: 'AKTIV', bereichId: '', zielAm: '', }) expect(ergebnis.name).toBe('Website neu') expect(ergebnis.beschreibung).toBeNull() expect(ergebnis.zielAm).toBeNull() }) it('weist einen zu kurzen Namen ab', () => { expect( projektSchema.safeParse({ name: 'X', beschreibung: '', status: 'AKTIV' }).success, ).toBe(false) }) })