@flighthq/geolocation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import type { GeolocationBackend, GeolocationErrorReason, GeolocationPermissionState, GeolocationRequestOptions, GeoPosition, GeoPositionResult } from '@flighthq/types';
2
+ export declare function clearGeolocationWatch(id: number): void;
3
+ export declare function createGeoPosition(): GeoPosition;
4
+ export declare function createWebGeolocationBackend(): GeolocationBackend;
5
+ export declare function getCurrentGeoPosition(options?: Readonly<GeolocationRequestOptions>): Promise<GeoPosition | null>;
6
+ export declare function getCurrentGeoPositionResult(options?: Readonly<GeolocationRequestOptions>): Promise<GeoPositionResult>;
7
+ export declare function getGeolocationBackend(): GeolocationBackend;
8
+ export declare function getGeolocationPermission(): Promise<GeolocationPermissionState>;
9
+ export declare function isGeolocationAvailable(): boolean;
10
+ export declare function onGeolocationPermissionChange(listener: (state: GeolocationPermissionState) => void): () => void;
11
+ export declare function requestGeolocationPermission(): Promise<boolean>;
12
+ export declare function setGeolocationBackend(backend: GeolocationBackend | null): void;
13
+ export declare function watchGeolocationPosition(handler: (position: Readonly<GeoPosition>) => void, options?: Readonly<GeolocationRequestOptions>, onError?: (reason: GeolocationErrorReason) => void): number;
14
+ //# sourceMappingURL=geolocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geolocation.d.ts","sourceRoot":"","sources":["../src/geolocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC1B,yBAAyB,EACzB,WAAW,EACX,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAGzB,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEtD;AAGD,wBAAgB,iBAAiB,IAAI,WAAW,CAY/C;AAKD,wBAAgB,2BAA2B,IAAI,kBAAkB,CA4GhE;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAEhH;AAID,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAErH;AAGD,wBAAgB,qBAAqB,IAAI,kBAAkB,CAG1D;AAKD,wBAAgB,wBAAwB,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAE9E;AAID,wBAAgB,sBAAsB,IAAI,OAAO,CAIhD;AAKD,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,GAAG,MAAM,IAAI,CAE/G;AAGD,wBAAgB,4BAA4B,IAAI,OAAO,CAAC,OAAO,CAAC,CAE/D;AAGD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,GAAG,IAAI,CAE9E;AAKD,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,WAAW,CAAC,KAAK,IAAI,EAClD,OAAO,CAAC,EAAE,QAAQ,CAAC,yBAAyB,CAAC,EAC7C,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,GACjD,MAAM,CAER"}
@@ -0,0 +1,219 @@
1
+ // Cancels an active position watch. No-op when the id is unknown or the backend lacks watching.
2
+ export function clearGeolocationWatch(id) {
3
+ getGeolocationBackend().clearWatch(id);
4
+ }
5
+ // Allocates a zeroed GeoPosition; use as a scratch value or when building a backend.
6
+ export function createGeoPosition() {
7
+ return {
8
+ accuracy: 0,
9
+ altitude: 0,
10
+ altitudeAccuracy: 0,
11
+ floorLevel: 0,
12
+ heading: 0,
13
+ latitude: 0,
14
+ longitude: 0,
15
+ speed: 0,
16
+ timestamp: 0,
17
+ };
18
+ }
19
+ // Builds the default web backend over navigator.geolocation. Position reads resolve to null and
20
+ // permission requests resolve to false when the API is absent (insecure context, jsdom) or the user
21
+ // denies access — location access is not guaranteed.
22
+ export function createWebGeolocationBackend() {
23
+ return {
24
+ clearWatch(id) {
25
+ const geo = getWebGeolocation();
26
+ if (geo === null || typeof geo.clearWatch !== 'function')
27
+ return;
28
+ try {
29
+ geo.clearWatch(id);
30
+ }
31
+ catch {
32
+ // Expected failure: the watch may already be gone or the host may deny access.
33
+ }
34
+ },
35
+ getCurrentPosition(options) {
36
+ return new Promise((resolve) => {
37
+ const geo = getWebGeolocation();
38
+ if (geo === null || typeof geo.getCurrentPosition !== 'function') {
39
+ resolve(null);
40
+ return;
41
+ }
42
+ try {
43
+ geo.getCurrentPosition((position) => resolve(mapWebPosition(position)), () => resolve(null), toPositionOptions(options));
44
+ }
45
+ catch {
46
+ resolve(null);
47
+ }
48
+ });
49
+ },
50
+ getCurrentPositionResult(options) {
51
+ return new Promise((resolve) => {
52
+ const geo = getWebGeolocation();
53
+ if (geo === null || typeof geo.getCurrentPosition !== 'function') {
54
+ resolve({ position: null, reason: 'unavailable' });
55
+ return;
56
+ }
57
+ try {
58
+ geo.getCurrentPosition((position) => resolve({ position: mapWebPosition(position), reason: null }), (error) => resolve({ position: null, reason: mapWebPositionError(error) }), toPositionOptions(options));
59
+ }
60
+ catch {
61
+ resolve({ position: null, reason: 'unavailable' });
62
+ }
63
+ });
64
+ },
65
+ async getPermission() {
66
+ const permissions = typeof navigator !== 'undefined' ? (navigator.permissions ?? null) : null;
67
+ if (permissions !== null && typeof permissions.query === 'function') {
68
+ try {
69
+ const status = await permissions.query({ name: 'geolocation' });
70
+ return status.state;
71
+ }
72
+ catch {
73
+ // Fall through to prompt default.
74
+ }
75
+ }
76
+ return 'prompt';
77
+ },
78
+ async requestPermission() {
79
+ const permissions = typeof navigator !== 'undefined' ? (navigator.permissions ?? null) : null;
80
+ if (permissions !== null && typeof permissions.query === 'function') {
81
+ try {
82
+ const status = await permissions.query({ name: 'geolocation' });
83
+ return status.state === 'granted';
84
+ }
85
+ catch {
86
+ // Fall through to a probe below.
87
+ }
88
+ }
89
+ return (await this.getCurrentPosition({})) !== null;
90
+ },
91
+ subscribePermission(listener) {
92
+ const permissions = typeof navigator !== 'undefined' ? (navigator.permissions ?? null) : null;
93
+ if (permissions === null || typeof permissions.query !== 'function')
94
+ return _noopUnsubscribe;
95
+ let status = null;
96
+ let handler = null;
97
+ permissions
98
+ .query({ name: 'geolocation' })
99
+ .then((s) => {
100
+ status = s;
101
+ handler = () => listener(s.state);
102
+ s.addEventListener('change', handler);
103
+ })
104
+ .catch(() => {
105
+ // Permissions API unavailable; subscription is a no-op.
106
+ });
107
+ return () => {
108
+ if (status !== null && handler !== null) {
109
+ status.removeEventListener('change', handler);
110
+ status = null;
111
+ handler = null;
112
+ }
113
+ };
114
+ },
115
+ watchPosition(listener, options, onError) {
116
+ const geo = getWebGeolocation();
117
+ if (geo === null || typeof geo.watchPosition !== 'function')
118
+ return -1;
119
+ try {
120
+ return geo.watchPosition((position) => listener(mapWebPosition(position)), onError !== undefined ? (error) => onError(mapWebPositionError(error)) : () => { }, toPositionOptions(options));
121
+ }
122
+ catch {
123
+ return -1;
124
+ }
125
+ },
126
+ };
127
+ }
128
+ // Resolves the device's current position, or null when access is denied or unavailable.
129
+ export function getCurrentGeoPosition(options) {
130
+ return getGeolocationBackend().getCurrentPosition(options ?? _emptyOptions);
131
+ }
132
+ // Resolves a GeoPositionResult carrying both the position and the error reason on failure.
133
+ // Use when the caller needs to distinguish denied / unavailable / timeout rather than just null.
134
+ export function getCurrentGeoPositionResult(options) {
135
+ return getGeolocationBackend().getCurrentPositionResult(options ?? _emptyOptions);
136
+ }
137
+ // The active geolocation backend, or a lazily-created web default. There is always a backend.
138
+ export function getGeolocationBackend() {
139
+ if (_backend === null)
140
+ _backend = createWebGeolocationBackend();
141
+ return _backend;
142
+ }
143
+ // Resolves the current permission state without triggering a user prompt.
144
+ // Returns 'granted', 'denied', or 'prompt' (the user has not yet been asked).
145
+ // Falls back to 'prompt' when the Permissions API is absent.
146
+ export function getGeolocationPermission() {
147
+ return getGeolocationBackend().getPermission();
148
+ }
149
+ // Returns true when the geolocation capability is available in the current context. Synchronous;
150
+ // does not trigger a permission prompt. False on insecure context, jsdom, or missing navigator.
151
+ export function isGeolocationAvailable() {
152
+ if (typeof navigator === 'undefined')
153
+ return false;
154
+ if (typeof window !== 'undefined' && window.isSecureContext === false)
155
+ return false;
156
+ return typeof navigator.geolocation !== 'undefined' && navigator.geolocation !== null;
157
+ }
158
+ // Subscribes to geolocation permission state changes. Invokes listener whenever the OS changes the
159
+ // permission (e.g., the user revokes access in Settings mid-session). Returns an unsubscribe
160
+ // function. No-op subscription when the Permissions API is absent.
161
+ export function onGeolocationPermissionChange(listener) {
162
+ return getGeolocationBackend().subscribePermission(listener);
163
+ }
164
+ // Requests location permission. Resolves true when granted, false when denied or unavailable.
165
+ export function requestGeolocationPermission() {
166
+ return getGeolocationBackend().requestPermission();
167
+ }
168
+ // Installs a native host geolocation backend; pass null to fall back to the web default.
169
+ export function setGeolocationBackend(backend) {
170
+ _backend = backend;
171
+ }
172
+ // Starts a position watch, invoking handler on each update. Returns the watch id, or -1 when
173
+ // watching is unavailable. Pair with clearGeolocationWatch.
174
+ // Pass onError to receive ongoing failure reasons (e.g., permission revoked mid-watch).
175
+ export function watchGeolocationPosition(handler, options, onError) {
176
+ return getGeolocationBackend().watchPosition(handler, options ?? _emptyOptions, onError);
177
+ }
178
+ let _backend = null;
179
+ const _emptyOptions = {};
180
+ const _noopUnsubscribe = () => { };
181
+ function getWebGeolocation() {
182
+ if (typeof navigator === 'undefined')
183
+ return null;
184
+ return navigator.geolocation ?? null;
185
+ }
186
+ function mapWebPosition(position) {
187
+ const coords = position.coords;
188
+ return {
189
+ accuracy: coords.accuracy,
190
+ altitude: coords.altitude ?? 0,
191
+ altitudeAccuracy: coords.altitudeAccuracy ?? 0,
192
+ // floorLevel is non-standard: absent from the W3C GeolocationCoordinates type, but some hosts
193
+ // (indoor-positioning platforms) populate it. Read it when present rather than forcing 0.
194
+ floorLevel: coords.floorLevel ?? 0,
195
+ heading: coords.heading ?? 0,
196
+ latitude: coords.latitude,
197
+ longitude: coords.longitude,
198
+ speed: coords.speed ?? 0,
199
+ timestamp: position.timestamp,
200
+ };
201
+ }
202
+ function mapWebPositionError(error) {
203
+ switch (error.code) {
204
+ case 1:
205
+ return 'denied';
206
+ case 3:
207
+ return 'timeout';
208
+ default:
209
+ return 'unavailable';
210
+ }
211
+ }
212
+ function toPositionOptions(options) {
213
+ return {
214
+ enableHighAccuracy: options.enableHighAccuracy ?? false,
215
+ maximumAge: options.maximumAgeMs,
216
+ timeout: options.timeoutMs,
217
+ };
218
+ }
219
+ //# sourceMappingURL=geolocation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geolocation.js","sourceRoot":"","sources":["../src/geolocation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './geolocation';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './geolocation';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/geolocation",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Device geolocation (current position, watch, permission) over a swappable web/native backend",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,283 @@
1
+ import type {
2
+ GeolocationBackend,
3
+ GeolocationErrorReason,
4
+ GeolocationPermissionState,
5
+ GeoPosition,
6
+ } from '@flighthq/types';
7
+
8
+ import {
9
+ clearGeolocationWatch,
10
+ createGeoPosition,
11
+ createWebGeolocationBackend,
12
+ getCurrentGeoPosition,
13
+ getCurrentGeoPositionResult,
14
+ getGeolocationBackend,
15
+ getGeolocationPermission,
16
+ isGeolocationAvailable,
17
+ onGeolocationPermissionChange,
18
+ requestGeolocationPermission,
19
+ setGeolocationBackend,
20
+ watchGeolocationPosition,
21
+ } from './geolocation';
22
+
23
+ function fakeBackend(): GeolocationBackend & { cleared: number[]; lastWatch: number } {
24
+ return {
25
+ cleared: [],
26
+ lastWatch: 0,
27
+ clearWatch(id) {
28
+ this.cleared.push(id);
29
+ },
30
+ async getCurrentPosition() {
31
+ const position = createGeoPosition();
32
+ position.latitude = 1;
33
+ position.longitude = 2;
34
+ return position;
35
+ },
36
+ async getCurrentPositionResult() {
37
+ const position = createGeoPosition();
38
+ position.latitude = 1;
39
+ position.longitude = 2;
40
+ return { position, reason: null };
41
+ },
42
+ async getPermission(): Promise<GeolocationPermissionState> {
43
+ return 'granted';
44
+ },
45
+ async requestPermission() {
46
+ return true;
47
+ },
48
+ subscribePermission(_listener: (state: GeolocationPermissionState) => void) {
49
+ return () => {};
50
+ },
51
+ watchPosition(listener, _options, onError) {
52
+ const position = createGeoPosition();
53
+ position.latitude = 3;
54
+ listener(position);
55
+ if (onError) onError('denied');
56
+ return ++this.lastWatch;
57
+ },
58
+ };
59
+ }
60
+
61
+ afterEach(() => setGeolocationBackend(null));
62
+
63
+ describe('clearGeolocationWatch', () => {
64
+ it('does not throw on the web backend in jsdom', () => {
65
+ expect(() => clearGeolocationWatch(0)).not.toThrow();
66
+ });
67
+
68
+ it('forwards the id to the active backend', () => {
69
+ const backend = fakeBackend();
70
+ setGeolocationBackend(backend);
71
+ clearGeolocationWatch(7);
72
+ expect(backend.cleared).toEqual([7]);
73
+ });
74
+ });
75
+
76
+ describe('createGeoPosition', () => {
77
+ it('allocates a zeroed position', () => {
78
+ expect(createGeoPosition()).toEqual({
79
+ accuracy: 0,
80
+ altitude: 0,
81
+ altitudeAccuracy: 0,
82
+ floorLevel: 0,
83
+ heading: 0,
84
+ latitude: 0,
85
+ longitude: 0,
86
+ speed: 0,
87
+ timestamp: 0,
88
+ });
89
+ });
90
+ });
91
+
92
+ describe('createWebGeolocationBackend', () => {
93
+ it('resolves null and does not throw when geolocation is absent', async () => {
94
+ const backend = createWebGeolocationBackend();
95
+ expect(await backend.getCurrentPosition({})).toBeNull();
96
+ expect(typeof backend.watchPosition(() => {}, {})).toBe('number');
97
+ expect(() => backend.clearWatch(-1)).not.toThrow();
98
+ expect(typeof (await backend.requestPermission())).toBe('boolean');
99
+ });
100
+
101
+ it('getPermission returns a GeolocationPermissionState string', async () => {
102
+ const backend = createWebGeolocationBackend();
103
+ const state = await backend.getPermission();
104
+ expect(['granted', 'denied', 'prompt']).toContain(state);
105
+ });
106
+
107
+ it('getCurrentPositionResult returns unavailable reason when geolocation is absent', async () => {
108
+ const backend = createWebGeolocationBackend();
109
+ const result = await backend.getCurrentPositionResult({});
110
+ expect(result.position).toBeNull();
111
+ expect(result.reason).toBe('unavailable');
112
+ });
113
+
114
+ it('subscribePermission returns an unsubscribe function', () => {
115
+ const backend = createWebGeolocationBackend();
116
+ const unsubscribe = backend.subscribePermission(() => {});
117
+ expect(typeof unsubscribe).toBe('function');
118
+ expect(() => unsubscribe()).not.toThrow();
119
+ });
120
+
121
+ it('reads a host-provided floorLevel from coords', async () => {
122
+ const original = Object.getOwnPropertyDescriptor(navigator, 'geolocation');
123
+ Object.defineProperty(navigator, 'geolocation', {
124
+ configurable: true,
125
+ value: {
126
+ getCurrentPosition(success: (position: unknown) => void) {
127
+ success({
128
+ coords: {
129
+ accuracy: 5,
130
+ altitude: null,
131
+ altitudeAccuracy: null,
132
+ floorLevel: 3,
133
+ heading: null,
134
+ latitude: 1,
135
+ longitude: 2,
136
+ speed: null,
137
+ },
138
+ timestamp: 123,
139
+ });
140
+ },
141
+ },
142
+ });
143
+ try {
144
+ const backend = createWebGeolocationBackend();
145
+ const position = await backend.getCurrentPosition({});
146
+ expect(position?.floorLevel).toBe(3);
147
+ } finally {
148
+ if (original !== undefined) Object.defineProperty(navigator, 'geolocation', original);
149
+ else delete (navigator as { geolocation?: unknown }).geolocation;
150
+ }
151
+ });
152
+ });
153
+
154
+ describe('getCurrentGeoPosition', () => {
155
+ it('returns the backend position', async () => {
156
+ setGeolocationBackend(fakeBackend());
157
+ const position = (await getCurrentGeoPosition()) as GeoPosition;
158
+ expect(position.latitude).toBe(1);
159
+ expect(position.longitude).toBe(2);
160
+ });
161
+ });
162
+
163
+ describe('getCurrentGeoPositionResult', () => {
164
+ it('returns position and null reason on success', async () => {
165
+ setGeolocationBackend(fakeBackend());
166
+ const result = await getCurrentGeoPositionResult();
167
+ expect(result.position).not.toBeNull();
168
+ expect(result.position!.latitude).toBe(1);
169
+ expect(result.reason).toBeNull();
170
+ });
171
+
172
+ it('returns null position with a reason on failure (web backend in jsdom)', async () => {
173
+ const result = await getCurrentGeoPositionResult();
174
+ expect(result.position).toBeNull();
175
+ expect(result.reason).toBe('unavailable');
176
+ });
177
+ });
178
+
179
+ describe('getGeolocationBackend', () => {
180
+ it('falls back to a web backend', () => {
181
+ expect(getGeolocationBackend()).not.toBeNull();
182
+ });
183
+
184
+ it('returns the registered backend', () => {
185
+ const backend = fakeBackend();
186
+ setGeolocationBackend(backend);
187
+ expect(getGeolocationBackend()).toBe(backend);
188
+ });
189
+ });
190
+
191
+ describe('getGeolocationPermission', () => {
192
+ it('reflects the backend permission state', async () => {
193
+ setGeolocationBackend(fakeBackend());
194
+ expect(await getGeolocationPermission()).toBe('granted');
195
+ });
196
+
197
+ it('returns a GeolocationPermissionState string from the web backend', async () => {
198
+ const state = await getGeolocationPermission();
199
+ expect(['granted', 'denied', 'prompt']).toContain(state);
200
+ });
201
+ });
202
+
203
+ describe('isGeolocationAvailable', () => {
204
+ it('returns a boolean', () => {
205
+ expect(typeof isGeolocationAvailable()).toBe('boolean');
206
+ });
207
+
208
+ it('returns false in jsdom (no secure context / no navigator.geolocation)', () => {
209
+ // jsdom does not provide navigator.geolocation, so this is expected to be false.
210
+ expect(isGeolocationAvailable()).toBe(false);
211
+ });
212
+ });
213
+
214
+ describe('onGeolocationPermissionChange', () => {
215
+ it('returns an unsubscribe function', () => {
216
+ const unsubscribe = onGeolocationPermissionChange(() => {});
217
+ expect(typeof unsubscribe).toBe('function');
218
+ expect(() => unsubscribe()).not.toThrow();
219
+ });
220
+
221
+ it('uses the backend subscribePermission', () => {
222
+ let subscribed = false;
223
+ let unsubscribed = false;
224
+ const backend = fakeBackend();
225
+ backend.subscribePermission = (_listener) => {
226
+ subscribed = true;
227
+ return () => {
228
+ unsubscribed = true;
229
+ };
230
+ };
231
+ setGeolocationBackend(backend);
232
+ const unsub = onGeolocationPermissionChange(() => {});
233
+ expect(subscribed).toBe(true);
234
+ unsub();
235
+ expect(unsubscribed).toBe(true);
236
+ });
237
+ });
238
+
239
+ describe('requestGeolocationPermission', () => {
240
+ it('reflects the backend result', async () => {
241
+ setGeolocationBackend(fakeBackend());
242
+ expect(await requestGeolocationPermission()).toBe(true);
243
+ });
244
+
245
+ it('returns a boolean from the web backend without throwing', async () => {
246
+ expect(typeof (await requestGeolocationPermission())).toBe('boolean');
247
+ });
248
+ });
249
+
250
+ describe('setGeolocationBackend', () => {
251
+ it('clears back to the web fallback when passed null', () => {
252
+ setGeolocationBackend(fakeBackend());
253
+ setGeolocationBackend(null);
254
+ expect(getGeolocationBackend()).not.toBeNull();
255
+ });
256
+ });
257
+
258
+ describe('watchGeolocationPosition', () => {
259
+ it('delivers positions and returns a watch id', () => {
260
+ setGeolocationBackend(fakeBackend());
261
+ let seen = 0;
262
+ const id = watchGeolocationPosition((position) => {
263
+ seen = position.latitude;
264
+ });
265
+ expect(id).toBe(1);
266
+ expect(seen).toBe(3);
267
+ });
268
+
269
+ it('delivers error reasons when onError is provided', () => {
270
+ setGeolocationBackend(fakeBackend());
271
+ const errors: GeolocationErrorReason[] = [];
272
+ watchGeolocationPosition(
273
+ () => {},
274
+ {},
275
+ (reason) => errors.push(reason),
276
+ );
277
+ expect(errors).toEqual(['denied']);
278
+ });
279
+
280
+ it('returns -1 from the web backend when watching is unavailable', () => {
281
+ expect(watchGeolocationPosition(() => {})).toBe(-1);
282
+ });
283
+ });