Files
webmail/lib/__tests__/recurrence-dst-gap.test.ts
T
Linus Rath 34fb9d247a fix: keep daily recurring events going past a DST gap
Daily expansion advanced its period anchor day by day and kept the
time a DST gap pushed it to: 02:30 became 03:30 on the spring-forward
night, and an all-day event's midnight became 01:00 where the change
happens at midnight (Chile, Brazil). Every later day then failed the
implicit byHour, so the series vanished from that night on.

Each daily candidate is now built from the start's wall-clock time on
its day and matched against that time, so the gap day still shows its
occurrence (moved past the gap) and the days after it are back at the
normal time.
2026-09-26 15:45:59 +02:00

41 lines
1.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { expandRecurringEvents } from '../recurrence-expansion';
import type { CalendarEvent } from '@/lib/jmap/types';
// Daily expansion advanced a Date across the browser's DST gap and kept the
// shifted time (02:30 became 03:30, midnight became 01:00); the implicit
// byHour then rejected every later day, so the series ended at the gap.
function daily(start: string, allDay: boolean, timeZone?: string): CalendarEvent {
return {
id: 'E1', uid: 'u1', title: 'Daily', calendarIds: { c: true },
start, duration: allDay ? 'P1D' : 'PT30M', showWithoutTime: allDay,
...(timeZone ? { timeZone } : {}),
recurrenceRules: [{ '@type': 'RecurrenceRule', frequency: 'daily' }],
} as unknown as CalendarEvent;
}
let originalTZ: string | undefined;
beforeEach(() => {
originalTZ = process.env.TZ;
});
afterEach(() => {
process.env.TZ = originalTZ;
});
describe('daily recurrence across a DST gap', () => {
it('keeps a 02:30 series going past the spring-forward night (Berlin)', () => {
process.env.TZ = 'Europe/Berlin';
const out = expandRecurringEvents([daily('2026-03-01T02:30:00', false, 'Europe/Berlin')], '2026-03-01T00:00:00', '2026-04-01T00:00:00');
expect(out).toHaveLength(31);
expect(out.at(-1)!.start.slice(0, 16)).toBe('2026-03-31T02:30');
});
it('keeps an all-day series going after a midnight gap (Santiago)', () => {
process.env.TZ = 'America/Santiago';
const out = expandRecurringEvents([daily('2026-08-20T00:00:00', true)], '2026-09-07T00:00:00', '2026-09-14T00:00:00');
expect(out).toHaveLength(7);
expect(out.map((o) => o.start.slice(11, 16))).toEqual(Array(7).fill('00:00'));
});
});