From dc6ceb1d1c6831f96a35ff2cb2bc29cb2f1267f3 Mon Sep 17 00:00:00 2001 From: Kerry Date: Mon, 18 Jul 2022 10:34:39 +0200 Subject: [PATCH] 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 --- .../views/beacon/_BeaconListItem.pcss | 7 + .../views/beacon/BeaconListItem.tsx | 13 +- .../views/beacon/BeaconViewDialog.tsx | 56 ++++-- src/components/views/beacon/DialogSidebar.tsx | 15 +- .../views/beacon/OwnBeaconStatus.tsx | 17 +- src/components/views/location/Map.tsx | 7 + src/components/views/messages/MBeaconBody.tsx | 2 +- src/utils/NativeEventUtils.ts | 25 +++ src/utils/location/useMap.ts | 2 +- .../views/beacon/BeaconListItem-test.tsx | 26 +++ .../views/beacon/BeaconViewDialog-test.tsx | 141 +++++++++++++-- .../views/beacon/DialogSidebar-test.tsx | 75 +++++++- .../BeaconListItem-test.tsx.snap | 2 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 165 ++++++++++++++---- test/test-utils/beacon.ts | 6 +- test/test-utils/utilities.ts | 3 +- 16 files changed, 473 insertions(+), 89 deletions(-) create mode 100644 src/utils/NativeEventUtils.ts diff --git a/res/css/components/views/beacon/_BeaconListItem.pcss b/res/css/components/views/beacon/_BeaconListItem.pcss index 00f8bcbe5b..42032b14c1 100644 --- a/res/css/components/views/beacon/_BeaconListItem.pcss +++ b/res/css/components/views/beacon/_BeaconListItem.pcss @@ -22,6 +22,8 @@ limitations under the License. padding: $spacing-12 0; border-bottom: 1px solid $system; + + cursor: pointer; } .mx_BeaconListItem_avatarIcon { @@ -61,3 +63,8 @@ limitations under the License. color: $tertiary-content; font-size: $font-10px; } + +.mx_BeaconListItem_interactions { + display: flex; + flex-direction: row; +} diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx index bcfb497176..414b45a7f7 100644 --- a/src/components/views/beacon/BeaconListItem.tsx +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and 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 { LocationAssetType } from 'matrix-js-sdk/src/@types/location'; import MatrixClientContext from '../../../contexts/MatrixClientContext'; import { useEventEmitterState } from '../../../hooks/useEventEmitter'; import { humanizeTime } from '../../../utils/humanize'; +import { preventDefaultWrapper } from '../../../utils/NativeEventUtils'; import { _t } from '../../../languageHandler'; import MemberAvatar from '../avatars/MemberAvatar'; import BeaconStatus from './BeaconStatus'; @@ -32,7 +33,7 @@ interface Props { beacon: Beacon; } -const BeaconListItem: React.FC = ({ beacon }) => { +const BeaconListItem: React.FC> = ({ beacon, ...rest }) => { const latestLocationState = useEventEmitterState( beacon, BeaconEvent.LocationUpdate, @@ -52,7 +53,7 @@ const BeaconListItem: React.FC = ({ beacon }) => { const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp); - return
  • + return
  • { isSelfLocation ? = ({ beacon }) => { label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner} displayStatus={BeaconDisplayStatus.Active} > - + { /* eat events from interactive share buttons + so parent click handlers are not triggered */ } +
    {})}> + +
    { _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) } diff --git a/src/components/views/beacon/BeaconViewDialog.tsx b/src/components/views/beacon/BeaconViewDialog.tsx index af21c64339..50a880c1b3 100644 --- a/src/components/views/beacon/BeaconViewDialog.tsx +++ b/src/components/views/beacon/BeaconViewDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and 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 { Beacon, @@ -45,7 +45,16 @@ interface IProps extends IDialogProps { roomId: Room['roomId']; matrixClient: MatrixClient; // 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 => { @@ -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; } => { - const bounds = useRef(getBeaconBounds(liveBeacons)); - const centerGeoUri = useRef( - focusBeacon?.latestLocationState?.uri || - getBoundsCenter(bounds.current), + const [bounds, setBounds] = useState(getBeaconBounds(liveBeacons)); + const [centerGeoUri, setCenterGeoUri] = useState( + beacon?.latestLocationState?.uri || + 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 */ const BeaconViewDialog: React.FC = ({ - focusBeacon, + initialFocusedBeacon, roomId, matrixClient, onFinished, }) => { const liveBeacons = useLiveBeacons(roomId, matrixClient); + const [focusedBeaconState, setFocusedBeaconState] = + useState({ beacon: initialFocusedBeacon, ts: 0 }); const [isSidebarOpen, setSidebarOpen] = useState(false); - const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon); + const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusedBeaconState); const [mapDisplayError, setMapDisplayError] = useState(); @@ -94,6 +124,10 @@ const BeaconViewDialog: React.FC = ({ } }, [mapDisplayError]); + const onBeaconListItemClick = (beacon: Beacon) => { + setFocusedBeaconState({ beacon, ts: Date.now() }); + }; + return ( = ({ } { isSidebarOpen ? - setSidebarOpen(false)} /> : + setSidebarOpen(false)} /> : setSidebarOpen(true)} diff --git a/src/components/views/beacon/DialogSidebar.tsx b/src/components/views/beacon/DialogSidebar.tsx index 4365b5fa8b..0b5442cade 100644 --- a/src/components/views/beacon/DialogSidebar.tsx +++ b/src/components/views/beacon/DialogSidebar.tsx @@ -26,9 +26,14 @@ import BeaconListItem from './BeaconListItem'; interface Props { beacons: Beacon[]; requestClose: () => void; + onBeaconClick: (beacon: Beacon) => void; } -const DialogSidebar: React.FC = ({ beacons, requestClose }) => { +const DialogSidebar: React.FC = ({ + beacons, + onBeaconClick, + requestClose, +}) => { return
    { _t('View List') } @@ -36,13 +41,17 @@ const DialogSidebar: React.FC = ({ beacons, requestClose }) => { className='mx_DialogSidebar_closeButton' onClick={requestClose} title={_t('Close sidebar')} - data-test-id='dialog-sidebar-close' + data-testid='dialog-sidebar-close' >
      - { beacons.map((beacon) => ) } + { beacons.map((beacon) => onBeaconClick(beacon)} + />) }
    ; }; diff --git a/src/components/views/beacon/OwnBeaconStatus.tsx b/src/components/views/beacon/OwnBeaconStatus.tsx index 8760376131..9a6126c0b7 100644 --- a/src/components/views/beacon/OwnBeaconStatus.tsx +++ b/src/components/views/beacon/OwnBeaconStatus.tsx @@ -19,6 +19,7 @@ import React, { HTMLProps } from 'react'; import { _t } from '../../../languageHandler'; import { useOwnLiveBeacons } from '../../../utils/beacon'; +import { preventDefaultWrapper } from '../../../utils/NativeEventUtils'; import BeaconStatus from './BeaconStatus'; import { BeaconDisplayStatus } from './displayStatus'; import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton'; @@ -45,14 +46,6 @@ const OwnBeaconStatus: React.FC> = ({ onResetLocationPublishError, } = 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 const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ? BeaconDisplayStatus.Error : @@ -68,7 +61,9 @@ const OwnBeaconStatus: React.FC> = ({ { ownDisplayStatus === BeaconDisplayStatus.Active && (onStopSharing)} className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton' disabled={stoppingInProgress} > @@ -78,6 +73,8 @@ const OwnBeaconStatus: React.FC> = ({ { hasLocationPublishError && @@ -87,6 +84,8 @@ const OwnBeaconStatus: React.FC> = ({ { hasStopSharingError && diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 023ff2d5cc..6cd75cfafc 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -80,6 +80,13 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) => interface MapProps { id: string; 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; bounds?: Bounds; className?: string; diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index a5022317d2..8c75de01ba 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -162,7 +162,7 @@ const MBeaconBody: React.FC = React.forwardRef(({ mxEvent, getRelati { roomId: mxEvent.getRoomId(), matrixClient, - focusBeacon: beacon, + initialFocusedBeacon: beacon, isMapDisplayError, }, "mx_BeaconViewDialog_wrapper", diff --git a/src/utils/NativeEventUtils.ts b/src/utils/NativeEventUtils.ts new file mode 100644 index 0000000000..3094b57bd4 --- /dev/null +++ b/src/utils/NativeEventUtils.ts @@ -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 = + (callback: () => void) => (e?: T) => { + e?.stopPropagation(); + e?.preventDefault(); + callback(); + }; diff --git a/src/utils/location/useMap.ts b/src/utils/location/useMap.ts index d408362950..55770cc5e2 100644 --- a/src/utils/location/useMap.ts +++ b/src/utils/location/useMap.ts @@ -35,7 +35,7 @@ export const useMap = ({ interactive, bodyId, onError, -}: UseMapProps): MapLibreMap => { +}: UseMapProps): MapLibreMap | undefined => { const [map, setMap] = useState(); useEffect( diff --git a/test/components/views/beacon/BeaconListItem-test.tsx b/test/components/views/beacon/BeaconListItem-test.tsx index e7e9fbb726..238a1dd041 100644 --- a/test/components/views/beacon/BeaconListItem-test.tsx +++ b/test/components/views/beacon/BeaconListItem-test.tsx @@ -27,6 +27,7 @@ import { act } from 'react-dom/test-utils'; import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem'; import MatrixClientContext from '../../../../src/contexts/MatrixClientContext'; import { + findByTestId, getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, @@ -169,5 +170,30 @@ describe('', () => { 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(); + }); + }); }); }); diff --git a/test/components/views/beacon/BeaconViewDialog-test.tsx b/test/components/views/beacon/BeaconViewDialog-test.tsx index 12b4093939..12c82968a5 100644 --- a/test/components/views/beacon/BeaconViewDialog-test.tsx +++ b/test/components/views/beacon/BeaconViewDialog-test.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { MatrixClient, @@ -28,15 +28,18 @@ import maplibregl from 'maplibre-gl'; import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog'; import { + findByAttr, findByTestId, getMockClientWithEventEmitter, makeBeaconEvent, makeBeaconInfoEvent, + makeRoomWithBeacons, makeRoomWithStateEvents, } from '../../../test-utils'; import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils'; import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore'; import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus'; +import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem'; describe('', () => { // 14.03.2022 16:15 @@ -89,13 +92,18 @@ describe('', () => { const getComponent = (props = {}) => mount(); + const openSidebar = (component: ReactWrapper) => act(() => { + findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); + component.setProps({}); + }); + beforeAll(() => { maplibregl.AttributionControl = jest.fn(); }); beforeEach(() => { jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore(); - + jest.spyOn(global.Date, 'now').mockReturnValue(now); jest.clearAllMocks(); }); @@ -225,10 +233,7 @@ describe('', () => { beacon.addLocations([location1]); const component = getComponent(); - act(() => { - findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); - component.setProps({}); - }); + openSidebar(component); expect(component.find('DialogSidebar').length).toBeTruthy(); }); @@ -240,20 +245,134 @@ describe('', () => { const component = getComponent(); // open the sidebar - act(() => { - findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click'); - component.setProps({}); - }); + openSidebar(component); expect(component.find('DialogSidebar').length).toBeTruthy(); // now close it act(() => { - findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + findByAttr('data-testid')(component, 'dialog-sidebar-close').at(0).simulate('click'); component.setProps({}); }); 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); + }); + }); }); diff --git a/test/components/views/beacon/DialogSidebar-test.tsx b/test/components/views/beacon/DialogSidebar-test.tsx index a5a1f0e5e7..8249ec3712 100644 --- a/test/components/views/beacon/DialogSidebar-test.tsx +++ b/test/components/views/beacon/DialogSidebar-test.tsx @@ -15,31 +15,88 @@ limitations under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { fireEvent, render } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; 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('', () => { const defaultProps = { beacons: [], requestClose: jest.fn(), + onBeaconClick: jest.fn(), }; - const getComponent = (props = {}) => - mount(); - it('renders sidebar correctly', () => { - const component = getComponent(); - expect(component).toMatchSnapshot(); + const now = 1647270879403; + + 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 = {}) => ( + + ); + ); + + 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', () => { const requestClose = jest.fn(); - const component = getComponent({ requestClose }); + const { getByTestId } = render(getComponent({ requestClose })); act(() => { - findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click'); + fireEvent.click(getByTestId('dialog-sidebar-close')); }); expect(requestClose).toHaveBeenCalled(); }); diff --git a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap index 221d534c02..9ddc5dd44c 100644 --- a/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/BeaconListItem-test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; +exports[` when a beacon is live and has locations renders beacon info 1`] = `"
  • Alice's carLive until 16:04
    Updated a few seconds ago
  • "`; diff --git a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap index e3b6f10490..6da5cc27c2 100644 --- a/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap +++ b/test/components/views/beacon/__snapshots__/DialogSidebar-test.tsx.snap @@ -1,53 +1,144 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` renders sidebar correctly 1`] = ` - +exports[` renders sidebar correctly with beacons 1`] = ` +
    - -

    - View List -

    -
    - +
    -
    -
    - + class="mx_DialogSidebar_closeButtonIcon" + /> +
      +
    1. + + +
      +
      +
      + + @alice:server.org + + + Live until 16:14 + +
      +
      +
      + +
      + +
      +
      +
      +
      +
      +
      + + Updated a few seconds ago + +
      +
    2. +
    +
    + ); +
    +`; + +exports[` renders sidebar correctly without beacons 1`] = ` +
    +
    +
    +

    + View List +

    +
    +
    +
    +
    +
    - + ); +
    `; diff --git a/test/test-utils/beacon.ts b/test/test-utils/beacon.ts index 9501cd2cb3..a58be78151 100644 --- a/test/test-utils/beacon.ts +++ b/test/test-utils/beacon.ts @@ -205,7 +205,11 @@ export const makeRoomWithBeacons = ( const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient }); const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event))); 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; }; diff --git a/test/test-utils/utilities.ts b/test/test-utils/utilities.ts index 666f7c68ea..5011a55593 100644 --- a/test/test-utils/utilities.ts +++ b/test/test-utils/utilities.ts @@ -35,7 +35,8 @@ export function untilDispatch(waitForAction: DispatcherAction): Promise (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 findById = findByAttr('id'); export const findByAriaLabel = findByAttr('aria-label');