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