diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index ee9ecdbad..6848285b8 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -89,7 +89,7 @@ export default { modelValue: [Array, String], validationRules: String, suppressError: Boolean, - useTextForValue: Boolean + useTextForValue: Boolean, }, computed: { formattedGroupName() { diff --git a/src/layouts/molding-questions/molding-questions.vue b/src/layouts/molding-questions/molding-questions.vue index c489dc500..0cbe8ed2b 100644 --- a/src/layouts/molding-questions/molding-questions.vue +++ b/src/layouts/molding-questions/molding-questions.vue @@ -63,12 +63,14 @@ import { Form, defineRule } from "vee-validate"; import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; // DEFINE VALIDATION RULES defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); export default { name: "molding-questions", + mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); @@ -111,7 +113,6 @@ export default { return this.getCmsContent("AlertPartsQuestions", "BodyText"); }, }, - mixins: [vehicleQuestionsMixin], methods: { arePagePrerequisitesValid() { return true; // TODO - DO TRUE TEST OF PAGEDATA diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index dcefad7c0..4fc2c58c2 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -63,7 +63,7 @@ import { Form, defineRule } from "vee-validate"; import { required } from "@/helpers/validation-rules"; import { errorMessages } from "@/constants/error-messages"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; -import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; // DEFINE VALIDATION RULES defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); @@ -92,7 +92,7 @@ export default { data() { return { selectedModel: [], - partsQuestionsData: this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS).partsOrQuestions.filter((p) => { + partsQuestionsData: this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS)?.partsOrQuestions.filter((p) => { if (Array.isArray(p.partQuestions) && p.partQuestions.length > 0) { return { glassName: p.glassName, diff --git a/src/layouts/vehicle-damage/side-door-options/side-door-options.vue b/src/layouts/vehicle-damage/side-door-options/side-door-options.vue index b4df1121e..284c3eee6 100644 --- a/src/layouts/vehicle-damage/side-door-options/side-door-options.vue +++ b/src/layouts/vehicle-damage/side-door-options/side-door-options.vue @@ -112,7 +112,7 @@ export default ({ this.selectedValues = this.getSideDoorReplacementOptions(this.selectedValues.selectedDoorSides, this.selectedValues.selectedDriverSideReplaceOptions, newValue); } }, - answersToDisplay(){ + answersToDisplay(){ const filteredAnswers = Array.isArray(this.answersFromCms) ? this.answersFromCms.filter(ans => { diff --git a/src/layouts/vehicle-parts/vehicle-parts.spec.js b/src/layouts/vehicle-parts/vehicle-parts.spec.js index dbe0cbfec..9f766c7e9 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.spec.js +++ b/src/layouts/vehicle-parts/vehicle-parts.spec.js @@ -10,6 +10,7 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { nextTick } from "vue"; import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; // Mock our module for promises. jest.mock("@/helpers/layout-helper.js", () => ({ @@ -21,6 +22,10 @@ jest.mock("@/helpers/cms-content-helper", () => ({ fetchCmsContentForPage: jest.fn(), })); +jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ + navigateToHeritageFunnel: jest.fn() + })); + // Mock store jest.mock("@/store", () => { return {}; }, { virtual: true }); @@ -211,17 +216,107 @@ describe("vehicle-parts.vue", () => { }); - test("ForwardButtonAction triggers a router.navigate change and saves selected parts to store", async () => { + test("ForwardButtonAction triggers a router.navigate change if there are child part questions", async () => { //Arrange - store.getters.pageData.mockReturnValueOnce(basePartResponse); - store.getters.lineItems = { glassParts: {} } - store.commit = jest.fn(); - + store.getters.pageData.mockReturnValueOnce({ + partsOrQuestions: [ + { + glassName: "Single", + glassLocation: "Windshield", + parts: [ + { + "partNumber": "FW03861GTYN", + "description": "rain sensor, heated glass, auto dimming mirror, solar, 3rd visor band, condensation sensor", + "color": "Green Tint", + "requiresRecalibration": false, + "requiresCapabilityQuestions": false, + "recalibrationType": null, + "childParts": null, + "childPartQuestions": [ + { + "questionSequence": 1, + "questionText": "Does the rubber seal around your windshield have a chrome strip running through it?", + "answers": [ + { + "answerResult": "WKT D1106 C", + "answerText": "Yes", + "nextQuestionSequence": null + }, + { + "answerResult": "WKT D1106 B", + "answerText": "No", + "nextQuestionSequence": null + } + ] + } + ] + } + ], + partQuestions: null + } + ] + }); + + const { wrapper } = setupMocks({ + mountOptionsMockData: { + router: { + navigate: jest.fn() + }, + route: { + query: { + fmgPage: 'vehicle-parts', + } + }, + store: { + getters: store.getters, + commit: store.commit + }, + } + }); + + wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'FW03861GTYN' } } }); + + //Act + vehicleParts.beforeRouteEnter.call( + wrapper.vm, + { query: { fmgPage: "vehicle-parts" } }, + undefined, + (c) => c(wrapper.vm) + ); + + await wrapper.vm.forwardButtonAction(); + + //Assert + expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); + }); + + test("ForwardButtonAction triggers a router.navigate change if there are capability questions", async () => { + + //Arrange + store.getters.pageData.mockReturnValueOnce({ + partsOrQuestions: [ + { + glassName: "Single", + glassLocation: "Windshield", + parts: [ + { + partNumber: "DB12209GTYN", + description: "heated glass, solar, 1 hole", + color: "Green Tint", + requiresRecalibration: false, + requiresCapabilityQuestions: true, + childParts: null + } + ], + partQuestions: null + } + ] + }); + const { wrapper } = setupMocks({ mountOptionsMockData: { router: { - navigate: jest.fn(), navigate: jest.fn() }, route: { @@ -251,6 +346,50 @@ describe("vehicle-parts.vue", () => { //Assert expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); }); + + test("ForwardButtonAction saves selected parts to store if no molding or capability questions", async () => { + + //Arrange + store.getters.pageData.mockReturnValueOnce(basePartResponse); + store.getters.lineItems = { glassParts: {} } + + const { wrapper } = setupMocks({ + mountOptionsMockData: { + router: { + navigate: jest.fn() + }, + route: { + query: { + fmgPage: 'vehicle-parts', + } + }, + store: { + getters: store.getters, + commit: store.commit + }, + navigateToHeritageFunnel: jest.fn() + } + }); + + wrapper.vm.$store.commit = jest.fn(); + wrapper.vm.$refs.loadingModal.showModal = jest.fn(); + + wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'DB12209GTYN' } } }); + + //Act + vehicleParts.beforeRouteEnter.call( + wrapper.vm, + { query: { fmgPage: "vehicle-parts" } }, + undefined, + (c) => c(wrapper.vm) + ); + + await wrapper.vm.forwardButtonAction(); + + //Assert + expect(wrapper.vm.$store.commit).toHaveBeenCalled(); + expect(navigateToHeritageFunnel).toHaveBeenCalled(); + }); }); function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} }) { @@ -288,6 +427,7 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); + wrapper.vm.$refs.loadingModal.showModal = jest.fn(); return { wrapper, apiPromise }; } diff --git a/src/layouts/vehicle-parts/vehicle-parts.vue b/src/layouts/vehicle-parts/vehicle-parts.vue index 832d4bb94..c81364341 100644 --- a/src/layouts/vehicle-parts/vehicle-parts.vue +++ b/src/layouts/vehicle-parts/vehicle-parts.vue @@ -57,6 +57,7 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import alert from "@/ux-components/alert/alert"; +import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; // Supporting Files import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { settleAllPromises } from "@/helpers/layout-helper"; @@ -65,10 +66,11 @@ import { Form } from "vee-validate"; import store from "@/store"; import { storeMutations } from "@/constants/store-mutations.js"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; -import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; export default { name: "vehicle-parts", + mixins: [vehicleQuestionsMixin], async beforeRouteEnter(to, from, next) { // Call APIs const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); @@ -150,17 +152,20 @@ export default { return "partQuestion"; }, }, - mixins: [vehicleQuestionsMixin], methods: { arePagePrerequisitesValid() { // Check if isRepair is populated and if the pageData we need is here (Parts data) - if (store.getters.damage.isRepair != null && Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)).length !== 0) { - return true; - } - return false; + return store.getters.damage.isRepair != null && + Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)).length !== 0; }, backButtonAction() { - this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); + const hasPartQuestions = this.hasPartQuestions(this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS).partsOrQuestions); + const backNavigationScenario = hasPartQuestions ? this.navigationScenarios.CLICKED_BACK_WITH_PART_QUESTION_ANSWERS : this.navigationScenarios.CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS; + + this.$router.navigate( + backNavigationScenario, + this.$route + ); }, async forwardButtonAction() { const matchedParts = []; diff --git a/src/mixins/vehicle-questions-mixin.spec.js b/src/mixins/vehicle-questions-mixin.spec.js new file mode 100644 index 000000000..01d41b9c8 --- /dev/null +++ b/src/mixins/vehicle-questions-mixin.spec.js @@ -0,0 +1,161 @@ +import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; +import { shallowMount } from "@vue/test-utils"; +import { setupMocksForJsFiles, getMountOptions } from "@/helpers/unit-test-helper.js"; + +describe("vehicle-questions-mixin", () => { + describe("hasPartQuestions", () => { + test("has no part questions => return false", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasPartQuestions = wrapper.vm.hasPartQuestions([ + { + partQuestions: [] + } + ]) + + // Assert + expect(hasPartQuestions).toBe(false); + }); + + test("has undefined part questions => return false", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasPartQuestions = wrapper.vm.hasPartQuestions([]) + + // Assert + expect(hasPartQuestions).toBe(false); + }); + + test("has part questions => return true", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasPartQuestions = wrapper.vm.hasPartQuestions([ + { + partQuestions: [{ + testProperty: "some value" + }] + } + ]) + + // Assert + expect(hasPartQuestions).toBe(true); + }); + }); + + describe("hasGlassLocationWithMultipleParts", () => { + test("has one part for one glass location => return false", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasGlassLocationWithMultipleParts = wrapper.vm.hasGlassLocationWithMultipleParts([ + { + glassName: "Something", + glassLocation: "somewhere", + parts: [{ + partNumber: "1234567" + }] + } + ]) + + // Assert + expect(hasGlassLocationWithMultipleParts).toBe(false); + }); + + test("has one part for every glass location => return false", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasGlassLocationWithMultipleParts = wrapper.vm.hasGlassLocationWithMultipleParts([ + { + glassName: "Something", + glassLocation: "somewhere", + parts: [{ + partNumber: "1234567" + }] + }, + { + glassName: "Another glass", + glassLocation: "somewhere else", + parts: [{ + partNumber: "1234568" + }] + }, + { + glassName: "Special glass", + glassLocation: "Another where", + parts: [{ + partNumber: "1234569" + }] + } + ]) + + // Assert + expect(hasGlassLocationWithMultipleParts).toBe(false); + }); + + test("has multiple parts for one glass location => return true", () => { + // Arrange + const { wrapper } = setupMocks({}); + + // Act + const hasGlassLocationWithMultipleParts = wrapper.vm.hasGlassLocationWithMultipleParts([ + { + glassName: "Something", + glassLocation: "somewhere", + parts: [{ + partNumber: "1234567" + }] + }, + { + glassName: "Another glass", + glassLocation: "somewhere else", + parts: [{ + partNumber: "1234568" + }] + }, + { + glassName: "Special glass", + glassLocation: "Another where", + parts: [ + { + partNumber: "1234569" + }, + { + partNumber: "1234560" + } + ] + } + ]) + + // Assert + expect(hasGlassLocationWithMultipleParts).toBe(true); + }); + }); +}); + +function setupMocks({}) { + const baseMixin = setupMocksForJsFiles({}); + + const mocks = getMountOptions({ + router: { + navigate: jest.fn() + }, + }); + + const mockVehicleQuestionComponent = { + template: '
', + mixins: [vehicleQuestionsMixin, baseMixin.baseMixin] + }; + + const wrapper = shallowMount(mockVehicleQuestionComponent, mocks); + + return { wrapper }; +} \ No newline at end of file diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index 7919779b2..b021e0297 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -40,6 +40,6 @@ export default { this.$refs.loadingModal.showModal(); navigateToHeritageFunnel(); } - } + }, } } \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 98b26483b..e0c5d537b 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -176,8 +176,11 @@ async function navigate(scenario, currentRoute, optionalQuery = {}, optionalPara if (destinationFmgPageValue !== undefined) { // We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one. - // Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided. - baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData); + // Update page data to the store for next page if provided. Otherwise, use existing page data or override with empty object + const existingPageData = store.getters.pageData(destinationFmgPageValue) ?? {}; + if (Object.keys(optionalPageData).length > 0 && Object.keys(existingPageData).length === 0) { + baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData); + } // if cookie and referralNumber/Date exists OR an emailAddress has been saved if ((getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) || store.getters.order.customer?.emailAddress) { diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index c252a3d0f..2bd259909 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -21,9 +21,11 @@ const navigationScenarios = { SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS", ANSWERED_QUESTIONS_WITH_SINGLE_PART: "ANSWERED_QUESTIONS_WITH_SINGLE_PART", ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS: "ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS", + SELECTED_PROVIDE_VIN_DIFFERENT_WAY: "SELECTED_PROVIDE_VIN_DIFFERENT_WAY", + CLICKED_BACK_WITH_PART_QUESTION_ANSWERS: "CLICKED_BACK_WITH_PART_QUESTION_ANSWERS", + CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS: "CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS", HAS_MOLDING_QUESTIONS: "HAS_MOLDING_QUESTIONS", HAS_CAPABILITY_QUESTIONS: "HAS_CAPABILITY_QUESTIONS", - SELECTED_PROVIDE_VIN_DIFFERENT_WAY: "SELECTED_PROVIDE_VIN_DIFFERENT_WAY" }; export { navigationScenarios }; diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index 73b501493..335e583c3 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -82,10 +82,6 @@ const routingTable = function(store) { { fmgPageValue: fmgPageValues.VEHICLE_PARTS, maps: [ - { - scenario: navigationScenarios.CLICKED_BACK, - destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, - }, { scenario: navigationScenarios.SELECTED_PARTS, destinationFmgPageValue: fmgPageValues.QUOTE, @@ -94,6 +90,14 @@ const routingTable = function(store) { scenario: navigationScenarios.CLICKED_FORWARD, destinationFmgPageValue: fmgPageValues.REVEAL, }, + { + scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTION_ANSWERS, + destinationFmgPageValue: fmgPageValues.PART_QUESTIONS, + }, + { + scenario: navigationScenarios.CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS, + destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, + }, ], }, { diff --git a/src/store/index.js b/src/store/index.js index 2c507a123..f3e3cf96a 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,6 +5,7 @@ import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/se import createPersistedState from "vuex-persistedstate"; import globalMethods from "@/global-methods"; import { storeActions } from "../constants/store-actions"; +import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; // Export State const getDefaultState = () => { @@ -265,6 +266,8 @@ export const mutations = { }, resetGlassPartsState(state) { state.order.lineItems.glassParts = null; + state.order.damage.partQuestionAnswers = null; + state.applicationUser.pageData[fmgPageValues.PART_QUESTIONS] = null; }, resetState(state) { Object.assign(state, getDefaultState()); 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 2fef5119f..4c9670097 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -92,10 +92,6 @@ export default { : this.selectedValues[0]; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); - }, methods: { displayLoader() { this.isLoaderDisplayed = true; diff --git a/src/ux-components/list-button/list-button.vue b/src/ux-components/list-button/list-button.vue index 34b6f92e0..aabb06ae5 100644 --- a/src/ux-components/list-button/list-button.vue +++ b/src/ux-components/list-button/list-button.vue @@ -92,10 +92,6 @@ export default { : this.selectedValues[0]; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); - }, methods: { displayLoader() { this.isLoaderDisplayed = true; diff --git a/src/ux-components/list-card/list-card.vue b/src/ux-components/list-card/list-card.vue index a4ef7eac8..a1c95157f 100644 --- a/src/ux-components/list-card/list-card.vue +++ b/src/ux-components/list-card/list-card.vue @@ -110,10 +110,6 @@ export default { this.checkValue = this.selectedValues == this.value || this.modelValue == this.value; } }, - unmounted() { // needed to clear this button's selectedValues if it is removed - this.checkValue = false; - this.handleCheckChange(); - }, computed: { getLabelClasses() { if (this.isWide) {