Live location sharing - send geolocation beacon events - happy path (#8127)
* geolocation utilities Signed-off-by: Kerry Archibald <kerrya@element.io> * messy send events Signed-off-by: Kerry Archibald <kerrya@element.io> * add geolocation services Signed-off-by: Kerry Archibald <kerrya@element.io> * geolocation tests Signed-off-by: Kerry Archibald <kerrya@element.io> * debounce with backup emit every 30s Signed-off-by: Kerry Archibald <kerrya@element.io> * import reorder Signed-off-by: Kerry Archibald <kerrya@element.io> * some more working tests Signed-off-by: Kerry Archibald <kerrya@element.io> * complicated timeout testing Signed-off-by: Kerry Archibald <kerrya@element.io> * publish first location immediately Signed-off-by: Kerry Archibald <kerrya@element.io> * move advanceDateAndTime to utils, tidy Signed-off-by: Kerry Archibald <kerrya@element.io> * typos Signed-off-by: Kerry Archibald <kerrya@element.io> * types and lint Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
f557ac9486
commit
e9b2aea97b
6 changed files with 378 additions and 255 deletions
|
@ -21,22 +21,22 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
import { ClientEvent, IClientWellKnown } from 'matrix-js-sdk/src/client';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
|
||||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
import { tileServerFromWellKnown } from '../../../utils/WellKnownUtils';
|
||||||
import { LocationShareType, ShareLocationFn } from './shareLocation';
|
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||||
import { Icon as LocationIcon } from '../../../../res/img/element-icons/location.svg';
|
import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from '../../../utils/beacon';
|
||||||
|
import { LocationShareError, findMapStyleUrl } from '../../../utils/location';
|
||||||
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
|
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { MapError } from './MapError';
|
import { MapError } from './MapError';
|
||||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
|
||||||
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
|
import LiveDurationDropdown, { DEFAULT_DURATION_MS } from './LiveDurationDropdown';
|
||||||
import { GenericPosition, genericPositionFromGeolocation, getGeoUri } from '../../../utils/beacon';
|
import { LocationShareType, ShareLocationFn } from './shareLocation';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import { LocationShareError, findMapStyleUrl } from '../../../utils/location';
|
|
||||||
|
|
||||||
export interface ILocationPickerProps {
|
export interface ILocationPickerProps {
|
||||||
sender: RoomMember;
|
sender: RoomMember;
|
||||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { debounce } from "lodash";
|
||||||
import {
|
import {
|
||||||
Beacon,
|
Beacon,
|
||||||
BeaconEvent,
|
BeaconEvent,
|
||||||
|
@ -21,13 +22,23 @@ import {
|
||||||
Room,
|
Room,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
BeaconInfoState, makeBeaconInfoContent,
|
BeaconInfoState, makeBeaconContent, makeBeaconInfoContent,
|
||||||
} from "matrix-js-sdk/src/content-helpers";
|
} from "matrix-js-sdk/src/content-helpers";
|
||||||
|
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import { arrayHasDiff } from "../utils/arrays";
|
import { arrayDiff } from "../utils/arrays";
|
||||||
|
import {
|
||||||
|
ClearWatchCallback,
|
||||||
|
GeolocationError,
|
||||||
|
mapGeolocationPositionToTimedGeo,
|
||||||
|
TimedGeoUri,
|
||||||
|
watchPosition,
|
||||||
|
} from "../utils/beacon";
|
||||||
|
import { getCurrentPosition } from "../utils/beacon/geolocation";
|
||||||
|
|
||||||
const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId;
|
const isOwnBeacon = (beacon: Beacon, userId: string): boolean => beacon.beaconInfoOwner === userId;
|
||||||
|
|
||||||
|
@ -35,6 +46,9 @@ export enum OwnBeaconStoreEvent {
|
||||||
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
LivenessChange = 'OwnBeaconStore.LivenessChange',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MOVING_UPDATE_INTERVAL = 2000;
|
||||||
|
const STATIC_UPDATE_INTERVAL = 30000;
|
||||||
|
|
||||||
type OwnBeaconStoreState = {
|
type OwnBeaconStoreState = {
|
||||||
beacons: Map<string, Beacon>;
|
beacons: Map<string, Beacon>;
|
||||||
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
|
beaconsByRoomId: Map<Room['roomId'], Set<string>>;
|
||||||
|
@ -46,6 +60,15 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
public readonly beacons = new Map<string, Beacon>();
|
public readonly beacons = new Map<string, Beacon>();
|
||||||
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
|
public readonly beaconsByRoomId = new Map<Room['roomId'], Set<string>>();
|
||||||
private liveBeaconIds = [];
|
private liveBeaconIds = [];
|
||||||
|
private locationInterval: number;
|
||||||
|
private geolocationError: GeolocationError | undefined;
|
||||||
|
private clearPositionWatch: ClearWatchCallback | undefined;
|
||||||
|
/**
|
||||||
|
* Track when the last position was published
|
||||||
|
* So we can manually get position on slow interval
|
||||||
|
* when the target is stationary
|
||||||
|
*/
|
||||||
|
private lastPublishedPositionTimestamp: number | undefined;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super(defaultDispatcher);
|
super(defaultDispatcher);
|
||||||
|
@ -55,12 +78,21 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
return OwnBeaconStore.internalInstance;
|
return OwnBeaconStore.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when we have live beacons
|
||||||
|
* and geolocation.watchPosition is active
|
||||||
|
*/
|
||||||
|
public get isMonitoringLiveLocation(): boolean {
|
||||||
|
return !!this.clearPositionWatch;
|
||||||
|
}
|
||||||
|
|
||||||
protected async onNotReady() {
|
protected async onNotReady() {
|
||||||
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
this.matrixClient.removeListener(BeaconEvent.LivenessChange, this.onBeaconLiveness);
|
||||||
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
this.matrixClient.removeListener(BeaconEvent.New, this.onNewBeacon);
|
||||||
|
|
||||||
this.beacons.forEach(beacon => beacon.destroy());
|
this.beacons.forEach(beacon => beacon.destroy());
|
||||||
|
|
||||||
|
this.stopPollingLocation();
|
||||||
this.beacons.clear();
|
this.beacons.clear();
|
||||||
this.beaconsByRoomId.clear();
|
this.beaconsByRoomId.clear();
|
||||||
this.liveBeaconIds = [];
|
this.liveBeaconIds = [];
|
||||||
|
@ -117,21 +149,12 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLive && this.liveBeaconIds.includes(beacon.identifier)) {
|
|
||||||
this.liveBeaconIds =
|
|
||||||
this.liveBeaconIds.filter(beaconId => beaconId !== beacon.identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLive && !this.liveBeaconIds.includes(beacon.identifier)) {
|
|
||||||
this.liveBeaconIds.push(beacon.identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
// beacon expired, update beacon to un-alive state
|
// beacon expired, update beacon to un-alive state
|
||||||
if (!isLive) {
|
if (!isLive) {
|
||||||
this.stopBeacon(beacon.identifier);
|
this.stopBeacon(beacon.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO start location polling here
|
this.checkLiveness();
|
||||||
|
|
||||||
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
|
this.emit(OwnBeaconStoreEvent.LivenessChange, this.getLiveBeaconIds());
|
||||||
};
|
};
|
||||||
|
@ -169,9 +192,29 @@ export class OwnBeaconStore extends AsyncStoreWithClient<OwnBeaconStoreState> {
|
||||||
.filter(beacon => beacon.isLive)
|
.filter(beacon => beacon.isLive)
|
||||||
.map(beacon => beacon.identifier);
|
.map(beacon => beacon.identifier);
|
||||||
|
|
||||||
if (arrayHasDiff(prevLiveBeaconIds, this.liveBeaconIds)) {
|
const diff = arrayDiff(prevLiveBeaconIds, this.liveBeaconIds);
|
||||||
|
|
||||||
|
if (diff.added.length || diff.removed.length) {
|
||||||
this.emit(OwnBeaconStoreEvent.LivenessChange, this.liveBeaconIds);
|
this.emit(OwnBeaconStoreEvent.LivenessChange, this.liveBeaconIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// publish current location immediately
|
||||||
|
// when there are new live beacons
|
||||||
|
// and we already have a live monitor
|
||||||
|
// so first position is published quickly
|
||||||
|
// even when target is stationary
|
||||||
|
//
|
||||||
|
// when there is no existing live monitor
|
||||||
|
// it will be created below by togglePollingLocation
|
||||||
|
// and publish first position quickly
|
||||||
|
if (diff.added.length && this.isMonitoringLiveLocation) {
|
||||||
|
this.publishCurrentLocationToBeacons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if overall liveness changed
|
||||||
|
if (!!prevLiveBeaconIds?.length !== !!this.liveBeaconIds.length) {
|
||||||
|
this.togglePollingLocation();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
private updateBeaconEvent = async (beacon: Beacon, update: Partial<BeaconInfoState>): Promise<void> => {
|
||||||
|
@ -188,4 +231,90 @@ 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> => {
|
||||||
|
if (!!this.liveBeaconIds.length) {
|
||||||
|
return this.startPollingLocation();
|
||||||
|
}
|
||||||
|
return this.stopPollingLocation();
|
||||||
|
};
|
||||||
|
|
||||||
|
private startPollingLocation = async () => {
|
||||||
|
// clear any existing interval
|
||||||
|
this.stopPollingLocation();
|
||||||
|
|
||||||
|
this.clearPositionWatch = await watchPosition(this.onWatchedPosition, this.onWatchedPositionError);
|
||||||
|
|
||||||
|
this.locationInterval = setInterval(() => {
|
||||||
|
if (!this.lastPublishedPositionTimestamp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// if position was last updated STATIC_UPDATE_INTERVAL ms ago or more
|
||||||
|
// get our position and publish it
|
||||||
|
if (this.lastPublishedPositionTimestamp <= Date.now() - STATIC_UPDATE_INTERVAL) {
|
||||||
|
this.publishCurrentLocationToBeacons();
|
||||||
|
}
|
||||||
|
}, STATIC_UPDATE_INTERVAL);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onWatchedPosition = (position: GeolocationPosition) => {
|
||||||
|
const timedGeoPosition = mapGeolocationPositionToTimedGeo(position);
|
||||||
|
|
||||||
|
// if this is our first position, publish immediateley
|
||||||
|
if (!this.lastPublishedPositionTimestamp) {
|
||||||
|
this.publishLocationToBeacons(timedGeoPosition);
|
||||||
|
} else {
|
||||||
|
this.debouncedPublishLocationToBeacons(timedGeoPosition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onWatchedPositionError = (error: GeolocationError) => {
|
||||||
|
this.geolocationError = error;
|
||||||
|
logger.error(this.geolocationError);
|
||||||
|
};
|
||||||
|
|
||||||
|
private stopPollingLocation = () => {
|
||||||
|
clearInterval(this.locationInterval);
|
||||||
|
this.locationInterval = undefined;
|
||||||
|
this.lastPublishedPositionTimestamp = undefined;
|
||||||
|
this.geolocationError = undefined;
|
||||||
|
|
||||||
|
if (this.clearPositionWatch) {
|
||||||
|
this.clearPositionWatch();
|
||||||
|
this.clearPositionWatch = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends m.location events to all live beacons
|
||||||
|
* Sets last published beacon
|
||||||
|
*/
|
||||||
|
private publishLocationToBeacons = async (position: TimedGeoUri) => {
|
||||||
|
this.lastPublishedPositionTimestamp = Date.now();
|
||||||
|
// TODO handle failure in individual beacon without rejecting rest
|
||||||
|
await Promise.all(this.liveBeaconIds.map(beaconId =>
|
||||||
|
this.sendLocationToBeacon(this.beacons.get(beaconId), position)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private debouncedPublishLocationToBeacons = debounce(this.publishLocationToBeacons, MOVING_UPDATE_INTERVAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends m.location event to referencing given beacon
|
||||||
|
*/
|
||||||
|
private sendLocationToBeacon = async (beacon: Beacon, { geoUri, timestamp }: TimedGeoUri) => {
|
||||||
|
const content = makeBeaconContent(geoUri, timestamp, beacon.beaconInfoId);
|
||||||
|
await this.matrixClient.sendEvent(beacon.roomId, M_BEACON.name, content);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current location
|
||||||
|
* (as opposed to using watched location)
|
||||||
|
* and publishes it to all live beacons
|
||||||
|
*/
|
||||||
|
private publishCurrentLocationToBeacons = async () => {
|
||||||
|
const position = await getCurrentPosition();
|
||||||
|
// TODO error handling
|
||||||
|
this.publishLocationToBeacons(mapGeolocationPositionToTimedGeo(position));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +23,17 @@ 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 } from '../../../../src/stores/OwnBeaconStore';
|
||||||
import {
|
import {
|
||||||
|
advanceDateAndTime,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
|
mockGeolocation,
|
||||||
resetAsyncStoreWithClient,
|
resetAsyncStoreWithClient,
|
||||||
setupAsyncStoreWithClient,
|
setupAsyncStoreWithClient,
|
||||||
} 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';
|
||||||
|
@ -40,6 +43,7 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||||
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
|
sendEvent: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 14.03.2022 16:15
|
// 14.03.2022 16:15
|
||||||
|
@ -69,14 +73,6 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
return [room1, room2];
|
return [room1, room2];
|
||||||
};
|
};
|
||||||
|
|
||||||
const advanceDateAndTime = (ms: number) => {
|
|
||||||
// bc liveness check uses Date.now we have to advance this mock
|
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(Date.now() + ms);
|
|
||||||
|
|
||||||
// then advance time for the interval by the same amount
|
|
||||||
jest.advanceTimersByTime(ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeOwnBeaconStore = async () => {
|
const makeOwnBeaconStore = async () => {
|
||||||
const store = OwnBeaconStore.instance;
|
const store = OwnBeaconStore.instance;
|
||||||
|
|
||||||
|
@ -137,12 +133,16 @@ describe('<RoomLiveShareWarning />', () => {
|
||||||
|
|
||||||
it('renders correctly with one live beacon in room', () => {
|
it('renders correctly with one live beacon in room', () => {
|
||||||
const component = getComponent({ roomId: room1Id });
|
const component = getComponent({ roomId: room1Id });
|
||||||
expect(component).toMatchSnapshot();
|
// beacons have generated ids that break snapshots
|
||||||
|
// assert on html
|
||||||
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with two live beacons in room', () => {
|
it('renders correctly with two live beacons in room', () => {
|
||||||
const component = getComponent({ roomId: room2Id });
|
const component = getComponent({ roomId: room2Id });
|
||||||
expect(component).toMatchSnapshot();
|
// beacons have generated ids that break snapshots
|
||||||
|
// assert on html
|
||||||
|
expect(component.html()).toMatchSnapshot();
|
||||||
// later expiry displayed
|
// later expiry displayed
|
||||||
expect(getExpiryText(component)).toEqual('12h left');
|
expect(getExpiryText(component)).toEqual('12h left');
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,191 +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`] = `
|
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>"`;
|
||||||
<RoomLiveShareWarning
|
|
||||||
roomId="$room1:server.org"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_RoomLiveShareWarning"
|
|
||||||
>
|
|
||||||
<StyledLiveBeaconIcon
|
|
||||||
className="mx_RoomLiveShareWarning_icon"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon"
|
|
||||||
/>
|
|
||||||
</StyledLiveBeaconIcon>
|
|
||||||
<span
|
|
||||||
className="mx_RoomLiveShareWarning_label"
|
|
||||||
>
|
|
||||||
You are sharing your live location
|
|
||||||
</span>
|
|
||||||
<LiveTimeRemaining
|
|
||||||
beacon={
|
|
||||||
Beacon {
|
|
||||||
"_beaconInfo": Object {
|
|
||||||
"assetType": "m.self",
|
|
||||||
"description": undefined,
|
|
||||||
"live": true,
|
|
||||||
"timeout": 3600000,
|
|
||||||
"timestamp": 1647270879403,
|
|
||||||
},
|
|
||||||
"_events": Object {
|
|
||||||
"Beacon.LivenessChange": Array [
|
|
||||||
[Function],
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
"Beacon.new": [Function],
|
|
||||||
"Beacon.update": [Function],
|
|
||||||
},
|
|
||||||
"_eventsCount": 3,
|
|
||||||
"_isLive": true,
|
|
||||||
"_maxListeners": undefined,
|
|
||||||
"livenessWatchInterval": 1000000000002,
|
|
||||||
"roomId": "$room1:server.org",
|
|
||||||
"rootEvent": Object {
|
|
||||||
"content": Object {
|
|
||||||
"org.matrix.msc3488.asset": Object {
|
|
||||||
"type": "m.self",
|
|
||||||
},
|
|
||||||
"org.matrix.msc3488.ts": 1647270879403,
|
|
||||||
"org.matrix.msc3489.beacon_info": Object {
|
|
||||||
"description": undefined,
|
|
||||||
"live": true,
|
|
||||||
"timeout": 3600000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"event_id": "$0",
|
|
||||||
"room_id": "$room1:server.org",
|
|
||||||
"state_key": "@alice:server.org",
|
|
||||||
"type": "org.matrix.msc3489.beacon_info.@alice:server.org.2",
|
|
||||||
},
|
|
||||||
Symbol(kCapture): false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mx_RoomLiveShareWarning_expiry"
|
|
||||||
data-test-id="room-live-share-expiry"
|
|
||||||
>
|
|
||||||
1h left
|
|
||||||
</span>
|
|
||||||
</LiveTimeRemaining>
|
|
||||||
<AccessibleButton
|
|
||||||
data-test-id="room-live-share-stop-sharing"
|
|
||||||
disabled={false}
|
|
||||||
element="button"
|
|
||||||
kind="danger"
|
|
||||||
onClick={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
|
||||||
data-test-id="room-live-share-stop-sharing"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Stop sharing
|
|
||||||
</button>
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</RoomLiveShareWarning>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`<RoomLiveShareWarning /> when user has live beacons renders correctly with two live beacons in room 1`] = `
|
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>"`;
|
||||||
<RoomLiveShareWarning
|
|
||||||
roomId="$room2:server.org"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_RoomLiveShareWarning"
|
|
||||||
>
|
|
||||||
<StyledLiveBeaconIcon
|
|
||||||
className="mx_RoomLiveShareWarning_icon"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_StyledLiveBeaconIcon mx_RoomLiveShareWarning_icon"
|
|
||||||
/>
|
|
||||||
</StyledLiveBeaconIcon>
|
|
||||||
<span
|
|
||||||
className="mx_RoomLiveShareWarning_label"
|
|
||||||
>
|
|
||||||
You are sharing your live location
|
|
||||||
</span>
|
|
||||||
<LiveTimeRemaining
|
|
||||||
beacon={
|
|
||||||
Beacon {
|
|
||||||
"_beaconInfo": Object {
|
|
||||||
"assetType": "m.self",
|
|
||||||
"description": undefined,
|
|
||||||
"live": true,
|
|
||||||
"timeout": 43200000,
|
|
||||||
"timestamp": 1647270879403,
|
|
||||||
},
|
|
||||||
"_events": Object {
|
|
||||||
"Beacon.LivenessChange": Array [
|
|
||||||
[Function],
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
"Beacon.new": [Function],
|
|
||||||
"Beacon.update": [Function],
|
|
||||||
},
|
|
||||||
"_eventsCount": 3,
|
|
||||||
"_isLive": true,
|
|
||||||
"_maxListeners": undefined,
|
|
||||||
"livenessWatchInterval": 1000000000010,
|
|
||||||
"roomId": "$room2:server.org",
|
|
||||||
"rootEvent": Object {
|
|
||||||
"content": Object {
|
|
||||||
"org.matrix.msc3488.asset": Object {
|
|
||||||
"type": "m.self",
|
|
||||||
},
|
|
||||||
"org.matrix.msc3488.ts": 1647270879403,
|
|
||||||
"org.matrix.msc3489.beacon_info": Object {
|
|
||||||
"description": undefined,
|
|
||||||
"live": true,
|
|
||||||
"timeout": 43200000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"event_id": "$2",
|
|
||||||
"room_id": "$room2:server.org",
|
|
||||||
"state_key": "@alice:server.org",
|
|
||||||
"type": "org.matrix.msc3489.beacon_info.@alice:server.org.4",
|
|
||||||
},
|
|
||||||
Symbol(kCapture): false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="mx_RoomLiveShareWarning_expiry"
|
|
||||||
data-test-id="room-live-share-expiry"
|
|
||||||
>
|
|
||||||
12h left
|
|
||||||
</span>
|
|
||||||
</LiveTimeRemaining>
|
|
||||||
<AccessibleButton
|
|
||||||
data-test-id="room-live-share-stop-sharing"
|
|
||||||
disabled={false}
|
|
||||||
element="button"
|
|
||||||
kind="danger"
|
|
||||||
onClick={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger"
|
|
||||||
data-test-id="room-live-share-stop-sharing"
|
|
||||||
onClick={[Function]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
Stop sharing
|
|
||||||
</button>
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</RoomLiveShareWarning>
|
|
||||||
`;
|
|
||||||
|
|
|
@ -14,17 +14,35 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room, Beacon, BeaconEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, Beacon, BeaconEvent, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
import { makeBeaconContent } from "matrix-js-sdk/src/content-helpers";
|
||||||
|
import { M_BEACON, M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
|
||||||
|
|
||||||
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore";
|
import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../src/stores/OwnBeaconStore";
|
||||||
import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../test-utils";
|
import {
|
||||||
import { makeBeaconInfoEvent } from "../test-utils/beacon";
|
advanceDateAndTime,
|
||||||
|
flushPromisesWithFakeTimers,
|
||||||
|
resetAsyncStoreWithClient,
|
||||||
|
setupAsyncStoreWithClient,
|
||||||
|
} from "../test-utils";
|
||||||
|
import {
|
||||||
|
makeBeaconInfoEvent,
|
||||||
|
makeGeolocationPosition,
|
||||||
|
mockGeolocation,
|
||||||
|
watchPositionMockImplementation,
|
||||||
|
} from "../test-utils/beacon";
|
||||||
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||||
|
|
||||||
|
// modern fake timers and lodash.debounce are a faff
|
||||||
|
// short circuit it
|
||||||
|
jest.mock("lodash", () => ({
|
||||||
|
debounce: jest.fn().mockImplementation(callback => callback),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
describe('OwnBeaconStore', () => {
|
describe('OwnBeaconStore', () => {
|
||||||
|
let geolocation;
|
||||||
// 14.03.2022 16:15
|
// 14.03.2022 16:15
|
||||||
const now = 1647270879403;
|
const now = 1647270879403;
|
||||||
const HOUR_MS = 3600000;
|
const HOUR_MS = 3600000;
|
||||||
|
@ -35,10 +53,15 @@ describe('OwnBeaconStore', () => {
|
||||||
getUserId: jest.fn().mockReturnValue(aliceId),
|
getUserId: jest.fn().mockReturnValue(aliceId),
|
||||||
getVisibleRooms: jest.fn().mockReturnValue([]),
|
getVisibleRooms: jest.fn().mockReturnValue([]),
|
||||||
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
|
sendEvent: jest.fn().mockResolvedValue({ event_id: '1' }),
|
||||||
});
|
});
|
||||||
const room1Id = '$room1:server.org';
|
const room1Id = '$room1:server.org';
|
||||||
const room2Id = '$room2:server.org';
|
const room2Id = '$room2:server.org';
|
||||||
|
|
||||||
|
// returned by default geolocation mocks
|
||||||
|
const defaultLocation = makeGeolocationPosition({});
|
||||||
|
const defaultLocationUri = 'geo:54.001927,-8.253491;u=1';
|
||||||
|
|
||||||
// beacon_info events
|
// beacon_info events
|
||||||
// created 'an hour ago'
|
// created 'an hour ago'
|
||||||
// with timeout of 3 hours
|
// with timeout of 3 hours
|
||||||
|
@ -89,13 +112,6 @@ describe('OwnBeaconStore', () => {
|
||||||
return [room1, room2];
|
return [room1, room2];
|
||||||
};
|
};
|
||||||
|
|
||||||
const advanceDateAndTime = (ms: number) => {
|
|
||||||
// bc liveness check uses Date.now we have to advance this mock
|
|
||||||
jest.spyOn(global.Date, 'now').mockReturnValue(now + ms);
|
|
||||||
// then advance time for the interval by the same amount
|
|
||||||
jest.advanceTimersByTime(ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeOwnBeaconStore = async () => {
|
const makeOwnBeaconStore = async () => {
|
||||||
const store = OwnBeaconStore.instance;
|
const store = OwnBeaconStore.instance;
|
||||||
|
|
||||||
|
@ -103,20 +119,53 @@ describe('OwnBeaconStore', () => {
|
||||||
return store;
|
return store;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expireBeaconAndEmit = (store, beaconInfoEvent: MatrixEvent): void => {
|
||||||
|
const beacon = store.getBeaconById(beaconInfoEvent.getType());
|
||||||
|
// time travel until beacon is expired
|
||||||
|
advanceDateAndTime(beacon.beaconInfo.timeout + 100);
|
||||||
|
|
||||||
|
// force an update on the beacon
|
||||||
|
// @ts-ignore
|
||||||
|
beacon.setBeaconInfo(beaconInfoEvent);
|
||||||
|
|
||||||
|
mockClient.emit(BeaconEvent.LivenessChange, false, beacon);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBeaconLivenessAndEmit = (store, beaconInfoEvent: MatrixEvent, isLive: boolean): void => {
|
||||||
|
const beacon = store.getBeaconById(beaconInfoEvent.getType());
|
||||||
|
// matches original state of event content
|
||||||
|
// except for live property
|
||||||
|
const updateEvent = makeBeaconInfoEvent(
|
||||||
|
beaconInfoEvent.getSender(),
|
||||||
|
beaconInfoEvent.getRoomId(),
|
||||||
|
{ isLive, timeout: beacon.beaconInfo.timeout },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
updateEvent.event.type = beaconInfoEvent.getType();
|
||||||
|
beacon.update(updateEvent);
|
||||||
|
|
||||||
|
mockClient.emit(BeaconEvent.Update, beaconInfoEvent, beacon);
|
||||||
|
mockClient.emit(BeaconEvent.LivenessChange, false, beacon);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addNewBeaconAndEmit = (beaconInfoEvent: MatrixEvent): void => {
|
||||||
|
const beacon = new Beacon(beaconInfoEvent);
|
||||||
|
mockClient.emit(BeaconEvent.New, beaconInfoEvent, beacon);
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
geolocation = mockGeolocation();
|
||||||
mockClient.getVisibleRooms.mockReturnValue([]);
|
mockClient.getVisibleRooms.mockReturnValue([]);
|
||||||
mockClient.unstable_setLiveBeacon.mockClear().mockResolvedValue({ event_id: '1' });
|
mockClient.unstable_setLiveBeacon.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();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await resetAsyncStoreWithClient(OwnBeaconStore.instance);
|
await resetAsyncStoreWithClient(OwnBeaconStore.instance);
|
||||||
});
|
|
||||||
|
|
||||||
it('works', async () => {
|
jest.clearAllTimers();
|
||||||
const store = await makeOwnBeaconStore();
|
|
||||||
expect(store.hasLiveBeacons()).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onReady()', () => {
|
describe('onReady()', () => {
|
||||||
|
@ -149,6 +198,38 @@ describe('OwnBeaconStore', () => {
|
||||||
alicesRoom2BeaconInfo.getType(),
|
alicesRoom2BeaconInfo.getType(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not do any geolocation when user has no live beacons', async () => {
|
||||||
|
makeRoomsWithStateEvents([bobsRoom1BeaconInfo, bobsOldRoom1BeaconInfo]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
expect(store.hasLiveBeacons()).toBe(false);
|
||||||
|
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
expect(geolocation.watchPosition).not.toHaveBeenCalled();
|
||||||
|
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does geolocation and sends location immediatley when user has live beacons', async () => {
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
alicesRoom2BeaconInfo,
|
||||||
|
]);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
expect(geolocation.watchPosition).toHaveBeenCalled();
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
||||||
|
room1Id,
|
||||||
|
M_BEACON.name,
|
||||||
|
makeBeaconContent(defaultLocationUri, defaultLocation.timestamp, alicesRoom1BeaconInfo.getId()),
|
||||||
|
);
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledWith(
|
||||||
|
room2Id,
|
||||||
|
M_BEACON.name,
|
||||||
|
makeBeaconContent(defaultLocationUri, defaultLocation.timestamp, alicesRoom2BeaconInfo.getId()),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onNotReady()', () => {
|
describe('onNotReady()', () => {
|
||||||
|
@ -372,12 +453,8 @@ describe('OwnBeaconStore', () => {
|
||||||
// live before
|
// live before
|
||||||
expect(store.hasLiveBeacons()).toBe(true);
|
expect(store.hasLiveBeacons()).toBe(true);
|
||||||
const emitSpy = jest.spyOn(store, 'emit');
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
const alicesBeacon = new Beacon(alicesRoom1BeaconInfo);
|
|
||||||
|
|
||||||
// time travel until beacon is expired
|
await expireBeaconAndEmit(store, alicesRoom1BeaconInfo);
|
||||||
advanceDateAndTime(HOUR_MS * 3);
|
|
||||||
|
|
||||||
mockClient.emit(BeaconEvent.LivenessChange, false, alicesBeacon);
|
|
||||||
|
|
||||||
expect(store.hasLiveBeacons()).toBe(false);
|
expect(store.hasLiveBeacons()).toBe(false);
|
||||||
expect(store.hasLiveBeacons(room1Id)).toBe(false);
|
expect(store.hasLiveBeacons(room1Id)).toBe(false);
|
||||||
|
@ -388,14 +465,10 @@ describe('OwnBeaconStore', () => {
|
||||||
makeRoomsWithStateEvents([
|
makeRoomsWithStateEvents([
|
||||||
alicesRoom1BeaconInfo,
|
alicesRoom1BeaconInfo,
|
||||||
]);
|
]);
|
||||||
await makeOwnBeaconStore();
|
const store = await makeOwnBeaconStore();
|
||||||
const alicesBeacon = new Beacon(alicesRoom1BeaconInfo);
|
|
||||||
const prevEventContent = alicesRoom1BeaconInfo.getContent();
|
const prevEventContent = alicesRoom1BeaconInfo.getContent();
|
||||||
|
|
||||||
// time travel until beacon is expired
|
await expireBeaconAndEmit(store, alicesRoom1BeaconInfo);
|
||||||
advanceDateAndTime(HOUR_MS * 3);
|
|
||||||
|
|
||||||
mockClient.emit(BeaconEvent.LivenessChange, false, alicesBeacon);
|
|
||||||
|
|
||||||
// matches original state of event content
|
// matches original state of event content
|
||||||
// except for live property
|
// except for live property
|
||||||
|
@ -422,15 +495,8 @@ describe('OwnBeaconStore', () => {
|
||||||
// not live before
|
// not live before
|
||||||
expect(store.hasLiveBeacons()).toBe(false);
|
expect(store.hasLiveBeacons()).toBe(false);
|
||||||
const emitSpy = jest.spyOn(store, 'emit');
|
const emitSpy = jest.spyOn(store, 'emit');
|
||||||
const alicesBeacon = new Beacon(alicesOldRoomIdBeaconInfo);
|
|
||||||
const liveUpdate = makeBeaconInfoEvent(
|
|
||||||
aliceId, room1Id, { isLive: true }, alicesOldRoomIdBeaconInfo.getId(), '$alice-room1-2',
|
|
||||||
);
|
|
||||||
|
|
||||||
// bring the beacon back to life
|
updateBeaconLivenessAndEmit(store, alicesOldRoomIdBeaconInfo, true);
|
||||||
alicesBeacon.update(liveUpdate);
|
|
||||||
|
|
||||||
mockClient.emit(BeaconEvent.LivenessChange, true, alicesBeacon);
|
|
||||||
|
|
||||||
expect(store.hasLiveBeacons()).toBe(true);
|
expect(store.hasLiveBeacons()).toBe(true);
|
||||||
expect(store.hasLiveBeacons(room1Id)).toBe(true);
|
expect(store.hasLiveBeacons(room1Id)).toBe(true);
|
||||||
|
@ -512,4 +578,120 @@ describe('OwnBeaconStore', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('sending positions', () => {
|
||||||
|
it('stops watching position when user has no more live beacons', async () => {
|
||||||
|
// geolocation is only going to emit 1 position
|
||||||
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([0]),
|
||||||
|
);
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
const store = await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
// two locations were published
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// expire the beacon
|
||||||
|
// user now has no live beacons
|
||||||
|
await expireBeaconAndEmit(store, alicesRoom1BeaconInfo);
|
||||||
|
|
||||||
|
// stop watching location
|
||||||
|
expect(geolocation.clearWatch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts watching position when user starts having live beacons', async () => {
|
||||||
|
makeRoomsWithStateEvents([]);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
expect(geolocation.watchPosition).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes position for new beacon immediately', async () => {
|
||||||
|
makeRoomsWithStateEvents([]);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
addNewBeaconAndEmit(alicesRoom1BeaconInfo);
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes subsequent positions', async () => {
|
||||||
|
// modern fake timers + debounce + promises are not friends
|
||||||
|
// just testing that positions are published
|
||||||
|
// not that the debounce works
|
||||||
|
|
||||||
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([0, 1000, 3000]),
|
||||||
|
);
|
||||||
|
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(0);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(5000);
|
||||||
|
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('publishes last known position after 30s of inactivity', async () => {
|
||||||
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
// published first location
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
advanceDateAndTime(31000);
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
// republished latest location
|
||||||
|
expect(mockClient.sendEvent).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not try to publish anything if there is no known position after 30s of inactivity', async () => {
|
||||||
|
// no position ever returned from geolocation
|
||||||
|
geolocation.watchPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([]),
|
||||||
|
);
|
||||||
|
geolocation.getCurrentPosition.mockImplementation(
|
||||||
|
watchPositionMockImplementation([]),
|
||||||
|
);
|
||||||
|
|
||||||
|
makeRoomsWithStateEvents([
|
||||||
|
alicesRoom1BeaconInfo,
|
||||||
|
]);
|
||||||
|
await makeOwnBeaconStore();
|
||||||
|
// wait for store to settle
|
||||||
|
await flushPromisesWithFakeTimers();
|
||||||
|
|
||||||
|
advanceDateAndTime(31000);
|
||||||
|
|
||||||
|
// no locations published
|
||||||
|
expect(mockClient.sendEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -229,8 +229,6 @@ describe('geolocation utilities', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves with current location', async () => {
|
it('resolves with current location', async () => {
|
||||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
|
||||||
|
|
||||||
geolocation.getCurrentPosition.mockImplementation((callback, error) => callback(defaultPosition));
|
geolocation.getCurrentPosition.mockImplementation((callback, error) => callback(defaultPosition));
|
||||||
|
|
||||||
const result = await getCurrentPosition();
|
const result = await getCurrentPosition();
|
||||||
|
|
Loading…
Reference in a new issue