Files
native/src/lib/__tests__/recurrence-summary.test.ts
T
Linus Rath a562ea59b8 fix: keep attendees' answers on save and deliver occurrence changes
Every save in the event editor rebuilt the participants with new ids
and put every attendee back to needs-action, the user's own answer on
an invitation included. Saves now keep each attendee's id and answer;
new attendees are asked. As on the web, only an organizer who moves an
event (its start, end, all-day or recurrence rule; not a time zone
rewrite, and not a zero-length event) asks the others again.

The series scopes did not fit that:

- "All events" wrote the opened occurrence's start, length, zone and
  participants onto the series: a stretched occurrence stretched every
  one, and one day's decline spread to the whole series. The series now
  keeps its own list and takes only the attendees the user added or
  removed, and its times change only when the occurrence moved (by the
  same delta), its rule only when it changed.
- "This and following" makes a new series, which Stalwart sends only to
  attendees who have not answered: its attendees are asked again.
- Stalwart sends a new occurrence override, the first change to one
  occurrence, only to attendees who have not answered (0.16.6 to 1.0).
  A change that keeps answers now goes out in two writes: the override
  as the occurrence stands without scheduling, then the change as an
  update of it, to the occurrence looked up again. Checked live on 1.0
  and 0.16.23.
- A save of an invitation someone else organizes writes no participants
  while the user left them alone, and never asks for scheduling, so the
  organizer gets no reply for a changed alert. The switch to send
  invitations is hidden there.

Also:

- a reminder change on one occurrence of a series that uses the
  calendar's default alerts turns the defaults off for the series first
  (keeping what it shows under ids of its own); 1.0 kept showing the
  defaults
- all-day and floating occurrences expanded on the device no longer
  carry the master's utcStart/utcEnd, so their reminders are scheduled
  for each day; master instants that are not the master's own (0.16.23's
  year 32548, or the next occurrence's) are ignored
- the editor warns when the attendees and the organizer go over the
  calendars capability's maxParticipantsPerEvent, and a refusal for it
  (Stalwart 1.0 enforces it, answers included) is explained in the
  editor, the series dialogs, the detail sheet and the invitation
  banner, in all languages; a failed save used to fail silently
- refused event creates, updates and answers keep their SetError type
- Calendar/get asks for its properties by name
2026-09-25 23:50:29 +02:00

53 lines
2.5 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { translate, type MessageParams } from '../../i18n';
import type { RecurrenceRule } from '../../api/types';
import { buildRecurrenceSummary, recurrenceEndLabel, recurrenceIntervalLabel } from '../recurrence';
const t = (key: string, fallback?: string, params?: MessageParams) =>
translate('en', key, fallback, params);
const rule = (r: Partial<RecurrenceRule>): RecurrenceRule => ({ '@type': 'RecurrenceRule', frequency: 'weekly', ...r });
describe('recurrence summary', () => {
it('uses plurals for the interval and the occurrence count', () => {
expect(recurrenceIntervalLabel('daily', 1, t)).toBe('Daily');
expect(recurrenceIntervalLabel('weekly', 3, t)).toBe('Every 3 weeks');
expect(recurrenceEndLabel(rule({ count: 1 }), t)).toBe('1 occurrence');
expect(recurrenceEndLabel(rule({ count: 12 }), t)).toBe('12 occurrences');
});
it('builds the summary from the catalog fragments', () => {
expect(buildRecurrenceSummary(rule({
interval: 2,
byDay: [{ day: 'we' }, { day: 'mo' }],
count: 5,
}), t)).toBe('Every 2 weeks on Mon, Wed · 5 occurrences');
expect(buildRecurrenceSummary(rule({
frequency: 'monthly',
byDay: [{ day: 'th', nthOfPeriod: 3 }],
until: '2027-03-01T23:59:59',
}), t)).toBe('Monthly on the third Thursday · Until Mar 1, 2027');
expect(buildRecurrenceSummary(rule({ frequency: 'hourly' }), t)).toBeNull();
});
});
describe('recurrenceRulesEqual', () => {
it('takes the editor\'s rewrite of an unchanged rule for the server\'s normalised copy', async () => {
const { recurrenceRulesEqual } = await import('../recurrence');
const stored = [{
'@type': 'RecurrenceRule' as const, frequency: 'weekly', interval: 1, rscale: 'gregorian', skip: 'omit',
firstDayOfWeek: 'mo', byDay: [],
}];
expect(recurrenceRulesEqual(stored, [{ frequency: 'weekly', interval: 1 }])).toBe(true);
expect(recurrenceRulesEqual(undefined, null)).toBe(true);
expect(recurrenceRulesEqual(stored, [{ frequency: 'daily', interval: 1 }])).toBe(false);
expect(recurrenceRulesEqual(stored, [{ frequency: 'weekly', interval: 2 }])).toBe(false);
expect(recurrenceRulesEqual(stored, [{ frequency: 'weekly', count: 5 }])).toBe(false);
expect(recurrenceRulesEqual(stored, undefined)).toBe(false);
expect(recurrenceRulesEqual(
[{ frequency: 'weekly', byDay: [{ day: 'mo' }, { day: 'we' }] }],
[{ frequency: 'weekly', byDay: [{ day: 'mo' }, { day: 'fr' }] }],
)).toBe(false);
});
});