Merge remote-tracking branch 'origin/develop' into feature/CSR-700

This commit is contained in:
Scott Kiener 2022-07-13 10:03:57 -04:00
commit 0d956d9e0f
59 changed files with 2707 additions and 2340 deletions

View file

@ -13,7 +13,9 @@ module.exports = {
"!src/helpers/unit-test-helper.js", "!src/helpers/unit-test-helper.js",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue", "!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/part-questions/**/*.vue", "!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue" "!src/layouts/reveal/**/*.vue",
"!src/ux-components/text-link/**/*.vue",
"!src/common-components/question-chain/**/*.vue",
// END // END
], // ! means exclude from coverage. ], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -10,6 +10,7 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
propsData: { propsData: {
isOverflowScrollable: true, isOverflowScrollable: true,
groupName: "group-name"
} }
}); });
@ -25,6 +26,7 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
propsData: { propsData: {
buttonType: "listCard", buttonType: "listCard",
groupName: "group-name"
} }
}); });
// Assert // Assert
@ -39,6 +41,7 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
propsData: { propsData: {
buttonType: "listButtonHorizontal", buttonType: "listButtonHorizontal",
groupName: "group-name"
} }
}); });
// Assert // Assert
@ -53,6 +56,7 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, { const wrapper = shallowMount(buttonQuestion, {
propsData: { propsData: {
buttonType: "radio", buttonType: "radio",
groupName: "group-name"
} }
}); });
// Assert // Assert
@ -77,7 +81,8 @@ describe("buttonQuestion.vue", () => {
// Act // Act
const localThis = { const localThis = {
isWide: false, isWide: false,
answers: ['a', 'b'] answers: ['a', 'b'],
groupName: "group-name"
} }
expect(buttonQuestion.computed.getColLength.call(localThis)).toBe(""); expect(buttonQuestion.computed.getColLength.call(localThis)).toBe("");
@ -109,7 +114,7 @@ describe("buttonQuestion.vue", () => {
describe("buttonQuestion.vue", () => { describe("buttonQuestion.vue", () => {
it("Should trigger event modelValue change to new value on when radio button selected", async () => { it("Should trigger event modelValue change to new value on when radio button selected", async () => {
// Act // Act
const wrapper = shallowMount(buttonQuestion, setupMocks({})); const wrapper = shallowMount(buttonQuestion, setupMocks({propsData: {groupName: "group-name"}}));
await wrapper.setProps({ await wrapper.setProps({
answers: ["2022", "2021", "2020"], answers: ["2022", "2021", "2020"],
isMultiSelect: false, isMultiSelect: false,
@ -129,6 +134,7 @@ describe("buttonQuestion.vue", () => {
propsData: { propsData: {
modelValue: ["2022", "2021", "2020"], modelValue: ["2022", "2021", "2020"],
isMultiSelect: true, isMultiSelect: true,
groupName: "group-name"
} }
})); }));
const val = { checkValue: true, value: "2019", } const val = { checkValue: true, value: "2019", }
@ -144,7 +150,8 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, setupMocks({ const wrapper = shallowMount(buttonQuestion, setupMocks({
propsData: { propsData: {
isMultiSelect: true, isMultiSelect: true,
modelValue: ['a', 'b'] modelValue: ['a', 'b'],
groupName: "group-name"
} }
})); }));
const val = { checkValue: true, value: "2021", } const val = { checkValue: true, value: "2021", }
@ -161,7 +168,8 @@ describe("buttonQuestion.vue", () => {
const wrapper = shallowMount(buttonQuestion, setupMocks({ const wrapper = shallowMount(buttonQuestion, setupMocks({
propsData: { propsData: {
isMultiSelect: true, isMultiSelect: true,
modelValue: ['a', 'b'] modelValue: ['a', 'b'],
groupName: "group-name"
} }
})); }));
@ -173,24 +181,6 @@ describe("buttonQuestion.vue", () => {
}); });
}); });
describe("buttonQuestion.vue", () => {
it("Should do nothing to this.selectedValues if this.selectedValues is not an array", () => {
// Act
const wrapper = shallowMount(buttonQuestion, setupMocks({
propsData: {
isMultiSelect: true,
modelValue: 'a',
}
}));
const val = { checkValue: true, value: "c", }
wrapper.vm.handleCheckedChanged(val);
// Assert
expect(wrapper.vm.selectedValues).toEqual("a");
});
});
function setupMocks(mountOptionsMockData = {}) { function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } }; const defaultMountOptions = { route: { query: { fmgPage: 'page-name' } } };
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData)); const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));

View file

@ -1,27 +1,26 @@
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component --> <!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
<template> <template>
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'"> <div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
<div v-if="questionText" class="question-text d-flex"> <div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fs-5 fw-bold w-100">{{ questionText }}</span> <span class="fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName"> <fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName">
<legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1"> <legend class="sr-only" :data-focus-target="formattedGroupName" :id="formattedGroupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }} {{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend> </legend>
<div :class="getComponentWrapperClasses"> <div :class="getComponentLoopWrapperClasses">
<div :class="getComponentWrapperClasses" v-for="answer in answers" :key="answer.Name ? answer.Name : answer">
<component <component
:is="buttonType" :is="buttonType"
v-for="answer in answers"
:key="answer.Name ? answer.Name : answer"
@isCheckedChanged="handleCheckedChanged" @isCheckedChanged="handleCheckedChanged"
:buttonID="answer.Name ? groupName + '-' + answer.Name : groupName + '-' + answer" :buttonID="answer.Name ? formattedGroupName + '-' + answer.Name : formattedGroupName + '-' + answer"
:value="getValues(answer)" :value="getValues(answer)"
:buttonLabel="answer.Text ? answer.Text : answer" :buttonLabel="answer.Text ? answer.Text : answer"
:buttonLabelSubCopy="answer.SubText" :buttonLabelSubCopy="answer.SubText"
:textPosition="textPosition" :textPosition="textPosition"
:isMultiSelect="isMultiSelect" :isMultiSelect="isMultiSelect"
:groupName="groupName" :groupName="formattedGroupName"
:selectingInitiatesLoad="selectingInitiatesLoad" :selectingInitiatesLoad="selectingInitiatesLoad"
:loaderColor="loaderColor" :loaderColor="loaderColor"
:loaderPosition="loaderPosition" :loaderPosition="loaderPosition"
@ -38,11 +37,18 @@
:class="[suppressError ? 'alertError' : '']" :class="[suppressError ? 'alertError' : '']"
:clearOnUnmount="clearOnUnmount" :clearOnUnmount="clearOnUnmount"
/> />
<!-- For nested questions -->
<transition name="fade" mode="out-in">
<div v-if="typeof selectedValues == 'string' && selectedValues == answer.Name">
<slot></slot>
</div>
</transition>
</div> </div>
</div>
</fieldset> </fieldset>
</div> </div>
<div class="row form-test-error mt-1"> <div class="row form-test-error mt-1">
<error-message :name="groupName" v-if="!suppressError"></error-message> <error-message :name="formattedGroupName" v-if="!suppressError"></error-message>
</div> </div>
</div> </div>
</template> </template>
@ -81,22 +87,31 @@ export default {
isRequired: Boolean, isRequired: Boolean,
isOverflowScrollable: Boolean, isOverflowScrollable: Boolean,
isWide: Boolean, isWide: Boolean,
modelValue: Array, modelValue: [Array, String],
validationRules: String, validationRules: String,
suppressError: Boolean, suppressError: Boolean,
useTextForValue: Boolean, useTextForValue: Boolean,
clearOnUnmount: { clearOnUnmount: {
type: Boolean, type: Boolean,
default: true default: true
} },
}, },
computed: { computed: {
getFieldSetClasses() { formattedGroupName() {
return this.isOverflowScrollable return this.groupName.replace(" ", "-");
? "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"
: "";
}, },
getComponentWrapperClasses() { getFieldSetClasses() {
if (this.isOverflowScrollable) {
return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0";
}
else if (this.buttonType == "listCard") {
return "w-100";
}
else {
return "";
}
},
getComponentLoopWrapperClasses() {
let classes; let classes;
switch (this.buttonType) { switch (this.buttonType) {
case "listButton": case "listButton":
@ -106,7 +121,7 @@ export default {
classes = "d-flex flex-row p-0"; classes = "d-flex flex-row p-0";
break; break;
case 'listCard': case 'listCard':
classes = 'row justify-content-center g-2' classes = "row g-2 justify-content-center";
break; break;
case 'radio': case 'radio':
classes = 'ui-radio d-flex' classes = 'ui-radio d-flex'
@ -114,11 +129,22 @@ export default {
} }
return classes; return classes;
}, },
getComponentWrapperClasses() {
let classes = "";
classes += this.isWide ? "col-12" : "col";
if (this.buttonType == "radio") {
classes += " radio-button-container";
}
return classes;
},
getColLength(){ getColLength(){
if(this.isWide) { if(this.isWide) {
return "12" return "12"
} else { } else {
return this.answers.length < 3 ? '' : '-4'; return "";
} }
}, },
selectedValues: { selectedValues: {
@ -135,18 +161,24 @@ export default {
if (this.useTextForValue){ if (this.useTextForValue){
return answer.Text return answer.Text
} }
return answer.Name ? answer.Name : answer; return answer.Name ? answer.Name : answer;
}, },
handleCheckedChanged(val) { handleCheckedChanged(val) {
if(this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.selectedValues = [val.value]; this.selectedValues = val.value;
} else { } else {
if(Array.isArray(this.selectedValues)) { if(Array.isArray(this.selectedValues)) {
const newSelectedValues = this.selectedValues; const newSelectedValues = this.selectedValues;
val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1); val.checkValue ? newSelectedValues.push(val.value) : newSelectedValues.splice(newSelectedValues.indexOf(val.value), 1);
this.selectedValues = newSelectedValues; this.selectedValues = newSelectedValues;
} }
else {
this.selectedValues = val.value;
}
} }
this.$emit("isCheckedChanged", val);
}, },
}, },
components: { components: {
@ -172,10 +204,18 @@ export default {
} }
.button-question { .button-question {
color: $black; color: $black;
.radio-button-container {
&:not(:last-child) {
padding-bottom: map-get($spacers, 2);
}
}
} }
.question-text { .question-text {
margin-top: 1.5rem; margin-top: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
& > span { & > span {
text-align: center; text-align: center;

View file

@ -2,14 +2,14 @@
<div v-for="(q, i) in questions" :key="i"> <div v-for="(q, i) in questions" :key="i">
<transition appear name="fade" mode="out-in"> <transition appear name="fade" mode="out-in">
<buttonQuestion <buttonQuestion
v-if="q.questionSequence === currentQuestion" v-if="q.answerSelected || (q.questionSequence === currentQuestionNum)"
class="radioQuestion" class="radioQuestion"
:class="(q.questionSequence === currentQuestionNum) && 'current-question'"
:questionText="q.questionText" :questionText="q.questionText"
:answers="q.answers" :answers="q.answers"
:groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`" :groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`"
textPosition="text-start"
v-model="selectedValue" v-model="selectedValue"
:isRequired=true isRequired
:validationRules="validationRules" :validationRules="validationRules"
:clearOnUnmount=false :clearOnUnmount=false
/> />
@ -24,97 +24,124 @@ export default {
name: "questionChain", name: "questionChain",
data() { data() {
return { return {
currentQuestion: 1, currentQuestionNum: 1,
answeredQuestions: [], questions: [{ "BlankObject": "NOT USED... placeholder for question #0 to simplify indexing"}],
}; };
}, },
props: { props: {
questionData: Object, questionData: Object,
validationRules: String, validationRules: String,
modelValue: Array, modelValue: Array,
partIndex: Number,
},
created() {
this.questionData.partQuestions.map((q, i) => {
let answerPair = [];
const eachQuestion = {
questionText: q.questionText,
questionSequence: q.questionSequence,
answers: q.answers.map((a) => {
answerPair.push(a.nextQuestionSequence ? a.nextQuestionSequence : a.answerResult);
return {
Text: a.answerText,
// Name will either be nextQuestionSequence or answerResult
// Name will be used by list-button as the input value.
// It must be a single string or number, so concatenating together a string with
// 4 pieces of data separated by pipe characters:
// question number|type of answer|answer value|answer text
Name: a.nextQuestionSequence ?
q.questionSequence + "|nextQuestion|" + a.nextQuestionSequence + "|" + a.answerText :
q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText,
nextQuestionSequence: a.nextQuestionSequence,
}
}),
answerSelected: "",
};
eachQuestion.answerPair = answerPair;
this.questions.push(eachQuestion);
});
}, },
computed: { computed: {
questions() {
console.log("answeredQuestions: ", this.answeredQuestions)
const questions = this.questionData.partQuestions.map((q, i) => {
return {
questionText: q.questionText,
questionSequence: q.questionSequence,
answers: q.answers.map((a) => {
return {
Text: a.answerText,
// Name will either be nextQuestionSequence or answerResult
Name: a.nextQuestionSequence ? a.nextQuestionSequence : "answer-" + a.answerResult,
nextQuestionSequence: a.nextQuestionSequence,
answerResult: a.answerResult,
}
})
}
});
// add an empty item to be array[0] since we start with 1
questions.unshift({ "DeliberatelyBlankObject": "This object has been added as a placeholder only for question #0"});
return questions;
},
selectedValue: { selectedValue: {
get: function() { get: function() {
return this.modelValue; return "";
}, },
set: function(returnedAnswer) { set: function(returnedAnswer) {
const isNewModelValueComplete = this.getNewModelValue(returnedAnswer); const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer);
if (isNewModelValueComplete) { if (isQuestionChainComplete) {
this.$emit("update:modelValue", isNewModelValueComplete); this.$emit("update:modelValue", isQuestionChainComplete);
} }
} }
} },
currentQuestion() {
return this.questions[this.currentQuestionNum];
},
}, },
methods: { methods: {
getNewModelValue(returnedAnswer) { handleReturnedAnswer(returnedAnswer) { // returns either a final answer or Boolean false
if (!returnedAnswer || !Array.isArray(returnedAnswer)) { return false } if (!returnedAnswer) { return false }
const lastAnswer = returnedAnswer[returnedAnswer.length - 1];
const currentQuestion = this.questions[this.currentQuestion];
if (lastAnswer.indexOf("answer-") === 0) { // Example returnedAnswers:
// if it is an answerResult // "1|nextQuestion|3|No"
const finalAnswer = lastAnswer.slice(7); // "5|answer|DW02104|Yes"
const currentQuestionSelectedAnswer = currentQuestion.answers.find( const returnedAnswerArray = returnedAnswer.split("|");
({ answerResult }) => answerResult === finalAnswer const questionNum = returnedAnswerArray[0];
); const questionType = returnedAnswerArray[1];
const questionAnswer = returnedAnswerArray[2];
const questionAnswerText = returnedAnswerArray[3];
// add current item to list of answered questions // remove all previous answers after the index of this one in questions
this.answeredQuestions.push( this.questions.map((q) => {
{ if ((q.questionSequence > questionNum) || (q.answerPair?.includes(questionAnswer))) {
questionText: currentQuestion.questionText, q.answerSelected = "";
selectedAnswerText: currentQuestionSelectedAnswer.Text,
}
);
return {
answerResult: finalAnswer,
answeredQuestions: this.answeredQuestions,
};
} else {
const currentQuestionSelectedAnswer = currentQuestion.answers.find(
({ nextQuestionSequence }) => nextQuestionSequence === parseInt(lastAnswer)
);
// add current item to list of answered questions
this.answeredQuestions.push(
{
questionText: currentQuestion.questionText,
selectedAnswerText: currentQuestionSelectedAnswer.Text,
}
);
this.currentQuestion = parseInt(lastAnswer); // update count to display next question
return false;
} }
return q;
});
// set this question as "answered"
this.questions[questionNum].answerSelected = questionAnswerText;
this.questions[questionNum].answerNumber = questionNum;
// update to next question index
this.currentQuestionNum = questionType === "nextQuestion" ? parseInt(questionAnswer) : parseInt(questionNum); // update count to display next question
// return false if there's a nextQuestion... or return an object with "final" answers
if (questionType === "nextQuestion") {
return false;
} else {
const answeredQuestions = [];
this.questions.forEach(( q ) => {
if (q.answerSelected) {
answeredQuestions.push({
questionText: q.questionText,
selectedAnswerText: q.answerSelected,
questionNum: q.answerNumber,
});
}
});
return {
answerResult: questionAnswer,
answeredQuestions: answeredQuestions,
partIndex: this.partIndex,
};
}
},
},
watch: {
currentQuestion: {
handler() {
// scrolls page to next active question
this.$nextTick(() => {
document.querySelector('.current-question').scrollIntoView({behavior: "smooth"});
})
},
deep: true
} }
}, },
components: { components: {
buttonQuestion, buttonQuestion,
}, },
}; };
</script> </script>

View file

@ -55,6 +55,10 @@ const endpoints = {
url: "/parts/api/v1/parts/parts-or-questions", url: "/parts/api/v1/parts/parts-or-questions",
method: "POST", method: "POST",
}, },
GetParts: {
url: "/parts/api/v1/parts/parts",
method: "POST",
},
SaveOrder: { SaveOrder: {
url: "/order/api/v1/order/save", url: "/order/api/v1/order/save",
method: "POST", method: "POST",

View file

@ -1,7 +1,10 @@
const storeActions = { const storeActions = {
// Content Actions
GET_ROUTE_INFO_ACTION: "getRouteInfo", GET_ROUTE_INFO_ACTION: "getRouteInfo",
GET_HOMEPAGE_NAME: "getHomepageName", GET_HOMEPAGE_NAME: "getHomepageName",
GET_PAGE_DATA: "getPageData", GET_PAGE_DATA: "getPageData",
// Vehicle Actions
GET_VEHICLE_YEARS: "getVehicleYears", GET_VEHICLE_YEARS: "getVehicleYears",
GET_VEHICLE_MAKES: "getVehicleMakes", GET_VEHICLE_MAKES: "getVehicleMakes",
GET_VEHICLE_MODELS: "getVehicleModels", GET_VEHICLE_MODELS: "getVehicleModels",
@ -9,11 +12,15 @@ const storeActions = {
SET_VEHICLE: "setVehicle", SET_VEHICLE: "setVehicle",
GET_DAMAGE_OPTIONS: "getDamageOptions", GET_DAMAGE_OPTIONS: "getDamageOptions",
GET_EVOX_IMAGE: "getEvoxImage", GET_EVOX_IMAGE: "getEvoxImage",
// Lookup Actions
LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms", LOOKUP_VEHICLE_BY_YMMS: "lookupVehicleByYmms",
LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin", LOOKUP_VEHICLE_BY_VIN: "lookupVehicleByVin",
LOOKUP_VIN_BY_PLATE: "lookupVinByPlate", LOOKUP_VIN_BY_PLATE: "lookupVinByPlate",
LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress", LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts",
SAVE_ORDER: "saveOrder", SAVE_ORDER: "saveOrder",
LOAD_ORDER: "loadOrder", LOAD_ORDER: "loadOrder",
UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE: "updateStoreWithSaveOrderResponse", UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE: "updateStoreWithSaveOrderResponse",
@ -23,7 +30,7 @@ const storeActions = {
LOG_CUSTOM_EVENT: "logCustomEvent", LOG_CUSTOM_EVENT: "logCustomEvent",
INITIALIZE_SESSION: "initializeSession", INITIALIZE_SESSION: "initializeSession",
GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser",
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration", CLEAR_VIN: "clearVin",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
@ -31,6 +38,21 @@ const storeActions = {
RESET_REGISTRATION_STATE_AND_DEPENDENCIES: "resetRegistrationAndDependencies", RESET_REGISTRATION_STATE_AND_DEPENDENCIES: "resetRegistrationAndDependencies",
RESET_PARTS_STATE_AND_DEPENDENCIES: "resetPartsAndDependencies", RESET_PARTS_STATE_AND_DEPENDENCIES: "resetPartsAndDependencies",
RESET_STATE: "resetState", RESET_STATE: "resetState",
// SAVE COMPONENT STATE
SAVE_VEHICLE_YEAR: "saveVehicleYear",
SAVE_VEHICLE_MAKE:"saveVehicleMake",
SAVE_VEHICLE_MODEL:"saveVehicleModel",
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
SAVE_VIN_LOOKUP: "saveVinLookup",
SAVE_SERVICE_LOCATION: "saveServiceLocation",
SAVE_EMAIL: "saveEmail",
SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP: "saveRegistrationLicensePlateLookup",
SAVE_VIN: "saveVin",
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
SAVE_GLASS_PARTS: "saveGlassParts",
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
}; };
export { storeActions }; export { storeActions };

View file

@ -11,10 +11,14 @@ const storeMutations = {
UPDATE_VEHICLE_IMAGE_VIF_NUMBER: "updateVehicleImageVifNumber", UPDATE_VEHICLE_IMAGE_VIF_NUMBER: "updateVehicleImageVifNumber",
UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor", UPDATE_VEHICLE_IMAGE_COLOR: "updateVehicleImageColor",
UPDATE_VEHICLE_VIN: "updateVehicleVin", UPDATE_VEHICLE_VIN: "updateVehicleVin",
UPDATE_VEHICLE: "updateVehicle",
UPDATE_IS_REPAIR: "updateIsRepair", UPDATE_IS_REPAIR: "updateIsRepair",
UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips", UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips",
UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace", UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace",
UPDATE_PART_QUESTION_ANSWERS: "updatePartQuestionAnswers",
UPDATE_GLASS_PARTS: "updateGlassParts", UPDATE_GLASS_PARTS: "updateGlassParts",
UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate", UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate",
UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress", UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress",
UPDATE_REGISTRATION_CITY: "updateRegistrationCity", UPDATE_REGISTRATION_CITY: "updateRegistrationCity",
@ -22,8 +26,12 @@ const storeMutations = {
UPDATE_REGISTRATION_ZIP_CODE: "updateRegistrationZipCode", UPDATE_REGISTRATION_ZIP_CODE: "updateRegistrationZipCode",
UPDATE_REGISTRATION_FIRST_NAME: "updateRegistrationFirstName", UPDATE_REGISTRATION_FIRST_NAME: "updateRegistrationFirstName",
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName", UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
UPDATE_REGISTRATION: "updateRegistration",
UPDATE_SERVICE_LOCATION_ZIP_CODE: "updateServiceLocationZipCode", UPDATE_SERVICE_LOCATION_ZIP_CODE: "updateServiceLocationZipCode",
UPDATE_SERVICE_LOCATION_STATE: "updateServiceLocationState", UPDATE_SERVICE_LOCATION_STATE: "updateServiceLocationState",
UPDATE_SERVICE_LOCATION: "updateServiceLocation",
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress", UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
// ORDER MUTATIONS // ORDER MUTATIONS
@ -48,7 +56,6 @@ const storeMutations = {
// OTHER MUTATIONS // OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData", UPDATE_PAGE_DATA: "updatePageData",
UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation", UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation",
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration",
UPDATE_SAVE_ORDER_PROMISE: "updateSaveOrderPromise", UPDATE_SAVE_ORDER_PROMISE: "updateSaveOrderPromise",
UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited",
}; };

View file

@ -3,10 +3,8 @@ import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveOrder } from "@/helpers/heritage-integration/order-helper.js"; import { saveOrder } from "@/helpers/heritage-integration/order-helper.js";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import router from "@/router"; import router from "@/router";
import baseMixin from "@/mixins/base-mixin.js";
/* /*
If the user has visited the funnel before this method will determine the bets place to If the user has visited the funnel before this method will determine the bets place to
@ -54,22 +52,6 @@ export async function navigateToHeritageFunnel() {
); );
} }
export async function navigateAfterSaveToHeritageFunnel(currentRoute) {
const currentComponent = currentRoute.matched[0].components;
currentComponent.default.methods.resetDependentState();
// Create the order (or save existing order) when navigating to Heritage Funnel.
await saveOrder();
router.navigateToExternalUrl(
externalUrls.HERITAGE_FUNNEL,
{
corid: store.getters.order.referralCorrelationId,
src: "concept-funnel"
}
);
}
/* /*
Logic for getting the last "valid" page a user visited. Logic for getting the last "valid" page a user visited.
*/ */

View file

@ -26,11 +26,11 @@ export function getMountOptions(mockData) {
mocks.prependActionToMethod = jest.fn(); mocks.prependActionToMethod = jest.fn();
mocks.dispatchStoreAction = jest.fn(); mocks.dispatchStoreAction = jest.fn();
mocks.dispatchStoreAction.mockImplementation((actionName) => { mocks.dispatchStoreAction.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter( let actionFilterResult = mockData.actionList?.filter(
(x) => x.actionName == actionName (x) => x.actionName == actionName
); );
if (actionFilterResult.length === 1) { if (actionFilterResult?.length === 1) {
return Promise.resolve({ return Promise.resolve({
data: actionFilterResult[0].data, data: actionFilterResult[0].data,
}); });

View file

@ -2,13 +2,14 @@
import addressLookup from "@/layouts/address-lookup/address-lookup.vue"; import addressLookup from "@/layouts/address-lookup/address-lookup.vue";
// Supporting Files // Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import store from "@/store"; import store from "@/store";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/helpers/damage-helper", () => ({ jest.mock("@/helpers/damage-helper", () => ({
@ -17,7 +18,12 @@ jest.mock("@/helpers/damage-helper", () => ({
})); }));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateAfterSaveToHeritageFunnel: jest.fn() navigateToHeritageFunnel: jest.fn()
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
})); }));
describe("address-lookup.vue", () => { describe("address-lookup.vue", () => {
@ -60,7 +66,14 @@ describe("address-lookup.vue", () => {
} }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: true isZipServiceable: true,
vinVehicles: [
{
vehicle: {
carId: "C00000"
}
}
]
}); });
store.commit(storeMutations.UPDATE_CAR_ID, "CARID2"); store.commit(storeMutations.UPDATE_CAR_ID, "CARID2");
@ -89,6 +102,7 @@ describe("address-lookup.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: true, isZipServiceable: true,
isStatePermissible: false,
lookupVinbyAddressResponse: { lookupVinbyAddressResponse: {
isStatePermissible: false, isStatePermissible: false,
vinVehicles: [{ vinVehicles: [{
@ -185,26 +199,17 @@ describe("address-lookup.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: true, isZipServiceable: true,
lookupVinbyAddressResponse: { vinVehicles: [
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{ {
vin: "TEST_VIN2",
vehicle: { vehicle: {
carId: "CARID2" carId: "C11111"
} }
}] }
} ]
}); });
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({ await wrapper.setData({
previouslyEnteredCarId: "C11111",
customerQuestions: { customerQuestions: {
addressQuestions: mockRegistrationAddress addressQuestions: mockRegistrationAddress
}, },
@ -219,52 +224,6 @@ describe("address-lookup.vue", () => {
expect(wrapper.vm.navigateForward).toHaveBeenCalled(); expect(wrapper.vm.navigateForward).toHaveBeenCalled();
}); });
test("if the car entered matches one of multiple vehicles found, update vehicle info and navigate to the heritage funnel", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks({
isZipServiceable: true,
lookupVinbyAddressResponse: {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
})
wrapper.vm.updateVehicleInfo = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.updateVehicleInfo).toHaveBeenCalled();
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalled();
});
test("if the car entered does not match any of the multiple vehicles found, navigate to address-vehicles page", async () => { test("if the car entered does not match any of the multiple vehicles found, navigate to address-vehicles page", async () => {
// Arrange // Arrange
const mockRegistrationAddress = { const mockRegistrationAddress = {
@ -276,21 +235,19 @@ describe("address-lookup.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: true, isZipServiceable: true,
lookupVinbyAddressResponse: { isStatePermissible: true,
isStatePermissible: true, vinVehicles: [{
vinVehicles: [{ vin: "TEST_VIN",
vin: "TEST_VIN", vehicle: {
vehicle: { carId: "CARID"
carId: "CARID" }
} },
}, {
{ vin: "TEST_VIN2",
vin: "TEST_VIN2", vehicle: {
vehicle: { carId: "CARID2"
carId: "CARID2" }
} }]
}]
}
}); });
store.commit(storeMutations.UPDATE_CAR_ID, "CARID_A"); store.commit(storeMutations.UPDATE_CAR_ID, "CARID_A");
@ -320,7 +277,7 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, undefined, {}, {}, carsFound); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, undefined, {}, {}, carsFound);
}); });
test("if the car entered matches one of the vehicles found but the zip is NOT serviceable, do not navigate forward", async () => { test("if the car entered matches one of the vehicles found but the zip is NOT serviceable, do not navigate forward", async () => {
@ -334,21 +291,19 @@ describe("address-lookup.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: false, isZipServiceable: false,
lookupVinbyAddressResponse: { isStatePermissible: true,
isStatePermissible: true, vinVehicles: [{
vinVehicles: [{ vin: "TEST_VIN",
vin: "TEST_VIN", vehicle: {
vehicle: { carId: "CARID"
carId: "CARID" }
} },
}, {
{ vin: "TEST_VIN2",
vin: "TEST_VIN2", vehicle: {
vehicle: { carId: "CARID2"
carId: "CARID2" }
} }]
}]
}
}); });
store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
@ -379,20 +334,10 @@ describe("address-lookup.vue", () => {
} }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
isZipServiceable: true, isZipServiceable: true,
lookupVinbyAddressResponse: { isStatePermissible: true
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID2"
}
}]
}
}); });
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({ await wrapper.setData({
customerQuestions: { customerQuestions: {
addressQuestions: mockRegistrationAddress addressQuestions: mockRegistrationAddress
@ -401,39 +346,30 @@ describe("address-lookup.vue", () => {
isGlassAvailableForCarId: false, isGlassAvailableForCarId: false,
}) })
let carEntered = [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
}];
let carsFound = [{ let carsFound = [{
vin: "TEST_VIN2", vin: "TEST_VIN2",
vehicle: { vehicle: {
carId: "CARID2" carId: "C0000"
} }
}]; }];
// Act // Act
await wrapper.vm.navigateForward(carEntered, carsFound); await wrapper.vm.navigateForward(carsFound);
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, undefined, {}, { "displayVehicleChangeAlert": true }, {}); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, undefined, {}, { "displayVehicleChangeAlert": true });
}); });
test("single car was found and matches entered vehicle => navigateForwardWithSingleCarMatch", async () => { test("single car was found and matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange // Arrange
const carEntered = {
carId: "CARID2"
};
const carsFound = [ const carsFound = [
{ {
vin: "TEST_VIN_2", vin: "TEST_VIN_2",
vehicle: { vehicle: {
carId: "CARID2" carId: "C0000"
} }
} }
]; ];
@ -442,7 +378,7 @@ describe("address-lookup.vue", () => {
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn(); wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act // Act
wrapper.vm.navigateForward(carEntered, carsFound); wrapper.vm.navigateForward(carsFound);
// Assert // Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1); expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
@ -450,15 +386,11 @@ describe("address-lookup.vue", () => {
test("multiple cars were found and one matches entered vehicle => navigateForwardWithSingleCarMatch", async () => { test("multiple cars were found and one matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange // Arrange
const carEntered = {
carId: "CARID2"
};
const carsFound = [ const carsFound = [
{ {
vin: "TEST_VIN_1", vin: "TEST_VIN_1",
vehicle: { vehicle: {
carId: "CARID1" carId: "C0000"
} }
}, },
{ {
@ -475,36 +407,17 @@ describe("address-lookup.vue", () => {
} }
]; ];
const { wrapper } = setupMocks({}, {}); const { wrapper } = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn(); wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act // Act
wrapper.vm.navigateForward(carEntered, carsFound); wrapper.vm.navigateForward(carsFound);
// Assert // Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1); expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
}); });
}); });
describe("resetting dependent state", () => {
test("when reseting dependent state, license plate is set to null and parts state and dependencies are reset", async () => {
// Arrange
const commitSpy = jest.spyOn(store, "commit");
const dispatchSpy = jest.spyOn(store, "dispatch");
const { wrapper } = setupMocks({
isZipServiceable: true
});
// Act
wrapper.vm.resetDependentState();
// Assert
expect(commitSpy).toBeCalledWith(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null);
expect(dispatchSpy).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
});
});
describe("registration and service zips", () => { describe("registration and service zips", () => {
describe("if registration zip is serviceable", () => { describe("if registration zip is serviceable", () => {
test("if registration address is provided => update service address on successful continue", async () => { test("if registration address is provided => update service address on successful continue", async () => {
@ -532,7 +445,9 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION); expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith("lookupVinByAddress", {"licenseLastName": undefined, "licenseState": "OH", "licenseStreetAddress": "1234 Main St", "licenseZip": "43215"}, false);
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith("validateZip", {"zip": "43215"});
}); });
}); });
@ -569,37 +484,37 @@ describe("address-lookup.vue", () => {
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true); expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true);
}); });
test("if registration address is provided user clicks continue => show service zip field on continue click", async () => { // test.only("if registration address is provided user clicks continue => show service zip field on continue click", async () => {
// Arrange // // Arrange
const mockRegistrationAddress = { // const mockRegistrationAddress = {
streetAddress: "1234 Main St", // streetAddress: "1234 Main St",
city: "Columbus", // city: "Columbus",
state: "OH", // state: "OH",
zipCode: "43215" // zipCode: "43215"
} // }
const { wrapper } = setupMocks({ // const { wrapper } = setupMocks({
isZipServiceable: false // isZipServiceable: false
} // }
); // );
expect(wrapper.vm.showServiceZipField).toBeFalsy(); // expect(wrapper.vm.showServiceZipField).toBeFalsy();
expect(wrapper.findComponent({ ref: "serviceZip" }).exists()).toBe(false); // expect(wrapper.findComponent({ ref: "serviceZip" }).exists()).toBe(false);
store.commit(storeMutations.UPDATE_CAR_ID, "CARID"); // store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({ // await wrapper.setData({
customerQuestions: { // customerQuestions: {
addressQuestions: mockRegistrationAddress // addressQuestions: mockRegistrationAddress
} // }
}) // })
// Act // // Act
await wrapper.vm.forwardButtonAction(); // await wrapper.vm.forwardButtonAction();
// Assert // // Assert
expect(wrapper.vm.showServiceZipField).toBe(true); // expect(wrapper.vm.showServiceZipField).toBe(true);
expect(wrapper.findComponent({ ref: "serviceZip" }).isVisible()).toBe(true); // expect(wrapper.findComponent({ ref: "serviceZip" }).isVisible()).toBe(true);
}); // });
test("if registration address, service zip are provided, and user clicks continue => don't update service address", async () => { test("if registration address, service zip are provided, and user clicks continue => don't update service address", async () => {
// Arrange // Arrange
@ -689,15 +604,15 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// // Assert // // Assert
expect(store.getters.order.serviceLocation.zipCode).not.toEqual(store.getters.vehicle.registration.zipCode); expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).not.toEqual(wrapper.vm.$store.getters.vehicle.registration.zipCode);
expect(store.getters.vehicle.registration.zipCode).toEqual("43215"); expect(wrapper.vm.$store.getters.vehicle.registration.zipCode).toEqual("12345");
expect(store.getters.order.serviceLocation.zipCode).toEqual("12345"); expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).toEqual("11111");
}); });
}); });
}); });
}); });
function setupMocks({ isZipServiceable = true, lookupVinbyAddressResponse, partsOrQuestions = [] }) { function setupMocks({ isZipServiceable = true, lookupVinbyAddressResponse, partsOrQuestions = [], isStatePermissible = true, vinVehicles =[], carId = 'C0000'}) {
store.commit(storeMutations.RESET_STATE); store.commit(storeMutations.RESET_STATE);
const wrapper = shallowMount(addressLookup, getMountOptions({ const wrapper = shallowMount(addressLookup, getMountOptions({
actionList: [ actionList: [
@ -728,10 +643,41 @@ function setupMocks({ isZipServiceable = true, lookupVinbyAddressResponse, parts
], ],
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
navigateAfterSave: jest.fn() navigate: jest.fn()
},
store: {
getters: {
vehicle: {
carId: carId,
registration: {
licensePlate: "TESTPLATE",
zipCode: "12345"
}
},
order: {
customer: {
emailAddress: "test@test.com"
},
serviceLocation: {
zipCode: "11111"
}
}
}
}, },
})); }));
const apiResponses = {
serviceZipValidationResponse:{
isServiceable: isZipServiceable
},
vinLookupResponse: {
isStatePermissible: isStatePermissible,
vinVehicles: vinVehicles
},
};
settleAllPromises.mockImplementation(() => apiResponses);
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();

View file

@ -1,67 +1,37 @@
<template> <template>
<Form <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }" autocomplete="off">
@submit="onSubmit" <div class="page-container-grouped-styles">
@invalid-submit="onInvalidSubmit" <loadingModal ref="loadingModal" />
ref="theForm" <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
v-slot="{ meta }" <vehicleBanner cmsWidgetName="VehicleBannerWidget" ref="vehicleBanner" :displayGenericVehicleImage=false />
autocomplete="off" > <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<div class="page-container-grouped-styles"> <div class="fade-on-route-transition sub-container make-tall">
<loadingModal ref="loadingModal"/> <customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" ref="vehicleBanner" :displayGenericVehicleImage=false /> <alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert" class="mb-4" cmsWidgetName="AlertVinNotFoundWidget" alertClass="alert-danger" v-bind:isDismissible="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<div class="fade-on-route-transition sub-container make-tall"> <alert ref="alertMatchedDifferentVehicle" v-if="displayMatchedDifferentVehicleAlert" class="mb-4" :manualHeadline="AlertMatchedDifferentVehicleHeader" :manualCopy="AlertMatchedDifferentVehicleBody" alertClass="alert-warning" v-bind:isDismissible="false" />
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert" <alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert" class="mb-4" alertClass="alert-danger" :manualHeadline="AlertNonServiceableZipHeader" :manualCopy="AlertNonServiceableZipBody" v-bind:isDismissible="false" />
class="mb-4"
cmsWidgetName="AlertVinNotFoundWidget" <alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert" class="mb-4" cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget" alertClass="alert-danger" v-bind:isDismissible="false" />
alertClass="alert-danger"
v-bind:isDismissible="false" <transition name="fade" mode="out-in">
/> <div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
<alert ref="alertMatchedDifferentVehicle" v-if="displayMatchedDifferentVehicleAlert" <div class="row mb-4">
class="mb-4" <div class="col">
:manualHeadline="AlertMatchedDifferentVehicleHeader" <textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZipCode" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" validationRules="service-zip-required|service-zip-format" />
:manualCopy="AlertMatchedDifferentVehicleBody" </div>
alertClass="alert-warning" </div>
v-bind:isDismissible="false" </div>
/> </transition>
<alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert" <funnel-footer cmsWidgetName="FunnelFooterWidget" ref="funnelFooter" :isDisabled="!meta.valid" @ForwardClicked="forwardButtonAction" @back-clicked="backButtonAction" :isForwardActionDisabled="!meta.valid" />
class="mb-4" </div>
alertClass="alert-danger" </div>
:manualHeadline="AlertNonServiceableZipHeader" </Form>
:manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false"
/>
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
class="mb-4"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger"
v-bind:isDismissible="false"
/>
<transition name="fade" mode="out-in">
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
<div class="row mb-4">
<div class="col">
<textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZipCode" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" validationRules="service-zip-required|service-zip-format" />
</div>
</div>
</div>
</transition>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@back-clicked="backButtonAction"
:isForwardActionDisabled="!meta.valid"
/>
</div>
</div>
</Form>
</template> </template>
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
@ -72,360 +42,319 @@ import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { Form, defineRule } from "vee-validate"; import {Form,defineRule} from "vee-validate";
import { required, regex } from "@/helpers/validation-rules"; import {required,regex} from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import {errorMessages} from "@/constants/error-messages";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import {fetchCmsContentForPage} from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import {settleAllPromises} from "@/helpers/layout-helper";
import {storeActions} from "@/constants/store-actions";
import {routerParams} from "@/router/router-constants/router-params";
import {getDamageString,isGlassAvailableForCarId} from "@/helpers/damage-helper";
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin"; import vinPagesMixin from "@/mixins/vin-pages-mixin";
defineRule("service-zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("service-zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("service-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT)); defineRule("service-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default { export default {
name: "address-lookup", name: "address-lookup",
mixins: [vinPagesMixin], mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [{
{ resultKey: "cmsContent",
resultKey: "cmsContent", promise: cmsContentPromise,
promise: cmsContentPromise, }, ];
},
];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
}); });
}, },
data() { data() {
return { return {
customerQuestions: { customerQuestions: {
addressQuestions: { addressQuestions: {
streetAddress: this.getRegistrationAddressFromStore(), streetAddress: this.getRegistrationAddressFromStore(),
city: this.getRegistrationCityFromStore(), city: this.getRegistrationCityFromStore(),
state: this.getRegistrationStateFromStore(), state: this.getRegistrationStateFromStore(),
zipCode: this.getRegistrationZipFromStore(), zipCode: this.getRegistrationZipFromStore(),
}, },
firstName: this.getRegistrationFirstNameFromStore(), firstName: this.getRegistrationFirstNameFromStore(),
lastName: this.getRegistrationLastNameFromStore(), lastName: this.getRegistrationLastNameFromStore(),
emailAddress: this.getEmailFromStore(), emailAddress: this.getEmailFromStore(),
}, },
serviceZipCode: this.getServiceZipFromStore(), serviceZipCode: this.getServiceZipFromStore(),
displayNonServiceableZipAlert: false, displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false, displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false, displayMatchedDifferentVehicleAlert: false,
displayVinLookupByHomeAddressNotAllowedAlert: false, displayVinLookupByHomeAddressNotAllowedAlert: false,
previouslyEnteredCarId: "", previouslyEnteredCarId: "",
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: false,
customAlertData: {}, customAlertData: {},
showServiceZipField: this.getServiceZipFromStore(), showServiceZipField: this.getServiceZipFromStore(),
isZipServicable: false, isZipServiceable: false,
} }
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null; return store.getters.vehicle.carId !== null;
}, },
resetDependentState() { backButtonAction() {
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null); // route to move backwards
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES); this.$router.navigate(
}, this.navigationScenarios.CLICKED_BACK,
backButtonAction() { this.$route
// route to move backwards );
this.$router.navigate( },
this.navigationScenarios.CLICKED_BACK, attachCustomEvents() {
this.$route this.prependActionToMethod(this, this.forwardButtonAction, () => {
); this.pushEventToGA(
}, this.$route.query[this.queryStrings.FMG_PAGE],
attachCustomEvents() { this.GaActions.SUBMITTED,
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.GaLabels.ADDRESS_LOOKUP,
this.pushEventToGA( true
this.$route.query[this.queryStrings.FMG_PAGE], );
this.GaActions.SUBMITTED, });
this.GaLabels.ADDRESS_LOOKUP, },
true getRegistrationAddressFromStore() {
); return this.$store.getters.vehicle.registration.address;
}); },
}, getRegistrationCityFromStore() {
getRegistrationAddressFromStore() { return this.$store.getters.vehicle.registration.city;
return store.getters.vehicle.registration.address; },
}, getRegistrationStateFromStore() {
getRegistrationCityFromStore() { return this.$store.getters.vehicle.registration.state;
return store.getters.vehicle.registration.city; },
}, getRegistrationZipFromStore() {
getRegistrationStateFromStore() { return this.$store.getters.vehicle.registration.zipCode;
return store.getters.vehicle.registration.state; },
}, getRegistrationFirstNameFromStore() {
getRegistrationZipFromStore() { return this.$store.getters.vehicle.registration.firstName;
return store.getters.vehicle.registration.zipCode; },
}, getRegistrationLastNameFromStore() {
getRegistrationFirstNameFromStore() { return this.$store.getters.vehicle.registration.lastName;
return store.getters.vehicle.registration.firstName; },
}, getEmailFromStore() {
getRegistrationLastNameFromStore() { return this.$store.getters.order.customer.emailAddress;
return store.getters.vehicle.registration.lastName; },
}, getServiceZipFromStore() {
getEmailFromStore() { return this.$store.getters.order.serviceLocation.zipCode;
return store.getters.order.customer.emailAddress; },
}, async forwardButtonAction() {
getServiceZipFromStore() { this.resetWarningsAndErrors();
return store.getters.order.serviceLocation.zipCode;
},
async forwardButtonAction() {
this.resetWarningsAndErrors();
// Lookup VIN(s) with the provided address // Vehicle info change in the flow, use a variable to keep track and commit to state at the end.
const vinLookupPromise = this.lookupVin( let vehicleInfoToCommit = {};
this.customerQuestions.lastName,
this.customerQuestions.addressQuestions.streetAddress,
this.customerQuestions.addressQuestions.zipCode,
this.customerQuestions.addressQuestions.state
);
// Verify if the service zip code or registration zip code provided is serviceable const vinLookupResponse = this.dispatchStoreAction(
const serviceZipValidationPromise = this.serviceZipCode ? this.validateZip(this.serviceZipCode) : this.validateZip(this.customerQuestions.addressQuestions.zipCode); storeActions.LOOKUP_VIN_BY_ADDRESS, {
licenseLastName: this.customerQuestions.lastName,
licenseStreetAddress: this.customerQuestions.addressQuestions.streetAddress,
licenseZip: this.customerQuestions.addressQuestions.zipCode,
licenseState: this.customerQuestions.addressQuestions.state
}, false);
const vinLookupResponse = await vinLookupPromise; // Settle promises and get results
const serviceZipValidationResponse = await serviceZipValidationPromise; const promiseResultMap = [{
resultKey: "vinLookupResponse",
promise: vinLookupResponse
},
{
resultKey: "serviceZipValidationResponse",
promise: this.serviceZipCode ?
this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.serviceZipCode}) :
this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.customerQuestions.addressQuestions.zipCode})
}
];
if (!vinLookupResponse.data.isStatePermissible) { const resultMap = await settleAllPromises(promiseResultMap);
// State Restrictions forbid lookup by address
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
this.$refs.funnelFooter.removeLoader();
return;
}
// if the neither the registration zip code or service zip code are not serviceable // Verify if the service zip code or registration zip code provided is serviceable
this.isZipServicable = serviceZipValidationResponse.data.isServiceable; if (!resultMap.vinLookupResponse.isStatePermissible) {
if (!this.isZipServicable) { // State Restrictions forbid lookup by address
this.displayNonServiceableZipAlert = true; this.displayVinLookupByHomeAddressNotAllowedAlert = true;
this.showServiceZipField = true; return this.$refs.funnelFooter.removeLoader();
this.$refs.funnelFooter.removeLoader(); }
} else if (!this.serviceZipCode) {
// if the registration zip code is servicable and nothing was entered for the service zip code
// then set the service zip code to the registration zip code
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
}
const carEntered = store.getters.vehicle; // If the neither the registration zip code or service zip code are not serviceable
const carsFound = vinLookupResponse.data.vinVehicles; this.isZipServiceable = resultMap.serviceZipValidationResponse.isServiceable;
if (!this.isZipServiceable) {
this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true;
return this.$refs.funnelFooter.removeLoader();
}
if (carsFound.length == 0) { // If the registration zip code is serviceable and nothing was entered for the service zip code
// No VINs found // then set the service zip code to the registration zip code
this.displayVinNotFoundAlert = true; if (!this.serviceZipCode) {
this.$refs.funnelFooter.removeLoader(); this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
return; }
} else if (carsFound.length == 1) {
const carFound = carsFound[0].vehicle;
this.isCarIdDifferent = carFound.carId !== carEntered.carId;
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) { const carsFound = resultMap.vinLookupResponse.vinVehicles;
// Display Alert
this.previouslyEnteredCarId = carFound.carId;
this.customAlertData.vehicleInfo = carFound;
this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId); // Handle cases for different amounts of VINS found for the address.
if (carsFound.length == 1) {
// Update button "Continue with..." // Single VIN found
this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`); const carFound = carsFound[0].vehicle;
this.$refs.funnelFooter.removeLoader();
return; this.isCarIdDifferent = carFound.carId !== this.$store.getters.vehicle.carId;
}
if (!this.isZipServicable) { if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
return; // Display Alert
} this.previouslyEnteredCarId = carFound.carId;
this.customAlertData.vehicleInfo = carFound;
this.displayMatchedDifferentVehicleAlert = true;
// update data if the zip or service zip is servicable this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId);
this.updateVehicleInfo(carsFound[0].vin, carFound);
this.updateCustomerInfo(serviceZipValidationResponse.data.state);
} else if (carsFound.length > 1) { // Update button "Continue with..."
if (!this.isZipServicable) { this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`);
return; return this.$refs.funnelFooter.removeLoader();
} }
// if multiple cars were found if (!this.isZipServiceable) {
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId); return;
if (matchingCars.length === 1) { }
// and one and only one of them matches the carId entered, save the vehicle info
// so we can go to the Heritage Funnel directly
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
}
// update data if the zip or service zip is servicable // update data if the zip or service zip is serviceable
this.updateCustomerInfo(serviceZipValidationResponse.data.state); vehicleInfoToCommit = Object.assign(carFound, { vin: carsFound[0].vin });
} } else if (carsFound.length > 1) {
this.navigateForward(carEntered, carsFound); // Multiple VINS found
}, if (!this.isZipServiceable) {
resetWarningsAndErrors() { return;
this.displayVinNotFoundAlert = false; }
this.displayNonServiceableZipAlert = false;
this.displayMatchedDifferentVehicleAlert = false;
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
},
navigateForward(carEntered, carsFound) {
this.updateServiceLocationIfNecessary();
if (carsFound.length == 1) { // If multiple cars were found and one and only one of them matches the carId entered, save the vehicle info
// if a different vehicle is found than the one entered and the selected glass // so we can go to the Heritage Funnel directly
// is not available for that vehicle const matchingCars = carsFound.filter(vin => vin.vehicle.carId === this.$store.getters.vehicle.carId);
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(
// then navigate back to "vehicle-damage", and display vehicle changed alert
// on that page
this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
this.$route, {}, {
displayVehicleChangeAlert: true
}, {}
);
} else {
// otherwise
this.navigateForwardWithSingleCarMatch();
}
} else if (carsFound.length > 1) {
// if multiple cars were found
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
if (matchingCars.length === 1) {
// and one and only of them matches the car id entered
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
this.navigateForwardWithSingleCarMatch();
} else {
// if there are no matches or there are multiple matches, navigate to "address-vehicles" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
}
}
}, if (matchingCars.length === 1) {
validateZip(zip) { vehicleInfoToCommit = Object.assign(matchingCars[0].vehicle, {vin: matchingCars[0].vin});
return this.dispatchStoreAction( }
storeActions.VALIDATE_ZIP, } else {
{ zip });
},
lookupVin(lastName, streetAddress, zip, state) {
return this.dispatchStoreAction(
storeActions.LOOKUP_VIN_BY_ADDRESS,
{
licenseLastName: lastName,
licenseStreetAddress: streetAddress,
licenseZip: zip,
licenseState: state
}, false
);
},
updateVehicleInfo(vin, vehicleInfo) {
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);
store.commit(storeMutations.UPDATE_MAKE, vehicleInfo.make);
store.commit(storeMutations.UPDATE_MODEL, vehicleInfo.model);
store.commit(storeMutations.UPDATE_STYLE, vehicleInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicleInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
},
updateCustomerInfo(serviceState) {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, this.customerQuestions.addressQuestions.state);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.customerQuestions.addressQuestions.zipCode);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
},
updateServiceLocationIfNecessary() {
const serviceLocation = store.getters.order.serviceLocation;
if (!serviceLocation.address && serviceLocation.zipCode && serviceLocation.zipCode == store.getters.vehicle.registration.zipCode) { // No VINS found.
this.dispatchStoreAction(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION); this.displayVinNotFoundAlert = true;
} return this.$refs.funnelFooter.removeLoader();
} }
},
mounted() {
this.attachCustomEvents();
},
computed: {
AlertNonServiceableZipHeader() {
const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode;
const text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zipCode);
return text;
},
AlertNonServiceableZipBody() {
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
},
AlertMatchedDifferentVehicleHeader() {
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
return text;
},
AlertMatchedDifferentVehicleBody() {
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`; // Save vehicle, customer, service and registration information
const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`; await this.dispatchStoreAction(storeActions.SAVE_REGISTRATION_ADDRESS_LOOKUP, {
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
vehicleInfo: Object.keys(vehicleInfoToCommit).length === 0 ? this.$store.getters.vehicle : vehicleInfoToCommit,
registrationInfo: {
firstName: this.customerQuestions.firstName,
lastName: this.customerQuestions.lastName,
address: this.customerQuestions.addressQuestions.streetAddress,
city: this.customerQuestions.addressQuestions.city,
state: resultMap.serviceZipValidationResponse.state,
zipCode: this.customerQuestions.addressQuestions.zipCode
}
}, false);
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText") await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.customerQuestions.emailAddress, false);
.replaceAll("{custom:glassText}", getDamageString()) await this.dispatchStoreAction(storeActions.SAVE_SERVICE_LOCATION, {
.replaceAll("{custom:vinYmmFound}", vinYmmFound) address: this.customerQuestions.addressQuestions.streetAddress,
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected); city: this.customerQuestions.addressQuestions.city,
zipCode: this.serviceZipCode,
state: resultMap.serviceZipValidationResponse.state,
}, false);
return content; return await this.navigateForward(carsFound);
}, },
}, async navigateForward(carsFound) {
watch: { // Match vehicles found to vehicles in state.
customerQuestions: { const matchingCars = carsFound.filter(car => car.vehicle.carId === this.$store.getters.vehicle.carId);
handler(newValue) {
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to Get my personalized quote // If a different vehicle is found than the one entered and the selected glass is not available for that vehicle then navigate back to "vehicle-damage"
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")); // display vehicle changed alert on that page.
this.showServiceZipField = false;
this.resetWarningsAndErrors(); if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle && matchingCars.length === 1) {
}, this.$router.navigate(this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, this.$route, {}, {[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true});
deep: true } else if (matchingCars.length === 1) {
}, await this.navigateForwardWithSingleCarMatch();
serviceZipCode: { } else {
handler(newValue) { this.$router.navigate(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
// if they modify the service zip code, then hide the error message }
this.displayNonServiceableZipAlert = false; },
}, resetWarningsAndErrors() {
}, this.displayVinNotFoundAlert = false;
showServiceZipField: { this.displayNonServiceableZipAlert = false;
handler(newValue) { this.displayMatchedDifferentVehicleAlert = false;
// if the Service Zip Code field is ever hidden, clear out it's value this.displayVinLookupByHomeAddressNotAllowedAlert = false;
if (!newValue) { },
this.serviceZipCode = null; },
} mounted() {
}, this.attachCustomEvents();
} },
}, computed: {
components: { AlertNonServiceableZipHeader() {
funnelHeader, const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode;
funnelFooter, const text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zipCode);
vehicleBanner, return text;
funnelSubHeader, },
customerQuestions, AlertNonServiceableZipBody() {
textboxQuestion, return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
alert, },
loadingModal, AlertMatchedDifferentVehicleHeader() {
Form return this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
}, },
AlertMatchedDifferentVehicleBody() {
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${this.$store.getters.vehicle.year} ${this.$store.getters.vehicle.make} ${this.$store.getters.vehicle.model}`;
return this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
.replaceAll("{custom:glassText}", getDamageString())
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
},
},
watch: {
customerQuestions: {
handler(newValue) {
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to "Get my personalized quote"
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
this.showServiceZipField = false;
this.resetWarningsAndErrors();
},
deep: true
},
serviceZipCode: {
handler(newValue) {
// If they modify the service zip code, then hide the error message.
this.displayNonServiceableZipAlert = false;
},
},
showServiceZipField: {
handler(newValue) {
// If the Service Zip Code field is ever hidden, clear out it's value
if (!newValue) {
this.serviceZipCode = null;
}
},
}
},
components: {
funnelHeader,
funnelFooter,
vehicleBanner,
funnelSubHeader,
customerQuestions,
textboxQuestion,
alert,
loadingModal,
Form
},
}; };
</script> </script>

View file

@ -577,7 +577,7 @@ function setupMocks({ mountOptions, props, isShallowMount = true, querySelectorF
...mountOptions, ...mountOptions,
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
navigateAfterSave: jest.fn() navigate: jest.fn()
}, },
loadScript: jest.fn().mockResolvedValue() loadScript: jest.fn().mockResolvedValue()
}); });

View file

@ -6,7 +6,7 @@
:questionText="questionText" :questionText="questionText"
:answers="vehicles" :answers="vehicles"
v-model="selectedVehicleVinAsArray" v-model="selectedVehicleVinAsArray"
isRequired=true isRequired
:validation-rules="validationRules" :validation-rules="validationRules"
/> />
<alert <alert

View file

@ -81,7 +81,7 @@ describe("addressVehicles.vue", () => {
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse)); wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {}); wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
wrapper.vm.navigateForward = jest.fn().mockImplementation(()=> {}); wrapper.vm.navigateForward = jest.fn().mockImplementation(()=> {});
@ -94,7 +94,6 @@ describe("addressVehicles.vue", () => {
wrapper.vm.$nextTick(); wrapper.vm.$nextTick();
//Assert //Assert
expect(wrapper.vm.updateCustomerInfo).toBeCalled();
expect(wrapper.vm.navigateForward).toBeCalled(); expect(wrapper.vm.navigateForward).toBeCalled();
wrapper.unmount(); wrapper.unmount();
@ -112,7 +111,7 @@ describe("addressVehicles.vue", () => {
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse)); wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {}); wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
// Act // Act
@ -128,88 +127,11 @@ describe("addressVehicles.vue", () => {
wrapper.unmount(); wrapper.unmount();
}); });
test("Should send dispatch reset if carId is different and selected glass not available for vehicle on updateCustomerInfo", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
// Act
await wrapper.setData({
selectedVehicleVin: '5NMS3CADXLH233004',
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.updateCustomerInfo(wrapper.vm.selectedVehicle.vin, wrapper.vm.selectedVehicle.vehicle);
//Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith("resetDamageAndDependencies");
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should send dispatch store action if lookupVin is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
await wrapper.vm.lookupVin('1234567890');
//Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith("lookupVehicleByVin", {"vin": "1234567890"});
wrapper.unmount();
});
test("If selectedVehicleVin changes, then should update isCarIdDifferent", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
await wrapper.setData({
selectedVehicleVin: '5NMS3CADXLH233004',
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.isCarIdDifferent).toBe(true);
wrapper.unmount();
});
test("If selectedVehicleVin changes, then text on funnel footer should be updated", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
// Act
await wrapper.setData({
selectedVehicleVin: '5NMS3CADXLH233004',
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toBeCalled();
wrapper.unmount();
});
test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => { test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$router.navigateAfterSave = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
// Act // Act
await wrapper.setData({ await wrapper.setData({
@ -220,7 +142,7 @@ describe("addressVehicles.vue", () => {
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toBeCalledTimes(1); expect(wrapper.vm.$router.navigate).toBeCalledTimes(1);
wrapper.unmount(); wrapper.unmount();
}); });
@ -230,7 +152,7 @@ describe("addressVehicles.vue", () => {
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn(); wrapper.vm.$refs.loadingModal.showModal = jest.fn();
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
// Act // Act
await wrapper.setData({ await wrapper.setData({

View file

@ -12,12 +12,12 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<alert <alert
ref="alertFoundMultipleVehicles" ref="alertFoundMultipleVehicles"
class="my-5" class="my-5"
alertClass="alert-warning" alertClass="alert-warning"
:manualHeadline="AlertFoundMultipleVehiclesHeader" :manualHeadline="AlertFoundMultipleVehiclesHeader"
manualCopy="" manualCopy=""
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<addressVehiclesQuestion <addressVehiclesQuestion
ref="addressVehiclesQuestion" ref="addressVehiclesQuestion"
cmsWidgetName="VehicleConfirmationQuestion" cmsWidgetName="VehicleConfirmationQuestion"
@ -60,7 +60,6 @@ import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
@ -70,6 +69,7 @@ import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder, splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy, getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper"; getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper";
import { routerParams } from "@/router/router-constants/router-params";
import vinPagesMixin from "@/mixins/vin-pages-mixin"; import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
@ -112,8 +112,7 @@ export default {
return this.VehiclesForQuestions.length; return this.VehiclesForQuestions.length;
}, },
AlertFoundMultipleVehiclesHeader() { AlertFoundMultipleVehiclesHeader() {
let text = this.getCmsContent("FoundMultipleVehicles", "HeadlineText").replaceAll("{custom:vehicleCount}", this.vehicleCount); return this.getCmsContent("FoundMultipleVehicles", "HeadlineText").replaceAll("{custom:vehicleCount}", this.vehicleCount);
return text;
}, },
AlertProvideVinBody() { AlertProvideVinBody() {
return this.getCmsContent("ProvideVinAlert", "BodyText"); return this.getCmsContent("ProvideVinAlert", "BodyText");
@ -123,10 +122,8 @@ export default {
return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody); return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody);
}, },
VehiclesForQuestions() { VehiclesForQuestions() {
const vehiclesData = this.VehiclesFromApi;
// Map API result data, to address-vehicles data structure // Map API result data, to address-vehicles data structure
const mappedData = vehiclesData.map((v) => { const mappedData = this.VehiclesFromApi.map((v) => {
const maskSymbol = "X"; const maskSymbol = "X";
const vinStart = maskSymbol.repeat(v.vin.length-4); const vinStart = maskSymbol.repeat(v.vin.length-4);
const vinEnd = v.vin.substring(v.vin.length-4); const vinEnd = v.vin.substring(v.vin.length-4);
@ -168,54 +165,30 @@ export default {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { async forwardButtonAction() {
const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => {
this.$refs.funnelFooter.removeLoader(); const vinLookup = await this.dispatchStoreAction(storeActions.LOOKUP_VEHICLE_BY_VIN,{ vin: this.selectedVehicle.vin })
}); .catch(() => {this.$refs.funnelFooter.removeLoader();});
if (!vinLookup) { if (!vinLookup) {
return; return;
} }
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId); this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId);
this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle);
this.navigateForward(); await this.dispatchStoreAction(storeActions.SAVE_VIN, {
vehicleInfo: Object.assign(this.selectedVehicle.vehicle, { vin: this.selectedVehicle.vin }),
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle
}, false);
return await this.navigateForward();
}, },
navigateForward() { async navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave( this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD,this.$route,{},{[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true },);
this.navigationScenarios.CLICKED_FORWARD,
this.$route,
{},
{ displayVehicleChangeAlert: true },
);
return;
} else { } else {
this.navigateForwardWithSingleCarMatch(); await this.navigateForwardWithSingleCarMatch();
return;
} }
}, },
lookupVin(vin) {
return this.dispatchStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);
},
resetDependentState() { // needed because navigateAfterSaveToHeritageFunnel calls it
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
updateCustomerInfo(vin, vehicle) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.dispatchStoreAction(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicle.year);
store.commit(storeMutations.UPDATE_MAKE, vehicle.make);
store.commit(storeMutations.UPDATE_MODEL, vehicle.model);
store.commit(storeMutations.UPDATE_STYLE, vehicle.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicle.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicle.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicle.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicle.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicle.imageColor);
},
}, },
watch: { watch: {
@ -223,7 +196,11 @@ export default {
handler() { handler() {
// does this vehicle match the previously selected carId? // does this vehicle match the previously selected carId?
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId; this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`); if (this.isCarIdDifferent) {
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);
} else {
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
}
}, },
deep: true deep: true
}, },

View file

@ -81,7 +81,7 @@ describe("estimate.vue", () => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
await wrapper.setData({ await wrapper.setData({
selectedValues: [vinLookupMethodSelections.HOMEADDRESS] selectedVinLookupMethod: vinLookupMethodSelections.HOMEADDRESS
}) })
//Act //Act
@ -97,11 +97,11 @@ describe("estimate.vue", () => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
await wrapper.setData({ await wrapper.setData({
selectedValues: [vinLookupMethodSelections.MANUALVIN] selectedVinLookupMethod: vinLookupMethodSelections.MANUALVIN
}) })
//Act //Act
wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
//Assert //Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
@ -132,7 +132,7 @@ describe("estimate.vue", () => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
await wrapper.setData({ await wrapper.setData({
selectedValues: [vinLookupMethodSelections.LICENSEPLATE] selectedVinLookupMethod: vinLookupMethodSelections.LICENSEPLATE
}) })
//Act //Act
@ -146,12 +146,9 @@ describe("estimate.vue", () => {
}); });
function setupMocks({ function setupMocks({
modelValueProp = ["Provide my VIN manually most specific to your vehicle"],
isMultiSelect = false,
groupName = "estimate", groupName = "estimate",
cmsQuestionText = "Let's get your VIN. Or we can look it up for you!", cmsQuestionText = "Let's get your VIN. Or we can look it up for you!",
cmsAnswers = [{ Name: "Provide my VIN manually Most specific to your vehicle" }, { Name: "Provide my license plate # Most accurate VIN match" }, { Name: "Provide my home address Most convenient VIN match" }], cmsAnswers = [{ Name: "Provide my VIN manually Most specific to your vehicle" }, { Name: "Provide my license plate # Most accurate VIN match" }, { Name: "Provide my home address Most convenient VIN match" }],
dataFromApi = [],
mountOptionsMockData = { mountOptionsMockData = {
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
@ -166,13 +163,6 @@ function setupMocks({
Answers: cmsAnswers Answers: cmsAnswers
}; };
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn()
}
}
const apiPromise = Promise.resolve(cmsContent); const apiPromise = Promise.resolve(cmsContent);
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); fetchCmsContentForPage.mockImplementation(() => Promise.resolve());

View file

@ -21,7 +21,7 @@
:answers="answersFromCms" :answers="answersFromCms"
groupName="vinLookupMethodOption" groupName="vinLookupMethodOption"
buttonType="listButton" buttonType="listButton"
v-model="selectedValues" v-model="selectedVinLookupMethod"
isRequired isRequired
validationRules="option-required" validationRules="option-required"
/> />
@ -52,7 +52,7 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import store from "@/store"; import store from "@/store";
import { storeMutations } from "@/constants/store-mutations"; import { storeActions } from "@/constants/store-actions";
import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js"; import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js";
// Define Validation Rules // Define Validation Rules
defineRule("option-required", required(errorMessages.OPTION_REQUIRED)); defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
@ -60,7 +60,7 @@ export default {
name: "estimate", name: "estimate",
data() { data() {
return { return {
selectedValues: [], selectedVinLookupMethod: "",
}; };
}, },
@ -86,7 +86,6 @@ export default {
} }
return false; return false;
}, },
resetDependentState() {},
backButtonAction() { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate( this.$router.navigate(
@ -94,28 +93,25 @@ export default {
this.$route this.$route
); );
}, },
forwardButtonAction() { async forwardButtonAction() {
if (this.selectedValues[0] === vinLookupMethodSelections.MANUALVIN) { if (this.selectedVinLookupMethod === vinLookupMethodSelections.MANUALVIN) {
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null); await this.dispatchStoreAction(storeActions.CLEAR_VIN);
this.$router.navigate( return this.$router.navigate(
this.navigationScenarios.SELECTED_MANUAL_VIN, this.navigationScenarios.SELECTED_MANUAL_VIN,
this.$route this.$route
); );
return;
} }
if (this.selectedValues[0] === vinLookupMethodSelections.LICENSEPLATE) { if (this.selectedVinLookupMethod === vinLookupMethodSelections.LICENSEPLATE) {
this.$router.navigate( return this.$router.navigate(
this.navigationScenarios.SELECTED_LICENSE_PLATE, this.navigationScenarios.SELECTED_LICENSE_PLATE,
this.$route this.$route
); );
return;
} }
if (this.selectedValues[0] === vinLookupMethodSelections.HOMEADDRESS) { if (this.selectedVinLookupMethod === vinLookupMethodSelections.HOMEADDRESS) {
this.$router.navigate( return this.$router.navigate(
this.navigationScenarios.SELECTED_HOME_ADDRESS, this.navigationScenarios.SELECTED_HOME_ADDRESS,
this.$route this.$route
); );
return;
} }
}, },
}, },

View file

@ -25,6 +25,12 @@ jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(), fetchCmsContentForPage: jest.fn(),
})); }));
// Mock damage helper
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: () => { return false; },
getDamageString: () => { return 'damage string'; }
}));
describe("license-plate-lookup.vue", () => { describe("license-plate-lookup.vue", () => {
describe("get values from store", () => { describe("get values from store", () => {
test("getLicensePlateFromStore returns store license plate", async () => { test("getLicensePlateFromStore returns store license plate", async () => {
@ -69,7 +75,7 @@ describe("license-plate-lookup.vue", () => {
test("getServiceZipFromStore returns store service zip", async () => { test("getServiceZipFromStore returns store service zip", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
const mockServiceZip = "11111"; const mockServiceZip = "12345";
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, mockServiceZip); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, mockServiceZip);
// ACT // ACT
@ -102,21 +108,20 @@ describe("license-plate-lookup.vue", () => {
describe("on forwardButtonAction click", () => { describe("on forwardButtonAction click", () => {
test("Navigate forward should be called and isCarIdDifferent should be set to false when data entered matches store data on forwardButtonAction click", async () => { test("Navigate forward should be called and isCarIdDifferent should be set to false when data entered matches store data on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
const mockCarId = "TESTID";
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => { // Arrange
return { data: { isServiceable: true } }; const mockCarId = "TESTID";
}); const { wrapper } = setupMocks({ carId: mockCarId, isServiceable: true });
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => Promise.resolve({
data: {
vehicle: {
carId: mockCarId
}
}
}));
//Act //Act
licensePlateLookup.beforeRouteEnter.call( licensePlateLookup.beforeRouteEnter.call(
@ -133,46 +138,27 @@ describe("license-plate-lookup.vue", () => {
expect(wrapper.vm.navigateForward).toHaveBeenCalled(); expect(wrapper.vm.navigateForward).toHaveBeenCalled();
}); });
test("Function should stop and datam isRegistrationZipServicable should be set to false when service zip entered returns false on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: false } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.isRegistrationZipServicable).toEqual(false);
});
test("Function should stop and datam isCarIdDifferent should be set to true when carId entered doesn't match store carId or previously entered carId on forwardButtonAction click", async () => { test("Function should stop and datam isCarIdDifferent should be set to true when carId entered doesn't match store carId or previously entered carId on forwardButtonAction click", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({});
// Setup state data / return data.
const { wrapper } = setupMocks({ carId: "C111111", isServiceable: true });
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []); store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: true } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; return '';
}); });
const vinLookup = { data: { vehicle: { carId: "TESTID1" } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => { // Mock store action call
return new Promise(resolve => resolve(vinLookup)); wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => Promise.resolve({
}); data: {
vehicle: {
carId: "C00000" // Make sure carId returned from call does not match carId in state.
}
}
}));
//Act //Act
licensePlateLookup.beforeRouteEnter.call( licensePlateLookup.beforeRouteEnter.call(
@ -191,21 +177,24 @@ describe("license-plate-lookup.vue", () => {
test("Navigate forward should be called and isCarId should be set to true when carId entered matches previously entered carId and rest of data entered matches store data on forwardButtonAction click", async () => { test("Navigate forward should be called and isCarId should be set to true when carId entered matches previously entered carId and rest of data entered matches store data on forwardButtonAction click", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({ carId: "C10000", isServiceable: true });
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return { data: { isServiceable: true } };
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; return '';
}); });
const vinLookup = { data: { vehicle: { carId: "TESTID1" } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => { wrapper.vm.previouslyEnteredCarId = "C00000";
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.previouslyEnteredCarId = "TESTID1";
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Mock store action call
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => Promise.resolve({
data: {
vehicle: {
carId: "C00000" // Make sure carId returned from call does not match carId in state.
}
}
}));
//Act //Act
licensePlateLookup.beforeRouteEnter.call( licensePlateLookup.beforeRouteEnter.call(
wrapper.vm, wrapper.vm,
@ -223,7 +212,7 @@ describe("license-plate-lookup.vue", () => {
}); });
describe("navigateForward", () => { describe("navigateForward", () => {
test("navigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => { test("navigate should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -233,8 +222,8 @@ describe("license-plate-lookup.vue", () => {
isCarIdDifferent: true, isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false isSelectedGlassAvailableForVehicle: false
}) })
wrapper.vm.$router.navigateAfterSave = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; return '';
}); });
@ -243,10 +232,10 @@ describe("license-plate-lookup.vue", () => {
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
test("navigateAfterSaveToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => { test("navigateToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -258,11 +247,11 @@ describe("license-plate-lookup.vue", () => {
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; return '';
}); });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
//Assert //Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toHaveBeenCalled(); expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
}); });
test("carId matches returned vehicle => navigateForwardWithSingleCarMatch", async () => { test("carId matches returned vehicle => navigateForwardWithSingleCarMatch", async () => {
@ -353,28 +342,18 @@ describe("license-plate-lookup.vue", () => {
test("registrationZip is serviceable and vehicle match is found => sets service zip/state to registration zip/state", async () => { test("registrationZip is serviceable and vehicle match is found => sets service zip/state to registration zip/state", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
const mockCarId = "TESTID";
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId)
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
if (zip)
return { data: { isServiceable: true, state: "OH" } };
return
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => new Promise(resolve => resolve(vinLookup)));
await wrapper.setData({ registrationZip: "00000" }); await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(store.getters.order.serviceLocation.zipCode).toEqual(store.getters.vehicle.registration.zipCode); expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).toEqual(wrapper.vm.$store.getters.vehicle.registration.zipCode);
expect(store.getters.vehicle.registration.zipCode).toEqual("00000"); expect(wrapper.vm.$store.getters.vehicle.registration.zipCode).toEqual("12345");
expect(store.getters.order.serviceLocation.zipCode).toEqual("00000"); expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).toEqual("12345");
}) })
test("vehicle match is found but registrationZip is not serviceable => shows service zip/state field", async () => { test("vehicle match is found but registrationZip is not serviceable => shows service zip/state field", async () => {
@ -385,7 +364,7 @@ describe("license-plate-lookup.vue", () => {
}); });
await wrapper.setData({ registrationZip: "00000" }); await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
@ -404,9 +383,9 @@ describe("license-plate-lookup.vue", () => {
}); });
await wrapper.setData({ registrationZip: "00000" }); await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
wrapper.vm.$router.navigateAfterSave = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
// At this point, serviceZip field is shown // At this point, serviceZip field is shown
// Act // Act
@ -417,32 +396,33 @@ describe("license-plate-lookup.vue", () => {
const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']"); const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']");
expect(serviceZipField.exists()).toBe(true); expect(serviceZipField.exists()).toBe(true);
expect(serviceZipField.isVisible()).toBe(true); 3 expect(serviceZipField.isVisible()).toBe(true); 3
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).not.toHaveBeenCalled(); expect(navigateToHeritage.navigateToHeritageFunnel).not.toHaveBeenCalled();
expect(wrapper.vm.$router.navigateAfterSave).not.toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).not.toHaveBeenCalled();
}); });
test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => user can continue", async () => { test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => user can continue", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({ isServiceable: false});
const registrationZip = "00000"; const registrationZip = "00000";
const serviceZip = "99999"; const serviceZip = "99999";
const mockCarId = "TestCarId";
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
return { data: { isServiceable: zip == registrationZip ? false : true, state: "XX" } };
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => Promise.resolve({
data: {
vehicle: {
carId: "C00000"
}
}
}));
await wrapper.setData({ registrationZip: registrationZip }); await wrapper.setData({ registrationZip: registrationZip });
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// At this point, serviceZip field is shown
// At this point, serviceZip field is shown
await wrapper.setData({ serviceZip: serviceZip }); await wrapper.setData({ serviceZip: serviceZip });
// Act // Act
// Continue after entering input into service zip field // Continue after entering input into service zip field
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
@ -450,37 +430,38 @@ describe("license-plate-lookup.vue", () => {
const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']"); const serviceZipField = wrapper.findComponent("[cmsWidgetName='ServiceZip']");
expect(serviceZipField.exists()).toBe(true); expect(serviceZipField.exists()).toBe(true);
expect(serviceZipField.isVisible()).toBe(true); expect(serviceZipField.isVisible()).toBe(true);
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
}); });
test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => service and registration zips/states saved", async () => { test("registrationZip is not serviceable so serviceZip field is shown, user enters serviceZip => service and registration zips/states saved", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({ isServiceable: true });
const registrationZip = "00000"; const registrationZip = "12345";
const serviceZip = "99999"; const serviceZip = "12345";
const mockCarId = "TestCarId";
store.commit(storeMutations.UPDATE_CAR_ID, mockCarId);
wrapper.vm.validateZip = jest.fn().mockImplementation((zip) => {
return { data: { isServiceable: zip == registrationZip ? false : true, state: "XX" } };
});
const vinLookup = { data: { vehicle: { carId: mockCarId } } }
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return new Promise(resolve => resolve(vinLookup));
});
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => Promise.resolve({
data: {
vehicle: {
carId: "C00000"
}
}
}));
await wrapper.setData({ registrationZip: registrationZip }); await wrapper.setData({ registrationZip: registrationZip });
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// At this point, serviceZip field is shown // At this point, serviceZip field is shown
await wrapper.setData({ serviceZip: serviceZip }); await wrapper.setData({ serviceZip: serviceZip });
// Act // Act
// Continue after entering value into service zip field // Continue after entering value into service zip field
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(store.getters.vehicle.registration.zipCode).toEqual(registrationZip); expect(wrapper.vm.$store.getters.vehicle.registration.zipCode).toEqual(registrationZip);
expect(store.getters.order.serviceLocation.zipCode).toEqual(serviceZip); expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).toEqual(serviceZip);
expect(wrapper.vm.navigateForward).toHaveBeenCalled(); expect(wrapper.vm.navigateForward).toHaveBeenCalled();
}); });
}) })
@ -506,60 +487,15 @@ describe("license-plate-lookup.vue", () => {
expect(arePagePrerequisitesValid).toBe(true); expect(arePagePrerequisitesValid).toBe(true);
}); });
test("Dispatch reset damage and dependencies should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when updateCustomerInfo is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.setData({
isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false
})
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
store.commit = jest.fn();
const vehicleInfo = { year: "2020", make: "honda", model: "civic", style: "2 door", carId: "TestId", category: "testCat", imageUrl: "image.jpg", imageVifNumber: "123", imageColor: "blue" }
await wrapper.vm.updateCustomerInfo('vin', vehicleInfo, 'registrationState');
//Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
})
test("dispatchStoreAction called on validate zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.validateZip("12345");
//Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
});
test("dispatchStoreAction called on lookup vin", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.lookupVin("zzz123fqsfwg");
//Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
});
}) })
}); });
function setupMocks({ function setupMocks({
pageHeaderWidgetHeaderText = {}, pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {}, mountOptionsMockData = {},
partsOrQuestions = [] partsOrQuestions = [],
isServiceable = false,
carId = ""
}) { }) {
store.commit(storeMutations.RESET_STATE); store.commit(storeMutations.RESET_STATE);
//Mock api responses //Mock api responses
@ -575,6 +511,12 @@ function setupMocks({
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3", "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
}, },
}, },
serviceZipValidationResponse: {
isServiceable: isServiceable
},
registrationZipValidationResponse: {
state: "CO"
}
}; };
mountOptionsMockData = { mountOptionsMockData = {
@ -582,6 +524,25 @@ function setupMocks({
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
}, },
store: {
getters: {
vehicle: {
registration: {
licensePlate: "TESTPLATE",
zipCode: "12345"
},
carId: carId
},
order: {
customer: {
emailAddress: "test@test.com"
},
serviceLocation: {
zipCode: "12345"
}
}
}
},
actionList: [ actionList: [
{ {
actionName: storeActions.GET_PARTS_OR_QUESTIONS, actionName: storeActions.GET_PARTS_OR_QUESTIONS,

View file

@ -1,9 +1,17 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="page-container-grouped-styles"> <div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/> <loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" /> <vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false"
/>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2"> <div class="row my-2">
@ -42,13 +50,13 @@
class="my-3" class="my-3"
:manualHeadline="NoServiceZipHeader" :manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody" :manualCopy="NoServiceZipBody"
v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent" v-if="!isRegistrationZipServiceable && isVinValid && !isCarIdDifferent"
alertClass="alert-danger" alertClass="alert-danger"
/> />
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion <textboxQuestion
v-if="!isRegistrationZipServicable" v-if="!isRegistrationZipServiceable"
cmsWidgetName="ServiceZip" cmsWidgetName="ServiceZip"
v-model="serviceZip" v-model="serviceZip"
inputId="serviceZip" inputId="serviceZip"
@ -56,11 +64,14 @@
/> />
</div> </div>
</div> </div>
<alert class="my-3" <alert
cmsWidgetName="NoMatchAlertWidget" class="my-3"
cmsWidgetName="NoMatchAlertWidget"
v-if="!isVinValid" v-if="!isVinValid"
alertClass="alert-warning" /> alertClass="alert-warning"
<alert class="my-3" />
<alert
class="my-3"
:manualHeadline="MatchedDifferentVehicleAlertHeader" :manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody" :manualCopy="MatchedDifferentVehicleAlertBody"
v-if="isCarIdDifferent" v-if="isCarIdDifferent"
@ -78,7 +89,6 @@
</Form> </Form>
</template> </template>
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
@ -87,25 +97,42 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from "@/common-components/loading-modal/loading-modal.vue";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { getDamageString, isGlassAvailableForCarId, } from "@/helpers/damage-helper"; import { getDamageString,isGlassAvailableForCarId} from "@/helpers/damage-helper";
import { required, regex, } from "@/helpers/validation-rules"; import { routerParams } from "@/router/router-constants/router-params";
import { Form, defineRule, } from "vee-validate"; import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import vinPagesMixin from "@/mixins/vin-pages-mixin"; import vinPagesMixin from "@/mixins/vin-pages-mixin";
import store from "@/store";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED)); defineRule(
"license-plate-required",
required(errorMessages.LICENSE_PLATE_REQUIRED)
);
defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT)); defineRule(
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED)); "zip-format",
defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT)); regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT)
);
defineRule(
"email-address-required",
required(errorMessages.EMAIL_ADDRESS_REQUIRED)
);
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
export default { export default {
name: "license-plate-lookup", name: "license-plate-lookup",
@ -132,7 +159,7 @@ export default {
}, },
data() { data() {
return { return {
isRegistrationZipServicable: true, isRegistrationZipServiceable: true,
isVinValid: true, isVinValid: true,
isCarIdDifferent: false, isCarIdDifferent: false,
licensePlate: this.getLicensePlateFromStore(), licensePlate: this.getLicensePlateFromStore(),
@ -150,38 +177,19 @@ export default {
}, },
computed: { computed: {
MatchedDifferentVehicleAlertHeader() { MatchedDifferentVehicleAlertHeader() {
let text = this.getCmsContent( return this.getCmsContent("MatchedDifferentVehicleAlertWidget","HeadlineText").replaceAll("{custom:damage}", getDamageString());
"MatchedDifferentVehicleAlertWidget",
"HeadlineText"
).replaceAll("{custom:damage}", getDamageString());
return text;
}, },
MatchedDifferentVehicleAlertBody() { MatchedDifferentVehicleAlertBody() {
let text = this.getCmsContent( return this.getCmsContent("MatchedDifferentVehicleAlertWidget","BodyText")
"MatchedDifferentVehicleAlertWidget",
"BodyText"
)
.replaceAll("{custom:damage}", getDamageString()) .replaceAll("{custom:damage}", getDamageString())
.replaceAll( .replaceAll("{custom:plateLookupYear}",this.customAlertData?.vehicleInfo?.year)
"{custom:plateLookupYear}", .replaceAll("{custom:plateLookupMake}",this.customAlertData?.vehicleInfo?.make)
this.customAlertData?.vehicleInfo?.year .replaceAll("{custom:plateLookupModel}",this.customAlertData?.vehicleInfo?.model);
)
.replaceAll(
"{custom:plateLookupMake}",
this.customAlertData?.vehicleInfo?.make
)
.replaceAll(
"{custom:plateLookupModel}",
this.customAlertData?.vehicleInfo?.model
);
return text;
}, },
NoServiceZipHeader() { NoServiceZipHeader() {
let text = this.getCmsContent( return this.getCmsContent("NoServiceZipWidget","HeadlineText")
"NoServiceZipWidget", .replaceAll("{custom:zip}", this.zipToDisplay);
"HeadlineText"
).replaceAll("{custom:zip}", this.zipToDisplay);
return text;
}, },
NoServiceZipBody() { NoServiceZipBody() {
return this.getCmsContent("NoServiceZipWidget", "BodyText"); return this.getCmsContent("NoServiceZipWidget", "BodyText");
@ -191,142 +199,110 @@ export default {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null; return store.getters.vehicle.carId !== null;
}, },
resetDependentState() {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, null);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, null);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, null);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, null);
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
attachCustomEvents() { attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA( this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE], this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED, this.GaActions.SUBMITTED,
this.GaLabels.LICENSE_PLATE_LOOKUP, this.GaLabels.LICENSE_PLATE_LOOKUP,
true true
); );
}); });
}, },
getLicensePlateFromStore() { getLicensePlateFromStore() {
return store.getters.vehicle.registration.licensePlate; return this.$store.getters.vehicle.registration.licensePlate;
}, },
getRegistrationZipFromStore() { getRegistrationZipFromStore() {
return store.getters.vehicle.registration.zipCode; return this.$store.getters.vehicle.registration.zipCode;
}, },
getEmailFromStore() { getEmailFromStore() {
return store.getters.order.customer.emailAddress; return this.$store.getters.order.customer.emailAddress;
}, },
getServiceZipFromStore() { getServiceZipFromStore() {
return store.getters.order.serviceLocation.zipCode; return this.$store.getters.order.serviceLocation.zipCode;
}, },
backButtonAction() { backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { async forwardButtonAction() {
//Call zip validation services const registrationZipValidationResponse = this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.registrationZip });
const registrationZipValidationPromise = this.validateZip(this.registrationZip);
const serviceZipValidationPromise = this.serviceZip ? this.validateZip(this.serviceZip) : null; // Settle promises and get results
const registrationZipValidationResults = await registrationZipValidationPromise; const promiseResultMap = [
const serviceZipValidationResults = serviceZipValidationPromise !== null ? (await serviceZipValidationPromise) : registrationZipValidationResults; {
resultKey: "registrationZipValidationResponse",
promise: registrationZipValidationResponse,
},
{
resultKey: "serviceZipValidationResponse",
promise: this.serviceZip ? this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.serviceZip }) : registrationZipValidationResponse,
}
];
const resultMap = await settleAllPromises(promiseResultMap);
//Handle service zip validations //Handle service zip validations
if (!serviceZipValidationResults.data.isServiceable) { if (!resultMap.serviceZipValidationResponse.isServiceable) {
this.$refs.funnelFooter.removeLoader();
this.isVinValid = true; this.isVinValid = true;
this.isRegistrationZipServicable = false; this.isRegistrationZipServiceable = false;
this.isCarIdDifferent = false; this.isCarIdDifferent = false;
this.zipToDisplay = this.serviceZip ? this.serviceZip : this.registrationZip; this.zipToDisplay = this.serviceZip ? this.serviceZip : this.registrationZip;
return; return this.$refs.funnelFooter.removeLoader();
} else if (!this.serviceZip) { }
this.serviceZip = this.registrationZip;
} if (!this.serviceZip) {
this.serviceZip = this.registrationZip;
}
//Lookup vin //Lookup vin
const vinLookup = await this.lookupVin( const vinLookup = await this.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_PLATE, {licensePlate: this.licensePlate, licenseState: resultMap.registrationZipValidationResponse.state}, false)
this.licensePlate, .catch(() => {
registrationZipValidationResults.data.state
).catch(() => {
this.$refs.funnelFooter.removeLoader();
this.isVinValid = false; this.isVinValid = false;
this.isCarIdDifferent = false; this.isCarIdDifferent = false;
return; return this.$refs.funnelFooter.removeLoader();
}); });
this.isCarIdDifferent = // Check if the CarId has changed.
vinLookup.data.vehicle.carId !== store.getters.vehicle.carId; this.isCarIdDifferent = vinLookup.data.vehicle.carId !== this.$store.getters.vehicle.carId;
//Handle changing car //Handle changing car
if ( if (this.isCarIdDifferent && vinLookup.data.vehicle.carId !== this.previouslyEnteredCarId) {
this.isCarIdDifferent && this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.vehicle.carId);
vinLookup.data.vehicle.carId !== this.previouslyEnteredCarId this.$refs.funnelFooter.updateButtonText(`Continue with ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`);
) {
this.previouslyEnteredCarId = vinLookup.data.vehicle.carId; this.previouslyEnteredCarId = vinLookup.data.vehicle.carId;
this.customAlertData.vehicleInfo = vinLookup.data.vehicle; this.customAlertData.vehicleInfo = vinLookup.data.vehicle;
this.$refs.funnelFooter.updateButtonText(
`Continue with ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`
);
this.isVinValid = true; this.isVinValid = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.vehicle.carId);
this.$refs.funnelFooter.removeLoader(); return this.$refs.funnelFooter.removeLoader();
return;
} }
//Save // Save vin, vehicle, customer, service and registration information
this.updateCustomerInfo( await this.dispatchStoreAction(storeActions.SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP, {
vinLookup.data.vin, isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
vinLookup.data.vehicle, vehicleInfo: Object.assign(vinLookup.data.vehicle, { vin: vinLookup.data.vin }),
registrationZipValidationResults.data.state, registrationInfo: {
serviceZipValidationResults.data.state licensePlate: this.licensePlate,
); state: resultMap.registrationZipValidationResponse.state,
zipCode: this.registrationZip,
}
}, false);
//Navigate await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
this.navigateForward(); await this.dispatchStoreAction(storeActions.SAVE_SERVICE_LOCATION, {
zipCode: this.serviceZip,
state: resultMap.serviceZipValidationResponse.state,
}, false);
return await this.navigateForward();
}, },
navigateForward() { async navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave( this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD,this.$route,{}, {[routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true });
this.navigationScenarios.CLICKED_FORWARD,
this.$route,
{},
{ displayVehicleChangeAlert: true },
{}
);
return;
} else { } else {
this.navigateForwardWithSingleCarMatch(); await this.navigateForwardWithSingleCarMatch();
return;
} }
}, },
validateZip(zip) {
return this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
zip,
});
},
lookupVin(plate, state) {
return this.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_PLATE,{ licensePlate: plate, licenseState: state }, false);
},
updateCustomerInfo(vin, vehicleInfo, registrationState, serviceState) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.dispatchStoreAction(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);
store.commit(storeMutations.UPDATE_MAKE, vehicleInfo.make);
store.commit(storeMutations.UPDATE_MODEL, vehicleInfo.model);
store.commit(storeMutations.UPDATE_STYLE, vehicleInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicleInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageVifColor);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, this.licensePlate);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.registrationZip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
},
}, },
watch: { watch: {
licensePlate() { licensePlate() {

View file

@ -1,39 +1,67 @@
<template> <template>
<div class="page-container-grouped-styles"> <Form
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> @submit="onSubmit"
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false /> @invalid-submit="onInvalidSubmit"
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> ref="theForm"
<div class="fade-on-route-transition sub-container make-tall"> v-slot="{ meta }"
<h1>Part Questions Page Placeholder</h1> >
<funnel-footer cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction" /> <div class="page-container-grouped-styles part-questions">
</div> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div> <vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="alertFewMoreQuestions"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader"
:manualCopy="AlertFewMoreQuestionsCopy"
v-bind:isDismissible="false"
/>
<div v-for="(part, i) in partsQuestionsData" :key="i">
<questionChain
ref="questionChain"
v-model="selectedModel"
:questionData="part"
:partIndex="i"
v-if="showThisPartQuestionChain(part, i)"
validationRules="questions-required"
/>
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="waitingForLoad || !meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template> </template>
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
// Supporting Files // Supporting Files
import { import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
fetchCmsContentForPage import { settleAllPromises } from "@/helpers/layout-helper";
} from "@/helpers/cms-content-helper";
import {
settleAllPromises
} from "@/helpers/layout-helper";
import {
storeMutations
} from "@/constants/store-mutations";
import {
storeActions
} from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import { import { storeActions } from "@/constants/store-actions";
fmgPageValues import { storeMutations } from "@/constants/store-mutations";
} from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default { export default {
name: "part-questions", name: "part-questions",
@ -42,10 +70,12 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [{ const promiseResultMap = [
resultKey: "cmsContent", {
promise: cmsContentPromise, resultKey: "cmsContent",
}, ]; promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -54,7 +84,46 @@ export default {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
}); });
}, },
data() {
return {
selectedModel: [],
partsQuestionsData: [],
waitingForLoad: true,
currentPartNum: 0,
glassNameAndParts: [],
hasMultipleParts: false,
};
},
computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent("AlertPartsQuestions", "HeadlineText");
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AlertPartsQuestions", "BodyText");
},
},
async mounted() {
const questionData = await this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS);
// Map API result data, to part-questions data structure
this.partsQuestionsData = questionData.partsOrQuestions.filter((p) => {
if (Array.isArray(p.partQuestions) && p.partQuestions.length > 0) {
return {
glassName: p.glassName,
glassLocation: p.glassLocation,
partQuestions: p.partQuestions,
};
}
});
this.waitingForLoad = false;
},
methods: { methods: {
showThisPartQuestionChain(part, i) {
if (part.partQuestions?.length < 1) { return false; } // return false if only one partQuestion
if (this.currentPartNum === i || part.answerData?.answerResult.length > 0) { return true; }
return false;
},
backButtonAction() { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate( this.$router.navigate(
@ -62,23 +131,93 @@ export default {
this.$route this.$route
); );
}, },
arePagePrerequisitesValid() { async forwardButtonAction() {
return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length !== 0; const partQuestionAnswersArray = this.partsQuestionsData.map((item) => {
}, return {
resetDependentState() { glassLocation: item.glassLocation,
// Set glassName: item.glassName,
store.commit(storeMutations.UPDATE_GLASS_PARTS, null); result: item.answerData.answerResult,
answeredQuestions: item.answerData.answeredQuestions,
};
});
// Invokes // save to vuex store as order.damage.partQuestionAnswers (array)
store.dispatch(storeActions.RESET_PARTS_AND_DEPS); await this.dispatchStoreAction(this.storeActions.SAVE_PART_QUESTION_ANSWERS, partQuestionAnswersArray, false);
// call API parts method
const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS)
.catch(() => {
this.$refs.funnelFooter.removeLoader();
});
if (!partsLookup) { return }
this.glassNameAndParts = partsLookup.data.glassNameAndParts;
// test response data for multiple parts
this.hasMultipleParts = this.glassNameAndParts.some((glass) => glass.parts?.length > 1);
// loop through all glass items
const collectedGlassParts = [];
this.glassNameAndParts.forEach((glass) => {
if (Array.isArray(glass.parts) && glass.parts.length === 1) {
const singlePart = glass.parts[0];
collectedGlassParts.push({
"partNumber": singlePart.partNumber,
"description": singlePart.description,
"color": singlePart.color,
"requiresRecalibration": singlePart.requiresRecalibration,
"childParts": singlePart.childParts,
"price": singlePart.price,
})
}
});
store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
this.navigateForward();
},
async navigateForward() {
if (this.hasMultipleParts) {
// if multiple parts on any glass
// go to vehicle-parts page and pass the partsData
this.$router.navigate(this.navigationScenarios.ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS,this.$route,{},{},{partsOrQuestions: this.glassNameAndParts});
} else {
// if single parts only
// go to quote page
this.$router.navigate(this.navigationScenarios.ANSWERED_QUESTIONS_WITH_SINGLE_PART,this.$route);
}
},
arePagePrerequisitesValid() {
return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length > 0;
},
},
watch: {
selectedModel(model) {
this.partsQuestionsData[model.partIndex].answerData = {
answerResult: model.answerResult,
answeredQuestions: model.answeredQuestions,
}
this.currentPartNum = model.partIndex + 1;
}, },
}, },
data() {},
components: { components: {
funnelHeader, funnelHeader,
vehicleBanner, vehicleBanner,
alert,
questionChain,
funnelSubHeader, funnelSubHeader,
funnelFooter, funnelFooter,
Form,
}, },
}; };
</script> </script>
<style lang="scss">
.part-questions {
.question-text {
margin-bottom: .5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -0,0 +1,10 @@
<script>
export default {
name: "quote",
methods: {
arePagePrerequisitesValid() {
return true;
}
}
}
</script>

View file

@ -51,7 +51,6 @@ export default {
resetDependentState() { resetDependentState() {
// Set // Set
store.commit(storeMutations.UPDATE_GLASS_PARTS, null); store.commit(storeMutations.UPDATE_GLASS_PARTS, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_PARTS_AND_DEPS); store.dispatch(storeActions.RESET_PARTS_AND_DEPS);
}, },

View file

@ -9,11 +9,10 @@ import replaceOptionsQuestion from "@/layouts/vehicle-damage/replace-options-que
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount, flushPromises } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store"; import store from "@/store";
import { validate } from "vee-validate"; import { validate } from "vee-validate";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
@ -78,7 +77,7 @@ describe("vehicle-damage.vue", () => {
}); });
test("Replace several pieces of glass on ForwardButtonAction triggers a router.navigateAfterSave and saves selections to store", async () => { test("Replace several pieces of glass on ForwardButtonAction triggers a router.navigate and saves selections to store", async () => {
//Arrange //Arrange
const partsData = { const partsData = {
partsOrQuestions: [{ partsOrQuestions: [{
@ -119,7 +118,7 @@ describe("vehicle-damage.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
pageHeaderWidgetHeaderText: "", pageHeaderWidgetHeaderText: "",
mountOptionsMockData: { mountOptionsMockData: {
router: { navigateAfterSave: jest.fn(), }, router: { navigate: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },], actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: { store: {
getters: { getters: {
@ -134,7 +133,7 @@ describe("vehicle-damage.vue", () => {
wrapper.vm.selectedWindshieldOptions = { wrapper.vm.selectedWindshieldOptions = {
selectedWindshieldChipCount: null, selectedWindshieldChipCount: null,
selectedWindshieldReplaceOptions: ["Single"], selectedWindshieldReplaceOptions: ["Single"],
selectedWindshieldDamageType: ["Replace"] selectedWindshieldDamageType: "Replace"
}; };
wrapper.vm.sideDoorOptionsData = { wrapper.vm.sideDoorOptionsData = {
@ -159,13 +158,12 @@ describe("vehicle-damage.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_IS_REPAIR, false); expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(storeActions.GET_DAMAGE_OPTIONS, {"carId": "C00000000"});
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_GLASS_TO_REPLACE, expectedGlassToReplace);
}); });
test("Windshield replace with multiple parts on ForwardButtonAction triggers a router.navigateAfterSave and saves selections to store", async () => { test("Windshield replace with multiple parts on ForwardButtonAction triggers a router.navigate and saves selections to store", async () => {
//Arrange //Arrange
const partsData = { const partsData = {
partsOrQuestions: [ partsOrQuestions: [
@ -219,7 +217,7 @@ describe("vehicle-damage.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
pageHeaderWidgetHeaderText: "", pageHeaderWidgetHeaderText: "",
mountOptionsMockData: { mountOptionsMockData: {
router: { navigateAfterSave: jest.fn(), }, router: { navigate: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },], actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: partsData, },],
store: { store: {
getters: { getters: {
@ -234,7 +232,7 @@ describe("vehicle-damage.vue", () => {
wrapper.vm.selectedWindshieldOptions = { wrapper.vm.selectedWindshieldOptions = {
selectedWindshieldChipCount: null, selectedWindshieldChipCount: null,
selectedWindshieldReplaceOptions: ["Single"], selectedWindshieldReplaceOptions: ["Single"],
selectedWindshieldDamageType: ["Replace"] selectedWindshieldDamageType: "Replace"
}; };
const expectedGlassToReplace = [{ glassLocation: "Windshield", glassName: "Single" },]; const expectedGlassToReplace = [{ glassLocation: "Windshield", glassName: "Single" },];
@ -250,10 +248,9 @@ describe("vehicle-damage.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_IS_REPAIR, false); expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(storeActions.GET_DAMAGE_OPTIONS, {"carId": "C00000000"});
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_GLASS_TO_REPLACE, expectedGlassToReplace);
}); });
@ -378,7 +375,7 @@ describe("vehicle-damage.vue", () => {
); );
wrapper.vm.selectedDamageLocations = ["Windshield"]; wrapper.vm.selectedDamageLocations = ["Windshield"];
wrapper.vm.selectedWindshieldOptions = { selectedWindshieldDamageType: ["Repair"] }; wrapper.vm.selectedWindshieldOptions = { selectedWindshieldDamageType: "Repair" };
//Assert //Assert
expect(wrapper.vm.isWindshieldRepair).toEqual(true); expect(wrapper.vm.isWindshieldRepair).toEqual(true);
@ -398,7 +395,7 @@ describe("vehicle-damage.vue", () => {
); );
wrapper.vm.selectedDamageLocations = ["SideDoor"]; wrapper.vm.selectedDamageLocations = ["SideDoor"];
wrapper.vm.selectedWindshieldOptions = { selectedWindshieldDamageType: ["Repair"] }; wrapper.vm.selectedWindshieldOptions = { selectedWindshieldDamageType: "Repair" };
//Assert //Assert
expect(wrapper.vm.isWindshieldRepair).toEqual(false); expect(wrapper.vm.isWindshieldRepair).toEqual(false);
@ -465,25 +462,6 @@ describe("vehicle-damage.vue", () => {
expect(arePagePrerequisitesValid).toBe(true); expect(arePagePrerequisitesValid).toBe(true);
}); });
test("Call invalidation, ResetPartsAndState should be called", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-damage" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.dispatch).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES)
});
}); });
describe("input validations", () => { describe("input validations", () => {
@ -531,7 +509,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm) (c) => c(wrapper.vm)
); );
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ location: damageLocation }] }, isRepair: true }; store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ glassLocation: damageLocation }] }, isRepair: true };
var glassSelections = wrapper.vm.getDamageLocationsFromStore(); var glassSelections = wrapper.vm.getDamageLocationsFromStore();
@ -540,19 +518,19 @@ describe("vehicle-damage.vue", () => {
}); });
const storeWindshieldOptions = [[1, false, "Windshield", "Single", { const storeWindshieldOptions = [[1, false, "Windshield", "Single", {
selectedWindshieldDamageType: [damageLocationsSelected.REPLACE], selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.SINGLE] selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.SINGLE]
}], }],
[2, false, "Windshield", "Driver", { [2, false, "Windshield", "Driver", {
selectedWindshieldDamageType: [damageLocationsSelected.REPLACE], selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.DRIVER] selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.DRIVER]
}], }],
[3, false, "Windshield", "Passenger", { [3, false, "Windshield", "Passenger", {
selectedWindshieldDamageType: [damageLocationsSelected.REPLACE], selectedWindshieldDamageType: damageLocationsSelected.REPLACE,
selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.PASSENGER] selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: [damageLocationsSelected.PASSENGER]
}], }],
[4, true, "", "", { [4, true, "", "", {
selectedWindshieldDamageType: [damageLocationsSelected.REPAIR], selectedWindshieldDamageType: damageLocationsSelected.REPAIR,
selectedWindshieldChipCount: [2], selectedWindshieldReplaceOptions: [] selectedWindshieldChipCount: [2], selectedWindshieldReplaceOptions: []
}] }]
]; ];
@ -575,7 +553,7 @@ describe("vehicle-damage.vue", () => {
eventBusItem: jest.fn(), eventBusItem: jest.fn(),
damage: damage:
{ {
glassToReplace: [{ location: damageLocation, name: damageName }], glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
isRepair: isRepair, isRepair: isRepair,
numberOfChips: 2 numberOfChips: 2
}, },
@ -605,7 +583,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm) (c) => c(wrapper.vm)
); );
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ location: damageLocation, name: damageName }] }, isRepair: true }; store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }] }, isRepair: true };
var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore(); var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore();
@ -632,7 +610,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm) (c) => c(wrapper.vm)
); );
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ location: damageLocation, name: damageName }] }, isRepair: true }; store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }] }, isRepair: true };
var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore(); var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore();
@ -656,7 +634,7 @@ describe("vehicle-damage.vue", () => {
(c) => c(wrapper.vm) (c) => c(wrapper.vm)
); );
store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ location: damageLocation, name: damageName }] }, isRepair: true }; store.getters = { vehicle: { carId: "C0000000" }, eventBusItem: jest.fn(), damage: { glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }] }, isRepair: true };
var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore(); var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore();
@ -682,7 +660,7 @@ describe("vehicle-damage.vue", () => {
// Arrange // Arrange
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { navigateAfterSave: jest.fn(), }, router: { navigate: jest.fn(), },
actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: {}, },], actionList: [{ actionName: storeActions.GET_PARTS_OR_QUESTIONS, data: {}, },],
store: { store: {
getters: { getters: {

View file

@ -162,10 +162,6 @@ export default {
return false; return false;
}, },
resetDependentState() {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
attachCustomEvents(){ attachCustomEvents(){
if(this.$store.getters.vehicle.imageVifNumber){ if(this.$store.getters.vehicle.imageVifNumber){
this.pushEventToGA(this.GaCategories.EVOX, `${this.GaActions.VIF}_${this.$store.getters.vehicle.imageVifNumber}`, this.pushEventToGA(this.GaCategories.EVOX, `${this.GaActions.VIF}_${this.$store.getters.vehicle.imageVifNumber}`,
@ -184,16 +180,16 @@ export default {
getDamageLocationsFromStore() { getDamageLocationsFromStore() {
var glassSelections = []; var glassSelections = [];
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD }) || if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.WINDSHIELD }) ||
store.getters.damage.isRepair) { store.getters.damage.isRepair) {
glassSelections.push(damageLocationsSelected.WINDSHIELD); glassSelections.push(damageLocationsSelected.WINDSHIELD);
} }
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER || if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.DRIVER ||
glass.location === damageLocationsSelected.PASSENGER })) { glass.glassLocation === damageLocationsSelected.PASSENGER })) {
glassSelections.push(damageLocationsSelected.SIDEDOOR); glassSelections.push(damageLocationsSelected.SIDEDOOR);
} }
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.REAR })) { if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.REAR })) {
glassSelections.push(damageLocationsSelected.REARWINDOW); glassSelections.push(damageLocationsSelected.REARWINDOW);
} }
@ -201,32 +197,32 @@ export default {
}, },
getWindshieldOptionsFromStore() { getWindshieldOptionsFromStore() {
var windShieldOptions = { selectedWindshieldDamageType: [], selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: []}; var windShieldOptions = { selectedWindshieldDamageType: "", selectedWindshieldChipCount: [], selectedWindshieldReplaceOptions: []};
if (store.getters.damage.isRepair === undefined) return windshieldOptions; if (store.getters.damage.isRepair === undefined) return windshieldOptions;
if (!store.getters.damage.isRepair) { if (!store.getters.damage.isRepair) {
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.SINGLE })) { glass.glassName === damageLocationsSelected.SINGLE })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPLACE;
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.SINGLE); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.SINGLE);
} }
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.DRIVER })) { glass.glassName === damageLocationsSelected.DRIVER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPLACE;
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.DRIVER); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.DRIVER);
} }
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.WINDSHIELD && if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.WINDSHIELD &&
glass.name === damageLocationsSelected.PASSENGER })) { glass.glassName === damageLocationsSelected.PASSENGER })) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPLACE); windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPLACE;
windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.PASSENGER); windShieldOptions.selectedWindshieldReplaceOptions.push(damageLocationsSelected.PASSENGER);
} }
} }
if (store.getters.damage.isRepair) { if (store.getters.damage.isRepair) {
windShieldOptions.selectedWindshieldDamageType.push(damageLocationsSelected.REPAIR); windShieldOptions.selectedWindshieldDamageType = damageLocationsSelected.REPAIR;
windShieldOptions.selectedWindshieldChipCount.push(store.getters.damage.numberOfChips); windShieldOptions.selectedWindshieldChipCount.push(store.getters.damage.numberOfChips);
} }
@ -236,11 +232,11 @@ export default {
getDoorSidesFromStore() { getDoorSidesFromStore() {
var doorSides = []; var doorSides = [];
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.DRIVER })){ if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.DRIVER })){
doorSides.push(damageLocationsSelected.DRIVERSIDE); doorSides.push(damageLocationsSelected.DRIVERSIDE);
} }
if (store.getters.damage.glassToReplace?.some(glass => { return glass.location === damageLocationsSelected.PASSENGER })){ if (store.getters.damage.glassToReplace?.some(glass => { return glass.glassLocation === damageLocationsSelected.PASSENGER })){
doorSides.push(damageLocationsSelected.PASSENGERSIDE); doorSides.push(damageLocationsSelected.PASSENGERSIDE);
} }
@ -251,8 +247,8 @@ export default {
var driverSideReplaceOptions = []; var driverSideReplaceOptions = [];
store.getters.damage.glassToReplace?.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.DRIVER){ if (glass.glassLocation === damageLocationsSelected.DRIVER){
driverSideReplaceOptions.push(glass.name); driverSideReplaceOptions.push(glass.glassName);
} }
}); });
@ -263,8 +259,8 @@ export default {
var passengerSideReplaceOptions = []; var passengerSideReplaceOptions = [];
store.getters.damage.glassToReplace?.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.PASSENGER){ if (glass.glassLocation === damageLocationsSelected.PASSENGER){
passengerSideReplaceOptions.push(glass.name); passengerSideReplaceOptions.push(glass.glassName);
} }
}); });
@ -275,8 +271,8 @@ export default {
var rearReplaceOptions = []; var rearReplaceOptions = [];
store.getters.damage.glassToReplace?.forEach(glass => { store.getters.damage.glassToReplace?.forEach(glass => {
if (glass.location === damageLocationsSelected.REAR){ if (glass.glassLocation === damageLocationsSelected.REAR){
rearReplaceOptions.push(glass.name); rearReplaceOptions.push(glass.glassName);
} }
}); });
@ -284,29 +280,24 @@ export default {
}, },
async forwardButtonAction() { async forwardButtonAction() {
store.commit(this.storeMutations.UPDATE_IS_REPAIR, this.isWindshieldRepair);
if (this.isWindshieldRepair){ await this.dispatchStoreAction(this.storeActions.SAVE_VEHICLE_DAMAGE, {
store.commit(this.storeMutations.UPDATE_NUMBER_OF_CHIPS, parseInt(this.selectedWindshieldOptions.selectedWindshieldChipCount)); isWindshieldRepair: this.isWindshieldRepair,
} else { selectedGlassToReplace: this.selectedGlassToReplace(),
store.commit(this.storeMutations.UPDATE_NUMBER_OF_CHIPS, null); selectedWindshieldChipCount: this.selectedWindshieldOptions.selectedWindshieldChipCount
} }, false);
store.commit(this.storeMutations.UPDATE_GLASS_TO_REPLACE, this.selectedGlassToReplace()); return this.navigateForward();
this.navigateForward();
}, },
navigateForward(){ navigateForward(){
// If vin already exists, navigate directly to vin-lookup // If vin already exists, navigate directly to vin-lookup
if(store.getters.vehicle.vin) { if(store.getters.vehicle.vin) {
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route);
return;
} }
else { else {
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, this.$route);
return;
} }
}, },
@ -359,12 +350,7 @@ export default {
}); });
}, },
isWindshieldRepair() { isWindshieldRepair() {
if (!this.isWindshieldDamageLocation) return false; return this.isWindshieldDamageLocation && this.selectedWindshieldOptions.selectedWindshieldDamageType === damageLocationsSelected.REPAIR;
return this.selectedWindshieldOptions.selectedWindshieldDamageType && this.selectedWindshieldOptions.selectedWindshieldDamageType.some(selectedDamageType =>
{
return selectedDamageType.toUpperCase() === "REPAIR";
});
}, },
isDriverSideReplace() { isDriverSideReplace() {
if (!this.isSideDoorDamageLocation) return false; if (!this.isSideDoorDamageLocation) return false;
@ -386,7 +372,7 @@ export default {
return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair; return this.isWindshieldDamageLocation && this.selectedDamageLocations.length > 1 && this.isWindshieldRepair;
}, },
hasSplitSingleConflict() { hasSplitSingleConflict() {
if (!this.selectedDamageLocations || !this.selectedDamageLocations.includes("Windshield") || !this.selectedWindshieldOptions.selectedWindshieldDamageType || !this.selectedWindshieldOptions.selectedWindshieldDamageType.includes("Replace") || !this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false; if (!this.selectedDamageLocations?.includes("Windshield") || this.selectedWindshieldOptions.selectedWindshieldDamageType === damageLocationsSelected.REPAIR || !this.selectedWindshieldOptions.selectedWindshieldReplaceOptions) return false;
return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield => return this.selectedWindshieldOptions.selectedWindshieldReplaceOptions.some(selectedSingleWindshield =>
{ {
@ -406,7 +392,7 @@ export default {
return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]; return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT];
}, },
shouldHideBackButton() { shouldHideBackButton() {
return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration; return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie()?.HasDelayedClaimRegistration;
} }
}, },

View file

@ -22,7 +22,7 @@ import store from "@/store";
export default ({ export default ({
name: "windshieldDamageTypeQuestion", name: "windshieldDamageTypeQuestion",
props: { props: {
modelValue: Array, modelValue: String,
groupName: String, groupName: String,
isAvailable: Boolean, isAvailable: Boolean,
suppressError: Boolean, suppressError: Boolean,

View file

@ -4,7 +4,7 @@
:isAvailable=isWindshieldDamageLocation :isAvailable=isWindshieldDamageLocation
:suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError" :suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError"
groupName="WindshieldDamageTypeQuestion" groupName="WindshieldDamageTypeQuestion"
v-model="selectedWindshieldDamageTypeValues" v-model="selectedWindshieldDamageTypeValue"
:validationRules="windshieldDamageTypeQuestionValidationRules" :validationRules="windshieldDamageTypeQuestionValidationRules"
/> />
<alert <alert
@ -21,7 +21,7 @@
validationRules="windshield-chip-count-required" validationRules="windshield-chip-count-required"
/> />
<replaceOptionsQuestion ref="replaceOptionsQuestion" cmsWidgetName="WindshieldReplaceOptionsQuestion" <replaceOptionsQuestion ref="replaceOptionsQuestion" cmsWidgetName="WindshieldReplaceOptionsQuestion"
:isAvailable=isReplaceOptionSelected :isAvailable="isReplaceOptionSelected"
isMultiSelect isMultiSelect
groupName="WindshieldReplaceOptions" groupName="WindshieldReplaceOptions"
v-model="selectedWindshieldReplaceOptionsValues" v-model="selectedWindshieldReplaceOptionsValues"
@ -55,21 +55,13 @@ defineRule("windshield-damage-type-required", required(errorMessages.WINDSHIELD_
defineRule("windshield-chip-count-required", required(errorMessages.WINDSHIELD_CHIP_COUNT_REQUIRED)); defineRule("windshield-chip-count-required", required(errorMessages.WINDSHIELD_CHIP_COUNT_REQUIRED));
defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED)); defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED));
defineRule("check-for-repair-and-replace", (value, [otherFieldValue]) => { defineRule("check-for-repair-and-replace", (selectedWindshieldDamageType, [selectedDamageLocations]) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.REPAIR.toUpperCase()) && return selectedWindshieldDamageType.toString() != damageLocationsSelected.REPAIR ||
otherFieldValue.toString().toUpperCase().includes(damageLocationsSelected.WINDSHIELD.toUpperCase()) && !selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD) ||
Array.isArray(otherFieldValue) && selectedDamageLocations.length === 1;
otherFieldValue.length > 1)
{
return false;
}
return true;
}); });
defineRule("repair-only", (value) => { defineRule("repair-only", (value) => {
if (value.toString().toUpperCase() === damageLocationsSelected.REPAIR.toUpperCase()) { return value.toString() === damageLocationsSelected.REPAIR;
return true;
}
return false;
}); });
defineRule("prevent-split-and-single-together", (value) => { defineRule("prevent-split-and-single-together", (value) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) && if (value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) &&
@ -91,7 +83,7 @@ export default ({
}, },
props: { props: {
modelValue: Array, modelValue: String,
selectedDamageLocations: Array, selectedDamageLocations: Array,
hasRepairReplaceConflict: Boolean, hasRepairReplaceConflict: Boolean,
hasSplitSingleConflict: Boolean, hasSplitSingleConflict: Boolean,
@ -120,7 +112,7 @@ export default ({
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
} }
}, },
selectedWindshieldDamageTypeValues:{ selectedWindshieldDamageTypeValue: {
get: function() { get: function() {
return this.selectedValues.selectedWindshieldDamageType; return this.selectedValues.selectedWindshieldDamageType;
}, },
@ -133,7 +125,7 @@ export default ({
return this.selectedValues.selectedWindshieldChipCount; return this.selectedValues.selectedWindshieldChipCount;
}, },
set: function(newValue) { set: function(newValue) {
this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValues, newValue, null); this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValue, newValue, null);
} }
}, },
selectedWindshieldReplaceOptionsValues: { selectedWindshieldReplaceOptionsValues: {
@ -141,7 +133,7 @@ export default ({
return this.selectedValues.selectedWindshieldReplaceOptions; return this.selectedValues.selectedWindshieldReplaceOptions;
}, },
set: function(newValue) { set: function(newValue) {
this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValues, null, newValue); this.selectedValues = this.getWindshieldOptions(this.selectedWindshieldDamageTypeValue, null, newValue);
} }
}, },
isWindshieldDamageLocation() { isWindshieldDamageLocation() {
@ -151,14 +143,10 @@ export default ({
}); });
}, },
isRepairOptionSelected(){ isRepairOptionSelected(){
if (!this.selectedWindshieldDamageTypeValues) return false; return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPAIR && this.isWindshieldDamageLocation;
return this.selectedWindshieldDamageTypeValues.some(val => val.toUpperCase() === "REPAIR") && this.isWindshieldDamageLocation;
}, },
isReplaceOptionSelected(){ isReplaceOptionSelected(){
if (!this.selectedWindshieldDamageTypeValues) return false; return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPLACE && this.isWindshieldDamageLocation;
return this.selectedWindshieldDamageTypeValues.some(val => val.toUpperCase() === "REPLACE") && this.isWindshieldDamageLocation;
}, },
isWindshieldReplaceAvailable() { isWindshieldReplaceAvailable() {
return !(Array.isArray(this.windshieldAvailableReplacementOptions) && this.windshieldAvailableReplacementOptions.length < 1); return !(Array.isArray(this.windshieldAvailableReplacementOptions) && this.windshieldAvailableReplacementOptions.length < 1);

View file

@ -7,8 +7,8 @@
:answers="makes" :answers="makes"
groupName="ChooseVehicleMake" groupName="ChooseVehicleMake"
textPosition="text-start" textPosition="text-start"
v-model="selectedValueAsArray" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>
@ -34,14 +34,12 @@ export default {
questionText(){ questionText(){
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
}, },
selectedValueAsArray: { selectedValue: {
get: function() { get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return this.modelValue
return modelValueAsArray;
}, },
set: function(newValue) { set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; this.$emit("update:modelValue", newValue);
this.$emit("update:modelValue", newValueAsScalar);
} }
} }
}, },

View file

@ -4,14 +4,11 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper.js"; import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
// Components // Components
import vehicleMake from "@/layouts/vehicle-make/vehicle-make.vue"; import vehicleMake from "@/layouts/vehicle-make/vehicle-make.vue";
import makeQuestion from "@/layouts/vehicle-make/make-question/make-question"; import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
import store from "@/store";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
commit: jest.fn(), commit: jest.fn(),
@ -110,33 +107,6 @@ describe("vehicle-make.vue", () => {
}); });
}); });
describe("vehicle-make.vue", () => {
test("Year set, call invalidation, model, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES)
});
});
function setupMocks({ function setupMocks({
vehicleMakeQuestionCmsContent = {}, vehicleMakeQuestionCmsContent = {},

View file

@ -81,27 +81,12 @@ export default {
} }
return false; return false;
}, },
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_MODEL, null);
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
},
}, },
watch: { watch: {
selectedMake(make) { selectedMake(make) {
this.$store.commit(this.storeMutations.UPDATE_MAKE, make); this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
this.$router.navigateAfterSave( this.$router.navigate(
this.navigationScenarios.SELECTED_MAKE, this.navigationScenarios.SELECTED_MAKE,
this.$route this.$route
); );

View file

@ -7,8 +7,8 @@
:answers="models" :answers="models"
groupName="ChooseVehicleModel" groupName="ChooseVehicleModel"
textPosition="text-start" textPosition="text-start"
v-model="selectedValueAsArray" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>
@ -34,14 +34,12 @@ export default {
questionText(){ questionText(){
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
}, },
selectedValueAsArray: { selectedValue: {
get: function() { get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return this.modelValue
return modelValueAsArray;
}, },
set: function(newValue) { set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; this.$emit("update:modelValue", newValue);
this.$emit("update:modelValue", newValueAsScalar);
} }
} }
}, },

View file

@ -8,10 +8,7 @@ import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import store from "@/store";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
@ -106,32 +103,6 @@ describe("vehicle-model.vue", () => {
}); });
}); });
describe("vehicle-model.vue", () => {
test("Year set, call invalidation, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES)
});
});
function setupMocks({ function setupMocks({
buttonQuestionContent = {}, buttonQuestionContent = {},

View file

@ -82,26 +82,12 @@ export default {
} }
return false; return false;
}, },
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
},
}, },
watch: { watch: {
selectedModel(model) { selectedModel(model) {
this.$store.commit(this.storeMutations.UPDATE_MODEL, model); this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
this.$router.navigateAfterSave( this.$router.navigate(
this.navigationScenarios.SELECTED_MODEL, this.navigationScenarios.SELECTED_MODEL,
this.$route this.$route
); );

View file

@ -69,24 +69,25 @@ describe("glass-part-question.vue", () => {
}); });
test("Should emit updateModelValue, and have correct attributes", async () => { test("Should emit updateModelValue, and have correct attributes", async () => {
//Arrange //Arrange
const { wrapper } = setupMocks(featureListData); const { wrapper } = setupMocks(featureListData);
store.getters.pageData.mockReset();
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] });
//Act //Act
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
const listCard = await wrapper.findComponent({ const listCard = await wrapper.findComponent({
name: "listCard", name: "buttonQuestion",
}); });
wrapper.setValue({ selectedTint: 'Green Tint' }); await wrapper.setData({ selectedTint: 'Green Tint' });
// to trigger the computed setter
wrapper.vm.selectedPartNumber = "DB12209GTYN";
//Assert //Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedTint: 'Green Tint' }]); expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ partNumber: "DB12209GTYN", color: "Green Tint"}]);
expect(listCard.attributes("buttonid")).toBe("Rear-Stationary-Green Tint");
expect(listCard.attributes("groupname")).toBe("Rear-Stationary"); expect(listCard.attributes("groupname")).toBe("Rear-Stationary");
expect(listCard.attributes("isradio")).toBe("true"); expect(listCard.attributes("validationrules")).toBe("Rear-Stationary-tint-required");
}); });
test("ResetTintAndPartSelections, should reset data elements ", async () => { test("ResetTintAndPartSelections, should reset data elements ", async () => {
@ -96,21 +97,101 @@ describe("glass-part-question.vue", () => {
//Act //Act
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
wrapper.setData({ selectedTint: { "Rear-Stationary": 'Green Tint' } }); await wrapper.setData({ selectedTint: 'Green Tint', selectedPartNumber: "test" });
expect(wrapper.vm.selectedTint).toStrictEqual({ "Rear-Stationary": 'Green Tint' }); expect(wrapper.vm.selectedTint).toEqual('Green Tint');
expect(wrapper.vm.selectedPartNumber).toEqual('test');
await wrapper.vm.ResetTintAndPartSelections(); await wrapper.vm.ResetTintAndPartSelections();
expect(wrapper.vm.selectedTint).toStrictEqual({});
expect(wrapper.vm.selectedTint).toEqual("Green Tint");
expect(wrapper.vm.selectedPartNumber).toEqual(null);
}); });
test("default is selected if only one option", async () => {
// Arrange
store.getters.pageData.mockReset();
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}]}] });
const { wrapper } = setupMocks(featureListData);
// Act
await wrapper.vm.$nextTick();
await wrapper.setData({ selectedTint: "Green Tint" });
// take emitted value, pass down as modelValue
// yes, yes, it's not ideal
await wrapper.setProps({ modelValue: wrapper.emitted()["update:modelValue"][0][0] })
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.selectedPartNumber).toBe("DB12209GTYN");
});
test("default is not selected if more than one option", async () => {
// Arrange
store.getters.pageData.mockReset();
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: [{ partNumber: "DB12209GTYN", color: "Green Tint"}, { partNumber: "DB12209GTYNXXX", color: "Green Tint"}]}] });
const { wrapper } = setupMocks(featureListData);
// Act
await wrapper.vm.$nextTick();
await wrapper.setData({ selectedTint: "Green Tint" });
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.emitted()["update:modelValue"]).toBeFalsy();
expect(wrapper.vm.selectedPartNumber).toBeFalsy();
})
const partsForSelectedTintTestCases = [
["Rear", "Stationary", "Green Tint", [{ partNumber: "Glass1", color: "Green Tint"}, { partNumber: "Glass3", color: "Green Tint"}, { partNumber: "Glass4", color: "Green Tint"}, { partNumber: "Glass6", color: "Green Tint"} ]],
["Rear", "Stationary", "Blue Tint", [{ partNumber: "Glass2", color: "Blue Tint"}, { partNumber: "Glass5", color: "Blue Tint"}]],
["Rear", "Stationary", "Red Tint", [{ partNumber: "Glass7", color: "Red Tint"}]],
["Windshield", "Single", "Green Tint", [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}]],
["Windshield", "Single", "Blue Tint", []],
["Driver", "Quarter", "Green Tint", []]
];
test.each(partsForSelectedTintTestCases)("partsForSelectedTint returns correct parts", async (glassLocation, glassName, selectedTint, expectedResults) => {
// Arrange
store.getters.pageData.mockReset();
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [
{
glassName: "Stationary",
glassLocation: "Rear",
parts: [
{ partNumber: "Glass1", color: "Green Tint"},
{ partNumber: "Glass2", color: "Blue Tint"},
{ partNumber: "Glass3", color: "Green Tint"},
{ partNumber: "Glass4", color: "Green Tint"},
{ partNumber: "Glass5", color: "Blue Tint"},
{ partNumber: "Glass6", color: "Green Tint"},
{ partNumber: "Glass7", color: "Red Tint"}
]
},
{
glassName: "Single",
glassLocation: "Windshield",
parts: [{ partNumber: "Windshield1", color: "Green Tint"}, { partNumber: "Windshield2", color: "Green Tint"}]
}
]});
const { wrapper } = setupMocks({
glassLocationProp: glassLocation,
glassNameProp: glassName,
colorAnswersProp: [],
});
// Act
await wrapper.setData({ selectedTint: selectedTint });
// Assert
expect(expectedResults).toEqual(wrapper.vm.partsForSelectedTint);
});
}); });
function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) {
function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelValueProp }) {
//Mock store //Mock store
store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: {}}); store.getters.pageData.mockReturnValueOnce({ partsOrQuestions: [{glassName: "Stationary", glassLocation: "Rear", parts: []}] });
store.getters.lineItems = { glassParts: {} } store.getters.lineItems = { glassParts: {} }
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({

View file

@ -1,245 +1,230 @@
<template> <template>
<div class="container">
<div class="row"> <div class="row">
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p> <p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
</div> </div>
</div> <div class="nested-radio">
<div class="row my-2">
<div <div class="col">
class="container nested-radio" <buttonQuestion
v-for="(value, name, index) in featureListData" v-model="selectedTint"
:key="index" :answers="tintSelectionOptions"
> buttonType="listCard"
<div class="row my-2"> :isWide="true"
<div class="col"> altText=""
<listCard isRequired
v-model="selectedTint[name]" :groupName="`${glassLocation}-${glassName}`"
:value="`${glassLocation}-${glassName}-${name}`" @isCheckedChanged="ResetTintAndPartSelections"
:isRadio="true" :validationRules="tintValidationRules"
:isWide="true" >
:buttonImage=" <div class="row my-2" aria-live="polite">
require(`@/assets/img/tints/${getTintSourceImage( <div class="col">
glassLocation, <buttonQuestion
name v-model="selectedPartNumber"
)}`) buttonType="radio"
" class="radioQuestion"
:buttonLabel="name" :questionText="glassFeatureQuestion"
altText="" :answers="featureListData[selectedTint]"
isRequired textPosition="text-start"
:buttonID="`${glassLocation}-${glassName}-${name}`" :loaderEnabled="false"
:groupName="`${glassLocation}-${glassName}`" isRequired
@isCheckedChanged="ResetTintAndPartSelections()" :groupName="`${glassLocation}-${glassName}-${selectedTint}`"
:validationRules="validationRules" :validationRules="partValidationRules"
/> />
<div class="row form-test-error mt-1"> </div>
<error-message :name="`${glassLocation}-${glassName}`" v-if="!suppressError"></error-message> </div>
</buttonQuestion>
</div>
</div> </div>
</div>
</div> </div>
<transition name="fade" mode="out-in">
<div
v-if="
selectedTint[name] != undefined &&
selectedTint[name].buttonId ===
`${glassLocation}-${glassName}-${name}`
"
class="row my-2"
aria-live="polite"
>
<div class="col">
<buttonQuestion
v-model="selectedPart[glassLocation]"
buttonType="radio"
class="radioQuestion"
:questionText="glassFeatureQuestion"
:answers="value"
textPosition="text-start"
:loaderEnabled="false"
isRequired
:groupName="`${glassLocation}-${glassName}-${name}`"
:validationRules="validationRules"
/>
</div>
</div>
</transition>
</div>
</template> </template>
<script> <script>
// Components // Components
import listCard from "@/ux-components/list-card/list-card";
import buttonQuestion from "@/common-components/button-question/button-question"; import buttonQuestion from "@/common-components/button-question/button-question";
// Supporting files // Supporting files
import { getTintImage } from "@/constants/tint-mapper"; import { getTintImage } from "@/constants/tint-mapper";
import { getCustomTransformValue } from "@/constants/dynamictext-mapper"; import { getCustomTransformValue } from "@/constants/dynamictext-mapper";
import { ErrorMessage } from 'vee-validate'; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
export default { export default {
name: "glass-part-question", name: "glass-part-question",
inheritAttrs: false, inheritAttrs: false,
data() { data() {
return { return {
glassColorQuestion: "", glassColorQuestion: "",
glassFeatureQuestion: "", glassFeatureQuestion: "",
selectedTint: {}, // v-model for the list card selections selectedTint: "",
}; };
},
props: {
glassName: String,
glassLocation: String,
colorAnswers: Array,
modelValue: Object,
validationRules: String,
},
mounted() {
this.LoadPreselectedValues();
},
components: {
listCard,
buttonQuestion,
ErrorMessage,
},
computed: {
colorQuestionText() {
return getCustomTransformValue(
this.glassColorQuestion,
`${this.glassLocation} ${this.glassName}`
);
}, },
props: {
selectedPart: { glassName: String,
get: function () { glassLocation: String,
return this.modelValue; colorAnswers: Array,
}, modelValue: Object,
set: function (newValue) { alreadyPopulatedPartsData: Array
this.$emit("update:modelValue", newValue);
},
}, },
mounted() {
// Creates a map of the feature list data in the correct Name/Value this.LoadPreselectedValues();
// format for the button-question component.
featureListData() {
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
// Create the key
const itemColor = item.ColorAnswerText;
arr[itemColor] = arr[itemColor] || [];
// Map the data to Name/Text object for ButtonQuestion
const mappedItem = item.FeatureAnswers.reduce((featureArr, item) => {
featureArr["Text"] = item.FeatureAnswerText; // Display to User
featureArr["Name"] = item.PartNumber; // Backing Value
return featureArr;
}, {});
// Add onto the final object
arr[itemColor].push(mappedItem);
return arr;
}, {});
return tintMapByColor;
}, },
components: {
PartDataFromApi() { buttonQuestion,
return this.$store.getters.pageData(this.$route.query.fmgPage);
},
},
methods: {
// Initialize the component data
initializeComponent(cmsContent) {
this.glassColorQuestion = cmsContent.ColorQuestionWidget.QuestionText;
this.glassFeatureQuestion = cmsContent.FeatureQuestionWidget.QuestionText;
}, },
computed: {
tintValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
return validationRuleName;
},
partValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
return validationRuleName;
},
colorQuestionText() {
return getCustomTransformValue(
this.glassColorQuestion,
`${this.glassLocation} ${this.glassName}`
);
},
// Gets tint images based on the glass type, and tint name. tintSelectionOptions() {
// Returns an empty string if the src or object is undefined. let tintOptions = [];
getTintSourceImage(glassLocation, tintColor) { Object.keys(this.featureListData).forEach((tintOption) => {
const tintSourceObject = getTintImage(glassLocation, tintColor); tintOptions.push({
Name: tintOption,
if (tintSourceObject === undefined || tintSourceObject.src == undefined) { Text: tintOption,
return ""; AnswerImageUrl: require(`@/assets/img/tints/${this.getTintSourceImage(
} this.glassLocation,
tintOption
return tintSourceObject.src; )}`),
}, });
// Reset selections when tint changes for the same glass to ensure proper selection.
// Also checks if only a single part is present for the tint.
ResetTintAndPartSelections() {
this.selectedTint = {};
this.selectedPart = {};
this.AutoSelectIfSinglePart();
},
// Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() {
// Check if the selected tint only has a single feature
Object.keys(this.PartDataFromApi.partsOrQuestions ?? {}).forEach(
(key) => {
const currentGlassSelection =
this.PartDataFromApi.partsOrQuestions[key];
if (
currentGlassSelection.glassName === this.glassName &&
currentGlassSelection.glassLocation === this.glassLocation
) {
if (currentGlassSelection.parts.length === 1) {
this.selectedPart = {
[currentGlassSelection.glassLocation]: [
currentGlassSelection.parts[0].partNumber,
],
};
}
}
}
);
},
// Loads the preselected values from the store.
LoadPreselectedValues() {
this.$nextTick(() => {
if (this.modelValue !== undefined) {
// Populate button-question model-value if parts data already exists in VueX
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts;
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber = alreadyPopulatedPartsData[key].partNumber;
const tintColor = alreadyPopulatedPartsData[key].color;
Object.keys(this.modelValue).forEach((key) => {
if (this.modelValue[key][0] === partNumber) {
this.selectedTint[tintColor] = {
buttonId: `${this.glassLocation}-${this.glassName}-${tintColor}`,
checkValue: "",
value: `${this.glassLocation}-${this.glassName}-${tintColor}`,
};
}
}); });
});
} return tintOptions;
}); },
selectedPartNumber: {
get() {
return this.modelValue?.partNumber;
},
set(newValue) {
this.$emit("update:modelValue", this.partsForSelectedTint.filter(part => part.partNumber == newValue)[0]);
},
},
partsForSelectedTint() {
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter(dataForGlassLocationAndName =>
dataForGlassLocationAndName.glassName == this.glassName &&
dataForGlassLocationAndName.glassLocation == this.glassLocation);
const matchingGlassParts = matchingGlass?.length == 1 ? matchingGlass[0].parts : [];
return matchingGlassParts.filter(part => part.color == this.selectedTint) ?? [];
},
// Creates a map of the feature list data in the correct Name/Value
// format for the button-question component.
featureListData() {
const tintMapByColor = this.colorAnswers.reduce((arr, item) => {
// Create the key
const itemColor = item.ColorAnswerText;
arr[itemColor] = arr[itemColor] || [];
// Map the data to Name/Text object for ButtonQuestion
const mappedItem = item.FeatureAnswers.reduce(
(featureArr, item) => {
featureArr["Text"] = item.FeatureAnswerText; // Display to User
featureArr["Name"] = item.PartNumber; // Backing Value
return featureArr;
},
{}
);
// Add onto the final object
arr[itemColor].push(mappedItem);
return arr;
}, {});
return tintMapByColor;
},
PartDataFromApi() {
return this.$store.getters.pageData(this.$route.query.fmgPage) ?? {};
},
}, },
}, methods: {
// Initialize the component data
initializeComponent(cmsContent) {
this.glassColorQuestion =
cmsContent.ColorQuestionWidget.QuestionText;
this.glassFeatureQuestion =
cmsContent.FeatureQuestionWidget.QuestionText;
},
// Gets tint images based on the glass type, and tint name.
// Returns an empty string if the src or object is undefined.
getTintSourceImage(glassLocation, tintColor) {
const tintSourceObject = getTintImage(glassLocation, tintColor);
if (
tintSourceObject === undefined ||
tintSourceObject.src == undefined
) {
return "";
}
return tintSourceObject.src;
},
// Reset selections when tint changes for the same glass to ensure proper selection.
// Also checks if only a single part is present for the tint.
ResetTintAndPartSelections() {
this.selectedPartNumber = null;
this.AutoSelectIfSinglePart();
},
// Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() {
if (this.partsForSelectedTint?.length == 1) {
this.selectedPartNumber = this.partsForSelectedTint[0].partNumber;
}
},
// Loads the preselected values from the store.
LoadPreselectedValues() {
this.$nextTick(() => {
if (this.modelValue !== undefined) {
// Populate button-question model-value if parts data already exists in VueX
this.selectedTint = this.alreadyPopulatedPartsData?.filter(part => part.partNumber === this.selectedPartNumber)[0].color;
}
});
},
},
watch: {
selectedTint() {
this.AutoSelectIfSinglePart();
}
}
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.nested-radio { .nested-radio {
.ui-radio { .ui-radio {
flex-direction: column; flex-direction: column;
margin: 0.25rem 0; margin: 0.25rem 0;
} }
p { p {
font-size: 0.875rem; font-size: 0.875rem;
} }
} }
.color-question-text { .color-question-text {
color: $black; color: $black;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
} }
</style> </style>

View file

@ -65,13 +65,13 @@ describe("vehicle-parts.vue", () => {
test("Set cms content called on load", async (done) => { test("Set cms content called on load", async (done) => {
//Arrange //Arrange
store.getters.pageData.mockReturnValue(basePartResponse); store.getters.pageData.mockReturnValue(basePartResponse);
store.getters.lineItems = { glassParts: {} } store.getters.lineItems = { glassParts: null }
const { wrapper, apiPromise } = setupMocks( const { wrapper, apiPromise } = setupMocks(
{ {
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigateAfterSave: jest.fn() navigate: jest.fn()
}, },
route: { route: {
query: { query: {
@ -104,12 +104,12 @@ describe("vehicle-parts.vue", () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValue(basePartResponse); store.getters.pageData.mockReturnValue(basePartResponse);
store.getters.lineItems = { glassParts: {} } store.getters.lineItems = { glassParts: null }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigateAfterSave: jest.fn() navigate: jest.fn()
}, },
route: { route: {
query: { query: {
@ -141,12 +141,12 @@ describe("vehicle-parts.vue", () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse); store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: { 0: { partNumber: 'DB12209YPYNOEM'} } } store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM' }] }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigateAfterSave: jest.fn() navigate: jest.fn()
}, },
route: { route: {
query: { query: {
@ -177,12 +177,12 @@ describe("vehicle-parts.vue", () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse); store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: {} } store.getters.lineItems = { glassParts: null }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigateAfterSave: jest.fn(), navigate: jest.fn(),
navigate: jest.fn() navigate: jest.fn()
}, },
route: { route: {
@ -216,14 +216,12 @@ describe("vehicle-parts.vue", () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse); store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: {} } store.getters.lineItems = { glassParts: {} }
store.commit = jest.fn(); store.commit = jest.fn();
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
navigateAfterSave: jest.fn(), navigate: jest.fn(),
navigate: jest.fn() navigate: jest.fn()
}, },
route: { route: {
@ -238,7 +236,7 @@ describe("vehicle-parts.vue", () => {
} }
}); });
wrapper.setData({ glassParts: { "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } } }); wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'DB12209GTYN' } } });
//Act //Act
vehicleParts.beforeRouteEnter.call( vehicleParts.beforeRouteEnter.call(
@ -251,10 +249,7 @@ describe("vehicle-parts.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
// expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
// expect(store.commit).toBeCalledWith(storeMutations.UPDATE_IS_REPAIR, false);
// expect(store.commit).toBeCalledWith(storeMutations.UPDATE_GLASS_TO_REPLACE, expectedGlassToReplace);
}); });
}); });
@ -285,13 +280,14 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const mountOptions = getMountOptions(mountOptionsMockData,); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleParts, mountOptions); const wrapper = shallowMount(vehicleParts, mountOptions);
const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", }); const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion", });
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent; partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -1,240 +1,265 @@
<template> <template>
<Form <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
@submit="onSubmit" <div class="page-container-grouped-styles vehicle-parts">
@invalid-submit="onInvalidSubmit" <funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
ref="theForm" <vehicleBanner
v-slot="{ meta }" ref="vehicleBanner"
> cmsWidgetName="VehicleBannerWidget"
<div class="page-container-grouped-styles vehicle-parts"> :displayGenericVehicleImage="false"
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" /> />
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" /> <funnelSubHeader
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" /> ref="funnelSubHeader"
<div class="fade-on-route-transition sub-container make-tall"> cmsWidgetName="FunnelSubHeaderWidget"
<div class="container-fluid prevent-squish my-5"> />
<div class="row"> <div class="fade-on-route-transition sub-container make-tall">
<div class="col"> <div class="prevent-squish my-5">
<alert <div class="row">
class="rounded border-0 shadow-sm" <div class="col">
alertClass="alert-warning" <alert
cmsWidgetName="AlertWidget" class="rounded border-0 shadow-sm"
:isDismissible="false" alertClass="alert-warning"
/> cmsWidgetName="AlertWidget"
:isDismissible="false"
/>
</div>
</div> </div>
</div> </div>
</div> <div v-for="(item, i) in PartsOrQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div v-for="(item, i) in PartsForQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div class="container-fluid">
<hr v-if="i > 0" /> <hr v-if="i > 0" />
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
:alreadyPopulatedPartsData="alreadyPopulatedPartsData"
/>
</div> </div>
<funnelFooter
<glassPartQuestion cmsWidgetName="FunnelFooterWidget"
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`" ref="funnelFooter"
v-model="glassParts[item.glassLocation + '-' + item.glassName]" :isForwardActionDisabled="isForwardActionDisabled"
:glassLocation="item.glassLocation" @back-clicked="backButtonAction"
:glassName="item.glassName" @ForwardClicked="forwardButtonAction"
:colorAnswers="item.colorAnswers"
validationRules="replace-options-required"
/> />
</div> </div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div> </div>
</div> </Form>
</Form>
</template> </template>
<script> <script>
// Components // Components
import glassPartQuestion from "@/layouts/vehicle-parts/glass-part-question/glass-part-question"; import glassPartQuestion from "@/layouts/vehicle-parts/glass-part-question/glass-part-question";
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
// Supporting Files // Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeMutations } from "@/constants/store-mutations"; import store from "@/store";
import store from "@/store"; import { Form } from "vee-validate";
import { Form, defineRule } from "vee-validate"; import { storeActions } from "@/constants/store-actions";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES export default {
defineRule("replace-options-required", required(errorMessages.OPTION_REQUIRED)); name: "vehicle-parts",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
export default { // Glass Part Question dynamic component
name: "vehicle-parts", Object.keys(vm.$refs)
async beforeRouteEnter(to, from, next) { .filter(
// Call APIs (r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); )
// Settle promises and get results .forEach((c) =>
const promiseResultMap = [{ vm.$refs[c][0].initializeComponent({
resultKey: "cmsContent", ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
promise: cmsContentPromise, FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
}, ]; })
const resultMap = await settleAllPromises(promiseResultMap); );
// Call the "next" function to complete the transition to this page. });
next((vm) => { },
vm.setCmsContent(resultMap.cmsContent); data() {
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
})
);
});
},
data() {
return {
glassParts: {},
alertWidgetData: Object,
};
},
components: {
Form,
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
},
computed: {
PartsForQuestions() {
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure
const mappedData = partsData.partsOrQuestions.map((g) => {
return { return {
glassName: g.glassName, glassParts: {},
glassLocation: g.glassLocation, alertWidgetData: Object,
colorAnswers: g.parts.reduce((arr, p) => { alreadyPopulatedPartsData: {},
arr.push({
ColorAnswerText: p.color,
FeatureAnswers: [
{
FeatureAnswerText:
p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
}; };
}); },
components: {
Form,
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
},
computed: {
isForwardActionDisabled() {
return (
Object.keys(this.matchedParts).length !==
this.PartsFromApi.partsOrQuestions.length
);
},
matchedParts() {
const matchedParts = [];
return mappedData; // Match them to the parts from the API.
}, this.PartsFromApi.partsOrQuestions.forEach((part) => {
const selectedPartForGlassLocationAndName =
this.glassParts[`${part.glassLocation}-${part.glassName}`];
const selectedPartData = part.parts.filter(
(part) =>
selectedPartForGlassLocationAndName &&
part.partNumber == selectedPartForGlassLocationAndName?.partNumber
)[0];
PartsFromApi() { if (selectedPartData) matchedParts.push(selectedPartData);
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS); });
},
RefPrefix() { return matchedParts;
return "partQuestion"; },
}, PartsOrQuestions() {
}, const partsData = this.PartsFromApi;
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; // Map API result data, to vehicle-parts data structure
}, const mappedData = partsData.partsOrQuestions.map((g) => {
backButtonAction() { return {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); glassName: g.glassName,
}, glassLocation: g.glassLocation,
forwardButtonAction() { colorAnswers: g.parts.reduce((arr, p) => {
const selectedGlassPartNumbers = []; arr.push({
const matchedParts = []; ColorAnswerText: p.color,
FeatureAnswers: [
{
FeatureAnswerText:
p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
};
});
// Compile all selected parts from the page. return mappedData;
for (let [key, value] of Object.entries(this.glassParts)) { },
for (let [glassKey, glassValue] of Object.entries(value)) {
selectedGlassPartNumbers.push(glassValue[0]);
}
}
// Match them to the parts from the API. PartsFromApi() {
for (let [key, value] of Object.entries( return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
this.PartsFromApi.partsOrQuestions },
)) {
for (let [partKey, partValue] of Object.entries(value.parts)) { RefPrefix() {
const currentPart = return "partQuestion";
this.PartsFromApi.partsOrQuestions[key].parts[partKey]; },
const isMatched = selectedGlassPartNumbers.some( },
(p) => p === currentPart.partNumber 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;
},
backButtonAction() {
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
async forwardButtonAction() {
const selectedGlassPartNumbers = [];
const matchedParts = [];
// Compile all selected parts from the page.
for (let [key, value] of Object.entries(this.glassParts)) {
for (let [glassKey, glassValue] of Object.entries(value)) {
selectedGlassPartNumbers.push(glassValue[0]);
}
}
// Match them to the parts from the API.
for (let [key, value] of Object.entries(
this.PartsFromApi.partsOrQuestions
)) {
for (let [partKey, partValue] of Object.entries(value.parts)) {
const currentPart =
this.PartsFromApi.partsOrQuestions[key].parts[partKey];
const isMatched = selectedGlassPartNumbers.some(
(p) => p === currentPart.partNumber
);
if (isMatched) {
matchedParts.push(currentPart);
}
}
}
// If no parts could be matched, throw an error.
if (this.isForwardActionDisabled) {
this.$refs.funnelFooter.removeLoader();
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store.
await this.dispatchStoreAction(
storeActions.SAVE_GLASS_PARTS,
matchedParts
); );
if (isMatched) { // Navigate to the next page.
matchedParts.push(currentPart); this.$router.navigate(
} this.navigationScenarios.SELECTED_PARTS,
} this.$route
} );
},
// If no parts could be matched, throw an error. LoadInitialPartsData() {
if (matchedParts.length === 0) { const partsData = this.PartsFromApi;
throw new Error("Could not match any parts to the selected parts"); const alreadyPopulatedPartsData =
} this.$store.getters.lineItems.glassParts === null
? {}
: this.$store.getters.lineItems.glassParts;
// Save parts to the store. partsData.partsOrQuestions.map((g) => {
store.commit(storeMutations.UPDATE_GLASS_PARTS, matchedParts); // If the part is already populated, use the value from the store and populate the v-model.
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
// Navigate to the next page. const partNumber = alreadyPopulatedPartsData[key].partNumber;
this.$router.navigateAfterSave( g.parts.forEach((p) => {
this.navigationScenarios.SELECTED_PARTS, if (p.partNumber === partNumber) {
this.$route this.glassParts[g.glassLocation + "-" + g.glassName] = {
); [g.glassLocation]: [partNumber],
}, };
}
resetDependentState() { });
// Nothing additional to reset here: The page save is already fully resetting all the line-items on the order });
},
LoadInitialPartsData() {
const partsData = this.PartsFromApi;
const alreadyPopulatedPartsData =
this.$store.getters.lineItems.glassParts === null
? {}
: this.$store.getters.lineItems.glassParts;
partsData.partsOrQuestions.map((g) => {
// If the part is already populated, use the value from the store and populate the v-model.
Object.keys(alreadyPopulatedPartsData).forEach((key) => {
const partNumber = alreadyPopulatedPartsData[key].partNumber;
g.parts.forEach((p) => {
if (p.partNumber === partNumber) {
this.glassParts[g.glassLocation + "-" + g.glassName] = {
[g.glassLocation]: [partNumber],
};
}
}); });
}); },
}); },
}, mounted() {
}, this.LoadInitialPartsData();
mounted() { },
this.LoadInitialPartsData(); };
},
};
</script> </script>

View file

@ -7,8 +7,8 @@
:answers="styles" :answers="styles"
groupName="ChooseVehicleStyle" groupName="ChooseVehicleStyle"
textPosition="text-start" textPosition="text-start"
v-model="selectedValueAsArray" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>
@ -34,14 +34,12 @@ export default {
questionText(){ questionText(){
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
}, },
selectedValueAsArray: { selectedValue: {
get: function() { get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return this.modelValue
return modelValueAsArray;
}, },
set: function(newValue) { set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; this.$emit("update:modelValue", newValue);
this.$emit("update:modelValue", newValueAsScalar);
} }
} }
}, },

View file

@ -23,12 +23,12 @@ import styleQuestion from "@/layouts/vehicle-style/style-question/style-question
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
export default { export default {
name: "vehicle-style", name: "vehicle-style",
@ -93,22 +93,13 @@ export default {
} }
return false; return false;
}, },
resetDependentState() {
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
store.commit(storeMutations.UPDATE_IS_REPAIR, null);
store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null);
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
},
}, },
watch: { watch: {
selectedStyle(style) { selectedStyle(style) {
this.$store.commit(this.storeMutations.UPDATE_STYLE, style); this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
this.setVehicle().then(() => { this.setVehicle().then(() => {
this.$router.navigateAfterSave( this.$router.navigate(
this.navigationScenarios.SELECTED_STYLE, this.navigationScenarios.SELECTED_STYLE,
this.$route this.$route
); );

View file

@ -74,34 +74,6 @@ describe("vehicle-year.vue", () => {
}); });
}); });
describe("vehicle-year.vue", () => {
test("Year set, call invalidation, make, model, style, carId, category should be null", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.resetDependentState();
//Assert
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MAKE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null)
expect(store.commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES)
expect(store.dispatch).toBeCalledWith(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES)
});
});
function setupMocks({ function setupMocks({
vehicleYearQuestionCmsContent = {}, vehicleYearQuestionCmsContent = {},
yearQuestionInitialData = {}, yearQuestionInitialData = {},

View file

@ -28,13 +28,11 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { experimentUniverses } from "@/constants/experiments"; import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper"; import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import store from "@/store";
export default { export default {
name: "vehicle-year", name: "vehicle-year",
@ -89,8 +87,8 @@ export default {
watch: { watch: {
selectedYear(year) { selectedYear(year) {
const parsedYear = parseInt(year); const parsedYear = parseInt(year);
this.$store.commit(this.storeMutations.UPDATE_YEAR, parsedYear); this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
this.$router.navigateAfterSave( this.$router.navigate(
this.navigationScenarios.SELECTED_YEAR, this.navigationScenarios.SELECTED_YEAR,
this.$route this.$route
); );
@ -100,22 +98,6 @@ export default {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return true; return true;
}, },
resetDependentState() {
// Set
store.commit(storeMutations.UPDATE_MAKE, null);
store.commit(storeMutations.UPDATE_MODEL, null);
store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
},
}, },
components: { components: {
yearQuestion, yearQuestion,

View file

@ -7,8 +7,8 @@
:answers="years" :answers="years"
groupName="ChooseVehicleYear" groupName="ChooseVehicleYear"
textPosition="text-start" textPosition="text-start"
v-model="selectedValueAsArray" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>
@ -35,14 +35,12 @@ export default {
questionText(){ questionText(){
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
}, },
selectedValueAsArray: { selectedValue: {
get: function() { get: function() {
const modelValueAsArray = this.modelValue ? [this.modelValue] : []; return this.modelValue
return modelValueAsArray;
}, },
set: function(newValue) { set: function(newValue) {
const newValueAsScalar = newValue && newValue.length > 0 ? newValue[0] : null; this.$emit("update:modelValue", newValue);
this.$emit("update:modelValue", newValueAsScalar);
} }
} }
}, },

View file

@ -2,6 +2,7 @@ import { shallowMount } from "@vue/test-utils";
import vinLookup from "./vin-lookup.vue"; import vinLookup from "./vin-lookup.vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import store from "@/store"; import store from "@/store";
@ -11,7 +12,7 @@ jest.mock("@/store", () => ({
getters: { getters: {
vehicle: { vehicle: {
year: 2019, year: 2019,
carId: 'initial carId' carId: 'C00000'
}, },
order: { order: {
serviceLocation: { serviceLocation: {
@ -32,7 +33,11 @@ jest.mock("@/store", () => ({
}, },
})); }));
import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "@/helpers/damage-helper"; // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
jest.mock("@/helpers/damage-helper", () => ({ jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn(() => { isGlassAvailableForCarId: jest.fn(() => {
@ -60,6 +65,7 @@ describe("vin-lookup.vue", () => {
it("Should call navigateForward() if the store carId matches the vin response carId and forward button is clicked", async () => { it("Should call navigateForward() if the store carId matches the vin response carId and forward button is clicked", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
mockOutPromises();
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Act // Act
@ -69,47 +75,14 @@ describe("vin-lookup.vue", () => {
expect(wrapper.vm.navigateForward).toHaveBeenCalled(); 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 () => { it("Should not call navigateForward() if the store carId does not match the vin response carId and forward button is clicked", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
// New lookup mockOutPromises('C11111');
wrapper.vm.vinTouched = true;
wrapper.vm.vinTouched = true;
wrapper.vm.vin = ""; wrapper.vm.vin = "";
wrapper.vm.initialVin = "foo"; wrapper.vm.initialVin = "foo";
const vehicleLookupApiResponse = {
data: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Act // Act
@ -122,17 +95,11 @@ describe("vin-lookup.vue", () => {
it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => { it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
const vehicleLookupApiResponse = { mockOutPromises('C11111');
data: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise); wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = 'C11111';
wrapper.vm.previouslyEnteredCarId = 'new carId';
// Act // Act
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
@ -171,18 +138,6 @@ describe("vin-lookup.vue", () => {
wrapper.vm.vinTouched = true; wrapper.vm.vinTouched = true;
wrapper.vm.vin = "foo"; wrapper.vm.vin = "foo";
wrapper.vm.initialVin = "!foo"; wrapper.vm.initialVin = "!foo";
const vehicleLookupApiResponse = {
status: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.reject(vehicleLookupApiResponse);
const response = {
status: 404
};
wrapper.vm.lookupVehicle = jest.fn().mockImplementation((response) => vinPromise);
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = 'new carId'; wrapper.vm.previouslyEnteredCarId = 'new carId';
@ -200,7 +155,7 @@ describe("vin-lookup.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
customMountOptions: { customMountOptions: {
router: { router: {
navigateAfterSave: jest.fn() navigate: jest.fn()
} }
} }
}); });
@ -214,8 +169,8 @@ describe("vin-lookup.vue", () => {
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
//Assert //Assert
expect(wrapper.vm.$router.navigateAfterSave).toBeCalledTimes(1); expect(wrapper.vm.$router.navigate).toBeCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, wrapper.vm.$route, expect.anything(), expect.anything(), expect.anything()); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, wrapper.vm.$route, expect.anything(), expect.anything());
}) })
test("carId matches => navigateForwardWithSingleCarMatch", async () => { test("carId matches => navigateForwardWithSingleCarMatch", async () => {
@ -267,23 +222,17 @@ function setupMocks({ customMountOptions }) {
return { wrapper }; return { wrapper };
} }
function mockOutPromises(wrapper) { function mockOutPromises(carId = 'C00000') {
const zipValidationApiResponse = { const apiResponses = {
data: { validateZipResponse: {
isServiceable: true isServiceable: true
} },
}; vehicleLookupResponse: {
const vehicleLookupApiResponse = { carId: carId
data: {
carId: 'initial carId'
} }
}; };
const zipPromise = Promise.resolve(zipValidationApiResponse); settleAllPromises.mockImplementation(() => apiResponses);
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => zipPromise);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
} }
function mockOutStubFunctions(wrapper) { function mockOutStubFunctions(wrapper) {

View file

@ -59,13 +59,6 @@
/> />
</div> </div>
</div> </div>
<alert
class="my-4"
v-model="customAlertData"
v-if="noMatchAlert"
alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget"
/>
<alert <alert
class="my-4" class="my-4"
:manualHeadline="PerfectMatchInsuranceVerifiedAlertHeader" :manualHeadline="PerfectMatchInsuranceVerifiedAlertHeader"
@ -131,14 +124,15 @@ import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files // Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "@/helpers/damage-helper"; import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules"; import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routerParams } from "@/router/router-constants/router-params";
import store from "@/store";
import vinPagesMixin from "@/mixins/vin-pages-mixin"; import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
@ -183,22 +177,16 @@ export default {
previouslyEnteredCarId: '', previouslyEnteredCarId: '',
invalidZip: '', invalidZip: '',
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0, vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
isInsuranceVerified: false, isSelectedGlassAvailableForVehicle: true
}; };
}, },
mounted() { mounted() {
this.attachCustomEvents(); this.attachCustomEvents();
if (this.vinPopulatedOnPageLoad) {
this.setupVinMask();
this.isInsuranceVerified = store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
}
}, },
watch: { watch: {
vin() { vin() {
this.vinNotFound = false; this.vinNotFound = false;
this.$refs.funnelFooter.updateButtonText( this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
);
}, },
zip() { zip() {
this.noServiceZip = false; this.noServiceZip = false;
@ -239,30 +227,31 @@ export default {
return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}", return this.getCmsContent("PerfectMatchInsuranceVerifiedAlert", "BodyText").replaceAll("{custom:damage}",
getIsWindshieldOnly()) getIsWindshieldOnly())
}, },
isInsuranceVerified() {
return store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
},
vinMask() {
if (this.vinPopulatedOnPageLoad) {
const lastSixChars = this.vin.substring(11, this.vin.length);
return `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`;
}
else {
return 'XXXXXXXXXXXXXXXXX';
}
},
}, },
methods: { methods: {
setupVinMask() {
const lastSixChars = this.vin.substring(11, this.vin.length);
this.vinMask = `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`;
},
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return store.getters.vehicle.carId !== null; return store.getters.vehicle.carId !== null;
}, },
resetDependentState() {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, null);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, null);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, null);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, null);
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
getEmailFromStore(){ getEmailFromStore(){
return store.getters.order.customer.emailAddress; return this.$store.getters.order.customer.emailAddress;
}, },
getVinFromStore(){ getVinFromStore(){
return store.getters.vehicle.vin; return this.$store.getters.vehicle.vin;
}, },
getZipFromStore(){ getZipFromStore(){
return store.getters.order.serviceLocation.zipCode; return this.$store.getters.order.serviceLocation.zipCode;
}, },
attachCustomEvents() { attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.prependActionToMethod(this, this.forwardButtonAction, () => {
@ -280,7 +269,7 @@ export default {
} }
}, },
backButtonAction() { backButtonAction() {
if (store.getters.vehicle.vin) { if (this.$store.getters.vehicle.vin) {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route);
} }
else { else {
@ -288,107 +277,103 @@ export default {
} }
}, },
async forwardButtonAction() { async forwardButtonAction() {
let zipValidationResponse;
let vehicleLookupResponse;
const zipValidation = this.validateZip(this.zip);
// If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation // If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation
if (!this.vinPopulatedOnPageLoad) { if (!this.vinPopulatedOnPageLoad) {
// Perform Zip Validation const validateZipResponse = this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.zip});
zipValidationResponse = await zipValidation; const vehicleLookupResponse = this.dispatchStoreAction(storeActions.LOOKUP_VEHICLE_BY_VIN, { vin: this.vin });
// Perform Vehicle Lookup // Settle promises and get results
const vehicleLookup = this.lookupVehicle(this.vin); const promiseResultMap = [
vehicleLookupResponse = await vehicleLookup.catch(() => { {
this.vinNotFound = true; resultKey: "validateZipResponse",
return false; promise: validateZipResponse,
}); },
{
resultKey: "vehicleLookupResponse",
promise: vehicleLookupResponse,
},
];
// Check if Service Zip entered is servicable, if not display an alert const resultMap = await settleAllPromises(promiseResultMap);
if (!zipValidationResponse.data.isServiceable) {
this.setupUiForNonServiceableZip(this.zip);
}
if (!vehicleLookupResponse || !zipValidationResponse.data.isServiceable) {
// If either lookup fails, remove the loader and stop processing the page.
this.$refs.funnelFooter.removeLoader();
return;
}
this.isCarIdDifferent = vehicleLookupResponse.data.carId !== store.getters.vehicle.carId;
if (this.isCarIdDifferent && (vehicleLookupResponse.data.carId !== this.previouslyEnteredCarId)) {
this.previouslyEnteredCarId = vehicleLookupResponse.data.carId;
this.noServiceZip = false;
this.customAlertData.vehicleInfo = vehicleLookupResponse.data;
this.$refs.funnelFooter.updateButtonText(`Continue with ${vehicleLookupResponse.data.year} ${vehicleLookupResponse.data.make} ${vehicleLookupResponse.data.model}`);
this.isVinValid = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vehicleLookupResponse.data.carId);
this.$refs.funnelFooter.removeLoader();
this.isCarIdDifferent = true;
return;
}
this.updateStore(vehicleLookupResponse.data, zipValidationResponse.data);
this.navigateForward();
} else {
// If a VIN has already been found. Validate the Service Zip (in case of changes)
const zipValidationResponse = await zipValidation;
// Check if Service Zip entered is serviceable // If either lookup fails, remove the loader and stop processing the page.
if (zipValidationResponse.data.isServiceable) { if (!resultMap.vehicleLookupResponse || !resultMap.validateZipResponse.isServiceable) {
// If the Service Zip entered is serviceable then save the Zip Info and Email Address and navigate forward
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.zip); // If the vehicle result is undefined, the vin entered was invalid.
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, zipValidationResponse.data.state); if(!resultMap.vehicleLookupResponse) {
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email); this.vinNotFound = true;
this.navigateForward(); }
} else {
// If the Service Zip is NOT serviceable then show an alert // Check if Service Zip entered is serviceable, if not display an alert
this.setupUiForNonServiceableZip(this.zip); if (!resultMap.validateZipResponse.isServiceable) {
this.$refs.funnelFooter.removeLoader(); this.setupUiForNonServiceableZip(this.zip);
}
// Remove loader and stop processing the page.
return this.$refs.funnelFooter.removeLoader();
} }
// Check if the CarId is different from the lookup vs what is in state currently.
this.isCarIdDifferent = resultMap.vehicleLookupResponse.carId !== this.$store.getters.vehicle.carId;
if (this.isCarIdDifferent && (resultMap.vehicleLookupResponse.carId !== this.previouslyEnteredCarId)) {
this.previouslyEnteredCarId = resultMap.vehicleLookupResponse.carId;
this.customAlertData.vehicleInfo = resultMap.vehicleLookupResponse;
this.$refs.funnelFooter.updateButtonText(`Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}`);
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(resultMap.vehicleLookupResponse.carId);
this.noServiceZip = false;
this.isVinValid = true;
return this.$refs.funnelFooter.removeLoader();
}
// Save vin, vehicle, customer and service information
await this.dispatchStoreAction(storeActions.SAVE_VIN_LOOKUP, {
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
vehicleInfo: Object.assign(resultMap.vehicleLookupResponse, { vin: this.vin })
}, false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
await this.dispatchStoreAction(storeActions.SAVE_SERVICE_LOCATION, {
zipCode: this.zip,
state: resultMap.validateZipResponse.state,
}, false);
return await this.navigateForward();
} }
}, // If a VIN has already been found. Validate the Service Zip (in case of changes)
navigateForward(){ const zipValidationResponse = await this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {zip: this.zip});
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, this.$route, {}, { displayVehicleChangeAlert: true }, {}); // Check if Service Zip entered is serviceable
return; if (zipValidationResponse.data.isServiceable) {
} else {
this.navigateForwardWithSingleCarMatch(); // If the Service Zip entered is serviceable then save the Zip Info and Email Address and navigate forward
return; if (this.$store.getters.order.serviceLocation.zipCode != this.zip) {
await this.dispatchStoreAction(storeActions.SAVE_SERVICE_LOCATION, {
zipCode: this.zip,
state: zipValidationResponse.data.state
}, false);
} }
},
validateZip(zip) { await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false);
return this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
zip, return await this.navigateForward();
});
},
lookupVehicle(vin) {
return this.dispatchStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);
},
updateStore(carInfo, zipInfo) {
if(this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle){
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
} }
store.commit(storeMutations.UPDATE_VEHICLE_VIN, this.vin);
store.commit(storeMutations.UPDATE_YEAR, carInfo.year); // If the Service Zip is NOT serviceable then show an alert
store.commit(storeMutations.UPDATE_MAKE, carInfo.make); this.setupUiForNonServiceableZip(this.zip);
store.commit(storeMutations.UPDATE_MODEL, carInfo.model);
store.commit(storeMutations.UPDATE_STYLE, carInfo.style); return this.$refs.funnelFooter.removeLoader();
store.commit(storeMutations.UPDATE_CAR_ID, carInfo.carId); },
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, carInfo.category); async navigateForward(){
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, carInfo.imageUrl); if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, carInfo.imageVifNumber); this.$router.navigate(this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, this.$route, {}, { [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true });
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, carInfo.imageVifNumber); } else {
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.zip); await this.navigateForwardWithSingleCarMatch();
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, zipInfo.state); }
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
}, },
setupUiForNonServiceableZip(zip) { setupUiForNonServiceableZip(zip) {
this.customAlertData.zip = zip; this.customAlertData.zip = zip;

View file

@ -1,7 +1,7 @@
import store from "@/store"; import store from "@/store";
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js"; import { storeMutations } from "@/constants/store-mutations.js";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
export default { export default {
methods: { methods: {
@ -13,15 +13,15 @@ export default {
const hasGlassLocationWithMultipleParts = partsOrQuestions.some(pq => pq.parts?.length > 1); const hasGlassLocationWithMultipleParts = partsOrQuestions.some(pq => pq.parts?.length > 1);
if (hasPartsQuestions) { if (hasPartsQuestions) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, this.$route, {}, {}, result.data); this.$router.navigate(this.navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, this.$route, {}, {}, result.data);
} }
else if (hasGlassLocationWithMultipleParts) { else if (hasGlassLocationWithMultipleParts) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, this.$route, {}, {}, result.data); this.$router.navigate(this.navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, this.$route, {}, {}, result.data);
} }
else { else {
store.commit(storeMutations.UPDATE_GLASS_PARTS, result.data); store.commit(storeMutations.UPDATE_GLASS_PARTS, result.data);
this.$refs.loadingModal.showModal(); this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route); navigateToHeritageFunnel();
} }
} }
} }

View file

@ -6,10 +6,10 @@ import { storeMutations } from "@/constants/store-mutations";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import store from "@/store"; import store from "@/store";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateAfterSaveToHeritageFunnel: jest.fn() navigateToHeritageFunnel: jest.fn()
})); }));
describe("vin-pages-mixin", () => { describe("vin-pages-mixin", () => {
@ -55,8 +55,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
test("multiple glass locations have part questions => go to parts-questions", async () => { test("multiple glass locations have part questions => go to parts-questions", async () => {
@ -163,8 +163,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
test("multiple glass locations selected, one has part question => go to parts-questions", async () => { test("multiple glass locations selected, one has part question => go to parts-questions", async () => {
@ -263,8 +263,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
test("a selected glass location has part questions and multiple parts => go to parts-questions", async () => { test("a selected glass location has part questions and multiple parts => go to parts-questions", async () => {
@ -411,8 +411,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
}); });
@ -453,8 +453,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
test("multiple glass locations selected, one of them has multiple parts => go to vehicle parts", async () => { test("multiple glass locations selected, one of them has multiple parts => go to vehicle parts", async () => {
@ -559,8 +559,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
test("multiple glass locations selected, multiple have multiple parts => go to vehicle-parts", async () => { test("multiple glass locations selected, multiple have multiple parts => go to vehicle-parts", async () => {
@ -731,8 +731,8 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1); expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions }); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
}); });
}); });
@ -771,7 +771,7 @@ describe("vin-pages-mixin", () => {
await wrapper.vm.navigateForwardWithSingleCarMatch(); await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert // Assert
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalledTimes(1); expect(navigateToHeritageFunnel).toHaveBeenCalledTimes(1);
}); });
test("multiple glass locations selected, each has one part and no part questions => go to heritage funnel", async () => { test("multiple glass locations selected, each has one part and no part questions => go to heritage funnel", async () => {
@ -864,7 +864,6 @@ describe("vin-pages-mixin", () => {
partsOrQuestions: partsOrQuestions partsOrQuestions: partsOrQuestions
}); });
// wrapper.vm.navigateAfterSaveToHeritageFunnel = jest.fn();
store.commit = jest.fn(); store.commit = jest.fn();
// Act // Act
@ -874,7 +873,7 @@ describe("vin-pages-mixin", () => {
expect(store.commit).toHaveBeenCalledTimes(1); expect(store.commit).toHaveBeenCalledTimes(1);
expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_GLASS_PARTS, { partsOrQuestions }) expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_GLASS_PARTS, { partsOrQuestions })
expect(wrapper.vm.$refs.loadingModal.showModal).toHaveBeenCalledTimes(1); expect(wrapper.vm.$refs.loadingModal.showModal).toHaveBeenCalledTimes(1);
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalledTimes(1); expect(navigateToHeritageFunnel).toHaveBeenCalledTimes(1);
}); });
}); });
}); });
@ -894,7 +893,7 @@ function setupMocks({ partsOrQuestions = [] }) {
const mocks = getMountOptions({ const mocks = getMountOptions({
router: { router: {
navigateAfterSave: jest.fn() navigate: jest.fn()
}, },
}); });

View file

@ -128,12 +128,9 @@ router.afterEach((to, from) => {
}); });
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => { router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData); navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData);
} }
router.navigateAfterSave = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, true, optionalQuery, optionalParams, optionalPageData);
}
router.navigateToExternalUrl = (url, optionalQuery = {}) => { router.navigateToExternalUrl = (url, optionalQuery = {}) => {
navigateToUrl(url, optionalQuery); navigateToUrl(url, optionalQuery);
@ -142,7 +139,7 @@ router.navigateToExternalUrl = (url, optionalQuery = {}) => {
// PRIVATE FUNCTIONS // PRIVATE FUNCTIONS
// Navigate to the next route, depending on the scenario. // Navigate to the next route, depending on the scenario.
async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) { async function navigate(scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) {
if (!scenario) { if (!scenario) {
console.error("No scenario provided. Please review the routing table."); console.error("No scenario provided. Please review the routing table.");
return; return;
@ -155,13 +152,6 @@ async function navigate(scenario, currentRoute, invalidateOnSave, optionalQuery
if (destinationFmgPageValue !== undefined) { 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. // We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our fmgPage one.
// If we need to do invalidation
const currentComponent = currentRoute.matched[0].components;
if (invalidateOnSave) {
resetDependentState(currentComponent);
}
// Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided. // 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); baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData);
@ -259,9 +249,4 @@ function arePagePrerequisitesValid(component) {
return component.default.methods.arePagePrerequisitesValid(); return component.default.methods.arePagePrerequisitesValid();
} }
// Reset dependant state on route change.
function resetDependentState(component) {
return component.default.methods.resetDependentState();
}
export default router; export default router;

View file

@ -12,6 +12,7 @@ const fmgPageValues = {
REVEAL: "reveal", REVEAL: "reveal",
ESTIMATE: "estimate", ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles", ADDRESS_VEHICLES: "address-vehicles",
QUOTE: "quote"
}; };
export { fmgPageValues }; export { fmgPageValues };

View file

@ -17,6 +17,8 @@ const navigationScenarios = {
SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN", SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN",
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE", SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",
SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS", 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",
}; };
export { navigationScenarios }; export { navigationScenarios };

View file

@ -216,7 +216,15 @@ const routingTable = [
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
} },
{
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
] ]
}, },
]; ];

View file

@ -4,6 +4,7 @@ import { storeMutations } from "@/constants/store-mutations";
import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper"; import { getDateForSavedSessionTimeout } from "@/helpers/heritage-integration/session-helper";
import createPersistedState from "vuex-persistedstate"; import createPersistedState from "vuex-persistedstate";
import globalMethods from "@/global-methods"; import globalMethods from "@/global-methods";
import { storeActions } from "../constants/store-actions";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -15,7 +16,7 @@ const getDefaultState = () => {
model: null, model: null,
style: null, style: null,
carId: null, carId: null,
category: null, category: null,
vin: null, vin: null,
imageUrl: null, imageUrl: null,
imageVifNumber: null, imageVifNumber: null,
@ -43,12 +44,13 @@ const getDefaultState = () => {
isRepair: null, isRepair: null,
numberOfChips: null, numberOfChips: null,
glassToReplace: null, glassToReplace: null,
partQuestionAnswers: null,
}, },
lineItems: { lineItems: {
glassParts: null, glassParts: null,
otherParts: null otherParts: null
}, },
payment:{ payment: {
isInsurance: null, isInsurance: null,
insuranceCoverage: { insuranceCoverage: {
isVerified: null isVerified: null
@ -115,6 +117,9 @@ export const mutations = {
updateGlassToReplace(state, glassToReplace) { updateGlassToReplace(state, glassToReplace) {
state.order.damage.glassToReplace = glassToReplace; state.order.damage.glassToReplace = glassToReplace;
}, },
updatePartQuestionAnswers(state, answersArray) {
state.order.damage.partQuestionAnswers = answersArray;
},
updateGlassParts(state, partsData) { updateGlassParts(state, partsData) {
state.order.lineItems.glassParts = partsData; state.order.lineItems.glassParts = partsData;
}, },
@ -139,36 +144,65 @@ export const mutations = {
updateInsuranceVerifiedStatus(state, isVerified) { updateInsuranceVerifiedStatus(state, isVerified) {
state.order.payment.insuranceCoverage.isVerified = isVerified; state.order.payment.insuranceCoverage.isVerified = isVerified;
}, },
updateRegistrationLicensePlate(state, licensePlate){ updateRegistrationLicensePlate(state, licensePlate) {
state.order.vehicle.registration.licensePlate = licensePlate; state.order.vehicle.registration.licensePlate = licensePlate;
}, },
updateRegistrationAddress(state, registrationAddress){ updateRegistrationAddress(state, registrationAddress) {
state.order.vehicle.registration.address = registrationAddress; state.order.vehicle.registration.address = registrationAddress;
}, },
updateRegistrationCity(state, registrationCity){ updateRegistrationCity(state, registrationCity) {
state.order.vehicle.registration.city = registrationCity; state.order.vehicle.registration.city = registrationCity;
}, },
updateRegistrationState(state, registrationState){ updateRegistrationState(state, registrationState) {
state.order.vehicle.registration.state = registrationState; state.order.vehicle.registration.state = registrationState;
}, },
updateRegistrationZipCode(state, registrationZipCode){ updateRegistrationZipCode(state, registrationZipCode) {
state.order.vehicle.registration.zipCode = registrationZipCode; state.order.vehicle.registration.zipCode = registrationZipCode;
}, },
updateServiceLocationZipCode(state, serviceLocationZip){ updateServiceLocationZipCode(state, serviceLocationZip) {
state.order.serviceLocation.zipCode = serviceLocationZip; state.order.serviceLocation.zipCode = serviceLocationZip;
}, },
updateServiceLocationState(state, serviceLocationState){ updateServiceLocationState(state, serviceLocationState) {
state.order.serviceLocation.state = serviceLocationState; state.order.serviceLocation.state = serviceLocationState;
}, },
updateRegistrationFirstName(state, firstName){ updateRegistrationFirstName(state, firstName) {
state.order.vehicle.registration.firstName = firstName; state.order.vehicle.registration.firstName = firstName;
}, },
updateRegistrationLastName(state, lastName){ updateRegistrationLastName(state, lastName) {
state.order.vehicle.registration.lastName = lastName; state.order.vehicle.registration.lastName = lastName;
}, },
updateCustomerEmailAddress(state, customerEmailAddress){ updateCustomerEmailAddress(state, customerEmailAddress) {
state.order.customer.emailAddress = customerEmailAddress; state.order.customer.emailAddress = customerEmailAddress;
}, },
updateVehicle(state, vehicleInfo) {
state.order.vehicle.year = vehicleInfo.year;
state.order.vehicle.make = vehicleInfo.make;
state.order.vehicle.model = vehicleInfo.model;
state.order.vehicle.style = vehicleInfo.style;
state.order.vehicle.carId = vehicleInfo.carId;
state.order.vehicle.category = vehicleInfo.category;
state.order.vehicle.vin = vehicleInfo.vin;
state.order.vehicle.imageUrl = vehicleInfo.imageUrl;
state.order.vehicle.imageVifNumber = vehicleInfo.imageVifNumber;
state.order.vehicle.imageColor = vehicleInfo.imageVifColor;
},
updateRegistration(state, registrationInfo) {
state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate;
state.order.vehicle.registration.address = registrationInfo?.address;
state.order.vehicle.registration.city = registrationInfo?.city;
state.order.vehicle.registration.state = registrationInfo?.state;
state.order.vehicle.registration.zipCode = registrationInfo?.zipCode;
state.order.vehicle.registration.firstName = registrationInfo?.firstName;
state.order.vehicle.registration.lastName = registrationInfo?.lastName;
},
updateServiceLocation(state, serviceLocationInfo) {
state.order.serviceLocation.address = serviceLocationInfo.address;
state.order.serviceLocation.city = serviceLocationInfo.city;
state.order.serviceLocation.state = serviceLocationInfo.state;
state.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
},
// applicationUser MUTATIONS // applicationUser MUTATIONS
updateSaveOrderPromise(state, saveOrderPromise){ updateSaveOrderPromise(state, saveOrderPromise){
@ -201,7 +235,7 @@ export const mutations = {
} }
}, },
// DEPENDENCY MUTATIONS // RESET DEPENDENCY MUTATIONS
resetVehicleState(state) { resetVehicleState(state) {
state.order.vehicle.year = null; state.order.vehicle.year = null;
state.order.vehicle.make = null; state.order.vehicle.make = null;
@ -258,8 +292,8 @@ export const mutations = {
address: orderInformation.vehicle.registration.streetAddress, address: orderInformation.vehicle.registration.streetAddress,
city: orderInformation.vehicle.registration.city, city: orderInformation.vehicle.registration.city,
state: orderInformation.vehicle.registration.state, state: orderInformation.vehicle.registration.state,
zipCode: orderInformation.vehicle.registration.zipCode, zipCode: orderInformation.vehicle.registration.zipCode,
licensePlate: orderInformation.vehicle.registration.licensePlateNumber, licensePlate: orderInformation.vehicle.registration.licensePlateNumber,
} }
}); });
@ -270,22 +304,15 @@ export const mutations = {
state.order.lineItems.glassParts = orderInformation.parts; state.order.lineItems.glassParts = orderInformation.parts;
state.order.accountNumber = orderInformation.accountNumber; state.order.accountNumber = orderInformation.accountNumber;
state.order.serviceLocation.address = orderInformation.serviceLocation.streetAddress, state.order.serviceLocation.address = orderInformation.serviceLocation.streetAddress,
state.order.serviceLocation.city = orderInformation.serviceLocation.city, state.order.serviceLocation.city = orderInformation.serviceLocation.city,
state.order.serviceLocation.state = orderInformation.serviceLocation.state, state.order.serviceLocation.state = orderInformation.serviceLocation.state,
state.order.serviceLocation.zipCode = orderInformation.serviceLocation.zipCode; state.order.serviceLocation.zipCode = orderInformation.serviceLocation.zipCode;
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder; state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified; state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified;
state.order.customer.emailAddress = orderInformation.customer.emailAddress; state.order.customer.emailAddress = orderInformation.customer.emailAddress;
}, },
updateServiceLocationWithVehicleRegistration(state) {
state.order.serviceLocation.address = state.order.vehicle.registration.address;
state.order.serviceLocation.city = state.order.vehicle.registration.city;
state.order.serviceLocation.state = state.order.vehicle.registration.state;
state.order.serviceLocation.zipCode = state.order.vehicle.registration.zipCode;
},
} }
// Export Getters // Export Getters
@ -310,6 +337,7 @@ export const getters = {
// Export Actions // Export Actions
export const actions = { export const actions = {
// Vehicle API Actions // Vehicle API Actions
getVehicleYears(context) { getVehicleYears(context) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -395,7 +423,7 @@ export const actions = {
}, },
getDamageOptions(context, { carId }) { getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method, methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`, endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {}, payload: {},
}); });
@ -407,7 +435,7 @@ export const actions = {
}) })
}, },
// DEPENDENCY ACTIONS // Dependency Actions
resetVehicleAndDependencies(context) { resetVehicleAndDependencies(context) {
context.commit(storeMutations.RESET_VEHICLE_STATE); context.commit(storeMutations.RESET_VEHICLE_STATE);
context.commit(storeMutations.RESET_DAMAGE_STATE); context.commit(storeMutations.RESET_DAMAGE_STATE);
@ -451,11 +479,18 @@ export const actions = {
payload: {}, payload: {},
}); });
}, },
getEvoxImage(context, { relativeUrl }) {
// Analytics Actions
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetPageData.method, method: endpoints.LogExperimentExposureIfAssigned.method,
endpoint: relativeUrl, endpoint: endpoints.LogExperimentExposureIfAssigned.url,
payload: {}, payload: {
userId: userId,
sessionKey: sessionKey,
pageName: pageName,
universeName: universeName
}
}); });
}, },
@ -468,9 +503,6 @@ export const actions = {
context.commit(storeMutations.UPDATE_SAVE_QUOTE_ID, saveQuoteId); context.commit(storeMutations.UPDATE_SAVE_QUOTE_ID, saveQuoteId);
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId); context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
}, },
updateServiceLocationWithVehicleRegistration(context) {
context.commit(storeMutations.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
},
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) { logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -492,7 +524,7 @@ export const actions = {
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: 'SafeliteDotCom', applicationName: 'SafeliteDotCom',
action: action, action: action,
event: event, event: event,
shouldUseSessionId: shouldUseSessionId shouldUseSessionId: shouldUseSessionId
}; };
@ -504,7 +536,6 @@ export const actions = {
logApiCall: false logApiCall: false
}); });
}, },
logCustomEvent(context, { userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId }) { logCustomEvent(context, { userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId }) {
var payload = { var payload = {
userId: userId, userId: userId,
@ -512,9 +543,9 @@ export const actions = {
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: 'SafeliteDotCom', applicationName: 'SafeliteDotCom',
category: category, category: category,
action: action, action: action,
label: label, label: label,
value: value, value: value,
shouldUseSessionId: shouldUseSessionId shouldUseSessionId: shouldUseSessionId
}; };
@ -526,7 +557,6 @@ export const actions = {
logApiCall: false logApiCall: false
}); });
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) { initializeSession(context, { userId, sessionId, userAgent, referrer }) {
var payload = { var payload = {
applicationName: 'SafeliteDotCom', applicationName: 'SafeliteDotCom',
@ -547,7 +577,14 @@ export const actions = {
}); });
}, },
GetExperimentsByUser(context, { userId }){ // Misc Actions
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
},
GetExperimentsByUser(context, { userId }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetExperimentsByUser.method, method: endpoints.GetExperimentsByUser.method,
endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`, endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`,
@ -555,8 +592,15 @@ export const actions = {
}); });
}, },
getEvoxImage(context, { relativeUrl }) {
return globalMethods.callHttpClient({
method: endpoints.GetPageData.method,
endpoint: relativeUrl,
payload: {},
});
},
// Parts API Actions // PartsOrQuestions API Actions
getPartsOrQuestions(context) { getPartsOrQuestions(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
@ -579,6 +623,31 @@ export const actions = {
}); });
}, },
// Parts API Actions
getParts(context) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const resultsArray = damage.partQuestionAnswers;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
return globalMethods.callHttpClient({
method: endpoints.GetParts.method,
endpoint: endpoints.GetParts.url,
payload: {
carId: carId,
glass: glassArray,
answerResults: resultsArray,
zip: zipCode,
vin: vin
},
});
},
// Order API Actions // Order API Actions
saveOrder(context) { saveOrder(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
@ -632,8 +701,7 @@ export const actions = {
}, },
}); });
}, },
loadOrder(context, { referralNumber, referralDate, referralCorrelationId, accountNumber }) {
loadOrder(context, { referralNumber, referralDate, referralCorrelationId, accountNumber}) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LoadOrder.method, method: endpoints.LoadOrder.method,
endpoint: endpoints.LoadOrder.url, endpoint: endpoints.LoadOrder.url,
@ -648,6 +716,182 @@ export const actions = {
context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data); context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data);
return response; return response;
}); });
},
// Business domain actions
// Vehicle
saveVehicleYear(context, year) {
//Reset dependent state when changing
if (context.state.order.vehicle.year !== year) {
context.commit(storeMutations.UPDATE_MAKE, null);
context.commit(storeMutations.UPDATE_MODEL, null);
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_YEAR, year);
}
},
saveVehicleMake(context, make) {
//Reset dependent state when changing
if (context.state.order.vehicle.make !== make) {
context.commit(storeMutations.UPDATE_MODEL, null);
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_MAKE, make);
}
},
saveVehicleModel(context, model) {
//Reset dependent state when changing
if (context.state.order.vehicle.model !== model) {
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_MODEL, model);
}
},
saveVehicleStyle(context, style) {
//Reset dependent state when changing
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_STYLE, style);
}
},
saveVehicleDamage(context, { isWindshieldRepair, selectedGlassToReplace, selectedWindshieldChipCount }) {
const selectedGlassPassedInSorted = selectedGlassToReplace.slice().sort();
const isGlassToReplaceTheSame = (context.state.order.damage.glassToReplace?.length === selectedGlassToReplace.length)
&& context.state.order.damage.glassToReplace
.slice()
.sort()
.every((obj, index) => obj.glassLocation === selectedGlassPassedInSorted[index].glassLocation && obj.glassName === selectedGlassPassedInSorted[index].glassName);
if (!isGlassToReplaceTheSame) {
//Reset dependent state when changing
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
// Save new values
context.commit(storeMutations.UPDATE_IS_REPAIR, isWindshieldRepair);
context.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, isWindshieldRepair ? parseInt(selectedWindshieldChipCount) : null);
context.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, selectedGlassToReplace);
}
},
// Vin lookup
saveVinLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
//Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
//Save new values
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
}
},
saveRegistrationLicensePlateLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
//Reset dependent state when changing
if (registrationInfo?.licensePlate !== context.state.order.vehicle.registration?.licensePlate) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
//Save new values
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
}
},
saveRegistrationAddressLookup(context, { isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
//Reset dependent state when changing
if (registrationInfo?.address !== context.state.order.vehicle.registration?.address || registrationInfo?.city !== context.state.order.vehicle.registration?.city || registrationInfo?.state !== context.state.order.vehicle.registration?.state || registrationInfo?.zipCode !== context.state.order.vehicle.registration?.zipCode || registrationInfo?.firstName !== context.state.order.vehicle.registration?.firstName || registrationInfo?.lastName !== context.state.order.vehicle.registration?.lastName) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
//Save new values
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
}
},
savePartQuestionAnswers(context, partQuestionAnswersArray) {
//Save new values
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
},
// Misc order actions
saveServiceLocation(context, serviceLocationInfo) {
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
},
saveEmail(context, email) {
context.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, email);
},
saveVin(context, { isSelectedGlassAvailableForVehicle, vehicleInfo }) {
//Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
//Save new values
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
}
},
saveGlassParts(context, parts) {
context.commit(storeMutations.UPDATE_GLASS_PARTS, parts);
},
clearVin(context) {
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
} }
} }
@ -665,3 +909,4 @@ export default createStore({
}); });
// Private Functions // Private Functions

View file

@ -1,7 +1,7 @@
import globalMethods from "@/global-methods"; import globalMethods from "@/global-methods";
import { mutations, state, actions, getters } from "@/store"; import { mutations, state, actions, getters } from "@/store";
import { storeMutations } from "@/constants/store-mutations"; import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
// Mock global method // Mock global method
globalMethods.callHttpClient = jest.fn(); globalMethods.callHttpClient = jest.fn();
@ -139,13 +139,13 @@ describe("Mutations", () => {
storeState.order.damage = { storeState.order.damage = {
isRepair: true, isRepair: true,
numberOfChips: 2, numberOfChips: 2,
glassToReplace: [{location: 'Rear', name: 'Stationary'}] glassToReplace: [{ location: 'Rear', name: 'Stationary' }]
} }
// Expect // Expect
expect(storeState.order.damage.isRepair).toEqual(true); expect(storeState.order.damage.isRepair).toEqual(true);
expect(storeState.order.damage.numberOfChips).toEqual(2); expect(storeState.order.damage.numberOfChips).toEqual(2);
expect(storeState.order.damage.glassToReplace).toStrictEqual([{location: 'Rear', name: 'Stationary'}]); expect(storeState.order.damage.glassToReplace).toStrictEqual([{ location: 'Rear', name: 'Stationary' }]);
// Act // Act
mutations.resetDamageState(storeState); mutations.resetDamageState(storeState);
@ -183,10 +183,10 @@ describe("Mutations", () => {
const storeState = state; const storeState = state;
// Act // Act
mutations.updateGlassParts(storeState, { 'Windshield-Single': 'PARTNUM101'}); mutations.updateGlassParts(storeState, { 'Windshield-Single': 'PARTNUM101' });
// Assert // Assert
expect(storeState.order.lineItems.glassParts).toEqual({ 'Windshield-Single': 'PARTNUM101'}); expect(storeState.order.lineItems.glassParts).toEqual({ 'Windshield-Single': 'PARTNUM101' });
}); });
it("Updates page data in state", () => { it("Updates page data in state", () => {
@ -201,52 +201,52 @@ describe("Mutations", () => {
}); });
it("updateStateWithOrderInformation, should set order information in state", () => { it("updateStateWithOrderInformation, should set order information in state", () => {
// Arrange // Arrange
const storeState = state; const storeState = state;
// Act // Act
mutations.updateStateWithOrderInformation(storeState, { mutations.updateStateWithOrderInformation(storeState, {
referralNumber: 123, referralNumber: 123,
referralDate: new Date().toUTCString(), referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx", referralCorrelationId: "xxx-xxx-xxx",
vehicle: { vehicle: {
year: "2019", year: "2019",
make: "Acura", make: "Acura",
model: "ILX", model: "ILX",
style: "4 DOOR SEDAN", style: "4 DOOR SEDAN",
carId: "C0000001", carId: "C0000001",
category: "CAR", category: "CAR",
registration: {} registration: {}
}, },
damage: { damage: {
glassToReplace: ["Windshield"], glassToReplace: ["Windshield"],
isRepair: false, isRepair: false,
numberOfChips: 0, numberOfChips: 0,
}, },
parts: [], parts: [],
accountNumber: "123456789", accountNumber: "123456789",
insuranceInfo: {}, insuranceInfo: {},
serviceLocation: {}, serviceLocation: {},
customer: {} customer: {}
}); });
// Assert // Assert
expect(storeState.order.referralNumber).toEqual(123); expect(storeState.order.referralNumber).toEqual(123);
expect(storeState.order.referralCorrelationId).toEqual("xxx-xxx-xxx"); expect(storeState.order.referralCorrelationId).toEqual("xxx-xxx-xxx");
expect(storeState.order.vehicle.year).toEqual("2019"); expect(storeState.order.vehicle.year).toEqual("2019");
expect(storeState.order.vehicle.make).toEqual("Acura"); expect(storeState.order.vehicle.make).toEqual("Acura");
expect(storeState.order.vehicle.model).toEqual("ILX"); expect(storeState.order.vehicle.model).toEqual("ILX");
}); });
it("updateInsuranceVerifiedStatus, should set isVerified flag", () => { it("updateInsuranceVerifiedStatus, should set isVerified flag", () => {
// Arrange // Arrange
const storeState = state; const storeState = state;
// Act // Act
mutations.updateInsuranceVerifiedStatus(storeState, true); mutations.updateInsuranceVerifiedStatus(storeState, true);
// Assert // Assert
expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true); expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true);
}); });
}); });
@ -604,11 +604,11 @@ describe("Actions", () => {
context.commit = commit; context.commit = commit;
// Act // Act
const response = await actions.loadOrder(context, {referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx"}); const response = await actions.loadOrder(context, { referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx" });
// Assert // Assert
expect(response.data).toEqual({ referralNumber: 123 }); expect(response.data).toEqual({ referralNumber: 123 });
expect(commit).toBeCalledWith(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, {"referralNumber": 123}); expect(commit).toBeCalledWith(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, { "referralNumber": 123 });
}); });
it("updateStoreWithSaveOrderResponse, should call commit six times", () => { it("updateStoreWithSaveOrderResponse, should call commit six times", () => {
@ -649,7 +649,7 @@ describe("Actions", () => {
// Act // Act
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ }); return Promise.resolve({});
}); });
// Assert // Assert
@ -670,7 +670,7 @@ describe("Actions", () => {
// Act // Act
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ }); return Promise.resolve({});
}); });
// Assert // Assert
@ -685,7 +685,7 @@ describe("Actions", () => {
// Act // Act
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
return Promise.resolve({ }); return Promise.resolve({});
}); });
// Assert // Assert
@ -693,8 +693,352 @@ describe("Actions", () => {
expect(response).toEqual({}); expect(response).toEqual({});
}); });
it("saveVin, should call mutation when CarId is different and selectedGlass is not available for vehicle", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
vin: "YYYYY"
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVin(context, { isCarIdDifferent: true, isSelectedGlassAvailableForVehicle: false, vehicleInfo: { carId: 'C010101', vin: "XXXXX" } });
// Assert
expect(dispatch).toBeCalledWith(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, { carId: 'C010101', vin: "XXXXX" });
});
it("saveEmail, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
actions.saveEmail(context, 'test@safelite.com');
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, 'test@safelite.com');
});
it("saveServiceLocation, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
actions.saveServiceLocation(context, { zipCode: "80020" });
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_SERVICE_LOCATION, { zipCode: "80020" });
});
it("saveGlassParts, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
actions.saveGlassParts(context, { glassParts: {} });
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_PARTS, { glassParts: {} });
});
it("clearVin, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
actions.clearVin(context);
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
});
it("saveVinLookup, should call mutation if vin is different", () => {
// Arrange
const context = state;
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
const payload = { isCarIdDifferent: true, isSelectedGlassAvailableForVehicle: false, vehicleInfo: { carId: 'C010101', vin: "XXXXX" }, registrationInfo: { zipCode: "80020" }, serviceLocationInfo: { state: "CO" }, customerEmail: "test@safleite.com" };
actions.saveVinLookup(context, payload);
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo);
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
});
it("saveRegistrationLicensePlateLookup, should call mutation if LP is different", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
registration: {
licensePlate: "ABC123"
}
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
const payload = { isCarIdDifferent: true, isSelectedGlassAvailableForVehicle: false, vehicleInfo: { carId: 'C010101', vin: "XXXXX" }, registrationInfo: { zipCode: "80020", licensePlate: "ALQX35" }, serviceLocationInfo: { state: "CO" }, customerEmail: "test@safelite.com" };
actions.saveRegistrationLicensePlateLookup(context, payload);
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo);
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
});
it("saveRegistrationAddressLookup, should call mutation when address is different", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
registration: {
address: "123 Main St"
}
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
const payload = { isCarIdDifferent: true, isSelectedGlassAvailableForVehicle: false, vehicleInfo: { carId: 'C010101', vin: "XXXXX" }, registrationInfo: { zipCode: "80020", address: "123 Marys Ave" }, serviceLocationInfo: { state: "CO" }, customerEmail: "test@safelite.com" };
actions.saveRegistrationAddressLookup(context, payload);
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo);
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
});
it("saveVehicleYear, should wipe out vehicle info if year changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
year: "2015"
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleYear(context, "2016");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MAKE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicleMake, should wipe out vehicle info if make changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
make: "Honda"
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleMake(context, "Toyota");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicle model, should wipe out vehicle info if model changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
model: "Civic"
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleModel(context, "Accord");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicleStyle, should wipe out vehicle info if style changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
style: "Sedan"
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleStyle(context, "SUV");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicleDamage, should wipe out damage if different", () => {
// Arrange
const context = state;
context.state = {
order: {
damage: {
glassToReplace: [{glassName: 'Single', glassLocation: 'Windshield'}]
}
}
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
const payload = { isWindshieldRepair: false, selectedGlassToReplace: [{glassName: 'Rear', glassLocation: 'quarter'}], selectedWindshieldChipCount: 0};
actions.saveVehicleDamage(context, payload);
// Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_IS_REPAIR, payload.isWindshieldRepair);
expect(commit).toBeCalledWith(storeMutations.UPDATE_NUMBER_OF_CHIPS, payload.isWindshieldRepair ? parseInt(payload.selectedWindshieldChipCount) : null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_GLASS_TO_REPLACE, payload.selectedGlassToReplace);
});
}); });
describe("Getters", () => { describe("Getters", () => {
it("Vehicle getter, should return vehicle data", () => { it("Vehicle getter, should return vehicle data", () => {
// Arrange // Arrange
@ -758,14 +1102,14 @@ describe("Getters", () => {
const storeState = state; const storeState = state;
// Act // Act
mutations.updateGlassParts(storeState, {"Rear-Stationary": 'PART101'}); mutations.updateGlassParts(storeState, { "Rear-Stationary": 'PART101' });
// Assert // Assert
expect(getters.lineItems(storeState).glassParts).toEqual({"Rear-Stationary": 'PART101'}); expect(getters.lineItems(storeState).glassParts).toEqual({ "Rear-Stationary": 'PART101' });
}); });
it("PageData getter, should return page data for specific page", () => { it("PageData getter, should return page data for specific page", () => {
// Arrange // Arrange
const storeState = state; const storeState = state;
@ -783,7 +1127,7 @@ describe("Getters", () => {
const storeState = state; const storeState = state;
//Act //Act
mutations.updateInsuranceVerifiedStatus(storeState, true ); mutations.updateInsuranceVerifiedStatus(storeState, true);
//Assert //Assert
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true); expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);

View file

@ -46,6 +46,7 @@ html {
input[type=radio]+label:before, input[type=radio]+label:before,
input[type=checkbox]+label:before { input[type=checkbox]+label:before {
border: 1px solid $red; border: 1px solid $red;
background-color: initial;
} }
input[type=checkbox]:checked + label:before { input[type=checkbox]:checked + label:before {
border: 1px solid $blue; border: 1px solid $blue;

View file

@ -119,14 +119,13 @@ export default {
if(!this.selectingInitiatesLoad) { if(!this.selectingInitiatesLoad) {
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
}, },
triggerButton() { triggerButton() {
if(this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true); this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
@ -135,6 +134,8 @@ export default {
value: this.value.toString(), value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(), buttonId: this.buttonID && this.buttonID.toString(),
}; };
this.handleChange(this.value);
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent); this.$emit("update:modelValue", emitEvent);
} }
@ -223,18 +224,26 @@ export default {
span { span {
font-size: .875rem; font-size: .875rem;
} }
} }
}
.col {
&:first-of-type { &:first-of-type {
label { .list-button-horizontal {
border-bottom-left-radius: 0.5rem; label {
border-top-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem;
z-index: 2; border-top-left-radius: 0.5rem;
z-index: 2;
}
} }
} }
&:last-of-type { &:last-of-type {
label { .list-button-horizontal {
border-bottom-right-radius: 0.5rem; label {
border-top-right-radius: 0.5rem; border-bottom-right-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
} }
} }
} }

View file

@ -6,8 +6,7 @@
@keyup.up="handleKeyupArrow()" @keyup.up="handleKeyupArrow()"
@keyup.down="handleKeyupArrow()" @keyup.down="handleKeyupArrow()"
@keyup.left="handleKeyupArrow()" @keyup.left="handleKeyupArrow()"
@keyup.right="handleKeyupArrow()" @keyup.right="handleKeyupArrow()">
>
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
@ -107,26 +106,25 @@ export default {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleInputChange() { handleInputChange() {
if(!this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.handleCheckChange(); this.triggerButton();
} }
else {
this.handleCheckChange();
}
}, },
handleKeyupArrow() { handleKeyupArrow() {
if (this.isMultiSelect) { if (this.isMultiSelect) {
return; // Prevent arrow keys from doing anything if element is a checkbox return; // Prevent arrow keys from doing anything if element is a checkbox
} }
if(!this.selectingInitiatesLoad) { this.handleInputChange();
this.handleCheckChange();
}
this.handleChange(this.value);
}, },
triggerButton() { triggerButton() {
if(this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true); this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
@ -135,6 +133,7 @@ export default {
value: this.value.toString(), value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(), buttonId: this.buttonID && this.buttonID.toString(),
}; };
this.handleChange(this.value);
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent); this.$emit("update:modelValue", emitEvent);
}, },

View file

@ -205,7 +205,7 @@ describe("list-card.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: [Boolean, String], buttonId: 'list-card-id'}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: "List Card Checkbox", buttonId: 'list-card-id'}]);
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {

View file

@ -1,8 +1,9 @@
<template> <template>
<div :class="'col' + colLength"> <div :class="{'h-100': !isWide}">
<div <div
class="list-card w-100 rounded-3 d-flex align-items-center h-100" class="list-card w-100 rounded-3 d-flex align-items-center"
:class="[ :class="[
'h-100',
isWide ? 'horizontal' : '', isWide ? 'horizontal' : '',
(errors.length > 0 || hasError) ? 'has-error' : '', (errors.length > 0 || hasError) ? 'has-error' : '',
]" ]"
@ -19,7 +20,8 @@
:value="value" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
v-model="checkValue" v-model="checkValue"
@change="handleInputChange()" :checked="checkValue"
@change="handleInputChange"
/> />
<label <label
tabindex="-1" tabindex="-1"
@ -27,7 +29,7 @@
:aria-labelledby="buttonID" :aria-labelledby="buttonID"
class="d-flex w-100 align-items-center px-2 h-100" class="d-flex w-100 align-items-center px-2 h-100"
:class="getLabelClasses" :class="getLabelClasses"
@mouseup="triggerButton()" @mouseup="triggerButton"
> >
<img <img
:id="buttonImageId" :id="buttonImageId"
@ -61,6 +63,7 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import { toRef } from "vue";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
export default { export default {
@ -93,7 +96,7 @@ export default {
}, },
data() { data() {
return { return {
checkValue: [Boolean, String], checkValue: null,
} }
}, },
created() { created() {
@ -102,6 +105,14 @@ export default {
? this.selectedValues.includes(this.value) ? this.selectedValues.includes(this.value)
: this.selectedValues[0]; : this.selectedValues[0];
} }
else if (Array.isArray(this.modelValue)) {
this.checkValue = this.isMultiSelect
? this.modelValue.includes(this.value)
: this.modelValue[0];
}
else {
this.checkValue = this.selectedValues == this.value || this.modelValue == this.value;
}
}, },
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync
if (this.clearOnUnmount) { if (this.clearOnUnmount) {
@ -136,14 +147,13 @@ export default {
if(!this.selectingInitiatesLoad) { if(!this.selectingInitiatesLoad) {
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
}, },
triggerButton() { triggerButton() {
if(this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true); this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
@ -152,6 +162,8 @@ export default {
value: this.value.toString(), value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(), buttonId: this.buttonID && this.buttonID.toString(),
}; };
this.handleChange(this.value);
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent); this.$emit("update:modelValue", emitEvent);
}, },
@ -164,6 +176,15 @@ export default {
this.checkValue = newVal.value; this.checkValue = newVal.value;
} }
}, },
selectedValues(newVal) {
if (typeof newVal === "string") {
this.handleChange(newVal);
this.checkValue = newVal == this.value;
}
else if (newVal !== undefined) {
this.checkValue = newVal.value;
}
},
}, },
setup(props) { setup(props) {
const inputType = props.isMultiSelect ? "checkbox" : "radio"; const inputType = props.isMultiSelect ? "checkbox" : "radio";
@ -183,8 +204,8 @@ export default {
const { const {
handleChange, handleChange,
errors, errors,
} = useField(props.groupName, props.validationRules, fieldOptions); } = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
return { return {
handleChange, handleChange,
errors, errors,

View file

@ -92,7 +92,7 @@ describe("radio.vue", () => {
isRequired: true, isRequired: true,
modelValue: ["List Card Checkbox"], modelValue: ["List Card Checkbox"],
value: "Car-Front", value: "Car-Front",
selectedValues: ["Car-Front"] selectedValues: "Car-Front"
}, },
}); });
// Assert // Assert

View file

@ -1,18 +1,19 @@
<template> <template>
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex --> <!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag and must contain tabindex -->
<div class="ui-radio form-check" :class="[hasError ? 'has-error' : '']"> <div class="ui-radio form-check" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']">
<input <input
type="radio" type="radio"
class="form-check-input" class="form-check-input"
aria-checked="false" aria-checked="false"
:name="groupName" :name="groupName"
:id="buttonID" :id="buttonID"
:aria-required="isRequired" :aria-required="isRequired"
:value="value" :value="value"
:v-model="checkValue" :v-model="checkValue"
@change="handleCheckChange()" @change="handleCheckChange"
:checked="checkValue" :checked="checkValue"
/> :validationRules="validationRules"
/>
<label class="d-flex align-items-start form-check-label" :for="buttonID"> <label class="d-flex align-items-start form-check-label" :for="buttonID">
<p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p> <p v-if="buttonLabel" class="m-0">{{ buttonLabel }}</p>
<span v-if="screenReaderOnlyText" class="sr-only">{{ <span v-if="screenReaderOnlyText" class="sr-only">{{
@ -23,7 +24,6 @@
</template> </template>
<script> <script>
import { toRefs } from "vue";
import { useField } from "vee-validate"; import { useField } from "vee-validate";
export default { export default {
name: "radio", name: "radio",
@ -37,8 +37,9 @@ export default {
default: "", default: "",
}, },
screenReaderOnlyText: String, screenReaderOnlyText: String,
selectedValues: [Array, String], selectedValues: String,
hasError: Boolean, hasError: Boolean,
validationRules: String
}, },
data() { data() {
return { return {
@ -47,16 +48,15 @@ export default {
}, },
created() { created() {
if (this.selectedValues) { if (this.selectedValues) {
this.checkValue = this.selectedValues[0] === this.value; this.checkValue = this.selectedValues === this.value;
}else{ this.handleCheckChange();
} else{
this.checkValue = false; this.checkValue = false;
} }
}, },
methods: { methods: {
handleClick(value) {
this.handleChange(value);
},
handleCheckChange() { handleCheckChange() {
this.handleChange(this.value);
const emitEvent = { const emitEvent = {
checkValue: this.checkValue, checkValue: this.checkValue,
value: this.value.toString(), value: this.value.toString(),
@ -88,13 +88,17 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.form-check { .form-check {
position: relative;
.form-check-input { .form-check-input {
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: 100%; border-radius: 50%;
margin-right: 0.5rem; margin-right: 0.5rem;
&:checked { &:checked {
background-color: $white; background-color: $white;
background-size: 71%; background-size: 71%;
background-position: center;
border: 1px solid $blue; border: 1px solid $blue;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50' xml:space='preserve'%3e%3ccircle cx='25' cy='25' r='25' fill='%231574A1'/%3e%3c/svg%3e");
+ label { + label {
@ -108,7 +112,15 @@ export default {
&:focus { &:focus {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
} }
&, & + label {
margin-top: 0;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
} }
&:hover { &:hover {
.form-check-input { .form-check-input {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;