Files
tessera-ctl/apps/web/src/components/reminders/reminder-notifier.test.tsx
T
schalli 53a49a109f
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 1m45s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m35s
perf(desktop): Dashboard-Aufbau in der Linux-App ohne Einblend-Animation, Melder gegen Fokus-Schuebe gedrosselt
Gemessen in der echten Linux-App (WebKitGTK): Haenger beim Oeffnen des
Dashboards von 550-1100 ms auf 250-350 ms. Im alpha-Log kamen beim Oeffnen
bis zu 50 Abfragen je Sekunde von Erinnerungs- und Nextcloud-Melder; Fokus-/
Sichtbarkeitswechsel loesen jetzt hoechstens alle 10 s eine Abfrage aus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-03 01:19:35 +02:00

159 lines
5.3 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);
});
it('viele Fokus-Ereignisse kurz hintereinander loesen nur eine Abfrage aus', async () => {
mockList.mockResolvedValue([]);
render(<ReminderNotifier />);
await advance(0);
await advance(15_000);
const before = mockList.mock.calls.length;
await act(async () => {
for (let i = 0; i < 50; i++) window.dispatchEvent(new Event('focus'));
await vi.advanceTimersByTimeAsync(0);
});
expect(mockList.mock.calls.length).toBe(before + 1);
});
});