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