feat(nextcloud-status): Benachrichtigung abonnieren und Mail bei Störung
- Glocke je Kachel (persönlich, Benutzen-Ebene), Tabelle NextcloudAlertSubscription mit RLS - Zwei-Fehlschläge-Regel und gemeldeter Zustand auf der Zeile, Anspruch vor dem Mailversand - Mail (nur Deutsch) über MailService, bis zu drei Versuche, Zugriff beim Senden erneut geprüft - Fehlercodes in der Mail als lesbarer Hinweis Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,9 @@ function render(ui: ReactElement) {
|
||||
const mockListInstances = vi.fn();
|
||||
const mockCheckAll = vi.fn();
|
||||
const mockCheckOne = vi.fn();
|
||||
const mockSubscribe = vi.fn();
|
||||
const mockUnsubscribe = vi.fn();
|
||||
const mockRequestPermission = vi.fn();
|
||||
|
||||
vi.mock('@/lib/nextcloud-status-api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/nextcloud-status-api')>();
|
||||
@@ -32,9 +35,15 @@ vi.mock('@/lib/nextcloud-status-api', async (importOriginal) => {
|
||||
listInstances: (...args: unknown[]) => mockListInstances(...args),
|
||||
checkAll: (...args: unknown[]) => mockCheckAll(...args),
|
||||
checkOne: (...args: unknown[]) => mockCheckOne(...args),
|
||||
subscribe: (...args: unknown[]) => mockSubscribe(...args),
|
||||
unsubscribe: (...args: unknown[]) => mockUnsubscribe(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/lib/reminder-notify', () => ({
|
||||
requestBrowserPermissionOnce: () => mockRequestPermission(),
|
||||
}));
|
||||
|
||||
let mockCanManage: boolean | null = false;
|
||||
vi.mock('@/lib/use-module-capability', () => ({
|
||||
useCanManageModule: () => mockCanManage,
|
||||
@@ -127,6 +136,9 @@ describe('NextcloudStatusPage', () => {
|
||||
mockListInstances.mockReset();
|
||||
mockCheckAll.mockReset();
|
||||
mockCheckOne.mockReset();
|
||||
mockSubscribe.mockReset().mockResolvedValue(undefined);
|
||||
mockUnsubscribe.mockReset().mockResolvedValue(undefined);
|
||||
mockRequestPermission.mockReset();
|
||||
mockCanManage = false;
|
||||
window.localStorage.clear();
|
||||
});
|
||||
@@ -285,4 +297,85 @@ describe('NextcloudStatusPage', () => {
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Glocke „Benachrichtigen“ (quick-261002-kxc)', () => {
|
||||
const tileOf = (name: string) =>
|
||||
screen
|
||||
.getAllByTestId('cloud-tile')
|
||||
.find((t) => t.getAttribute('aria-label')?.startsWith(name)) as HTMLElement;
|
||||
|
||||
const withBells = (subscribedIds: string[]): NextcloudList => ({
|
||||
...LIST,
|
||||
instances: LIST.instances.map((i) => ({ ...i, subscribed: subscribedIds.includes(i.id) })),
|
||||
});
|
||||
|
||||
it('ist auf der Benutzen-Ebene an jeder Kachel sichtbar und zeigt den gemerkten Stand', async () => {
|
||||
mockCanManage = false;
|
||||
mockListInstances.mockResolvedValue(withBells(['b']));
|
||||
render(<NextcloudStatusPage />);
|
||||
await screen.findAllByTestId('cloud-tile');
|
||||
expect(screen.getAllByRole('button', { name: 'Benachrichtigen' })).toHaveLength(4);
|
||||
expect(
|
||||
within(tileOf('Kunde Gelb')).getByRole('button', { name: 'Benachrichtigen' }),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
expect(
|
||||
within(tileOf('Kunde Grün')).getByRole('button', { name: 'Benachrichtigen' }),
|
||||
).toHaveAttribute('aria-pressed', 'false');
|
||||
});
|
||||
|
||||
it('Klick schaltet ein (subscribe, Browser-Erlaubnis einmalig angefragt) und wieder aus (unsubscribe)', async () => {
|
||||
mockListInstances.mockResolvedValue(withBells([]));
|
||||
render(<NextcloudStatusPage />);
|
||||
await screen.findAllByTestId('cloud-tile');
|
||||
const bell = () =>
|
||||
within(tileOf('Kunde Grün')).getByRole('button', { name: 'Benachrichtigen' });
|
||||
|
||||
fireEvent.click(bell());
|
||||
await waitFor(() => expect(mockSubscribe).toHaveBeenCalledWith('a'));
|
||||
await waitFor(() => expect(bell()).toHaveAttribute('aria-pressed', 'true'));
|
||||
expect(mockRequestPermission).toHaveBeenCalledTimes(1);
|
||||
expect(mockUnsubscribe).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(bell());
|
||||
await waitFor(() => expect(mockUnsubscribe).toHaveBeenCalledWith('a'));
|
||||
await waitFor(() => expect(bell()).toHaveAttribute('aria-pressed', 'false'));
|
||||
expect(mockRequestPermission).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('bei einem Fehler wird zurückgenommen und der Hinweis gezeigt', async () => {
|
||||
mockListInstances.mockResolvedValue(withBells([]));
|
||||
mockSubscribe.mockRejectedValue(new Error('nein'));
|
||||
render(<NextcloudStatusPage />);
|
||||
await screen.findAllByTestId('cloud-tile');
|
||||
const bell = () =>
|
||||
within(tileOf('Kunde Grün')).getByRole('button', { name: 'Benachrichtigen' });
|
||||
fireEvent.click(bell());
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'Die Benachrichtigung konnte nicht geändert werden.',
|
||||
);
|
||||
expect(bell()).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(mockRequestPermission).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('die Prüfung einer einzelnen Kachel behält den Stand der Glocke', async () => {
|
||||
mockCanManage = true;
|
||||
mockListInstances.mockResolvedValue(withBells(['c']));
|
||||
// Einzelantwort ohne `subscribed`
|
||||
mockCheckOne.mockResolvedValue({ ...LIST.instances[2] });
|
||||
render(<NextcloudStatusPage />);
|
||||
await screen.findAllByTestId('cloud-tile');
|
||||
fireEvent.click(
|
||||
within(tileOf('Kunde Rot')).getByRole('button', { name: 'Diese Cloud jetzt prüfen' }),
|
||||
);
|
||||
await waitFor(() => expect(mockCheckOne).toHaveBeenCalledWith('c'));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
within(tileOf('Kunde Rot')).getByRole('button', { name: 'Diese Cloud jetzt prüfen' }),
|
||||
).not.toBeDisabled(),
|
||||
);
|
||||
expect(
|
||||
within(tileOf('Kunde Rot')).getByRole('button', { name: 'Benachrichtigen' }),
|
||||
).toHaveAttribute('aria-pressed', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user