1'use strict'; 2 3import { Platform } from 'react-native'; 4 5import { Location, Permissions, Constants, TaskManager } from 'expo'; 6import * as TestUtils from '../TestUtils'; 7 8const BACKGROUND_LOCATION_TASK = 'background-location-updates'; 9const GEOFENCING_TASK = 'geofencing-task'; 10 11export const name = 'Location'; 12 13export async function test(t) { 14 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 15 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 16 17 t.describe('Location', () => { 18 t.describe('Location.getProviderStatusAsync()', () => { 19 const timeout = 1000; 20 t.it( 21 'checks if location services are enabled', 22 async () => { 23 const result = await Location.getProviderStatusAsync(); 24 t.expect(result.locationServicesEnabled).not.toBe(undefined); 25 }, 26 timeout 27 ); 28 if (Platform.OS === 'android') { 29 t.it( 30 'detects when GPS sensor is enabled', 31 async () => { 32 const result = await Location.getProviderStatusAsync(); 33 t.expect(result.gpsAvailable).not.toBe(undefined); 34 }, 35 timeout 36 ); 37 t.it( 38 'detects when network location is enabled', 39 async () => { 40 const result = await Location.getProviderStatusAsync(); 41 t.expect(result.networkAvailable).not.toBe(undefined); 42 }, 43 timeout 44 ); 45 t.it( 46 'detects when passive location is enabled', 47 async () => { 48 const result = await Location.getProviderStatusAsync(); 49 t.expect(result.passiveAvailable).not.toBe(undefined); 50 }, 51 timeout 52 ); 53 } 54 }); 55 describeWithPermissions('Location.getCurrentPositionAsync()', () => { 56 // Manual interaction: 57 // 1. Just try 58 // 2. iOS Settings --> General --> Reset --> Reset Location & Privacy, 59 // try gain and "Allow" 60 // 3. Retry from experience restart. 61 // 4. Retry from app restart. 62 // 5. iOS Settings --> General --> Reset --> Reset Location & Privacy, 63 // try gain and "Don't Allow" 64 // 6. Retry from experience restart. 65 // 7. Retry from app restart. 66 67 const testShapeOrUnauthorized = options => async () => { 68 const providerStatus = await Location.getProviderStatusAsync(); 69 if (providerStatus.locationServicesEnabled) { 70 const { status } = await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 71 return Permissions.askAsync(Permissions.LOCATION); 72 }); 73 if (status === 'granted') { 74 const { 75 coords: { latitude, longitude, altitude, accuracy, altitudeAccuracy, heading, speed }, 76 timestamp, 77 } = await Location.getCurrentPositionAsync(options); 78 t.expect(typeof latitude === 'number').toBe(true); 79 t.expect(typeof longitude === 'number').toBe(true); 80 t.expect(typeof altitude === 'number').toBe(true); 81 t.expect(typeof accuracy === 'number').toBe(true); 82 t.expect(Platform.OS !== 'ios' || typeof altitudeAccuracy === 'number').toBe(true); 83 t.expect(typeof heading === 'number').toBe(true); 84 t.expect(typeof speed === 'number').toBe(true); 85 t.expect(typeof timestamp === 'number').toBe(true); 86 } else { 87 let error; 88 try { 89 await Location.getCurrentPositionAsync(options); 90 } catch (e) { 91 error = e; 92 } 93 t.expect(error.message).toMatch(/Not authorized/); 94 } 95 } else { 96 let error; 97 try { 98 await Location.getCurrentPositionAsync(options); 99 } catch (e) { 100 error = e; 101 } 102 t.expect(error.message).toMatch(/Location services are disabled/); 103 } 104 }; 105 106 const second = 1000; 107 const timeout = 20 * second; // Allow manual touch on permissions dialog 108 109 t.it( 110 'gets a result of the correct shape (without high accuracy), or ' + 111 'throws error if no permission or disabled', 112 testShapeOrUnauthorized({ accuracy: Location.Accuracy.Balanced }), 113 timeout 114 ); 115 t.it( 116 'gets a result of the correct shape (without high accuracy), or ' + 117 'throws error if no permission or disabled (when trying again immediately)', 118 testShapeOrUnauthorized({ accuracy: Location.Accuracy.Balanced }), 119 timeout 120 ); 121 t.it( 122 'gets a result of the correct shape (with high accuracy), or ' + 123 'throws error if no permission or disabled (when trying again immediately)', 124 testShapeOrUnauthorized({ accuracy: Location.Accuracy.Highest }), 125 timeout 126 ); 127 128 t.it( 129 'gets a result of the correct shape (without high accuracy), or ' + 130 'throws error if no permission or disabled (when trying again after 1 second)', 131 async () => { 132 await new Promise(resolve => setTimeout(resolve, 1000)); 133 await testShapeOrUnauthorized({ accuracy: Location.Accuracy.Balanced })(); 134 }, 135 timeout + second 136 ); 137 138 t.it( 139 'gets a result of the correct shape (with high accuracy), or ' + 140 'throws error if no permission or disabled (when trying again after 1 second)', 141 async () => { 142 await new Promise(resolve => setTimeout(resolve, 1000)); 143 await testShapeOrUnauthorized({ accuracy: Location.Accuracy.Highest })(); 144 }, 145 timeout + second 146 ); 147 }); 148 149 describeWithPermissions('Location.getHeadingAsync()', () => { 150 const testCompass = options => async () => { 151 // Disable Compass Test if in simulator 152 if (Constants.isDevice) { 153 const { status } = await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 154 return Permissions.askAsync(Permissions.LOCATION); 155 }); 156 if (status === 'granted') { 157 const heading = await Location.getHeadingAsync(); 158 t.expect(typeof heading.magHeading === 'number').toBe(true); 159 t.expect(typeof heading.trueHeading === 'number').toBe(true); 160 t.expect(typeof heading.accuracy === 'number').toBe(true); 161 } else { 162 let error; 163 try { 164 await Location.getHeadingAsync(); 165 } catch (e) { 166 error = e; 167 } 168 t.expect(error.message).toMatch(/Not authorized/); 169 } 170 } 171 }; 172 const second = 1000; 173 const timeout = 20 * second; // Allow manual touch on permissions dialog 174 175 t.it( 176 'Checks if compass is returning right values (trueHeading, magHeading, accuracy)', 177 testCompass(), 178 timeout 179 ); 180 }); 181 182 t.describe('Location.geocodeAsync()', () => { 183 const timeout = 2000; 184 185 t.it( 186 'geocodes a place of the right shape', 187 async () => { 188 const result = await Location.geocodeAsync('900 State St, Salem, OR'); 189 t.expect(Array.isArray(result)).toBe(true); 190 t.expect(typeof result[0]).toBe('object'); 191 const { latitude, longitude, accuracy, altitude } = result[0]; 192 t.expect(typeof latitude).toBe('number'); 193 t.expect(typeof longitude).toBe('number'); 194 t.expect(typeof accuracy).toBe('number'); 195 t.expect(typeof altitude).toBe('number'); 196 }, 197 timeout 198 ); 199 200 t.it( 201 'returns an empty array when the address is not found', 202 async () => { 203 const result = await Location.geocodeAsync(':('); 204 t.expect(result).toEqual([]); 205 }, 206 timeout 207 ); 208 }); 209 210 t.describe('Location.reverseGeocodeAsync()', () => { 211 const timeout = 2000; 212 213 t.it( 214 'gives a right shape address of a point location', 215 async () => { 216 const result = await Location.reverseGeocodeAsync({ 217 latitude: 60.166595, 218 longitude: 24.944865, 219 }); 220 t.expect(Array.isArray(result)).toBe(true); 221 t.expect(typeof result[0]).toBe('object'); 222 const fields = ['city', 'street', 'region', 'country', 'postalCode', 'name']; 223 fields.forEach(field => { 224 t 225 .expect(typeof result[field] === 'string' || typeof result[field] === 'undefined') 226 .toBe(true); 227 }); 228 }, 229 timeout 230 ); 231 232 t.it("throws for a location where `latitude` and `longitude` aren't numbers", async () => { 233 let error; 234 try { 235 await Location.reverseGeocodeAsync({ 236 latitude: '60', 237 longitude: '24', 238 }); 239 } catch (e) { 240 error = e; 241 } 242 t.expect(error instanceof TypeError).toBe(true); 243 }); 244 }); 245 246 t.describe('Location.hasServicesEnabledAsync()', () => { 247 t.it('checks if location services are enabled', async () => { 248 const result = await Location.hasServicesEnabledAsync(); 249 t.expect(result).toBe(true); 250 }); 251 }); 252 253 describeWithPermissions('Location - background location updates', () => { 254 async function expectTaskAccuracyToBe(accuracy) { 255 const locationTask = await TaskManager.getTaskOptionsAsync(BACKGROUND_LOCATION_TASK); 256 257 t.expect(locationTask).toBeDefined(); 258 t.expect(locationTask.accuracy).toBe(accuracy); 259 } 260 261 t.it('starts location updates', async () => { 262 await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 263 }); 264 265 t.it('has started location updates', async () => { 266 const started = await Location.hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 267 t.expect(started).toBe(true); 268 }); 269 270 t.it('defaults to balanced accuracy', async () => { 271 await expectTaskAccuracyToBe(Location.Accuracy.Balanced); 272 }); 273 274 t.it('can update existing task', async () => { 275 const newAccuracy = Location.Accuracy.Highest; 276 await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, { 277 accuracy: newAccuracy, 278 }); 279 expectTaskAccuracyToBe(newAccuracy); 280 }); 281 282 t.it('stops location updates', async () => { 283 await Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 284 }); 285 286 t.it('has stopped location updates', async () => { 287 const started = await Location.hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 288 t.expect(started).toBe(false); 289 }); 290 }); 291 292 describeWithPermissions('Location - geofencing', () => { 293 const regions = [ 294 { 295 identifier: 'Kraków, Poland', 296 radius: 8000, 297 latitude: 50.0468548, 298 longitude: 19.9348341, 299 notifyOnEntry: true, 300 notifyOnExit: true, 301 }, 302 { 303 identifier: 'Apple', 304 radius: 1000, 305 latitude: 37.3270145, 306 longitude: -122.0310273, 307 notifyOnEntry: true, 308 notifyOnExit: true, 309 }, 310 ]; 311 312 async function expectTaskRegionsToBeLike(regions) { 313 const geofencingTask = await TaskManager.getTaskOptionsAsync(GEOFENCING_TASK); 314 315 t.expect(geofencingTask).toBeDefined(); 316 t.expect(geofencingTask.regions).toBeDefined(); 317 t.expect(geofencingTask.regions.length).toBe(regions.length); 318 319 for (let i = 0; i < regions.length; i++) { 320 t.expect(geofencingTask.regions[i].identifier).toBe(regions[i].identifier); 321 t.expect(geofencingTask.regions[i].radius).toBe(regions[i].radius); 322 t.expect(geofencingTask.regions[i].latitude).toBe(regions[i].latitude); 323 t.expect(geofencingTask.regions[i].longitude).toBe(regions[i].longitude); 324 } 325 } 326 327 t.it('starts geofencing', async () => { 328 await Location.startGeofencingAsync(GEOFENCING_TASK, regions); 329 }); 330 331 t.it('has started geofencing', async () => { 332 const started = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); 333 t.expect(started).toBe(true); 334 }); 335 336 t.it('is monitoring correct regions', async () => { 337 expectTaskRegionsToBeLike(regions); 338 }); 339 340 t.it('can update geofencing regions', async () => { 341 const newRegions = regions.slice(1); 342 await Location.startGeofencingAsync(GEOFENCING_TASK, newRegions); 343 expectTaskRegionsToBeLike(newRegions); 344 }); 345 346 t.it('stops geofencing', async () => { 347 await Location.stopGeofencingAsync(GEOFENCING_TASK); 348 }); 349 350 t.it('has stopped geofencing', async () => { 351 const started = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); 352 t.expect(started).toBe(false); 353 }); 354 355 t.it('throws when starting geofencing with incorrect regions', async () => { 356 await (async () => { 357 let error; 358 try { 359 await Location.startGeofencingAsync(GEOFENCING_TASK, []); 360 } catch (e) { 361 error = e; 362 } 363 t.expect(error instanceof Error).toBe(true); 364 })(); 365 366 await (async () => { 367 let error; 368 try { 369 await Location.startGeofencingAsync(GEOFENCING_TASK, [{ longitude: 'not a number' }]); 370 } catch (e) { 371 error = e; 372 } 373 t.expect(error instanceof TypeError).toBe(true); 374 })(); 375 }); 376 }); 377 }); 378} 379 380// Define empty tasks, otherwise tasks might automatically unregister themselves if no task is defined. 381TaskManager.defineTask(BACKGROUND_LOCATION_TASK, () => {}); 382TaskManager.defineTask(GEOFENCING_TASK, () => {}); 383