diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 50ecb0eb..3e06d0dc 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -88,6 +88,7 @@ stages:
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
+ __VUE_APP_GOOGLE_MAPS_API_SCRIPT__: $(__VUE_APP_GOOGLE_MAPS_API_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters:
awsCliContainer: awscli
@@ -137,6 +138,7 @@ stages:
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
+ __VUE_APP_GOOGLE_MAPS_API_SCRIPT__: $(__VUE_APP_GOOGLE_MAPS_API_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters:
awsCliContainer: awscli
@@ -186,6 +188,7 @@ stages:
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
+ __VUE_APP_GOOGLE_MAPS_API_SCRIPT__: $(__VUE_APP_GOOGLE_MAPS_API_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters:
awsCliContainer: awscli
@@ -235,6 +238,7 @@ stages:
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
+ __VUE_APP_GOOGLE_MAPS_API_SCRIPT__: $(__VUE_APP_GOOGLE_MAPS_API_SCRIPT__)
- template: templates/digital/invalidate-cloudfront-cache.yml@AzureDevOps
parameters:
awsCliContainer: awscli
diff --git a/public/index.html b/public/index.html
index 7edb81cc..6db2d816 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,6 +5,7 @@
window.dataLayer = [{}];
+
diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js
index a24aa40a..fbbf0400 100644
--- a/src/constants/endpoints.js
+++ b/src/constants/endpoints.js
@@ -140,7 +140,7 @@ const endpoints = Object.freeze({
method: 'GET'
},
GooglePlaces: {
- url: 'https://maps.googleapis.com/maps/api/js?key={apiKey}&libraries=places'
+ url: (apiKey) => `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`
},
ValidateClientTag: {
url: '/clientauth/api/v1/clientauth/validate-client-tag',
diff --git a/src/iss-components/address-questions/address-questions.vue b/src/iss-components/address-questions/address-questions.vue
index 39144ad2..665a3772 100644
--- a/src/iss-components/address-questions/address-questions.vue
+++ b/src/iss-components/address-questions/address-questions.vue
@@ -196,7 +196,7 @@ export default {
const addressField1 = document.getElementById('autocomplete');
const self = this;
- const url = endpoints.GooglePlaces.url.replace('{apiKey}', applicationConfig.GOOGLE_PLACES_API_KEY);
+ const url = endpoints.GooglePlaces.url(applicationConfig.GOOGLE_PLACES_API_KEY);
this.$loadScript(url)
.then(() => {
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
new file mode 100644
index 00000000..efab5bbf
--- /dev/null
+++ b/src/iss-components/google-map/__snapshots__/google-map.spec.js.snap
@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Google Map should render expected initial data 1`] = `
+Object {
+ "country": "USA",
+ "geocoder": null,
+}
+`;
diff --git a/src/iss-components/google-map/google-map.spec.js b/src/iss-components/google-map/google-map.spec.js
new file mode 100644
index 00000000..2c4ad709
--- /dev/null
+++ b/src/iss-components/google-map/google-map.spec.js
@@ -0,0 +1,428 @@
+import { shallowMount } from '@vue/test-utils';
+import googleMap from '@/iss-components/google-map/google-map.vue';
+
+const mockFitBounds = jest.fn();
+const mockMapInstance = {
+ fitBounds: mockFitBounds
+};
+const mockMap = jest.fn(() => mockMapInstance);
+
+const mockGeocode = jest.fn();
+const mockGeocoderInstance = {
+ geocode: mockGeocode
+};
+const mockGeocoder = jest.fn(() => (mockGeocoderInstance));
+
+const mockAdvancedMarkerElement = jest.fn();
+
+const mockImportLibrary = jest.fn().mockImplementation(() => (Promise.resolve({
+ Map: mockMap,
+ Geocoder: mockGeocoder,
+ AdvancedMarkerElement: mockAdvancedMarkerElement
+})));
+
+const mockExtend = jest.fn();
+const mockLatLngBound = { extend: mockExtend };
+
+const setupGoogleMock = () => {
+ global.window.google = {
+ maps: {
+ importLibrary: mockImportLibrary,
+ LatLngBounds: jest.fn(() => (mockLatLngBound))
+ }
+ };
+};
+
+beforeEach(() => {
+ // clear constructors
+ mockMap.mockClear();
+ mockGeocoder.mockClear();
+ mockAdvancedMarkerElement.mockClear();
+
+ // clear methods
+ mockExtend.mockClear();
+ mockGeocode.mockClear();
+ mockFitBounds.mockClear();
+
+ setupGoogleMock();
+});
+
+describe('Google Map', () => {
+ describe('should render', () => {
+ test('expected initial data', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ await wrapper.vm.$nextTick();
+
+ // Assert
+ expect(wrapper.vm.$data).toMatchSnapshot();
+ });
+ test('map', () => {
+ // Arrange
+ const mapReference = '#map';
+ const wrapper = shallowMount(googleMap, {});
+
+ // Act
+ const map = wrapper.find(mapReference);
+
+ // Assert
+ expect(map.exists()).toBeTruthy();
+ });
+ });
+ test.each([
+ ['13001', '13001 USA'],
+ ['9', '9 USA'],
+ ['', ' USA'],
+ [null, ' USA']
+ ])(
+ 'given that zipcode set to "%p", computed zipCodeAddress should return "%p"',
+ async (zipCode, expected) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ await wrapper.setData({ zipCode });
+
+ // Act
+ const result = wrapper.vm.zipCodeAddress;
+
+ // Assert
+ expect(result).toBe(expected);
+ }
+ );
+ test('watch addresses creates map with expected markers', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const zipCode = '00882';
+ await wrapper.setData({ zipCode });
+
+ const mockGetCenter = jest.fn();
+ const mockNorthEast = jest.fn();
+ const mockSouthWest = jest.fn();
+ const zipCodeGeocodeResult = {
+ results: [
+ {
+ geometry: {
+ bounds:
+ {
+ getCenter: mockGetCenter,
+ getNorthEast: mockNorthEast,
+ getSouthWest: mockSouthWest
+ }
+ }
+ }
+ ]
+ };
+ const [address1, address2, position1, position2] = ['a1', 'a2', 'p1', 'p2'];
+ mockGeocode.mockImplementation((obj) => {
+ let result = {};
+ if (obj.address.includes(zipCode)) {
+ result = zipCodeGeocodeResult;
+ } else if (obj.address === address1) {
+ result = {
+ results: [
+ { geometry: { location: position1 } }
+ ]
+ };
+ } else if (obj.address === address2) {
+ result = {
+ results: [
+ { geometry: { location: position2 } }
+ ]
+ };
+ }
+ return Promise.resolve(result);
+ });
+ const newAddresses = [address1, address2];
+
+ // Act
+ await wrapper.vm.$options.watch.addresses.call(wrapper.vm, newAddresses);
+
+ for (let i = 0; i < 11; i++) {
+ // eslint-disable-next-line no-await-in-loop
+ await wrapper.vm.$nextTick();
+ }
+
+ // Assert
+ expect(mockMap).toBeCalledTimes(1);
+ expect(mockAdvancedMarkerElement).toBeCalledTimes(2);
+ expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
+ map: expect.anything(),
+ position: position1
+ }));
+ expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
+ map: expect.anything(),
+ position: position2
+ }));
+ expect(mockGeocoder).toBeCalledTimes(1);
+
+ expect(mockExtend).toBeCalledTimes(4);
+ expect(mockFitBounds).toBeCalledTimes(1);
+
+ expect(mockGetCenter).toBeCalledTimes(1);
+ expect(mockNorthEast).toBeCalledTimes(1);
+ expect(mockSouthWest).toBeCalledTimes(1);
+ });
+ describe('method', () => {
+ test('getMap calls map constructor with expected parameters', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const center = { lat: 90, lng: -5 };
+
+ // Act
+ const result = await wrapper.vm.getMap(center);
+
+ // Assert
+ expect(mockMap).toHaveBeenCalledWith(
+ null,
+ expect.objectContaining({ center, mapId: 'map_id', mapTypeControl: false })
+ );
+ expect(result).toBe(mockMapInstance);
+ });
+ test.each([
+ [undefined, 0],
+ [null, 0],
+ [[], 0],
+ [['loc1'], 1],
+ [['loc1', 'loc1', 'loc1', 'loc1'], 4]
+ ])(
+ 'given parameter %p, getBounds calls extend %p time(s)',
+ (locations, numberOfExtendCalls) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+
+ // Act
+ const result = wrapper.vm.getBounds(locations);
+
+ // Assert
+ expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCalls);
+ expect(result).toBe(mockLatLngBound);
+ }
+ );
+ test.each([
+ [1, null, mockGeocoderInstance],
+ [1, undefined, mockGeocoderInstance],
+ [0, {}, {}],
+ [0, { value: 'any value' }, { value: 'any value' }]
+ ])(
+ 'setGeocoder calls Geocoder constructor %p time(s) when original geocoder %p and geocoder set to %p',
+ async (numberOfCalls, originalGeocoder, newGeocoder) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ await wrapper.setData({ geocoder: originalGeocoder });
+
+ // Act
+ await wrapper.vm.setGeocoder();
+
+ // Assert
+ expect(mockGeocoder).toHaveBeenCalledTimes(numberOfCalls);
+ expect(wrapper.vm.geocoder).toEqual(newGeocoder);
+ }
+ );
+ test.each([
+ [null, 0],
+ [undefined, 0],
+ [[], 0],
+ [['p1'], 1],
+ [['p1', 'p2', 'p3'], 3]
+ ])(
+ 'addMarkersToMap given positions argument %p, creates AdvancedMarkerElement %p time(s)',
+ async (positions, numberOfCalls) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const map = jest.fn();
+
+ // Act
+ await wrapper.vm.addMarkersToMap(map, positions);
+
+ // Assert
+ expect(mockAdvancedMarkerElement).toHaveBeenCalledTimes(numberOfCalls);
+ }
+ );
+ describe('getLocationsFromAddresses', () => {
+ test.each([
+ [null, 0],
+ [undefined, 0],
+ [[], 0],
+ [['a1'], 1],
+ [['a1', 'a2', 'a3'], 3]
+ ])(
+ 'given address(es) %p calls geocode %p times',
+ async (addresses, numberOfCalls) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+
+ // Act
+ await wrapper.vm.getLocationsFromAddresses(addresses);
+
+ // Assert
+ expect(mockGeocode).toHaveBeenCalledTimes(numberOfCalls);
+ expect(wrapper.vm.geocoder).not.toBeNull();
+ }
+ );
+ test('only returns geocode results that are not null', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const addresses = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8'];
+ mockGeocode.mockImplementationOnce(() => null)
+ .mockImplementationOnce(() => ({ results: null }))
+ .mockImplementationOnce(() => ({ results: [] }))
+ .mockImplementationOnce(() => ({ results: [{}] }))
+ .mockImplementationOnce(() => (
+ { results: [{ geometry: undefined }] }
+ ))
+ .mockImplementationOnce(() => (
+ { results: [{ geometry: {} }] }
+ ))
+ .mockImplementationOnce(() => (
+ {
+ results: [
+ { geometry: { location: 'some value' } }
+ ]
+ }
+ ))
+ .mockImplementationOnce(() => (
+ {
+ results: [
+ { geometry: { location: 'some other value' } }
+ ]
+ }
+ ));
+ const expectedResult = ['some value', 'some other value'];
+
+ // Act
+ const result = await wrapper.vm.getLocationsFromAddresses(addresses);
+
+ // Assert
+ expect(result.length).toBe(2);
+ expect(result).toEqual(expectedResult);
+ });
+ });
+ describe('getBoundsFromAddress', () => {
+ test('returns expected when geocode returns valid result', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const address = 'random address';
+ const expectedResult = 'bounds returned';
+ const geocodeResult = {
+ results: [
+ {
+ geometry: { bounds: expectedResult }
+ }
+ ]
+ };
+ mockGeocode.mockImplementationOnce(() => Promise.resolve(geocodeResult));
+
+ // Act
+ const result = await wrapper.vm.getBoundsFromAddress(address);
+
+ // Assert
+ expect(result).toBe(expectedResult);
+ expect(wrapper.vm.geocoder).not.toBeNull();
+ });
+ test('returns null when geocode returns invalid result', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const address = 'random address';
+ mockGeocode.mockImplementationOnce(() => Promise.resolve({}));
+
+ // Act
+ const result = await wrapper.vm.getBoundsFromAddress(address);
+
+ // Assert
+ expect(result).toBeNull();
+ expect(wrapper.vm.geocoder).not.toBeNull();
+ });
+ });
+ describe('createMapWithMarkersForAddresses', () => {
+ test('calls expected when addresses provided', async () => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const zipCode = '00882';
+ await wrapper.setData({ zipCode });
+ const mockGetCenter = jest.fn();
+ const mockNorthEast = jest.fn();
+ const mockSouthWest = jest.fn();
+ const zipCodeGeocodeResult = {
+ results: [
+ {
+ geometry: {
+ bounds:
+ {
+ getCenter: mockGetCenter,
+ getNorthEast: mockNorthEast,
+ getSouthWest: mockSouthWest
+ }
+ }
+ }
+ ]
+ };
+ const shopGeocodeResult = {
+ results: [
+ { geometry: { location: 'valid location' } }
+ ]
+ };
+ mockGeocode.mockImplementation((obj) =>
+ Promise.resolve(obj.address.includes(zipCode)
+ ? zipCodeGeocodeResult
+ : shopGeocodeResult));
+ const addresses = ['a1', 'a2'];
+
+ // Act
+ await wrapper.vm.createMapWithMarkersForAddresses(addresses);
+
+ // Assert
+ expect(mockMap).toBeCalledTimes(1);
+ expect(mockAdvancedMarkerElement).toBeCalledTimes(2);
+ expect(mockGeocoder).toBeCalledTimes(1);
+
+ expect(mockExtend).toBeCalledTimes(4);
+ expect(mockFitBounds).toBeCalledTimes(1);
+
+ expect(mockGetCenter).toBeCalledTimes(1);
+ expect(mockNorthEast).toBeCalledTimes(1);
+ expect(mockSouthWest).toBeCalledTimes(1);
+ });
+ test.each([null, undefined, []])(
+ 'calls expected when addresses argument is "%p"',
+ async (addresses) => {
+ // Arrange
+ const wrapper = shallowMount(googleMap, {});
+ const zipCode = '00882';
+ await wrapper.setData({ zipCode });
+ const mockGetCenter = jest.fn();
+ const mockNorthEast = jest.fn();
+ const mockSouthWest = jest.fn();
+ const zipCodeGeocodeResult = {
+ results: [
+ {
+ geometry: {
+ bounds:
+ {
+ getCenter: mockGetCenter,
+ getNorthEast: mockNorthEast,
+ getSouthWest: mockSouthWest
+ }
+ }
+ }
+ ]
+ };
+ mockGeocode.mockImplementation(() => Promise.resolve(zipCodeGeocodeResult));
+
+ // Act
+ await wrapper.vm.createMapWithMarkersForAddresses(addresses);
+
+ // Assert
+ expect(mockMap).toBeCalledTimes(1);
+ expect(mockAdvancedMarkerElement).toBeCalledTimes(0);
+ expect(mockGeocoder).toBeCalledTimes(1);
+
+ expect(mockExtend).toBeCalledTimes(2);
+ expect(mockFitBounds).toBeCalledTimes(1);
+
+ expect(mockGetCenter).toBeCalledTimes(1);
+ expect(mockNorthEast).toBeCalledTimes(1);
+ expect(mockSouthWest).toBeCalledTimes(1);
+ }
+ );
+ });
+ });
+});
diff --git a/src/iss-components/google-map/google-map.vue b/src/iss-components/google-map/google-map.vue
new file mode 100644
index 00000000..fa5a57d8
--- /dev/null
+++ b/src/iss-components/google-map/google-map.vue
@@ -0,0 +1,93 @@
+
+