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>
459 lines
20 KiB
TypeScript
459 lines
20 KiB
TypeScript
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('next-intl', () => ({
|
|
useTranslations: () => (key: string) => 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(),
|
|
createReminder: vi.fn(),
|
|
updateReminder: vi.fn(),
|
|
snoozeReminder: vi.fn(),
|
|
deleteReminder: vi.fn(),
|
|
getReminderEmailStatus: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import {
|
|
createReminder,
|
|
deleteReminder,
|
|
getReminderEmailStatus,
|
|
listReminders,
|
|
ReminderRequestError,
|
|
snoozeReminder,
|
|
updateReminder,
|
|
} from '@/lib/reminders-api';
|
|
import { ReminderWidget } from './reminder-widget';
|
|
|
|
const mockList = listReminders as ReturnType<typeof vi.fn>;
|
|
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 {
|
|
id,
|
|
title,
|
|
description,
|
|
dueAt,
|
|
emailEnabled: false,
|
|
createdAt: dueAt,
|
|
updatedAt: dueAt,
|
|
};
|
|
}
|
|
|
|
const props = { instanceId: 'w1', config: {}, isEditMode: false };
|
|
|
|
let requestPermission: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
window.localStorage.clear();
|
|
mockList.mockReset();
|
|
mockCreate.mockReset();
|
|
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 });
|
|
vi.stubGlobal('Notification', ctor);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
async function openForm() {
|
|
fireEvent.click(await screen.findByText('reminder.add'));
|
|
}
|
|
|
|
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.timeLabel'), { target: { value: '14:30' } });
|
|
fireEvent.click(screen.getByText('reminder.save'));
|
|
}
|
|
|
|
describe('ReminderWidget', () => {
|
|
it('listet die Erinnerungen nach Faelligkeit sortiert', async () => {
|
|
mockList.mockResolvedValue([
|
|
reminder('b', 'Spaeter', '2099-01-02T10:00:00.000Z'),
|
|
reminder('a', 'Frueh', '2099-01-01T10:00:00.000Z', 'Beschreibung'),
|
|
]);
|
|
render(<ReminderWidget {...props} />);
|
|
const rows = await screen.findAllByTestId('reminder-row');
|
|
expect(rows[0].textContent).toContain('Frueh');
|
|
expect(rows[0].textContent).toContain('Beschreibung');
|
|
expect(rows[1].textContent).toContain('Spaeter');
|
|
});
|
|
|
|
it('zeigt den Leerzustand', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
render(<ReminderWidget {...props} />);
|
|
expect(await screen.findByText('reminder.empty')).toBeTruthy();
|
|
});
|
|
|
|
it('das Rendern fragt keine Benachrichtigungs-Erlaubnis (D-04)', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
render(<ReminderWidget {...props} />);
|
|
await screen.findByText('reminder.empty');
|
|
expect(requestPermission).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Anlegen sendet die aus den Ortszeit-Eingaben gebildete ISO-Zeit', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
mockCreate.mockResolvedValue(reminder('n', 'Zahnarzt', '2099-03-04T13:30:00.000Z'));
|
|
render(<ReminderWidget {...props} />);
|
|
await openForm();
|
|
fillAndSubmit('Zahnarzt');
|
|
await waitFor(() => expect(mockCreate).toHaveBeenCalledTimes(1));
|
|
const arg = mockCreate.mock.calls[0][0];
|
|
expect(arg.title).toBe('Zahnarzt');
|
|
expect(arg.dueAt).toBe(new Date('2099-03-04T14:30').toISOString());
|
|
});
|
|
|
|
it('die erste Anlage fragt die Erlaubnis einmal, die zweite nicht mehr', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
mockCreate.mockResolvedValue(reminder('n', 'x', '2099-03-04T13:30:00.000Z'));
|
|
render(<ReminderWidget {...props} />);
|
|
|
|
await openForm();
|
|
fillAndSubmit('Erste');
|
|
await waitFor(() => expect(mockCreate).toHaveBeenCalledTimes(1));
|
|
expect(requestPermission).toHaveBeenCalledTimes(1);
|
|
|
|
// Dialog ist nach dem Speichern zu; neu oeffnen
|
|
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
(Notification as unknown as { permission: string }).permission = 'default';
|
|
await openForm();
|
|
fillAndSubmit('Zweite');
|
|
await waitFor(() => expect(mockCreate).toHaveBeenCalledTimes(2));
|
|
expect(requestPermission).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('lehnt eine Vergangenheit im Formular ab, ohne zu senden', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
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.click(screen.getByText('reminder.save'));
|
|
expect(await screen.findByText('reminder.pastError')).toBeTruthy();
|
|
expect(mockCreate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('meldet REMINDERS_CHANGED nach dem Anlegen', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
mockCreate.mockResolvedValue(reminder('n', 'x', '2099-03-04T13:30:00.000Z'));
|
|
const handler = vi.fn();
|
|
window.addEventListener('tessera:reminders-changed', handler);
|
|
render(<ReminderWidget {...props} />);
|
|
await openForm();
|
|
await act(async () => {
|
|
fillAndSubmit('Meldung');
|
|
});
|
|
await waitFor(() => expect(handler).toHaveBeenCalled());
|
|
window.removeEventListener('tessera:reminders-changed', handler);
|
|
});
|
|
});
|
|
|
|
const iso = (offsetMs: number) => new Date(Date.now() + offsetMs).toISOString();
|
|
|
|
describe('ReminderWidget — faellig und Aktionen (D-03)', () => {
|
|
it('eine faellige Zeile ist hervorgehoben mit Faellig, Erledigt und Spaeter erinnern, ohne Bearbeiten/Loeschen', async () => {
|
|
mockList.mockResolvedValue([reminder('a', 'Faellig', iso(-60_000))]);
|
|
render(<ReminderWidget {...props} />);
|
|
const row = await screen.findByTestId('reminder-row');
|
|
expect(row.getAttribute('data-due')).toBe('true');
|
|
expect(row.className).toContain('border-status-warn');
|
|
expect(within(row).getByText('reminder.due')).toBeTruthy();
|
|
expect(within(row).getByText('reminder.done')).toBeTruthy();
|
|
expect(within(row).getByText('reminder.snooze')).toBeTruthy();
|
|
expect(within(row).queryByLabelText('reminder.edit')).toBeNull();
|
|
expect(within(row).queryByLabelText('reminder.delete')).toBeNull();
|
|
});
|
|
|
|
it('eine kuenftige Zeile bietet Bearbeiten und Loeschen, aber nicht Erledigt/Spaeter', async () => {
|
|
mockList.mockResolvedValue([reminder('a', 'Kommt', iso(3600_000))]);
|
|
render(<ReminderWidget {...props} />);
|
|
const row = await screen.findByTestId('reminder-row');
|
|
expect(row.getAttribute('data-due')).toBe('false');
|
|
expect(within(row).getByLabelText('reminder.edit')).toBeTruthy();
|
|
expect(within(row).getByLabelText('reminder.delete')).toBeTruthy();
|
|
expect(within(row).queryByText('reminder.done')).toBeNull();
|
|
expect(within(row).queryByText('reminder.snooze')).toBeNull();
|
|
});
|
|
|
|
it('Erledigt ruft deleteReminder und die Zeile verschwindet nach dem Neuladen', async () => {
|
|
mockList.mockResolvedValueOnce([reminder('a', 'Faellig', iso(-60_000))]).mockResolvedValue([]);
|
|
mockDelete.mockResolvedValue({ deleted: true });
|
|
render(<ReminderWidget {...props} />);
|
|
fireEvent.click(await screen.findByText('reminder.done'));
|
|
await waitFor(() => expect(mockDelete).toHaveBeenCalledWith('a'));
|
|
await waitFor(() => expect(screen.queryByTestId('reminder-row')).toBeNull());
|
|
});
|
|
|
|
it('jede Spaeter-Option ruft snoozeReminder mit dem Zeitpunkt aus snoozeTarget', async () => {
|
|
const original = new Date(Date.now() - 5 * 60_000);
|
|
mockList.mockResolvedValue([reminder('a', 'Faellig', original.toISOString())]);
|
|
mockSnooze.mockResolvedValue({});
|
|
render(<ReminderWidget {...props} />);
|
|
|
|
for (const [label, expected] of [
|
|
['reminder.snooze10m', () => Date.now() + 10 * 60_000],
|
|
['reminder.snooze1h', () => Date.now() + 60 * 60_000],
|
|
] as const) {
|
|
mockSnooze.mockClear();
|
|
fireEvent.click(await screen.findByText('reminder.snooze'));
|
|
fireEvent.click(await screen.findByText(label));
|
|
await waitFor(() => expect(mockSnooze).toHaveBeenCalledTimes(1));
|
|
const [id, dueAt] = mockSnooze.mock.calls[0];
|
|
expect(id).toBe('a');
|
|
expect(Math.abs(new Date(dueAt).getTime() - expected())).toBeLessThan(5_000);
|
|
}
|
|
|
|
mockSnooze.mockClear();
|
|
fireEvent.click(await screen.findByText('reminder.snooze'));
|
|
fireEvent.click(await screen.findByText('reminder.snoozeTomorrow'));
|
|
await waitFor(() => expect(mockSnooze).toHaveBeenCalledTimes(1));
|
|
const tomorrow = new Date(original);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
expect(mockSnooze.mock.calls[0][1]).toBe(tomorrow.toISOString());
|
|
});
|
|
|
|
it('Loeschen fragt zweistufig nach und ruft dann deleteReminder', async () => {
|
|
mockList.mockResolvedValue([reminder('a', 'Kommt', iso(3600_000))]);
|
|
mockDelete.mockResolvedValue({ deleted: true });
|
|
render(<ReminderWidget {...props} />);
|
|
fireEvent.click(await screen.findByLabelText('reminder.delete'));
|
|
expect(mockDelete).not.toHaveBeenCalled();
|
|
fireEvent.click(screen.getByText('reminder.yes'));
|
|
await waitFor(() => expect(mockDelete).toHaveBeenCalledWith('a'));
|
|
});
|
|
|
|
it('Bearbeiten oeffnet das Formular mit den Werten und speichert per updateReminder', async () => {
|
|
const due = new Date(Date.now() + 3600_000);
|
|
mockList.mockResolvedValue([reminder('a', 'Alt', due.toISOString(), 'Text')]);
|
|
mockUpdate.mockResolvedValue({});
|
|
render(<ReminderWidget {...props} />);
|
|
fireEvent.click(await screen.findByLabelText('reminder.edit'));
|
|
const titleInput = screen.getByLabelText('reminder.titleLabel') as HTMLInputElement;
|
|
expect(titleInput.value).toBe('Alt');
|
|
fireEvent.change(titleInput, { target: { value: 'Neu' } });
|
|
fireEvent.click(screen.getByText('reminder.save'));
|
|
await waitFor(() => expect(mockUpdate).toHaveBeenCalledTimes(1));
|
|
const [id, patch] = mockUpdate.mock.calls[0];
|
|
expect(id).toBe('a');
|
|
expect(patch.title).toBe('Neu');
|
|
expect(patch.description).toBe('Text');
|
|
// Bearbeiten fragt nie die Benachrichtigungs-Erlaubnis
|
|
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));
|
|
render(<ReminderWidget {...props} />);
|
|
fireEvent.click(await screen.findByText('reminder.snooze'));
|
|
fireEvent.click(await screen.findByText('reminder.snooze10m'));
|
|
expect(await screen.findByText('reminder.notDue')).toBeTruthy();
|
|
});
|
|
|
|
it('zeigt bei blockierten Browser-Benachrichtigungen einen Hinweis, sonst nicht', async () => {
|
|
mockList.mockResolvedValue([]);
|
|
(Notification as unknown as { permission: string }).permission = 'denied';
|
|
const { unmount } = render(<ReminderWidget {...props} />);
|
|
expect(await screen.findByTestId('reminder-permission-hint')).toBeTruthy();
|
|
unmount();
|
|
(Notification as unknown as { permission: string }).permission = 'default';
|
|
render(<ReminderWidget {...props} />);
|
|
await screen.findByText('reminder.empty');
|
|
expect(screen.queryByTestId('reminder-permission-hint')).toBeNull();
|
|
});
|
|
|
|
it('zeigt in der Desktop-App keinen Hinweis', async () => {
|
|
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ = { invoke: vi.fn() };
|
|
(Notification as unknown as { permission: string }).permission = 'denied';
|
|
mockList.mockResolvedValue([]);
|
|
render(<ReminderWidget {...props} />);
|
|
await screen.findByText('reminder.empty');
|
|
expect(screen.queryByTestId('reminder-permission-hint')).toBeNull();
|
|
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
|
|
});
|
|
});
|
|
|
|
describe('ReminderWidget — Faelligkeit ohne Neuladen', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('eine Zeile wird durch den lokalen 10-s-Takt faellig', async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-09-29T12:00:00.000Z'));
|
|
mockList.mockResolvedValue([reminder('a', 'Bald', '2026-09-29T12:00:30.000Z')]);
|
|
render(<ReminderWidget {...props} />);
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
});
|
|
expect(screen.getByTestId('reminder-row').getAttribute('data-due')).toBe('false');
|
|
await act(async () => {
|
|
await vi.advanceTimersByTimeAsync(40_000);
|
|
});
|
|
expect(screen.getByTestId('reminder-row').getAttribute('data-due')).toBe('true');
|
|
});
|
|
});
|
|
|
|
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();
|
|
// 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');
|
|
});
|
|
});
|