Live location share - focus on user location on list item click (PSG-609) (#9051)

* extract preventDefaultWrapper into utils

* add click handling to beacon list item

* add click handling to dialog sidebar

* focus in on beacons when clicked in list

* stylelint

* fussy import ordering

* test beacon focusing in beaocnviewdialog
This commit is contained in:
Kerry 2022-07-18 10:34:39 +02:00 committed by GitHub
parent 38a913488f
commit dc6ceb1d1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 473 additions and 89 deletions

View file

@ -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('<BeaconListItem />', () => {
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();
});
});
});
});

View file

@ -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('<BeaconViewDialog />', () => {
// 14.03.2022 16:15
@ -89,13 +92,18 @@ describe('<BeaconViewDialog />', () => {
const getComponent = (props = {}) =>
mount(<BeaconViewDialog {...defaultProps} {...props} />);
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('<BeaconViewDialog />', () => {
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('<BeaconViewDialog />', () => {
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);
});
});
});

View file

@ -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('<DialogSidebar />', () => {
const defaultProps = {
beacons: [],
requestClose: jest.fn(),
onBeaconClick: jest.fn(),
};
const getComponent = (props = {}) =>
mount(<DialogSidebar {...defaultProps} {...props} />);
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 = {}) => (
<MatrixClientContext.Provider value={client}>
<DialogSidebar {...defaultProps} {...props} />);
</MatrixClientContext.Provider>);
beforeEach(() => {
// mock now so time based text in snapshots is stable
jest.spyOn(Date, 'now').mockReturnValue(now);
});
afterAll(() => {
jest.spyOn(Date, 'now').mockRestore();
});
it('renders sidebar correctly without beacons', () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it('renders sidebar correctly with beacons', () => {
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
const { container } = render(getComponent({ beacons: [beacon] }));
expect(container).toMatchSnapshot();
});
it('calls on beacon click', () => {
const onBeaconClick = jest.fn();
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
act(() => {
const [listItem] = container.getElementsByClassName('mx_BeaconListItem');
fireEvent.click(listItem);
});
expect(onBeaconClick).toHaveBeenCalled();
});
it('closes on close button click', () => {
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();
});

View file

@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&amp;mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div class=\\"mx_BeaconListItem_interactions\\"><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&amp;mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;

View file

@ -1,53 +1,144 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DialogSidebar /> renders sidebar correctly 1`] = `
<DialogSidebar
beacons={Array []}
requestClose={[MockFunction]}
>
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
<div>
<div
className="mx_DialogSidebar"
class="mx_DialogSidebar"
>
<div
className="mx_DialogSidebar_header"
class="mx_DialogSidebar_header"
>
<Heading
size="h4"
<h4
class="mx_Heading_h4"
>
<h4
className="mx_Heading_h4"
>
View List
</h4>
</Heading>
<AccessibleButton
className="mx_DialogSidebar_closeButton"
data-test-id="dialog-sidebar-close"
element="div"
onClick={[MockFunction]}
View List
</h4>
<div
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
data-testid="dialog-sidebar-close"
role="button"
tabIndex={0}
tabindex="0"
title="Close sidebar"
>
<div
className="mx_AccessibleButton mx_DialogSidebar_closeButton"
data-test-id="dialog-sidebar-close"
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
title="Close sidebar"
>
<div
className="mx_DialogSidebar_closeButtonIcon"
/>
</div>
</AccessibleButton>
class="mx_DialogSidebar_closeButtonIcon"
/>
</div>
</div>
<ol
className="mx_DialogSidebar_list"
class="mx_DialogSidebar_list"
>
<li
class="mx_BeaconListItem"
>
<span
class="mx_BaseAvatar mx_BeaconListItem_avatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 20.8px; width: 32px; line-height: 32px;"
/>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
src=""
style="width: 32px; height: 32px;"
/>
</span>
<div
class="mx_BeaconListItem_info"
>
<div
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
>
<div
class="mx_BeaconStatus_description"
>
<span
class="mx_BeaconStatus_label"
>
@alice:server.org
</span>
<span
class="mx_BeaconStatus_expiryTime"
>
Live until 16:14
</span>
</div>
<div
class="mx_BeaconListItem_interactions"
>
<div
tabindex="0"
>
<a
data-test-id="open-location-in-osm"
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
rel="noreferrer noopener"
target="_blank"
>
<div
class="mx_ShareLatestLocation_icon"
/>
</a>
</div>
<div
class="mx_CopyableText mx_ShareLatestLocation_copy"
>
<div
aria-label="Copy"
class="mx_AccessibleButton mx_CopyableText_copyButton"
role="button"
tabindex="0"
/>
</div>
</div>
</div>
<span
class="mx_BeaconListItem_lastUpdated"
>
Updated a few seconds ago
</span>
</div>
</li>
</ol>
</div>
);
</div>
`;
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
<div>
<div
class="mx_DialogSidebar"
>
<div
class="mx_DialogSidebar_header"
>
<h4
class="mx_Heading_h4"
>
View List
</h4>
<div
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
data-testid="dialog-sidebar-close"
role="button"
tabindex="0"
title="Close sidebar"
>
<div
class="mx_DialogSidebar_closeButtonIcon"
/>
</div>
</div>
<ol
class="mx_DialogSidebar_list"
/>
</div>
</DialogSidebar>
);
</div>
`;