fix(reminders): keine Dauermeldung ohne localStorage, Mail nach Zeitaenderung, null-Pruefung
- 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>
This commit is contained in:
@@ -21,11 +21,11 @@ vi.mock('@/lib/reminders-api', async () => {
|
||||
});
|
||||
|
||||
import {
|
||||
ReminderRequestError,
|
||||
createReminder,
|
||||
deleteReminder,
|
||||
getReminderEmailStatus,
|
||||
listReminders,
|
||||
ReminderRequestError,
|
||||
snoozeReminder,
|
||||
updateReminder,
|
||||
} from '@/lib/reminders-api';
|
||||
@@ -79,7 +79,9 @@ async function openForm() {
|
||||
|
||||
function fillAndSubmit(title: string) {
|
||||
fireEvent.change(screen.getByLabelText('reminder.titleLabel'), { target: { value: title } });
|
||||
fireEvent.change(screen.getByLabelText('reminder.dateLabel'), { target: { value: '2099-03-04' } });
|
||||
fireEvent.change(screen.getByLabelText('reminder.dateLabel'), {
|
||||
target: { value: '2099-03-04' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('reminder.timeLabel'), { target: { value: '14:30' } });
|
||||
fireEvent.click(screen.getByText('reminder.save'));
|
||||
}
|
||||
@@ -146,7 +148,9 @@ describe('ReminderWidget', () => {
|
||||
render(<ReminderWidget {...props} />);
|
||||
await openForm();
|
||||
fireEvent.change(screen.getByLabelText('reminder.titleLabel'), { target: { value: 'x' } });
|
||||
fireEvent.change(screen.getByLabelText('reminder.dateLabel'), { target: { value: '2001-01-01' } });
|
||||
fireEvent.change(screen.getByLabelText('reminder.dateLabel'), {
|
||||
target: { value: '2001-01-01' },
|
||||
});
|
||||
fireEvent.click(screen.getByText('reminder.save'));
|
||||
expect(await screen.findByText('reminder.pastError')).toBeTruthy();
|
||||
expect(mockCreate).not.toHaveBeenCalled();
|
||||
@@ -260,6 +264,38 @@ describe('ReminderWidget — faellig und Aktionen (D-03)', () => {
|
||||
expect(requestPermission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Bearbeiten ohne Aenderung von Datum/Uhrzeit sendet kein dueAt (keine Sekunden-Verschiebung)', async () => {
|
||||
// Faellig in 30 s, mit Sekunden: auf Minuten gekuerzt laege sie in der Vergangenheit.
|
||||
const due = new Date(Date.now() + 30_000);
|
||||
due.setSeconds(due.getSeconds(), 500);
|
||||
mockList.mockResolvedValue([reminder('a', 'Alt', due.toISOString())]);
|
||||
mockUpdate.mockResolvedValue({});
|
||||
render(<ReminderWidget {...props} />);
|
||||
fireEvent.click(await screen.findByLabelText('reminder.edit'));
|
||||
fireEvent.change(screen.getByLabelText('reminder.titleLabel'), { target: { value: 'Neu' } });
|
||||
fireEvent.click(screen.getByText('reminder.save'));
|
||||
await waitFor(() => expect(mockUpdate).toHaveBeenCalledTimes(1));
|
||||
expect(screen.queryByText('reminder.pastError')).toBeNull();
|
||||
expect(mockUpdate.mock.calls[0][1]).not.toHaveProperty('dueAt');
|
||||
expect(mockUpdate.mock.calls[0][1].title).toBe('Neu');
|
||||
});
|
||||
|
||||
it('Bearbeiten mit geaenderter Uhrzeit sendet das neue dueAt', async () => {
|
||||
const due = new Date(Date.now() + 2 * 3600_000);
|
||||
mockList.mockResolvedValue([reminder('a', 'Alt', due.toISOString())]);
|
||||
mockUpdate.mockResolvedValue({});
|
||||
render(<ReminderWidget {...props} />);
|
||||
fireEvent.click(await screen.findByLabelText('reminder.edit'));
|
||||
const next = new Date(Date.now() + 26 * 3600_000);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const nextDate = `${next.getFullYear()}-${pad(next.getMonth() + 1)}-${pad(next.getDate())}`;
|
||||
fireEvent.change(screen.getByLabelText('reminder.dateLabel'), { target: { value: nextDate } });
|
||||
fireEvent.change(screen.getByLabelText('reminder.timeLabel'), { target: { value: '09:15' } });
|
||||
fireEvent.click(screen.getByText('reminder.save'));
|
||||
await waitFor(() => expect(mockUpdate).toHaveBeenCalledTimes(1));
|
||||
expect(mockUpdate.mock.calls[0][1].dueAt).toBe(new Date(`${nextDate}T09:15`).toISOString());
|
||||
});
|
||||
|
||||
it('409 beim Verschieben zeigt den Hinweis und laedt neu', async () => {
|
||||
mockList.mockResolvedValue([reminder('a', 'Faellig', iso(-60_000))]);
|
||||
mockSnooze.mockRejectedValue(new ReminderRequestError(409));
|
||||
@@ -313,6 +349,27 @@ describe('ReminderWidget — Faelligkeit ohne Neuladen', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReminderWidget — Spaeter erinnern kurz nach Mitternacht', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('faellig gestern 23:50, jetzt 00:10: die Option heisst „Heute“ und verschiebt auf heute 23:50', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
vi.setSystemTime(new Date(2026, 8, 30, 0, 10));
|
||||
mockList.mockResolvedValue([
|
||||
reminder('a', 'Spaet', new Date(2026, 8, 29, 23, 50).toISOString()),
|
||||
]);
|
||||
mockSnooze.mockResolvedValue({});
|
||||
render(<ReminderWidget {...props} />);
|
||||
fireEvent.click(await screen.findByText('reminder.snooze'));
|
||||
expect(screen.queryByText('reminder.snoozeTomorrow')).toBeNull();
|
||||
fireEvent.click(await screen.findByText('reminder.snoozeToday'));
|
||||
await waitFor(() => expect(mockSnooze).toHaveBeenCalledTimes(1));
|
||||
expect(mockSnooze.mock.calls[0][1]).toBe(new Date(2026, 8, 30, 23, 50).toISOString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReminderWidget — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
async function openFormAndWaitForStatus() {
|
||||
await openForm();
|
||||
@@ -383,7 +440,10 @@ describe('ReminderWidget — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
it('Bearbeiten einer Erinnerung mit angehakter Mail sendet emailEnabled nur bei Aenderung', async () => {
|
||||
mockEmailStatus.mockResolvedValue({ smtpConfigured: false, hasEmail: true });
|
||||
mockList.mockResolvedValue([
|
||||
{ ...reminder('a', 'Alt', new Date(Date.now() + 3600_000).toISOString()), emailEnabled: true },
|
||||
{
|
||||
...reminder('a', 'Alt', new Date(Date.now() + 3600_000).toISOString()),
|
||||
emailEnabled: true,
|
||||
},
|
||||
]);
|
||||
mockUpdate.mockResolvedValue({});
|
||||
render(<ReminderWidget {...props} />);
|
||||
|
||||
Reference in New Issue
Block a user