diff --git a/apps/web/src/components/settings/calendar-settings-panel.test.tsx b/apps/web/src/components/settings/calendar-settings-panel.test.tsx new file mode 100644 index 0000000..8ca7c8a --- /dev/null +++ b/apps/web/src/components/settings/calendar-settings-panel.test.tsx @@ -0,0 +1,131 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('next-intl', () => ({ + useTranslations: (namespace: string) => (key: string) => { + const translations: Record> = { + widgets: { + 'calendar.sourceEmpty': + 'Keine Kalenderquellen eingerichtet. Fügen Sie eine Quelle hinzu, um Termine anzuzeigen.', + 'calendar.addSource': 'Quelle hinzufügen', + 'calendar.deleteDialogCancel': 'Löschen abbrechen', + 'calendar.deleteDialogLabel': 'Löschen bestätigen', + 'calendar.saveError': 'Fehler beim Speichern', + 'calendar.saveSuccess': 'Gespeichert', + }, + settings: { + sourceDeleteCta: 'Quelle löschen', + sourceDeleteConfirm: 'Möchten Sie diese Kalenderquelle wirklich löschen?', + cancel: 'Abbrechen', + }, + }; + return translations[namespace]?.[key] ?? key; + }, +})); + +vi.mock('@/lib/calendar-api', () => ({ + fetchSources: vi.fn(), + addSource: vi.fn(), + updateSource: vi.fn(), + deleteSource: vi.fn(), + testSource: vi.fn(), +})); + +vi.mock('./calendar-source-form', () => ({ + CalendarSourceForm: () =>
, +})); + +import { fetchSources, deleteSource } from '@/lib/calendar-api'; +import type { CalendarSource } from '@/lib/calendar-api'; +import { CalendarSettingsPanel } from './calendar-settings-panel'; + +const baseSource: CalendarSource = { + id: 'src-1', + userId: 'user-1', + tenantId: 'tenant-1', + name: 'Firmenkalender', + type: 'caldav', + url: 'https://example.test/dav', + isVisible: true, + syncIntervalMin: 15, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', +}; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +async function renderWithOneSource() { + vi.mocked(fetchSources).mockResolvedValue([baseSource]); + render(); + await screen.findByText('Firmenkalender'); +} + +describe('CalendarSettingsPanel — delete confirmation dialog (D-01, T-JT4-04)', () => { + it('opens the delete confirmation dialog when clicking the delete icon', async () => { + const user = userEvent.setup(); + await renderWithOneSource(); + + await user.click(screen.getByRole('button', { name: 'Delete Firmenkalender' })); + + expect( + screen.getByRole('alertdialog', { name: 'Löschen bestätigen' }), + ).toBeInTheDocument(); + }); + + it('mouse-clicking the backdrop aborts: the source is NOT deleted and the dialog closes', async () => { + const user = userEvent.setup(); + await renderWithOneSource(); + + await user.click(screen.getByRole('button', { name: 'Delete Firmenkalender' })); + await user.click(screen.getByRole('button', { name: 'Löschen abbrechen' })); + + expect(deleteSource).not.toHaveBeenCalled(); + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); + // Source is still listed — the abort path never removed it. + expect(screen.getByText('Firmenkalender')).toBeInTheDocument(); + }); + + it('a real keyboard activation (Enter on the focused backdrop button) also aborts, never deletes', async () => { + const user = userEvent.setup(); + await renderWithOneSource(); + + await user.click(screen.getByRole('button', { name: 'Delete Firmenkalender' })); + + const backdrop = screen.getByRole('button', { name: 'Löschen abbrechen' }); + backdrop.focus(); + await user.keyboard('{Enter}'); + + expect(deleteSource).not.toHaveBeenCalled(); + expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(); + expect(screen.getByText('Firmenkalender')).toBeInTheDocument(); + }); + + it('clicking the explicit delete CTA inside the dialog deletes the source', async () => { + vi.mocked(deleteSource).mockResolvedValue(undefined); + const user = userEvent.setup(); + await renderWithOneSource(); + + await user.click(screen.getByRole('button', { name: 'Delete Firmenkalender' })); + await user.click(screen.getByRole('button', { name: 'Quelle löschen' })); + + await waitFor(() => expect(deleteSource).toHaveBeenCalledWith('src-1')); + await waitFor(() => + expect(screen.queryByText('Firmenkalender')).not.toBeInTheDocument(), + ); + }); + + it('clicking inside the dialog card itself does not close it or delete anything', async () => { + const user = userEvent.setup(); + await renderWithOneSource(); + + await user.click(screen.getByRole('button', { name: 'Delete Firmenkalender' })); + await user.click(screen.getByRole('alertdialog', { name: 'Löschen bestätigen' })); + + expect(deleteSource).not.toHaveBeenCalled(); + expect(screen.getByRole('alertdialog')).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/settings/calendar-settings-panel.tsx b/apps/web/src/components/settings/calendar-settings-panel.tsx index e5f2b49..42d0e98 100644 --- a/apps/web/src/components/settings/calendar-settings-panel.tsx +++ b/apps/web/src/components/settings/calendar-settings-panel.tsx @@ -370,17 +370,21 @@ export function CalendarSettingsPanel() { {/* Delete confirmation dialog (UI-SPEC destructive pattern) */} {deletingId && ( -
{ - if (e.target === e.currentTarget) setDeletingId(null); - }} - > +
+ {/* Hintergrund — jetzt eine echte, benannte Schaltflaeche. Sie MUSS + abbrechen und darf unter keinen Umstaenden loeschen (T-JT4-04). */} +