xref: /expo/apps/test-suite/tests/Calendar.js (revision b52e368f)
1import * as Calendar from 'expo-calendar';
2import { UnavailabilityError } from 'expo-modules-core';
3import { Platform } from 'react-native';
4
5import * as TestUtils from '../TestUtils';
6
7export const name = 'Calendar';
8
9async function createTestCalendarAsync(patch = {}) {
10  return await Calendar.createCalendarAsync({
11    title: 'Expo test-suite calendar',
12    color: '#4B968A',
13    entityType: Calendar.EntityTypes.EVENT,
14    name: 'expo-test-suite-calendar',
15    sourceId: await pickCalendarSourceIdAsync(),
16    source: {
17      isLocalAccount: true,
18      name: 'expo',
19    },
20    ownerAccount: 'expo',
21    accessLevel: Calendar.CalendarAccessLevel.OWNER,
22    ...patch,
23  });
24}
25
26async function getCalendarByIdAsync(calendarId) {
27  const calendars = await Calendar.getCalendarsAsync();
28  return calendars.find((calendar) => calendar.id === calendarId);
29}
30
31async function pickCalendarSourceIdAsync() {
32  if (Platform.OS === 'ios') {
33    const sources = await Calendar.getSourcesAsync();
34    const mainSource = sources.find((source) => source.name === 'iCloud') || sources[0];
35    return mainSource && mainSource.id;
36  }
37}
38
39async function createTestEventAsync(calendarId, customArgs = {}) {
40  return await Calendar.createEventAsync(calendarId, {
41    title: 'App.js Conference',
42    startDate: +new Date(2019, 3, 4), // 4th April 2019, months are counted from 0
43    endDate: +new Date(2019, 3, 5), // 5th April 2019
44    timeZone: 'Europe/Warsaw',
45    allDay: true,
46    location: 'Qubus Hotel, Nadwiślańska 6, 30-527 Kraków, Poland',
47    notes: 'The very first Expo & React Native conference in Europe',
48    availability: Calendar.Availability.BUSY,
49    ...customArgs,
50  });
51}
52
53async function createTestAttendeeAsync(eventId) {
54  return await Calendar.createAttendeeAsync(eventId, {
55    name: 'Guest',
56    email: '[email protected]',
57    role: Calendar.AttendeeRole.ATTENDEE,
58    status: Calendar.AttendeeStatus.ACCEPTED,
59    type: Calendar.AttendeeType.PERSON,
60  });
61}
62
63async function getAttendeeByIdAsync(eventId, attendeeId) {
64  const attendees = await Calendar.getAttendeesForEventAsync(eventId);
65  return attendees.find((attendee) => attendee.id === attendeeId);
66}
67
68export async function test(t) {
69  const shouldSkipTestsRequiringPermissions =
70    await TestUtils.shouldSkipTestsRequiringPermissionsAsync();
71  const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe;
72
73  function testCalendarShape(calendar) {
74    t.expect(calendar).toBeDefined();
75    t.expect(typeof calendar.id).toBe('string');
76    t.expect(typeof calendar.title).toBe('string');
77    t.expect(typeof calendar.source).toBe('object');
78    testCalendarSourceShape(calendar.source);
79    t.expect(typeof calendar.color).toBe('string');
80    t.expect(typeof calendar.allowsModifications).toBe('boolean');
81
82    t.expect(Array.isArray(calendar.allowedAvailabilities)).toBe(true);
83    calendar.allowedAvailabilities.forEach((availability) => {
84      t.expect(Object.values(Calendar.Availability)).toContain(availability);
85    });
86
87    if (Platform.OS === 'ios') {
88      t.expect(typeof calendar.entityType).toBe('string');
89      t.expect(Object.values(Calendar.EntityTypes)).toContain(calendar.entityType);
90
91      t.expect(typeof calendar.type).toBe('string');
92      t.expect(Object.values(Calendar.CalendarType)).toContain(calendar.type);
93    }
94    if (Platform.OS === 'android') {
95      t.expect(typeof calendar.isPrimary).toBe('boolean');
96      calendar.name && t.expect(typeof calendar.name).toBe('string');
97      t.expect(typeof calendar.ownerAccount).toBe('string');
98      calendar.timeZone && t.expect(typeof calendar.timeZone).toBe('string');
99
100      t.expect(Array.isArray(calendar.allowedReminders)).toBe(true);
101      calendar.allowedReminders.forEach((reminder) => {
102        t.expect(Object.values(Calendar.AlarmMethod)).toContain(reminder);
103      });
104
105      t.expect(Array.isArray(calendar.allowedAttendeeTypes)).toBe(true);
106      calendar.allowedAttendeeTypes.forEach((attendeeType) => {
107        t.expect(Object.values(Calendar.AttendeeType)).toContain(attendeeType);
108      });
109
110      t.expect(typeof calendar.isVisible).toBe('boolean');
111      t.expect(typeof calendar.isSynced).toBe('boolean');
112      t.expect(typeof calendar.accessLevel).toBe('string');
113    }
114  }
115
116  function testEventShape(event) {
117    t.expect(event).toBeDefined();
118    t.expect(typeof event.id).toBe('string');
119    t.expect(typeof event.calendarId).toBe('string');
120    t.expect(typeof event.title).toBe('string');
121    t.expect(typeof event.startDate).toBe('string');
122    t.expect(typeof event.endDate).toBe('string');
123    t.expect(typeof event.allDay).toBe('boolean');
124    t.expect(typeof event.location).toBe('string');
125    t.expect(typeof event.notes).toBe('string');
126    t.expect(Array.isArray(event.alarms)).toBe(true);
127    event.recurrenceRule && t.expect(typeof event.recurrenceRule).toBe('object');
128    t.expect(Object.values(Calendar.Availability)).toContain(event.availability);
129    event.timeZone && t.expect(typeof event.timeZone).toBe('string');
130
131    if (Platform.OS === 'ios') {
132      event.url && t.expect(typeof event.url).toBe('string');
133      t.expect(typeof event.creationDate).toBe('string');
134      t.expect(typeof event.lastModifiedDate).toBe('string');
135      t.expect(typeof event.originalStartDate).toBe('string');
136      t.expect(typeof event.isDetached).toBe('boolean');
137      t.expect(Object.values(Calendar.EventStatus)).toContain(event.status);
138
139      if (event.organizer) {
140        t.expect(typeof event.organizer).toBe('object');
141        testAttendeeShape(event.organizer);
142      }
143    }
144    if (Platform.OS === 'android') {
145      t.expect(typeof event.endTimeZone).toBe('string');
146      t.expect(typeof event.organizerEmail).toBe('string');
147      t.expect(Object.values(Calendar.EventAccessLevel)).toContain(event.accessLevel);
148      t.expect(typeof event.guestsCanModify).toBe('boolean');
149      t.expect(typeof event.guestsCanInviteOthers).toBe('boolean');
150      t.expect(typeof event.guestsCanSeeGuests).toBe('boolean');
151      event.originalId && t.expect(typeof event.originalId).toBe('string');
152      event.instanceId && t.expect(typeof event.instanceId).toBe('string');
153    }
154  }
155
156  function testCalendarSourceShape(source) {
157    t.expect(source).toBeDefined();
158    t.expect(typeof source.type).toBe('string');
159
160    if (source.name !== null) {
161      // source.name can be null if it refers to the local (unnamed) calendar.
162      t.expect(typeof source.name).toBe('string');
163    }
164
165    if (Platform.OS === 'ios') {
166      t.expect(typeof source.id).toBe('string');
167    }
168    if (Platform.OS === 'android') {
169      t.expect(typeof source.isLocalAccount).toBe('boolean');
170    }
171  }
172
173  function testAttendeeShape(attendee) {
174    t.expect(attendee).toBeDefined();
175    t.expect(typeof attendee.name).toBe('string');
176    t.expect(typeof attendee.role).toBe('string');
177    t.expect(Object.values(Calendar.AttendeeRole)).toContain(attendee.role);
178    t.expect(typeof attendee.status).toBe('string');
179    t.expect(Object.values(Calendar.AttendeeStatus)).toContain(attendee.status);
180    t.expect(typeof attendee.type).toBe('string');
181    t.expect(Object.values(Calendar.AttendeeType)).toContain(attendee.type);
182
183    if (Platform.OS === 'ios') {
184      t.expect(typeof attendee.url).toBe('string');
185      t.expect(typeof attendee.isCurrentUser).toBe('boolean');
186    }
187    if (Platform.OS === 'android') {
188      t.expect(typeof attendee.id).toBe('string');
189      t.expect(typeof attendee.email).toBe('string');
190    }
191  }
192
193  function expectMethodsToReject(methods) {
194    for (const methodName of methods) {
195      t.describe(`${methodName}()`, () => {
196        t.it('rejects with UnavailabilityError on unsupported platform', async () => {
197          let error;
198          try {
199            await Calendar[methodName]();
200          } catch (e) {
201            error = e;
202          }
203          t.expect(error instanceof UnavailabilityError).toBe(true);
204          t.expect(error.message).toBe(new UnavailabilityError('Calendar', methodName).message);
205        });
206      });
207    }
208  }
209
210  describeWithPermissions('Calendar', () => {
211    t.describe('requestCalendarPermissionsAsync()', () => {
212      t.it('requests for Calendar permissions', async () => {
213        const results = await Calendar.requestCalendarPermissionsAsync();
214
215        t.expect(results.granted).toBe(true);
216        t.expect(results.status).toBe('granted');
217      });
218    });
219
220    t.describe('createCalendarAsync()', () => {
221      let calendarId;
222
223      t.it('creates a calendar', async () => {
224        calendarId = await createTestCalendarAsync();
225        const calendar = await getCalendarByIdAsync(calendarId);
226
227        t.expect(calendarId).toBeDefined();
228        t.expect(typeof calendarId).toBe('string');
229        testCalendarShape(calendar);
230      });
231
232      t.afterAll(async () => {
233        await Calendar.deleteCalendarAsync(calendarId);
234      });
235    });
236
237    t.describe('getCalendarsAsync()', () => {
238      let calendarId;
239
240      t.beforeAll(async () => {
241        calendarId = await createTestCalendarAsync();
242      });
243
244      t.it('returns an array of calendars with correct shape', async () => {
245        const calendars = await Calendar.getCalendarsAsync();
246
247        t.expect(Array.isArray(calendars)).toBeTruthy();
248
249        for (const calendar of calendars) {
250          testCalendarShape(calendar);
251        }
252      });
253
254      if (Platform.OS === 'ios') {
255        t.it('returns an array of calendars for reminders', async () => {
256          const calendars = await Calendar.getCalendarsAsync(Calendar.EntityTypes.REMINDER);
257
258          t.expect(Array.isArray(calendars)).toBeTruthy();
259
260          for (const calendar of calendars) {
261            t.expect(calendar.entityType).toBe(Calendar.EntityTypes.REMINDER);
262          }
263        });
264      }
265
266      t.afterAll(async () => {
267        await Calendar.deleteCalendarAsync(calendarId);
268      });
269    });
270
271    t.describe('deleteCalendarAsync()', () => {
272      t.it('deletes a calendar', async () => {
273        const calendarId = await createTestCalendarAsync();
274        await Calendar.deleteCalendarAsync(calendarId);
275
276        const calendars = await Calendar.getCalendarsAsync();
277        t.expect(calendars.findIndex((calendar) => calendar.id === calendarId)).toBe(-1);
278      });
279    });
280
281    t.describe('updateCalendarAsync()', () => {
282      let calendarId;
283
284      t.beforeAll(async () => {
285        calendarId = await createTestCalendarAsync();
286      });
287
288      t.it('updates a calendar', async () => {
289        const newTitle = 'New test-suite calendar title';
290        const updatedCalendarId = await Calendar.updateCalendarAsync(calendarId, {
291          title: newTitle,
292        });
293        const updatedCalendar = await getCalendarByIdAsync(calendarId);
294
295        t.expect(updatedCalendarId).toBe(calendarId);
296        t.expect(updatedCalendar.title).toBe(newTitle);
297      });
298
299      t.afterAll(async () => {
300        await Calendar.deleteCalendarAsync(calendarId);
301      });
302    });
303
304    t.describe('createEventAsync()', () => {
305      let calendarId;
306
307      t.beforeAll(async () => {
308        calendarId = await createTestCalendarAsync();
309      });
310
311      t.it('creates an event in the specific calendar', async () => {
312        const eventId = await createTestEventAsync(calendarId);
313
314        t.expect(eventId).toBeDefined();
315        t.expect(typeof eventId).toBe('string');
316      });
317
318      t.it('creates an event with the recurrence rule', async () => {
319        const eventId = await createTestEventAsync(calendarId, {
320          recurrenceRule: {
321            endDate: new Date(2019, 3, 5),
322            frequency: 'daily',
323            interval: 1,
324          },
325        });
326
327        t.expect(eventId).toBeDefined();
328        t.expect(typeof eventId).toBe('string');
329      });
330
331      if (Platform.OS === 'ios') {
332        t.it('rejects when time zone is invalid', async () => {
333          let error;
334          try {
335            await createTestEventAsync(calendarId, { timeZone: '' });
336          } catch (e) {
337            error = e;
338          }
339          t.expect(error).toBeDefined();
340          t.expect(error.code).toBe('E_EVENT_INVALID_TIMEZONE');
341        });
342      }
343
344      t.afterAll(async () => {
345        await Calendar.deleteCalendarAsync(calendarId);
346      });
347    });
348
349    t.describe('getEventsAsync()', () => {
350      let calendarId, eventId;
351
352      t.beforeEach(async () => {
353        calendarId = await createTestCalendarAsync();
354        eventId = await createTestEventAsync(calendarId);
355      });
356
357      t.it('resolves to an array with an event of the correct shape', async () => {
358        const events = await Calendar.getEventsAsync(
359          [calendarId],
360          +new Date(2019, 3, 1),
361          +new Date(2019, 3, 29)
362        );
363
364        t.expect(Array.isArray(events)).toBe(true);
365        t.expect(events.length).toBe(1);
366        t.expect(events[0].id).toBe(eventId);
367        testEventShape(events[0]);
368      });
369
370      t.afterEach(async () => {
371        await Calendar.deleteCalendarAsync(calendarId);
372      });
373    });
374
375    t.describe('getEventAsync()', () => {
376      let calendarId, eventId;
377
378      t.beforeAll(async () => {
379        calendarId = await createTestCalendarAsync();
380        eventId = await createTestEventAsync(calendarId);
381      });
382
383      t.it('returns event with given id', async () => {
384        const event = await Calendar.getEventAsync(eventId);
385
386        t.expect(event).toBeDefined();
387        t.expect(event.id).toBe(eventId);
388        testEventShape(event);
389      });
390
391      t.afterAll(async () => {
392        await Calendar.deleteCalendarAsync(calendarId);
393      });
394    });
395
396    t.describe('updateEventAsync()', () => {
397      let calendarId, eventId;
398
399      t.beforeAll(async () => {
400        calendarId = await createTestCalendarAsync();
401        eventId = await createTestEventAsync(calendarId);
402      });
403
404      t.it('updates an event', async () => {
405        await Calendar.updateEventAsync(eventId, {
406          availability: Calendar.Availability.FREE,
407        });
408        const updatedEvent = await Calendar.getEventAsync(eventId);
409
410        t.expect(updatedEvent).toBeDefined();
411        t.expect(updatedEvent.id).toBe(eventId);
412        t.expect([Calendar.Availability.FREE, Calendar.Availability.NOT_SUPPORTED]).toContain(
413          updatedEvent.availability
414        );
415      });
416
417      t.afterAll(async () => {
418        await Calendar.deleteCalendarAsync(calendarId);
419      });
420    });
421
422    t.describe('deleteEventAsync()', () => {
423      let calendarId, eventId;
424
425      t.beforeAll(async () => {
426        calendarId = await createTestCalendarAsync();
427        eventId = await createTestEventAsync(calendarId);
428      });
429
430      t.it('deletes an event', async () => {
431        await Calendar.deleteEventAsync(eventId);
432        let error;
433
434        try {
435          await Calendar.getEventAsync(eventId);
436        } catch (e) {
437          error = e;
438        }
439        t.expect(error).toBeDefined();
440        t.expect(error instanceof Error).toBe(true);
441        t.expect(error.code).toBe('E_EVENT_NOT_FOUND');
442      });
443
444      t.afterAll(async () => {
445        await Calendar.deleteCalendarAsync(calendarId);
446      });
447    });
448
449    if (Platform.OS === 'android') {
450      t.describe('createAttendeeAsync()', () => {
451        let calendarId, eventId;
452
453        t.beforeAll(async () => {
454          calendarId = await createTestCalendarAsync();
455          eventId = await createTestEventAsync(calendarId);
456        });
457
458        t.it('creates an attendee', async () => {
459          const attendeeId = await createTestAttendeeAsync(eventId);
460          const attendees = await Calendar.getAttendeesForEventAsync(eventId);
461
462          t.expect(Array.isArray(attendees)).toBe(true);
463
464          const newAttendee = attendees.find((attendee) => attendee.id === attendeeId);
465
466          t.expect(newAttendee).toBeDefined();
467          testAttendeeShape(newAttendee);
468        });
469
470        t.afterAll(async () => {
471          await Calendar.deleteCalendarAsync(calendarId);
472        });
473      });
474
475      t.describe('updateAttendeeAsync()', () => {
476        let calendarId, eventId, attendeeId;
477
478        t.beforeAll(async () => {
479          calendarId = await createTestCalendarAsync();
480          eventId = await createTestEventAsync(calendarId);
481          attendeeId = await createTestAttendeeAsync(eventId);
482        });
483
484        t.it('updates attendee record', async () => {
485          const updatedAttendeeId = await Calendar.updateAttendeeAsync(attendeeId, {
486            role: Calendar.AttendeeRole.PERFORMER,
487          });
488          const updatedAttendee = await getAttendeeByIdAsync(eventId, attendeeId);
489
490          t.expect(updatedAttendeeId).toBe(attendeeId);
491          t.expect(updatedAttendee).toBeDefined();
492          t.expect(updatedAttendee.role).toBe(Calendar.AttendeeRole.PERFORMER);
493        });
494
495        t.afterAll(async () => {
496          await Calendar.deleteCalendarAsync(calendarId);
497        });
498      });
499
500      t.describe('deleteAttendeeAsync()', () => {
501        let calendarId, eventId;
502
503        t.beforeAll(async () => {
504          calendarId = await createTestCalendarAsync();
505          eventId = await createTestEventAsync(calendarId);
506        });
507
508        t.it('deletes an attendee', async () => {
509          const attendeeId = await createTestAttendeeAsync(eventId);
510          await Calendar.deleteAttendeeAsync(attendeeId);
511
512          const attendee = await getAttendeeByIdAsync(eventId, attendeeId);
513
514          t.expect(attendee).toBeUndefined();
515        });
516
517        t.afterAll(async () => {
518          await Calendar.deleteCalendarAsync(calendarId);
519        });
520      });
521    } else {
522      expectMethodsToReject(['createAttendeeAsync', 'updateAttendeeAsync', 'deleteAttendeeAsync']);
523    }
524
525    if (Platform.OS === 'ios') {
526      t.describe('getDefaultCalendarAsync()', () => {
527        t.it('get default calendar', async () => {
528          const calendar = await Calendar.getDefaultCalendarAsync();
529
530          testCalendarShape(calendar);
531        });
532      });
533
534      t.describe('getSourcesAsync()', () => {
535        t.it('returns an array of sources', async () => {
536          const sources = await Calendar.getSourcesAsync();
537
538          t.expect(Array.isArray(sources)).toBe(true);
539        });
540      });
541    } else {
542      expectMethodsToReject(['getSourcesAsync']);
543    }
544  });
545}
546