geo.getCurrentPosition and some testing helpers (#8150)

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-25 12:30:50 +01:00 committed by GitHub
parent f229ad6407
commit 0d513b3a2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 4 deletions

View file

@ -33,7 +33,7 @@ export enum GeolocationError {
const GeolocationOptions = {
timeout: 5000,
maximumAge: 1000,
maximumAge: 2000,
};
const isGeolocationPositionError = (error: unknown): error is GeolocationPositionError =>
@ -112,13 +112,31 @@ export const mapGeolocationPositionToTimedGeo = (position: GeolocationPosition):
return { timestamp: position.timestamp, geoUri: getGeoUri(genericPositionFromGeolocation(position)) };
};
/**
* Gets current position, returns a promise
* @returns Promise<GeolocationPosition>
*/
export const getCurrentPosition = async (): Promise<GeolocationPosition> => {
try {
const position = await new Promise((resolve: PositionCallback, reject) => {
getGeolocation().getCurrentPosition(resolve, reject, GeolocationOptions);
});
return position;
} catch (error) {
throw new Error(mapGeolocationError(error));
}
};
export type ClearWatchCallback = () => void;
export const watchPosition = (
onWatchPosition: PositionCallback,
onWatchPositionError: (error: GeolocationError) => void): () => void => {
onWatchPositionError: (error: GeolocationError) => void): ClearWatchCallback => {
try {
const onError = (error) => onWatchPositionError(mapGeolocationError(error));
const watchId = getGeolocation().watchPosition(onWatchPosition, onError, GeolocationOptions);
const clearWatch = () => getGeolocation().clearWatch(watchId);
const clearWatch = () => {
getGeolocation().clearWatch(watchId);
};
return clearWatch;
} catch (error) {
throw new Error(mapGeolocationError(error));

View file

@ -141,3 +141,27 @@ export const mockGeolocation = (): MockedObject<Geolocation> => {
return mockGeolocation;
};
/**
* Creates a mock watchPosition implementation
* that calls success callback at the provided delays
* ```
* geolocation.watchPosition.mockImplementation([0, 1000, 5000, 50])
* ```
* will call the provided handler with a mock position at
* next tick, 1000ms, 6000ms, 6050ms
*/
export const watchPositionMockImplementation = (delays: number[]) => {
return (callback: PositionCallback) => {
const position = makeGeolocationPosition({});
let totalDelay = 0;
delays.map(delayMs => {
totalDelay += delayMs;
const timeout = setTimeout(() => {
callback({ ...position, timestamp: position.timestamp + totalDelay });
}, totalDelay);
return timeout;
});
};
};

View file

@ -31,6 +31,16 @@ export const findByTagAndTestId = findByTagAndAttr('data-test-id');
export const flushPromises = async () => await new Promise(resolve => setTimeout(resolve));
// with jest's modern fake timers process.nextTick is also mocked,
// flushing promises in the normal way then waits for some advancement
// of the fake timers
// https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4?permalink_comment_id=4018174#gistcomment-4018174
export const flushPromisesWithFakeTimers = async (): Promise<void> => {
const promise = new Promise(resolve => process.nextTick(resolve));
jest.advanceTimersByTime(1);
await promise;
};
/**
* Call fn before calling componentDidUpdate on a react component instance, inst.
* @param {React.Component} inst an instance of a React component.
@ -57,3 +67,13 @@ export function waitForUpdate(inst: React.Component, updates = 1): Promise<void>
};
});
}
/**
* Advance jests fake timers and Date.now mock by ms
* Useful for testing code using timeouts or intervals
* that also checks timestamps
*/
export const advanceDateAndTime = (ms: number) => {
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms);
jest.advanceTimersByTime(ms);
};

View file

@ -23,6 +23,7 @@ import {
mapGeolocationPositionToTimedGeo,
watchPosition,
} from "../../../src/utils/beacon";
import { getCurrentPosition } from "../../../src/utils/beacon/geolocation";
import { makeGeolocationPosition, mockGeolocation } from "../../test-utils/beacon";
describe('geolocation utilities', () => {
@ -166,7 +167,7 @@ describe('geolocation utilities', () => {
const [, , options] = geolocation.watchPosition.mock.calls[0];
expect(options).toEqual({
maximumAge: 1000,
maximumAge: 2000,
timeout: 5000,
});
});
@ -204,4 +205,36 @@ describe('geolocation utilities', () => {
expect(errorHandler).toHaveBeenCalledWith(GeolocationError.PermissionDenied);
});
});
describe('getCurrentPosition()', () => {
it('throws with unavailable error when geolocation is not available', async () => {
// suppress expected errors from test log
jest.spyOn(logger, 'error').mockImplementation(() => { });
// remove the mock we added
// @ts-ignore illegal assignment to readonly property
navigator.geolocation = undefined;
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Unavailable);
});
it('throws with geolocation error when geolocation.getCurrentPosition fails', async () => {
// suppress expected errors from test log
jest.spyOn(logger, 'error').mockImplementation(() => { });
const timeoutError = getMockGeolocationPositionError(3, 'message');
geolocation.getCurrentPosition.mockImplementation((callback, error) => error(timeoutError));
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Timeout);
});
it('resolves with current location', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => { });
geolocation.getCurrentPosition.mockImplementation((callback, error) => callback(defaultPosition));
const result = await getCurrentPosition();
expect(result).toEqual(defaultPosition);
});
});
});