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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, unknown>;
|
||||||
|
const lookup = (path: string): string | undefined =>
|
||||||
|
path.split('.').reduce<unknown>((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<string, string> } }).widgets.calendar;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderForm() {
|
||||||
|
const onSave = vi.fn();
|
||||||
|
const onCancel = vi.fn();
|
||||||
|
render(<CalendarSourceForm onSave={onSave} onCancel={onCancel} />);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -53,6 +53,10 @@ interface CalendarSourceFormProps {
|
|||||||
* Color (8-color palette).
|
* Color (8-color palette).
|
||||||
*
|
*
|
||||||
* T-05-14: Client-side https-only validation before submit.
|
* 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({
|
export function CalendarSourceForm({
|
||||||
onSave,
|
onSave,
|
||||||
@@ -79,6 +83,16 @@ export function CalendarSourceForm({
|
|||||||
|
|
||||||
const isICS = type === 'ics';
|
const isICS = type === 'ics';
|
||||||
const isExchange = type === 'exchange';
|
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)
|
// Validate URL is https (T-05-14)
|
||||||
const validateUrl = useCallback((value: string): boolean => {
|
const validateUrl = useCallback((value: string): boolean => {
|
||||||
@@ -248,7 +262,7 @@ export function CalendarSourceForm({
|
|||||||
id="source-url"
|
id="source-url"
|
||||||
type="url"
|
type="url"
|
||||||
required
|
required
|
||||||
placeholder="https://"
|
placeholder={urlPlaceholder}
|
||||||
className={`h-9 w-full max-w-md rounded border px-3 text-sm text-foreground bg-background ${
|
className={`h-9 w-full max-w-md rounded border px-3 text-sm text-foreground bg-background ${
|
||||||
urlError ? 'border-destructive' : 'border-border'
|
urlError ? 'border-destructive' : 'border-border'
|
||||||
}`}
|
}`}
|
||||||
@@ -265,6 +279,11 @@ export function CalendarSourceForm({
|
|||||||
{urlError && (
|
{urlError && (
|
||||||
<p className="mt-1 text-xs text-destructive">{urlError}</p>
|
<p className="mt-1 text-xs text-destructive">{urlError}</p>
|
||||||
)}
|
)}
|
||||||
|
{isEws && (
|
||||||
|
<p data-testid="source-url-hint-ews" className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{t('calendar.formFieldUrlHintEws')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Username (hidden for ICS) */}
|
{/* Username (hidden for ICS) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user