From 7b78fe1e74f5b21a7942aec755d5a307d01cba78 Mon Sep 17 00:00:00 2001 From: Kulbhushan Kaushik Date: Wed, 23 Nov 2022 08:36:21 -0500 Subject: [PATCH] commit --- .../questions-page-layout.spec.js | 250 +++++++++ .../part-questions/part-questions.spec.js | 0 src/mixins/vehicle-questions-mixin.js | 514 ++++++++++++++++++ 3 files changed, 764 insertions(+) delete mode 100644 src/layouts/part-questions/part-questions.spec.js create mode 100644 src/mixins/vehicle-questions-mixin.js diff --git a/src/common-components/questions-page-layout/questions-page-layout.spec.js b/src/common-components/questions-page-layout/questions-page-layout.spec.js index e69de29b..48427452 100644 --- a/src/common-components/questions-page-layout/questions-page-layout.spec.js +++ b/src/common-components/questions-page-layout/questions-page-layout.spec.js @@ -0,0 +1,250 @@ +// Components +import questionsPageLayout from "@/common-components/questions-page-layout/questions-page-layout"; + +// Supporting Files +import { shallowMount } from "@vue/test-utils"; +import { getMountOptions } from "@/helpers/unit-test-helper.js"; +import baseMixin from "../../mixins/base-mixin"; +import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin"; + +// Mock our module for promises. +jest.mock("@/helpers/layout-helper.js", () => ({ + settleAllPromises: jest.fn(), +})); + +// Mock fetchCmsContentForPage +jest.mock("@/helpers/cms-content-helper", () => ({ + fetchCmsContentForPage: jest.fn(), +})); + + +describe("questionsPageLayout.vue", () => { + describe("method showThisQuestionChain...", () => { + test("Should return true if index prop and passed index match", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 0 }); + + const testGlassPiece = { + questions: [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?", + answers: [], + }, + ], + }; + const testIndex = 0; + + // Act + const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex); + + //Assert + expect(result).toBe(true); + + wrapper.unmount(); + }); + + test("Should return true if answerResult exists", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 0 }); + + const testGlassPiece = { + questions: [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?", + answers: [], + }, + ], + answerData: { + answerResult: "test", + }, + }; + const testIndex = 1; + + // Act + const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex); + + //Assert + expect(result).toBe(true); + + wrapper.unmount(); + }); + + test("Should return false if indexes don't match and answerResult is missing", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 0 }); + + const testGlassPiece = { + questions: [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?", + answers: [], + }, + ], + }; + const testIndex = 1; + + // Act + const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex); + + //Assert + expect(result).toBe(false); + + wrapper.unmount(); + }); + + test("Should return false if no questions", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 0 }); + + const testGlassPiece = {}; + const testIndex = 0; + + // Act + const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex); + + //Assert + expect(result).toBe(false); + + wrapper.unmount(); + }); + + test("Should return false if suppressed", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({ index: 0 }); + + const testGlassPiece = { + isSuppressedPart: true, + questions: [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?", + answers: [], + }, + ], + answerData: { + answerResult: "test", + }, + }; + const testIndex = 0; + + // Act + const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex); + + //Assert + expect(result).toBe(false); + + wrapper.unmount(); + }); + }); + + describe("method handleForwardButtonAction...", () => { + test("Should emit forwardButtonAction", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({}); + + // Act + wrapper.vm.handleForwardButtonAction(); + + //Assert + expect(wrapper.emitted()).toHaveProperty("forwardButtonAction"); + + wrapper.unmount(); + }); + }); + + describe("method handleBackButtonAction...", () => { + test("Should emit backButtonAction", async () => { + // Arrange + const { wrapper } = setupMocks({}); + await wrapper.setProps({}); + + // Act + wrapper.vm.handleBackButtonAction(); + + //Assert + expect(wrapper.emitted()).toHaveProperty("back-click"); + + wrapper.unmount(); + }); + }); +}); + +function setupMocks() { + const baseStoreGettersPageData = () => { + return { + partsOrQuestions: [ + { + parts: null, + partQuestions: [ + { + questionSequence: 1, + questionText: + "Is your vehicle equipped with the Panoramic Sunroof which can be identified by having a glass panel over the rear seats?", + answers: [ + { + answerResult: "", + answerText: "Yes", + nextQuestionSequence: 2, + }, + { + answerResult: "", + answerText: "No", + nextQuestionSequence: 3, + }, + ], + }, + ], + glassLocation: "Windshield", + glassName: "Single", + answerKey: "Windshield-Single", + answerData: null, + }, + ], + }; + }; + const baseStoreGettersDamage = () => { + return { + partsQuestionAnswers: [ + { + glassLocation: "Windshield", + glassName: "Single", + result: "FW04848", + answeredQuestions: [ + { + questionText: + "Is your vehicle equipped with the Panoramic Sunroof which can be identified by having a glass panel over the rear seats?", + selectedAnswerText: "Yes", + questionNum: 1, + }, + { + questionText: + "Is your vehicle equipped with a heated windshield that melts snow and ice from underneath the windshield wiper blades?", + selectedAnswerText: "Yes", + questionNum: 2, + }, + ], + }, + ], + }; + }; + const mountOptions = getMountOptions({ + mixins: [baseMixin, vehicleQuestionsMixin], + }); + mountOptions["attachTo"] = document.body; + const wrapper = shallowMount(questionsPageLayout, mountOptions); + + return { wrapper }; +} diff --git a/src/layouts/part-questions/part-questions.spec.js b/src/layouts/part-questions/part-questions.spec.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/mixins/vehicle-questions-mixin.js b/src/mixins/vehicle-questions-mixin.js new file mode 100644 index 00000000..0be25396 --- /dev/null +++ b/src/mixins/vehicle-questions-mixin.js @@ -0,0 +1,514 @@ +import { issPageValues } from "@/router/router-constants/issPage-values"; +import { storeMutations } from "@/constants/store-mutations.js"; +import { navigationScenarios } from "../router/router-constants/navigation-scenarios"; +import { storeActions } from "@/constants/store-actions"; +import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; +import baseMixin from "@/mixins/base-mixin.js"; +import store from "@/store"; + +export default { + data() { + // TODO KO DELETE AFTER QUOTE MVP + return { + shouldGoToHeritageQuote: false, + }; + }, + methods: { + hasPartQuestions(partsOrQuestions) { + return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0); + }, + hasGlassLocationWithMultipleParts(partsOrQuestions) { + return partsOrQuestions?.some((pq) => pq.parts?.length > 1); + }, + hasChildPartQuestions(partsOrQuestions) { + return partsOrQuestions?.some((pq) => { + return pq.parts?.some((part) => part.childPartQuestions?.length > 0); + }); + }, + hasCapabilityQuestions(partsOrQuestions) { + return partsOrQuestions?.some((pq) => { + return pq.parts?.some((part) => part.requiresCapabilityQuestions === true); + }); + }, + + // method to only include keys listed for lineItems.glassParts in + reducedGlassPartsArray(glassParts) { + const reducedGlassParts = []; + glassParts.forEach((glass) => { + if (Array.isArray(glass.parts) && glass.parts.length === 1) { + const singlePart = glass.parts[0]; + reducedGlassParts.push({ + partNumber: singlePart.partNumber, + description: singlePart.description, + color: singlePart.color, + partType: singlePart.partType, + canSafeliteRecalibrate: singlePart.canSafeliteRecalibrate, + requiresRecalibration: singlePart.requiresRecalibration, + requiresCapabilityQuestions: singlePart.requiresCapabilityQuestions, + recalibrationType: singlePart.recalibrationType, + childParts: singlePart.childParts, + price: singlePart.price, + }); + } + }); + return reducedGlassParts; + }, + comparePageIndices(currentPage, fmgPage) { + const orderedVehicleQuestionPages = [ + issPageValues.PART_QUESTIONS, + issPageValues.VEHICLE_PARTS, + issPageValues.MOLDING_QUESTIONS, + issPageValues.CAPABILITY_QUESTIONS, + issPageValues.QUOTE, + ]; + + return ( + orderedVehicleQuestionPages.indexOf(currentPage) - + orderedVehicleQuestionPages.indexOf(fmgPage) + ); + }, + currentPageComesBeforePage(currentPage = this.$route.query.fmgPage, fmgPage) { + return this.comparePageIndices(currentPage, fmgPage) < 0; + }, + currentPageComesAfterPage(currentPage = this.$route.query.fmgPage, fmgPage) { + return this.comparePageIndices(currentPage, fmgPage) > 0; + }, + setupInitialData(glass, index, alreadyAnsweredQuestions, vm) { + const self = vm ?? this; + + // clear answerData if no questions are already answered + if (!alreadyAnsweredQuestions) { + glass.answerData = null; + } + + alreadyAnsweredQuestions?.forEach((answeredGlass) => { + // if answeredGlass lacks any of these properties then exit + if ( + !answeredGlass.glassLocation || + !answeredGlass.glassName || + !answeredGlass.answeredQuestions || + (!answeredGlass.result && !answeredGlass.partNum) + ) { + return; + } + + // test if glass parts match + if ( + glass.glassLocation === answeredGlass.glassLocation && + glass.glassName === answeredGlass.glassName + ) { + let answerString = ""; + + // loop through answeredQuestions for matches + answeredGlass.answeredQuestions.forEach((answeredQuestion) => { + if (!answeredQuestion.questionNum || !answeredQuestion.selectedAnswerText) { + return; + } + // determine which answer was previously chosen + const chosenAns = glass.questions[ + answeredQuestion.questionNum - 1 + ].answers.find((a) => { + return ( + a.answerText.toUpperCase() === + answeredQuestion.selectedAnswerText.toUpperCase() + ); + }); + // set the answerString to use for answerSelected + if (chosenAns.nextQuestionSequence) { + answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`; + } else { + answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`; + } + + // mark this question as answered (question-chain will read this) + glass.questions[answeredQuestion.questionNum - 1].answerSelected = + answerString; + // mark this question as suppressed if needed (question-chain uses this) + if (answeredQuestion.suppressThisQuestion) { + glass.questions[ + answeredQuestion.questionNum - 1 + ].suppressThisQuestion = true; + } + }); + + // advance the currentGlassIndex + self.currentGlassIndex = index; + + const answerResult = answeredGlass.partNum + ? answeredGlass.partNum + : answeredGlass.result; + // the above logic for answerResult covers all 3 ___-questions page scenarios + + // add answerData to current glass + glass.answerData = { + answerResult: answerResult, + answeredQuestions: answeredGlass.answeredQuestions, + }; + } + }); + + return glass; + }, + handleAnswerUpdates(answer, glassKey, vm) { + // only runs when all questions in a question-chain have been answered + // when selectedAnswers updates, user has completed this part's question chain and has a final answer + // (does not get run for each invididual question's answer, only when + // all relevant questions for the current part have been answered) + + /* answer example format: + { + "answerResult": "DD11132", + "answeredQuestions": [ + { + "questionText": "Is your Grand Cherokee the Laredo model?", + "selectedAnswerText": "Yes", + "questionNum": 1, + } + ], + "index": 0 + } + */ + + const self = vm ?? this; + + // clear out any preloaded answers + self.selectedAnswers = {}; + + // loop through every answered question on the currently answered glass part + answer.answeredQuestions?.forEach((answeredQuestion) => { + /* answeredQuestion example format: + { + "questionText": "Is your Grand Cherokee the Laredo model?", + "selectedAnswerText": "Yes", + "questionNum": 1, + } + */ + + const answeredQuestionText = answeredQuestion.questionText.toUpperCase(); + const answeredQuestionAnswer = answeredQuestion.selectedAnswerText.toUpperCase(); + + // HANDLE DUPLICATE QUESTIONS + + // loop through all glass data + self.questionsData.forEach((glass, glassIndex) => { + /* glass example format: + { + "glassName": "Single", + "glassLocation": "Windshield", + "parts": null, + "questions": [ + { + "questionSequence": 1, + "questionText": "Is your vehicle equipped with a black dotted pattern behind the rear view mirror, known as a third visor frit?", + "answers": [ + { + "answerResult": "DW01537", + "answerText": "Yes", + "nextQuestionSequence": null + }, + { + "answerResult": "DW01537b", + "answerText": "No", + "nextQuestionSequence": null + } + ] + } + ], + "answerKey": "Windshield-Single", + "answerData": null + } + */ + + // limit duplicate search to glass pieces that follow after the currently being answered glass piece + if (glassIndex > answer.index) { + let indexToSuppressTo; + // reset this glass piece, in case user is changing their previous answers + glass.answerData = null; + glass.isSuppressedPart = null; + + // loop through this glass piece's questions, looking for a questionText match + glass.questions.forEach((question, questionIndex) => { + // clear out any previously set answers + question.answerSelected = null; + + // clear or set suppressThisQuestion property for each question + if (indexToSuppressTo && questionIndex + 1 < indexToSuppressTo) { + question.suppressThisQuestion = true; + } else { + question.suppressThisQuestion = null; + } + + // handle duplicate questions + if (question.questionText.toUpperCase() === answeredQuestionText) { + let matchedAnswer; + + // handle matching answer in duplicated question + question.answers.forEach((ans) => { + if (ans.answerText.toUpperCase() === answeredQuestionAnswer) { + matchedAnswer = ans; + ans.selected = true; + } else { + ans.selected = null; + } + }); + + // handle duplicate's nextQuestion logic on other related questions + if (matchedAnswer.nextQuestionSequence) { + // clear any suppression on the nextQuestion + glass.questions[ + matchedAnswer.nextQuestionSequence - 1 + ].suppressThisQuestion = null; + // if the duplicate is 1ST question in array, set indexToSuppressTo + if (questionIndex === 0) { + if ( + !indexToSuppressTo || + matchedAnswer.nextQuestionSequence < indexToSuppressTo + ) { + indexToSuppressTo = matchedAnswer.nextQuestionSequence; + } + } + } + + // update answers in this glass piece's questions with duplication logic modifications + glass.questions.forEach((q) => { + q.answers.forEach((a) => { + // revert any previously set nextQuestion logic modifications + if ( + a.originalNextQuestionSequence === + question.questionSequence + ) { + // restore original nextQuestionSequence + a.nextQuestionSequence = a.originalNextQuestionSequence; + self.originalNextQuestionSequence = null; + // restore original answerResult + if (a.originalAnswerResult) { + a.answerResult = a.originalAnswerResult; + a.originalAnswerResult = null; + } + } + // modify logic on any related questions + if (a.nextQuestionSequence === question.questionSequence) { + // update either nextQuestionSequence or answerResult + + // update questions that lead to duplicated question + if (matchedAnswer.nextQuestionSequence) { + a.originalNextQuestionSequence = + a.nextQuestionSequence; + a.nextQuestionSequence = + matchedAnswer.nextQuestionSequence; + } else { + a.originalNextQuestionSequence = + a.nextQuestionSequence; + a.nextQuestionSequence = null; + a.originalAnswerResult = + a.originalAnswerResult || a.answerResult; + a.answerResult = matchedAnswer.answerResult; + } + } + }); + }); + + // suppress current question + question.suppressThisQuestion = true; + + // are there any questions left that are not suppressed? + const remainingQuestions = glass.questions.filter((q) => { + return !q.suppressThisQuestion; + }); + + if (remainingQuestions.length < 1) { + // this is the final answer for this glass piece + + // mark this glass piece as completely answered by adding answerData + const answeredQuestionObj = { + questionText: question.questionText, + selectedAnswerText: matchedAnswer.answerText, + questionNum: question.questionSequence, + suppressThisQuestion: question.suppressThisQuestion, + }; + // set the answerData (used as indicator that it has been already answered) + glass.answerData = { + answerResult: matchedAnswer.nextQuestionSequence + ? matchedAnswer.nextQuestionSequence + : matchedAnswer.answerResult, + answeredQuestions: [answeredQuestionObj], + }; + + // suppress this glass piece because it has an answer + glass.isSuppressedPart = true; + } + } + }); + + // Update key to force re-render of glass piece with duplicate question in case user changes previous related answer in the chain + self.questionsData[glassIndex].key = + self.questionsData[glassIndex].key + Date.now().toString(); + } + }); + }); + + // set final answer data for the current answered glass part + self.questionsData[answer.index].answerData = { + answerResult: answer.answerResult, + answeredQuestions: answer.answeredQuestions, + }; + + // this part has been fully answered, so advance to next part's question chain + for (let i = answer.index + 1; i < this.questionsData.length; i++) { + // if this part has not yet been fully answered, then make it the current part + if (!this.questionsData[i].answerData?.answerResult) { + this.currentGlassIndex = i; + break; + } + } + }, + // TODO KO Delete `shouldGoToHeritageQuote` + // Can't use `this` because navigateForward is also called from vin-pages-mixin + async navigateForward(partsOrQuestions, vm, shouldGoToHeritageQuote) { + const self = vm ?? this; + const currentPage = self.$route.query.fmgPage; + + const hasPartQuestions = this.hasPartQuestions(partsOrQuestions); + const hasGlassLocationWithMultipleParts = + this.hasGlassLocationWithMultipleParts(partsOrQuestions); + const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions); + const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions); + + if (hasPartQuestions && this.currentPageComesBeforePage(currentPage, issPageValues.PART_QUESTIONS)) + { + self.$router.navigate( + self.navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, + self.$route, + {}, + {}, + { partsOrQuestions: partsOrQuestions } + ); + } else if (hasGlassLocationWithMultipleParts && this.currentPageComesBeforePage(currentPage, issPageValues.VEHICLE_PARTS)) + { + // if multiple parts on any glass + // go to vehicle-parts page and pass the partsData + self.$router.navigate( + self.navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE, + self.$route, + {}, + {}, + { partsOrQuestions: partsOrQuestions } + ); + } else if ( + hasChildPartQuestions && + this.currentPageComesBeforePage(currentPage, issPageValues.MOLDING_QUESTIONS) + ) { + // if any childpart questions + // go to molding-questions page and pass the partsData + self.$router.navigateWithSaving( + self.navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS, + self.$route, + {}, + {}, + { partsOrQuestions: partsOrQuestions } + ); + } else if ( + hasCapabilityQuestions && + this.currentPageComesBeforePage(currentPage, issPageValues.CAPABILITY_QUESTIONS) + ) { + // if has capability questions + // go to capability-questions page and pass the partsData + + // mimic part-questions page data for consistency + for (let partOrQuestion of partsOrQuestions) { + if (this.hasCapabilityQuestions([partOrQuestion])) { + let capabilityQuestionsForGlassLocation = ( + await baseMixin.methods.dispatchStoreAction( + storeActions.GET_CAPABILITY_QUESTIONS, + { + carId: store.getters.vehicle.carId, + partNumber: partOrQuestion.parts[0].partNumber, + } + ) + ).data; + + capabilityQuestionsForGlassLocation.forEach((question) => { + question.answers = question.answers.map((answer) => { + return { + ...answer, + answerResult: answer.answerResult1, + }; + }); + }); + + partOrQuestion.capabilityQuestions = capabilityQuestionsForGlassLocation; + } + } + + self.$router.navigateWithSaving( + self.navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS, + self.$route, + {}, + {}, + { partsOrQuestions } + ); + } else { + // if single parts only + const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions); + + // save to store lineItems.glassParts + self.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts); + + shouldGoToHeritageQuote + ? navigateToHeritageFunnel() + : self.$router.navigateWithSaving( + self.navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, + self.$route + ); + } + }, + // Can't use `this` because navigateForward is also called from quote + navigateBack(vm) { + const self = vm ?? this; + const partsOrQuestions = ( + self.$store.getters.pageData(issPageValues.CAPABILITY_QUESTIONS) ?? + self.$store.getters.pageData(issPageValues.MOLDING_QUESTIONS) ?? + self.$store.getters.pageData(issPageValues.VEHICLE_PARTS) ?? + self.$store.getters.pageData(issPageValues.PART_QUESTIONS) + )?.partsOrQuestions; + const hasPartQuestions = this.hasPartQuestions(partsOrQuestions); + const hasGlassLocationWithMultipleParts = + this.hasGlassLocationWithMultipleParts(partsOrQuestions); + const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions); + const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions); + let backNavigationScenario = self.$store.getters.vehicle.vin + ? navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS + : navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS; + + const currentPage = self.$route.query.fmgPage; + if ( + hasCapabilityQuestions && + this.currentPageComesAfterPage(currentPage, issPageValues.CAPABILITY_QUESTIONS) + ) { + backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS; + } else if ( + hasChildPartQuestions && + this.currentPageComesAfterPage(currentPage, issPageValues.MOLDING_QUESTIONS) + ) { + backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS; + } else if ( + hasGlassLocationWithMultipleParts && + this.currentPageComesAfterPage(currentPage, issPageValues.VEHICLE_PARTS) + ) { + backNavigationScenario = + navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE; + } else if ( + hasPartQuestions && + this.currentPageComesAfterPage(currentPage, issPageValues.PART_QUESTIONS) + ) { + backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS; + } + + self.$router.navigate(backNavigationScenario, self.$route); + }, + // TODO KO delete this after quote mvp + async handleTempButtonClicked(vm) { + this.shouldGoToHeritageQuote = true; + }, + }, +};