1'use strict'; 2 3import { isMatch } from 'lodash'; 4import { Platform } from 'react-native'; 5import { Notifications, Permissions } from 'expo'; 6 7import { waitFor } from './helpers'; 8import * as TestUtils from '../TestUtils'; 9 10export const name = 'Notifications'; 11 12const localNotificationFactory = overrides => ({ 13 title: 'Notification title', 14 body: 'Body text of the notification', 15 ...overrides, 16}); 17 18// We need to accumulate notifications, because listener of current test 19// was being notified of notifications scheduled by previous tests. 20const waitForCallOfListener = (notificationOverrides, options) => 21 new Promise(async (resolve, reject) => { 22 let timeout = options ? options.timeout : null; 23 let notificationsCount = (options ? options.notificationsCount : null) || (timeout ? null : 1); 24 let executorFunction = 25 (options ? options.executor : null) || Notifications.presentLocalNotificationAsync; 26 let subscription = null; 27 let notifications = []; 28 29 const finish = () => { 30 if (subscription) { 31 subscription.remove(); 32 } 33 resolve(notifications); 34 }; 35 36 const listener = notification => { 37 notifications.push(notification); 38 if (notificationsCount === notifications.length) { 39 finish(); 40 } 41 }; 42 subscription = Notifications.addListener(listener); 43 executorFunction(localNotificationFactory(notificationOverrides)).catch(reject); 44 if (timeout) { 45 await waitFor(timeout); 46 finish(); 47 } 48 }); 49 50export async function test(t) { 51 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 52 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 53 54 describeWithPermissions('Notifications', () => { 55 t.beforeAll(async () => { 56 await Permissions.askAsync(Permissions.NOTIFICATIONS); 57 }); 58 59 t.afterEach(async () => { 60 if (Platform.OS === 'android') { 61 await Notifications.dismissAllNotificationsAsync(); 62 } 63 }); 64 65 t.describe('getExpoPushTokenAsync', () => { 66 t.it('resolves with a string', async () => { 67 const expoPushToken = await Notifications.getExpoPushTokenAsync(); 68 t.expect(typeof expoPushToken === 'string').toBe(true); 69 }); 70 }); 71 72 t.describe('presentLocalNotificationAsync', () => { 73 t.it('resolves with notificationId', async () => { 74 let error = null; 75 try { 76 const notificationId = await Notifications.presentLocalNotificationAsync( 77 localNotificationFactory() 78 ); 79 t.expect(notificationId).toBeDefined(); 80 } catch (e) { 81 error = e; 82 } 83 t.expect(error).toBeNull(); 84 }); 85 86 // It turns out iOS rejects such notifications, while Android does not. 87 if (Platform.OS === 'ios') { 88 t.it('rejects notification with empty body', async () => { 89 let error = null; 90 try { 91 await Notifications.presentLocalNotificationAsync( 92 localNotificationFactory({ body: null }) 93 ); 94 } catch (e) { 95 error = e; 96 } 97 t.expect(error).not.toBeNull(); 98 }); 99 } 100 101 t.it('rejects notification with empty title', async () => { 102 let error = null; 103 try { 104 await Notifications.presentLocalNotificationAsync( 105 localNotificationFactory({ title: null }) 106 ); 107 } catch (e) { 108 error = e; 109 } 110 t.expect(error).not.toBeNull(); 111 }); 112 }); 113 114 t.describe('addListener', () => { 115 t.it('is notified of new notifications', async () => { 116 const notificationsListener = t.jasmine.createSpy('notificationsListener'); 117 const subscription = Notifications.addListener(notificationsListener); 118 await Notifications.presentLocalNotificationAsync(localNotificationFactory()); 119 await waitFor(500); 120 t.expect(notificationsListener).toHaveBeenCalled(); 121 subscription.remove(); 122 }); 123 124 t.it('reported notifications have origin=received', async () => { 125 const notifications = await waitForCallOfListener(); 126 t.expect(isMatch(notifications[0], { origin: 'received' })).toBe(true); 127 }); 128 129 t.it('reported notifications have proper data attached', async () => { 130 const data = { scheduledAt: new Date().getTime() }; 131 const notifications = await waitForCallOfListener({ data }, { timeout: 1000 }); 132 let hasMatched = false; 133 notifications.forEach(notification => { 134 hasMatched = hasMatched || isMatch(notification, { data }); 135 }); 136 t.expect(hasMatched).toBe(true); 137 }); 138 }); 139 140 t.describe('scheduleLocalNotificationAsync', () => { 141 // Android schedules notifications in a too unpredictable manner for it to be testable. 142 if (Platform.OS === 'ios') { 143 t.it('schedules local notifications', async () => { 144 const notificationsListener = t.jasmine.createSpy('notificationsListener'); 145 const subscription = Notifications.addListener(notificationsListener); 146 Notifications.scheduleLocalNotificationAsync(localNotificationFactory(), { 147 time: new Date().getTime() + 1000, 148 }); 149 await waitFor(800); 150 t.expect(notificationsListener).not.toHaveBeenCalled(); 151 await waitFor(500); 152 t.expect(notificationsListener).toHaveBeenCalled(); 153 subscription.remove(); 154 }); 155 156 t.it('data is properly set', async () => { 157 const data = { scheduledAt: new Date().getTime() }; 158 const notifications = await waitForCallOfListener( 159 { data }, 160 { 161 timeout: 3000, 162 executor: notification => 163 Notifications.scheduleLocalNotificationAsync(notification, { 164 time: new Date().getTime() + 1000, 165 }), 166 } 167 ); 168 let hasMatched = false; 169 notifications.forEach(notification => { 170 hasMatched = hasMatched || isMatch(notification, { data }); 171 }); 172 t.expect(hasMatched).toBe(true); 173 }); 174 } 175 }); 176 177 if (Platform.OS === 'android') { 178 t.describe('cancelScheduledNotificationAsync', () => { 179 t.it('cancels a scheduled notification', async () => { 180 let error = null; 181 try { 182 const data = { scheduledAt: new Date().getTime() }; 183 const notificationId = await Notifications.scheduleLocalNotificationAsync( 184 localNotificationFactory(), 185 { 186 time: new Date().getTime() + 1000, 187 } 188 ); 189 await Notifications.cancelScheduledNotificationAsync(notificationId); 190 const notifications = await waitForCallOfListener(null, { 191 timeout: 3000, 192 executor: async () => {}, 193 }); 194 let hasMatched = false; 195 notifications.forEach(notification => { 196 hasMatched = hasMatched || isMatch(notification, { data }); 197 }); 198 t.expect(hasMatched).toBe(false); 199 } catch (e) { 200 error = e; 201 } 202 t.expect(error).toBeNull(); 203 }); 204 }); 205 206 t.describe('cancelAllScheduledNotificationsAsync', () => { 207 t.it('cancels a scheduled notification', async () => { 208 let error = null; 209 try { 210 const data = { scheduledAt: new Date().getTime() }; 211 await Notifications.scheduleLocalNotificationAsync(localNotificationFactory(), { 212 time: new Date().getTime() + 1000, 213 }); 214 await Notifications.cancelAllScheduledNotificationsAsync(); 215 const notifications = await waitForCallOfListener(null, { 216 timeout: 3000, 217 executor: async () => {}, 218 }); 219 let hasMatched = false; 220 notifications.forEach(notification => { 221 hasMatched = hasMatched || isMatch(notification, { data }); 222 }); 223 t.expect(hasMatched).toBe(false); 224 } catch (e) { 225 error = e; 226 } 227 t.expect(error).toBeNull(); 228 }); 229 }); 230 } 231 232 if (Platform.OS === 'ios') { 233 t.describe('getBadgeNumberAsync', () => { 234 t.it('resolves with a number', async () => { 235 const badgeNumber = await Notifications.getBadgeNumberAsync(); 236 t.expect(typeof badgeNumber === 'number').toBe(true); 237 }); 238 }); 239 240 t.describe('setBadgeNumberAsync', () => { 241 t.afterEach(async () => await Notifications.setBadgeNumberAsync(0)); 242 243 t.it('sets the badge number', async () => { 244 await Notifications.setBadgeNumberAsync(10); 245 const badgeNumber = await Notifications.getBadgeNumberAsync(); 246 t.expect(badgeNumber).toEqual(10); 247 }); 248 }); 249 } 250 }); 251} 252