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
|
@ -27,13 +27,13 @@ interface Props {
|
|||
}
|
||||
|
||||
const LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
||||
const hasLiveBeacons = useEventEmitterState(
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
() => OwnBeaconStore.instance.hasLiveBeacons(),
|
||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
if (!hasLiveBeacons) {
|
||||
if (!isMonitoringLiveLocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -77,6 +77,13 @@ type LiveBeaconsState = {
|
|||
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
||||
const [stoppingInProgress, setStoppingInProgress] = useState(false);
|
||||
|
||||
// do we have an active geolocation.watchPosition
|
||||
const isMonitoringLiveLocation = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||
);
|
||||
|
||||
const liveBeaconIds = useEventEmitterState(
|
||||
OwnBeaconStore.instance,
|
||||
OwnBeaconStoreEvent.LivenessChange,
|
||||
|
@ -88,7 +95,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
|||
setStoppingInProgress(false);
|
||||
}, [liveBeaconIds]);
|
||||
|
||||
if (!liveBeaconIds?.length) {
|
||||
if (!isMonitoringLiveLocation || !liveBeaconIds?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconIn
|
|||
|
||||
export enum OwnBeaconStoreEvent {
|
||||
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
||||
MonitoringLivePosition = 'OwnBeaconStore.MonitoringLivePosition',
|
||||
}
|
||||
|
||||
const MOVING_UPDATE_INTERVAL = 2000;
|
||||
|
@ -232,18 +233,28 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
||||
};
|
||||
|
||||
private togglePollingLocation = async (): Promise<void> => {
|
||||
private togglePollingLocation = () => {
|
||||
if (!!this.liveBeaconIds.length) {
|
||||
return this.startPollingLocation();
|
||||
this.startPollingLocation();
|
||||
} else {
|
||||
this.stopPollingLocation();
|
||||
}
|
||||
return this.stopPollingLocation();
|
||||
};
|
||||
|
||||
private startPollingLocation = async () => {
|
||||
// clear any existing interval
|
||||
this.stopPollingLocation();
|
||||
|
||||
this.clearPositionWatch = await watchPosition(this.onWatchedPosition, this.onWatchedPositionError);
|
||||
try {
|
||||
this.clearPositionWatch = await watchPosition(
|
||||
this.onWatchedPosition,
|
||||
this.onGeolocationError,
|
||||
);
|
||||
} catch (error) {
|
||||
this.onGeolocationError(error?.message);
|
||||
// don't set locationInterval if geolocation failed to setup
|
||||
return;
|
||||
}
|
||||
|
||||
this.locationInterval = setInterval(() => {
|
||||
if (!this.lastPublishedPositionTimestamp) {
|
||||
|
@ -255,6 +266,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
this.publishCurrentLocationToBeacons();
|
||||
}
|
||||
}, STATIC_UPDATE_INTERVAL);
|
||||
|
||||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
};
|
||||
|
||||
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||
|
@ -268,11 +281,6 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onWatchedPositionError = (error: GeolocationError) => {
|
||||
this.geolocationError = error;
|
||||
logger.error(this.geolocationError);
|
||||
};
|
||||
|
||||
private stopPollingLocation = () => {
|
||||
clearInterval(this.locationInterval);
|
||||
this.locationInterval = undefined;
|
||||
|
@ -283,6 +291,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
this.clearPositionWatch();
|
||||
this.clearPositionWatch = undefined;
|
||||
}
|
||||
|
||||
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -313,8 +323,31 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
|||
* and publishes it to all live beacons
|
||||
*/
|
||||
private publishCurrentLocationToBeacons = async () => {
|
||||
const position = await getCurrentPosition();
|
||||
// TODO error handling
|
||||
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
||||
try {
|
||||
const position = await getCurrentPosition();
|
||||
// TODO error handling
|
||||
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
||||
} catch (error) {
|
||||
this.onGeolocationError(error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
private onGeolocationError = async (error: GeolocationError): Promise<void> => {
|
||||
this.geolocationError = error;
|
||||
logger.error('Geolocation failed', this.geolocationError);
|
||||
|
||||
// other errors are considered non-fatal
|
||||
// and self recovering
|
||||
if (![
|
||||
GeolocationError.Unavailable,
|
||||
GeolocationError.PermissionDenied,
|
||||
].includes(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopPollingLocation();
|
||||
// kill live beacons when location permissions are revoked
|
||||
// TODO may need adjustment when PSF-797 is done
|
||||
await Promise.all(this.liveBeaconIds.map(this.stopBeacon));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,9 +49,9 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
expect(component.html()).toBe(null);
|
||||
});
|
||||
|
||||
describe('when user has live beacons', () => {
|
||||
describe('when user has live location monitor', () => {
|
||||
beforeEach(() => {
|
||||
mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(true);
|
||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
|
||||
});
|
||||
it('renders correctly when not minimized', () => {
|
||||
const component = getComponent();
|
||||
|
@ -68,8 +68,8 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
|||
// started out rendered
|
||||
expect(component.html()).toBeTruthy();
|
||||
|
||||
mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(false);
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LivenessChange, false);
|
||||
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
|
||||
await flushPromises();
|
||||
component.setProps({});
|
||||
|
|
|
@ -18,10 +18,11 @@ import React from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { mount } from 'enzyme';
|
||||
import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
|
||||
import '../../../skinned-sdk';
|
||||
import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
|
||||
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnBeaconStore';
|
||||
import {
|
||||
advanceDateAndTime,
|
||||
findByTestId,
|
||||
|
@ -33,7 +34,6 @@ import {
|
|||
} from '../../../test-utils';
|
||||
|
||||
jest.useFakeTimers();
|
||||
mockGeolocation();
|
||||
describe('<RoomLiveShareWarning />', () => {
|
||||
const aliceId = '@alice:server.org';
|
||||
const room1Id = '$room1:server.org';
|
||||
|
@ -94,6 +94,7 @@ describe('<RoomLiveShareWarning />', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockGeolocation();
|
||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||
mockClient.unstable_setLiveBeacon.mockClear();
|
||||
});
|
||||
|
@ -123,7 +124,22 @@ describe('<RoomLiveShareWarning />', () => {
|
|||
expect(component.html()).toBe(null);
|
||||
});
|
||||
|
||||
describe('when user has live beacons', () => {
|
||||
it('does not render when geolocation is not working', async () => {
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
// @ts-ignore
|
||||
navigator.geolocation = undefined;
|
||||
await act(async () => {
|
||||
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
|
||||
await makeOwnBeaconStore();
|
||||
});
|
||||
const component = getComponent({ roomId: room1Id });
|
||||
|
||||
// beacons have generated ids that break snapshots
|
||||
// assert on html
|
||||
expect(component.html()).toBeNull();
|
||||
});
|
||||
|
||||
describe('when user has live beacons and geolocation is available', () => {
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
|
||||
|
@ -164,6 +180,22 @@ describe('<RoomLiveShareWarning />', () => {
|
|||
expect(component.html()).toBe(null);
|
||||
});
|
||||
|
||||
it('removes itself when user stops monitoring live position', async () => {
|
||||
const component = getComponent({ roomId: room1Id });
|
||||
// started out rendered
|
||||
expect(component.html()).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
// cheat to clear this
|
||||
// @ts-ignore
|
||||
OwnBeaconStore.instance.clearPositionWatch = undefined;
|
||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
expect(component.html()).toBe(null);
|
||||
});
|
||||
|
||||
it('renders when user adds a live beacon', async () => {
|
||||
const component = getComponent({ roomId: room3Id });
|
||||
// started out not rendered
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correctly when minimized 1`] = `
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when minimized 1`] = `
|
||||
<LeftPanelLiveShareWarning
|
||||
isMinimized={true}
|
||||
>
|
||||
|
@ -15,7 +15,7 @@ exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correc
|
|||
</LeftPanelLiveShareWarning>
|
||||
`;
|
||||
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correctly when not minimized 1`] = `
|
||||
exports[`<LeftPanelLiveShareWarning /> when user has live location monitor renders correctly when not minimized 1`] = `
|
||||
<LeftPanelLiveShareWarning>
|
||||
<div
|
||||
className="mx_LeftPanelLiveShareWarning"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<RoomLiveShareWarning /> when user has live beacons renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">1h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||
|
||||
exports[`<RoomLiveShareWarning /> when user has live beacons renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||
exports[`<RoomLiveShareWarning /> when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = `"<div class=\\"mx_RoomLiveShareWarning\\"><div class=\\"mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon\\"></div><span class=\\"mx_RoomLiveShareWarning_label\\">You are sharing your live location</span><span data-test-id=\\"room-live-share-expiry\\" class=\\"mx_RoomLiveShareWarning_expiry\\">12h left</span><button data-test-id=\\"room-live-share-stop-sharing\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger\\">Stop sharing</button></div>"`;
|
||||
|
|
|
@ -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]),
|
||||
|
|
|
@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MockedObject } from "jest-mock";
|
||||
import { makeBeaconInfoContent, makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
|
||||
import { MockedObject } from "jest-mock";
|
||||
|
||||
import { getMockGeolocationPositionError } from "./location";
|
||||
|
||||
type InfoContentProps = {
|
||||
timeout: number;
|
||||
|
@ -150,16 +152,31 @@ export const mockGeolocation = (): MockedObject<Geolocation> => {
|
|||
* ```
|
||||
* will call the provided handler with a mock position at
|
||||
* next tick, 1000ms, 6000ms, 6050ms
|
||||
*
|
||||
* to produce errors provide an array of error codes
|
||||
* that will be applied to the delay with the same index
|
||||
* eg:
|
||||
* ```
|
||||
* // return two good positions, then a permission denied error
|
||||
* geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
|
||||
* [0, 1000, 3000], [0, 0, 1]),
|
||||
* );
|
||||
* ```
|
||||
* See for error codes: https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
*/
|
||||
export const watchPositionMockImplementation = (delays: number[]) => {
|
||||
return (callback: PositionCallback) => {
|
||||
export const watchPositionMockImplementation = (delays: number[], errorCodes: number[] = []) => {
|
||||
return (callback: PositionCallback, error: PositionErrorCallback) => {
|
||||
const position = makeGeolocationPosition({});
|
||||
|
||||
let totalDelay = 0;
|
||||
delays.map(delayMs => {
|
||||
delays.map((delayMs, index) => {
|
||||
totalDelay += delayMs;
|
||||
const timeout = setTimeout(() => {
|
||||
callback({ ...position, timestamp: position.timestamp + totalDelay });
|
||||
if (errorCodes[index]) {
|
||||
error(getMockGeolocationPositionError(errorCodes[index], 'error message'));
|
||||
} else {
|
||||
callback({ ...position, timestamp: position.timestamp + totalDelay });
|
||||
}
|
||||
}, totalDelay);
|
||||
return timeout;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from './beacon';
|
||||
export * from './client';
|
||||
export * from './location';
|
||||
export * from './platform';
|
||||
export * from './test-utils';
|
||||
// TODO @@TR: Export voice.ts, which currently isn't exported here because it causes all tests to depend on skinning
|
||||
|
|
|
@ -48,3 +48,11 @@ export const makeLocationEvent = (geoUri: string, assetType?: LocationAssetType)
|
|||
},
|
||||
);
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
export const getMockGeolocationPositionError = (code: number, message: string): GeolocationPositionError => ({
|
||||
code, message,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3,
|
||||
});
|
||||
|
|
|
@ -24,20 +24,16 @@ import {
|
|||
watchPosition,
|
||||
} from "../../../src/utils/beacon";
|
||||
import { getCurrentPosition } from "../../../src/utils/beacon/geolocation";
|
||||
import { makeGeolocationPosition, mockGeolocation } from "../../test-utils/beacon";
|
||||
import {
|
||||
makeGeolocationPosition,
|
||||
mockGeolocation,
|
||||
getMockGeolocationPositionError,
|
||||
} from "../../test-utils";
|
||||
|
||||
describe('geolocation utilities', () => {
|
||||
let geolocation;
|
||||
const defaultPosition = makeGeolocationPosition({});
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
const getMockGeolocationPositionError = (code, message) => ({
|
||||
code, message,
|
||||
PERMISSION_DENIED: 1,
|
||||
POSITION_UNAVAILABLE: 2,
|
||||
TIMEOUT: 3,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
geolocation = mockGeolocation();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue