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