Live location share - focus on user location on list item click (PSG-609) (#9051)
* extract preventDefaultWrapper into utils * add click handling to beacon list item * add click handling to dialog sidebar * focus in on beacons when clicked in list * stylelint * fussy import ordering * test beacon focusing in beaocnviewdialog
This commit is contained in:
parent
38a913488f
commit
dc6ceb1d1c
16 changed files with 473 additions and 89 deletions
|
@ -22,6 +22,8 @@ limitations under the License.
|
||||||
padding: $spacing-12 0;
|
padding: $spacing-12 0;
|
||||||
|
|
||||||
border-bottom: 1px solid $system;
|
border-bottom: 1px solid $system;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BeaconListItem_avatarIcon {
|
.mx_BeaconListItem_avatarIcon {
|
||||||
|
@ -61,3 +63,8 @@ limitations under the License.
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_BeaconListItem_interactions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { HTMLProps, useContext } from 'react';
|
||||||
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||||
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||||
|
|
||||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||||
import { humanizeTime } from '../../../utils/humanize';
|
import { humanizeTime } from '../../../utils/humanize';
|
||||||
|
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import BeaconStatus from './BeaconStatus';
|
import BeaconStatus from './BeaconStatus';
|
||||||
|
@ -32,7 +33,7 @@ interface Props {
|
||||||
beacon: Beacon;
|
beacon: Beacon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ...rest }) => {
|
||||||
const latestLocationState = useEventEmitterState(
|
const latestLocationState = useEventEmitterState(
|
||||||
beacon,
|
beacon,
|
||||||
BeaconEvent.LocationUpdate,
|
BeaconEvent.LocationUpdate,
|
||||||
|
@ -52,7 +53,7 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||||
|
|
||||||
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
||||||
|
|
||||||
return <li className='mx_BeaconListItem'>
|
return <li className='mx_BeaconListItem' {...rest}>
|
||||||
{ isSelfLocation ?
|
{ isSelfLocation ?
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
className='mx_BeaconListItem_avatar'
|
className='mx_BeaconListItem_avatar'
|
||||||
|
@ -69,7 +70,11 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||||
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
||||||
displayStatus={BeaconDisplayStatus.Active}
|
displayStatus={BeaconDisplayStatus.Active}
|
||||||
>
|
>
|
||||||
|
{ /* eat events from interactive share buttons
|
||||||
|
so parent click handlers are not triggered */ }
|
||||||
|
<div className='mx_BeaconListItem_interactions' onClick={preventDefaultWrapper(() => {})}>
|
||||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||||
|
</div>
|
||||||
</BeaconStatus>
|
</BeaconStatus>
|
||||||
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import {
|
import {
|
||||||
Beacon,
|
Beacon,
|
||||||
|
@ -45,7 +45,16 @@ interface IProps extends IDialogProps {
|
||||||
roomId: Room['roomId'];
|
roomId: Room['roomId'];
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
// open the map centered on this beacon's location
|
// open the map centered on this beacon's location
|
||||||
focusBeacon?: Beacon;
|
initialFocusedBeacon?: Beacon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// track the 'focused time' as ts
|
||||||
|
// to make it possible to refocus the same beacon
|
||||||
|
// as the beacon location may change
|
||||||
|
// or the map may move around
|
||||||
|
interface FocusedBeaconState {
|
||||||
|
ts: number;
|
||||||
|
beacon?: Beacon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||||
|
@ -59,31 +68,52 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): {
|
const useInitialMapPosition = (liveBeacons: Beacon[], { beacon, ts }: FocusedBeaconState): {
|
||||||
bounds?: Bounds; centerGeoUri: string;
|
bounds?: Bounds; centerGeoUri: string;
|
||||||
} => {
|
} => {
|
||||||
const bounds = useRef<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
const [bounds, setBounds] = useState<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||||
const centerGeoUri = useRef<string>(
|
const [centerGeoUri, setCenterGeoUri] = useState<string>(
|
||||||
focusBeacon?.latestLocationState?.uri ||
|
beacon?.latestLocationState?.uri ||
|
||||||
getBoundsCenter(bounds.current),
|
getBoundsCenter(bounds),
|
||||||
);
|
);
|
||||||
return { bounds: bounds.current, centerGeoUri: centerGeoUri.current };
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
// this check ignores the first initial focused beacon state
|
||||||
|
// as centering logic on map zooms to show everything
|
||||||
|
// instead of focusing down
|
||||||
|
ts !== 0 &&
|
||||||
|
// only set focus to a known location
|
||||||
|
beacon?.latestLocationState?.uri
|
||||||
|
) {
|
||||||
|
// append custom `mxTs` parameter to geoUri
|
||||||
|
// so map is triggered to refocus on this uri
|
||||||
|
// event if it was previously the center geouri
|
||||||
|
// but the map have moved/zoomed
|
||||||
|
setCenterGeoUri(`${beacon?.latestLocationState?.uri};mxTs=${Date.now()}`);
|
||||||
|
setBounds(getBeaconBounds([beacon]));
|
||||||
|
}
|
||||||
|
}, [beacon, ts]);
|
||||||
|
|
||||||
|
return { bounds, centerGeoUri };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to view live beacons maximised
|
* Dialog to view live beacons maximised
|
||||||
*/
|
*/
|
||||||
const BeaconViewDialog: React.FC<IProps> = ({
|
const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
focusBeacon,
|
initialFocusedBeacon,
|
||||||
roomId,
|
roomId,
|
||||||
matrixClient,
|
matrixClient,
|
||||||
onFinished,
|
onFinished,
|
||||||
}) => {
|
}) => {
|
||||||
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
||||||
|
const [focusedBeaconState, setFocusedBeaconState] =
|
||||||
|
useState<FocusedBeaconState>({ beacon: initialFocusedBeacon, ts: 0 });
|
||||||
|
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
|
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusedBeaconState);
|
||||||
|
|
||||||
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
||||||
|
|
||||||
|
@ -94,6 +124,10 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}, [mapDisplayError]);
|
}, [mapDisplayError]);
|
||||||
|
|
||||||
|
const onBeaconListItemClick = (beacon: Beacon) => {
|
||||||
|
setFocusedBeaconState({ beacon, ts: Date.now() });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_BeaconViewDialog'
|
className='mx_BeaconViewDialog'
|
||||||
|
@ -144,7 +178,7 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
</MapFallback>
|
</MapFallback>
|
||||||
}
|
}
|
||||||
{ isSidebarOpen ?
|
{ isSidebarOpen ?
|
||||||
<DialogSidebar beacons={liveBeacons} requestClose={() => setSidebarOpen(false)} /> :
|
<DialogSidebar beacons={liveBeacons} onBeaconClick={onBeaconListItemClick} requestClose={() => setSidebarOpen(false)} /> :
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='primary'
|
kind='primary'
|
||||||
onClick={() => setSidebarOpen(true)}
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
|
|
@ -26,9 +26,14 @@ import BeaconListItem from './BeaconListItem';
|
||||||
interface Props {
|
interface Props {
|
||||||
beacons: Beacon[];
|
beacons: Beacon[];
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
onBeaconClick: (beacon: Beacon) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
const DialogSidebar: React.FC<Props> = ({
|
||||||
|
beacons,
|
||||||
|
onBeaconClick,
|
||||||
|
requestClose,
|
||||||
|
}) => {
|
||||||
return <div className='mx_DialogSidebar'>
|
return <div className='mx_DialogSidebar'>
|
||||||
<div className='mx_DialogSidebar_header'>
|
<div className='mx_DialogSidebar_header'>
|
||||||
<Heading size='h4'>{ _t('View List') }</Heading>
|
<Heading size='h4'>{ _t('View List') }</Heading>
|
||||||
|
@ -36,13 +41,17 @@ const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
||||||
className='mx_DialogSidebar_closeButton'
|
className='mx_DialogSidebar_closeButton'
|
||||||
onClick={requestClose}
|
onClick={requestClose}
|
||||||
title={_t('Close sidebar')}
|
title={_t('Close sidebar')}
|
||||||
data-test-id='dialog-sidebar-close'
|
data-testid='dialog-sidebar-close'
|
||||||
>
|
>
|
||||||
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<ol className='mx_DialogSidebar_list'>
|
<ol className='mx_DialogSidebar_list'>
|
||||||
{ beacons.map((beacon) => <BeaconListItem key={beacon.identifier} beacon={beacon} />) }
|
{ beacons.map((beacon) => <BeaconListItem
|
||||||
|
key={beacon.identifier}
|
||||||
|
beacon={beacon}
|
||||||
|
onClick={() => onBeaconClick(beacon)}
|
||||||
|
/>) }
|
||||||
</ol>
|
</ol>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React, { HTMLProps } from 'react';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
||||||
|
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||||
import BeaconStatus from './BeaconStatus';
|
import BeaconStatus from './BeaconStatus';
|
||||||
import { BeaconDisplayStatus } from './displayStatus';
|
import { BeaconDisplayStatus } from './displayStatus';
|
||||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
|
@ -45,14 +46,6 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
onResetLocationPublishError,
|
onResetLocationPublishError,
|
||||||
} = useOwnLiveBeacons([beacon?.identifier]);
|
} = useOwnLiveBeacons([beacon?.identifier]);
|
||||||
|
|
||||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
|
||||||
// moving under the beacon status on stop/retry click
|
|
||||||
const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => {
|
|
||||||
e?.stopPropagation();
|
|
||||||
e?.preventDefault();
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
// combine display status with errors that only occur for user's own beacons
|
// combine display status with errors that only occur for user's own beacons
|
||||||
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
||||||
BeaconDisplayStatus.Error :
|
BeaconDisplayStatus.Error :
|
||||||
|
@ -68,7 +61,9 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
||||||
data-test-id='beacon-status-stop-beacon'
|
data-test-id='beacon-status-stop-beacon'
|
||||||
kind='link'
|
kind='link'
|
||||||
onClick={preventDefaultWrapper(onStopSharing)}
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
|
onClick={preventDefaultWrapper<ButtonEvent>(onStopSharing)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
disabled={stoppingInProgress}
|
disabled={stoppingInProgress}
|
||||||
>
|
>
|
||||||
|
@ -78,6 +73,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ hasLocationPublishError && <AccessibleButton
|
{ hasLocationPublishError && <AccessibleButton
|
||||||
data-test-id='beacon-status-reset-wire-error'
|
data-test-id='beacon-status-reset-wire-error'
|
||||||
kind='link'
|
kind='link'
|
||||||
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
>
|
>
|
||||||
|
@ -87,6 +84,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ hasStopSharingError && <AccessibleButton
|
{ hasStopSharingError && <AccessibleButton
|
||||||
data-test-id='beacon-status-stop-beacon-retry'
|
data-test-id='beacon-status-stop-beacon-retry'
|
||||||
kind='link'
|
kind='link'
|
||||||
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
onClick={preventDefaultWrapper(onStopSharing)}
|
onClick={preventDefaultWrapper(onStopSharing)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
>
|
>
|
||||||
|
|
|
@ -80,6 +80,13 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) =>
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
id: string;
|
id: string;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
|
/**
|
||||||
|
* set map center to geoUri coords
|
||||||
|
* Center will only be set to valid geoUri
|
||||||
|
* this prop is only simply diffed by useEffect, so to trigger *recentering* of the same geoUri
|
||||||
|
* append the uri with a var not used by the geoUri spec
|
||||||
|
* eg a timestamp: `geo:54,42;mxTs=123`
|
||||||
|
*/
|
||||||
centerGeoUri?: string;
|
centerGeoUri?: string;
|
||||||
bounds?: Bounds;
|
bounds?: Bounds;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -162,7 +162,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
|
||||||
{
|
{
|
||||||
roomId: mxEvent.getRoomId(),
|
roomId: mxEvent.getRoomId(),
|
||||||
matrixClient,
|
matrixClient,
|
||||||
focusBeacon: beacon,
|
initialFocusedBeacon: beacon,
|
||||||
isMapDisplayError,
|
isMapDisplayError,
|
||||||
},
|
},
|
||||||
"mx_BeaconViewDialog_wrapper",
|
"mx_BeaconViewDialog_wrapper",
|
||||||
|
|
25
src/utils/NativeEventUtils.ts
Normal file
25
src/utils/NativeEventUtils.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// Wrap DOM event handlers with stopPropagation and preventDefault
|
||||||
|
export const preventDefaultWrapper =
|
||||||
|
<T extends React.BaseSyntheticEvent = React.BaseSyntheticEvent>(callback: () => void) => (e?: T) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
e?.preventDefault();
|
||||||
|
callback();
|
||||||
|
};
|
|
@ -35,7 +35,7 @@ export const useMap = ({
|
||||||
interactive,
|
interactive,
|
||||||
bodyId,
|
bodyId,
|
||||||
onError,
|
onError,
|
||||||
}: UseMapProps): MapLibreMap => {
|
}: UseMapProps): MapLibreMap | undefined => {
|
||||||
const [map, setMap] = useState<MapLibreMap>();
|
const [map, setMap] = useState<MapLibreMap>();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { act } from 'react-dom/test-utils';
|
||||||
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
import {
|
import {
|
||||||
|
findByTestId,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconEvent,
|
makeBeaconEvent,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
|
@ -169,5 +170,30 @@ describe('<BeaconListItem />', () => {
|
||||||
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('interactions', () => {
|
||||||
|
it('does not call onClick handler when clicking share button', () => {
|
||||||
|
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const component = getComponent({ beacon, onClick });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(component, 'open-location-in-osm').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
expect(onClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onClick handler when clicking outside of share buttons', () => {
|
||||||
|
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const component = getComponent({ beacon, onClick });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click the beacon name
|
||||||
|
component.find('.mx_BeaconStatus_description').simulate('click');
|
||||||
|
});
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import {
|
import {
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
|
@ -28,15 +28,18 @@ import maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
|
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
|
||||||
import {
|
import {
|
||||||
|
findByAttr,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconEvent,
|
makeBeaconEvent,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
|
makeRoomWithBeacons,
|
||||||
makeRoomWithStateEvents,
|
makeRoomWithStateEvents,
|
||||||
} from '../../../test-utils';
|
} from '../../../test-utils';
|
||||||
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
|
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
|
||||||
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
||||||
import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus';
|
import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus';
|
||||||
|
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||||
|
|
||||||
describe('<BeaconViewDialog />', () => {
|
describe('<BeaconViewDialog />', () => {
|
||||||
// 14.03.2022 16:15
|
// 14.03.2022 16:15
|
||||||
|
@ -89,13 +92,18 @@ describe('<BeaconViewDialog />', () => {
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
mount(<BeaconViewDialog {...defaultProps} {...props} />);
|
mount(<BeaconViewDialog {...defaultProps} {...props} />);
|
||||||
|
|
||||||
|
const openSidebar = (component: ReactWrapper) => act(() => {
|
||||||
|
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
maplibregl.AttributionControl = jest.fn();
|
maplibregl.AttributionControl = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore();
|
jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore();
|
||||||
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -225,10 +233,7 @@ describe('<BeaconViewDialog />', () => {
|
||||||
beacon.addLocations([location1]);
|
beacon.addLocations([location1]);
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
|
||||||
act(() => {
|
openSidebar(component);
|
||||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
|
||||||
component.setProps({});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -240,20 +245,134 @@ describe('<BeaconViewDialog />', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
|
||||||
// open the sidebar
|
// open the sidebar
|
||||||
act(() => {
|
openSidebar(component);
|
||||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
|
||||||
component.setProps({});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||||
|
|
||||||
// now close it
|
// now close it
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
findByAttr('data-testid')(component, 'dialog-sidebar-close').at(0).simulate('click');
|
||||||
component.setProps({});
|
component.setProps({});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeFalsy();
|
expect(component.find('DialogSidebar').length).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('focused beacons', () => {
|
||||||
|
const beacon2Event = makeBeaconInfoEvent(bobId,
|
||||||
|
roomId,
|
||||||
|
{ isLive: true },
|
||||||
|
'$bob-room1-2',
|
||||||
|
);
|
||||||
|
|
||||||
|
const location2 = makeBeaconEvent(
|
||||||
|
bobId, { beaconInfoId: beacon2Event.getId(), geoUri: 'geo:33,22', timestamp: now + 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const fitBoundsOptions = { maxZoom: 15, padding: 100 };
|
||||||
|
|
||||||
|
it('opens map with both beacons in view on first load without initialFocusedBeacon', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// start centered on mid point between both beacons
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 42, lon: 31.5 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fit both beacons, only called once
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens map with both beacons in view on first load with an initially focused beacon', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
getComponent({ beacons: [beacon1, beacon2], initialFocusedBeacon: beacon1 });
|
||||||
|
|
||||||
|
// start centered on initialFocusedBeacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fit both beacons, only called once
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses on beacon location on sidebar list item click', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// reset call counts on map mocks after initial render
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
openSidebar(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the first beacon in the list
|
||||||
|
component.find(BeaconListItem).at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// centered on clicked beacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fitted just to clicked beacon
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[41, 51], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refocuses on same beacon when clicking list item again', () => {
|
||||||
|
// test the map responds to refocusing the same beacon
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// reset call counts on map mocks after initial render
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
openSidebar(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the second beacon in the list
|
||||||
|
component.find(BeaconListItem).at(1).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedBounds = new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [22, 33],
|
||||||
|
);
|
||||||
|
|
||||||
|
// date is mocked but this relies on timestamp, manually mock a tick
|
||||||
|
jest.spyOn(global.Date, 'now').mockReturnValue(now + 1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the second beacon in the list
|
||||||
|
component.find(BeaconListItem).at(1).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// centered on clicked beacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 33, lon: 22 });
|
||||||
|
// bounds fitted just to clicked beacon
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(expectedBounds, fitBoundsOptions);
|
||||||
|
// each called once per click
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,31 +15,88 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar';
|
import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar';
|
||||||
import { findByTestId } from '../../../test-utils';
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
makeBeaconEvent,
|
||||||
|
makeBeaconInfoEvent,
|
||||||
|
makeRoomWithBeacons,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from '../../../test-utils';
|
||||||
|
|
||||||
describe('<DialogSidebar />', () => {
|
describe('<DialogSidebar />', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
beacons: [],
|
beacons: [],
|
||||||
requestClose: jest.fn(),
|
requestClose: jest.fn(),
|
||||||
|
onBeaconClick: jest.fn(),
|
||||||
};
|
};
|
||||||
const getComponent = (props = {}) =>
|
|
||||||
mount(<DialogSidebar {...defaultProps} {...props} />);
|
|
||||||
|
|
||||||
it('renders sidebar correctly', () => {
|
const now = 1647270879403;
|
||||||
const component = getComponent();
|
|
||||||
expect(component).toMatchSnapshot();
|
const roomId = '!room:server.org';
|
||||||
|
const aliceId = '@alice:server.org';
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(aliceId),
|
||||||
|
getRoom: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const beaconEvent = makeBeaconInfoEvent(aliceId,
|
||||||
|
roomId,
|
||||||
|
{ isLive: true, timestamp: now },
|
||||||
|
'$alice-room1-1',
|
||||||
|
);
|
||||||
|
const location1 = makeBeaconEvent(
|
||||||
|
aliceId, { beaconInfoId: beaconEvent.getId(), geoUri: 'geo:51,41', timestamp: now },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getComponent = (props = {}) => (
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<DialogSidebar {...defaultProps} {...props} />);
|
||||||
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// mock now so time based text in snapshots is stable
|
||||||
|
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.spyOn(Date, 'now').mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders sidebar correctly without beacons', () => {
|
||||||
|
const { container } = render(getComponent());
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders sidebar correctly with beacons', () => {
|
||||||
|
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||||
|
const { container } = render(getComponent({ beacons: [beacon] }));
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls on beacon click', () => {
|
||||||
|
const onBeaconClick = jest.fn();
|
||||||
|
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||||
|
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
const [listItem] = container.getElementsByClassName('mx_BeaconListItem');
|
||||||
|
fireEvent.click(listItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onBeaconClick).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes on close button click', () => {
|
it('closes on close button click', () => {
|
||||||
const requestClose = jest.fn();
|
const requestClose = jest.fn();
|
||||||
const component = getComponent({ requestClose });
|
const { getByTestId } = render(getComponent({ requestClose }));
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
fireEvent.click(getByTestId('dialog-sidebar-close'));
|
||||||
});
|
});
|
||||||
expect(requestClose).toHaveBeenCalled();
|
expect(requestClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div class=\\"mx_BeaconListItem_interactions\\"><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
||||||
|
|
|
@ -1,53 +1,144 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<DialogSidebar /> renders sidebar correctly 1`] = `
|
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||||
<DialogSidebar
|
<div>
|
||||||
beacons={Array []}
|
<div
|
||||||
requestClose={[MockFunction]}
|
class="mx_DialogSidebar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_DialogSidebar"
|
class="mx_DialogSidebar_header"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_DialogSidebar_header"
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
size="h4"
|
|
||||||
>
|
>
|
||||||
<h4
|
<h4
|
||||||
className="mx_Heading_h4"
|
class="mx_Heading_h4"
|
||||||
>
|
>
|
||||||
View List
|
View List
|
||||||
</h4>
|
</h4>
|
||||||
</Heading>
|
<div
|
||||||
<AccessibleButton
|
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||||
className="mx_DialogSidebar_closeButton"
|
data-testid="dialog-sidebar-close"
|
||||||
data-test-id="dialog-sidebar-close"
|
|
||||||
element="div"
|
|
||||||
onClick={[MockFunction]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabindex="0"
|
||||||
title="Close sidebar"
|
title="Close sidebar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
class="mx_DialogSidebar_closeButtonIcon"
|
||||||
data-test-id="dialog-sidebar-close"
|
|
||||||
onClick={[MockFunction]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
title="Close sidebar"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_DialogSidebar_closeButtonIcon"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
<ol
|
<ol
|
||||||
className="mx_DialogSidebar_list"
|
class="mx_DialogSidebar_list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="mx_BeaconListItem"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar mx_BeaconListItem_avatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
src=""
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconListItem_info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconStatus_description"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconStatus_label"
|
||||||
|
>
|
||||||
|
@alice:server.org
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconStatus_expiryTime"
|
||||||
|
>
|
||||||
|
Live until 16:14
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconListItem_interactions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
data-test-id="open-location-in-osm"
|
||||||
|
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_ShareLatestLocation_icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogSidebar>
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconListItem_lastUpdated"
|
||||||
|
>
|
||||||
|
Updated a few seconds ago
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar_header"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="mx_Heading_h4"
|
||||||
|
>
|
||||||
|
View List
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||||
|
data-testid="dialog-sidebar-close"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Close sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar_closeButtonIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol
|
||||||
|
class="mx_DialogSidebar_list"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -205,7 +205,11 @@ export const makeRoomWithBeacons = (
|
||||||
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
|
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
|
||||||
const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event)));
|
const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event)));
|
||||||
if (locationEvents) {
|
if (locationEvents) {
|
||||||
beacons.forEach(beacon => beacon.addLocations(locationEvents));
|
beacons.forEach(beacon => {
|
||||||
|
// this filtering happens in roomState, which is bypassed here
|
||||||
|
const validLocationEvents = locationEvents?.filter(event => event.getSender() === beacon.beaconInfoOwner);
|
||||||
|
beacon.addLocations(validLocationEvents);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return beacons;
|
return beacons;
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,8 @@ export function untilDispatch(waitForAction: DispatcherAction): Promise<ActionPa
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const findByAttr = (attr: string) => (component: ReactWrapper, value: string) => component.find(`[${attr}="${value}"]`);
|
export const findByAttr = (attr: string) => (component: ReactWrapper, value: string) =>
|
||||||
|
component.find(`[${attr}="${value}"]`);
|
||||||
export const findByTestId = findByAttr('data-test-id');
|
export const findByTestId = findByAttr('data-test-id');
|
||||||
export const findById = findByAttr('id');
|
export const findById = findByAttr('id');
|
||||||
export const findByAriaLabel = findByAttr('aria-label');
|
export const findByAriaLabel = findByAttr('aria-label');
|
||||||
|
|
Loading…
Reference in a new issue