diff --git a/src/constants/analytics.js b/src/constants/analytics.js index 7649a96b..1cb1a160 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -1,5 +1,5 @@ -import packageNames from "./package-names"; -import { paymentMethods } from "./payment-method-constants"; +import packageNames from './package-names'; +import { paymentMethods } from './payment-method-constants'; const analyticsPageEvents = Object.freeze({ ENTRY: 'ENTRY', @@ -49,12 +49,12 @@ const analyticsPaymentTypeMap = new Map([ [paymentMethods.PAY_AT_TIME_OF_SERVICE, 'pay_at_service'], [paymentMethods.PAYPAL, 'pay_pal'], [paymentMethods.APPLEPAY, 'apple_pay'], -]) +]); const analyticsServicePackageMap = new Map([ [packageNames.TIER_ONE, 'basic'], [packageNames.TIER_TWO, 'essentials'], [packageNames.TIER_THREE, 'essentialsplus'] -]) +]); export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, analyticsPaymentTypeMap, analyticsServicePackageMap }; diff --git a/src/layouts/order-confirmation/order-confirmation.spec.js b/src/layouts/order-confirmation/order-confirmation.spec.js index af243e29..ed1f1922 100644 --- a/src/layouts/order-confirmation/order-confirmation.spec.js +++ b/src/layouts/order-confirmation/order-confirmation.spec.js @@ -14,6 +14,8 @@ import coverageType from '@/constants/coverage-type'; import { deepClone } from '@/helpers/object-helper'; import { AppointmentTypeStrings } from '@/constants/schedule-constants'; import { paymentMethods } from '@/constants/payment-method-constants'; +import partTypeStrings from '@/constants/part-type-strings'; +import packageNames from '@/constants/package-names'; jest.mock('@/helpers/layout-helper.js', () => jest.fn()); @@ -28,7 +30,8 @@ const mockMixin = { methods: { getCmsContent: jest.fn(), setCmsContent: jest.fn(), - savePageDataToStore: jest.fn() + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() } }; @@ -958,6 +961,250 @@ describe('OrderConfirmation.vue', () => { }); }); describe('Methods', () => { + describe('handleAnalyticsEvents', () => { + let mixin; + beforeEach(() => { + mixin = { + methods: { + getCmsContent: jest.fn(), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + }); + test('should call pushEventToGA with confirmation event for mobile repair', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.MOBILE; + order.damage.isRepair = true; + order.isVerified = false; + order.isMobileAppointment = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('mobile'), true); + }); + test('should call pushEventToGA with confirmation event for in-shop replace', () => { + // Arrange + const order = deepClone(sessionStorage); + order.serviceLocation.appointmentType = AppointmentTypeStrings.IN_SHOP; + order.damage.isRepair = false; + order.isVerified = true; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('confirmation', 'safelite', expect.stringContaining('replace'), true); + }); + test('should call pushEventToGA for payment PIA successful when payment.isPayInAdvance is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = true; + order.payment.paymentMethod = paymentMethods.CREDIT_CARD; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('payment_page', 'pia_successful', expect.any(String), true); + }); + test('should not call pushEventToGA for PIA when payment.isPayInAdvance is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.payment.isPayInAdvance = false; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const paymentPageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'payment_page'); + expect(paymentPageCalls.length).toBe(0); + }); + test('should call pushEventToGA for service package when submittedOrder.servicePackage exists', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = packageNames.TIER_ONE; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('service_package', 'package_purchased', expect.any(String), true); + }); + test('should not call pushEventToGA for service package when submittedOrder.servicePackage is null', () => { + // Arrange + const order = deepClone(sessionStorage); + order.servicePackage = null; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const servicePackageCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'service_package'); + expect(servicePackageCalls.length).toBe(0); + }); + test('should call pushEventToGA for total price when isFirstLoad is true and cart total greater than 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // Act + wrapper.vm.handleAnalyticsEvents(true); + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('total_price', expectedTotal.toString(), expect.any(String), true); + }); + test('should not call pushEventToGA for total price when isFirstLoad is false', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 100; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: expectedTotal }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + useMainStore().pageData = jest.fn().mockReturnValue({ isFirstLoad: false }); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should not call pushEventToGA for total price when cart total is 0', () => { + // Arrange + const order = deepClone(sessionStorage); + const expectedTotal = 0; + order.lineItems.glassParts = [{ partType: 'WINDSHIELD', sellingPrice: 0 }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + const totalPriceCalls = mixin.methods.pushEventToGA.mock.calls.filter(call => call[0] === 'total_price'); + expect(totalPriceCalls.length).toBe(0); + }); + test('should call pushEventToGA for rain defense purchased when hasRainRepel is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.RAIN_DEFENSE }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for rain defense no_purchase when hasRainRepel is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('rain_defense', 'no_purchase', expect.any(String), true); + }); + test('should call pushEventToGA for visitor_info_confirmation with ClientSite referring_site', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'referring_site', 'ClientSite', true); + }); + test('should call pushEventToGA for visitor_info_confirmation with client_name', () => { + // Arrange + const order = deepClone(sessionStorage); + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('visitor_info_confirmation', 'client_name', expect.any(String), true); + }); + test('should call pushEventToGA for wipers purchased when hasWipers is true', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = [{ partType: partTypeStrings.FRONT_WIPER }]; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const mixin = { + methods: { + getCmsContent: jest.fn().mockReturnValue([{ Text: 'Front Beam' }]), + setCmsContent: jest.fn(), + savePageDataToStore: jest.fn(), + pushEventToGA: jest.fn() + } + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'purchased', expect.any(String), true); + }); + test('should call pushEventToGA for wipers no_purchase when hasWipers is false', () => { + // Arrange + const order = deepClone(sessionStorage); + order.lineItems.vaps = []; + const mockStoreActions = () => { + useMainStore().getSubmittedOrder = jest.fn().mockImplementation(() => order); + }; + const { wrapper } = getMountedComponent({}, {}, mockStoreActions, mixin); + + // handleAnalyticsEvents is called on mount, no need to act + + // Assert + expect(mixin.methods.pushEventToGA).toHaveBeenCalledWith('wipers', 'no_purchase', 'none_none', true); + }); + });; describe('formatDate', () => { test.each([ ['Wednesday, April 22, 2020', '2020-04-22'], diff --git a/src/layouts/order-confirmation/order-confirmation.vue b/src/layouts/order-confirmation/order-confirmation.vue index dd8a3198..063346f2 100644 --- a/src/layouts/order-confirmation/order-confirmation.vue +++ b/src/layouts/order-confirmation/order-confirmation.vue @@ -627,7 +627,7 @@ export default { handleAnalyticsEvents(isFirstLoad) { const mobileOrInshop = this.submittedOrder.isMobileAppointment ? 'mobile' : 'in_shop'; const verifiedOrNotVerified = this.submittedOrder.isVerified ? 'verified' : 'not_verified'; - const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; + const repairOrReplace = this.submittedOrder.isRepair ? 'repair' : 'replace'; this.pushEventToGA('confirmation', 'safelite', `${mobileOrInshop}_${repairOrReplace}_${verifiedOrNotVerified}`, true); if (this.payment.isPayInAdvance) { @@ -651,7 +651,7 @@ export default { } else { coverageType = 'unverified'; } - const recalibrationType = this.submittedOrder.lineItems.glassParts.find((part) => part.requiresRecalibration).recalibrationType; + const recalibrationType = this.submittedOrder.lineItems.glassParts?.find((part) => part.requiresRecalibration)?.recalibrationType; this.pushEventToGA(`recalibration_scheduled_${coverageType}`, this.vehicle.carId, `recal_type_${recalibrationType}`.replace(/ /g, '_').toLowerCase(), true); } diff --git a/src/layouts/payment-page-adyen/payment-page-adyen.vue b/src/layouts/payment-page-adyen/payment-page-adyen.vue index cdf6529f..8608bc29 100644 --- a/src/layouts/payment-page-adyen/payment-page-adyen.vue +++ b/src/layouts/payment-page-adyen/payment-page-adyen.vue @@ -551,7 +551,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); this.hasPaymentFailureError = true; @@ -567,7 +567,7 @@ export default { await global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); this.hasPaymentFailureError = true; diff --git a/src/layouts/payment-return-adyen/payment-return-adyen.vue b/src/layouts/payment-return-adyen/payment-return-adyen.vue index 33c35194..85e1888b 100644 --- a/src/layouts/payment-return-adyen/payment-return-adyen.vue +++ b/src/layouts/payment-return-adyen/payment-return-adyen.vue @@ -121,7 +121,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_failed', paymentMethodForAnalytics, true, null, result?.resultCode); } @@ -137,7 +137,7 @@ export default { global.$logger.logError(stringToLog); const paymentMethodForAnalytics = analyticsPaymentTypeMap.get(this.piaType); - this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, true, true, null, 0); + this.pushEventToGA('transaction_declined_displayed', `${paymentMethodForAnalytics}_declined`, 'true', true, null, 0); this.pushEventToGA('payment_page', 'pia_error', paymentMethodForAnalytics, true, null, error?.name); }