Send pin drop location share events (#7967)
* center icon better Signed-off-by: Kerry Archibald <kerrya@element.io> * remove debug Signed-off-by: Kerry Archibald <kerrya@element.io> * retrigger all builds Signed-off-by: Kerry Archibald <kerrya@element.io> * set assetType on share event Signed-off-by: Kerry Archibald <kerrya@element.io> * use pin marker on map for pin drop share Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io> * test events Signed-off-by: Kerry Archibald <kerrya@element.io> * pin drop helper text Signed-off-by: Kerry Archibald <kerrya@element.io> * use generic location type Signed-off-by: Kerry Archibald <kerrya@element.io> * add navigationcontrol when in pin mode Signed-off-by: Kerry Archibald <kerrya@element.io> * allow pin drop without location permissions Signed-off-by: Kerry Archibald <kerrya@element.io> * remove geolocate control when pin dropping without geo perms Signed-off-by: Kerry Archibald <kerrya@element.io> * test locationpicker Signed-off-by: Kerry Archibald <kerrya@element.io> * test marker type, tidy Signed-off-by: Kerry Archibald <kerrya@element.io> * tweak style Signed-off-by: Kerry Archibald <kerrya@element.io> * lint Signed-off-by: Kerry Archibald <kerrya@element.io>
This commit is contained in:
parent
288e47fd81
commit
14684c6296
9 changed files with 541 additions and 145 deletions
|
@ -13,90 +13,305 @@ 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 maplibregl from "maplibre-gl";
|
||||
import { mount } from "enzyme";
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { mocked } from 'jest-mock';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
|
||||
import "../../../skinned-sdk"; // Must be first for skinning to work
|
||||
import { getGeoUri } from "../../../../src/components/views/location/LocationPicker";
|
||||
import LocationPicker, { getGeoUri } from "../../../../src/components/views/location/LocationPicker";
|
||||
import { LocationShareType } from "../../../../src/components/views/location/shareLocation";
|
||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import { findByTestId } from '../../../test-utils';
|
||||
|
||||
jest.mock('../../../../src/components/views/messages/MLocationBody', () => ({
|
||||
findMapStyleUrl: jest.fn().mockReturnValue('tileserver.com'),
|
||||
}));
|
||||
|
||||
describe("LocationPicker", () => {
|
||||
describe("getGeoUri", () => {
|
||||
it("Renders a URI with only lat and lon", () => {
|
||||
const pos: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: undefined,
|
||||
heading: undefined,
|
||||
speed: undefined,
|
||||
},
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: undefined,
|
||||
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||
});
|
||||
|
||||
it("Nulls in location are not shown in URI", () => {
|
||||
const pos: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: null,
|
||||
accuracy: null,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
},
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: null,
|
||||
accuracy: null,
|
||||
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||
});
|
||||
|
||||
it("Renders a URI with 3 coords", () => {
|
||||
const pos: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 332.54,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: undefined,
|
||||
heading: undefined,
|
||||
speed: undefined,
|
||||
},
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 332.54,
|
||||
accuracy: undefined,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,332.54");
|
||||
});
|
||||
|
||||
it("Renders a URI with accuracy", () => {
|
||||
const pos: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: 21,
|
||||
altitudeAccuracy: undefined,
|
||||
heading: undefined,
|
||||
speed: undefined,
|
||||
},
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: 21,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4;u=21");
|
||||
});
|
||||
|
||||
it("Renders a URI with accuracy and altitude", () => {
|
||||
const pos: GeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 12.3,
|
||||
accuracy: 21,
|
||||
altitudeAccuracy: undefined,
|
||||
heading: undefined,
|
||||
speed: undefined,
|
||||
},
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 12.3,
|
||||
accuracy: 21,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,12.3;u=21");
|
||||
});
|
||||
});
|
||||
|
||||
describe('<LocationPicker />', () => {
|
||||
const roomId = '!room:server.org';
|
||||
const userId = '@user:server.org';
|
||||
const sender = new RoomMember(roomId, userId);
|
||||
const defaultProps = {
|
||||
sender,
|
||||
shareType: LocationShareType.Own,
|
||||
onChoose: jest.fn(),
|
||||
onFinished: jest.fn(),
|
||||
};
|
||||
const mockClient = {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
getClientWellKnown: jest.fn(),
|
||||
};
|
||||
const getComponent = (props = {}) => mount(<LocationPicker {...defaultProps} {...props} />, {
|
||||
wrappingComponent: MatrixClientContext.Provider,
|
||||
wrappingComponentProps: { value: mockClient },
|
||||
});
|
||||
|
||||
const mockMap = new maplibregl.Map();
|
||||
const mockGeolocate = new maplibregl.GeolocateControl();
|
||||
const mockMarker = new maplibregl.Marker();
|
||||
|
||||
const mockGeolocationPosition = {
|
||||
coords: {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 12.3,
|
||||
accuracy: 21,
|
||||
},
|
||||
timestamp: 123,
|
||||
};
|
||||
const mockClickEvent = {
|
||||
lngLat: {
|
||||
lat: 43.2,
|
||||
lng: 12.4,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(logger, 'error').mockRestore();
|
||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
||||
jest.clearAllMocks();
|
||||
mocked(mockMap).addControl.mockReset();
|
||||
});
|
||||
|
||||
it('displays error when map emits an error', () => {
|
||||
// suppress expected error log
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
const wrapper = getComponent();
|
||||
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockMap).emit('error', { error: 'Something went wrong' });
|
||||
wrapper.setProps({});
|
||||
});
|
||||
|
||||
expect(findByTestId(wrapper, 'location-picker-error').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('displays error when map setup throws', () => {
|
||||
// suppress expected error log
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
|
||||
// throw an error
|
||||
mocked(mockMap).addControl.mockImplementation(() => { throw new Error('oups'); });
|
||||
|
||||
const wrapper = getComponent();
|
||||
wrapper.setProps({});
|
||||
|
||||
expect(findByTestId(wrapper, 'location-picker-error').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('initiates map with geolocation', () => {
|
||||
getComponent();
|
||||
|
||||
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockMap).emit('load');
|
||||
});
|
||||
|
||||
expect(mockGeolocate.trigger).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('for Own location share type', () => {
|
||||
it('closes and displays error when geolocation errors', () => {
|
||||
// suppress expected error log
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished });
|
||||
|
||||
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mockMap.emit('load');
|
||||
// @ts-ignore
|
||||
mockGeolocate.emit('error', {});
|
||||
});
|
||||
|
||||
// dialog is closed on error
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets position on geolocate event', () => {
|
||||
const wrapper = getComponent();
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||
wrapper.setProps({});
|
||||
});
|
||||
|
||||
// marker added
|
||||
expect(maplibregl.Marker).toHaveBeenCalled();
|
||||
expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(
|
||||
12.4, 43.2,
|
||||
));
|
||||
// submit button is enabled when position is truthy
|
||||
expect(findByTestId(wrapper, 'location-picker-submit-button').at(0).props().disabled).toBeFalsy();
|
||||
expect(wrapper.find('MemberAvatar').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('submits location', () => {
|
||||
const onChoose = jest.fn();
|
||||
const wrapper = getComponent({ onChoose });
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||
// make sure button is enabled
|
||||
wrapper.setProps({});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click');
|
||||
});
|
||||
|
||||
// content of this call is tested in LocationShareMenu-test
|
||||
expect(onChoose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('for Pin drop location share type', () => {
|
||||
const shareType = LocationShareType.Pin;
|
||||
it('initiates map with geolocation', () => {
|
||||
getComponent({ shareType });
|
||||
|
||||
expect(mockMap.addControl).toHaveBeenCalledWith(mockGeolocate);
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockMap).emit('load');
|
||||
});
|
||||
|
||||
expect(mockGeolocate.trigger).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes geolocation control on geolocation error', () => {
|
||||
// suppress expected error log
|
||||
jest.spyOn(logger, 'error').mockImplementation(() => { });
|
||||
const onFinished = jest.fn();
|
||||
getComponent({ onFinished, shareType });
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mockMap.emit('load');
|
||||
// @ts-ignore
|
||||
mockGeolocate.emit('error', {});
|
||||
});
|
||||
|
||||
expect(mockMap.removeControl).toHaveBeenCalledWith(mockGeolocate);
|
||||
// dialog is not closed
|
||||
expect(onFinished).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not set position on geolocate event', () => {
|
||||
getComponent({ shareType });
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockGeolocate).emit('geolocate', mockGeolocationPosition);
|
||||
});
|
||||
|
||||
// marker added
|
||||
expect(maplibregl.Marker).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets position on click event', () => {
|
||||
const wrapper = getComponent({ shareType });
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockMap).emit('click', mockClickEvent);
|
||||
wrapper.setProps({});
|
||||
});
|
||||
|
||||
// marker added
|
||||
expect(maplibregl.Marker).toHaveBeenCalled();
|
||||
expect(mockMarker.setLngLat).toHaveBeenCalledWith(new maplibregl.LngLat(
|
||||
12.4, 43.2,
|
||||
));
|
||||
|
||||
// marker is set, icon not avatar
|
||||
expect(wrapper.find('.mx_MLocationBody_markerIcon').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('submits location', () => {
|
||||
const onChoose = jest.fn();
|
||||
const wrapper = getComponent({ onChoose, shareType });
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
mocked(mockMap).emit('click', mockClickEvent);
|
||||
wrapper.setProps({});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
findByTestId(wrapper, 'location-picker-submit-button').at(0).simulate('click');
|
||||
});
|
||||
|
||||
// content of this call is tested in LocationShareMenu-test
|
||||
expect(onChoose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
|||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { mocked } from 'jest-mock';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ASSET_NODE_TYPE, LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||
|
||||
import '../../../skinned-sdk';
|
||||
import LocationShareMenu from '../../../../src/components/views/location/LocationShareMenu';
|
||||
|
@ -27,7 +28,7 @@ import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
|||
import { ChevronFace } from '../../../../src/components/structures/ContextMenu';
|
||||
import SettingsStore from '../../../../src/settings/SettingsStore';
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import { LocationShareType } from '../../../../src/components/views/location/ShareType';
|
||||
import { LocationShareType } from '../../../../src/components/views/location/shareLocation';
|
||||
import { findByTestId } from '../../../test-utils';
|
||||
|
||||
jest.mock('../../../../src/components/views/messages/MLocationBody', () => ({
|
||||
|
@ -58,6 +59,7 @@ describe('<LocationShareMenu />', () => {
|
|||
getClientWellKnown: jest.fn().mockResolvedValue({
|
||||
map_style_url: 'maps.com',
|
||||
}),
|
||||
sendMessage: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
@ -70,6 +72,17 @@ describe('<LocationShareMenu />', () => {
|
|||
roomId: '!room:server.org',
|
||||
sender: new RoomMember('!room:server.org', userId),
|
||||
};
|
||||
|
||||
const position = {
|
||||
coords: {
|
||||
latitude: -36.24484561954707,
|
||||
longitude: 175.46884959563613,
|
||||
accuracy: 10,
|
||||
},
|
||||
timestamp: 1646305006802,
|
||||
type: 'geolocate',
|
||||
};
|
||||
|
||||
const getComponent = (props = {}) =>
|
||||
mount(<LocationShareMenu {...defaultProps} {...props} />, {
|
||||
wrappingComponent: MatrixClientContext.Provider,
|
||||
|
@ -81,6 +94,8 @@ describe('<LocationShareMenu />', () => {
|
|||
(settingName) => settingName === "feature_location_share_pin_drop",
|
||||
);
|
||||
|
||||
mockClient.sendMessage.mockClear();
|
||||
|
||||
jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient as unknown as MatrixClient);
|
||||
});
|
||||
|
||||
|
@ -88,6 +103,21 @@ describe('<LocationShareMenu />', () => {
|
|||
findByTestId(component, `share-location-option-${shareType}`);
|
||||
const getBackButton = component => findByTestId(component, 'share-dialog-buttons-back');
|
||||
const getCancelButton = component => findByTestId(component, 'share-dialog-buttons-cancel');
|
||||
const getSubmitButton = component => findByTestId(component, 'location-picker-submit-button');
|
||||
const setLocation = (component) => {
|
||||
// set the location
|
||||
const locationPickerInstance = component.find('LocationPicker').instance();
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
locationPickerInstance.onGeolocate(position);
|
||||
// make sure button gets enabled
|
||||
component.setProps({});
|
||||
});
|
||||
};
|
||||
const setShareType = (component, shareType) => act(() => {
|
||||
getShareTypeOption(component, shareType).at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
describe('when only Own share type is enabled', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -115,6 +145,28 @@ describe('<LocationShareMenu />', () => {
|
|||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates static own location share event on submission', () => {
|
||||
const onFinished = jest.fn();
|
||||
const component = getComponent({ onFinished });
|
||||
|
||||
setLocation(component);
|
||||
|
||||
act(() => {
|
||||
getSubmitButton(component).at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
|
||||
expect(messageRoomId).toEqual(defaultProps.roomId);
|
||||
expect(relation).toEqual(null);
|
||||
expect(messageBody).toEqual(expect.objectContaining({
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: LocationAssetType.Self,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with pin drop share type enabled', () => {
|
||||
|
@ -147,11 +199,7 @@ describe('<LocationShareMenu />', () => {
|
|||
it('selecting own location share type advances to location picker', () => {
|
||||
const component = getComponent();
|
||||
|
||||
act(() => {
|
||||
getShareTypeOption(component, LocationShareType.Own).at(0).simulate('click');
|
||||
});
|
||||
|
||||
component.setProps({});
|
||||
setShareType(component, LocationShareType.Own);
|
||||
|
||||
expect(component.find('LocationPicker').length).toBeTruthy();
|
||||
});
|
||||
|
@ -162,10 +210,7 @@ describe('<LocationShareMenu />', () => {
|
|||
const component = getComponent({ onFinished });
|
||||
|
||||
// advance to location picker
|
||||
act(() => {
|
||||
getShareTypeOption(component, LocationShareType.Own).at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
setShareType(component, LocationShareType.Own);
|
||||
|
||||
expect(component.find('LocationPicker').length).toBeTruthy();
|
||||
|
||||
|
@ -177,5 +222,31 @@ describe('<LocationShareMenu />', () => {
|
|||
// back to share type
|
||||
expect(component.find('ShareType').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates pin drop location share event on submission', () => {
|
||||
// feature_location_share_pin_drop is set to enabled by default mocking
|
||||
const onFinished = jest.fn();
|
||||
const component = getComponent({ onFinished });
|
||||
|
||||
// advance to location picker
|
||||
setShareType(component, LocationShareType.Pin);
|
||||
|
||||
setLocation(component);
|
||||
|
||||
act(() => {
|
||||
getSubmitButton(component).at(0).simulate('click');
|
||||
component.setProps({});
|
||||
});
|
||||
|
||||
expect(onFinished).toHaveBeenCalled();
|
||||
const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
|
||||
expect(messageRoomId).toEqual(defaultProps.roomId);
|
||||
expect(relation).toEqual(null);
|
||||
expect(messageBody).toEqual(expect.objectContaining({
|
||||
[ASSET_NODE_TYPE.name]: {
|
||||
type: LocationAssetType.Pin,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue