c2e4467dd8
- Merkliste zusaetzlich im Arbeitsspeicher (sonst alle 10 s dieselbe Meldung) - Aendern der Faelligkeit atomar gegen gleichzeitiges Faelligwerden, setzt Mail-Spur zurueck - UpdateReminderDto lehnt null ab (400 statt 500) - keine Mails an deaktivierte Benutzer - Spaeter erinnern beschriftet heute/morgen nach dem berechneten Zeitpunkt - Bearbeiten schickt dueAt nur bei geaenderter Zeit Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
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<string, string>) =>
|
|
values?.title ? `${key}:${values.title}` : key,
|
|
useLocale: () => 'de',
|
|
}));
|
|
|
|
vi.mock('@/lib/reminders-api', async () => {
|
|
const actual = await vi.importActual<typeof import('@/lib/reminders-api')>('@/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<typeof import('@/lib/reminder-notify')>('@/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<typeof vi.fn>;
|
|
const mockShow = showReminderNotification as ReturnType<typeof vi.fn>;
|
|
|
|
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(<ReminderNotifier />);
|
|
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(<ReminderNotifier />);
|
|
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(<ReminderNotifier />);
|
|
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(<ReminderNotifier />);
|
|
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(<ReminderNotifier />);
|
|
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(<ReminderNotifier />);
|
|
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);
|
|
});
|
|
});
|