diff --git a/jest.config.js b/jest.config.js index aaf2d9ff8..0a6fbc048 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,8 +26,7 @@ module.exports = { testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], coverageThreshold: { global: { - statements: 75, - // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 + statements: 80, }, }, // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit diff --git a/src/constants/analytics.js b/src/constants/analytics.js index b5b24660d..2b0fc1a8f 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -12,6 +12,7 @@ const GaEvents = { const GaCategories = { API_RESPONSE: "Api_Response", EVOX: "Evox", + FUNNEL_ENTRY: "funnel_entry", }; const GaActions = { @@ -20,6 +21,7 @@ const GaActions = { VIF: "vif", SUBMITTED: "Submitted", DISPLAYED: "Displayed", + ZIP_CODE_PROVIDED: "zip_code_provided", }; const GaLabels = { diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index c2a297823..fc0daf323 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -62,6 +62,7 @@ const storeActions = { SAVE_VEHICLE_MAKE: "saveVehicleMake", SAVE_VEHICLE_MODEL: "saveVehicleModel", SAVE_VEHICLE_STYLE: "saveVehicleStyle", + SAVE_VEHICLE: "saveVehicle", SAVE_VEHICLE_DAMAGE: "saveVehicleDamage", SAVE_VIN_LOOKUP: "saveVinLookup", SAVE_SERVICE_ZIP_CODE_INFO: "saveServiceZipCodeInfo", diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index a53958dd5..13c81f815 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -74,6 +74,9 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index 1c38a72c2..bd1ce8fe2 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -44,7 +44,8 @@ type="radio" name="day-of-month" v-model="selectedDate" - @click="fireDateClickedEvent" + @click="fireDateSelectedEvent" + @keypress.enter="fireDateSelectedEvent" :value="date.inputValue" :id="`${month.monthLabel}-${date.dateNum.toString()}`" />
@@ -85,6 +93,7 @@ import textBlock from "@/digital-components/text-block/text-block"; import vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehicle-review"; import damageReview from "@/layouts/review/review-sections/damage-review/damage-review"; import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review"; +import serviceLocationReview from "@/layouts/review/review-sections/service-location-review/service-location-review"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; @@ -102,6 +111,8 @@ export default { storeActions.GET_RAIN_DEFENSE ); + const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData(); + // Settle promises and get results const promiseResultMap = [ { @@ -109,12 +120,8 @@ export default { promise: cmsContentPromise, }, { - resultKey: "wipers", - promise: wipersPromise, - }, - { - resultKey: "rainDefense", - promise: rainDefensePromise, + resultKey: "servicePackageData", + promise: servicePackageInitialDataPromise, }, ]; @@ -123,21 +130,19 @@ export default { next((vm) => { vm.setCmsContent(resultMap.cmsContent); - vm.availableWipers = resultMap.wipers; - vm.availableRainDefense = [resultMap.rainDefense]; + vm.$refs.servicePackageReview.initializeComponent(resultMap.servicePackageData); }); }, data() { - return { - availableWipers: null, - availableRainDefense: null, - }; + return {}; }, methods: { arePagePrerequisitesValid() { return true; }, - backButtonAction() {}, + backButtonAction() { + this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); + }, forwardButtonAction() {}, editVehicle() { this.$router.navigateWithoutSaving( @@ -157,6 +162,12 @@ export default { this.$route ); }, + editServiceLocation() { + this.$router.navigateWithoutSaving( + this.navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT, + this.$route + ); + }, }, computed: { subHeaderTitle() { @@ -174,12 +185,12 @@ export default { damageInfo() { return this.$store.getters.damage; }, - availableVaps() { - return [...(this.availableWipers ?? []), ...(this.availableRainDefense ?? [])]; - }, lineItems() { return this.$store.getters.lineItems; }, + serviceLocationInfo() { + return this.$store.getters.order.serviceLocation; + }, }, components: { funnelHeader, @@ -190,6 +201,7 @@ export default { vehicleReview, damageReview, servicePackageReview, + serviceLocationReview, }, }; diff --git a/src/layouts/schedule/schedule.spec.js b/src/layouts/schedule/schedule.spec.js new file mode 100644 index 000000000..654e0e4c9 --- /dev/null +++ b/src/layouts/schedule/schedule.spec.js @@ -0,0 +1,228 @@ +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import { applicationConfig } from "@/constants/application-config"; +import { storeActions } from "@/constants/store-actions"; +import schedule from "@/layouts/schedule/schedule.vue"; +import store from "@/store"; +import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper"; +import { nextTick } from "vue"; + +jest.mock("@/store", () => ({ + commit: jest.fn(), + dispatch: jest.fn(), +})); + +// Mock fetchCmsContentForPage +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: () => Promise.resolve("content"), + splitCopyOnCMSPlaceHolder: jest.fn(() => ["A", "B"]), +})); + +jest.mock( + "@/store", + () => { + return {}; + }, + { virtual: true } +); + +store.getters = { + order: { + schedule: { + date: "2020-01-01", + }, + lineItems: { + glassParts: [], + supportingItems: [], + }, + serviceLocation: { + appointmentType: "Inshop", + }, + }, + lineItems: { + glassParts: [], + supportingItems: [], + }, +}; + +describe("schedule.vue", () => { + test("Should navigateWithoutSaving", async () => { + //Arrange + const { wrapper } = setupMocks({ + customMountOptions: { + router: { + navigateWithoutSaving: jest.fn(), + }, + route: { schedule }, + }, + }); + + wrapper.vm.dispatchStoreAction = jest.fn(() => { + return { + data: [], + }; + }); + + //Act + await wrapper.vm.forwardButtonAction(); + + //Assert + expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled(); + }); + test("should pass arePagePrerequisitesValid with a mobile order and no providerNumber", () => { + //Arrange + const { wrapper } = setupMocks({}); + store.getters = { + order: { + schedule: { + date: "2020-01-01", + }, + serviceLocation: { + zipCode: "12345", + zipCodeCtu: "value", + appointmentType: "Mobile", + }, + damage: { + isRepair: true, + }, + referralNumber: "1234567", + }, + payment: { + isInsurance: true, + }, + lineItems: { + supportingItems: [], + }, + }; + + let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + expect(arePagePrerequisitesValid).toBe(true); + }); + test("should pass arePagePrerequisitesValid with a inshop order and providerNumber", () => { + //Arrange + const { wrapper } = setupMocks({}); + store.getters = { + order: { + schedule: { + date: "2020-01-01", + }, + serviceLocation: { + zipCode: "12345", + zipCodeCtu: "value", + appointmentType: "Inshop", + provider: { + providerNumber: "5", + }, + }, + damage: { + isRepair: true, + }, + referralNumber: "1234567", + }, + payment: { + isInsurance: true, + }, + lineItems: { + supportingItems: [], + }, + }; + + let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + expect(arePagePrerequisitesValid).toBe(true); + }); + test("should fail arePagePrerequisitesValid with a replace with no glass parts", () => { + //Arrange + const { wrapper } = setupMocks({}); + store.getters = { + order: { + schedule: { + date: "2020-01-01", + }, + serviceLocation: { + zipCode: "12345", + zipCodeCtu: "value", + appointmentType: "Inshop", + provider: { + providerNumber: "5", + }, + }, + damage: { + isRepair: false, + }, + referralNumber: "1234567", + }, + payment: { + isInsurance: true, + }, + lineItems: { + supportingItems: [], + glassParts: [], + }, + }; + + let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + expect(arePagePrerequisitesValid).toBe(false); + }); + test("should fail arePagePrerequisitesValid without isInsurance", () => { + //Arrange + const { wrapper } = setupMocks({}); + store.getters = { + order: { + schedule: { + date: "2020-01-01", + }, + serviceLocation: { + zipCode: "12345", + zipCodeCtu: "value", + appointmentType: "Inshop", + provider: { + providerNumber: "5", + }, + }, + damage: { + isRepair: true, + }, + referralNumber: "1234567", + }, + payment: { + isInsurance: null, + }, + lineItems: { + supportingItems: [], + glassParts: [], + }, + }; + + let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid(); + + expect(arePagePrerequisitesValid).toBe(false); + }); +}); + +const mockCmsContent = {}; + +function setupMocks({ customMountOptions }) { + const mountOptions = getMountOptions({ + ...customMountOptions, + }); + + mountOptions.global.mocks["$store"] = store; + mountOptions["attachTo"] = document.body; + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { + if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) + return mockCmsContent[widgetName][fieldName]; + }), + }, + }, + ]; + + const wrapper = shallowMount(schedule, mountOptions); + wrapper.vm.setCmsContent = jest.fn(); + return { wrapper }; +} diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.spec.js b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.spec.js new file mode 100644 index 000000000..43d997892 --- /dev/null +++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.spec.js @@ -0,0 +1,712 @@ +import { shallowMount, flushPromises } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import timeSlotModalQuestion from "@/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue"; +import store from "@/store"; +import { + RouteCodeFlags, + PREMIUM_TIME_SLOT_ID_FLAG, + PREMIUM_FEE_PART_TYPE, +} from "@/constants/schedule-constants"; + +describe("time-slot-modal-list-button-question.vue", () => { + test("When selected time slot is changed, a correctly formatted selectedTimeSlot should be emitted", async () => { + //Arrange + const date = "2023-07-01"; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + const expectedEmit = [ + [ + { + date: date, + routeCode: timeSlotId, + startTime: startTime, + endTime: endTime, + jobMaxMinutes: jobMaxMinutes.toString(), + }, + ], + ]; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + }, + }, + }); + + wrapper.vm.$refs.timeSlots.closeModal = jest.fn(); + wrapper.vm.selectedTimeSlotId = timeSlotId; + + //Act + wrapper.vm.setSelectedTimeSlot(); + + //Assert + expect(wrapper.emitted()["update:modelValue"]).toEqual(expectedEmit); + }); +}); +describe("time-slot-modal-list-button-question.vue Supplemental information", () => { + test("The correct supplemental information should be obtained from CMS - Mobile - Not Premium ", async () => { + //Arrange + const mobileCmsWidgetName = "MobileNotPremium"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "test" }, + appointmentType: "Mobile", + mobileCmsWidgetName: mobileCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toEqual( + mockCmsContent[mobileCmsWidgetName]["BodyText"] + ); + }); + test("The correct supplemental information should be obtained from CMS - Mobile - Premium", async () => { + //Arrange + const mobileCmsWidgetName = "MobilePremium"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "test" + PREMIUM_TIME_SLOT_ID_FLAG }, + appointmentType: "Mobile", + mobileCmsWidgetName: mobileCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toEqual( + mockCmsContent[mobileCmsWidgetName]["BodyText"] + ); + }); + test("No supplemental information should be obtained from CMS - Inshop", async () => { + //Arrange + const inshopWidgetName = "Inshop"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "test" }, + appointmentType: "Inshop", + cmsWidgetName: inshopWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toBeNull(); + }); + test("No supplemental information should be obtained from CMS - Drop off, nothing selected", async () => { + //Arrange + const dropOffWidgetName = "Dropoff"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: null, + appointmentType: "Dropoff", + cmsWidgetName: dropOffWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toBeNull(); + }); + test("Correct supplemental information should be retrieved, Dropoff regular", async () => { + //Arrange + const dropOffWidgetName = "Dropoff"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + dropoffCmsWidgetName: dropOffWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toEqual( + mockCmsContent[dropOffWidgetName]["BodyText"] + ); + }); + test("Correct supplemental information should be retrieved, Dropoff Overnight", async () => { + //Arrange + const overnightDropOffCmsWidgetName = "Overnight"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF }, + appointmentType: "Dropoff", + overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.supplementalInformationBlock).toEqual( + mockCmsContent[overnightDropOffCmsWidgetName]["BodyText"] + ); + }); + test("Correct supplemental information should be retrieved, Dropoff Sameday", async () => { + //Arrange + const sameDayDropOffCmsWidgetName = "Sameday"; + const date = new Date().toISOString().split("T")[0]; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName, + }, + }, + }); + + //Assert + expect(wrapper.vm.supplementalInformationBlock).toEqual( + mockCmsContent[sameDayDropOffCmsWidgetName]["BodyText"] + ); + }); +}); + +describe("time-slot-modal-list-button-question.vue Disclaimer", () => { + test("The correct disclaimer information should be obtained from CMS - Dropoff - Sameday ", async () => { + //Arrange + const sameDayDropOffCmsWidgetName = "Sameday"; + const date = new Date().toISOString().split("T")[0]; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName, + }, + }, + }); + + //Assert + expect(wrapper.vm.disclaimerTextBlockCopy).toEqual( + mockCmsContent[sameDayDropOffCmsWidgetName]["FooterText"] + ); + }); + test("Correct disclaimer information should be retrieved, Dropoff regular", async () => { + //Arrange + const dropOffWidgetName = "Dropoff"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + dropoffCmsWidgetName: dropOffWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.disclaimerTextBlockCopy).toEqual( + mockCmsContent[dropOffWidgetName]["FooterText"] + ); + }); + test("Correct disclaimer information should be retrieved, Dropoff overnight", async () => { + //Arrange + const overnightDropOffCmsWidgetName = "Overnight"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF }, + appointmentType: "Dropoff", + overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.disclaimerTextBlockCopy).toEqual( + mockCmsContent[overnightDropOffCmsWidgetName]["FooterText"] + ); + }); + test("No disclaimer information should be retrieved, non-dropoff", async () => { + //Arrange + const inshopWidgetName = "Inshop"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "test" }, + appointmentType: "Inshop", + cmsWidgetName: inshopWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.disclaimerTextBlockCopy).toBeNull(); + }); +}); + +describe("time-slot-modal-list-button-question.vue Duration", () => { + test("The correct duration information should be displayed - Dropoff - Sameday ", async () => { + //Arrange + const sameDayDropOffCmsWidgetName = "Sameday"; + const date = new Date().toISOString().split("T")[0]; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName, + }, + }, + }); + + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[sameDayDropOffCmsWidgetName]["SubheaderText"] + ); + }); + test("Correct duration information should be retrieved, Dropoff overnight", async () => { + //Arrange + const overnightDropOffCmsWidgetName = "Overnight"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.OVERNIGHT_DROP_OFF }, + appointmentType: "Dropoff", + overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[overnightDropOffCmsWidgetName]["SubheaderText"] + ); + }); + test("Correct duration information should be retrieved, Dropoff", async () => { + //Arrange + const dropoffCmsWidgetName = "Dropoff"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + modelValue: { routeCode: "Test" + RouteCodeFlags.ALL_DAY_DROP_OFF }, + appointmentType: "Dropoff", + dropoffCmsWidgetName: dropoffCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[dropoffCmsWidgetName]["SubheaderText"] + ); + }); + test("Correct duration information should be retrieved, Inshop - convert to hours", async () => { + //Arrange + const cmsWidgetName = "Inshop"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: 180, + estimatedServiceMinutesMinimum: 120, + modelValue: { routeCode: "Test" }, + appointmentType: "Inshop", + cmsWidgetName: cmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[cmsWidgetName]["SubheaderText"] + " 2 - 3 hours" + ); + }); + test("Correct duration information should be retrieved, Inshop - convert to minutes", async () => { + //Arrange + const cmsWidgetName = "Inshop"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: 90, + estimatedServiceMinutesMinimum: 60, + modelValue: { routeCode: "Test" }, + appointmentType: "Inshop", + cmsWidgetName: cmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[cmsWidgetName]["SubheaderText"] + " 60 - 90 minutes" + ); + }); + test("Correct duration information should be retrieved, Inshop - min = maximum time", async () => { + //Arrange + const cmsWidgetName = "Inshop"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: 240, + estimatedServiceMinutesMinimum: 240, + modelValue: { routeCode: "Test" }, + appointmentType: "Inshop", + cmsWidgetName: cmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toEqual( + mockCmsContent[cmsWidgetName]["SubheaderText"] + " 4 hours" + ); + }); + test("No duration information should be displayed, Mobile", async () => { + //Arrange + const mobileCmsWidgetName = "Mobile"; + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: 240, + estimatedServiceMinutesMinimum: 240, + modelValue: { routeCode: "Test" }, + appointmentType: "Mobile", + mobileCmsWidgetName: mobileCmsWidgetName, + }, + }, + }); + //Assert + expect(wrapper.vm.durationTextBlockCopy).toBeNull(); + }); +}); + +describe("time-slot-modal-list-button-question.vue Available Timeslots", () => { + test("Available Timeslots should be formatted correctly - Dropoff - Sameday ", async () => { + //Arrange + const sameDayDropOffCmsWidgetName = "Sameday"; + const date = new Date().toISOString().split("T")[0]; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + appointmentType: "Dropoff", + sameDayDropOffCmsWidgetName: sameDayDropOffCmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { + buttonLabel: mockCmsContent[sameDayDropOffCmsWidgetName]["HeaderText"], + value: timeSlotId, + }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); + test("Available Timeslots should be formatted correctly - Dropoff - Overnight ", async () => { + //Arrange + const overnightDropOffCmsWidgetName = "Overnight"; + const date = new Date().toISOString().split("T")[0]; + const timeSlotId = "testRouteCodeId" + RouteCodeFlags.OVERNIGHT_DROP_OFF; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + appointmentType: "Dropoff", + overnightDropOffCmsWidgetName: overnightDropOffCmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { + buttonLabel: mockCmsContent[overnightDropOffCmsWidgetName]["HeaderText"], + value: timeSlotId, + }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); + test("Available Timeslots should be formatted correctly - Dropoff", async () => { + //Arrange + const dropoffCmsWidgetName = "Dropoff"; + const date = "2020-01-01"; + const timeSlotId = "testRouteCodeId" + RouteCodeFlags.ALL_DAY_DROP_OFF; + const startTime = "8:00 AM"; + const endTime = "8:30 AM"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + appointmentType: "Dropoff", + dropoffCmsWidgetName: dropoffCmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { + buttonLabel: mockCmsContent[dropoffCmsWidgetName]["HeaderText"], + value: timeSlotId, + }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); + test("Available Timeslots should be formatted correctly - Mobile not premium", async () => { + //Arrange + const mobileCmsWidgetName = "MobileNotPremium"; + const date = "2020-01-01"; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00"; + const endTime = "8:30"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + appointmentType: "Mobile", + mobileCmsWidgetName: mobileCmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { + buttonLabel: startTime + " AM - " + endTime + " AM", + value: timeSlotId, + }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); + test("Available Timeslots should be formatted correctly - Mobile with premium", async () => { + //Arrange + const mobilePremiumCmsWidgetName = "MobilePremium"; + const date = "2020-01-01"; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00"; + const endTime = "8:30"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + premiumAppointmentFee: { + partType: PREMIUM_FEE_PART_TYPE, + }, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + offerPremium: true, + }, + ], + }, + appointmentType: "Mobile", + mobilePremiumCmsWidgetName: mobilePremiumCmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { + additionalButtonData: { + isPremiumAppointment: true, + }, + buttonLabel: mockCmsContent[mobilePremiumCmsWidgetName]["HeaderText"], + buttonLabelSubCopy: "+$15.99", + value: timeSlotId + PREMIUM_TIME_SLOT_ID_FLAG, + }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); + test("Available Timeslots should be formatted correctly - Inshop", async () => { + //Arrange + const cmsWidgetName = "Inshop"; + const date = "2020-01-01"; + const timeSlotId = "testRouteCodeId"; + const startTime = "8:00"; + const endTime = "8:30"; + const jobMaxMinutes = 100; + + const { wrapper } = setupMocks({ + customMountOptions: { + propsData: { + estimatedServiceMinutesMaximum: jobMaxMinutes, + dateAndTimeSlotData: { + date: date, + timeSlots: [ + { + id: timeSlotId, + startTime: startTime, + endTime: endTime, + }, + ], + }, + appointmentType: "Inshop", + cmsWidgetName: cmsWidgetName, + }, + }, + }); + + //Assert + const expectedTimeSlotObject = { buttonLabel: startTime + " AM", value: timeSlotId }; + expect(wrapper.vm.availableTimeSlots[0]).toEqual(expectedTimeSlotObject); + }); +}); + +const mockCmsContent = { + MobileNotPremium: { + BodyText: "Mobile, not premium BodyText", + FooterText: "Mobile, not premium FooterText", + SubheaderText: "Mobile, not premium SubheaderText", + HeaderText: "Mobile, not premium HeaderText", + }, + MobilePremium: { + "BodyText:": "Mobile, with premium BodyText", + FooterText: "Mobile, with premium FooterText", + SubheaderText: "Mobile, with premium SubheaderText", + HeaderText: "Mobile, with premium HeaderText", + }, + Inshop: { + BodyText: "Inshop BodyText", + FooterText: "Inshop FooterText", + SubheaderText: "Inshop SubheaderText", + HeaderText: "Inshop HeaderText", + }, + Dropoff: { + BodyText: "Dropoff BodyText", + FooterText: "Dropoff FooterText", + SubheaderText: "Dropoff SubheaderText", + HeaderText: "Dropoff HeaderText", + }, + Overnight: { + BodyText: "Dropoff Overnight", + FooterText: "Dropoff, Overnight FooterText", + SubheaderText: "Dropoff, Overnight SubheaderText", + HeaderText: "Dropoff, Overnight HeaderText", + }, + Sameday: { + BodyText: "Dropoff Sameday", + FooterText: "Dropoff, Sameday FooterText", + SubheaderText: "Dropoff, Sameday SubheaderText", + HeaderText: "Dropoff, Sameday HeaderText", + }, +}; + +function setupMocks({ customMountOptions }) { + const mountOptions = getMountOptions({ + ...customMountOptions, + }); + mountOptions.global.mocks["$store"] = store; + mountOptions["attachTo"] = document.body; + mountOptions.mixins = [ + { + methods: { + getCmsContent: jest.fn().mockImplementation((widgetName, fieldName) => { + if (mockCmsContent[widgetName] && mockCmsContent[widgetName][fieldName]) + return mockCmsContent[widgetName][fieldName]; + }), + getTotalLineItemPrice: jest.fn().mockImplementation(() => 15.99), + }, + }, + ]; + + const wrapper = shallowMount(timeSlotModalQuestion, mountOptions); + wrapper.vm.setCmsContent = jest.fn(); + return { wrapper }; +} diff --git a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue index 13f33ae05..724facf6c 100644 --- a/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue +++ b/src/layouts/schedule/time-slot-modal-question/time-slot-modal-question.vue @@ -150,7 +150,7 @@ export default { return this.modelValue?.routeCode; }, set: function (newValue) { - // Button Question only supports Number, or String data types so we must get the full object to emit + // Button Question only supports Number, or String data types so we must convert to the full object before emitting this.selectedValue = this.getSelectedTimeSlotObject(newValue); }, }, @@ -326,7 +326,7 @@ export default { } }, modal() { - return this.$refs["timeSlots"]; + return this.$refs[this.modalName]; }, }, methods: { diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 07c6535dd..6873ce294 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -17,7 +17,10 @@ @click-event="openModal" />
- + {{ errorMessage }}
diff --git a/src/layouts/vehicle-damage/vehicle-damage.spec.js b/src/layouts/vehicle-damage/vehicle-damage.spec.js index 28b85bf00..30e144310 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.spec.js +++ b/src/layouts/vehicle-damage/vehicle-damage.spec.js @@ -52,6 +52,7 @@ jest.mock("@/store", () => ({ damage: { glassToReplace: [], }, + order: { serviceLocation: { zipCode: "11111" } }, }, })); @@ -133,6 +134,7 @@ describe("vehicle-damage.vue", () => { getters: { vehicle: {}, payment: { insuranceCoverage: { isVerified: false } }, + order: { serviceLocation: { zipCode: "11111" } }, }, }, }, @@ -175,7 +177,7 @@ describe("vehicle-damage.vue", () => { expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( storeActions.GET_DAMAGE_OPTIONS, - { carId: "C00000000" } + { carId: "C00000000", zipCode: "11111" } ); }); @@ -241,6 +243,7 @@ describe("vehicle-damage.vue", () => { getters: { vehicle: {}, payment: { insuranceCoverage: { isVerified: false } }, + order: { serviceLocation: { zipCode: "11111" } }, }, }, }, @@ -270,11 +273,10 @@ describe("vehicle-damage.vue", () => { expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( storeActions.GET_DAMAGE_OPTIONS, - { carId: "C00000000" } + { carId: "C00000000", zipCode: "11111" } ); }); }); - describe("alert", () => { test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be rendered", () => { // Arrange & Act @@ -321,7 +323,6 @@ describe("vehicle-damage.vue", () => { expect(wrapper.findComponent({ ref: "vehicleChangeAlert" }).exists()).toBe(false); }); }); - describe("glass selections and corresponding variables", () => { test("isWindshieldDamageLocation is true when windshield is selected", async () => { //Arrange @@ -453,7 +454,6 @@ describe("vehicle-damage.vue", () => { expect(wrapper.vm.isPassengerSideReplace).toEqual(true); }); }); - describe("state validations", () => { test("CarId set, arePagePrerequisitesValid should be true ", async () => { //Arrange @@ -474,7 +474,6 @@ describe("vehicle-damage.vue", () => { expect(arePagePrerequisitesValid).toBe(true); }); }); - describe("input validations", () => { // THE FOLLOWING TEST IS NOT NECESSARILY REQUIRED FOR COVERAGE // BUT KEEP FOR AN EXAMPLE OF A VALIDATION TEST @@ -501,7 +500,6 @@ describe("vehicle-damage.vue", () => { }); }); }); - describe("get glass options from store", () => { const damageLocations = [ ["Windshield", [damageLocationsSelected.WINDSHIELD]], @@ -528,6 +526,7 @@ describe("vehicle-damage.vue", () => { eventBusItem: jest.fn(), damage: { glassToReplace: [{ glassLocation: damageLocation }] }, isRepair: true, + order: { serviceLocation: { zipCode: "11111" } }, }; var glassSelections = wrapper.vm.getDamageLocationsFromStore(); @@ -605,6 +604,7 @@ describe("vehicle-damage.vue", () => { isRepair: isRepair, numberOfChips: 2, }, + order: { serviceLocation: { zipCode: "11111" } }, }; var windshieldSelections = wrapper.vm.getWindshieldOptionsFromStore(); @@ -641,6 +641,7 @@ describe("vehicle-damage.vue", () => { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }], }, isRepair: true, + order: { serviceLocation: { zipCode: "11111" } }, }; var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore(); @@ -677,6 +678,7 @@ describe("vehicle-damage.vue", () => { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }], }, isRepair: true, + order: { serviceLocation: { zipCode: "11111" } }, }; var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore(); @@ -711,6 +713,7 @@ describe("vehicle-damage.vue", () => { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }], }, isRepair: true, + order: { serviceLocation: { zipCode: "11111" } }, }; var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore(); @@ -720,7 +723,6 @@ describe("vehicle-damage.vue", () => { } ); }); - describe("hide back button", () => { test("if claim registration is delayed => should hide back button", () => { // Arrange @@ -744,6 +746,7 @@ describe("vehicle-damage.vue", () => { getters: { vehicle: {}, payment: { insuranceCoverage: { isVerified: true } }, + order: { serviceLocation: { zipCode: "11111" } }, }, }, }, @@ -784,6 +787,7 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo isVerified: false, }, }, + order: { serviceLocation: { zipCode: "11111" } }, }, }, }; diff --git a/src/layouts/vehicle-damage/vehicle-damage.vue b/src/layouts/vehicle-damage/vehicle-damage.vue index ae89a61e6..6ea4fe81d 100644 --- a/src/layouts/vehicle-damage/vehicle-damage.vue +++ b/src/layouts/vehicle-damage/vehicle-damage.vue @@ -81,7 +81,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; - +import { queryStrings } from "@/constants/query-strings"; // DEFINE VALIDATION RULES defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED)); @@ -90,9 +90,20 @@ export default { async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const lowerCaseParams = new URLSearchParams(); + for (const [name, value] of urlParams) { + lowerCaseParams.append(name.toLowerCase(), value); + } + + const zip = lowerCaseParams.get(queryStrings.ZIP_CODE) + ? lowerCaseParams.get(queryStrings.ZIP_CODE) + : store.getters.order.serviceLocation.zipCode; + const damageOptionsPromise = baseMixin.methods.dispatchStoreAction( storeActions.GET_DAMAGE_OPTIONS, - { carId: store.getters.vehicle.carId } + { carId: store.getters.vehicle.carId, zipCode: zip } ); // Settle promises and get results @@ -135,6 +146,7 @@ export default { }, selectedWindshieldOptions: this.getWindshieldOptionsFromStore(), selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore(), + serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode, }; }, mounted() { @@ -157,6 +169,18 @@ export default { true ); } + if (this.serviceZipCode) { + this.pushEventToGA( + this.GaCategories.FUNNEL_ENTRY, + this.GaActions.ZIP_CODE_PROVIDED, + this.serviceZipCode, + true + ); + } + }, + + getZipFromStore() { + return this.$store.getters.order.serviceLocation.zipCode; }, backButtonAction() { diff --git a/src/layouts/vehicle/vehicle-question/vehicle-question.vue b/src/layouts/vehicle/vehicle-question/vehicle-question.vue index 283f26811..79b58bfaa 100644 --- a/src/layouts/vehicle/vehicle-question/vehicle-question.vue +++ b/src/layouts/vehicle/vehicle-question/vehicle-question.vue @@ -1,9 +1,5 @@ diff --git a/src/layouts/vehicle/vehicle.vue b/src/layouts/vehicle/vehicle.vue index bea1fdd8e..24ae0d215 100644 --- a/src/layouts/vehicle/vehicle.vue +++ b/src/layouts/vehicle/vehicle.vue @@ -15,8 +15,9 @@ ref="vehicleYearQuestion" class="mb-2 mt-4" v-model="selectedYear" + :isDisabled="!yearOptions.length" + :options="yearOptions" cmsWidgetName="VehicleYearQuestion" - :updateValues="updateYearValues" validationRules="year-required" placeHolderText="Select year" inputId="yearQuestionField" /> @@ -25,8 +26,9 @@ ref="vehicleMakeQuestion" class="mb-2 mt-4" v-model="selectedMake" + :isDisabled="!makeOptions.length" + :options="makeOptions" cmsWidgetName="VehicleMakeQuestion" - :updateValues="updateMakeValues" validationRules="make-required" placeHolderText="Select make" inputId="makeQuestionField" /> @@ -36,7 +38,8 @@ class="mb-2 mt-4" v-model="selectedModel" cmsWidgetName="VehicleModelQuestion" - :updateValues="updateModelValues" + :isDisabled="!modelOptions.length" + :options="modelOptions" validationRules="model-required" placeHolderText="Select model" inputId="modelQuestionField" /> @@ -46,7 +49,8 @@ class="mb-2 mt-4" v-model="selectedStyle" cmsWidgetName="VehicleStyleQuestion" - :updateValues="updateStyleValues" + :isDisabled="!styleOptions.length" + :options="styleOptions" validationRules="style-required" placeHolderText="Select style" inputId="styleQuestionField" /> @@ -101,10 +105,14 @@ export default { name: "vehicle", data() { return { - selectedYear: null, - selectedMake: null, - selectedModel: null, - selectedStyle: null, + selectedYear: this.selectedYearfromStore(), + selectedMake: this.selectedMakefromStore(), + selectedModel: this.selectedModelfromStore(), + selectedStyle: this.selectedStylefromStore(), + yearOptions: [], + makeOptions: [], + modelOptions: [], + styleOptions: [], }; }, @@ -113,10 +121,6 @@ export default { validationRules: String, }, - mounted() { - this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore); - }, - async beforeRouteEnter(to, from, next) { // Call APIs @@ -124,6 +128,43 @@ export default { const experimentForLogging = store.getters.applicationUser.experiments.find( (e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL ); + const yearQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( + storeActions.GET_VEHICLE_YEARS, + {} + ); + var makeQuestionInitialDataPromise = null; + var modelQuestionInitialDataPromise = null; + var styleQuestionInitialDataPromise = null; + + if ( + store.getters.order.vehicle.make && + store.getters.order.vehicle.model && + store.getters.order.vehicle.style + ) { + makeQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( + storeActions.GET_VEHICLE_MAKES, + { + year: store.getters.order.vehicle.year, + } + ); + + modelQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( + storeActions.GET_VEHICLE_MODELS, + { + year: store.getters.order.vehicle.year, + make: store.getters.order.vehicle.make, + } + ); + + styleQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( + storeActions.GET_VEHICLE_STYLES, + { + year: store.getters.order.vehicle.year, + make: store.getters.order.vehicle.make, + model: store.getters.order.vehicle.model, + } + ); + } // If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure. if (experimentForLogging !== undefined) { @@ -146,6 +187,22 @@ export default { resultKey: "cmsContent", promise: cmsContentPromise, }, + { + resultKey: "yearQuestionInitialData", + promise: yearQuestionInitialDataPromise, + }, + { + resultKey: "makeQuestionInitialData", + promise: makeQuestionInitialDataPromise, + }, + { + resultKey: "modelQuestionInitialData", + promise: modelQuestionInitialDataPromise, + }, + { + resultKey: "styleQuestionInitialData", + promise: styleQuestionInitialDataPromise, + }, ]; let resultMap = await settleAllPromises(promiseResultMap); @@ -153,48 +210,74 @@ export default { // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); + vm.initializeYearComponent(resultMap.yearQuestionInitialData); + + if ( + makeQuestionInitialDataPromise && + modelQuestionInitialDataPromise && + styleQuestionInitialDataPromise + ) + vm.initializeMMSComponent( + resultMap.makeQuestionInitialData, + resultMap.modelQuestionInitialData, + resultMap.styleQuestionInitialData + ); }); }, watch: { - selectedYear(year) { - const parsedYear = parseInt(year); - this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear); + async selectedYear(year) { if (year) { - this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore); + const result = await this.getMakeOptions(year); + this.makeOptions = result?.data; + if (this.selectedYearfromStore() !== year) { + this.selectedMake = null; + this.selectedModel = null; + this.selectedStyle = null; + } } else { - this.$refs["vehicleMakeQuestion"].clearValues(); + this.makeOptions = []; } }, - selectedMake(make) { - this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false); + async selectedMake(make) { if (make) { - this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore); + const result = await this.getModelOptions(this.selectedYear, make); + this.modelOptions = result?.data; + if (this.selectedMakefromStore() !== make) { + this.selectedModel = null; + this.selectedStyle = null; + } } else { - this.$refs["vehicleModelQuestion"].clearValues(); + this.modelOptions = []; } }, - selectedModel(model) { - this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false); + async selectedModel(model) { if (model) { - this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore); + const result = await this.getStyleOptions( + this.selectedYear, + this.selectedMake, + model + ); + this.styleOptions = result?.data; + if (this.selectedModelfromStore() !== model) { + this.selectedStyle = null; + } } else { - this.$refs["vehicleStyleQuestion"].clearValues(); + this.styleOptions = []; } }, selectedStyle(style) { - this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false); - this.setVehicle(); + this.setVehicle(this.selectedYear, this.selectedMake, this.selectedModel, style); }, }, methods: { - setVehicle() { + setVehicle(year, make, model, style) { return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, { - year: this.$store.getters.vehicle.year, - make: this.$store.getters.vehicle.make, - model: this.$store.getters.vehicle.model, - style: this.$store.getters.vehicle.style, + year: year, + make: make, + model: model, + style: style, }); }, @@ -207,42 +290,29 @@ export default { }, navigateForward() { + this.dispatchStoreAction( + storeActions.SAVE_VEHICLE, + { + year: this.selectedYear, + make: this.selectedMake, + model: this.selectedModel, + style: this.selectedStyle, + }, + false + ); this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); }, - async updateYearValues() { - return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {}); + initializeYearComponent(initialData) { + this.yearOptions = initialData; }, - - async updateMakeValues() { - return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, { - year: store.getters.vehicle.year, - }); - }, - - async updateModelValues() { - return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, { - year: store.getters.vehicle.year, - - make: store.getters.vehicle.make, - }); - }, - - async updateStyleValues() { - return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, { - year: store.getters.vehicle.year, - make: store.getters.vehicle.make, - model: store.getters.vehicle.model, - }); - }, - }, - - computed: { - displayGeneric() { - return !this.selectedStyle; + initializeMMSComponent(makeOptions, modelOptions, styleOptions) { + this.makeOptions = makeOptions; + this.modelOptions = modelOptions; + this.styleOptions = styleOptions; }, selectedYearfromStore() { - return store.getters.vehicle.year; + return store.getters.vehicle.year?.toString(); }, selectedMakefromStore() { return store.getters.vehicle.make; @@ -253,6 +323,32 @@ export default { selectedStylefromStore() { return store.getters.vehicle.style; }, + async getMakeOptions(year) { + return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, { + year: year, + }); + }, + + async getModelOptions(year, make) { + return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, { + year: year, + make: make, + }); + }, + + async getStyleOptions(year, make, model) { + return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, { + year: year, + make: make, + model: model, + }); + }, + }, + + computed: { + displayGeneric() { + return !this.selectedStyle; + }, }, components: { diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index ba1764bd3..c0b1f1788 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -15,7 +15,9 @@ v-model="vin" customInputId="vin" isRequired - validationRules="vin-required|vin-format" + :validationRules=" + !vinPopulatedOnPageLoad ? 'vin-required|vin-format' : '' + " :isDisabled="vinPopulatedOnPageLoad" maxLength="17" :mask="vinMask" @@ -348,37 +350,22 @@ export default { // If a VIN has already been found. Validate the Service Zip (in case of changes) const zipCodeData = await this.getZipCodeData(this.serviceZipCode); - // Check if Service Zip entered is serviceable then save the ZIP info and email address + // Check if Service Zip entered is serviceable then save the ZIP info if (zipCodeData.isServiceable) { - //Only save the service location if the zip changed or we lack zipCodeCtu + //Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu if ( this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode || !this.$store.getters.order.serviceLocation.zipCodeCtu ) { - const vehicleRegistrationInfo = this.$store.getters.vehicle.registration; - if (vehicleRegistrationInfo.zipCode == this.serviceZipCode) { - await this.dispatchStoreAction( - storeActions.SAVE_SERVICE_LOCATION, - { - address: vehicleRegistrationInfo.address, - city: vehicleRegistrationInfo.city, - state: vehicleRegistrationInfo.state, - zipCode: vehicleRegistrationInfo.zipCode, - zipCodeCtu: zipCodeData.zipCodeCtu, - }, - false - ); - } else { - await this.dispatchStoreAction( - storeActions.SAVE_SERVICE_ZIP_CODE_INFO, - { - state: zipCodeData.state, - zipCode: this.serviceZipCode, - zipCodeCtu: zipCodeData.zipCodeCtu, - }, - false - ); - } + await this.dispatchStoreAction( + storeActions.SAVE_SERVICE_ZIP_CODE_INFO, + { + state: zipCodeData.state, + zipCode: this.serviceZipCode, + zipCodeCtu: zipCodeData.zipCodeCtu, + }, + false + ); } await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false); diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js index 4a0c49c81..4b05067ea 100644 --- a/src/mixins/vehicle-questions-mixin.js +++ b/src/mixins/vehicle-questions-mixin.js @@ -446,7 +446,9 @@ export default { const payment = store.getters.payment; - if (payment.isInsurance && payment.insuranceCoverage.isVerified) { + if (store.getters.order.referralNumber?.length === 6) { + navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal }); + } else if (payment.isInsurance && payment.insuranceCoverage.isVerified) { navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal }); } else { self.$router.navigateWithSaving( diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index 88ca110dd..ae1c880ae 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -50,6 +50,7 @@ const navigationScenarios = { CLICKED_VEHICLE_EDIT: "CLICKED_VEHICLE_EDIT", CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT", CLICKED_SERVICE_PACKAGE_EDIT: "CLICKED_SERVICE_PACKAGE_EDIT", + CLICKED_SERVICE_LOCATION_EDIT: "CLICKED_SERVICE_LOCATION_EDIT", }; export { navigationScenarios }; diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index 4238b659a..017e1372a 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -480,6 +480,10 @@ const routingTable = function (store) { scenario: navigationScenarios.CLICKED_BACK, destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS, }, + { + scenario: navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT, + destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION, + }, ], }, ]; diff --git a/src/store/index.js b/src/store/index.js index 951887533..4bdc53977 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -535,6 +535,9 @@ export const getters = { }, eventBus: (state) => state.applicationUser.eventBus, damage: (state) => state.order.damage, + hasExactlyOneChip: (state) => { + return state.order.damage?.numberOfChips === 1; + }, hasAnyNonWindshieldGlassParts: (state) => { const nonWindshieldItems = state.order.damage.glassToReplace?.filter( (glassToReplace) => glassToReplace.glassLocation != "Windshield" @@ -766,11 +769,12 @@ export const actions = { }); }, - getDamageOptions(context, { carId }) { + getDamageOptions(context, { carId, zipCode }) { return globalMethods.callHttpClient({ methods: endpoints.GetDamageOptions.method, endpoint: `${endpoints.GetDamageOptions.url}/${carId}`, payload: {}, + additionalSuccessEventDataHandler: (response) => "QueryStringZip: " + zipCode, }); }, @@ -1622,6 +1626,25 @@ export const actions = { } }, + saveVehicle(context, { year, make, model, style }) { + context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); + context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); + context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); + + if (context.state.order.vehicle.year !== year) { + context.commit(storeMutations.UPDATE_YEAR, year); + } + if (context.state.order.vehicle.make !== make) { + context.commit(storeMutations.UPDATE_MAKE, make); + } + if (context.state.order.vehicle.model !== model) { + context.commit(storeMutations.UPDATE_MODEL, model); + } + if (context.state.order.vehicle.style !== style) { + context.commit(storeMutations.UPDATE_STYLE, style); + } + }, + saveVehicleDamage( context, { isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount } diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 4a6788433..08aa406c9 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -1702,6 +1702,59 @@ describe("Actions", () => { expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null); }); + it("saveVehicle, should save vehicle info", () => { + // Arrange + const context = state; + + context.state = { + order: { + vehicle: { + year: "2016", + make: "Toyota", + model: "Accord", + style: "SUV", + }, + }, + }; + + const commit = jest.fn(); + const dispatch = jest.fn(); + + context.commit = commit; + context.dispatch = dispatch; + + //Act + const payload = { + year: "2015", + make: "Honda", + model: "Civic", + style: "Sedan", + }; + actions.saveVehicle(context, payload); + + //Assert + expect(dispatch).toHaveBeenNthCalledWith( + 1, + storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES + ); + expect(dispatch).toHaveBeenNthCalledWith( + 2, + storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES + ); + if (context.state.order.vehicle.year !== payload.year) { + expect(commit).toBeCalledWith(storeMutations.UPDATE_YEAR, payload.year); + } + if (context.state.order.vehicle.make !== payload.make) { + expect(commit).toBeCalledWith(storeMutations.UPDATE_MAKE, payload.make); + } + if (context.state.order.vehicle.model !== payload.model) { + expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, payload.model); + } + if (context.state.order.vehicle.style !== payload.style) { + expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, payload.style); + } + }); + it("saveVehicleDamage, should wipe out damage if different", () => { // Arrange const context = state;