Files
webmail/lib/__tests__/recurrence-rule.test.ts
T
Linus Rath 6d4b695489 fix: ask attendees again when their organizer drags a meeting
A move outside the editor kept every answer. A drag or resize in the
week and day views, a drop in the month view and an applied counter
proposal wrote the new time with the attendees' stored answers:
Stalwart's update then said "accepted" for a time nobody had accepted,
and a dragged occurrence, which gets its own override, reached no
attendee who had answered (live on 1.0 and 0.16.23). When the user
organizes the meeting these moves now ask the attendees again, as the
editor does; one helper (`userOrganizes`) decides for both.

Also:
- A changed recurrence rule counts as a move for the series: a custom
  rule moved from Monday to Tuesday asks again. A rule the user did not
  touch is no change however the server spells it, and on one
  occurrence, where no rule applies, it is none.
- An event that lasts no time (a deadline) keeps its zero length on
  save instead of becoming an hour long and asking everyone again.
- An attendee's save of their own copy - an alert on one occurrence, a
  drag - wrote the participants again and sent the organizer a
  "Reply:" mail. It now leaves the participants alone and sends
  nothing. The editor shows someone else's attendee list read-only and
  no "Send invitations" box, which only ever controlled the organizer's
  messages; answers go out through the answer buttons.
- An existing override written on the base event (0.16 before 0.16.20,
  and ranges the browser expands) gets the series' organizer again when
  it has none, as every save wrote it before 4c20b09c: 0.16.19 refuses
  an answer to such an occurrence. 0.16.23 takes the key and drops it;
  1.0 refuses it inside an override once, and the store writes the
  override again without it (both live).
- The comment on `organizerCalendarAddress` said Stalwart takes the
  ORGANIZER solely from it. Both versions take it from the owner
  participant when it is missing (live).
2026-09-26 00:25:27 +02:00

72 lines
2.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { createRecurrenceRule, sameRecurrenceRules } from '@/lib/recurrence-rule';
import type { CalendarNDay, CalendarRecurrenceRule } from '@/lib/jmap/types';
describe('createRecurrenceRule (#805 — no default SKIP=OMIT)', () => {
it('does not emit rscale or skip (which serialise to RSCALE=GREGORIAN;SKIP=OMIT)', () => {
const rule = createRecurrenceRule('yearly');
expect('skip' in rule).toBe(false);
expect('rscale' in rule).toBe(false);
expect(rule.skip).toBeUndefined();
expect(rule.rscale).toBeUndefined();
});
it('builds a clean Gregorian default rule', () => {
expect(createRecurrenceRule('daily')).toEqual({
'@type': 'RecurrenceRule',
frequency: 'daily',
interval: 1,
firstDayOfWeek: 'mo',
byDay: null,
byMonthDay: null,
byMonth: null,
byYearDay: null,
byWeekNo: null,
byHour: null,
byMinute: null,
bySecond: null,
bySetPosition: null,
count: null,
until: null,
});
});
it('applies overrides (interval / count / until) without reintroducing skip', () => {
const rule = createRecurrenceRule('yearly', { interval: 2, count: 100 });
expect(rule.interval).toBe(2);
expect(rule.count).toBe(100);
expect(rule.until).toBeNull();
expect('skip' in rule).toBe(false);
});
});
describe('sameRecurrenceRules', () => {
const weekly = (overrides: Partial<CalendarRecurrenceRule> = {}) => createRecurrenceRule('weekly', overrides);
it('matches a rule the editor wrote with its defaults to the server\'s copy without them', () => {
const fromServer = { '@type': 'RecurrenceRule', frequency: 'weekly' } as CalendarRecurrenceRule;
expect(sameRecurrenceRules([weekly()], [fromServer])).toBe(true);
expect(sameRecurrenceRules(null, [])).toBe(true);
});
it('ignores the order of by-lists', () => {
// As 1.0 returns an NDay: typed, nthOfPeriod left out.
const nday = (day: string) => ({ '@type': 'NDay', day }) as CalendarNDay;
expect(sameRecurrenceRules(
[weekly({ byDay: [{ day: 'mo' }, { day: 'we' }] })],
[weekly({ byDay: [nday('we'), nday('mo')] })],
)).toBe(true);
});
it('tells a custom rule moved from Monday to Tuesday apart', () => {
expect(sameRecurrenceRules([weekly({ byDay: [{ day: 'mo' }] })], [weekly({ byDay: [{ day: 'tu' }] })])).toBe(false);
});
it('tells other frequencies, intervals, ends and a removed rule apart', () => {
expect(sameRecurrenceRules([weekly()], [createRecurrenceRule('daily')])).toBe(false);
expect(sameRecurrenceRules([weekly()], [weekly({ interval: 2 })])).toBe(false);
expect(sameRecurrenceRules([weekly({ count: 5 })], [weekly({ count: 6 })])).toBe(false);
expect(sameRecurrenceRules([weekly()], null)).toBe(false);
});
});