xref: /expo/apps/test-suite/tests/Location.js (revision bb8f4f99)
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