feat(web): CalendarConfig im Einstellungsfeld, Changelog- und Handbuch-Eintrag

- Kalender-Zweig im Einstellungsfeld ersetzt den englischen Fliesstext durch CalendarConfig (Monatsansicht ein/aus, Anzahl Termine 0..10, Zeitraum 7/14/30/60/90) nach Muster ClockConfig; Speichern per partiellem updateWidgetConfig
- Paneltest um drei Kalender-Tests erweitert (7/7 gruen), next-intl-Mock interpoliert jetzt {platzhalter}
- CHANGELOG.md: zweiter Eintrag unter Unveroeffentlicht/Geaendert; Anwenderhandbuch: Widget-Tabellenzeile und Dashboard-Widgets-Absatz ergaenzt
- Gesamtlauf: 51 Testdateien / 332 Tests gruen, Type-Check Exit 0 (quick-260916-htc)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 13:12:48 +02:00
parent 61996dceef
commit 6d8c7c46a8
4 changed files with 177 additions and 16 deletions
@@ -19,7 +19,12 @@ vi.mock('next-intl', async () => {
| string
| undefined;
return {
useTranslations: (ns?: string) => (key: string) => lookup(ns ? `${ns}.${key}` : key) ?? key,
useTranslations:
(ns?: string) =>
(key: string, values?: Record<string, unknown>) => {
const raw = lookup(ns ? `${ns}.${key}` : key) ?? key;
return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? '')) : raw;
},
};
});
@@ -36,6 +41,7 @@ vi.mock('@/components/settings/search-provider-form', () => ({
}));
const clockTexts = (de as { widgets: { clock: Record<string, string> } }).widgets.clock;
const cal = (de as { widgets: { calendar: Record<string, string> } }).widgets.calendar;
const WIDGET = { id: 'c1', widgetType: 'clock', config: { timezone: 'Europe/Berlin', timeFontSizePt: 24 } };
@@ -105,3 +111,74 @@ describe('WidgetSettingsPanel — Schriftgroesse der Uhrzeit (quick-260916-bwo)'
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});
describe('WidgetSettingsPanel — Kalender-Einstellungen (quick-260916-htc)', () => {
function renderCalendarExpanded(config: Record<string, unknown> = {}) {
const onWidgetUpdate = vi.fn();
const widget = { id: 'k1', widgetType: 'calendar', config };
render(<WidgetSettingsPanel widgets={[widget]} onWidgetUpdate={onWidgetUpdate} />);
fireEvent.click(screen.getByRole('button', { name: /Kalender #1/ }));
return { onWidgetUpdate };
}
it('Test 5: Vorbelegung aus der Konfiguration, Optionslisten und uebersetzte Link-Zeile', () => {
renderCalendarExpanded();
const showMonth = screen.getByLabelText(cal.configShowMonth) as HTMLInputElement;
expect(showMonth.checked).toBe(true);
const maxEventsSelect = screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement;
expect(maxEventsSelect.value).toBe('3');
const maxEventsOptionTexts = Array.from(maxEventsSelect.options).map((o) => o.textContent);
expect(maxEventsOptionTexts).toEqual([
cal.configMaxEventsNone,
cal.configMaxEventsOne,
...Array.from({ length: 9 }, (_, i) => cal.configMaxEventsMany.replace('{count}', String(i + 2))),
]);
const lookaheadSelect = screen.getByLabelText(cal.configLookahead) as HTMLSelectElement;
expect(lookaheadSelect.value).toBe('30');
const lookaheadOptionTexts = Array.from(lookaheadSelect.options).map((o) => o.textContent);
expect(lookaheadOptionTexts).toEqual(
[7, 14, 30, 60, 90].map((days) => cal.configLookaheadOption.replace('{days}', String(days))),
);
expect(screen.getByText(cal.configSourcesHint)).toBeInTheDocument();
const link = screen.getByRole('link', { name: cal.configSourcesLink });
expect(link).toHaveAttribute('href', '/settings/dashboard/calendar');
expect(screen.queryByText(/managed under/)).not.toBeInTheDocument();
});
it('Test 6: jede Aenderung ruft updateWidgetConfig/onWidgetUpdate nur mit dem geaenderten Feld auf', async () => {
const { onWidgetUpdate } = renderCalendarExpanded();
fireEvent.change(screen.getByLabelText(cal.configMaxEvents), { target: { value: '5' } });
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { maxEvents: 5 }));
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { maxEvents: 5 });
fireEvent.change(screen.getByLabelText(cal.configLookahead), { target: { value: '14' } });
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { lookaheadDays: 14 }));
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { lookaheadDays: 14 });
fireEvent.click(screen.getByLabelText(cal.configShowMonth));
await vi.waitFor(() => expect(onWidgetUpdate).toHaveBeenCalledWith('k1', { showMonth: false }));
expect(updateWidgetConfig).toHaveBeenCalledWith('k1', { showMonth: false });
expect(updateWidgetConfig).toHaveBeenCalledTimes(3);
});
it('Test 7: bestehende Werte werden angezeigt, ungueltige Werte geklemmt', () => {
renderCalendarExpanded({ showMonth: false, maxEvents: 7, lookaheadDays: 60 });
expect((screen.getByLabelText(cal.configShowMonth) as HTMLInputElement).checked).toBe(false);
expect((screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement).value).toBe('7');
expect((screen.getByLabelText(cal.configLookahead) as HTMLSelectElement).value).toBe('60');
cleanup();
renderCalendarExpanded({ maxEvents: 42, lookaheadDays: 45 });
expect((screen.getByLabelText(cal.configMaxEvents) as HTMLSelectElement).value).toBe('10');
expect((screen.getByLabelText(cal.configLookahead) as HTMLSelectElement).value).toBe('30');
});
});