This commit is contained in:
Kulbhushan Kaushik 2022-11-23 08:36:21 -05:00
parent a0bcbce6f5
commit 7b78fe1e74
3 changed files with 764 additions and 0 deletions

View file

@ -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 };
}

View file

@ -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;
},
},
};