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