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 LeftPanelLiveShareWarning: React.FC<Props> = ({ isMinimized }) => {
|
||||||
const hasLiveBeacons = useEventEmitterState(
|
const isMonitoringLiveLocation = useEventEmitterState(
|
||||||
OwnBeaconStore.instance,
|
OwnBeaconStore.instance,
|
||||||
OwnBeaconStoreEvent.LivenessChange,
|
OwnBeaconStoreEvent.MonitoringLivePosition,
|
||||||
() => OwnBeaconStore.instance.hasLiveBeacons(),
|
() => OwnBeaconStore.instance.isMonitoringLiveLocation,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasLiveBeacons) {
|
if (!isMonitoringLiveLocation) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,13 @@ type LiveBeaconsState = {
|
||||||
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
||||||
const [stoppingInProgress, setStoppingInProgress] = useState(false);
|
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(
|
const liveBeaconIds = useEventEmitterState(
|
||||||
OwnBeaconStore.instance,
|
OwnBeaconStore.instance,
|
||||||
OwnBeaconStoreEvent.LivenessChange,
|
OwnBeaconStoreEvent.LivenessChange,
|
||||||
|
@ -88,7 +95,7 @@ const useLiveBeacons = (roomId: Room['roomId']): LiveBeaconsState => {
|
||||||
setStoppingInProgress(false);
|
setStoppingInProgress(false);
|
||||||
}, [liveBeaconIds]);
|
}, [liveBeaconIds]);
|
||||||
|
|
||||||
if (!liveBeaconIds?.length) {
|
if (!isMonitoringLiveLocation || !liveBeaconIds?.length) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconIn
|
||||||
|
|
||||||
export enum OwnBeaconStoreEvent {
|
export enum OwnBeaconStoreEvent {
|
||||||
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
||||||
|
MonitoringLivePosition = 'OwnBeaconStore.MonitoringLivePosition',
|
||||||
}
|
}
|
||||||
|
|
||||||
const MOVING_UPDATE_INTERVAL = 2000;
|
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);
|
await this.matrixClient.unstable_setLiveBeacon(beacon.roomId, beacon.beaconInfoEventType, updateContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
private togglePollingLocation = async (): Promise<void> => {
|
private togglePollingLocation = () => {
|
||||||
if (!!this.liveBeaconIds.length) {
|
if (!!this.liveBeaconIds.length) {
|
||||||
return this.startPollingLocation();
|
this.startPollingLocation();
|
||||||
|
} else {
|
||||||
|
this.stopPollingLocation();
|
||||||
}
|
}
|
||||||
return this.stopPollingLocation();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private startPollingLocation = async () => {
|
private startPollingLocation = async () => {
|
||||||
// clear any existing interval
|
// clear any existing interval
|
||||||
this.stopPollingLocation();
|
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(() => {
|
this.locationInterval = setInterval(() => {
|
||||||
if (!this.lastPublishedPositionTimestamp) {
|
if (!this.lastPublishedPositionTimestamp) {
|
||||||
|
@ -255,6 +266,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.publishCurrentLocationToBeacons();
|
this.publishCurrentLocationToBeacons();
|
||||||
}
|
}
|
||||||
}, STATIC_UPDATE_INTERVAL);
|
}, STATIC_UPDATE_INTERVAL);
|
||||||
|
|
||||||
|
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onWatchedPosition = (position: GeolocationPosition) => {
|
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 = () => {
|
private stopPollingLocation = () => {
|
||||||
clearInterval(this.locationInterval);
|
clearInterval(this.locationInterval);
|
||||||
this.locationInterval = undefined;
|
this.locationInterval = undefined;
|
||||||
|
@ -283,6 +291,8 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
this.clearPositionWatch();
|
this.clearPositionWatch();
|
||||||
this.clearPositionWatch = undefined;
|
this.clearPositionWatch = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -313,8 +323,31 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
* and publishes it to all live beacons
|
* and publishes it to all live beacons
|
||||||
*/
|
*/
|
||||||
private publishCurrentLocationToBeacons = async () => {
|
private publishCurrentLocationToBeacons = async () => {
|
||||||
|
try {
|
||||||
const position = await getCurrentPosition();
|
const position = await getCurrentPosition();
|
||||||
// TODO error handling
|
// TODO error handling
|
||||||
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
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);
|
expect(component.html()).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when user has live beacons', () => {
|
describe('when user has live location monitor', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(true);
|
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = true;
|
||||||
});
|
});
|
||||||
it('renders correctly when not minimized', () => {
|
it('renders correctly when not minimized', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
@ -68,8 +68,8 @@ describe('<LeftPanelLiveShareWarning />', () => {
|
||||||
// started out rendered
|
// started out rendered
|
||||||
expect(component.html()).toBeTruthy();
|
expect(component.html()).toBeTruthy();
|
||||||
|
|
||||||
mocked(OwnBeaconStore.instance).hasLiveBeacons.mockReturnValue(false);
|
mocked(OwnBeaconStore.instance).isMonitoringLiveLocation = false;
|
||||||
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.LivenessChange, false);
|
OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition);
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
component.setProps({});
|
component.setProps({});
|
||||||
|
|
|
@ -18,10 +18,11 @@ import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
import { Room, Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
|
||||||
import '../../../skinned-sdk';
|
import '../../../skinned-sdk';
|
||||||
import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
|
import RoomLiveShareWarning from '../../../../src/components/views/beacon/RoomLiveShareWarning';
|
||||||
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from '../../../../src/stores/OwnBeaconStore';
|
||||||
import {
|
import {
|
||||||
advanceDateAndTime,
|
advanceDateAndTime,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
|
@ -33,7 +34,6 @@ import {
|
||||||
} from '../../../test-utils';
|
} from '../../../test-utils';
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
mockGeolocation();
|
|
||||||
describe('<RoomLiveShareWarning />', () => {
|
describe('<RoomLiveShareWarning />', () => {
|
||||||
const aliceId = '@alice:server.org';
|
const aliceId = '@alice:server.org';
|
||||||
const room1Id = '$room1:server.org';
|
const room1Id = '$room1:server.org';
|
||||||
|
@ -94,6 +94,7 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
mockGeolocation();
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
mockClient.unstable_setLiveBeacon.mockClear();
|
mockClient.unstable_setLiveBeacon.mockClear();
|
||||||
});
|
});
|
||||||
|
@ -123,7 +124,22 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
expect(component.html()).toBe(null);
|
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 () => {
|
beforeEach(async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
|
await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]);
|
||||||
|
@ -164,6 +180,22 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
expect(component.html()).toBe(null);
|
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 () => {
|
it('renders when user adds a live beacon', async () => {
|
||||||
const component = getComponent({ roomId: room3Id });
|
const component = getComponent({ roomId: room3Id });
|
||||||
// started out not rendered
|
// started out not rendered
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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
|
<LeftPanelLiveShareWarning
|
||||||
isMinimized={true}
|
isMinimized={true}
|
||||||
>
|
>
|
||||||
|
@ -15,7 +15,7 @@ exports[`<LeftPanelLiveShareWarning /> when user has live beacons renders correc
|
||||||
</LeftPanelLiveShareWarning>
|
</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>
|
<LeftPanelLiveShareWarning>
|
||||||
<div
|
<div
|
||||||
className="mx_LeftPanelLiveShareWarning"
|
className="mx_LeftPanelLiveShareWarning"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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 { Room, Beacon, BeaconEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
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 { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore";
|
||||||
import {
|
import {
|
||||||
|
@ -160,6 +161,7 @@ describe('OwnBeaconStore', () => {
|
||||||
mockClient.sendEvent.mockClear().mockResolvedValue({ event_id: '1' });
|
mockClient.sendEvent.mockClear().mockResolvedValue({ event_id: '1' });
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
jest.spyOn(OwnBeaconStore.instance, 'emit').mockRestore();
|
||||||
|
jest.spyOn(logger, 'error').mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
@ -600,24 +602,66 @@ describe('OwnBeaconStore', () => {
|
||||||
|
|
||||||
// stop watching location
|
// stop watching location
|
||||||
expect(geolocation.clearWatch).toHaveBeenCalled();
|
expect(geolocation.clearWatch).toHaveBeenCalled();
|
||||||
|
expect(store.isMonitoringLiveLocation).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts watching position when user starts having live beacons', async () => {
|
describe('when store is initialised with live beacons', () => {
|
||||||
makeRoomsWithStateEvents([]);
|
it('starts watching position', async () => {
|
||||||
await makeOwnBeaconStore();
|
makeRoomsWithStateEvents([
|
||||||
// wait for store to settle
|
alicesRoom1BeaconInfo,
|
||||||
await flushPromisesWithFakeTimers();
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
|
|
||||||
// wait for store to settle
|
// wait for store to settle
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
expect(geolocation.watchPosition).toHaveBeenCalled();
|
expect(geolocation.watchPosition).toHaveBeenCalled();
|
||||||
|
expect(store.isMonitoringLiveLocation).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('adding a new beacon', () => {
|
||||||
it('publishes position for new beacon immediately', async () => {
|
it('publishes position for new beacon immediately', async () => {
|
||||||
makeRoomsWithStateEvents([]);
|
makeRoomsWithStateEvents([]);
|
||||||
await makeOwnBeaconStore();
|
const store = await makeOwnBeaconStore();
|
||||||
// wait for store to settle
|
// wait for store to settle
|
||||||
await flushPromisesWithFakeTimers();
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
@ -626,6 +670,44 @@ describe('OwnBeaconStore', () => {
|
||||||
await flushPromisesWithFakeTimers();
|
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 () => {
|
it('publishes subsequent positions', async () => {
|
||||||
|
@ -650,6 +732,57 @@ describe('OwnBeaconStore', () => {
|
||||||
expect(mockClient.sendEvent).toHaveBeenCalledTimes(3);
|
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 () => {
|
it('publishes last known position after 30s of inactivity', async () => {
|
||||||
geolocation.watchPosition.mockImplementation(
|
geolocation.watchPosition.mockImplementation(
|
||||||
watchPositionMockImplementation([0]),
|
watchPositionMockImplementation([0]),
|
||||||
|
|
|
@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MockedObject } from "jest-mock";
|
||||||
import { makeBeaconInfoContent, makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
import { makeBeaconInfoContent, makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||||
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
|
import { LocationAssetType } from "matrix-js-sdk/src/@types/location";
|
||||||
import { MockedObject } from "jest-mock";
|
|
||||||
|
import { getMockGeolocationPositionError } from "./location";
|
||||||
|
|
||||||
type InfoContentProps = {
|
type InfoContentProps = {
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
@ -150,16 +152,31 @@ export const mockGeolocation = (): MockedObject<Geolocation> => {
|
||||||
* ```
|
* ```
|
||||||
* will call the provided handler with a mock position at
|
* will call the provided handler with a mock position at
|
||||||
* next tick, 1000ms, 6000ms, 6050ms
|
* 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[]) => {
|
export const watchPositionMockImplementation = (delays: number[], errorCodes: number[] = []) => {
|
||||||
return (callback: PositionCallback) => {
|
return (callback: PositionCallback, error: PositionErrorCallback) => {
|
||||||
const position = makeGeolocationPosition({});
|
const position = makeGeolocationPosition({});
|
||||||
|
|
||||||
let totalDelay = 0;
|
let totalDelay = 0;
|
||||||
delays.map(delayMs => {
|
delays.map((delayMs, index) => {
|
||||||
totalDelay += delayMs;
|
totalDelay += delayMs;
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
if (errorCodes[index]) {
|
||||||
|
error(getMockGeolocationPositionError(errorCodes[index], 'error message'));
|
||||||
|
} else {
|
||||||
callback({ ...position, timestamp: position.timestamp + totalDelay });
|
callback({ ...position, timestamp: position.timestamp + totalDelay });
|
||||||
|
}
|
||||||
}, totalDelay);
|
}, totalDelay);
|
||||||
return timeout;
|
return timeout;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './beacon';
|
export * from './beacon';
|
||||||
export * from './client';
|
export * from './client';
|
||||||
|
export * from './location';
|
||||||
export * from './platform';
|
export * from './platform';
|
||||||
export * from './test-utils';
|
export * from './test-utils';
|
||||||
// TODO @@TR: Export voice.ts, which currently isn't exported here because it causes all tests to depend on skinning
|
// 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,
|
watchPosition,
|
||||||
} from "../../../src/utils/beacon";
|
} from "../../../src/utils/beacon";
|
||||||
import { getCurrentPosition } from "../../../src/utils/beacon/geolocation";
|
import { getCurrentPosition } from "../../../src/utils/beacon/geolocation";
|
||||||
import { makeGeolocationPosition, mockGeolocation } from "../../test-utils/beacon";
|
import {
|
||||||
|
makeGeolocationPosition,
|
||||||
|
mockGeolocation,
|
||||||
|
getMockGeolocationPositionError,
|
||||||
|
} from "../../test-utils";
|
||||||
|
|
||||||
describe('geolocation utilities', () => {
|
describe('geolocation utilities', () => {
|
||||||
let geolocation;
|
let geolocation;
|
||||||
const defaultPosition = makeGeolocationPosition({});
|
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(() => {
|
beforeEach(() => {
|
||||||
geolocation = mockGeolocation();
|
geolocation = mockGeolocation();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue