feat(260929-if2): Erinnerung zusaetzlich per E-Mail, Doku und Aenderungsliste
- E-Mail-Planer: Anspruch vor dem Senden (genau eine Mail je Faelligkeit, hoechstens 3 Versuche), Systemlesen nur fuer die Kandidatenabfrage - MailService.sendReminderEmail (Berliner Zeit, nur Text), GET /reminders/email-status, Haken im Formular mit Erklaerung - Zugriffsklassifikation und Erlaubnisliste fuer forSystem nachgezogen, Aenderungsliste und Anwenderanleitung Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ vi.mock('@/lib/reminders-api', async () => {
|
||||
updateReminder: vi.fn(),
|
||||
snoozeReminder: vi.fn(),
|
||||
deleteReminder: vi.fn(),
|
||||
getReminderEmailStatus: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
ReminderRequestError,
|
||||
createReminder,
|
||||
deleteReminder,
|
||||
getReminderEmailStatus,
|
||||
listReminders,
|
||||
snoozeReminder,
|
||||
updateReminder,
|
||||
@@ -34,6 +36,7 @@ const mockCreate = createReminder as ReturnType<typeof vi.fn>;
|
||||
const mockUpdate = updateReminder as ReturnType<typeof vi.fn>;
|
||||
const mockSnooze = snoozeReminder as ReturnType<typeof vi.fn>;
|
||||
const mockDelete = deleteReminder as ReturnType<typeof vi.fn>;
|
||||
const mockEmailStatus = getReminderEmailStatus as ReturnType<typeof vi.fn>;
|
||||
|
||||
function reminder(id: string, title: string, dueAt: string, description = '') {
|
||||
return {
|
||||
@@ -58,6 +61,8 @@ beforeEach(() => {
|
||||
mockUpdate.mockReset();
|
||||
mockSnooze.mockReset();
|
||||
mockDelete.mockReset();
|
||||
mockEmailStatus.mockReset();
|
||||
mockEmailStatus.mockResolvedValue({ smtpConfigured: true, hasEmail: true });
|
||||
requestPermission = vi.fn(async () => 'granted');
|
||||
const ctor = vi.fn();
|
||||
Object.assign(ctor, { permission: 'default', requestPermission });
|
||||
@@ -307,3 +312,87 @@ describe('ReminderWidget — Faelligkeit ohne Neuladen', () => {
|
||||
expect(screen.getByTestId('reminder-row').getAttribute('data-due')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReminderWidget — E-Mail-Erinnerung (Aufgabe 3, E-09)', () => {
|
||||
async function openFormAndWaitForStatus() {
|
||||
await openForm();
|
||||
// Der Status wird beim Einhaengen der Kachel geladen
|
||||
await waitFor(() => expect(mockEmailStatus).toHaveBeenCalledTimes(1));
|
||||
return screen.getByLabelText('reminder.emailLabel') as HTMLInputElement;
|
||||
}
|
||||
|
||||
it('ist bedienbar und wird als emailEnabled gesendet, wenn Versand und Adresse da sind', async () => {
|
||||
mockList.mockResolvedValue([]);
|
||||
mockCreate.mockResolvedValue(reminder('n', 'x', '2099-03-04T13:30:00.000Z'));
|
||||
render(<ReminderWidget {...props} />);
|
||||
const box = await openFormAndWaitForStatus();
|
||||
await waitFor(() => expect(box.disabled).toBe(false));
|
||||
fireEvent.click(box);
|
||||
fillAndSubmit('Mit Mail');
|
||||
await waitFor(() => expect(mockCreate).toHaveBeenCalledTimes(1));
|
||||
expect(mockCreate.mock.calls[0][0].emailEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('ohne E-Mail-Haken wird emailEnabled gar nicht gesendet', async () => {
|
||||
mockList.mockResolvedValue([]);
|
||||
mockCreate.mockResolvedValue(reminder('n', 'x', '2099-03-04T13:30:00.000Z'));
|
||||
render(<ReminderWidget {...props} />);
|
||||
await openFormAndWaitForStatus();
|
||||
fillAndSubmit('Ohne Mail');
|
||||
await waitFor(() => expect(mockCreate).toHaveBeenCalledTimes(1));
|
||||
expect(mockCreate.mock.calls[0][0]).not.toHaveProperty('emailEnabled');
|
||||
});
|
||||
|
||||
it('ist ausgegraut mit Erklaerung, wenn kein E-Mail-Versand eingerichtet ist', async () => {
|
||||
mockEmailStatus.mockResolvedValue({ smtpConfigured: false, hasEmail: true });
|
||||
mockList.mockResolvedValue([]);
|
||||
render(<ReminderWidget {...props} />);
|
||||
const box = await openFormAndWaitForStatus();
|
||||
expect(await screen.findByText('reminder.emailNoSmtp')).toBeTruthy();
|
||||
expect(box.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('ist ausgegraut mit eigener Erklaerung, wenn das Konto keine Adresse hat', async () => {
|
||||
mockEmailStatus.mockResolvedValue({ smtpConfigured: true, hasEmail: false });
|
||||
mockList.mockResolvedValue([]);
|
||||
render(<ReminderWidget {...props} />);
|
||||
const box = await openFormAndWaitForStatus();
|
||||
expect(await screen.findByText('reminder.emailNoAddress')).toBeTruthy();
|
||||
expect(box.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('gilt als nicht moeglich, solange der Status fehlt oder fehlschlaegt', async () => {
|
||||
mockEmailStatus.mockRejectedValue(new Error('offline'));
|
||||
mockList.mockResolvedValue([]);
|
||||
render(<ReminderWidget {...props} />);
|
||||
const box = await openFormAndWaitForStatus();
|
||||
expect(box.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('zeigt bei Zeilen mit emailEnabled ein Mail-Symbol, sonst nicht', async () => {
|
||||
mockList.mockResolvedValue([
|
||||
{ ...reminder('a', 'Mit', '2099-01-01T10:00:00.000Z'), emailEnabled: true },
|
||||
reminder('b', 'Ohne', '2099-01-02T10:00:00.000Z'),
|
||||
]);
|
||||
render(<ReminderWidget {...props} />);
|
||||
const rows = await screen.findAllByTestId('reminder-row');
|
||||
expect(within(rows[0]).getByTestId('reminder-email-icon')).toBeTruthy();
|
||||
expect(within(rows[1]).queryByTestId('reminder-email-icon')).toBeNull();
|
||||
});
|
||||
|
||||
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 },
|
||||
]);
|
||||
mockUpdate.mockResolvedValue({});
|
||||
render(<ReminderWidget {...props} />);
|
||||
fireEvent.click(await screen.findByLabelText('reminder.edit'));
|
||||
const box = screen.getByLabelText('reminder.emailLabel') as HTMLInputElement;
|
||||
expect(box.checked).toBe(true);
|
||||
expect(box.disabled).toBe(false); // angehakt bleibt bedienbar, zum Abwaehlen
|
||||
fireEvent.click(screen.getByText('reminder.save'));
|
||||
await waitFor(() => expect(mockUpdate).toHaveBeenCalledTimes(1));
|
||||
expect(mockUpdate.mock.calls[0][1]).not.toHaveProperty('emailEnabled');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user