1'use strict'; 2 3import Constants from 'expo-constants'; 4import * as Location from 'expo-location'; 5import * as TaskManager from 'expo-task-manager'; 6import { Platform } from 'react-native'; 7 8import * as TestUtils from '../TestUtils'; 9 10const BACKGROUND_LOCATION_TASK = 'background-location-updates'; 11const GEOFENCING_TASK = 'geofencing-task'; 12 13export const name = 'Location'; 14 15export async function test(t) { 16 const shouldSkipTestsRequiringPermissions = await TestUtils.shouldSkipTestsRequiringPermissionsAsync(); 17 const describeWithPermissions = shouldSkipTestsRequiringPermissions ? t.xdescribe : t.describe; 18 19 const testShapeOrUnauthorized = testFunction => async () => { 20 const providerStatus = await Location.getProviderStatusAsync(); 21 if (providerStatus.locationServicesEnabled) { 22 const { status } = await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 23 return Location.requestForegroundPermissionsAsync(); 24 }); 25 if (status === 'granted') { 26 const location = await testFunction(); 27 testLocationShape(location); 28 } else { 29 let error; 30 try { 31 await testFunction(); 32 } catch (e) { 33 error = e; 34 } 35 t.expect(error.message).toMatch(/Not authorized/); 36 } 37 } else { 38 let error; 39 try { 40 await testFunction(); 41 } catch (e) { 42 error = e; 43 } 44 t.expect(error.message).toMatch(/Location services are disabled/); 45 } 46 }; 47 48 function testLocationShape(location) { 49 t.expect(typeof location === 'object').toBe(true); 50 51 const { coords, timestamp } = location; 52 const { latitude, longitude, altitude, accuracy, altitudeAccuracy, heading, speed } = coords; 53 54 t.expect(typeof latitude === 'number').toBe(true); 55 t.expect(typeof longitude === 'number').toBe(true); 56 t.expect(typeof altitude === 'number' || altitude === null).toBe(true); 57 t.expect(typeof accuracy === 'number' || accuracy === null).toBe(true); 58 t.expect(typeof altitudeAccuracy === 'number' || altitudeAccuracy === null).toBe(true); 59 t.expect(typeof heading === 'number' || heading === null).toBe(true); 60 t.expect(typeof speed === 'number' || speed === null).toBe(true); 61 t.expect(typeof timestamp === 'number').toBe(true); 62 } 63 64 t.describe('Location', () => { 65 // On Android, foreground permission needs to be asked before the background permissions 66 describeWithPermissions('Location.requestForegroundPermissionsAsync()', () => { 67 t.it('requests foreground location permissions', async () => { 68 const permission = await Location.requestForegroundPermissionsAsync(); 69 t.expect(permission.granted).toBe(true); 70 t.expect(permission.status).toBe(Location.PermissionStatus.GRANTED); 71 }); 72 }); 73 74 describeWithPermissions('Location.getForegroundPermissionsAsync()', () => { 75 t.it('gets foreground location permissions', async () => { 76 const permission = await Location.getForegroundPermissionsAsync(); 77 t.expect(permission.granted).toBe(true); 78 t.expect(permission.status).toBe(Location.PermissionStatus.GRANTED); 79 }); 80 }); 81 82 describeWithPermissions('Location.requestBackgroundPermissionsAsync()', () => { 83 t.it('requests background location permissions', async () => { 84 const permission = await Location.requestBackgroundPermissionsAsync(); 85 t.expect(permission.granted).toBe(true); 86 t.expect(permission.status).toBe(Location.PermissionStatus.GRANTED); 87 }); 88 }); 89 90 describeWithPermissions('Location.getBackgroundPermissionsAsync()', () => { 91 t.it('gets background location permissions', async () => { 92 const permission = await Location.getBackgroundPermissionsAsync(); 93 t.expect(permission.granted).toBe(true); 94 t.expect(permission.status).toBe(Location.PermissionStatus.GRANTED); 95 }); 96 }); 97 98 t.describe('Location.hasServicesEnabledAsync()', () => { 99 t.it('checks if location services are enabled', async () => { 100 const result = await Location.hasServicesEnabledAsync(); 101 t.expect(result).toBe(true); 102 }); 103 }); 104 105 t.describe('Location.getProviderStatusAsync()', () => { 106 const timeout = 1000; 107 t.it( 108 'checks if location services are enabled', 109 async () => { 110 const result = await Location.getProviderStatusAsync(); 111 t.expect(result.locationServicesEnabled).not.toBe(undefined); 112 }, 113 timeout 114 ); 115 if (Platform.OS === 'android') { 116 t.it( 117 'detects when GPS sensor is enabled', 118 async () => { 119 const result = await Location.getProviderStatusAsync(); 120 t.expect(result.gpsAvailable).not.toBe(undefined); 121 }, 122 timeout 123 ); 124 t.it( 125 'detects when network location is enabled', 126 async () => { 127 const result = await Location.getProviderStatusAsync(); 128 t.expect(result.networkAvailable).not.toBe(undefined); 129 }, 130 timeout 131 ); 132 t.it( 133 'detects when passive location is enabled', 134 async () => { 135 const result = await Location.getProviderStatusAsync(); 136 t.expect(result.passiveAvailable).not.toBe(undefined); 137 }, 138 timeout 139 ); 140 } 141 }); 142 143 t.describe('Location.enableNetworkProviderAsync()', () => { 144 // To properly test this, you need to change device's location mode to "Device only" in system settings. 145 // In this mode, network provider is off. 146 147 t.it( 148 'asks user to enable network provider or just resolves on iOS', 149 async () => { 150 try { 151 await Location.enableNetworkProviderAsync(); 152 153 if (Platform.OS === 'android') { 154 const result = await Location.getProviderStatusAsync(); 155 t.expect(result.networkAvailable).toBe(true); 156 } 157 } catch (error) { 158 // User has denied the dialog. 159 t.expect(error.code).toBe('E_LOCATION_SETTINGS_UNSATISFIED'); 160 } 161 }, 162 20000 163 ); 164 }); 165 166 describeWithPermissions('Location.getCurrentPositionAsync()', () => { 167 // Manual interaction: 168 // 1. Just try 169 // 2. iOS Settings --> General --> Reset --> Reset Location & Privacy, 170 // try gain and "Allow" 171 // 3. Retry from experience restart. 172 // 4. Retry from app restart. 173 // 5. iOS Settings --> General --> Reset --> Reset Location & Privacy, 174 // try gain and "Don't Allow" 175 // 6. Retry from experience restart. 176 // 7. Retry from app restart. 177 const second = 1000; 178 const timeout = 20 * second; // Allow manual touch on permissions dialog 179 180 t.it( 181 'gets a result of the correct shape (without high accuracy), or ' + 182 'throws error if no permission or disabled', 183 testShapeOrUnauthorized(() => 184 Location.getCurrentPositionAsync({ 185 accuracy: Location.Accuracy.Balanced, 186 }) 187 ), 188 timeout 189 ); 190 t.it( 191 'gets a result of the correct shape (without high accuracy), or ' + 192 'throws error if no permission or disabled (when trying again immediately)', 193 testShapeOrUnauthorized(() => 194 Location.getCurrentPositionAsync({ 195 accuracy: Location.Accuracy.Balanced, 196 }) 197 ), 198 timeout 199 ); 200 t.it( 201 'gets a result of the correct shape (with high accuracy), or ' + 202 'throws error if no permission or disabled (when trying again immediately)', 203 testShapeOrUnauthorized(() => 204 Location.getCurrentPositionAsync({ 205 accuracy: Location.Accuracy.Highest, 206 }) 207 ), 208 timeout 209 ); 210 211 t.it( 212 'gets a result of the correct shape (without high accuracy), or ' + 213 'throws error if no permission or disabled (when trying again after 1 second)', 214 async () => { 215 await new Promise(resolve => setTimeout(resolve, 1000)); 216 await testShapeOrUnauthorized(() => 217 Location.getCurrentPositionAsync({ 218 accuracy: Location.Accuracy.Balanced, 219 }) 220 )(); 221 }, 222 timeout + second 223 ); 224 225 t.it( 226 'gets a result of the correct shape (with high accuracy), or ' + 227 'throws error if no permission or disabled (when trying again after 1 second)', 228 async () => { 229 await new Promise(resolve => setTimeout(resolve, 1000)); 230 await testShapeOrUnauthorized(() => 231 Location.getCurrentPositionAsync({ 232 accuracy: Location.Accuracy.Highest, 233 }) 234 )(); 235 }, 236 timeout + second 237 ); 238 239 t.it( 240 'resolves when called simultaneously', 241 async () => { 242 await Promise.all([ 243 Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Low }), 244 Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Lowest }), 245 Location.getCurrentPositionAsync(), 246 ]); 247 }, 248 timeout 249 ); 250 251 t.it('resolves when watchPositionAsync is running', async () => { 252 const subscriber = await Location.watchPositionAsync({}, () => {}); 253 await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Low }); 254 subscriber.remove(); 255 }); 256 }); 257 258 describeWithPermissions('Location.getLastKnownPositionAsync()', () => { 259 const second = 1000; 260 const timeout = 20 * second; // Allow manual touch on permissions dialog 261 262 t.it( 263 'gets a result of the correct shape, or throws error if no permission or disabled', 264 testShapeOrUnauthorized(() => Location.getLastKnownPositionAsync()), 265 timeout 266 ); 267 268 t.it( 269 'returns the same or newer location as previously ran `getCurrentPositionAsync`', 270 async () => { 271 const current = await Location.getCurrentPositionAsync({ 272 accuracy: Location.Accuracy.Lowest, 273 }); 274 const lastKnown = await Location.getLastKnownPositionAsync(); 275 276 t.expect(current).not.toBeNull(); 277 t.expect(lastKnown).not.toBeNull(); 278 t.expect(lastKnown.timestamp).toBeGreaterThanOrEqual(current.timestamp); 279 } 280 ); 281 282 t.it('returns null if maxAge is zero', async () => { 283 const location = await Location.getLastKnownPositionAsync({ maxAge: 0 }); 284 t.expect(location).toBeNull(); 285 }); 286 287 t.it('returns null if maxAge is negative', async () => { 288 const location = await Location.getLastKnownPositionAsync({ maxAge: -1000 }); 289 t.expect(location).toBeNull(); 290 }); 291 292 t.it("returns location that doesn't exceed maxAge or null", async () => { 293 const maxAge = 5000; 294 const timestampBeforeCall = Date.now(); 295 const location = await Location.getLastKnownPositionAsync({ maxAge }); 296 297 if (location !== null) { 298 t.expect(timestampBeforeCall - location.timestamp).toBeLessThan(maxAge); 299 } 300 }); 301 302 t.it('returns location with required accuracy or null', async () => { 303 const requiredAccuracy = 70; 304 const location = await Location.getLastKnownPositionAsync({ requiredAccuracy }); 305 306 if (location !== null) { 307 t.expect(location.coords.accuracy).toBeLessThanOrEqual(requiredAccuracy); 308 } 309 }); 310 311 t.it('returns null if required accuracy is zero', async () => { 312 const location = await Location.getLastKnownPositionAsync({ 313 requiredAccuracy: 0, 314 }); 315 t.expect(location).toBeNull(); 316 }); 317 318 t.it( 319 'resolves when called simultaneously', 320 async () => { 321 await Promise.all([ 322 Location.getLastKnownPositionAsync(), 323 Location.getLastKnownPositionAsync({ maxAge: 1000 }), 324 Location.getLastKnownPositionAsync({ requiredAccuracy: 100 }), 325 ]); 326 }, 327 timeout 328 ); 329 330 t.it('resolves when watchPositionAsync is running', async () => { 331 const subscriber = await Location.watchPositionAsync({}, () => {}); 332 await Location.getLastKnownPositionAsync(); 333 subscriber.remove(); 334 }); 335 }); 336 337 describeWithPermissions('Location.watchPositionAsync()', () => { 338 t.it('gets a result of the correct shape', async () => { 339 await new Promise(async (resolve, reject) => { 340 const subscriber = await Location.watchPositionAsync({}, location => { 341 testLocationShape(location); 342 subscriber.remove(); 343 resolve(); 344 }); 345 }); 346 }); 347 348 t.it('can be called simultaneously', async () => { 349 const spies = [1, 2, 3].map(number => t.jasmine.createSpy(`watchPosition${number}`)); 350 351 const subscribers = await Promise.all( 352 spies.map(spy => Location.watchPositionAsync({}, spy)) 353 ); 354 355 await new Promise(resolve => setTimeout(resolve, 3000)); 356 357 spies.forEach(spy => t.expect(spy).toHaveBeenCalled()); 358 subscribers.forEach(subscriber => subscriber.remove()); 359 }); 360 }); 361 362 if (Platform.OS !== 'web') { 363 describeWithPermissions('Location.getHeadingAsync()', () => { 364 const testCompass = options => async () => { 365 // Disable Compass Test if in simulator 366 if (Constants.isDevice) { 367 const { status } = await TestUtils.acceptPermissionsAndRunCommandAsync(() => { 368 return Location.requestForegroundPermissionsAsync(); 369 }); 370 if (status === 'granted') { 371 const heading = await Location.getHeadingAsync(); 372 t.expect(typeof heading.magHeading === 'number').toBe(true); 373 t.expect(typeof heading.trueHeading === 'number').toBe(true); 374 t.expect(typeof heading.accuracy === 'number').toBe(true); 375 } else { 376 let error; 377 try { 378 await Location.getHeadingAsync(); 379 } catch (e) { 380 error = e; 381 } 382 t.expect(error.message).toMatch(/Not authorized/); 383 } 384 } 385 }; 386 const second = 1000; 387 const timeout = 20 * second; // Allow manual touch on permissions dialog 388 389 t.it( 390 'Checks if compass is returning right values (trueHeading, magHeading, accuracy)', 391 testCompass(), 392 timeout 393 ); 394 }); 395 396 t.describe('Location.geocodeAsync()', () => { 397 const timeout = 2000; 398 399 t.it( 400 'geocodes a place of the right shape', 401 async () => { 402 const result = await Location.geocodeAsync('900 State St, Salem, OR'); 403 t.expect(Array.isArray(result)).toBe(true); 404 t.expect(typeof result[0]).toBe('object'); 405 const { latitude, longitude, accuracy, altitude } = result[0]; 406 t.expect(typeof latitude).toBe('number'); 407 t.expect(typeof longitude).toBe('number'); 408 t.expect(typeof accuracy).toBe('number'); 409 t.expect(typeof altitude).toBe('number'); 410 }, 411 timeout 412 ); 413 414 t.it( 415 'returns an empty array when the address is not found', 416 async () => { 417 const result = await Location.geocodeAsync(':('); 418 t.expect(result).toEqual([]); 419 }, 420 timeout 421 ); 422 }); 423 424 t.describe('Location.reverseGeocodeAsync()', () => { 425 const timeout = 2000; 426 427 t.it( 428 'gives a right shape address of a point location', 429 async () => { 430 const result = await Location.reverseGeocodeAsync({ 431 latitude: 60.166595, 432 longitude: 24.944865, 433 }); 434 t.expect(Array.isArray(result)).toBe(true); 435 t.expect(typeof result[0]).toBe('object'); 436 const fields = ['city', 'street', 'region', 'country', 'postalCode', 'name']; 437 fields.forEach(field => { 438 t.expect( 439 typeof result[field] === 'string' || typeof result[field] === 'undefined' 440 ).toBe(true); 441 }); 442 }, 443 timeout 444 ); 445 446 t.it("throws for a location where `latitude` and `longitude` aren't numbers", async () => { 447 let error; 448 try { 449 await Location.reverseGeocodeAsync({ 450 latitude: '60', 451 longitude: '24', 452 }); 453 } catch (e) { 454 error = e; 455 } 456 t.expect(error instanceof TypeError).toBe(true); 457 }); 458 }); 459 460 describeWithPermissions('Location - background location updates', () => { 461 async function expectTaskAccuracyToBe(accuracy) { 462 const locationTask = await TaskManager.getTaskOptionsAsync(BACKGROUND_LOCATION_TASK); 463 464 t.expect(locationTask).toBeDefined(); 465 t.expect(locationTask.accuracy).toBe(accuracy); 466 } 467 468 t.it('starts location updates', async () => { 469 await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 470 }); 471 472 t.it('has started location updates', async () => { 473 const started = await Location.hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 474 t.expect(started).toBe(true); 475 }); 476 477 t.it('defaults to balanced accuracy', async () => { 478 await expectTaskAccuracyToBe(Location.Accuracy.Balanced); 479 }); 480 481 t.it('can update existing task', async () => { 482 const newAccuracy = Location.Accuracy.Highest; 483 await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, { 484 accuracy: newAccuracy, 485 }); 486 expectTaskAccuracyToBe(newAccuracy); 487 }); 488 489 t.it('stops location updates', async () => { 490 await Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 491 }); 492 493 t.it('has stopped location updates', async () => { 494 const started = await Location.hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TASK); 495 t.expect(started).toBe(false); 496 }); 497 }); 498 499 describeWithPermissions('Location - geofencing', () => { 500 const regions = [ 501 { 502 identifier: 'Kraków, Poland', 503 radius: 8000, 504 latitude: 50.0468548, 505 longitude: 19.9348341, 506 notifyOnEntry: true, 507 notifyOnExit: true, 508 }, 509 { 510 identifier: 'Apple', 511 radius: 1000, 512 latitude: 37.3270145, 513 longitude: -122.0310273, 514 notifyOnEntry: true, 515 notifyOnExit: true, 516 }, 517 ]; 518 519 async function expectTaskRegionsToBeLike(regions) { 520 const geofencingTask = await TaskManager.getTaskOptionsAsync(GEOFENCING_TASK); 521 522 t.expect(geofencingTask).toBeDefined(); 523 t.expect(geofencingTask.regions).toBeDefined(); 524 t.expect(geofencingTask.regions.length).toBe(regions.length); 525 526 for (let i = 0; i < regions.length; i++) { 527 t.expect(geofencingTask.regions[i].identifier).toBe(regions[i].identifier); 528 t.expect(geofencingTask.regions[i].radius).toBe(regions[i].radius); 529 t.expect(geofencingTask.regions[i].latitude).toBe(regions[i].latitude); 530 t.expect(geofencingTask.regions[i].longitude).toBe(regions[i].longitude); 531 } 532 } 533 534 t.it('starts geofencing', async () => { 535 await Location.startGeofencingAsync(GEOFENCING_TASK, regions); 536 }); 537 538 t.it('has started geofencing', async () => { 539 const started = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); 540 t.expect(started).toBe(true); 541 }); 542 543 t.it('is monitoring correct regions', async () => { 544 expectTaskRegionsToBeLike(regions); 545 }); 546 547 t.it('can update geofencing regions', async () => { 548 const newRegions = regions.slice(1); 549 await Location.startGeofencingAsync(GEOFENCING_TASK, newRegions); 550 expectTaskRegionsToBeLike(newRegions); 551 }); 552 553 t.it('stops geofencing', async () => { 554 await Location.stopGeofencingAsync(GEOFENCING_TASK); 555 }); 556 557 t.it('has stopped geofencing', async () => { 558 const started = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); 559 t.expect(started).toBe(false); 560 }); 561 562 t.it('throws when starting geofencing with incorrect regions', async () => { 563 await (async () => { 564 let error; 565 try { 566 await Location.startGeofencingAsync(GEOFENCING_TASK, []); 567 } catch (e) { 568 error = e; 569 } 570 t.expect(error instanceof Error).toBe(true); 571 })(); 572 573 await (async () => { 574 let error; 575 try { 576 await Location.startGeofencingAsync(GEOFENCING_TASK, [{ longitude: 'not a number' }]); 577 } catch (e) { 578 error = e; 579 } 580 t.expect(error instanceof TypeError).toBe(true); 581 })(); 582 }); 583 }); 584 } 585 }); 586} 587 588// Define empty tasks, otherwise tasks might automatically unregister themselves if no task is defined. 589TaskManager.defineTask(BACKGROUND_LOCATION_TASK, () => {}); 590TaskManager.defineTask(GEOFENCING_TASK, () => {}); 591