diff --git a/src/iss-components/google-map/__snapshots__/google-map.spec.js.snap b/src/iss-components/google-map/__snapshots__/google-map.spec.js.snap index 74a1d74d..87f5298e 100644 --- a/src/iss-components/google-map/__snapshots__/google-map.spec.js.snap +++ b/src/iss-components/google-map/__snapshots__/google-map.spec.js.snap @@ -20,5 +20,6 @@ Object { ], }, }, + "infoWindow": null, } `; diff --git a/src/iss-components/google-map/google-map.spec.js b/src/iss-components/google-map/google-map.spec.js index b26d36a2..331c78e1 100644 --- a/src/iss-components/google-map/google-map.spec.js +++ b/src/iss-components/google-map/google-map.spec.js @@ -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); diff --git a/src/iss-components/google-map/google-map.vue b/src/iss-components/google-map/google-map.vue index 0c901dec..b3de8c7d 100644 --- a/src/iss-components/google-map/google-map.vue +++ b/src/iss-components/google-map/google-map.vue @@ -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 = `
+ ${marker.title} + ${marker.addressLines?.map((address) => `
${address} `).join()} +
Get directions +
`; + + 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); } } }; diff --git a/src/layouts/tpa-search/tpa-search.spec.js b/src/layouts/tpa-search/tpa-search.spec.js index 5687b9ca..393938b8 100644 --- a/src/layouts/tpa-search/tpa-search.spec.js +++ b/src/layouts/tpa-search/tpa-search.spec.js @@ -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', diff --git a/src/layouts/tpa-search/tpa-search.vue b/src/layouts/tpa-search/tpa-search.vue index 4aab4f05..de33f4bb 100644 --- a/src/layouts/tpa-search/tpa-search.vue +++ b/src/layouts/tpa-search/tpa-search.vue @@ -46,7 +46,7 @@
- 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)}
${ + buttonBodyCopy: `${this.getFullProviderAddress(provider)}
${ cellNumber ?? '' }`, value: provider?.providerNumber ?? ''