feat(web): Uebersetzungsschluessel + Hilfsmodul calendar-month.ts fuer Kalender-Widget
- 16 neue Uebersetzungsschluessel unter widgets.calendar in de.json/en.json (Monatsnavigation, Naechste-Termine-Block, Einstellungsfelder) - Neues Hilfsmodul calendar-month.ts: resolveCalendarConfig, buildCalendarDays, groupEventsByDate, computeFetchWindow, selectUpcomingEvents, Formatierungsfunktionen (quick-260916-htc) - 8 Unit-Tests fuer das Hilfsmodul, Umlaut-Waechter weiterhin 3/3 gruen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { CalendarEvent } from '@/lib/calendar-api';
|
||||
import {
|
||||
addDays,
|
||||
buildCalendarDays,
|
||||
computeFetchWindow,
|
||||
dateKey,
|
||||
formatEventDate,
|
||||
formatEventTime,
|
||||
formatMonthLabel,
|
||||
gridStartFor,
|
||||
groupEventsByDate,
|
||||
resolveCalendarConfig,
|
||||
selectUpcomingEvents,
|
||||
startOfLocalDay,
|
||||
} from './calendar-month';
|
||||
|
||||
/** Kleine Fabrik fuer Testtermine (Muster aus dem Plan). */
|
||||
function ev(id: string, start: Date, end: Date, extra?: Partial<CalendarEvent>): CalendarEvent {
|
||||
return {
|
||||
id,
|
||||
sourceId: 's1',
|
||||
title: id,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
allDay: false,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
describe('calendar-month', () => {
|
||||
it('Test 1: buildCalendarDays liefert 42 Zellen ab Montag mit korrektem Monatsflag und heutigem Tag', () => {
|
||||
const days = buildCalendarDays(new Date(2026, 6, 1), new Map(), new Date(2026, 6, 15));
|
||||
|
||||
expect(days).toHaveLength(42);
|
||||
expect(days[0].key).toBe('2026-06-29');
|
||||
expect(days[0].inCurrentMonth).toBe(false);
|
||||
expect(days[2].key).toBe('2026-07-01');
|
||||
expect(days[2].inCurrentMonth).toBe(true);
|
||||
expect(days[41].key).toBe('2026-08-09');
|
||||
|
||||
const todayCells = days.filter((d) => d.isToday);
|
||||
expect(todayCells).toHaveLength(1);
|
||||
expect(todayCells[0].key).toBe('2026-07-15');
|
||||
|
||||
const outside = days.filter((d) => !d.inCurrentMonth);
|
||||
expect(outside).toHaveLength(11);
|
||||
});
|
||||
|
||||
it('Test 2: groupEventsByDate gruppiert nach lokalem Startdatum, buildCalendarDays uebernimmt die Zaehlung', () => {
|
||||
const events = [
|
||||
ev('e1', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
ev('e2', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0)),
|
||||
ev('e3', new Date(2026, 6, 21, 10, 0), new Date(2026, 6, 21, 11, 0)),
|
||||
];
|
||||
|
||||
const grouped = groupEventsByDate(events);
|
||||
expect(grouped.size).toBe(2);
|
||||
expect(grouped.get('2026-07-20')).toHaveLength(2);
|
||||
|
||||
const days = buildCalendarDays(new Date(2026, 6, 1), grouped);
|
||||
const day20 = days.find((d) => d.key === '2026-07-20');
|
||||
expect(day20?.events.length).toBe(2);
|
||||
});
|
||||
|
||||
it('Test 3: resolveCalendarConfig klemmt ungueltige Werte auf die Vorgaben/Grenzen zurueck', () => {
|
||||
expect(resolveCalendarConfig({})).toEqual({ showMonth: true, maxEvents: 3, lookaheadDays: 30 });
|
||||
expect(resolveCalendarConfig({ showMonth: false }).showMonth).toBe(false);
|
||||
expect(resolveCalendarConfig({ showMonth: 'nein' }).showMonth).toBe(true);
|
||||
expect(resolveCalendarConfig({ maxEvents: 99 }).maxEvents).toBe(10);
|
||||
expect(resolveCalendarConfig({ maxEvents: -1 }).maxEvents).toBe(0);
|
||||
expect(resolveCalendarConfig({ maxEvents: 4.7 }).maxEvents).toBe(4);
|
||||
expect(resolveCalendarConfig({ maxEvents: '5' }).maxEvents).toBe(3);
|
||||
expect(resolveCalendarConfig({ lookaheadDays: 45 }).lookaheadDays).toBe(30);
|
||||
expect(resolveCalendarConfig({ lookaheadDays: 90 }).lookaheadDays).toBe(90);
|
||||
});
|
||||
|
||||
it('Test 4: computeFetchWindow deckt Raster und Vorschau-Zeitraum als Tagesgrenzen ab', () => {
|
||||
const w1 = computeFetchWindow(new Date(2026, 6, 1), 30, new Date(2026, 6, 15, 10, 30));
|
||||
expect(w1.from.getTime()).toBe(new Date(2026, 5, 29).getTime());
|
||||
expect(w1.to.getTime()).toBe(new Date(2026, 7, 14).getTime());
|
||||
|
||||
const w2 = computeFetchWindow(new Date(2026, 6, 1), 30, new Date(2026, 4, 1, 8, 0));
|
||||
expect(w2.from.getTime()).toBe(new Date(2026, 4, 1).getTime());
|
||||
expect(w2.to.getTime()).toBe(new Date(2026, 7, 10).getTime());
|
||||
|
||||
const w3 = computeFetchWindow(new Date(2026, 6, 1), 90, new Date(2026, 6, 15, 10, 30));
|
||||
expect(w3.to.getTime()).toBe(new Date(2026, 9, 13).getTime());
|
||||
|
||||
for (const w of [w1, w2, w3]) {
|
||||
for (const d of [w.from, w.to]) {
|
||||
expect(d.getHours()).toBe(0);
|
||||
expect(d.getMinutes()).toBe(0);
|
||||
expect(d.getSeconds()).toBe(0);
|
||||
expect(d.getMilliseconds()).toBe(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('Test 5: selectUpcomingEvents filtert Vergangenheit/Horizont, sortiert nach Start und kuerzt auf maxEvents', () => {
|
||||
const now = new Date(2026, 6, 15, 10, 0);
|
||||
const events = [
|
||||
ev('gestern', new Date(2026, 6, 14, 11, 0), new Date(2026, 6, 14, 12, 0)),
|
||||
ev('läuft', new Date(2026, 6, 15, 9, 0), new Date(2026, 6, 15, 11, 0)),
|
||||
ev('heute 15', new Date(2026, 6, 15, 15, 0), new Date(2026, 6, 15, 16, 0)),
|
||||
ev('in 5 Tagen', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
ev('in 10 Tagen', new Date(2026, 6, 25, 9, 0), new Date(2026, 6, 25, 10, 0)),
|
||||
];
|
||||
|
||||
const result = selectUpcomingEvents(events, 7, 2, now);
|
||||
expect(result.map((e) => e.title)).toEqual(['läuft', 'heute 15']);
|
||||
|
||||
expect(selectUpcomingEvents(events, 7, 0, now)).toEqual([]);
|
||||
});
|
||||
|
||||
it('Test 6: formatEventDate/formatEventTime/formatMonthLabel formatieren de-DE', () => {
|
||||
const timed = ev('t1', new Date(2026, 6, 20, 9, 5), new Date(2026, 6, 20, 10, 0));
|
||||
const timedText = formatEventDate(timed);
|
||||
expect(timedText).toMatch(/20\.07\./);
|
||||
expect(timedText).toMatch(/09:05/);
|
||||
|
||||
const allDay = ev('t2', new Date(2026, 6, 20, 0, 0), new Date(2026, 6, 20, 23, 59), { allDay: true });
|
||||
const allDayText = formatEventDate(allDay);
|
||||
expect(allDayText).toMatch(/20\.07\./);
|
||||
expect(allDayText).not.toMatch(/\d{2}:\d{2}/);
|
||||
|
||||
expect(formatMonthLabel(new Date(2026, 6, 1))).toBe('Juli 2026');
|
||||
expect(formatEventTime(new Date(2026, 6, 20, 9, 5).toISOString())).toBe('09:05');
|
||||
});
|
||||
|
||||
it('Test 7: gridStartFor findet den Montag am oder vor dem 1. des Monats', () => {
|
||||
expect(gridStartFor(new Date(2026, 7, 1)).getTime()).toBe(new Date(2026, 6, 27).getTime());
|
||||
expect(gridStartFor(new Date(2026, 5, 1)).getTime()).toBe(new Date(2026, 5, 1).getTime());
|
||||
});
|
||||
|
||||
it('Zusatz: dateKey/startOfLocalDay/addDays arbeiten lokal ohne UTC-Versatz', () => {
|
||||
expect(dateKey(new Date(2026, 6, 5))).toBe('2026-07-05');
|
||||
const start = startOfLocalDay(new Date(2026, 6, 5, 14, 30, 15));
|
||||
expect(start.getHours()).toBe(0);
|
||||
expect(addDays(new Date(2026, 6, 30), 2).getDate()).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user