import { describe, expect, it } from 'vitest' import { VOREINSTELLUNG, einstellungSchema, inRuhezeit, meldungsSchluessel, ruhezeitText, sendenEntscheiden, ueberblickFaellig, zeitblockErinnerungFaellig, type Einstellung, type Meldung, } from './benachrichtigung' /** Dienstag, 17. März 2026. Berlin liegt dann auf UTC+1. */ function berlin(stunde: number, minute = 0): Date { return new Date(Date.UTC(2026, 2, 17, stunde - 1, minute)) } /** Samstag, 21. März 2026. */ function samstag(stunde: number): Date { return new Date(Date.UTC(2026, 2, 21, stunde - 1, 0)) } const einstellung: Einstellung = { ...VOREINSTELLUNG, anlaesse: ['tagesueberblick', 'faelligkeit', 'zeitblock'], perPush: true, } function meldung(teil: Partial = {}): Meldung { return { anlass: 'faelligkeit', schluessel: 'faelligkeit:a1:2026-03-17', titel: 'Fällig heute', text: 'Angebot schreiben', pfad: '/aufgaben', ...teil, } } describe('VOREINSTELLUNG', () => { it('meldet ab Werk über keinen Weg', () => { // Wer beim ersten Start überschüttet wird, schaltet alles ab. expect(VOREINSTELLUNG.perPush).toBe(false) expect(VOREINSTELLUNG.perMail).toBe(false) }) it('hat eine Ruhezeit über Nacht', () => { expect(VOREINSTELLUNG.ruheVon).toBe(20 * 60) expect(VOREINSTELLUNG.ruheBis).toBe(7 * 60) }) }) describe('inRuhezeit', () => { it('erkennt die Nacht, obwohl sie über Mitternacht läuft', () => { expect(inRuhezeit(einstellung, berlin(21))).toBe(true) expect(inRuhezeit(einstellung, berlin(2))).toBe(true) expect(inRuhezeit(einstellung, berlin(6, 59))).toBe(true) }) it('lässt den Tag frei', () => { expect(inRuhezeit(einstellung, berlin(7))).toBe(false) expect(inRuhezeit(einstellung, berlin(12))).toBe(false) expect(inRuhezeit(einstellung, berlin(19, 59))).toBe(false) }) it('beginnt pünktlich mit der Ruhe', () => { expect(inRuhezeit(einstellung, berlin(20))).toBe(true) }) it('kennt auch eine Ruhezeit innerhalb eines Tages', () => { const mittags: Einstellung = { ...einstellung, ruheVon: 12 * 60, ruheBis: 13 * 60 } expect(inRuhezeit(mittags, berlin(12, 30))).toBe(true) expect(inRuhezeit(mittags, berlin(11))).toBe(false) expect(inRuhezeit(mittags, berlin(14))).toBe(false) }) it('deutet gleiche Zeiten als "keine Ruhezeit"', () => { const ohne: Einstellung = { ...einstellung, ruheVon: 0, ruheBis: 0 } expect(inRuhezeit(ohne, berlin(3))).toBe(false) }) it('schweigt das Wochenende, wenn gewünscht', () => { const still: Einstellung = { ...einstellung, ruheAmWochenende: true } expect(inRuhezeit(still, samstag(12))).toBe(true) expect(inRuhezeit(still, berlin(12))).toBe(false) }) it('lässt das Wochenende sonst in Ruhe', () => { expect(inRuhezeit(einstellung, samstag(12))).toBe(false) }) }) describe('sendenEntscheiden', () => { const leer = new Set() it('lässt eine gewöhnliche Meldung durch', () => { expect(sendenEntscheiden(einstellung, meldung(), leer, berlin(10))).toEqual({ senden: true, }) }) it('schweigt bei abgeschaltetem Anlass', () => { const ohne: Einstellung = { ...einstellung, anlaesse: ['tagesueberblick'] } expect(sendenEntscheiden(ohne, meldung(), leer, berlin(10))).toEqual({ senden: false, grund: 'abgeschaltet', }) }) it('schweigt, wenn es keinen Weg hinaus gibt', () => { const stumm: Einstellung = { ...einstellung, perPush: false, perMail: false } expect(sendenEntscheiden(stumm, meldung(), leer, berlin(10))).toEqual({ senden: false, grund: 'kein-weg', }) }) it('schweigt in der Ruhezeit', () => { expect(sendenEntscheiden(einstellung, meldung(), leer, berlin(22))).toEqual({ senden: false, grund: 'ruhezeit', }) }) it('meldet dieselbe Sache nicht zweimal', () => { const gemeldet = new Set(['faelligkeit:a1:2026-03-17']) expect(sendenEntscheiden(einstellung, meldung(), gemeldet, berlin(10))).toEqual({ senden: false, grund: 'schon-gemeldet', }) }) it('lässt den Tagesüberblick auch in der Ruhezeit hinaus', () => { // Sonst käme er nie, wenn jemand früh aufsteht und die Ruhe bis 07:00 läuft. const ueberblick = meldung({ anlass: 'tagesueberblick', schluessel: 'tagesueberblick:tag:2026-03-17', }) expect(sendenEntscheiden(einstellung, ueberblick, leer, berlin(6, 30))).toEqual({ senden: true, }) }) it('prüft die Abschaltung vor der Ruhezeit', () => { // Ein abgeschalteter Anlass ist abgeschaltet, nicht "gerade still". const ohne: Einstellung = { ...einstellung, anlaesse: [] } expect(sendenEntscheiden(ohne, meldung(), leer, berlin(22)).senden).toBe(false) expect( sendenEntscheiden(ohne, meldung(), leer, berlin(22)) as { grund: string }, ).toMatchObject({ grund: 'abgeschaltet' }) }) }) describe('meldungsSchluessel', () => { it('trennt Anlass, Sache und Tag', () => { expect(meldungsSchluessel('faelligkeit', 'a1', '2026-03-17')).toBe( 'faelligkeit:a1:2026-03-17', ) }) it('erlaubt dieselbe Sache an einem anderen Tag erneut', () => { // Eine Aufgabe, die heute fällig war, ist es morgen immer noch. expect(meldungsSchluessel('faelligkeit', 'a1', '2026-03-17')).not.toBe( meldungsSchluessel('faelligkeit', 'a1', '2026-03-18'), ) }) }) describe('ueberblickFaellig', () => { it('trifft das Zeitfenster', () => { expect(ueberblickFaellig(einstellung, berlin(7))).toBe(true) expect(ueberblickFaellig(einstellung, berlin(7, 14))).toBe(true) }) it('verpasst das Fenster davor und danach', () => { expect(ueberblickFaellig(einstellung, berlin(6, 59))).toBe(false) expect(ueberblickFaellig(einstellung, berlin(7, 15))).toBe(false) }) }) describe('zeitblockErinnerungFaellig', () => { const beginn = berlin(10) it('erinnert im Vorlauffenster', () => { // Vorlauf 10 Minuten, Toleranz 15: von 25 bis 10 Minuten vorher. expect(zeitblockErinnerungFaellig(einstellung, beginn, berlin(9, 55))).toBe(true) }) it('erinnert nicht zu früh', () => { expect(zeitblockErinnerungFaellig(einstellung, beginn, berlin(9, 30))).toBe(false) }) it('erinnert nicht mehr, wenn der Block schon läuft', () => { expect(zeitblockErinnerungFaellig(einstellung, beginn, berlin(10, 5))).toBe(false) }) }) describe('einstellungSchema', () => { const gueltig = { anlaesse: ['tagesueberblick'], perPush: 'true', perMail: '', ueberblickUm: '07:00', ruheVon: '20:00', ruheBis: '07:00', ruheAmWochenende: '', zeitblockVorlaufMinuten: '10', } it('liest Uhrzeiten in Minuten um', () => { const ergebnis = einstellungSchema.parse(gueltig) expect(ergebnis.ueberblickUm).toBe(420) expect(ergebnis.ruheVon).toBe(1200) }) it('weist unsinnige Uhrzeiten ab', () => { expect(einstellungSchema.safeParse({ ...gueltig, ruheVon: '25:00' }).success).toBe( false, ) }) it('begrenzt den Vorlauf', () => { expect( einstellungSchema.safeParse({ ...gueltig, zeitblockVorlaufMinuten: '121' }).success, ).toBe(false) expect( einstellungSchema.safeParse({ ...gueltig, zeitblockVorlaufMinuten: '120' }).success, ).toBe(true) }) it('weist unbekannte Anlässe ab', () => { expect( einstellungSchema.safeParse({ ...gueltig, anlaesse: ['geburtstag'] }).success, ).toBe(false) }) }) describe('ruhezeitText', () => { it('beschreibt die Nachtruhe', () => { expect(ruhezeitText(einstellung)).toBe('Still von 20:00 bis 07:00.') }) it('nennt das Wochenende mit', () => { expect(ruhezeitText({ ...einstellung, ruheAmWochenende: true })).toBe( 'Still von 20:00 bis 07:00 und am ganzen Wochenende.', ) }) it('sagt es auch, wenn es keine Ruhezeit gibt', () => { expect(ruhezeitText({ ...einstellung, ruheVon: 0, ruheBis: 0 })).toBe( 'Keine Ruhezeit.', ) }) })