Live location sharing - render users own beacons in timeline (#8296)
* extract location markers into generic Marker Signed-off-by: Kerry Archibald <kerrya@element.io> * wrap marker in smartmarker Signed-off-by: Kerry Archibald <kerrya@element.io> * test smartmarker Signed-off-by: Kerry Archibald <kerrya@element.io> * working map in location body Signed-off-by: Kerry Archibald <kerrya@element.io> * remove skinned sdk Signed-off-by: Kerry Archibald <kerrya@element.io> * use new ZoomButtons in MLocationBody Signed-off-by: Kerry Archibald <kerrya@element.io> * test LocationViewDialog Signed-off-by: Kerry Archibald <kerrya@element.io> * update commentt Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * extract livetimeremaining into own component Signed-off-by: Kerry Archibald <kerrya@element.io> * extract more beacon state utils Signed-off-by: Kerry Archibald <kerrya@element.io> * update tests for roomlivesharewarning Signed-off-by: Kerry Archibald <kerrya@element.io> * add beacon map and status chin Signed-off-by: Kerry Archibald <kerrya@element.io> * add handling for bubbles Signed-off-by: Kerry Archibald <kerrya@element.io> * tests for BeaconBody Signed-off-by: Kerry Archibald <kerrya@element.io> * move displaystatus check up to mbeaconbody Signed-off-by: Kerry Archibald <kerrya@element.io> * test BeaconStatus Signed-off-by: Kerry Archibald <kerrya@element.io> * rename BeaconStatusChin -> BeaconStatus Signed-off-by: Kerry Archibald <kerrya@element.io> * make BeaconStatus generic Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * adjust spinner size Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * add static time remaining option to beacon status Signed-off-by: Kerry Archibald <kerrya@element.io> * render time differently for own beacon Signed-off-by: Kerry Archibald <kerrya@element.io> * use children to add actions to BeaconStatus Signed-off-by: Kerry Archibald <kerrya@element.io> * add OwnBeaconStatus wrapper with stop button Signed-off-by: Kerry Archibald <kerrya@element.io> * add error states for own beacon Signed-off-by: Kerry Archibald <kerrya@element.io> * test OwnBeaconStatus Signed-off-by: Kerry Archibald <kerrya@element.io> * move ownbeaconstatus to write dir Signed-off-by: Kerry Archibald <kerrya@element.io> * improve button styling Signed-off-by: Kerry Archibald <kerrya@element.io> * i18n Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io>pull/21833/head
							parent
							
								
									7a1a2c41d2
								
							
						
					
					
						commit
						1c215e2b71
					
				|  | @ -7,6 +7,7 @@ | |||
| @import "./components/views/beacon/_BeaconStatus.scss"; | ||||
| @import "./components/views/beacon/_LeftPanelLiveShareWarning.scss"; | ||||
| @import "./components/views/beacon/_LiveTimeRemaining.scss"; | ||||
| @import "./components/views/beacon/_OwnBeaconStatus.scss"; | ||||
| @import "./components/views/beacon/_RoomLiveShareWarning.scss"; | ||||
| @import "./components/views/beacon/_StyledLiveBeaconIcon.scss"; | ||||
| @import "./components/views/location/_LiveDurationDropdown.scss"; | ||||
|  |  | |||
|  | @ -45,16 +45,17 @@ limitations under the License. | |||
|     margin-right: $spacing-8; | ||||
| } | ||||
| 
 | ||||
| .mx_BeaconStatus_activeDescription { | ||||
| .mx_BeaconStatus_description { | ||||
|     flex: 1; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     line-height: $font-14px; | ||||
| 
 | ||||
|     padding-right: $spacing-8; | ||||
| 
 | ||||
|     // TODO handle text-overflow | ||||
| } | ||||
| 
 | ||||
| .mx_BeaconStatus_stopButton { | ||||
|     // override button link_inline styles | ||||
|     color: $alert !important; | ||||
|     font-weight: $font-semi-bold !important; | ||||
|     text-transform: uppercase; | ||||
| .mx_BeaconStatus_expiryTime { | ||||
|     color: $secondary-content; | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| .mx_EventTile[data-layout="bubble"] .mx_OwnBeaconStatus_button { | ||||
|     // align to top to make room for timestamp | ||||
|     // in bubble view | ||||
|     align-self: start; | ||||
| } | ||||
| 
 | ||||
| .mx_OwnBeaconStatus_destructiveButton { | ||||
|     // override button link_inline styles | ||||
|     color: $alert !important; | ||||
|     font-weight: $font-semi-bold !important; | ||||
| } | ||||
|  | @ -20,20 +20,33 @@ import { Beacon } from 'matrix-js-sdk/src/matrix'; | |||
| 
 | ||||
| import StyledLiveBeaconIcon from './StyledLiveBeaconIcon'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| import LiveTimeRemaining from './LiveTimeRemaining'; | ||||
| import { BeaconDisplayStatus } from './displayStatus'; | ||||
| import { getBeaconExpiryTimestamp } from '../../../utils/beacon'; | ||||
| import { formatTime } from '../../../DateUtils'; | ||||
| 
 | ||||
| interface Props { | ||||
|     displayStatus: BeaconDisplayStatus; | ||||
|     displayLiveTimeRemaining?: boolean; | ||||
|     beacon?: Beacon; | ||||
|     label?: string; | ||||
|     // assumes permission to stop was checked by parent
 | ||||
|     stopBeacon?: () => void; | ||||
| } | ||||
| 
 | ||||
| const BeaconExpiryTime: React.FC<{ beacon: Beacon }> = ({ beacon }) => { | ||||
|     const expiryTime = formatTime(new Date(getBeaconExpiryTimestamp(beacon))); | ||||
|     return <span className='mx_BeaconStatus_expiryTime'>{ _t('Live until %(expiryTime)s', { expiryTime }) }</span>; | ||||
| }; | ||||
| 
 | ||||
| const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = | ||||
|     ({ beacon, displayStatus, label, stopBeacon, className, ...rest }) => { | ||||
|     ({ | ||||
|         beacon, | ||||
|         displayStatus, | ||||
|         displayLiveTimeRemaining, | ||||
|         label, | ||||
|         className, | ||||
|         children, | ||||
|         ...rest | ||||
|     }) => { | ||||
|         const isIdle = displayStatus === BeaconDisplayStatus.Loading || | ||||
|             displayStatus === BeaconDisplayStatus.Stopped; | ||||
| 
 | ||||
|  | @ -46,25 +59,25 @@ const BeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = | |||
|                 withError={displayStatus === BeaconDisplayStatus.Error} | ||||
|                 isIdle={isIdle} | ||||
|             /> | ||||
|             { displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> } | ||||
|             { displayStatus === BeaconDisplayStatus.Stopped && <span>{ _t('Live location ended') }</span> } | ||||
|             <div className='mx_BeaconStatus_description'> | ||||
| 
 | ||||
|             { /* TODO error */ } | ||||
|                 { displayStatus === BeaconDisplayStatus.Loading && <span>{ _t('Loading live location...') }</span> } | ||||
|                 { displayStatus === BeaconDisplayStatus.Stopped && <span>{ _t('Live location ended') }</span> } | ||||
| 
 | ||||
|             { displayStatus === BeaconDisplayStatus.Active && beacon && <> | ||||
|                 <div className='mx_BeaconStatus_activeDescription'> | ||||
|                     { label } | ||||
|                     <LiveTimeRemaining beacon={beacon} /> | ||||
|                 </div> | ||||
|                 { stopBeacon && <AccessibleButton | ||||
|                     data-test-id='beacon-status-stop-beacon' | ||||
|                     kind='link' | ||||
|                     onClick={stopBeacon} | ||||
|                     className='mx_BeaconStatus_stopButton' | ||||
|                 >{ _t('Stop') }</AccessibleButton> | ||||
|                 { displayStatus === BeaconDisplayStatus.Error && <span>{ _t('Live location error') }</span> } | ||||
| 
 | ||||
|                 { displayStatus === BeaconDisplayStatus.Active && beacon && <> | ||||
|                     <> | ||||
|                         { label } | ||||
|                         { displayLiveTimeRemaining ? | ||||
|                             <LiveTimeRemaining beacon={beacon} /> : | ||||
|                             <BeaconExpiryTime beacon={beacon} /> | ||||
|                         } | ||||
|                     </> | ||||
|                 </> | ||||
|                 } | ||||
|             </> | ||||
|             } | ||||
|             </div> | ||||
|             { children } | ||||
|         </div>; | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,89 @@ | |||
| /* | ||||
| 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 { Beacon } from 'matrix-js-sdk/src/matrix'; | ||||
| import React, { HTMLProps } from 'react'; | ||||
| 
 | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { useOwnLiveBeacons } from '../../../utils/beacon'; | ||||
| import BeaconStatus from './BeaconStatus'; | ||||
| import { BeaconDisplayStatus } from './displayStatus'; | ||||
| import AccessibleButton from '../elements/AccessibleButton'; | ||||
| 
 | ||||
| interface Props { | ||||
|     displayStatus: BeaconDisplayStatus; | ||||
|     beacon?: Beacon; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Wraps BeaconStatus with more capabilities | ||||
|  * for errors and actions available for users own live beacons | ||||
|  */ | ||||
| const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({ | ||||
|     beacon, displayStatus, className, ...rest | ||||
| }) => { | ||||
|     const { | ||||
|         hasWireError, | ||||
|         hasStopSharingError, | ||||
|         stoppingInProgress, | ||||
|         onStopSharing, | ||||
|         onResetWireError, | ||||
|     } = useOwnLiveBeacons([beacon?.identifier]); | ||||
| 
 | ||||
|     // combine display status with errors that only occur for user's own beacons
 | ||||
|     const ownDisplayStatus = hasWireError || hasStopSharingError ? | ||||
|         BeaconDisplayStatus.Error : | ||||
|         displayStatus; | ||||
| 
 | ||||
|     return <BeaconStatus | ||||
|         className='mx_MBeaconBody_chin' | ||||
|         beacon={beacon} | ||||
|         displayStatus={ownDisplayStatus} | ||||
|         label={_t('Live location enabled')} | ||||
|         displayLiveTimeRemaining | ||||
|         {...rest} | ||||
|     > | ||||
|         { ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton | ||||
|             data-test-id='beacon-status-stop-beacon' | ||||
|             kind='link' | ||||
|             onClick={onStopSharing} | ||||
|             className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton' | ||||
|             disabled={stoppingInProgress} | ||||
|         > | ||||
|             { _t('Stop') } | ||||
|         </AccessibleButton> | ||||
|         } | ||||
|         { hasWireError && <AccessibleButton | ||||
|             data-test-id='beacon-status-reset-wire-error' | ||||
|             kind='link' | ||||
|             onClick={onResetWireError} | ||||
|             className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton' | ||||
|         > | ||||
|             { _t('Retry') } | ||||
|         </AccessibleButton> | ||||
|         } | ||||
|         { hasStopSharingError && <AccessibleButton | ||||
|             data-test-id='beacon-status-stop-beacon-retry' | ||||
|             kind='link' | ||||
|             onClick={onStopSharing} | ||||
|             className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton' | ||||
|         > | ||||
|             { _t('Retry') } | ||||
|         </AccessibleButton> } | ||||
|     </BeaconStatus>; | ||||
| }; | ||||
| 
 | ||||
| export default OwnBeaconStatus; | ||||
|  | @ -14,22 +14,24 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import React, { useContext, useEffect, useState } from 'react'; | ||||
| import { Beacon, BeaconEvent, MatrixEvent } from 'matrix-js-sdk/src/matrix'; | ||||
| import { BeaconLocationState } from 'matrix-js-sdk/src/content-helpers'; | ||||
| import { randomString } from 'matrix-js-sdk/src/randomstring'; | ||||
| 
 | ||||
| import { Icon as LocationMarkerIcon } from '../../../../res/img/element-icons/location.svg'; | ||||
| import MatrixClientContext from '../../../contexts/MatrixClientContext'; | ||||
| import { useEventEmitterState } from '../../../hooks/useEventEmitter'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| import { useBeacon } from '../../../utils/beacon'; | ||||
| import { isSelfLocation } from '../../../utils/location'; | ||||
| import { BeaconDisplayStatus, getBeaconDisplayStatus } from '../beacon/displayStatus'; | ||||
| import BeaconStatus from '../beacon/BeaconStatus'; | ||||
| import Spinner from '../elements/Spinner'; | ||||
| import Map from '../location/Map'; | ||||
| import SmartMarker from '../location/SmartMarker'; | ||||
| import BeaconStatus from '../beacon/BeaconStatus'; | ||||
| import OwnBeaconStatus from '../beacon/OwnBeaconStatus'; | ||||
| import { IBodyProps } from "./IBodyProps"; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| 
 | ||||
| const useBeaconState = (beaconInfoEvent: MatrixEvent): { | ||||
|     beacon?: Beacon; | ||||
|  | @ -83,13 +85,13 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) => | |||
|         latestLocationState, | ||||
|     } = useBeaconState(mxEvent); | ||||
|     const mapId = useUniqueId(mxEvent.getId()); | ||||
| 
 | ||||
|     const [error, setError] = useState<Error>(); | ||||
| 
 | ||||
|     const matrixClient = useContext(MatrixClientContext); | ||||
|     const displayStatus = getBeaconDisplayStatus(isLive, latestLocationState, error); | ||||
| 
 | ||||
|     const markerRoomMember = isSelfLocation(mxEvent.getContent()) ? mxEvent.sender : undefined; | ||||
| 
 | ||||
|     const isOwnBeacon = beacon?.beaconInfoOwner === matrixClient.getUserId(); | ||||
| 
 | ||||
|     return ( | ||||
|         <div className='mx_MBeaconBody' ref={ref}> | ||||
|             { displayStatus === BeaconDisplayStatus.Active ? | ||||
|  | @ -106,6 +108,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) => | |||
|                                 id={`${mapId}-marker`} | ||||
|                                 geoUri={latestLocationState.uri} | ||||
|                                 roomMember={markerRoomMember} | ||||
|                                 useMemberColor | ||||
|                             /> | ||||
|                     } | ||||
|                 </Map> | ||||
|  | @ -116,12 +119,19 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent }, ref) => | |||
|                     } | ||||
|                 </div> | ||||
|             } | ||||
|             <BeaconStatus | ||||
|                 className='mx_MBeaconBody_chin' | ||||
|                 beacon={beacon} | ||||
|                 displayStatus={displayStatus} | ||||
|                 label={_t('View live location')} | ||||
|             /> | ||||
|             { isOwnBeacon ? | ||||
|                 <OwnBeaconStatus | ||||
|                     className='mx_MBeaconBody_chin' | ||||
|                     beacon={beacon} | ||||
|                     displayStatus={displayStatus} | ||||
|                 /> : | ||||
|                 <BeaconStatus | ||||
|                     className='mx_MBeaconBody_chin' | ||||
|                     beacon={beacon} | ||||
|                     displayStatus={displayStatus} | ||||
|                     label={_t('View live location')} | ||||
|                 /> | ||||
|             } | ||||
|         </div> | ||||
|     ); | ||||
| }); | ||||
|  |  | |||
|  | @ -2905,11 +2905,14 @@ | |||
|     "Beta": "Beta", | ||||
|     "Leave the beta": "Leave the beta", | ||||
|     "Join the beta": "Join the beta", | ||||
|     "Live until %(expiryTime)s": "Live until %(expiryTime)s", | ||||
|     "Loading live location...": "Loading live location...", | ||||
|     "Live location ended": "Live location ended", | ||||
|     "Live location error": "Live location error", | ||||
|     "An error occured whilst sharing your live location": "An error occured whilst sharing your live location", | ||||
|     "You are sharing your live location": "You are sharing your live location", | ||||
|     "%(timeRemaining)s left": "%(timeRemaining)s left", | ||||
|     "Live location enabled": "Live location enabled", | ||||
|     "An error occured whilst sharing your live location, please try again": "An error occured whilst sharing your live location, please try again", | ||||
|     "An error occurred while stopping your live location, please try again": "An error occurred while stopping your live location, please try again", | ||||
|     "Stop sharing": "Stop sharing", | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ limitations under the License. | |||
| import React from 'react'; | ||||
| import { mount } from 'enzyme'; | ||||
| import { Beacon } from 'matrix-js-sdk/src/matrix'; | ||||
| import { act } from 'react-dom/test-utils'; | ||||
| 
 | ||||
| import BeaconStatus from '../../../../src/components/views/beacon/BeaconStatus'; | ||||
| import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus'; | ||||
|  | @ -26,6 +25,7 @@ import { findByTestId, makeBeaconInfoEvent } from '../../../test-utils'; | |||
| describe('<BeaconStatus />', () => { | ||||
|     const defaultProps = { | ||||
|         displayStatus: BeaconDisplayStatus.Loading, | ||||
|         label: 'test label', | ||||
|     }; | ||||
|     const getComponent = (props = {}) => | ||||
|         mount(<BeaconStatus {...defaultProps} {...props} />); | ||||
|  | @ -40,28 +40,42 @@ describe('<BeaconStatus />', () => { | |||
|         expect(component).toMatchSnapshot(); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders active state without stop buttons', () => { | ||||
|         // mock for stable snapshot
 | ||||
|         jest.spyOn(Date, 'now').mockReturnValue(123456789); | ||||
|         const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:server', {}, '$1')); | ||||
|         const component = getComponent({ beacon, displayStatus: BeaconDisplayStatus.Active }); | ||||
|         expect(component).toMatchSnapshot(); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders active state with stop button', () => { | ||||
|         const stopBeacon = jest.fn(); | ||||
|         const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:sever')); | ||||
|         const component = getComponent({ | ||||
|             beacon, | ||||
|             stopBeacon, | ||||
|             displayStatus: BeaconDisplayStatus.Active, | ||||
|         }); | ||||
|         expect(findByTestId(component, 'beacon-status-stop-beacon')).toMatchSnapshot(); | ||||
| 
 | ||||
|         act(() => { | ||||
|             findByTestId(component, 'beacon-status-stop-beacon').at(0).simulate('click'); | ||||
|     describe('active state', () => { | ||||
|         it('renders without children', () => { | ||||
|             // mock for stable snapshot
 | ||||
|             jest.spyOn(Date, 'now').mockReturnValue(123456789); | ||||
|             const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:server', {}, '$1')); | ||||
|             const component = getComponent({ beacon, displayStatus: BeaconDisplayStatus.Active }); | ||||
|             expect(component).toMatchSnapshot(); | ||||
|         }); | ||||
| 
 | ||||
|         expect(stopBeacon).toHaveBeenCalled(); | ||||
|         it('renders with children', () => { | ||||
|             const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:sever')); | ||||
|             const component = getComponent({ | ||||
|                 beacon, | ||||
|                 children: <span data-test-id='test'>test</span>, | ||||
|                 displayStatus: BeaconDisplayStatus.Active, | ||||
|             }); | ||||
|             expect(findByTestId(component, 'test-child')).toMatchSnapshot(); | ||||
|         }); | ||||
| 
 | ||||
|         it('renders static remaining time when displayLiveTimeRemaining is falsy', () => { | ||||
|             // mock for stable snapshot
 | ||||
|             jest.spyOn(Date, 'now').mockReturnValue(123456789); | ||||
|             const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:server', {}, '$1')); | ||||
|             const component = getComponent({ beacon, displayStatus: BeaconDisplayStatus.Active }); | ||||
|             expect(component.text().includes('Live until 11:17')).toBeTruthy(); | ||||
|         }); | ||||
| 
 | ||||
|         it('renders live time remaining when displayLiveTimeRemaining is truthy', () => { | ||||
|             // mock for stable snapshot
 | ||||
|             jest.spyOn(Date, 'now').mockReturnValue(123456789); | ||||
|             const beacon = new Beacon(makeBeaconInfoEvent('@user:server', '!room:server', {}, '$1')); | ||||
|             const component = getComponent({ | ||||
|                 beacon, displayStatus: BeaconDisplayStatus.Active, | ||||
|                 displayLiveTimeRemaining: true, | ||||
|             }); | ||||
|             expect(component.text().includes('1h left')).toBeTruthy(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  |  | |||
|  | @ -0,0 +1,161 @@ | |||
| /* | ||||
| 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'; | ||||
| import { mount } from 'enzyme'; | ||||
| import { act } from 'react-dom/test-utils'; | ||||
| import { mocked } from 'jest-mock'; | ||||
| import { Beacon } from 'matrix-js-sdk/src/matrix'; | ||||
| 
 | ||||
| import OwnBeaconStatus from '../../../../src/components/views/beacon/OwnBeaconStatus'; | ||||
| import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus'; | ||||
| import { useOwnLiveBeacons } from '../../../../src/utils/beacon'; | ||||
| import { findByTestId, makeBeaconInfoEvent } from '../../../test-utils'; | ||||
| 
 | ||||
| jest.mock('../../../../src/utils/beacon/useOwnLiveBeacons', () => ({ | ||||
|     useOwnLiveBeacons: jest.fn(), | ||||
| })); | ||||
| 
 | ||||
| describe('<OwnBeaconStatus />', () => { | ||||
|     const defaultProps = { | ||||
|         displayStatus: BeaconDisplayStatus.Loading, | ||||
|     }; | ||||
|     const userId = '@user:server'; | ||||
|     const roomId = '!room:server'; | ||||
|     let defaultBeacon; | ||||
|     const getComponent = (props = {}) => | ||||
|         mount(<OwnBeaconStatus {...defaultProps} {...props} />); | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         jest.spyOn(global.Date, 'now').mockReturnValue(123456789); | ||||
|         mocked(useOwnLiveBeacons).mockClear().mockReturnValue({}); | ||||
| 
 | ||||
|         defaultBeacon = new Beacon(makeBeaconInfoEvent(userId, roomId)); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders without a beacon instance', () => { | ||||
|         const component = getComponent(); | ||||
|         expect(component).toMatchSnapshot(); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders loading state correctly', () => { | ||||
|         const component = getComponent(); | ||||
|         expect(component.find('BeaconStatus').props()).toBeTruthy(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('Active state', () => { | ||||
|         it('renders stop button', () => { | ||||
|             const displayStatus = BeaconDisplayStatus.Active; | ||||
|             mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                 onStopSharing: jest.fn(), | ||||
|             }); | ||||
|             const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
|             expect(component.text()).toContain('Live location enabled'); | ||||
| 
 | ||||
|             expect(findByTestId(component, 'beacon-status-stop-beacon').length).toBeTruthy(); | ||||
|         }); | ||||
| 
 | ||||
|         it('stops sharing on stop button click', () => { | ||||
|             const displayStatus = BeaconDisplayStatus.Active; | ||||
|             const onStopSharing = jest.fn(); | ||||
|             mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                 onStopSharing, | ||||
|             }); | ||||
|             const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
| 
 | ||||
|             act(() => { | ||||
|                 findByTestId(component, 'beacon-status-stop-beacon').at(0).simulate('click'); | ||||
|             }); | ||||
| 
 | ||||
|             expect(onStopSharing).toHaveBeenCalled(); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('errors', () => { | ||||
|         it('renders in error mode when displayStatus is error', () => { | ||||
|             const displayStatus = BeaconDisplayStatus.Error; | ||||
|             const component = getComponent({ displayStatus }); | ||||
|             expect(component.text()).toEqual('Live location error'); | ||||
| 
 | ||||
|             // no actions for plain error
 | ||||
|             expect(component.find('AccessibleButton').length).toBeFalsy(); | ||||
|         }); | ||||
| 
 | ||||
|         describe('with wire error', () => { | ||||
|             it('renders in error mode', () => { | ||||
|                 const displayStatus = BeaconDisplayStatus.Active; | ||||
|                 mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                     hasWireError: true, | ||||
|                     onResetWireError: jest.fn(), | ||||
|                 }); | ||||
|                 const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
|                 expect(component.text()).toContain('Live location error'); | ||||
|                 // retry button
 | ||||
|                 expect(findByTestId(component, 'beacon-status-reset-wire-error').length).toBeTruthy(); | ||||
|             }); | ||||
| 
 | ||||
|             it('retry button resets wire error', () => { | ||||
|                 const displayStatus = BeaconDisplayStatus.Active; | ||||
|                 const onResetWireError = jest.fn(); | ||||
|                 mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                     hasWireError: true, | ||||
|                     onResetWireError, | ||||
|                 }); | ||||
|                 const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
|                 act(() => { | ||||
|                     findByTestId(component, 'beacon-status-reset-wire-error').at(0).simulate('click'); | ||||
|                 }); | ||||
| 
 | ||||
|                 expect(onResetWireError).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         describe('with stopping error', () => { | ||||
|             it('renders in error mode', () => { | ||||
|                 const displayStatus = BeaconDisplayStatus.Active; | ||||
|                 mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                     hasWireError: false, | ||||
|                     hasStopSharingError: true, | ||||
|                     onStopSharing: jest.fn(), | ||||
|                 }); | ||||
|                 const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
|                 expect(component.text()).toContain('Live location error'); | ||||
|                 // retry button
 | ||||
|                 expect(findByTestId(component, 'beacon-status-stop-beacon-retry').length).toBeTruthy(); | ||||
|             }); | ||||
| 
 | ||||
|             it('retry button retries stop sharing', () => { | ||||
|                 const displayStatus = BeaconDisplayStatus.Active; | ||||
|                 const onStopSharing = jest.fn(); | ||||
|                 mocked(useOwnLiveBeacons).mockReturnValue({ | ||||
|                     hasStopSharingError: true, | ||||
|                     onStopSharing, | ||||
|                 }); | ||||
|                 const component = getComponent({ displayStatus, beacon: defaultBeacon }); | ||||
|                 act(() => { | ||||
|                     findByTestId(component, 'beacon-status-stop-beacon-retry').at(0).simulate('click'); | ||||
|                 }); | ||||
| 
 | ||||
|                 expect(onStopSharing).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     it('renders loading state correctly', () => { | ||||
|         const component = getComponent(); | ||||
|         expect(component).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|  | @ -1,43 +1,8 @@ | |||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
| 
 | ||||
| exports[`<BeaconStatus /> renders active state with stop button 1`] = ` | ||||
| Array [ | ||||
|   <AccessibleButton | ||||
|     className="mx_BeaconStatus_stopButton" | ||||
|     data-test-id="beacon-status-stop-beacon" | ||||
|     element="div" | ||||
|     kind="link" | ||||
|     onClick={[MockFunction]} | ||||
|     role="button" | ||||
|     tabIndex={0} | ||||
|   > | ||||
|     <div | ||||
|       className="mx_AccessibleButton mx_BeaconStatus_stopButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" | ||||
|       data-test-id="beacon-status-stop-beacon" | ||||
|       onClick={[MockFunction]} | ||||
|       onKeyDown={[Function]} | ||||
|       onKeyUp={[Function]} | ||||
|       role="button" | ||||
|       tabIndex={0} | ||||
|     > | ||||
|       Stop | ||||
|     </div> | ||||
|   </AccessibleButton>, | ||||
|   <div | ||||
|     className="mx_AccessibleButton mx_BeaconStatus_stopButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" | ||||
|     data-test-id="beacon-status-stop-beacon" | ||||
|     onClick={[MockFunction]} | ||||
|     onKeyDown={[Function]} | ||||
|     onKeyUp={[Function]} | ||||
|     role="button" | ||||
|     tabIndex={0} | ||||
|   > | ||||
|     Stop | ||||
|   </div>, | ||||
| ] | ||||
| `; | ||||
| exports[`<BeaconStatus /> active state renders with children 1`] = `null`; | ||||
| 
 | ||||
| exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | ||||
| exports[`<BeaconStatus /> active state renders without children 1`] = ` | ||||
| <BeaconStatus | ||||
|   beacon={ | ||||
|     Beacon { | ||||
|  | @ -48,10 +13,8 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
|         "timeout": 3600000, | ||||
|         "timestamp": 123456789, | ||||
|       }, | ||||
|       "_events": Object { | ||||
|         "Beacon.update": [Function], | ||||
|       }, | ||||
|       "_eventsCount": 1, | ||||
|       "_events": Object {}, | ||||
|       "_eventsCount": 0, | ||||
|       "_isLive": undefined, | ||||
|       "_latestLocationState": undefined, | ||||
|       "_maxListeners": undefined, | ||||
|  | @ -79,6 +42,7 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
|     } | ||||
|   } | ||||
|   displayStatus="Active" | ||||
|   label="test label" | ||||
| > | ||||
|   <div | ||||
|     className="mx_BeaconStatus mx_BeaconStatus_Active" | ||||
|  | @ -93,9 +57,10 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
|       /> | ||||
|     </StyledLiveBeaconIcon> | ||||
|     <div | ||||
|       className="mx_BeaconStatus_activeDescription" | ||||
|       className="mx_BeaconStatus_description" | ||||
|     > | ||||
|       <LiveTimeRemaining | ||||
|       test label | ||||
|       <BeaconExpiryTime | ||||
|         beacon={ | ||||
|           Beacon { | ||||
|             "_beaconInfo": Object { | ||||
|  | @ -105,10 +70,8 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
|               "timeout": 3600000, | ||||
|               "timestamp": 123456789, | ||||
|             }, | ||||
|             "_events": Object { | ||||
|               "Beacon.update": [Function], | ||||
|             }, | ||||
|             "_eventsCount": 1, | ||||
|             "_events": Object {}, | ||||
|             "_eventsCount": 0, | ||||
|             "_isLive": undefined, | ||||
|             "_latestLocationState": undefined, | ||||
|             "_maxListeners": undefined, | ||||
|  | @ -137,12 +100,11 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
|         } | ||||
|       > | ||||
|         <span | ||||
|           className="mx_LiveTimeRemaining" | ||||
|           data-test-id="room-live-share-expiry" | ||||
|           className="mx_BeaconStatus_expiryTime" | ||||
|         > | ||||
|           1h left | ||||
|           Live until 11:17 | ||||
|         </span> | ||||
|       </LiveTimeRemaining> | ||||
|       </BeaconExpiryTime> | ||||
|     </div> | ||||
|   </div> | ||||
| </BeaconStatus> | ||||
|  | @ -151,6 +113,7 @@ exports[`<BeaconStatus /> renders active state without stop buttons 1`] = ` | |||
| exports[`<BeaconStatus /> renders loading state 1`] = ` | ||||
| <BeaconStatus | ||||
|   displayStatus="Loading" | ||||
|   label="test label" | ||||
| > | ||||
|   <div | ||||
|     className="mx_BeaconStatus mx_BeaconStatus_Loading" | ||||
|  | @ -164,9 +127,13 @@ exports[`<BeaconStatus /> renders loading state 1`] = ` | |||
|         className="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle" | ||||
|       /> | ||||
|     </StyledLiveBeaconIcon> | ||||
|     <span> | ||||
|       Loading live location... | ||||
|     </span> | ||||
|     <div | ||||
|       className="mx_BeaconStatus_description" | ||||
|     > | ||||
|       <span> | ||||
|         Loading live location... | ||||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
| </BeaconStatus> | ||||
| `; | ||||
|  | @ -174,6 +141,7 @@ exports[`<BeaconStatus /> renders loading state 1`] = ` | |||
| exports[`<BeaconStatus /> renders stopped state 1`] = ` | ||||
| <BeaconStatus | ||||
|   displayStatus="Stopped" | ||||
|   label="test label" | ||||
| > | ||||
|   <div | ||||
|     className="mx_BeaconStatus mx_BeaconStatus_Stopped" | ||||
|  | @ -187,9 +155,13 @@ exports[`<BeaconStatus /> renders stopped state 1`] = ` | |||
|         className="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle" | ||||
|       /> | ||||
|     </StyledLiveBeaconIcon> | ||||
|     <span> | ||||
|       Live location ended | ||||
|     </span> | ||||
|     <div | ||||
|       className="mx_BeaconStatus_description" | ||||
|     > | ||||
|       <span> | ||||
|         Live location ended | ||||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
| </BeaconStatus> | ||||
| `; | ||||
|  |  | |||
|  | @ -0,0 +1,35 @@ | |||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
| 
 | ||||
| exports[`<OwnBeaconStatus /> renders without a beacon instance 1`] = ` | ||||
| <OwnBeaconStatus | ||||
|   displayStatus="Loading" | ||||
| > | ||||
|   <BeaconStatus | ||||
|     className="mx_MBeaconBody_chin" | ||||
|     displayLiveTimeRemaining={true} | ||||
|     displayStatus="Loading" | ||||
|     label="Live location enabled" | ||||
|   > | ||||
|     <div | ||||
|       className="mx_BeaconStatus mx_BeaconStatus_Loading mx_MBeaconBody_chin" | ||||
|     > | ||||
|       <StyledLiveBeaconIcon | ||||
|         className="mx_BeaconStatus_icon" | ||||
|         isIdle={true} | ||||
|         withError={false} | ||||
|       > | ||||
|         <div | ||||
|           className="mx_StyledLiveBeaconIcon mx_BeaconStatus_icon mx_StyledLiveBeaconIcon_idle" | ||||
|         /> | ||||
|       </StyledLiveBeaconIcon> | ||||
|       <div | ||||
|         className="mx_BeaconStatus_description" | ||||
|       > | ||||
|         <span> | ||||
|           Loading live location... | ||||
|         </span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </BeaconStatus> | ||||
| </OwnBeaconStatus> | ||||
| `; | ||||
		Loading…
	
		Reference in New Issue
	
	 Kerry
						Kerry