Live location sharing: set map bounds to include all locations (#8324)

* open a dialog with map centered around first beacon

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test dialog opening from beacon body

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test beaconmarker

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add bounds to Map comp

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add focusBeacon to beaconviewdialog, use bounds

Signed-off-by: Kerry Archibald <kerrya@element.io>

* lint

Signed-off-by: Kerry Archibald <kerrya@element.io>

* use membercolor on beacon view markers

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add lnglatbounds to maplibre mock

Signed-off-by: Kerry Archibald <kerrya@element.io>

* update snapshots for expanded maplibre Map mock

Signed-off-by: Kerry Archibald <kerrya@element.io>

* test map bounds

Signed-off-by: Kerry Archibald <kerrya@element.io>

* tidy copy paste comment

Signed-off-by: Kerry Archibald <kerrya@element.io>

* add fallback when no more live locations

Signed-off-by: Kerry Archibald <kerrya@element.io>

* accurate signature for getBoundsCenter

Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
Kerry 2022-04-19 13:35:39 +02:00 committed by GitHub
parent 6b13988eaa
commit f70186ea9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 246 additions and 21 deletions

View file

@ -26,6 +26,7 @@ import {
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
import {
findByTestId,
getMockClientWithEventEmitter,
makeBeaconEvent,
makeBeaconInfoEvent,
@ -118,4 +119,37 @@ describe('<BeaconViewDialog />', () => {
// two markers now!
expect(component.find('BeaconMarker').length).toEqual(2);
});
it('renders a fallback when no live beacons remain', () => {
const onFinished = jest.fn();
const room = makeRoomWithStateEvents([defaultEvent]);
const beacon = room.currentState.beacons.get(getBeaconInfoIdentifier(defaultEvent));
beacon.addLocations([location1]);
const component = getComponent({ onFinished });
expect(component.find('BeaconMarker').length).toEqual(1);
// this will replace the defaultEvent
// leading to no more live beacons
const anotherBeaconEvent = makeBeaconInfoEvent(aliceId,
roomId,
{ isLive: false },
'$bob-room1-1',
);
act(() => {
// emits RoomStateEvent.BeaconLiveness
room.currentState.setStateEvents([anotherBeaconEvent]);
});
component.setProps({});
// map placeholder
expect(findByTestId(component, 'beacon-view-dialog-map-fallback')).toMatchSnapshot();
act(() => {
findByTestId(component, 'beacon-view-dialog-fallback-close').at(0).simulate('click');
});
expect(onFinished).toHaveBeenCalled();
});
});

View file

@ -61,6 +61,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],
@ -79,6 +80,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],
@ -111,6 +113,7 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
Symbol(kCapture): false,
}
}
useMemberColor={true}
>
<span>
<ForwardRef
@ -139,9 +142,10 @@ exports[`<BeaconMarker /> renders marker when beacon has location 1`] = `
Symbol(kCapture): false,
}
}
useMemberColor={true}
>
<div
className="mx_Marker mx_Marker_defaultColor"
className="mx_Marker mx_Username_color4"
id="!room:server_@alice:server"
>
<div

View file

@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<BeaconViewDialog /> renders a fallback when no live beacons remain 1`] = `
<div
className="mx_BeaconViewDialog_map mx_BeaconViewDialog_mapFallback"
data-test-id="beacon-view-dialog-map-fallback"
>
<div
className="mx_BeaconViewDialog_mapFallbackIcon"
/>
<span
className="mx_BeaconViewDialog_mapFallbackMessage"
>
No live locations
</span>
<AccessibleButton
data-test-id="beacon-view-dialog-fallback-close"
element="div"
kind="primary"
onClick={[MockFunction]}
role="button"
tabIndex={0}
>
<div
className="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
data-test-id="beacon-view-dialog-fallback-close"
onClick={[MockFunction]}
onKeyDown={[Function]}
onKeyUp={[Function]}
role="button"
tabIndex={0}
>
Close
</div>
</AccessibleButton>
</div>
`;

View file

@ -115,6 +115,38 @@ describe('<Map />', () => {
});
});
describe('map bounds', () => {
it('does not try to fit map bounds when no bounds provided', () => {
getComponent({ bounds: null });
expect(mockMap.fitBounds).not.toHaveBeenCalled();
});
it('fits map to bounds', () => {
const bounds = { north: 51, south: 50, east: 42, west: 41 };
getComponent({ bounds });
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds([bounds.west, bounds.south],
[bounds.east, bounds.north]), { padding: 100 });
});
it('handles invalid bounds', () => {
const logSpy = jest.spyOn(logger, 'error').mockImplementation();
const bounds = { north: 'a', south: 'b', east: 42, west: 41 };
getComponent({ bounds });
expect(mockMap.fitBounds).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith('Invalid map bounds', new Error('Invalid LngLat object: (41, NaN)'));
});
it('updates map bounds when bounds prop changes', () => {
const component = getComponent({ centerGeoUri: 'geo:51,42' });
const bounds = { north: 51, south: 50, east: 42, west: 41 };
const bounds2 = { north: 53, south: 51, east: 45, west: 44 };
component.setProps({ bounds });
component.setProps({ bounds: bounds2 });
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
});
});
describe('children', () => {
it('renders without children', () => {
const component = getComponent({ children: null });

View file

@ -24,6 +24,7 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
"_eventsCount": 1,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction] {
"calls": Array [
@ -76,6 +77,7 @@ exports[`<LocationViewDialog /> renders map correctly 1`] = `
"_eventsCount": 1,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction] {
"calls": Array [

View file

@ -9,6 +9,7 @@ exports[`<SmartMarker /> creates a marker on mount 1`] = `
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],
@ -45,6 +46,7 @@ exports[`<SmartMarker /> removes marker on unmount 1`] = `
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],

View file

@ -8,6 +8,7 @@ exports[`<ZoomButtons /> renders buttons 1`] = `
"_eventsCount": 0,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction],
"setStyle": [MockFunction],

View file

@ -86,7 +86,6 @@ describe('<MBeaconBody />', () => {
});
const modalSpy = jest.spyOn(Modal, 'createTrackedDialog').mockReturnValue(undefined);
beforeEach(() => {
jest.clearAllMocks();
});
@ -123,7 +122,6 @@ describe('<MBeaconBody />', () => {
);
makeRoomWithStateEvents([beaconInfoEvent]);
const component = getComponent({ mxEvent: beaconInfoEvent });
act(() => {
component.find('.mx_MBeaconBody_map').simulate('click');
});
@ -268,6 +266,40 @@ describe('<MBeaconBody />', () => {
expect(modalSpy).toHaveBeenCalled();
});
it('does nothing on click when a beacon has no location', () => {
makeRoomWithStateEvents([aliceBeaconInfo]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find('.mx_MBeaconBody_map').simulate('click');
});
expect(modalSpy).not.toHaveBeenCalled();
});
it('renders a live beacon with a location correctly', () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo]);
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo));
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
expect(component.find('Map').length).toBeTruthy;
});
it('opens maximised map view on click when beacon has a live location', () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo]);
const beaconInstance = room.currentState.beacons.get(getBeaconInfoIdentifier(aliceBeaconInfo));
beaconInstance.addLocations([location1]);
const component = getComponent({ mxEvent: aliceBeaconInfo });
act(() => {
component.find('Map').simulate('click');
});
// opens modal
expect(modalSpy).toHaveBeenCalled();
});
it('updates latest location', () => {
const room = makeRoomWithStateEvents([aliceBeaconInfo]);
const component = getComponent({ mxEvent: aliceBeaconInfo });

View file

@ -124,6 +124,7 @@ exports[`MLocationBody <MLocationBody> without error renders map correctly 1`] =
"_eventsCount": 1,
"_maxListeners": undefined,
"addControl": [MockFunction],
"fitBounds": [MockFunction],
"removeControl": [MockFunction],
"setCenter": [MockFunction] {
"calls": Array [