import { act, render } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('next-intl', () => ({ useTranslations: () => (key: string, values?: Record) => values?.title ? `${key}:${values.title}` : key, useLocale: () => 'de', })); vi.mock('@/lib/reminders-api', async () => { const actual = await vi.importActual('@/lib/reminders-api'); return { REMINDERS_CHANGED_EVENT: actual.REMINDERS_CHANGED_EVENT, ReminderRequestError: actual.ReminderRequestError, listReminders: vi.fn(), }; }); vi.mock('@/lib/reminder-notify', async () => { const actual = await vi.importActual('@/lib/reminder-notify'); return { ...actual, showReminderNotification: vi.fn(async () => undefined) }; }); import { resetNotifiedMemoryForTests, showReminderNotification } from '@/lib/reminder-notify'; import { listReminders, ReminderRequestError } from '@/lib/reminders-api'; import { ReminderNotifier } from './reminder-notifier'; const mockList = listReminders as ReturnType; const mockShow = showReminderNotification as ReturnType; const NOW = new Date('2026-09-29T12:00:00.000Z'); function reminder(over: Partial<{ id: string; dueAt: string; description: string }> = {}) { return { id: 'r1', title: 'Zahnarzt', description: '', dueAt: new Date(NOW.getTime() + 30_000).toISOString(), emailEnabled: false, createdAt: NOW.toISOString(), updatedAt: NOW.toISOString(), ...over, }; } async function advance(ms: number) { await act(async () => { await vi.advanceTimersByTimeAsync(ms); }); } beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(NOW); window.localStorage.clear(); resetNotifiedMemoryForTests(); mockList.mockReset(); mockShow.mockClear(); }); afterEach(() => { vi.useRealTimers(); }); describe('ReminderNotifier', () => { it('meldet eine faellige Erinnerung genau einmal ueber mehrere Takte', async () => { mockList.mockResolvedValue([reminder()]); render(); await advance(0); expect(mockShow).not.toHaveBeenCalled(); // noch nicht faellig (D-02) await advance(40_000); // ueber dueAt (+30 s) hinweg await advance(30_000); expect(mockShow).toHaveBeenCalledTimes(1); expect(mockShow.mock.calls[0][0]).toMatchObject({ title: 'reminder.notificationTitle:Zahnarzt', tag: `reminder-r1-${reminder().dueAt}`, }); }); it('meldet nichts, solange nichts faellig ist', async () => { mockList.mockResolvedValue([ reminder({ dueAt: new Date(NOW.getTime() + 3600_000).toISOString() }), ]); render(); await advance(30_000); expect(mockShow).not.toHaveBeenCalled(); }); it('nimmt die Beschreibung als Text, gekuerzt auf 200 Zeichen', async () => { mockList.mockResolvedValue([ reminder({ dueAt: new Date(NOW.getTime() - 1000).toISOString(), description: 'x'.repeat(300), }), ]); render(); await advance(0); expect(mockShow.mock.calls[0][0].body).toBe('x'.repeat(200)); }); it('meldet nach geaendertem dueAt (Verschieben) erneut', async () => { const first = reminder({ dueAt: new Date(NOW.getTime() - 1000).toISOString() }); mockList.mockResolvedValue([first]); render(); await advance(0); expect(mockShow).toHaveBeenCalledTimes(1); // Verschoben auf +10 s (in der Zukunft), dann faellig mockList.mockResolvedValue([{ ...first, dueAt: new Date(Date.now() + 10_000).toISOString() }]); await act(async () => { window.dispatchEvent(new Event('tessera:reminders-changed')); await vi.advanceTimersByTimeAsync(0); }); expect(mockShow).toHaveBeenCalledTimes(1); await advance(20_000); expect(mockShow).toHaveBeenCalledTimes(2); }); it('das Aenderungsereignis laedt die Liste neu', async () => { mockList.mockResolvedValue([]); render(); await advance(0); const before = mockList.mock.calls.length; await act(async () => { window.dispatchEvent(new Event('tessera:reminders-changed')); await vi.advanceTimersByTimeAsync(0); }); expect(mockList.mock.calls.length).toBe(before + 1); }); it('pausiert bei 401 bis zum naechsten Fokus', async () => { mockList.mockRejectedValue(new ReminderRequestError(401)); render(); await advance(0); const after401 = mockList.mock.calls.length; await advance(180_000); expect(mockList.mock.calls.length).toBe(after401); mockList.mockResolvedValue([]); await act(async () => { window.dispatchEvent(new Event('focus')); await vi.advanceTimersByTimeAsync(0); }); expect(mockList.mock.calls.length).toBe(after401 + 1); }); });