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/store-actions.js b/src/constants/store-actions.js index 9d7d3ea6a..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", @@ -85,6 +86,7 @@ const storeActions = { "saveSupportingItemsSuppressingStateResetting", SAVE_VAPS: "saveVaps", SAVE_CUSTOMER_DETAILS: "saveCustomerDetails", + SAVE_SERVICE_LOCATION_TECH_NOTES: "saveServiceLocationTechNotes", }; export { storeActions }; diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 76d510507..7b57187c2 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -34,11 +34,13 @@ const storeMutations = { UPDATE_SERVICE_ZIP: "updateServiceZip", UPDATE_SERVICE_LOCATION: "updateServiceLocation", + UPDATE_SERVICE_LOCATION_TECH_NOTES: "updateServiceLocationTechNotes", + UPDATE_TECH_NOTES: "updateTechNotes", UPDATE_SCHEDULE: "updateSchedule", UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress", - //CUSTOMER MUTATIONS + // CUSTOMER MUTATIONS UPDATE_CUSTOMER_DETAILS: "updateCustomerDetails", // ORDER MUTATIONS diff --git a/src/digital-components/dropdown-question/dropdown-question.vue b/src/digital-components/dropdown-question/dropdown-question.vue index 7c0dd0d3b..6c032b740 100644 --- a/src/digital-components/dropdown-question/dropdown-question.vue +++ b/src/digital-components/dropdown-question/dropdown-question.vue @@ -16,9 +16,13 @@ :disabled="isDisabled" :aria-required="isRequired" :validationRules="validationRules" - :placeHolderText="placeHolderText"> + :placeHolderText="placeHolderText" + :valueAsKey="valueAsKey"> - @@ -47,6 +51,11 @@ export default { placeHolderText: String, cmsWidgetName: String, hasError: Boolean, + valueAsKey: { + type: Boolean, + required: false, + default: false, + }, }, setup(props) { const uuid = uuidv4(); diff --git a/src/digital-components/textarea-question/textarea-question.vue b/src/digital-components/textarea-question/textarea-question.vue index 0ffe5ec0a..83d718976 100644 --- a/src/digital-components/textarea-question/textarea-question.vue +++ b/src/digital-components/textarea-question/textarea-question.vue @@ -54,7 +54,7 @@ export default { }, }, remainingCount() { - return this.maxLength - this.value.length; + return this.maxLength - (this.value?.length ?? 0); }, urgentCountdown() { return this.remainingCount <= this.maxLength * 0.1 ? true : false; diff --git a/src/layouts/customer-details/customer-details.vue b/src/layouts/customer-details/customer-details.vue index 753bf8fc1..be533117f 100644 --- a/src/layouts/customer-details/customer-details.vue +++ b/src/layouts/customer-details/customer-details.vue @@ -32,7 +32,7 @@ + v-model="isSmsOptin" /> ({ + 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/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/store/index.js b/src/store/index.js index fb86b61c3..4bdc53977 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -59,9 +59,14 @@ const getDefaultState = () => { zipCodeCtu: null, }, }, + techNotes: null, }, customer: { + firstName: null, + lastName: null, emailAddress: null, + phoneNumber: null, + isSmsOptin: null, }, damage: { isRepair: null, @@ -272,6 +277,9 @@ export const mutations = { }, }; }, + updateServiceLocationTechNotes(state, techNotes) { + state.order.serviceLocation.techNotes = techNotes; + }, updateSchedule(state, scheduleInfo) { if (scheduleInfo) { state.order.schedule.date = scheduleInfo.date; @@ -281,14 +289,13 @@ export const mutations = { state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes; } }, - updateCustomerDetails(state, detailsInfo) { - if (detailsInfo) { - state.order.customerDetails.firstName = detailsInfo.firstName; - state.order.customerDetails.lastName = detailsInfo.lastName; - state.order.customerDetails.email = detailsInfo.email; - state.order.customerDetails.telephone = detailsInfo.telephone; - state.order.customerDetails.textUpdates = detailsInfo.textUpdates; - state.order.customerDetails.techNotes = detailsInfo.techNotes; + updateCustomerDetails(state, customerDetails) { + if (customerDetails) { + state.order.customer.firstName = customerDetails.firstName; + state.order.customer.lastName = customerDetails.lastName; + state.order.customer.emailAddress = customerDetails.emailAddress; + state.order.customer.phoneNumber = customerDetails.phoneNumber; + state.order.customer.isSmsOptin = customerDetails.isSmsOptin; } }, @@ -1619,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 } @@ -1947,8 +1973,8 @@ export const actions = { context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo); }, - saveDetails(context, scheduleInfo) { - context.commit(storeMutations.UPDATE_DETAILS, scheduleInfo); + saveCustomerDetails(context, customerDetails) { + context.commit(storeMutations.UPDATE_CUSTOMER_DETAILS, customerDetails); }, saveServiceZipCodeInfo(context, serviceZipCodeInfo) { @@ -1982,6 +2008,10 @@ export const actions = { context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo); }, + saveServiceLocationTechNotes(context, techNotesInfo) { + context.commit(storeMutations.UPDATE_SERVICE_LOCATION_TECH_NOTES, techNotesInfo); + }, + saveEmail(context, email) { context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email === "" ? null : email); }, 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;