diff --git a/docs/public/static/error-codes.json b/docs/public/static/error-codes.json index 7692a07d00909..077305b9019a4 100644 --- a/docs/public/static/error-codes.json +++ b/docs/public/static/error-codes.json @@ -288,5 +288,7 @@ "288": "MUI X: useDisposable failed to detect React StrictMode.\nThe instance was disposed on StrictMode's simulated unmount and is about to be reused while torn down.\nThis is an internal invariant violation — please report it at https://github.com/mui/mui-x/issues.", "289": "MUI X Chat docs-correctness guard: could not load entry point %s (%s). The export surface cannot be enumerated.", "290": "MUI X Chat docs-correctness guard: %s has no module symbol (%s); it may be missing exports.", - "291": "MUI X Scheduler: All events must have a unique `id`.\nWithout an `id`, an event cannot be tracked and silently overwrites another event in the calendar state.\nAdd an `id` to every event, or set `eventModelStructure.id.getter` to derive one from your event model.\nAn event was provided without an `id`:\n%s" + "291": "MUI X Scheduler: All events must have a unique `id`.\nWithout an `id`, an event cannot be tracked and silently overwrites another event in the calendar state.\nAdd an `id` to every event, or set `eventModelStructure.id.getter` to derive one from your event model.\nAn event was provided without an `id`:\n%s", + "292": "MUI X Scheduler: Invalid FREQ value \"%s\". The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Provide a supported frequency value.", + "293": "MUI X Scheduler: The recurrence rule must include a FREQ value. The frequency (DAILY, WEEKLY, MONTHLY, or YEARLY) is required for recurrence rules. Provide a freq value." } diff --git a/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.test.ts b/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.test.ts index 7d4b2dcef6758..a22f7e092595b 100644 --- a/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.test.ts +++ b/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.test.ts @@ -115,6 +115,46 @@ describe('recurring-events/rRuleString', () => { ); }); + it('should throw when FREQ has an unsupported value', () => { + expect(() => parseRRule(adapter, 'FREQ=HOURLY', 'default')).to.throw( + 'MUI X Scheduler: Invalid FREQ value "HOURLY". The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Provide a supported frequency value.', + ); + }); + + it('should throw when the object input has an unsupported freq value', () => { + expect(() => + parseRRule(adapter, { freq: 'HOURLY' as SchedulerEventRecurrenceRule['freq'] }, 'default'), + ).to.throw( + 'MUI X Scheduler: Invalid FREQ value "HOURLY". The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Provide a supported frequency value.', + ); + }); + + it('should reject a non-canonical object freq instead of normalizing it', () => { + expect(() => + parseRRule(adapter, { freq: 'daily' as SchedulerEventRecurrenceRule['freq'] }, 'default'), + ).to.throw( + 'MUI X Scheduler: Invalid FREQ value "daily". The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Provide a supported frequency value.', + ); + }); + + it('should throw a dedicated message when the object input has no freq value', () => { + expect(() => parseRRule(adapter, {} as SchedulerEventRecurrenceRule, 'default')).to.throw( + 'MUI X Scheduler: The recurrence rule must include a FREQ value. The frequency (DAILY, WEEKLY, MONTHLY, or YEARLY) is required for recurrence rules. Provide a freq value.', + ); + }); + + it('should throw when the object freq matches a prototype key', () => { + expect(() => + parseRRule( + adapter, + { freq: 'toString' as SchedulerEventRecurrenceRule['freq'] }, + 'default', + ), + ).to.throw( + 'MUI X Scheduler: Invalid FREQ value "toString". The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. Provide a supported frequency value.', + ); + }); + it('should throw when the RRULE contains unsupported properties', () => { expect(() => parseRRule(adapter, 'FREQ=DAILY;FOO=bar', 'default')).to.throw( 'MUI X Scheduler: Unsupported RRULE property "FOO". Supported properties are: FREQ, INTERVAL, BYDAY, BYMONTHDAY, BYMONTH, UNTIL, COUNT. Remove or replace the unsupported property.', diff --git a/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.ts b/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.ts index 39d7187c93ce1..f9d2faae5bf55 100644 --- a/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.ts +++ b/packages/x-scheduler-internals-premium/src/internals/utils/recurring-events/rRuleString.ts @@ -2,6 +2,7 @@ import { TemporalTimezone } from '@mui/x-scheduler-internals/base-ui-copy'; import { Adapter } from '@mui/x-scheduler-internals/use-adapter'; import { RecurringEventByDayValue, + RecurringEventFrequency, SchedulerProcessedEventRecurrenceRule, SchedulerEventRecurrenceRule, } from '@mui/x-scheduler-internals/models'; @@ -18,12 +19,40 @@ const SUPPORTED_RRULE_KEYS = new Set([ 'COUNT', ]); +// Fails to compile if `RecurringEventFrequency` gains or loses a member, keeping the runtime check in sync with the type. +const SUPPORTED_FREQUENCIES: Record = { + DAILY: true, + WEEKLY: true, + MONTHLY: true, + YEARLY: true, +}; + +function validateFreq(freq: string): RecurringEventFrequency { + if (!freq) { + throw new Error( + 'MUI X Scheduler: The recurrence rule must include a FREQ value. ' + + 'The frequency (DAILY, WEEKLY, MONTHLY, or YEARLY) is required for recurrence rules. ' + + 'Provide a freq value.', + ); + } + if (!Object.prototype.hasOwnProperty.call(SUPPORTED_FREQUENCIES, freq)) { + throw new Error( + `MUI X Scheduler: Invalid FREQ value "${freq}". ` + + 'The frequency must be one of DAILY, WEEKLY, MONTHLY, or YEARLY. ' + + 'Provide a supported frequency value.', + ); + } + return freq as RecurringEventFrequency; +} + export function parseRRule( adapter: Adapter, input: string | SchedulerEventRecurrenceRule, timezone: TemporalTimezone, ): SchedulerProcessedEventRecurrenceRule { if (typeof input === 'object') { + // The typed object API validates `freq` as-is; unlike the RRULE string below, it is not normalized. + validateFreq(input.freq); if (input.until != null) { return { ...input, @@ -72,7 +101,7 @@ export function parseRRule( } const rrule: SchedulerProcessedEventRecurrenceRule = { - freq: rruleObject.FREQ as SchedulerProcessedEventRecurrenceRule['freq'], + freq: validateFreq(rruleObject.FREQ), }; if (rruleObject.INTERVAL) {