Live location sharing - handle geolocation errors (#8179)

* display live share warning only when geolocation is happening

Signed-off-by: Kerry Archibald <kerrya@element.io>

* kill beacons when geolocation is unavailable or permissions denied

Signed-off-by: Kerry Archibald <kerrya@element.io>

* polish and comments

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-03-28 18:46:39 +02:00 committed by GitHub
parent 2520d81784
commit d2b97e251e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 287 additions and 60 deletions

View file

@ -17,6 +17,7 @@ limitations under the License.
import { Room, Beacon, BeaconEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { logger } from "matrix-js-sdk/src/logger";
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore";
import {
@ -160,6 +161,7 @@ describe('OwnBeaconStore', () => {
mockClient.sendEvent.mockClear().mockResolvedValue({ event_id: '1' });
jest.spyOn(global.Date, 'now').mockReturnValue(now);
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
jest.spyOn(logger, 'error').mockRestore();
});
afterEach(async () => {
@ -600,32 +602,112 @@ describe('OwnBeaconStore', () => {
// stop watching location
expect(geolocation.clearWatch).toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(false);
});
it('starts watching position when user starts having live beacons', async () => {
makeRoomsWithStateEvents([]);
await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
describe('when store is initialised with live beacons', () => {
it('starts watching position', async () => {
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(geolocation.watchPosition).toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(true);
});
expect(geolocation.watchPosition).toHaveBeenCalled();
it('kills live beacon when geolocation is unavailable', async () => {
const errorLogSpy = jest.spyOn(logger, 'error').mockImplementation(() => { });
// remove the mock we set
// @ts-ignore
navigator.geolocation = undefined;
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(store.isMonitoringLiveLocation).toEqual(false);
expect(errorLogSpy).toHaveBeenCalledWith('Geolocation failed', "Unavailable");
});
it('kills live beacon when geolocation permissions are not granted', async () => {
// similar case to the test above
// but these errors are handled differently
// above is thrown by element, this passed to error callback by geolocation
// return only a permission denied error
geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
[0], [1]),
);
const errorLogSpy = jest.spyOn(logger, 'error').mockImplementation(() => { });
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(store.isMonitoringLiveLocation).toEqual(false);
expect(errorLogSpy).toHaveBeenCalledWith('Geolocation failed', "PermissionDenied");
});
});
it('publishes position for new beacon immediately', async () => {
makeRoomsWithStateEvents([]);
await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
describe('adding a new beacon', () => {
it('publishes position for new beacon immediately', async () => {
makeRoomsWithStateEvents([]);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
// wait for store to settle
await flushPromisesWithFakeTimers();
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(mockClient.sendEvent).toHaveBeenCalled();
expect(mockClient.sendEvent).toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(true);
});
it('kills live beacons when geolocation is unavailable', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => { });
// @ts-ignore
navigator.geolocation = undefined;
makeRoomsWithStateEvents([]);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
// wait for store to settle
await flushPromisesWithFakeTimers();
// stop beacon
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(false);
});
it('publishes position for new beacon immediately when there were already live beacons', async () => {
makeRoomsWithStateEvents([alicesRoom2BeaconInfo]);
await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(mockClient.sendEvent).toHaveBeenCalledTimes(1);
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
// wait for store to settle
await flushPromisesWithFakeTimers();
expect(geolocation.getCurrentPosition).toHaveBeenCalled();
// once for original event,
// then both live beacons get current position published
// after new beacon is added
expect(mockClient.sendEvent).toHaveBeenCalledTimes(3);
});
});
it('publishes subsequent positions', async () => {
@ -650,6 +732,57 @@ describe('OwnBeaconStore', () => {
expect(mockClient.sendEvent).toHaveBeenCalledTimes(3);
});
it('stops live beacons when geolocation permissions are revoked', async () => {
jest.spyOn(logger, 'error').mockImplementation(() => { });
// return two good positions, then a permission denied error
geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
[0, 1000, 3000], [0, 0, 1]),
);
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
expect(mockClient.sendEvent).toHaveBeenCalledTimes(0);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
jest.advanceTimersByTime(5000);
// first two events were sent successfully
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
// stop beacon
expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(false);
});
it('keeps sharing positions when geolocation has a non fatal error', async () => {
const errorLogSpy = jest.spyOn(logger, 'error').mockImplementation(() => { });
// return good position, timeout error, good position
geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
[0, 1000, 3000], [0, 3, 0]),
);
makeRoomsWithStateEvents([
alicesRoom1BeaconInfo,
]);
expect(mockClient.sendEvent).toHaveBeenCalledTimes(0);
const store = await makeOwnBeaconStore();
// wait for store to settle
await flushPromisesWithFakeTimers();
jest.advanceTimersByTime(5000);
// two good locations were sent
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
// still sharing
expect(mockClient.unstable_setLiveBeacon).not.toHaveBeenCalled();
expect(store.isMonitoringLiveLocation).toEqual(true);
expect(errorLogSpy).toHaveBeenCalledWith('Geolocation failed', 'error message');
});
it('publishes last known position after 30s of inactivity', async () => {
geolocation.watchPosition.mockImplementation(
watchPositionMockImplementation([0]),