From 2306a6dee1e4d74ae01f35946b993a9f07ce7f8b Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 16 Sep 2026 12:47:09 +0200 Subject: [PATCH] feat(web): typabhaengiger URL-Platzhalter + EWS-Hinweis im Kalenderquellen-Formular - Platzhalter je Typ (CalDAV/ICS/Exchange-Graph/Exchange-EWS), ohne Typ weiterhin https:// - Grauer Hinweis unter dem Feld nur bei Exchange + EWS, unterhalb eines eventuellen Fehlers - Neuer Komponententest mit 6 Faellen (TDD: rot vor der Implementierung) Co-Authored-By: Claude Opus 5 (1M context) --- .../settings/calendar-source-form.test.tsx | 113 ++++++++++++++++++ .../settings/calendar-source-form.tsx | 21 +++- 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/components/settings/calendar-source-form.test.tsx diff --git a/apps/web/src/components/settings/calendar-source-form.test.tsx b/apps/web/src/components/settings/calendar-source-form.test.tsx new file mode 100644 index 0000000..11303b9 --- /dev/null +++ b/apps/web/src/components/settings/calendar-source-form.test.tsx @@ -0,0 +1,113 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import de from '@/messages/de.json'; +import { CalendarSourceForm } from './calendar-source-form'; + +/** + * calendar-source-form.test — NEU (quick-260916-hiv). Deckt den + * typabhaengigen URL-Platzhalter und den grauen EWS-Hinweis ab. + * Texte aus der echten de.json. + */ +vi.mock('next-intl', async () => { + const messages = (await import('@/messages/de.json')).default as Record; + const lookup = (path: string): string | undefined => + path.split('.').reduce((o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages) as + | string + | undefined; + return { + useTranslations: (ns?: string) => (key: string) => lookup(ns ? `${ns}.${key}` : key) ?? key, + }; +}); + +vi.mock('@/lib/calendar-api', () => ({ + testSourceConfig: vi.fn(), + testSource: vi.fn(), +})); + +const t = (de as { widgets: { calendar: Record } }).widgets.calendar; + +afterEach(() => { + cleanup(); +}); + +function renderForm() { + const onSave = vi.fn(); + const onCancel = vi.fn(); + render(); + return { + onSave, + onCancel, + urlInput: screen.getByLabelText(/Adresse \(URL\)/) as HTMLInputElement, + typeSelect: screen.getByLabelText(/^Typ/) as HTMLSelectElement, + }; +} + +describe('CalendarSourceForm — URL-Platzhalter je Typ + EWS-Hinweis (quick-260916-hiv)', () => { + it('Test 1: ohne Typ zeigt das URL-Feld https:// und keinen EWS-Hinweis', () => { + const { urlInput } = renderForm(); + + expect(urlInput.placeholder).toBe('https://'); + expect(screen.queryByTestId('source-url-hint-ews')).not.toBeInTheDocument(); + }); + + it('Test 2: CalDAV zeigt den CalDAV-Platzhalter, keinen Hinweis', () => { + const { urlInput, typeSelect } = renderForm(); + + fireEvent.change(typeSelect, { target: { value: 'caldav' } }); + + expect(urlInput.placeholder).toBe(t.formFieldUrlPlaceholderCaldav); + expect(screen.queryByTestId('source-url-hint-ews')).not.toBeInTheDocument(); + }); + + it('Test 3: ICS zeigt den ICS-Platzhalter, keinen Hinweis', () => { + const { urlInput, typeSelect } = renderForm(); + + fireEvent.change(typeSelect, { target: { value: 'ics' } }); + + expect(urlInput.placeholder).toBe(t.formFieldUrlPlaceholderIcs); + expect(screen.queryByTestId('source-url-hint-ews')).not.toBeInTheDocument(); + }); + + it('Test 4: Exchange (Standardmodus Graph) zeigt den Graph-Platzhalter, keinen Hinweis', () => { + const { urlInput, typeSelect } = renderForm(); + + fireEvent.change(typeSelect, { target: { value: 'exchange' } }); + + expect(urlInput.placeholder).toBe(t.formFieldUrlPlaceholderGraph); + expect(screen.queryByTestId('source-url-hint-ews')).not.toBeInTheDocument(); + }); + + it('Test 5: Exchange + EWS zeigt den EWS-Platzhalter und den grauen Hinweis; zurueck auf Graph verschwindet beides', () => { + const { urlInput, typeSelect } = renderForm(); + + fireEvent.change(typeSelect, { target: { value: 'exchange' } }); + const exchangeModeSelect = screen.getByLabelText(/Exchange-Anbindung/) as HTMLSelectElement; + fireEvent.change(exchangeModeSelect, { target: { value: 'ews' } }); + + expect(urlInput.placeholder).toBe(t.formFieldUrlPlaceholderEws); + const hint = screen.getByTestId('source-url-hint-ews'); + expect(hint).toHaveTextContent(t.formFieldUrlHintEws); + expect(hint.className).toContain('text-muted-foreground'); + + fireEvent.change(exchangeModeSelect, { target: { value: 'graph' } }); + + expect(screen.queryByTestId('source-url-hint-ews')).not.toBeInTheDocument(); + expect(urlInput.placeholder).toBe(t.formFieldUrlPlaceholderGraph); + }); + + it('Test 6: Exchange + EWS mit http-Fehler zeigt Fehler UND Hinweis, Hinweis steht nach dem Fehler', () => { + const { urlInput, typeSelect } = renderForm(); + + fireEvent.change(typeSelect, { target: { value: 'exchange' } }); + const exchangeModeSelect = screen.getByLabelText(/Exchange-Anbindung/) as HTMLSelectElement; + fireEvent.change(exchangeModeSelect, { target: { value: 'ews' } }); + + fireEvent.change(urlInput, { target: { value: 'http://mail.firma.de/EWS/Exchange.asmx' } }); + + const fehler = screen.getByText(t.formUrlErrorHttps); + const hinweis = screen.getByTestId('source-url-hint-ews'); + expect(fehler).toBeInTheDocument(); + expect(hinweis).toBeInTheDocument(); + expect(fehler.compareDocumentPosition(hinweis) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); +}); diff --git a/apps/web/src/components/settings/calendar-source-form.tsx b/apps/web/src/components/settings/calendar-source-form.tsx index 255e469..027aaca 100644 --- a/apps/web/src/components/settings/calendar-source-form.tsx +++ b/apps/web/src/components/settings/calendar-source-form.tsx @@ -53,6 +53,10 @@ interface CalendarSourceFormProps { * Color (8-color palette). * * T-05-14: Client-side https-only validation before submit. + * + * Quick 260916-hiv: the URL field's placeholder depends on the selected type + * (CalDAV / ICS / Exchange Graph / Exchange EWS), and Exchange EWS shows a + * hint below the field explaining that the full EWS address is required. */ export function CalendarSourceForm({ onSave, @@ -79,6 +83,16 @@ export function CalendarSourceForm({ const isICS = type === 'ics'; const isExchange = type === 'exchange'; + const isEws = isExchange && exchangeMode === 'ews'; + const urlPlaceholder = isExchange + ? isEws + ? t('calendar.formFieldUrlPlaceholderEws') + : t('calendar.formFieldUrlPlaceholderGraph') + : type === 'caldav' + ? t('calendar.formFieldUrlPlaceholderCaldav') + : type === 'ics' + ? t('calendar.formFieldUrlPlaceholderIcs') + : 'https://'; // Validate URL is https (T-05-14) const validateUrl = useCallback((value: string): boolean => { @@ -248,7 +262,7 @@ export function CalendarSourceForm({ id="source-url" type="url" required - placeholder="https://" + placeholder={urlPlaceholder} className={`h-9 w-full max-w-md rounded border px-3 text-sm text-foreground bg-background ${ urlError ? 'border-destructive' : 'border-border' }`} @@ -265,6 +279,11 @@ export function CalendarSourceForm({ {urlError && (

{urlError}

)} + {isEws && ( +

+ {t('calendar.formFieldUrlHintEws')} +

+ )} {/* Username (hidden for ICS) */}