diff --git a/jest.config.js b/jest.config.js index f6178df61..33b7d1138 100644 --- a/jest.config.js +++ b/jest.config.js @@ -28,4 +28,6 @@ module.exports = { // 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 }, }, + // Uncomment this to avoid the massive amount of warnings we are getting for onSubmit and onInvalidSubmit + // silent: true, }; diff --git a/src/constants/analytics.js b/src/constants/analytics.js index eed7da2fe..b5b24660d 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -19,6 +19,7 @@ const GaActions = { CLICKED: "Clicked", VIF: "vif", SUBMITTED: "Submitted", + DISPLAYED: "Displayed", }; const GaLabels = { diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index 64703d682..8f5fd83ff 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -90,6 +90,14 @@ const endpoints = { url: "/parts/api/v1/parts/supporting-items", method: "POST", }, + GetAlertReasons: { + url: "/location/api/v1/location/alert-reasons", + method: "GET", + }, + GetProviders: { + url: "/location/api/v1/location/providers", + method: "GET", + }, GetCapabilityQuestions: { url: "/parts/api/v1/parts/capability-questions", method: "GET", diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index 019ac2310..723980af3 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -21,6 +21,7 @@ const storeActions = { LOOKUP_VIN_BY_PLATE: "lookupVinByPlate", LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress", LOOKUP_VIN_BY_IMAGE: "lookupVinByImage", + GET_ALERT_REASONS_BY_CTU: "getAlertReasonsByCtu", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS: "getParts", GET_WIPERS: "getWipers", @@ -32,6 +33,7 @@ const storeActions = { GET_MOBILE_FEE_PART: "getMobileFeePart", GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails", GET_SHOP_TIME_SLOTS: "getShopTimeSlots", + GET_PROVIDERS: "getProviders", SAVE_SESSION: "saveSession", LOAD_SESSION: "loadSession", UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse", @@ -60,6 +62,7 @@ const storeActions = { SAVE_VEHICLE_DAMAGE: "saveVehicleDamage", SAVE_VIN_LOOKUP: "saveVinLookup", SAVE_SERVICE_LOCATION: "saveServiceLocation", + SAVE_SCHEDULE: "saveSchedule", SAVE_EMAIL: "saveEmail", SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP: "saveRegistrationLicensePlateLookup", SAVE_VIN: "saveVin", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 840aff30d..8eeb70c99 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -33,6 +33,7 @@ const storeMutations = { UPDATE_REGISTRATION: "updateRegistration", UPDATE_SERVICE_LOCATION: "updateServiceLocation", + UPDATE_SCHEDULE: "updateSchedule", UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress", diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index 5d8db68d4..44f66ad4f 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -87,6 +87,7 @@ import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-bu import listCard from "@/ux-components/list-card/list-card"; import radio from "@/ux-components/radio/radio"; import { useField, ErrorMessage } from "vee-validate"; +import { queryStrings } from "@/constants/query-strings"; export default { name: "buttonQuestion", @@ -120,7 +121,7 @@ export default { isOverflowScrollable: Boolean, isWide: Boolean, isCashOrInsurance: Boolean, - modelValue: [Array, Number, String], + modelValue: [Array, Number, String, Object], value: [Number, String], validationRules: String, suppressError: Boolean, @@ -129,6 +130,10 @@ export default { additionalButtonStyling: String, isSmallQuestionText: Boolean, customButtonQuestionId: String, + logDisplayedValuesEvent: { + type: Boolean, + default: false, + }, }, setup(props) { const propsClone = Object.assign({}, props); @@ -250,8 +255,33 @@ export default { }, }, watch: { - modelValue() { - this.resetField(); + modelValue(newValue) { + this.resetField({ + value: newValue, + }); + }, + answers() { + //once we get the answers to display from parent, see if we need a GA event to log what we showed + if (this.logDisplayedValuesEvent && this.answers.length > 0) { + var eventLabel = ""; + //build comma separated list of all items in button list that we are going to display on page + this.answers.forEach((item) => { + if (item.Name) { + eventLabel += item.Name + ","; + } + if (item.buttonLabel) { + eventLabel += item.buttonLabel + ","; + } + }); + + eventLabel = eventLabel.slice(0, -1); //remove the last comma + this.pushEventToGA( + this.$route.query[queryStrings.FMG_PAGE], + this.GaActions.DISPLAYED, + eventLabel, + true + ); + } }, }, components: { diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index cec960fa1..e1f3fb462 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -170,6 +170,9 @@ export default { } } } + &.show .modal-dialog { + transform: translateY(0); + } } body { .modal-backdrop { diff --git a/src/digital-components/text-block/text-block.vue b/src/digital-components/text-block/text-block.vue index 9e7df6312..e93e2e6c4 100644 --- a/src/digital-components/text-block/text-block.vue +++ b/src/digital-components/text-block/text-block.vue @@ -1,7 +1,7 @@ @@ -11,6 +11,11 @@ export default { props: { customText: String, // used to allow the insert of token values into textblock justifyText: String, // left, right, center + margin: { + // bootstrap margin to apply to the block. + type: String, + default: "mt-2", + }, typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation) fontWeight: String, // bold=500, default is 400 cmsWidgetName: String, diff --git a/src/fmg-components/funnel-header/menu-modal/menu-modal.vue b/src/fmg-components/funnel-header/menu-modal/menu-modal.vue index ccf2c38e0..55ff95c48 100644 --- a/src/fmg-components/funnel-header/menu-modal/menu-modal.vue +++ b/src/fmg-components/funnel-header/menu-modal/menu-modal.vue @@ -66,6 +66,7 @@ + + diff --git a/src/layouts/schedule/helpers/schedule-helper.js b/src/layouts/schedule/helpers/schedule-helper.js new file mode 100644 index 000000000..005bd4b19 --- /dev/null +++ b/src/layouts/schedule/helpers/schedule-helper.js @@ -0,0 +1,13 @@ +import { storeActions } from "@/constants/store-actions"; +import baseMixin from "@/mixins/base-mixin.js"; + +export async function getAlertReasons(ctu) { + const alertReasons = await baseMixin.methods.dispatchStoreAction( + storeActions.GET_ALERT_REASONS_BY_CTU, + { + ctu: ctu, + } + ); + + return Promise.resolve(alertReasons); +} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 9b73e6880..d8f65f668 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -18,6 +18,17 @@ + + // Components +import alert from "@/ux-components/alert/alert"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; @@ -48,6 +60,7 @@ import baseMixin from "@/mixins/base-mixin.js"; import { storeActions } from "@/constants/store-actions"; import { settleAllPromises } from "@/helpers/layout-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; +import { getAlertReasons } from "@/layouts/schedule/helpers/schedule-helper"; import { doesCopyContainRouterLink, splitCopyOnCMSPlaceHolder, @@ -56,6 +69,7 @@ import { } from "@/helpers/cms-content-helper"; import { errorMessages } from "@/constants/error-messages"; import { required } from "@/helpers/validation-rules"; +import store from "@/store"; defineRule("date-required", required(errorMessages.DATE_REQUIRED)); @@ -65,18 +79,25 @@ export default { return { selectedDate: null, selectableDatesData: [], + weatherAlerts: [], }; }, async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); + const alertReasonsPromise = getAlertReasons(store.getters.order.serviceLocation.zipCodeCtu); + // Settle promises and get results const promiseResultMap = [ { resultKey: "cmsContent", promise: cmsContentPromise, }, + { + resultKey: "alertReasons", + promise: alertReasonsPromise, + }, ]; const resultMap = await settleAllPromises(promiseResultMap); @@ -84,6 +105,7 @@ export default { // Call the "next" function to complete the transition to this page. next((vm) => { vm.setCmsContent(resultMap.cmsContent); + vm.setData(resultMap.alertReasons); }); }, computed: { @@ -94,6 +116,9 @@ export default { // Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText); }, + displayWeatherAlert() { + return this.weatherAlerts.length > 0; + }, }, methods: { doesCopyContainRouterLink, @@ -139,10 +164,56 @@ export default { }); return responseData; }, + setData(alertReasonsData) { + if (alertReasonsData) { + this.convertReasonsToCmsAlerts(alertReasonsData); + } + }, + cmsHeadlineTextFound(widgetName) { + return this.getCmsContent(widgetName, "HeadlineText") !== ""; + }, + convertReasonsToCmsAlerts(data) { + this.weatherAlerts = data.reduce((newObj, alert) => { + newObj.push({ + cmsWidgetName: `LocationAlert-${alert}`, + alertReason: alert, + }); + return newObj; + }, []); + }, + async getWeatherAlertReasons(ctu) { + await getAlertReasons(ctu) + .then((response) => { + if (response.data) { + this.convertReasonsToCmsAlerts(response.data); + } + }) + .catch(() => { + console.log("error fetching alert reasons.."); + }); + }, + getAvailableDates(startDate, endDate) { + return this.mockSelectableDatesData; + }, + getServiceZipCtuCodeFromStore() { + return store.getters.order.serviceLocation.zipCodeCtu; + }, backButtonAction() { this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); }, - forwardButtonAction() { + async forwardButtonAction() { + //TODO: replace properties with real values once they are available + await this.dispatchStoreAction( + this.storeActions.SAVE_SCHEDULE, + { + date: "2023-07-04T00:00:00", + startTime: "2023-07-04T12:00:00", + endTime: "2023-07-04T17:00:00", + routeCode: "03341-01820-S-B*20232*11 AM", + }, + false + ); + navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); }, }, @@ -154,6 +225,7 @@ export default { }, }, components: { + alert, funnelHeader, funnelFooter, funnelSubHeader, @@ -161,6 +233,7 @@ export default { loadingModal, datePicker, }, + mounted() {}, }; diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 6260df9d1..d6711f363 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -2,6 +2,7 @@
answer.Name == "Mobile") != -1 + ) { + this.selectedValues = "Mobile"; + } + }, + immediate: true, + }, isMobileOnly: { handler(newValue) { if (newValue) { this.selectedValues = "Mobile"; - } else { - this.selectedValues = null; } }, }, diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js index 061fd6bcd..cb075c38f 100644 --- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js +++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js @@ -48,7 +48,7 @@ describe("service-location-helper.js", () => { it("Should return null if no service zip code is passed in", async () => { // Arrange const serviceZipCode = null; - const serviceType = "Replace"; + const damageType = "Replace"; const parentAccountNumber = 167132; const billToAccountNumber = 1234; const expected = null; @@ -56,7 +56,7 @@ describe("service-location-helper.js", () => { // Act const result = await getPricedMobileFeePart( serviceZipCode, - serviceType, + damageType, parentAccountNumber, billToAccountNumber ); @@ -68,7 +68,7 @@ describe("service-location-helper.js", () => { it("Should return the priced mobile fee part", async () => { // Arrange const serviceZipCode = "43235"; - const serviceType = "Replace"; + const damageType = "Replace"; const parentAccountNumber = 167132; const billToAccountNumber = 1234; @@ -84,7 +84,7 @@ describe("service-location-helper.js", () => { // Act const result = await getPricedMobileFeePart( serviceZipCode, - serviceType, + damageType, parentAccountNumber, billToAccountNumber ); 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 4b0a4a1ac..b1b981429 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,15 +17,15 @@ @click-event="openModal" aria-label="Modal window" />
-
{{ errorMessage }}
+ + captureApartmentNumberOrBusinessName="true" + preserveCityAndStateOnReset="true" /> { describe("service-location.vue", () => { describe("beforeRouteEnter", () => { - test("on load sets the mobile fee part when an service zip code has already been provided", async () => { + test("on load sets the mobile fee part when a service zip code has already been provided", async () => { // Arrange const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + const mobileFeePart = { partNumber: "MOBILE FEE", description: "MOBILE FEE", @@ -585,6 +587,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -611,6 +615,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -637,6 +643,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -663,6 +671,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -689,6 +699,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -716,6 +728,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -743,6 +757,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -770,6 +786,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -797,6 +815,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -824,6 +844,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -851,6 +873,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -878,6 +902,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -905,6 +931,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -932,6 +960,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -959,6 +989,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -989,6 +1021,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1015,6 +1049,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1041,6 +1077,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1068,6 +1106,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1095,6 +1135,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1122,6 +1164,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1151,6 +1195,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1178,6 +1224,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1205,6 +1253,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1232,6 +1282,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1259,6 +1311,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1286,6 +1340,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1313,6 +1369,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, @@ -1342,6 +1400,8 @@ describe("service-location.vue", () => { const { wrapper } = setupMocks({}); + wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn(); + // Act await serviceLocation.beforeRouteEnter.call( wrapper.vm, diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 27c1f53ca..cfbc9b03d 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -3,7 +3,7 @@
- + + modalWidgetName="ServiceZipModalWidget" + :onZipUpdateCallback="reloadShopData" /> + modalWidgetName="MobileLocationModalWidget" + :onZipUpdateCallback="reloadShopData" /> + + + { return true; }); +const defaultProvider = { + providerNumber: null, + address: { + streetAddress: null, + city: null, + state: null, + zip: null, + }, +}; + export default { name: "service-location", data() { @@ -133,10 +158,12 @@ export default { isRecalibrationServiceableInshop: null, isGlassServiceableMobile: null, isRecalibrationServiceableMobile: null, + selectedAppointmentType: this.getSelectedAppointmentType(), + selectedProvider: this.getSelectedProvider(), + selectedProviderNumber: this.getSelectedProvider().providerNumber, mobileFeePart: null, zipContainsMilitaryBase: false, - selectedAppointmentType: "", - providerNumber: null, + zipCodeCtu: null, }; }, async beforeRouteEnter(to, from, next) { @@ -150,6 +177,8 @@ export default { const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode); + const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode); + // Settle promises and get results const promiseResultMap = [ { @@ -168,6 +197,10 @@ export default { resultKey: "zipCodeData", promise: getZipCodeData, }, + { + resultKey: "shopQuestionInitialData", + promise: shopQuestionInitialDataPromise, + }, ]; const resultMap = await settleAllPromises(promiseResultMap); @@ -180,6 +213,7 @@ export default { resultMap.serviceabilityDetails, resultMap.mobileFeePart ); + vm.$refs.shopQuestion.initializeComponent(resultMap.shopQuestionInitialData); }); }, computed: { @@ -194,7 +228,7 @@ export default { if (newValue.zipCode !== this.zipCode) { this.resetMobileLocation(); this.selectedAppointmentType = null; - this.providerNumber = null; + this.selectedProvider = defaultProvider; } this.state = newValue.state; @@ -229,7 +263,7 @@ export default { if (!this.selectedAppointmentType == "Mobile") { this.selectedAppointmentType = null; } - this.providerNumber = null; + this.selectedProvider = defaultProvider; } }, }, @@ -247,6 +281,12 @@ export default { return this.isGlassServiceableInshop; } }, + isShopQuestionDisplayed() { + return ( + this.selectedAppointmentType === "Inshop" || + this.selectedAppointmentType === "Dropoff" + ); + }, // Specifically check for isRecalibrationServiceableMobile === false, not null or true. requiresInshopRecalibration() { return ( @@ -286,6 +326,7 @@ export default { setData(zipCodeData, serviceabilityDetails, mobileFeePart) { if (zipCodeData) { this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase; + this.zipCodeCtu = zipCodeData.zipCodeCtu; } if (serviceabilityDetails) { @@ -313,6 +354,12 @@ export default { getServiceZipCodeFromStore() { return store.getters.order.serviceLocation.zipCode; }, + getSelectedAppointmentType() { + return store.getters.order.serviceLocation.appointmentType; + }, + getSelectedProvider() { + return store.getters.order.serviceLocation.provider ?? defaultProvider; + }, setMobileFeePart(mobileFeePart) { this.mobileFeePart = mobileFeePart; }, @@ -334,8 +381,32 @@ export default { backButtonAction() { this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); }, - forwardButtonAction() { - this.$router.navigateWithoutSaving( + async forwardButtonAction() { + await this.dispatchStoreAction( + this.storeActions.SAVE_SERVICE_LOCATION, + { + address: this.streetAddress, + address2: this.apartmentNumberOrBusinessName, + city: this.city, + state: this.state, + zipCode: this.zipCode, + zipCodeCtu: this.zipCodeCtu, + appointmentType: this.selectedAppointmentType, + isVehicleProtected: this.isVehicleProtected, + provider: { + providerNumber: this.selectedProvider?.providerNumber, + address: { + streetAddress: this.selectedProvider?.address?.streetAddress, + city: this.selectedProvider?.address?.city, + state: this.selectedProvider?.address?.state, + zip: this.selectedProvider?.address?.zipCode, + }, + }, + }, + false + ); + + this.$router.navigateWithSaving( this.navigationScenarios.SELECTED_LOCATION, this.$route ); @@ -343,6 +414,12 @@ export default { openModalAction(modalName) { this.$refs[modalName].openModal(); }, + async reloadShopData() { + await this.$refs.shopQuestion.reloadShopData(this.zipCode); + }, + onProviderSelected(selectedProvider) { + this.selectedProvider = selectedProvider; + }, }, components: { alert, @@ -355,6 +432,7 @@ export default { Form, loadingModal, contentGroupModal, + shopQuestion, }, }; diff --git a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue index 0698fc340..b8971498a 100644 --- a/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue +++ b/src/layouts/service-location/service-zip-modal-question/service-zip-modal-question.vue @@ -69,6 +69,9 @@ export default { }, linkWidgetName: String, modalWidgetName: String, + onZipUpdateCallback: { + type: Function, + }, }, setup() { const textboxQuestionWidgetName = "ServiceZipQuestionWidget"; @@ -161,6 +164,10 @@ export default { // Update the page level model this.$emit("update:modelValue", this.internalModel); + if (this.onZipUpdateCallback) { + await this.onZipUpdateCallback(serviceZipCode); + } + this.closeModal(); } } else { diff --git a/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.spec.js b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.spec.js new file mode 100644 index 000000000..b9eb6299b --- /dev/null +++ b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.spec.js @@ -0,0 +1,70 @@ +import { mount } from "@vue/test-utils"; +import shopListButton from "./shop-list-button"; +import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"; + +describe("service-package-radio.vue", () => { + it("Should include buttonLabel in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"])); + }); + + it("Should include buttonLabelSubCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"])); + }); + + it("Should include buttonLabelAuxillaryCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelAuxillaryCopy"])); + }); +}); + +const mockProps = { + buttonLabel: "buttonLabel test copy", + buttonLabelSubCopy: "buttonLabelSubCopy test copy", + buttonBodyCopy: + "
  • buttonBodyCopy test copy
  • 2
  • 3
  • 4
  • 5
", + buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy", + value: 0, + modelValue: 0, + groupName: "mockGroup", +}; + +function setupMocks({ mountOptionsMockData = {} }) { + const wrapper = mount(shopListButton, { + ...mountOptionsMockData, + mixins: [inputButtonWrapperMixin], + }); + + return { wrapper }; +} diff --git a/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue new file mode 100644 index 000000000..b8003148f --- /dev/null +++ b/src/layouts/service-location/shop-question/shop-list-button/shop-list-button.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/layouts/service-location/shop-question/shop-question.spec.js b/src/layouts/service-location/shop-question/shop-question.spec.js new file mode 100644 index 000000000..70a158f0a --- /dev/null +++ b/src/layouts/service-location/shop-question/shop-question.spec.js @@ -0,0 +1,592 @@ +import { shallowMount } from "@vue/test-utils"; +import { storeActions } from "@/constants/store-actions"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import shopQuestion from "./shop-question"; + +jest.mock("@/mixins/base-mixin", () => ({ + methods: { + dispatchStoreAction(action, items, encode) { + if (action === mockGetProvidersStoreAction) { + return new Promise((resolve) => { + resolve(mockNewShopList); + }); + } + }, + scrollToPageBottom() {}, + }, +})); + +const mockGetProvidersStoreAction = storeActions.GET_PROVIDERS; +const mockNewShopList = { + shopProviders: [ + { + address: { + city: "COLUMBUS", + country: "US", + state: "OH", + streetAddress: "1670 HARMON AVE", + zipCode: "43223", + }, + distanceInMiles: 15.9727889297435, + providerNumber: "006747", + }, + { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + { + address: { + city: "Columbus", + country: "US", + state: "OH", + streetAddress: "4580 W Broad St", + zipCode: "43228", + }, + distanceInMiles: 19.4618116611001, + providerNumber: "003342", + }, + ], +}; + +const mockCmsContent = { + QuestionText: "Select a shop:", +}; + +const cmsWidgetName = "ShopQuestionWidget"; +const shopQuestionInitialData = { + shopProviders: [ + { + address: { + city: "WESTERVILLE", + country: "US", + state: "OH", + streetAddress: "4403 EXECUTIVE PKWY", + zipCode: "43081", + }, + distanceInMiles: 5.16769294095201, + providerNumber: "003335", + }, + { + address: { + city: "WORTHINGTON", + country: "US", + state: "OH", + streetAddress: "760 DEARBORN PARK LN", + zipCode: "43085", + }, + distanceInMiles: 10.5865432478478, + providerNumber: "001820", + }, + { + address: { + city: "COLUMBUS", + country: "US", + state: "OH", + streetAddress: "5015 N HIGH ST", + zipCode: "43214", + }, + distanceInMiles: 11.738869544543, + providerNumber: "003343", + }, + { + address: { + city: "COLUMBUS", + country: "US", + state: "OH", + streetAddress: "1670 HARMON AVE", + zipCode: "43223", + }, + distanceInMiles: 15.9727889297435, + providerNumber: "006747", + }, + { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + { + address: { + city: "Columbus", + country: "US", + state: "OH", + streetAddress: "4580 W Broad St", + zipCode: "43228", + }, + distanceInMiles: 19.4618116611001, + providerNumber: "003342", + }, + ], +}; + +const mockMixin = { + methods: { + getCmsContent: jest.fn((widgetName, cmsFieldName) => { + if (widgetName === cmsWidgetName) { + return mockCmsContent[cmsFieldName]; + } + + return null; + }), + }, +}; + +describe("shop-question.vue", () => { + beforeEach(() => { + const container = document.createElement("div"); + container.scrollTo = jest.fn(); + + container.classList.add("page-container-grouped-styles"); + document.body.appendChild(container); + }); + + it("Should display first three shops when an appointment type has already been selected", async () => { + // Arrange + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: null, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + isDisplayed: true, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(shopQuestionInitialData); + + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.answers.length).toEqual(3); + expect(wrapper.vm.answers).toEqual([ + { + buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081", + buttonLabel: "4403 Executive Pkwy", + buttonLabelSubCopy: "5 mi", + value: "003335", + }, + { + buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085", + buttonLabel: "760 Dearborn Park Ln", + buttonLabelSubCopy: "10.5 mi", + value: "001820", + }, + { + buttonBodyCopy: "5015 N High St, Columbus, OH 43214", + buttonLabel: "5015 N High St", + buttonLabelSubCopy: "11.5 mi", + value: "003343", + }, + ]); + }); + + it("Should display the 'Show more locations' link when there are more than three locations to chose from", async () => { + // Arrange + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + isDisplayed: true, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(shopQuestionInitialData); + + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" }); + + // Assert + expect(showMoreShopsLink.exists()).toBe(true); + expect(showMoreShopsLink.isVisible()).toBe(true); + }); + + it("Should not display the 'Show more locations' link when there are fewer than three locations to chose from", async () => { + // Arrange + const alsoShopQuestionInitialData = { + shopProviders: [ + { + address: { + city: "WESTERVILLE", + country: "US", + state: "OH", + streetAddress: "4403 EXECUTIVE PKWY", + zipCode: "43081", + }, + distanceInMiles: 5.16769294095201, + providerNumber: "003335", + }, + { + address: { + city: "WORTHINGTON", + country: "US", + state: "OH", + streetAddress: "760 DEARBORN PARK LN", + zipCode: "43085", + }, + distanceInMiles: 10.5865432478478, + providerNumber: "001820", + }, + { + address: { + city: "COLUMBUS", + country: "US", + state: "OH", + streetAddress: "5015 N HIGH ST", + zipCode: "43214", + }, + distanceInMiles: 11.738869544543, + providerNumber: "003343", + }, + ], + }; + + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + isDisplayed: true, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(alsoShopQuestionInitialData); + + await wrapper.vm.$nextTick(); + + const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" }); + + // Assert + expect(showMoreShopsLink.exists()).toBe(true); + expect(showMoreShopsLink.isVisible()).toBe(false); + }); + + it("Should display the next three shops when the 'Show more location' link is clicked", async () => { + // Arrange + const displayedAnswers = [ + { + buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081", + buttonLabel: "4403 Executive Pkwy", + buttonLabelSubCopy: "5 mi", + value: "003335", + }, + { + buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085", + buttonLabel: "760 Dearborn Park Ln", + buttonLabelSubCopy: "10.5 mi", + value: "001820", + }, + { + buttonBodyCopy: "5015 N High St, Columbus, OH 43214", + buttonLabel: "5015 N High St", + buttonLabelSubCopy: "11.5 mi", + value: "003343", + }, + ]; + + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + isDisplayed: true, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(shopQuestionInitialData); + + await wrapper.vm.$nextTick(); + + wrapper.vm.answers = displayedAnswers; + wrapper.vm.shopIndex = 3; + + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" }); + + await wrapper.vm.$nextTick(); + + showMoreShopsLink.trigger("click"); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.answers.length).toBe(6); + expect(wrapper.vm.answers).toEqual([ + { + buttonBodyCopy: "4403 Executive Pkwy, Westerville, OH 43081", + buttonLabel: "4403 Executive Pkwy", + buttonLabelSubCopy: "5 mi", + value: "003335", + }, + { + buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085", + buttonLabel: "760 Dearborn Park Ln", + buttonLabelSubCopy: "10.5 mi", + value: "001820", + }, + { + buttonBodyCopy: "5015 N High St, Columbus, OH 43214", + buttonLabel: "5015 N High St", + buttonLabelSubCopy: "11.5 mi", + value: "003343", + }, + { + buttonBodyCopy: "1670 Harmon Ave, Columbus, OH 43223", + buttonLabel: "1670 Harmon Ave", + buttonLabelSubCopy: "16 mi", + value: "006747", + }, + { + buttonBodyCopy: "3938 Powell Rd, Powell, OH 43065", + buttonLabel: "3938 Powell Rd", + buttonLabelSubCopy: "16.5 mi", + value: "003341", + }, + { + buttonBodyCopy: "4580 W Broad St, Columbus, OH 43228", + buttonLabel: "4580 W Broad St", + buttonLabelSubCopy: "19.5 mi", + value: "003342", + }, + ]); + }); + + it("Should display the number of shops necessary to show a previously selected shop", async () => { + // Arrange + const selectedProvider = { + providerNumber: "003341", + }; + + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: selectedProvider.providerNumber, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(shopQuestionInitialData); + + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.answers.length).toEqual(5); + }); + + it("Should reset the answers when the selected appointment type changes", async () => { + // Arrange + const { wrapper } = setupMocks({ + mixins: [mockMixin], + props: { + modelValue: { + address: { + city: "POWELL", + country: "US", + state: "OH", + streetAddress: "3938 POWELL RD", + zipCode: "43065", + }, + distanceInMiles: 16.2690495685233, + providerNumber: "003341", + }, + serviceZipCode: "43081", + selectedAppointmentType: "Dropoff", + cmsWidgetName: cmsWidgetName, + }, + mountOptions: { + attachTo: document.body, + }, + }); + + // Act + wrapper.vm.initializeComponent(shopQuestionInitialData); + + await wrapper.vm.$nextTick(); + + await wrapper.setProps({ + selectedAppointmentType: "Inshop", + }); + + await wrapper.vm.$nextTick(); + + // Assert + expect(wrapper.vm.answers.length).toEqual(3); + }); + + // it("Should reload the shops when the service zip code changes", async () => { + // // Arrange + // const { wrapper } = setupMocks({ + // mixins: [mockMixin], + // props: { + // modelValue: { + // address: { + // city: "POWELL", + // country: "US", + // state: "OH", + // streetAddress: "3938 POWELL RD", + // zipCode: "43065", + // }, + // distanceInMiles: 16.2690495685233, + // providerNumber: "003341", + // }, + // serviceZipCode: "43081", + // selectedAppointmentType: "Dropoff", + // cmsWidgetName: cmsWidgetName, + // isDisplayed: true + // }, + // mountOptions: { + // attachTo: document.body, + // }, + // }); + + // wrapper.vm.$options.methods.loadInitialData = jest.fn().mockImplementation(() => { + // return new Promise((resolve) => { + // resolve(mockNewShopList); + // }); + // }); + + // // Act + // wrapper.vm.initializeComponent(shopQuestionInitialData); + + // await wrapper.vm.$options.watch.serviceZipCode.handler.call(wrapper.vm, "43054"); + + // // Assert + // expect(wrapper.vm.shopProviders.length).toEqual(3); + // expect(wrapper.vm.shopProviders).toEqual(mockNewShopList.shopProviders); + // }); + + // it("Should clear the existing answers when the service zip code changes", async () => { + // // Arrange + // const { wrapper } = setupMocks({ + // mixins: [mockMixin], + // props: { + // modelValue: { + // address: { + // city: "POWELL", + // country: "US", + // state: "OH", + // streetAddress: "3938 POWELL RD", + // zipCode: "43065", + // }, + // distanceInMiles: 16.2690495685233, + // providerNumber: "003341", + // }, + // serviceZipCode: "43081", + // selectedAppointmentType: "Dropoff", + // cmsWidgetName: cmsWidgetName, + // }, + // mountOptions: { + // attachTo: document.body, + // }, + // }); + + // //Act + // wrapper.vm.initializeComponent(shopQuestionInitialData); + + // wrapper.setProps({ + // selectedAppointmentType: "Inshop", + // }); + + // await wrapper.vm.$nextTick(); + + // // Assert + // expect(wrapper.vm.answers.length).toEqual(3); + // }); +}); + +function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) { + const resultingMountOptions = getMountOptions({ + ...mountOptions, + mixins, + }); + + if (props) resultingMountOptions.propsData = props; + + const wrapper = isShallowMount + ? shallowMount(shopQuestion, resultingMountOptions) + : mount(shopQuestion, resultingMountOptions); + + return { wrapper }; +} diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue new file mode 100644 index 000000000..2b7e41742 --- /dev/null +++ b/src/layouts/service-location/shop-question/shop-question.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/src/layouts/vin-lookup/vin-information/vin-information.vue b/src/layouts/vin-lookup/vin-information/vin-information.vue index 9571cb5eb..a0eaf9536 100644 --- a/src/layouts/vin-lookup/vin-information/vin-information.vue +++ b/src/layouts/vin-lookup/vin-information/vin-information.vue @@ -1,6 +1,6 @@