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:
parent
2520d81784
commit
d2b97e251e
12 changed files with 287 additions and 60 deletions
|
@ -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]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue