Live location share - tiles without tile server (PSG-591) (#8962)

* live location without map POC

* styles

* force map tiles to show no map for test build

* check latestlocationstate exists

* just use loading style map fallback when cant display map

* style map error for tile view

* set pointer cursor when map error is clickable

* test mbeaconbody with map display error, lint

* lint more good

* remove changes for first attempt tile

* make maperror test id more accurate

* fussy import ordering

* PR tweaks
pull/28788/head^2
Kerry 2022-07-06 16:34:33 +02:00 committed by GitHub
parent e65409861a
commit 60faf6d025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 430 additions and 211 deletions

View File

@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_OwnBeaconStatus_button {
margin-left: $spacing-8;
}
.mx_EventTile[data-layout="bubble"] .mx_OwnBeaconStatus_button { .mx_EventTile[data-layout="bubble"] .mx_OwnBeaconStatus_button {
// align to top to make room for timestamp // align to top to make room for timestamp
// in bubble view // in bubble view

View File

@ -18,9 +18,41 @@ limitations under the License.
padding: 100px $spacing-32 0; padding: 100px $spacing-32 0;
text-align: center; text-align: center;
p { --mx-map-error-icon-color: $secondary-content;
margin: $spacing-16 0 $spacing-32; --mx-map-error-icon-size: 58px;
}
.mx_MapError.mx_MapError_isMinimised {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: $spacing-24;
background-color: $panels;
font-size: $font-12px;
line-height: $font-16px;
--mx-map-error-icon-color: $alert;
--mx-map-error-icon-size: 26px;
.mx_MapError_message {
margin: 0;
max-width: 275px;
} }
.mx_MapError_heading {
padding-top: $spacing-8;
// override h3 heading size
font-size: inherit !important;
font-weight: normal !important;
}
}
.mx_MapError_message {
margin: $spacing-16 0 $spacing-32;
} }
.mx_MapError_heading { .mx_MapError_heading {
@ -28,9 +60,9 @@ limitations under the License.
} }
.mx_MapError_icon { .mx_MapError_icon {
height: 58px; height: var(--mx-map-error-icon-size);
path { path {
fill: $secondary-content; fill: var(--mx-map-error-icon-color);
} }
} }

View File

@ -24,6 +24,29 @@ limitations under the License.
overflow: hidden; overflow: hidden;
} }
.mx_MBeaconBody.mx_MBeaconBody_withoutMap {
height: auto;
.mx_MBeaconBody_chin {
position: relative;
background-color: transparent;
}
}
.mx_MBeaconBody_withoutMapContent {
background-color: $panels;
border-radius: 4px;
}
.mx_MBeaconBody_withoutMapInfoLastUpdated {
// 48px lines up with icon in BeaconStatus
margin-top: -$spacing-8;
padding: 0 $spacing-8 $spacing-8 48px;
color: $tertiary-content;
font-size: $font-10px;
}
.mx_MBeaconBody_map { .mx_MBeaconBody_map {
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -32,11 +55,18 @@ limitations under the License.
cursor: pointer; cursor: pointer;
} }
.mx_MBeaconBody_mapFallback { .mx_MBeaconBody_mapFallback,
.mx_MBeaconBody_mapError {
// pushes spinner/icon up // pushes spinner/icon up
// to appear more centered with the footer // to appear more centered with the footer
padding-bottom: 50px; padding-bottom: 50px !important;
}
.mx_MBeaconBody_mapErrorInteractive {
cursor: pointer;
}
.mx_MBeaconBody_mapFallback {
cursor: default; cursor: default;
} }

View File

@ -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 } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { import {
Beacon, Beacon,
@ -38,6 +38,8 @@ import DialogSidebar from './DialogSidebar';
import DialogOwnBeaconStatus from './DialogOwnBeaconStatus'; import DialogOwnBeaconStatus from './DialogOwnBeaconStatus';
import BeaconStatusTooltip from './BeaconStatusTooltip'; import BeaconStatusTooltip from './BeaconStatusTooltip';
import MapFallback from '../location/MapFallback'; import MapFallback from '../location/MapFallback';
import { MapError } from '../location/MapError';
import { LocationShareError } from '../../../utils/location';
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
roomId: Room['roomId']; roomId: Room['roomId'];
@ -83,6 +85,15 @@ const BeaconViewDialog: React.FC<IProps> = ({
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon); const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
const [mapDisplayError, setMapDisplayError] = useState<Error>();
// automatically open the sidebar if there is no map to see
useEffect(() => {
if (mapDisplayError) {
setSidebarOpen(true);
}
}, [mapDisplayError]);
return ( return (
<BaseDialog <BaseDialog
className='mx_BeaconViewDialog' className='mx_BeaconViewDialog'
@ -90,11 +101,12 @@ const BeaconViewDialog: React.FC<IProps> = ({
fixedWidth={false} fixedWidth={false}
> >
<MatrixClientContext.Provider value={matrixClient}> <MatrixClientContext.Provider value={matrixClient}>
{ !!liveBeacons?.length ? <Map { (!!liveBeacons?.length && !mapDisplayError) && <Map
id='mx_BeaconViewDialog' id='mx_BeaconViewDialog'
bounds={bounds} bounds={bounds}
centerGeoUri={centerGeoUri} centerGeoUri={centerGeoUri}
interactive interactive
onError={setMapDisplayError}
className="mx_BeaconViewDialog_map" className="mx_BeaconViewDialog_map"
> >
{ {
@ -109,7 +121,14 @@ const BeaconViewDialog: React.FC<IProps> = ({
<ZoomButtons map={map} /> <ZoomButtons map={map} />
</> </>
} }
</Map> : </Map> }
{ mapDisplayError &&
<MapError
error={mapDisplayError.message as LocationShareError}
isMinimised
/>
}
{ !liveBeacons?.length && !mapDisplayError &&
<MapFallback <MapFallback
data-test-id='beacon-view-dialog-map-fallback' data-test-id='beacon-view-dialog-map-fallback'
className='mx_BeaconViewDialog_map' className='mx_BeaconViewDialog_map'

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import classNames from 'classnames';
import { Icon as WarningBadge } from '../../../../res/img/element-icons/warning-badge.svg'; import { Icon as WarningBadge } from '../../../../res/img/element-icons/warning-badge.svg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -22,18 +23,38 @@ import { getLocationShareErrorMessage, LocationShareError } from '../../../utils
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import Heading from '../typography/Heading'; import Heading from '../typography/Heading';
interface Props { export interface MapErrorProps {
onFinished: () => void;
error: LocationShareError; error: LocationShareError;
onFinished?: () => void;
isMinimised?: boolean;
className?: string;
onClick?: () => void;
} }
export const MapError: React.FC<Props> = ({ export const MapError: React.FC<MapErrorProps> = ({
onFinished, error, error,
}) => (<div data-test-id='location-picker-error' className="mx_MapError"> isMinimised,
<WarningBadge className="mx_MapError_icon" /> className,
<Heading className="mx_MapError_heading" size='h3'>{ _t("Unable to load map") }</Heading> onFinished,
<p> onClick,
{ getLocationShareErrorMessage(error) } }) => (
</p> <div data-test-id='map-rendering-error'
<AccessibleButton element='button' kind="primary" onClick={onFinished}>{ _t("OK") }</AccessibleButton> className={classNames('mx_MapError', className, { 'mx_MapError_isMinimised': isMinimised })}
</div>); onClick={onClick}
>
<WarningBadge className='mx_MapError_icon' />
<Heading className='mx_MapError_heading' size='h3'>{ _t('Unable to load map') }</Heading>
<p className='mx_MapError_message'>
{ getLocationShareErrorMessage(error) }
</p>
{ onFinished &&
<AccessibleButton
element='button'
kind='primary'
onClick={onFinished}
>
{ _t('OK') }
</AccessibleButton>
}
</div>
);

View File

@ -30,7 +30,6 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
const MapFallback: React.FC<Props> = ({ className, isLoading, children, ...rest }) => { const MapFallback: React.FC<Props> = ({ className, isLoading, children, ...rest }) => {
return <div className={classNames('mx_MapFallback', className)} {...rest}> return <div className={classNames('mx_MapFallback', className)} {...rest}>
<MapFallbackImage className='mx_MapFallback_bg' /> <MapFallbackImage className='mx_MapFallback_bg' />
{ /* <div className='mx_MapFallback_bg'/> */ }
{ isLoading ? <Spinner h={32} w={32} /> : <LocationMarkerIcon className='mx_MapFallback_icon' /> } { isLoading ? <Spinner h={32} w={32} /> : <LocationMarkerIcon className='mx_MapFallback_icon' /> }
{ children } { children }
</div>; </div>;

View File

@ -26,17 +26,19 @@ import {
import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers'; import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers';
import { randomString } from 'matrix-js-sdk/src/randomstring'; import { randomString } from 'matrix-js-sdk/src/randomstring';
import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon'; import { M_BEACON } from 'matrix-js-sdk/src/@types/beacon';
import classNames from 'classnames';
import MatrixClientContext from '../../../contexts/MatrixClientContext'; import MatrixClientContext from '../../../contexts/MatrixClientContext';
import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { useEventEmitterState } from '../../../hooks/useEventEmitter';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { isBeaconWaitingToStart, useBeacon } from '../../../utils/beacon'; import { isBeaconWaitingToStart, useBeacon } from '../../../utils/beacon';
import { isSelfLocation } from '../../../utils/location'; import { isSelfLocation, LocationShareError } from '../../../utils/location';
import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus'; import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus';
import BeaconStatus from '../beacon/BeaconStatus'; import BeaconStatus from '../beacon/BeaconStatus';
import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; import OwnBeaconStatus from '../beacon/OwnBeaconStatus';
import Map from '../location/Map'; import Map from '../location/Map';
import { MapError } from '../location/MapError';
import MapFallback from '../location/MapFallback'; import MapFallback from '../location/MapFallback';
import SmartMarker from '../location/SmartMarker'; import SmartMarker from '../location/SmartMarker';
import { GetRelationsForEvent } from '../rooms/EventTile'; import { GetRelationsForEvent } from '../rooms/EventTile';
@ -136,7 +138,16 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
const matrixClient = useContext(MatrixClientContext); const matrixClient = useContext(MatrixClientContext);
const [error, setError] = useState<Error>(); const [error, setError] = useState<Error>();
const displayStatus = getBeaconDisplayStatus(isLive, latestLocationState, error, waitingToStart); const isMapDisplayError = error?.message === LocationShareError.MapStyleUrlNotConfigured ||
error?.message === LocationShareError.MapStyleUrlNotReachable;
const displayStatus = getBeaconDisplayStatus(
isLive,
latestLocationState,
// if we are unable to display maps because it is not configured for the server
// don't display an error
isMapDisplayError ? undefined : error,
waitingToStart,
);
const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined; const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined;
const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId(); const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId();
@ -152,6 +163,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
roomId: mxEvent.getRoomId(), roomId: mxEvent.getRoomId(),
matrixClient, matrixClient,
focusBeacon: beacon, focusBeacon: beacon,
isMapDisplayError,
}, },
"mx_BeaconViewDialog_wrapper", "mx_BeaconViewDialog_wrapper",
false, // isPriority false, // isPriority
@ -160,8 +172,11 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
}; };
return ( return (
<div className='mx_MBeaconBody' ref={ref}> <div
{ displayStatus === BeaconDisplayStatus.Active ? className='mx_MBeaconBody'
ref={ref}
>
{ (displayStatus === BeaconDisplayStatus.Active && !isMapDisplayError) ?
<Map <Map
id={mapId} id={mapId}
centerGeoUri={latestLocationState.uri} centerGeoUri={latestLocationState.uri}
@ -180,10 +195,23 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
/> />
} }
</Map> </Map>
: <MapFallback : isMapDisplayError ?
isLoading={displayStatus === BeaconDisplayStatus.Loading} <MapError
className='mx_MBeaconBody_map mx_MBeaconBody_mapFallback' error={error.message as LocationShareError}
/> onClick={onClick}
className={classNames(
'mx_MBeaconBody_mapError',
// set interactive class when maximised map can be opened
{ 'mx_MBeaconBody_mapErrorInteractive':
displayStatus === BeaconDisplayStatus.Active,
},
)}
isMinimised
/> :
<MapFallback
isLoading={displayStatus === BeaconDisplayStatus.Loading}
className='mx_MBeaconBody_map mx_MBeaconBody_mapFallback'
/>
} }
{ isOwnBeacon ? { isOwnBeacon ?
<OwnBeaconStatus <OwnBeaconStatus

View File

@ -99,7 +99,7 @@ describe("LocationPicker", () => {
wrapper.setProps({}); wrapper.setProps({});
}); });
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual( expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured correctly to display maps, " "This homeserver is not configured correctly to display maps, "
+ "or the configured map server may be unreachable.", + "or the configured map server may be unreachable.",
); );
@ -115,7 +115,7 @@ describe("LocationPicker", () => {
const wrapper = getComponent(); const wrapper = getComponent();
wrapper.setProps({}); wrapper.setProps({});
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual( expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured to display maps.", "This homeserver is not configured to display maps.",
); );
}); });
@ -130,7 +130,7 @@ describe("LocationPicker", () => {
const wrapper = getComponent(); const wrapper = getComponent();
wrapper.setProps({}); wrapper.setProps({});
expect(findByTestId(wrapper, 'location-picker-error').find('p').text()).toEqual( expect(findByTestId(wrapper, 'map-rendering-error').find('p').text()).toEqual(
"This homeserver is not configured correctly to display maps, " "This homeserver is not configured correctly to display maps, "
+ "or the configured map server may be unreachable.", + "or the configured map server may be unreachable.",
); );

View File

@ -15,28 +15,45 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { render, RenderResult } from '@testing-library/react';
import { MapError } from '../../../../src/components/views/location/MapError'; import { MapError, MapErrorProps } from '../../../../src/components/views/location/MapError';
import { LocationShareError } from '../../../../src/utils/location'; import { LocationShareError } from '../../../../src/utils/location';
describe('<MapError />', () => { describe('<MapError />', () => {
const defaultProps = { const defaultProps = {
onFinished: jest.fn(), onFinished: jest.fn(),
error: LocationShareError.MapStyleUrlNotConfigured, error: LocationShareError.MapStyleUrlNotConfigured,
className: 'test',
}; };
const getComponent = (props = {}) => const getComponent = (props: Partial<MapErrorProps> = {}): RenderResult =>
mount(<MapError {...defaultProps} {...props} />); render(<MapError {...defaultProps} {...props} />);
it('renders correctly for MapStyleUrlNotConfigured', () => { it('renders correctly for MapStyleUrlNotConfigured', () => {
const component = getComponent(); const { container } = getComponent();
expect(component).toMatchSnapshot(); expect(container).toMatchSnapshot();
}); });
it('renders correctly for MapStyleUrlNotReachable', () => { it('renders correctly for MapStyleUrlNotReachable', () => {
const component = getComponent({ const { container } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable, error: LocationShareError.MapStyleUrlNotReachable,
}); });
expect(component).toMatchSnapshot(); expect(container).toMatchSnapshot();
});
it('does not render button when onFinished falsy', () => {
const { queryByText } = getComponent({
error: LocationShareError.MapStyleUrlNotReachable,
onFinished: undefined,
});
// no button
expect(queryByText('OK')).toBeFalsy();
});
it('applies class when isMinimised is truthy', () => {
const { container } = getComponent({
isMinimised: true,
});
expect(container).toMatchSnapshot();
}); });
}); });

View File

@ -1,95 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = ` exports[`<MapError /> applies class when isMinimised is truthy 1`] = `
<MapError <div>
error="MapStyleUrlNotConfigured"
onFinished={[MockFunction]}
>
<div <div
className="mx_MapError" class="mx_MapError test mx_MapError_isMinimised"
data-test-id="location-picker-error" data-test-id="map-rendering-error"
> >
<div <div
className="mx_MapError_icon" class="mx_MapError_icon"
/> />
<Heading <h3
className="mx_MapError_heading" class="mx_Heading_h3 mx_MapError_heading"
size="h3" >
Unable to load map
</h3>
<p
class="mx_MapError_message"
> >
<h3
className="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p>
This homeserver is not configured to display maps. This homeserver is not configured to display maps.
</p> </p>
<AccessibleButton <button
element="button" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
kind="primary"
onClick={[MockFunction]}
role="button" role="button"
tabIndex={0} tabindex="0"
> >
<button OK
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" </button>
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
>
OK
</button>
</AccessibleButton>
</div> </div>
</MapError> </div>
`;
exports[`<MapError /> renders correctly for MapStyleUrlNotConfigured 1`] = `
<div>
<div
class="mx_MapError test"
data-test-id="map-rendering-error"
>
<div
class="mx_MapError_icon"
/>
<h3
class="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
<p
class="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
<button
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
OK
</button>
</div>
</div>
`; `;
exports[`<MapError /> renders correctly for MapStyleUrlNotReachable 1`] = ` exports[`<MapError /> renders correctly for MapStyleUrlNotReachable 1`] = `
<MapError <div>
error="MapStyleUrlNotReachable"
onFinished={[MockFunction]}
>
<div <div
className="mx_MapError" class="mx_MapError test"
data-test-id="location-picker-error" data-test-id="map-rendering-error"
> >
<div <div
className="mx_MapError_icon" class="mx_MapError_icon"
/> />
<Heading <h3
className="mx_MapError_heading" class="mx_Heading_h3 mx_MapError_heading"
size="h3" >
Unable to load map
</h3>
<p
class="mx_MapError_message"
> >
<h3
className="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p>
This homeserver is not configured correctly to display maps, or the configured map server may be unreachable. This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.
</p> </p>
<AccessibleButton <button
element="button" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
kind="primary"
onClick={[MockFunction]}
role="button" role="button"
tabIndex={0} tabindex="0"
> >
<button OK
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary" </button>
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
>
OK
</button>
</AccessibleButton>
</div> </div>
</MapError> </div>
`; `;

View File

@ -33,6 +33,7 @@ import {
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
makeBeaconEvent, makeBeaconEvent,
makeBeaconInfoEvent, makeBeaconInfoEvent,
makeRoomWithBeacons,
makeRoomWithStateEvents, makeRoomWithStateEvents,
} from '../../../test-utils'; } from '../../../test-utils';
import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks'; import { RoomPermalinkCreator } from '../../../../src/utils/permalinks/Permalinks';
@ -40,6 +41,9 @@ import { MediaEventHelper } from '../../../../src/utils/MediaEventHelper';
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
import Modal from '../../../../src/Modal'; import Modal from '../../../../src/Modal';
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
import { MapError } from '../../../../src/components/views/location/MapError';
import * as mapUtilHooks from '../../../../src/utils/location/useMap';
import { LocationShareError } from '../../../../src/utils/location';
describe('<MBeaconBody />', () => { describe('<MBeaconBody />', () => {
// 14.03.2022 16:15 // 14.03.2022 16:15
@ -94,112 +98,116 @@ describe('<MBeaconBody />', () => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('renders stopped beacon UI for an explicitly stopped beacon', () => { const testBeaconStatuses = () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId, it('renders stopped beacon UI for an explicitly stopped beacon', () => {
roomId, const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
{ isLive: false }, roomId,
'$alice-room1-1', { isLive: false },
); '$alice-room1-1',
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient }); );
const component = getComponent({ mxEvent: beaconInfoEvent }); makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
expect(component.text()).toEqual("Live location ended"); const component = getComponent({ mxEvent: beaconInfoEvent });
}); expect(component.text()).toEqual("Live location ended");
it('renders stopped beacon UI for an expired beacon', () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
// puts this beacons live period in the past
{ isLive: true, timestamp: now - 600000, timeout: 500 },
'$alice-room1-1',
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Live location ended");
});
it('renders loading beacon UI for a beacon that has not started yet', () => {
const beaconInfoEvent = makeBeaconInfoEvent(
aliceId,
roomId,
// puts this beacons start timestamp in the future
{ isLive: true, timestamp: now + 60000, timeout: 500 },
'$alice-room1-1',
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Loading live location...");
});
it('does not open maximised map when on click when beacon is stopped', () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
// puts this beacons live period in the past
{ isLive: true, timestamp: now - 600000, timeout: 500 },
'$alice-room1-1',
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
act(() => {
component.find('.mx_MBeaconBody_map').at(0).simulate('click');
}); });
expect(modalSpy).not.toHaveBeenCalled(); it('renders stopped beacon UI for an expired beacon', () => {
}); const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
it('renders stopped UI when a beacon event is not the latest beacon for a user', () => { // puts this beacons live period in the past
const aliceBeaconInfo1 = makeBeaconInfoEvent( { isLive: true, timestamp: now - 600000, timeout: 500 },
aliceId, '$alice-room1-1',
roomId, );
// this one is a little older makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
{ isLive: true, timestamp: now - 500 }, const component = getComponent({ mxEvent: beaconInfoEvent });
'$alice-room1-1', expect(component.text()).toEqual("Live location ended");
);
aliceBeaconInfo1.event.origin_server_ts = now - 500;
const aliceBeaconInfo2 = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true },
'$alice-room1-2',
);
makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
});
it('renders stopped UI when a beacon event is replaced', () => {
const aliceBeaconInfo1 = makeBeaconInfoEvent(
aliceId,
roomId,
// this one is a little older
{ isLive: true, timestamp: now - 500 },
'$alice-room1-1',
);
aliceBeaconInfo1.event.origin_server_ts = now - 500;
const aliceBeaconInfo2 = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true },
'$alice-room1-2',
);
const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo1));
// update alice's beacon with a new edition
// beacon instance emits
act(() => {
beaconInstance.update(aliceBeaconInfo2);
}); });
component.setProps({}); it('renders loading beacon UI for a beacon that has not started yet', () => {
const beaconInfoEvent = makeBeaconInfoEvent(
aliceId,
roomId,
// puts this beacons start timestamp in the future
{ isLive: true, timestamp: now + 60000, timeout: 500 },
'$alice-room1-1',
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.text()).toEqual("Loading live location...");
});
// beacon1 has been superceded by beacon2 it('does not open maximised map when on click when beacon is stopped', () => {
expect(component.text()).toEqual("Live location ended"); const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
}); roomId,
// puts this beacons live period in the past
{ isLive: true, timestamp: now - 600000, timeout: 500 },
'$alice-room1-1',
);
makeRoomWithStateEvents([beaconInfoEvent], { roomId, mockClient });
const component = getComponent({ mxEvent: beaconInfoEvent });
act(() => {
component.find('.mx_MBeaconBody_map').at(0).simulate('click');
});
expect(modalSpy).not.toHaveBeenCalled();
});
it('renders stopped UI when a beacon event is not the latest beacon for a user', () => {
const aliceBeaconInfo1 = makeBeaconInfoEvent(
aliceId,
roomId,
// this one is a little older
{ isLive: true, timestamp: now - 500 },
'$alice-room1-1',
);
aliceBeaconInfo1.event.origin_server_ts = now - 500;
const aliceBeaconInfo2 = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true },
'$alice-room1-2',
);
makeRoomWithStateEvents([aliceBeaconInfo1, aliceBeaconInfo2], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
});
it('renders stopped UI when a beacon event is replaced', () => {
const aliceBeaconInfo1 = makeBeaconInfoEvent(
aliceId,
roomId,
// this one is a little older
{ isLive: true, timestamp: now - 500 },
'$alice-room1-1',
);
aliceBeaconInfo1.event.origin_server_ts = now - 500;
const aliceBeaconInfo2 = makeBeaconInfoEvent(
aliceId,
roomId,
{ isLive: true },
'$alice-room1-2',
);
const room = makeRoomWithStateEvents([aliceBeaconInfo1], { roomId, mockClient });
const component = getComponent({ mxEvent: aliceBeaconInfo1 });
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo1));
// update alice's beacon with a new edition
// beacon instance emits
act(() => {
beaconInstance.update(aliceBeaconInfo2);
});
component.setProps({});
// beacon1 has been superceded by beacon2
expect(component.text()).toEqual("Live location ended");
});
};
testBeaconStatuses();
describe('on liveness change', () => { describe('on liveness change', () => {
it('renders stopped UI when a beacon stops being live', () => { it('renders stopped UI when a beacon stops being live', () => {
@ -458,4 +466,34 @@ describe('<MBeaconBody />', () => {
); );
}); });
}); });
describe('when map display is not configured', () => {
beforeEach(() => {
// mock map utils to raise MapStyleUrlNotConfigured error
jest.spyOn(mapUtilHooks, 'useMap').mockImplementation(
({ onError }) => {
onError(new Error(LocationShareError.MapStyleUrlNotConfigured));
return mockMap;
});
});
it('renders maps unavailable error for a live beacon with location', () => {
const beaconInfoEvent = makeBeaconInfoEvent(aliceId,
roomId,
{ isLive: true },
'$alice-room1-1',
);
const location1 = makeBeaconEvent(
aliceId, { beaconInfoId: beaconInfoEvent.getId(), geoUri: 'geo:51,41', timestamp: now + 1 },
);
makeRoomWithBeacons(roomId, mockClient, [beaconInfoEvent], [location1]);
const component = getComponent({ mxEvent: beaconInfoEvent });
expect(component.find(MapError)).toMatchSnapshot();
});
// test that statuses display as expected with a map display error
testBeaconStatuses();
});
}); });

View File

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MBeaconBody /> when map display is not configured renders maps unavailable error for a live beacon with location 1`] = `
<MapError
className="mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive"
error="MapStyleUrlNotConfigured"
isMinimised={true}
onClick={[Function]}
>
<div
className="mx_MapError mx_MBeaconBody_mapError mx_MBeaconBody_mapErrorInteractive mx_MapError_isMinimised"
data-test-id="map-rendering-error"
onClick={[Function]}
>
<div
className="mx_MapError_icon"
/>
<Heading
className="mx_MapError_heading"
size="h3"
>
<h3
className="mx_Heading_h3 mx_MapError_heading"
>
Unable to load map
</h3>
</Heading>
<p
className="mx_MapError_message"
>
This homeserver is not configured to display maps.
</p>
</div>
</MapError>
`;