diff --git a/jest.config.js b/jest.config.js index a73aac7ea..a511fdc02 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,6 +23,7 @@ module.exports = { "!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue", "!src/ux-components/alert/alert.vue", "!src/helpers/validation-rules.js", + "!src/common-components/question-chain/question-chain", // END ], // ! means exclude from coverage. testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], diff --git a/src/common-components/button-question/button-question.spec.js b/src/common-components/button-question/button-question.spec.js index b70304035..9cf75368e 100644 --- a/src/common-components/button-question/button-question.spec.js +++ b/src/common-components/button-question/button-question.spec.js @@ -112,7 +112,8 @@ describe("buttonQuestion.vue", () => { const wrapper = shallowMount(buttonQuestion, setupMocks({})); await wrapper.setProps({ answers: ["2022", "2021", "2020"], - isMultiSelect: false + isMultiSelect: false, + modelValue: [] }); const val = { checkValue: true, value: "2021", } wrapper.vm.handleCheckedChanged(val); diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index efc1cf051..f1c2ad98a 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -36,6 +36,7 @@ data-test="button" :validationRules="validationRules" :class="[suppressError ? 'alertError' : '']" + :clearOnUnmount="clearOnUnmount" /> @@ -84,6 +85,10 @@ export default { validationRules: String, suppressError: Boolean, useTextForValue: Boolean, + clearOnUnmount: { + type: Boolean, + default: true + } }, computed: { getFieldSetClasses() { @@ -133,17 +138,14 @@ export default { return answer.Name ? answer.Name : answer; }, handleCheckedChanged(val) { - - if(this.isMultiSelect && this.selectedValues) { - // Add or remove item to array of data to emit - const newSelectedValues = this.selectedValues; - + if(this.selectingInitiatesLoad) { + this.selectedValues = [val.value]; + } else { if(Array.isArray(this.selectedValues)) { + const newSelectedValues = this.selectedValues; val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1); this.selectedValues = newSelectedValues; } - } else { - this.selectedValues = [val.value]; } }, }, diff --git a/src/common-components/question-chain/question-chain.vue b/src/common-components/question-chain/question-chain.vue new file mode 100644 index 000000000..d605d7fde --- /dev/null +++ b/src/common-components/question-chain/question-chain.vue @@ -0,0 +1,119 @@ + + + \ No newline at end of file diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index e8349ef40..1924b0063 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -14,7 +14,7 @@ const storeMutations = { UPDATE_IS_REPAIR: "updateIsRepair", UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips", UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace", - UPDATE_PARTS: "updateParts", + UPDATE_GLASS_PARTS: "updateGlassParts", UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate", UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress", UPDATE_REGISTRATION_CITY: "updateRegistrationCity", @@ -40,7 +40,7 @@ const storeMutations = { RESET_VEHICLE_STATE: "resetVehicleState", RESET_DAMAGE_STATE: "resetDamageState", RESET_REGISTRATION_STATE: "resetRegistrationState", - RESET_PARTS_STATE: "resetPartsState", + RESET_GLASS_PARTS_STATE: "resetGlassPartsState", RESET_STATE: "resetState", // OTHER MUTATIONS diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 36a7d6465..af202feed 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -67,7 +67,7 @@ export default { }, resetDependentState() { // Set - store.commit(storeMutations.UPDATE_PARTS, null); + store.commit(storeMutations.UPDATE_GLASS_PARTS, null); // Invokes store.dispatch(storeActions.RESET_PARTS_AND_DEPS); diff --git a/src/layouts/reveal/reveal.vue b/src/layouts/reveal/reveal.vue index 200fec263..7ca9d903f 100644 --- a/src/layouts/reveal/reveal.vue +++ b/src/layouts/reveal/reveal.vue @@ -50,7 +50,7 @@ export default { }, resetDependentState() { // Set - store.commit(storeMutations.UPDATE_PARTS, null); + store.commit(storeMutations.UPDATE_GLASS_PARTS, null); // Invokes store.dispatch(storeActions.RESET_PARTS_AND_DEPS); diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index a16d0b0a8..9ab861f0f 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -198,7 +198,7 @@ methods: { } // Save parts to the store. - store.commit(storeMutations.UPDATE_PARTS, matchedParts); + store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts); // Navigate to the next page. this.$router.navigateAfterSave( diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js index 1c580f4a6..e782f345e 100644 --- a/src/layouts/vin-lookup/vin-lookup.spec.js +++ b/src/layouts/vin-lookup/vin-lookup.spec.js @@ -69,9 +69,38 @@ describe("vin-lookup.vue", () => { expect(wrapper.vm.navigateForward).toHaveBeenCalled(); }); + it("Should do a VIN lookup if the user has clicked on the VIN field and entered a new VIN or changed a previously matched VIN.", async () => { + // Arrange + const { wrapper } = setupMocks({}); + wrapper.vm.vinTouched = true; + wrapper.vm.vin = "foo"; + wrapper.vm.initialVin = "!foo"; + + wrapper.vm.navigateForward = jest.fn(); + const vehicleLookupApiResponse = { + data: { + carId: 'new carId' // does not match the store value + } + }; + const vinPromise = Promise.resolve(vehicleLookupApiResponse); + + wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise); + + // Act + await wrapper.vm.forwardButtonAction(); + + //Assert + expect(wrapper.vm.lookupVehicle).toHaveBeenCalled(); + }); + it("Should not call navigateForward() if the store carId does not match the vin response carId and forward button is clicked", async () => { // Arrange const { wrapper } = setupMocks({}); + // New lookup + wrapper.vm.vinTouched = true; + wrapper.vm.vin = ""; + wrapper.vm.initialVin = "foo"; + const vehicleLookupApiResponse = { data: { carId: 'new carId' // does not match the store value @@ -138,14 +167,21 @@ describe("vin-lookup.vue", () => { it("Should not call navigateForward() when forward button is clicked but lookupVehicle errors out.", async () => { // Arrange const { wrapper } = setupMocks({}); + wrapper.vm.vinTouched = true; + wrapper.vm.vin = "foo"; + wrapper.vm.initialVin = "!foo"; + const vehicleLookupApiResponse = { - data: { + status: { carId: 'new carId' // does not match the store value } }; const vinPromise = Promise.reject(vehicleLookupApiResponse); - wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise); + const response = { + status: 404 + }; + wrapper.vm.lookupVehicle = jest.fn().mockImplementation((response) => vinPromise); wrapper.vm.navigateForward = jest.fn(); wrapper.vm.previouslyEnteredCarId = 'new carId'; diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 33c192941..392cde7f1 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -27,6 +27,7 @@ maxLength="17" :mask="vinMask" @focus="setVinTouched" + @maska="rawVinValue = $event.target.dataset.maskRawValue" /> @@ -142,6 +143,7 @@ import { Form, defineRule } from "vee-validate"; import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; +import { StatusCodes } from 'http-status-codes'; // DEFINE VALIDATION RULES defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -187,6 +189,7 @@ export default { vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0, initialVin: this.getVinFromStore(), vinTouched: false, + rawVinValue: "", }; }, mounted() { @@ -202,10 +205,14 @@ export default { this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") ); }, + zip() { + this.noServiceZip = false; + }, }, computed: { perfectMatchNewVinAlert() { - const isVinPerfectMatch = this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore(); + const vinToCheck = this.vinTouched ? this.rawVinValue : this.initialVin; + const isVinPerfectMatch = this.vinPopulatedOnPageLoad && vinToCheck === this.getVinFromStore(); this.updateIsCarIdDifferent(isVinPerfectMatch); return isVinPerfectMatch; }, @@ -259,7 +266,7 @@ export default { }, setupVinMask() { const lastSixChars = this.initialVin.substring(11, this.initialVin.length); - this.vinMask = `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`; + this.vinMask = `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`; }, arePagePrerequisitesValid() { return store.getters.vehicle.carId !== null; @@ -304,34 +311,58 @@ export default { } }, async forwardButtonAction() { - // If there is no change to the VIN entered then navigate forward without performing lookup. - if (this.vinPopulatedOnPageLoad && this.vin == this.initialVin) { - this.navigateForward(); - return; - } + let zipValidationResponse; + let vehicleLookupResponse; const zipValidation = this.validateZip(this.zip); - const vehicleLookup = this.lookupVehicle(this.vin); - const zipValidationResponse = await zipValidation; - const vehicleLookupResponse = await vehicleLookup.catch(() => { - this.vinNotFound = true; - this.$refs.funnelFooter.removeLoader(); - this.noServiceZip = false; - return false; - }); + if (this.vinTouched && this.vin != this.initialVin) { + // If the user has clicked on the VIN field, either they are doing a new VIN lookup or changing the VIN previously matched. + // Therefore we need to do a Vehicle Lookup + const vinToLookup = this.vinTouched ? this.vin : this.initialVin; + const vehicleLookup = this.lookupVehicle(vinToLookup); + + zipValidationResponse = await zipValidation; + vehicleLookupResponse = await vehicleLookup.catch((response) => { + if (response.status == StatusCodes.NOT_FOUND) { + this.vinNotFound = true; + this.$refs.funnelFooter.removeLoader(); + return false; + } + }); + + // Check if Service Zip entered is servicable, if not display an alert + if (!zipValidationResponse.data.isServiceable) { + this.customAlertData.zip = this.zip; + this.noServiceZip = true; + this.invalidZip = this.zip; + } + + if (!vehicleLookupResponse || !zipValidationResponse.data.isServiceable) { + this.$refs.funnelFooter.removeLoader(); + return; + } + } else { + const zipValidationResponse = await zipValidation; + + // Check if Service Zip entered is serviceable, if not display an alert + if (!zipValidationResponse.data.isServiceable) { + this.customAlertData.zip = this.zip; + this.noServiceZip = true; + this.invalidZip = this.zip; + this.$refs.funnelFooter.removeLoader(); + + return; + + } else { + this.navigateForward(); + return; + } + } if (!vehicleLookupResponse) { return; } - - if (!zipValidationResponse.data.isServiceable) { - this.customAlertData.zip = this.zip; - this.$refs.funnelFooter.removeLoader(); - this.noServiceZip = true; - this.invalidZip = this.zip; - return; - } this.isCarIdDifferent = vehicleLookupResponse.data.carId !== store.getters.vehicle.carId; diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index 8f33fdf92..383bcf237 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -19,7 +19,7 @@ export default { this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, this.$route, {}, {}, result.data); } else { - store.commit(storeMutations.UPDATE_PARTS, result.data); + store.commit(storeMutations.UPDATE_GLASS_PARTS, result.data); this.$refs.loadingModal.showModal(); navigateAfterSaveToHeritageFunnel(this.$route); } diff --git a/src/mixins/vin-pages-mixin.spec.js b/src/mixins/vin-pages-mixin.spec.js index 747b62793..aef929d52 100644 --- a/src/mixins/vin-pages-mixin.spec.js +++ b/src/mixins/vin-pages-mixin.spec.js @@ -872,7 +872,7 @@ describe("vin-pages-mixin", () => { // Assert expect(store.commit).toHaveBeenCalledTimes(1); - expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_PARTS, { partsOrQuestions }) + expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_GLASS_PARTS, { partsOrQuestions }) expect(wrapper.vm.$refs.loadingModal.showModal).toHaveBeenCalledTimes(1); expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalledTimes(1); }); diff --git a/src/router/index.js b/src/router/index.js index 74ee110de..2b2d09859 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -46,6 +46,7 @@ const routes = [ // If the saved session has timed out, clear the session, execute 404 logic. if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { + await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE); await GoToFunnelStartOn404(next); } diff --git a/src/store/index.js b/src/store/index.js index c3e54d27b..5730adf53 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -111,7 +111,7 @@ export const mutations = { updateGlassToReplace(state, glassToReplace) { state.order.damage.glassToReplace = glassToReplace; }, - updateParts(state, partsData) { + updateGlassParts(state, partsData) { state.order.lineItems.glassParts = partsData; }, updatePageData(state, pageData) { @@ -212,9 +212,8 @@ export const mutations = { state.order.vehicle.registration.firstName = null; state.order.vehicle.registration.lastName = null; }, - resetPartsState(state) { + resetGlassPartsState(state) { state.order.lineItems.glassParts = null; - state.order.lineItems.otherParts = null; }, resetState(state) { Object.assign(state, getDefaultState()); @@ -400,14 +399,14 @@ export const actions = { }, resetDamageAndDependencies(context) { context.commit(storeMutations.RESET_DAMAGE_STATE); - context.commit(storeMutations.RESET_PARTS_STATE); + context.commit(storeMutations.RESET_GLASS_PARTS_STATE); }, resetRegistrationAndDependencies(context) { context.commit(storeMutations.RESET_REGISTRATION_STATE); - context.commit(storeMutations.RESET_PARTS_STATE) + context.commit(storeMutations.RESET_GLASS_PARTS_STATE) }, resetPartsAndDependencies(context) { - context.commit(storeMutations.RESET_PARTS_STATE); + context.commit(storeMutations.RESET_GLASS_PARTS_STATE); }, resetState(context) { context.commit(storeMutations.RESET_STATE); diff --git a/src/store/store.spec.js b/src/store/store.spec.js index bdb49ece7..c70629296 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -183,7 +183,7 @@ describe("Mutations", () => { const storeState = state; // Act - mutations.updateParts(storeState, { 'Windshield-Single': 'PARTNUM101'}); + mutations.updateGlassParts(storeState, { 'Windshield-Single': 'PARTNUM101'}); // Assert expect(storeState.order.lineItems.glassParts).toEqual({ 'Windshield-Single': 'PARTNUM101'}); @@ -446,7 +446,7 @@ describe("Actions", () => { await actions.resetDamageAndDependencies(context) expect(commit).toBeCalledWith(storeMutations.RESET_DAMAGE_STATE); - expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_STATE); + expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE); }); @@ -462,7 +462,7 @@ describe("Actions", () => { await actions.resetRegistrationAndDependencies(context) expect(commit).toBeCalledWith(storeMutations.RESET_REGISTRATION_STATE); - expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_STATE); + expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE); }); @@ -477,7 +477,7 @@ describe("Actions", () => { // Act await actions.resetPartsAndDependencies(context) - expect(commit).toBeCalledWith(storeMutations.RESET_PARTS_STATE); + expect(commit).toBeCalledWith(storeMutations.RESET_GLASS_PARTS_STATE); }); @@ -747,7 +747,7 @@ describe("Getters", () => { const storeState = state; // Act - mutations.updateParts(storeState, {"Rear-Stationary": 'PART101'}); + mutations.updateGlassParts(storeState, {"Rear-Stationary": 'PART101'}); // Assert expect(getters.lineItems(storeState).glassParts).toEqual({"Rear-Stationary": 'PART101'}); diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue index eef4be443..dfd322cb3 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -78,6 +78,10 @@ export default { validationRules: String, selectedValues: [Array, String], hasError: Boolean, + clearOnUnmount: { + type: Boolean, + default: true + } }, data() { return { @@ -92,9 +96,11 @@ export default { : this.selectedValues[0]; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); + unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync + if (this.clearOnUnmount) { + this.checkValue = false; + this.handleCheckChange(); + } }, methods: { displayLoader() { diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index f09384934..861619093 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -78,6 +78,10 @@ export default { validationRules: String, selectedValues: [Array, String], hasError: Boolean, + clearOnUnmount: { + type: Boolean, + default: true + } }, data() { return { @@ -92,9 +96,11 @@ export default { : this.selectedValues[0]; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); + unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync + if (this.clearOnUnmount) { + this.checkValue = false; + this.handleCheckChange(); + } }, methods: { displayLoader() { diff --git a/src/ux-components/list-card/list-card.vue b/src/ux-components/list-card/list-card.vue index 264d7c299..88aad0b6a 100644 --- a/src/ux-components/list-card/list-card.vue +++ b/src/ux-components/list-card/list-card.vue @@ -86,6 +86,10 @@ export default { selectedValues: [Array, String], modelValue: Object, hasError: Boolean, + clearOnUnmount: { + type: Boolean, + default: true + } }, data() { return { @@ -99,9 +103,11 @@ export default { : this.selectedValues[0]; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); + unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync + if (this.clearOnUnmount) { + this.checkValue = false; + this.handleCheckChange(); + } }, computed: { getLabelClasses() {