Merge pull request #832 from Safelite/feature/digital/SSR-776
SSR-776 Make Google Maps pins clickable
This commit is contained in:
commit
aab351ca6c
5 changed files with 183 additions and 52 deletions
|
|
@ -20,5 +20,6 @@ Object {
|
|||
],
|
||||
},
|
||||
},
|
||||
"infoWindow": null,
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -21,12 +21,19 @@ const mockGeocoderInstance2 = {
|
|||
};
|
||||
const mockGeocoder = jest.fn(() => (mockGeocoderInstance1));
|
||||
|
||||
const mockAdvancedMarkerElement = jest.fn();
|
||||
const mockAdvancedMarkerInstance = {
|
||||
addListener: jest.fn()
|
||||
};
|
||||
|
||||
const mockAdvancedMarkerElement = jest.fn(() => (mockAdvancedMarkerInstance));
|
||||
const mockPinElement = jest.fn();
|
||||
const mockInfoWindow = jest.fn();
|
||||
|
||||
const mockImportLibrary = jest.fn().mockImplementation(() => (Promise.resolve({
|
||||
Map: mockMap,
|
||||
Geocoder: mockGeocoder,
|
||||
AdvancedMarkerElement: mockAdvancedMarkerElement
|
||||
AdvancedMarkerElement: mockAdvancedMarkerElement,
|
||||
PinElement: mockPinElement
|
||||
})));
|
||||
|
||||
const mockExtend = jest.fn();
|
||||
|
|
@ -36,7 +43,8 @@ const setupGoogleMock = () => {
|
|||
global.window.google = {
|
||||
maps: {
|
||||
importLibrary: mockImportLibrary,
|
||||
LatLngBounds: jest.fn(() => (mockLatLngBound))
|
||||
LatLngBounds: jest.fn(() => (mockLatLngBound)),
|
||||
InfoWindow: mockInfoWindow
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -46,6 +54,7 @@ beforeEach(() => {
|
|||
mockMap.mockClear();
|
||||
mockGeocoder.mockClear();
|
||||
mockAdvancedMarkerElement.mockClear();
|
||||
mockPinElement.mockClear();
|
||||
|
||||
// clear methods
|
||||
mockExtend.mockClear();
|
||||
|
|
@ -150,7 +159,13 @@ describe('Google Map', () => {
|
|||
|
||||
const zipCode = '00882';
|
||||
const [address1, address2, position1, position2] = ['a1', 'a2', 'p1', 'p2'];
|
||||
const addresses = [address1, address2];
|
||||
const markers = [
|
||||
{
|
||||
fullAddress: address1
|
||||
},
|
||||
{
|
||||
fullAddress: address2
|
||||
}];
|
||||
mockGeocode.mockImplementation((obj) => {
|
||||
let result = {};
|
||||
if (obj.address.includes(zipCode)) {
|
||||
|
|
@ -171,7 +186,7 @@ describe('Google Map', () => {
|
|||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
const propsData = { addresses, zipCode };
|
||||
const propsData = { markers, zipCode };
|
||||
const { wrapper } = getMountedComponent({}, {}, propsData);
|
||||
await awaitingSetupTicks(wrapper);
|
||||
|
||||
|
|
@ -198,8 +213,6 @@ describe('Google Map', () => {
|
|||
test('watch addresses creates map with expected markers', async () => {
|
||||
// Arrange
|
||||
const zipCode = '00882';
|
||||
const { wrapper } = getMountedComponent({}, { zipCode });
|
||||
await awaitingSetupTicks(wrapper);
|
||||
|
||||
const mockGetCenter = jest.fn();
|
||||
const mockNorthEast = jest.fn();
|
||||
|
|
@ -238,10 +251,18 @@ describe('Google Map', () => {
|
|||
}
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
const newAddresses = [address1, address2];
|
||||
const markers = [
|
||||
{
|
||||
fullAddress: address1
|
||||
},
|
||||
{
|
||||
fullAddress: address2
|
||||
}];
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$options.watch.addresses.call(wrapper.vm, newAddresses);
|
||||
const { wrapper } = getMountedComponent({}, { zipCode });
|
||||
await awaitingSetupTicks(wrapper);
|
||||
await wrapper.vm.$options.watch.markers.call(wrapper.vm, markers);
|
||||
await awaitingSetupTicks(wrapper);
|
||||
|
||||
// Assert
|
||||
|
|
@ -260,9 +281,9 @@ describe('Google Map', () => {
|
|||
expect(mockExtend).toBeCalledTimes(6);
|
||||
expect(mockFitBounds).toBeCalledTimes(2);
|
||||
|
||||
expect(mockGetCenter).toBeCalledTimes(1);
|
||||
expect(mockNorthEast).toBeCalledTimes(1);
|
||||
expect(mockSouthWest).toBeCalledTimes(1);
|
||||
expect(mockGetCenter).toBeCalledTimes(2);
|
||||
expect(mockNorthEast).toBeCalledTimes(2);
|
||||
expect(mockSouthWest).toBeCalledTimes(2);
|
||||
});
|
||||
describe('method', () => {
|
||||
test('getMap calls map constructor with expected parameters', async () => {
|
||||
|
|
@ -295,12 +316,20 @@ describe('Google Map', () => {
|
|||
await awaitingSetupTicks(wrapper);
|
||||
const numberOfExtendCallsDuringMount = 2;
|
||||
|
||||
mockGeocode.mockImplementation(() => {
|
||||
const result = {
|
||||
results: [
|
||||
{ geometry: { location: 'p1' } }
|
||||
]
|
||||
};
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = wrapper.vm.getBounds(locations);
|
||||
|
||||
// Assert
|
||||
expect(mockExtend).toHaveBeenCalledTimes(
|
||||
numberOfExtendCallsDuringMount + numberOfExtendCallsPostMount);
|
||||
expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsDuringMount + numberOfExtendCallsPostMount);
|
||||
expect(result).toBe(mockLatLngBound);
|
||||
}
|
||||
);
|
||||
|
|
@ -362,7 +391,7 @@ describe('Google Map', () => {
|
|||
expect(mockGeocode).toHaveBeenCalledTimes(numberOfCallsFromMount);
|
||||
|
||||
// Act
|
||||
await wrapper.vm.getLocationsFromAddresses(addresses);
|
||||
await wrapper.vm.getLocationsFromAddresses(addresses?.map((a) => ({ fullAddress: a })));
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
|
|
@ -374,7 +403,7 @@ describe('Google Map', () => {
|
|||
// Arrange
|
||||
const wrapper = shallowMount(googleMap, {});
|
||||
await awaitingSetupTicks(wrapper);
|
||||
const addresses = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8'];
|
||||
const addresses = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8'].map((a) => ({ fullAddress: a }));
|
||||
mockGeocode.mockImplementationOnce(() => null)
|
||||
.mockImplementationOnce(() => ({ results: null }))
|
||||
.mockImplementationOnce(() => ({ results: [] }))
|
||||
|
|
@ -402,7 +431,7 @@ describe('Google Map', () => {
|
|||
const expectedResult = ['some value', 'some other value'];
|
||||
|
||||
// Act
|
||||
const result = await wrapper.vm.getLocationsFromAddresses(addresses);
|
||||
const result = (await wrapper.vm.getLocationsFromAddresses(addresses)).map((a) => a.position);
|
||||
|
||||
// Assert
|
||||
expect(result.length).toBe(2);
|
||||
|
|
@ -477,7 +506,7 @@ describe('Google Map', () => {
|
|||
Promise.resolve(obj.address.includes(zipCode)
|
||||
? zipCodeGeocodeResult
|
||||
: shopGeocodeResult));
|
||||
const addresses = ['a1', 'a2'];
|
||||
const addresses = [{ fullAddress: 'a1' }, { fullAddress: 'a2' }];
|
||||
const { wrapper } = getMountedComponent({}, { zipCode });
|
||||
await awaitingSetupTicks(wrapper);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@
|
|||
export default {
|
||||
name: 'google-map',
|
||||
props: {
|
||||
addresses: Array,
|
||||
markers: Array,
|
||||
zipCode: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
country: 'USA',
|
||||
geocoder: null
|
||||
geocoder: null,
|
||||
infoWindow: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -23,12 +24,12 @@ export default {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
async addresses(newAddresses) {
|
||||
this.createMapWithMarkersForAddresses(newAddresses);
|
||||
async markers(newMarkers) {
|
||||
await this.createMapWithMarkersForAddresses(newMarkers);
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.createMapWithMarkersForAddresses(this.addresses);
|
||||
async beforeMount() {
|
||||
await this.createMapWithMarkersForAddresses(this.markers);
|
||||
},
|
||||
methods: {
|
||||
async getMap(center) {
|
||||
|
|
@ -50,20 +51,58 @@ export default {
|
|||
this.geocoder = new Geocoder();
|
||||
}
|
||||
},
|
||||
async addMarkersToMap(map, positions) {
|
||||
const { AdvancedMarkerElement } = await window.google.maps.importLibrary('marker');
|
||||
positions?.forEach((position) => new AdvancedMarkerElement({ map, position }));
|
||||
async addMarkersToMap(map, markers) {
|
||||
const { AdvancedMarkerElement, PinElement } = await window.google.maps.importLibrary('marker');
|
||||
markers?.forEach((marker, index) => {
|
||||
const pinElement = new PinElement({
|
||||
glyph: String.fromCharCode('A'.charCodeAt(0) + index),
|
||||
glyphColor: '#000000',
|
||||
borderColor: '#000000'
|
||||
});
|
||||
const markerElement = new AdvancedMarkerElement({
|
||||
map,
|
||||
position: marker.position,
|
||||
title: marker.title,
|
||||
content: pinElement.element,
|
||||
zIndex: 1000 - index
|
||||
});
|
||||
this.createMarkerInfoWindows(markerElement, marker);
|
||||
});
|
||||
},
|
||||
async getLocationsFromAddresses(addresses) {
|
||||
createMarkerInfoWindows(markerElement, marker) {
|
||||
const directionsUrl = this.createGetDirectionsUrl(marker);
|
||||
const content = `<div>
|
||||
<span style="color: black; font-weight:bold"> ${marker.title} </span>
|
||||
${marker.addressLines?.map((address) => `<br/> ${address} `).join()}
|
||||
<br /> <a href="${directionsUrl}" target="_blank">Get directions</a>
|
||||
</div>`;
|
||||
|
||||
const infoWindow = new window.google.maps.InfoWindow({ content });
|
||||
|
||||
markerElement.addListener('click', () => {
|
||||
this.infoWindow?.close();
|
||||
this.infoWindow = infoWindow;
|
||||
infoWindow.open(markerElement.map, markerElement);
|
||||
});
|
||||
},
|
||||
createGetDirectionsUrl(marker) {
|
||||
return `https://maps.google.com/maps?saddr=&daddr=${encodeURIComponent(marker.fullAddress)}`;
|
||||
},
|
||||
async getLocationsFromAddresses(markers) {
|
||||
await this.setGeocoder();
|
||||
|
||||
const geocodeShopAddressPromises = addresses?.map((address) => this.geocoder.geocode({ address })) ?? [];
|
||||
const geocodeShopAddressPromises = markers?.map((marker) => this.geocoder.geocode({ address: marker.fullAddress })) ?? [];
|
||||
const geocodeShopAddressResults = await Promise.all(geocodeShopAddressPromises);
|
||||
return geocodeShopAddressResults.map((result) =>
|
||||
(result?.results?.length > 0
|
||||
? result.results[0].geometry?.location
|
||||
: null))
|
||||
.filter((loc) => loc != null);
|
||||
|
||||
return markers?.map((marker, index) => {
|
||||
const results = geocodeShopAddressResults[index]?.results;
|
||||
let position = null;
|
||||
if (results && results.length > 0) {
|
||||
position = results[0].geometry?.location;
|
||||
}
|
||||
|
||||
return { ...marker, position };
|
||||
}).filter((marker) => marker.position != null) ?? [];
|
||||
},
|
||||
async getBoundsFromAddress(address) {
|
||||
await this.setGeocoder();
|
||||
|
|
@ -72,16 +111,17 @@ export default {
|
|||
? result.results[0].geometry?.bounds
|
||||
: null;
|
||||
},
|
||||
async createMapWithMarkersForAddresses(addresses) {
|
||||
const shopPositions = await this.getLocationsFromAddresses(addresses);
|
||||
async createMapWithMarkersForAddresses(markers) {
|
||||
const markerPositions = await this.getLocationsFromAddresses(markers);
|
||||
const zipBounds = await this.getBoundsFromAddress(this.zipCodeAddress);
|
||||
const map = await this.getMap(zipBounds?.getCenter());
|
||||
|
||||
await this.addMarkersToMap(map, shopPositions);
|
||||
await this.addMarkersToMap(map, markerPositions);
|
||||
|
||||
const positionsToDisplay = shopPositions.concat(zipBounds?.getNorthEast(), zipBounds?.getSouthWest());
|
||||
const positionsToDisplay = markerPositions.map((marker) => marker.position)
|
||||
.concat(zipBounds?.getNorthEast(), zipBounds?.getSouthWest());
|
||||
const bounds = this.getBounds(positionsToDisplay);
|
||||
map?.fitBounds(bounds);
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ describe('TPA search page', () => {
|
|||
// Assert
|
||||
expect(map.exists()).toBeTruthy();
|
||||
expect(map.classes()).toContain('mb-4');
|
||||
expect(map.props().addresses).toEqual(expectedAddresses);
|
||||
expect(map.props().markers.map((p) => p.fullAddress)).toEqual(expectedAddresses);
|
||||
expect(map.props().zipCode).toBe(zipCode);
|
||||
});
|
||||
test('search radius filter', async () => {
|
||||
|
|
@ -582,7 +582,7 @@ describe('TPA search page', () => {
|
|||
];
|
||||
|
||||
// Act
|
||||
const result = wrapper.vm.providerAddresses;
|
||||
const result = wrapper.vm.providerAddresses.map((pa) => pa.fullAddress);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expectedResult);
|
||||
|
|
@ -1015,6 +1015,33 @@ describe('TPA search page', () => {
|
|||
['address', 'city', null, 'zip', 'Address, City zip'],
|
||||
['address', 'city', 'ST', null, 'Address, City, ST'],
|
||||
[null, null, null, null, '']
|
||||
])(
|
||||
'getFullProviderAddress returns expected',
|
||||
(streetAddress, city, state, zipCode, expected) => {
|
||||
// Arrange
|
||||
const { wrapper } = getMountedComponent();
|
||||
const provider = {
|
||||
address: {
|
||||
streetAddress,
|
||||
city,
|
||||
state,
|
||||
zipCode
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = wrapper.vm.getFullProviderAddress(provider);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
);
|
||||
test.each([
|
||||
['address', 'city', 'ST', 'zip', 'Address'],
|
||||
['address', null, 'ST', 'zip', 'Address'],
|
||||
['address', 'city', null, 'zip', 'Address'],
|
||||
['address', 'city', 'ST', null, 'Address'],
|
||||
[null, null, null, null, '']
|
||||
])(
|
||||
'getProviderAddress returns expected',
|
||||
(streetAddress, city, state, zipCode, expected) => {
|
||||
|
|
@ -1036,6 +1063,33 @@ describe('TPA search page', () => {
|
|||
expect(result).toEqual(expected);
|
||||
}
|
||||
);
|
||||
test.each([
|
||||
['address', 'city', 'ST', 'zip', 'City, ST zip'],
|
||||
['address', null, 'ST', 'zip', 'ST zip'],
|
||||
['address', 'city', null, 'zip', 'City zip'],
|
||||
['address', 'city', 'ST', null, 'City, ST'],
|
||||
[null, null, null, null, '']
|
||||
])(
|
||||
'getProviderCityZipState returns expected',
|
||||
(streetAddress, city, state, zipCode, expected) => {
|
||||
// Arrange
|
||||
const { wrapper } = getMountedComponent();
|
||||
const provider = {
|
||||
address: {
|
||||
streetAddress,
|
||||
city,
|
||||
state,
|
||||
zipCode
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = wrapper.vm.getProviderCityZipState(provider);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
);
|
||||
describe('getProviderButtonData', () => {
|
||||
test.each([null, undefined, {}])(
|
||||
'returns empty list when getTpaProviders returns no data',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<googleMap
|
||||
id="map"
|
||||
class="mb-4"
|
||||
:addresses="providerAddresses"
|
||||
:markers="providerAddresses"
|
||||
:zipCode="mapZipCode" />
|
||||
<Form
|
||||
id="providerSelectionForm"
|
||||
|
|
@ -281,10 +281,11 @@ export default {
|
|||
)?.replaceAll('{custom:radiusInMiles}', this.radiusInMiles);
|
||||
},
|
||||
providerAddresses() {
|
||||
return (
|
||||
this.providers?.map((provider) =>
|
||||
this.getProviderAddress(provider)) ?? []
|
||||
);
|
||||
return this.providers?.map((provider) => ({
|
||||
title: provider.companyName,
|
||||
fullAddress: this.getFullProviderAddress(provider),
|
||||
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
|
||||
})) ?? [];
|
||||
},
|
||||
providerButtonData() {
|
||||
return (
|
||||
|
|
@ -350,7 +351,17 @@ export default {
|
|||
this.providers = providers;
|
||||
this.selectedProviderNumber = providerNumber;
|
||||
},
|
||||
getFullProviderAddress(provider) {
|
||||
const addressLine1 = this.getProviderAddress(provider);
|
||||
const addressLine2 = this.getProviderCityZipState(provider);
|
||||
const joinString =
|
||||
addressLine1.length > 0 && addressLine2.length > 0 ? ', ' : '';
|
||||
return [addressLine1, addressLine2].join(joinString);
|
||||
},
|
||||
getProviderAddress(provider) {
|
||||
return toTitleCase(provider?.address?.streetAddress);
|
||||
},
|
||||
getProviderCityZipState(provider) {
|
||||
const city = toTitleCase(provider?.address?.city);
|
||||
const state = provider?.address?.state ?? '';
|
||||
const zipCode = provider?.address?.zipCode ?? '';
|
||||
|
|
@ -368,11 +379,7 @@ export default {
|
|||
if (zipCode) {
|
||||
addressLine2 += zipCode;
|
||||
}
|
||||
|
||||
const addressLine1 = toTitleCase(provider?.address?.streetAddress);
|
||||
const joinString =
|
||||
addressLine1.length > 0 && addressLine2.length > 0 ? ', ' : '';
|
||||
return [addressLine1, addressLine2].join(joinString);
|
||||
return addressLine2;
|
||||
},
|
||||
async getProviderButtonData() {
|
||||
this.reloadingProviders = true;
|
||||
|
|
@ -423,7 +430,7 @@ export default {
|
|||
return {
|
||||
buttonLabel: provider?.companyName ?? '',
|
||||
buttonLabelSubCopy: distance === null ? '' : `${distance} mi`,
|
||||
buttonBodyCopy: `${this.getProviderAddress(provider)}<br>${
|
||||
buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${
|
||||
cellNumber ?? ''
|
||||
}`,
|
||||
value: provider?.providerNumber ?? ''
|
||||
|
|
|
|||
Loading…
Reference in a new issue