feat(notifications): add channel routing matrix, daily summary, and task watch button

This commit is contained in:
sandrodach committed 2026-10-30 09:32:52 +01:00
1 parent 6795edbf4b
commit d2afcf49df
7 files changed
+461 -32

No files matched your search

@@ -0,0 +1,106 @@
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { UserSettingsView } from '../UserSettingsView';
import { api } from '../../../lib/api';
import { User } from '../../../lib/types';
vi.mock('../../../lib/api', async () => {
const actual = await vi.importActual<typeof import('../../../lib/api')>('../../../lib/api');
return {
...actual,
api: {
...actual.api,
getMe: vi.fn(async () => ({ isLastAdmin: false })),
getAuthStatus: vi.fn(async () => ({ demoMode: false })),
updateSettings: vi.fn(async (patch) => ({ ...baseUser, ...patch })),
},
};
});
const baseUser: User = {
id: 'u1',
email: 'anna@example.com',
displayName: 'Anna',
role: 'member',
isActive: true,
alarmChannels: ['email'],
createdAt: '2026-10-30T00:00:00Z',
};
describe('UserSettingsView notification matrix', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('derives events.due from alarmChannels, disables Discord without webhook, and saves the exact notificationSettings object', async () => {
const onUserUpdated = vi.fn();
render(
<UserSettingsView
currentUser={baseUser}
activeTheme="dark"
themes={[]}
customThemes={[]}
onThemeChange={() => {}}
onCustomThemesChange={() => {}}
onUserUpdated={onUserUpdated}
onBack={() => {}}
/>
);
// Open "Alarme" section
fireEvent.click(screen.getByText('Alarme'));
// Without Discord webhook, Discord switches are disabled and hint is visible
const discordSwitches = screen.getAllByTitle('Bitte zuerst unten eine Discord Webhook URL eintragen');
expect(discordSwitches.length).toBe(5);
expect((discordSwitches[0] as HTMLButtonElement).disabled).toBe(true);
expect(screen.getByText(/Discord-Schalter sind deaktiviert/)).toBeTruthy();
// Enter a Discord Webhook URL to enable the Discord column
fireEvent.change(screen.getByPlaceholderText(/https:\/\/discord\.com\/api\/webhooks/), {
target: { value: 'https://discord.com/api/webhooks/123/abc' },
});
// Derive check: alarmChannels=['email'] -> due.email=true, due.discord=false
expect(screen.getByTitle('Fällige Aufgaben (sofort) – E-Mail').getAttribute('aria-checked')).toBe('true');
expect(screen.getByTitle('Fällige Aufgaben (sofort) – Discord').getAttribute('aria-checked')).toBe('false');
// Toggle settings in the matrix, daily summary, and global switches
fireEvent.click(screen.getByTitle('Fällige Aufgaben (sofort) – Discord'));
fireEvent.click(screen.getByTitle('Geteilte Aufgabe erledigt – E-Mail'));
fireEvent.click(screen.getByTitle('Beobachtete Aufgaben – Discord'));
fireEvent.click(screen.getByTitle('Alle Benachrichtigungen pausieren'));
fireEvent.click(screen.getByTitle('Auch eigene Änderungen an beobachteten Aufgaben melden'));
fireEvent.click(screen.getByTitle('Tageszusammenfassung'));
fireEvent.change(screen.getByLabelText('Uhrzeit der Tageszusammenfassung'), {
target: { value: '08:30' },
});
fireEvent.click(screen.getByTitle('Tageszusammenfassung – Discord'));
fireEvent.click(screen.getByRole('button', { name: 'Alarm-Einstellungen speichern' }));
await waitFor(() => expect(api.updateSettings).toHaveBeenCalledTimes(1));
expect(api.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
discordWebhookUrl: 'https://discord.com/api/webhooks/123/abc',
notificationSettings: {
paused: true,
includeOwnWatched: true,
events: {
due: { email: true, discord: true },
shared: { email: true, discord: false },
taskDone: { email: true, discord: false },
watched: { email: true, discord: false },
},
dailySummary: {
enabled: true,
time: '08:30',
email: true,
discord: true,
},
},
})
);
});
});