Merge pull request #679 from Safelite/rlsmerge/develop-to-quote-page-mvp

Rlsmerge/develop to quote page mvp
This commit is contained in:
katieoh-safelite 2022-08-18 16:34:40 -04:00 committed by GitHub
commit 3a819af3af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 4592 additions and 2097 deletions

View file

@ -13,6 +13,8 @@ module.exports = {
"!src/router/**/*.js", "!src/router/**/*.js",
"!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/molding-questions/**/*.vue",
"!src/layouts/capability-questions/**/*.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/ux-components/text-link/**/*.vue",

View file

@ -7,6 +7,7 @@
<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="formattedGroupName"> <fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName">
<legend class="sr-only" :data-focus-target="formattedGroupName" :id="formattedGroupName" tabindex="-1"> <legend class="sr-only" :data-focus-target="formattedGroupName" :id="formattedGroupName" tabindex="-1">
{{ questionText }}
{{(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="getComponentLoopWrapperClasses" role="application"> <div :class="getComponentLoopWrapperClasses" role="application">
@ -36,7 +37,7 @@
data-test="button" data-test="button"
:validationRules="validationRules" :validationRules="validationRules"
:class="[suppressError ? 'alertError' : '' , isCashOrInsurance ? 'radio-fancy' : '']" :class="[suppressError ? 'alertError' : '' , isCashOrInsurance ? 'radio-fancy' : '']"
:clearOnUnmount="clearOnUnmount" :valueToLogType="valueToLogType"
/> />
<!-- For nested questions --> <!-- For nested questions -->
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
@ -93,10 +94,7 @@ export default {
validationRules: String, validationRules: String,
suppressError: Boolean, suppressError: Boolean,
useTextForValue: Boolean, useTextForValue: Boolean,
clearOnUnmount: { valueToLogType: String,
type: Boolean,
default: true
},
}, },
computed: { computed: {
formattedGroupName() { formattedGroupName() {
@ -179,7 +177,6 @@ export default {
this.selectedValues = val.value; this.selectedValues = val.value;
} }
} }
this.$emit("isCheckedChanged", val); this.$emit("isCheckedChanged", val);
}, },
}, },

View file

@ -7,11 +7,12 @@
<menuModal/> <menuModal/>
</div> </div>
<alert <alert
class="position-absolute rounded-0 w-100 border-0 shadow-sm"
v-if="displayGlobalAlert" v-if="displayGlobalAlert"
:alertClass="globalAlertMessage.type" class="position-absolute rounded-0 w-100 border-0 shadow-sm"
cmsWidgetName="GlobalAlert"
:manualHeadline="globalAlertMessage.messageHeadline" :manualHeadline="globalAlertMessage.messageHeadline"
:manualCopy="globalAlertMessage.messageCopy" :manualCopy="globalAlertMessage.messageCopy"
:alertClass="globalAlertMessage.type"
v-bind:isDismissible="globalAlertMessage.isDismissible" v-bind:isDismissible="globalAlertMessage.isDismissible"
/> />
</template> </template>

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="menu-modal-container"> <div class="menu-modal-container">
<button class="menu-button" type="button" :class="[isActive ? 'active' : '']" data-bs-toggle="modal" data-bs-target="#footerModal"> <button class="menu-button" type="button" :class="[isActive ? 'active' : '']" data-bs-toggle="modal" data-bs-target="#footerModal" aria-label="Hamburger Menu (modal window)">
<div class="bar1"></div> <div class="bar1"></div>
<div class="bar2"></div> <div class="bar2"></div>
<div class="bar3"></div> <div class="bar3"></div>
@ -8,6 +8,13 @@
</div> </div>
<!-- Modal --> <!-- Modal -->
<div class="modal fade" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true" v-on="{ 'show.bs.modal' : show, 'hide.bs.modal' : hide }" :style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`"> <div class="modal fade" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true" v-on="{ 'show.bs.modal' : show, 'hide.bs.modal' : hide }" :style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
<div class="menu-modal-container">
<button class="menu-button" type="button" :class="[isActive ? 'active' : '']" data-bs-toggle="modal" data-bs-target="#footerModal" aria-label="Hamburger Menu (modal window)">
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
</button>
</div>
<div class="modal-dialog modal-fullscreen"> <div class="modal-dialog modal-fullscreen">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header visually-hidden"> <div class="modal-header visually-hidden">
@ -100,6 +107,8 @@ export default {
height: calc(100% - 72px); height: calc(100% - 72px);
top: 72px; top: 72px;
border-top: 1px solid $gray-300; border-top: 1px solid $gray-300;
overflow-x: visible;
overflow-y: visible;
.modal-body { .modal-body {
padding: 2rem; padding: 2rem;
} }
@ -114,6 +123,45 @@ export default {
border-top: none; border-top: none;
padding: 2rem; padding: 2rem;
} }
.menu-modal-container {
position: absolute;
padding: 1.47rem 1rem 1.47rem 1.47rem;
right: 0;
top: -4.5rem;
button {
border: none;
&.menu-button {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
box-shadow: none;
background-color: $white;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;//Required to prevent 'squish' on iPhone
z-index: 1056;
.bar1,
.bar2,
.bar3 {
width: 14px;
height: 2px;
background-color: $blue;
margin: 1px 0;
transition: 0.25s;
}
&.active .bar1 {
transform: rotate(-45deg) translate(-3px, 3px);
}
&.active .bar2 {opacity: 0;}
&.active .bar3 {
transform: rotate(45deg) translate(-3px, -3px);
}
}
}
}
//.modal-backdrop styles are in common-styles.scss //.modal-backdrop styles are in common-styles.scss
} }
</style> </style>

View file

@ -7,11 +7,11 @@
:class="(q.questionSequence === currentQuestionNum) && 'current-question'" :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="`${keyString}-${q.questionSequence}`"
v-model="selectedValue" v-model="q.answerSelected"
@isCheckedChanged="handleChainCompleted"
isRequired isRequired
:validationRules="validationRules" :validationRules="validationRules"
:clearOnUnmount=false
/> />
</transition> </transition>
</div> </div>
@ -19,13 +19,14 @@
<script> <script>
import buttonQuestion from "@/common-components/button-question/button-question"; import buttonQuestion from "@/common-components/button-question/button-question";
import { useValidateForm } from "vee-validate";
export default { export default {
name: "questionChain", name: "questionChain",
data() { data() {
return { return {
currentQuestionNum: 1, currentQuestionNum: 0,
questions: [{ "BlankObject": "NOT USED... placeholder for question #0 to simplify indexing"}], questions: [],
}; };
}, },
props: { props: {
@ -33,53 +34,59 @@ export default {
validationRules: String, validationRules: String,
modelValue: Array, modelValue: Array,
partIndex: Number, partIndex: Number,
keyString: String,
}, },
created() { async created() {
this.questionData.partQuestions.map((q, i) => { // validate form upon create to prevent out of sync / persistent valid states
let answerPair = []; await useValidateForm(); // do a test validation check, without triggering full validation
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: {
selectedValue: {
get: function() {
return "";
},
set: function(returnedAnswer) {
const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer);
if (isQuestionChainComplete) { this.questionData.map((q, i) => {
this.$emit("update:modelValue", isQuestionChainComplete); let answerPair = [];
} const question = {
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: q.answerSelected || "",
};
question.answerPair = answerPair;
if (!q.isDuplicateQuestion) {
this.questions.push(question);
} }
}, });
currentQuestion() {
return this.questions[this.currentQuestionNum]; if (!this.modelValue?.length > 0) {
}, // set this.currentQuestionNum to first valid question
this.currentQuestionNum = this.questions[0].questionSequence;
// scroll the next question into view
this.$nextTick(() => {
document.querySelector('.current-question').scrollIntoView({behavior: "smooth"});
})
}
}, },
methods: { methods: {
handleReturnedAnswer(returnedAnswer) { // returns either a final answer or Boolean false handleChainCompleted(returnedAnswer) {
const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer.value);
if (isQuestionChainComplete) {
this.$emit("update:modelValue", isQuestionChainComplete);
}
},
handleReturnedAnswer(returnedAnswer) { // this method will return either a final answer or Boolean false
if (!returnedAnswer) { return false } if (!returnedAnswer) { return false }
// Example returnedAnswers: // Example returnedAnswers:
@ -87,28 +94,31 @@ export default {
// "5|answer|DW02104|Yes" // "5|answer|DW02104|Yes"
const returnedAnswerArray = returnedAnswer.split("|"); const returnedAnswerArray = returnedAnswer.split("|");
const questionNum = returnedAnswerArray[0]; const questionNum = parseInt(returnedAnswerArray[0]);
const questionType = returnedAnswerArray[1]; const questionType = returnedAnswerArray[1];
const questionAnswer = returnedAnswerArray[2]; const questionAnswer = returnedAnswerArray[2];
const questionAnswerText = returnedAnswerArray[3]; const questionAnswerText = returnedAnswerArray[3];
// remove all previous answers after the index of this one in questions this.questions.forEach((q) => {
this.questions.map((q) => { // mark this question as "answered"
if ((q.questionSequence > questionNum) || (q.answerPair?.includes(questionAnswer))) { if (q.questionSequence === questionNum) {
q.answerSelected = ""; q.answerSelected = returnedAnswer;
q.answerNumber = questionNum;
}
// remove all previous answers after the index of this one in questions
if ((q.questionSequence > questionNum)) {
delete q.answerSelected;
} }
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 // return false if there's a nextQuestion... or return an object with "final" answers
if (questionType === "nextQuestion") { if (questionType === "nextQuestion") {
// update to next question index
this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question
// scroll the next question into view
this.$nextTick(() => {
document.querySelector('.current-question').scrollIntoView({behavior: "smooth"});
})
return false; return false;
} else { } else {
const answeredQuestions = []; const answeredQuestions = [];
@ -116,30 +126,23 @@ export default {
if (q.answerSelected) { if (q.answerSelected) {
answeredQuestions.push({ answeredQuestions.push({
questionText: q.questionText, questionText: q.questionText,
selectedAnswerText: q.answerSelected, selectedAnswerText: q.answerSelected.split("|")[3],
questionNum: q.answerNumber, questionNum: q.questionSequence,
}); });
} }
}); });
// reset current question index
this.currentQuestionNum = 0; // reset count
return { return {
answerResult: questionAnswer, answerResult: questionAnswer,
answeredQuestions: answeredQuestions, answeredQuestions: answeredQuestions,
partIndex: this.partIndex, 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,
}, },

View file

@ -13,6 +13,7 @@
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
:aria-label="questionText"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']" :class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules" :validationRules="validationRules"
@change="handleChange" @change="handleChange"

View file

@ -29,5 +29,8 @@ const GaLabels = {
ADDRESS_LOOKUP: 'Address_Look_up', ADDRESS_LOOKUP: 'Address_Look_up',
}; };
const ValueToLogTypes = {
LAST_5: "last_5",
};
export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents}; export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes };

View file

@ -5,7 +5,9 @@ const applicationConfig = {
ANALYTICS_SESSION_TIMEOUT_MINUTES: 30, ANALYTICS_SESSION_TIMEOUT_MINUTES: 30,
SAVED_SESSION_TIMEOUT_DAYS: 45, SAVED_SESSION_TIMEOUT_DAYS: 45,
COOKIE_PATH: "/", COOKIE_PATH: "/",
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT // "Localhost", "Dev", "QA", and "Prod" CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
APPLICATION_NAME: "FixMyGlass",
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass"
}; };
export { applicationConfig }; export { applicationConfig };

View file

@ -59,6 +59,14 @@ const endpoints = {
url: "/parts/api/v1/parts/parts", url: "/parts/api/v1/parts/parts",
method: "POST", method: "POST",
}, },
GetCapabilityQuestions: {
url: "/parts/api/v1/parts/capability-questions",
method: "GET"
},
ApplyCapabilityAnswerToPart: {
url: "/parts/api/v1/parts/apply-capability-answer-to-part",
method: "POST"
},
SaveOrder: { SaveOrder: {
url: "/order/api/v1/order/save", url: "/order/api/v1/order/save",
method: "POST", method: "POST",
@ -72,7 +80,7 @@ const endpoints = {
method: "GET", method: "GET",
}, },
LogExperimentExposureIfAssigned:{ LogExperimentExposureIfAssigned:{
url: "/analytics/api/v1/analytics/log-experiment-exposure", url: "/experiments/api/v1/experiments/log-exposure",
method: "POST", method: "POST",
}, },
LogPageView:{ LogPageView:{
@ -90,6 +98,10 @@ const endpoints = {
GetExperimentsByUser: { GetExperimentsByUser: {
url: "/analytics/api/v1/analytics/get-experiments", url: "/analytics/api/v1/analytics/get-experiments",
method: "GET", method: "GET",
},
RunExperimentsForTrigger: {
url: "/experiments/api/v1/experiments/run",
method: "POST"
} }
}; };

View file

@ -5,6 +5,11 @@ const experimentUniverses = {
const experimentSettings = { const experimentSettings = {
GOOGLE_CUSTOM_DIMENSION_INDEX: 'Google Custom Dimension Index' GOOGLE_CUSTOM_DIMENSION_INDEX: 'Google Custom Dimension Index'
} }
const experimentTriggers = {
SITE_ENTRY: "SiteEntry",
PAGE_ENTRY: "PageEntry"
};
export { experimentUniverses, experimentSettings}; export { experimentUniverses, experimentSettings, experimentTriggers };

View file

@ -0,0 +1,3 @@
export const headerKeys = {
EXPERIMENT: "X-Experiment-Data"
}

View file

@ -21,6 +21,8 @@ const storeActions = {
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts", GET_PARTS: "getParts",
GET_CAPABILITY_QUESTIONS: "getCapabilityQuestions",
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
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",
@ -30,7 +32,9 @@ 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",
RUN_EXPERIMENTS_FOR_TRIGGER: "runExperimentsForTrigger",
CLEAR_VIN: "clearVin", CLEAR_VIN: "clearVin",
RESET_SAVE_ORDER_PROMISE: "resetSaveOrderPromise",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
@ -53,6 +57,7 @@ const storeActions = {
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup", SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
SAVE_GLASS_PARTS: "saveGlassParts", SAVE_GLASS_PARTS: "saveGlassParts",
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers", SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
SAVE_CAPABILITY_QUESTION_ANSWERS: "saveCapabilityQuestionAnswers"
}; };
export { storeActions }; export { storeActions };

View file

@ -17,7 +17,9 @@ const storeMutations = {
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_PART_QUESTION_ANSWERS: "updatePartQuestionAnswers",
UPDATE_CAPABILITY_QUESTION_ANSWERS: "updateCapabilityQuestionAnswers",
UPDATE_GLASS_PARTS: "updateGlassParts", UPDATE_GLASS_PARTS: "updateGlassParts",
UPDATE_OTHER_PARTS: "updateOtherParts",
UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate", UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate",
UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress", UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress",
@ -39,7 +41,8 @@ const storeMutations = {
UPDATE_REFERRAL_DATE: "updateReferralDate", UPDATE_REFERRAL_DATE: "updateReferralDate",
UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId", UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId",
UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber", UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber",
UPDATE_SAVE_QUOTE_ID: "updateSaveQuoteId", UPDATE_EON: "updateEON",
UPDATE_SAVED_SESSION_ID: "updateSavedSessionId",
UPDATE_CRM_CUSTOMER_ID: "updateCrmCustomerId", UPDATE_CRM_CUSTOMER_ID: "updateCrmCustomerId",
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
@ -52,12 +55,17 @@ const storeMutations = {
RESET_REGISTRATION_STATE: "resetRegistrationState", RESET_REGISTRATION_STATE: "resetRegistrationState",
RESET_GLASS_PARTS_STATE: "resetGlassPartsState", RESET_GLASS_PARTS_STATE: "resetGlassPartsState",
RESET_STATE: "resetState", RESET_STATE: "resetState",
RESET_SAVE_ORDER_PROMISE: "resetSaveOrderPromise",
// 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_SAVE_ORDER_PROMISE: "updateSaveOrderPromise", UPDATE_SAVE_ORDER_PROMISE: "updateSaveOrderPromise",
UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited", UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited",
// EXPERIMENT MUTATIONS
UPDATE_EXPERIMENTS: "updateExperiments",
UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry",
}; };
export { storeMutations }; export { storeMutations };

View file

@ -1,16 +1,21 @@
import axios from "axios"; import axios from "axios";
import analyticsMixIn from "@/mixins/analytics-mixin.js"; import analyticsMixIn from "@/mixins/analytics-mixin.js";
import store from "@/store";
import { applicationConfig } from "@/constants/application-config.js"; import { applicationConfig } from "@/constants/application-config.js";
import { GaCategories, GaActions, GaLabels } from "@/constants/analytics"; import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
import { headerKeys } from "@/constants/header-keys";
export default { export default {
callHttpClient({ method, endpoint, payload, logApiCall = true }) { callHttpClient({ method, endpoint, payload, logApiCall = true }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO; const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" }); const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" });
const headers = {
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings)
}
axios({ method: method, url: cfDistroUrl + endpoint, data: payloadAndAnalyticsData, crossDomain: true, responseType: {} }) axios({ method: method, url: cfDistroUrl + endpoint, data: payloadAndAnalyticsData, crossDomain: true, responseType: {}, headers: headers })
.then((response) => { .then((response) => {
if (logApiCall) { if (logApiCall) {

View file

@ -7,21 +7,20 @@ import { applicationConfig } from "@/constants/application-config";
*/ */
export function updateOrCreateFunnelCookie() { export function updateOrCreateFunnelCookie() {
const wasClaimRegistrationDelayed = getFunnelCookie()?.HasDelayedClaimRegistration; const wasClaimRegistrationDelayed = getFunnelCookie()?.HasDelayedClaimRegistration;
const shouldSuppressConceptFunnel = getFunnelCookie()?.SuppressConceptFunnel;
// Create the cookie
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}={}; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()};`;
// Set up cookie with all the props. // Set up cookie with all the props.
setFunnelCookieProperties({ setFunnelCookieProperties({
LastTouched: new Date().toUTCString(), LastTouched: new Date().toUTCString(),
SavedQuoteTimeoutDate: store.getters.applicationUser.savedSessionTimeout, SavedSessionTimeoutDate: store.getters.applicationUser.savedSessionTimeout,
DidHeritageFunnelUpdateLast: false, DidHeritageFunnelUpdateLast: false,
ShouldResetState: false, ShouldResetState: false,
ReferralNumber: store.getters.order.referralNumber, ReferralNumber: store.getters.order.referralNumber,
ReferralDate: store.getters.order.referralDate, ReferralDate: store.getters.order.referralDate,
ReferralCorrelationId: store.getters.order.referralCorrelationId, ReferralCorrelationId: store.getters.order.referralCorrelationId,
ReferralParentAccountNumber: store.getters.order.accountNumber, ReferralParentAccountNumber: store.getters.order.accountNumber,
HasDelayedClaimRegistration: wasClaimRegistrationDelayed HasDelayedClaimRegistration: wasClaimRegistrationDelayed,
SuppressConceptFunnel: shouldSuppressConceptFunnel
}); });
} }
@ -46,14 +45,14 @@ export function getFunnelCookie() {
Removes cookie from browser. Removes cookie from browser.
*/ */
export function deleteFunnelCookie() { export function deleteFunnelCookie() {
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=; Max-Age=0; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()}`; createOrUpdateCookie(cookieNames.FUNNEL_SESSION_INFO, undefined, { maxAge: 0 });
} }
/* /*
Gets cookie domain value. Localhost will be empty "". Gets cookie domain value. Localhost will be empty "".
*/ */
export function getCookieDomainValue() { export function getCookieDomainValue() {
return location.hostname.includes("localhost") ? "" : `domain=${getDomainWithoutSubdomain()};`; return isLocalhost() ? "" : `domain=${getDomainWithoutSubdomain()};`;
} }
/* /*
@ -104,10 +103,17 @@ export function getSessionIdValue(){
return '00000000-0000-0000-0000-000000000000'; return '00000000-0000-0000-0000-000000000000';
} }
export function setCookieProperties(properties) { /*
Updates session ID cookie with new expiration date
*/
export function updateSessionIdCookie() {
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 });
}
export function setCookieProperties(properties, { useDefaultFunnelCookieAttributes = true, maxAge, isSecure }) {
if (typeof properties == "object") { if (typeof properties == "object") {
Object.keys(properties).forEach(key => { Object.keys(properties).forEach(key => {
document.cookie = `${key}=${properties[key]}`; createOrUpdateCookie(key, properties[key], { useDefaultFunnelCookieAttributes, maxAge, isSecure });
}); });
} }
} }
@ -131,20 +137,39 @@ function setFunnelCookieProperties(properties) {
Object.keys(properties).forEach(key => { Object.keys(properties).forEach(key => {
cookie[key] = properties[key]; cookie[key] = properties[key];
}); });
const cookieValueJson = JSON.stringify(cookie);
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${cookieValueJson}; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()}`;
} }
const cookieValueJson = JSON.stringify(cookie ?? {});
createOrUpdateCookie(cookieNames.FUNNEL_SESSION_INFO, cookieValueJson, {});
} }
} }
/*
Used to create a cookie.
`useDefaultFunnelCookieAttributes` will set the path and domain to our defaults
*/
function createOrUpdateCookie(key, value = "", { useDefaultFunnelCookieAttributes = true, maxAge, isSecure = true }) {
let cookieToAdd = `${key}=${value}; `;
if (useDefaultFunnelCookieAttributes) {
cookieToAdd += `path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()} `;
}
if (isSecure && !isLocalhost()) {
cookieToAdd += `secure; `;
}
if (!isNaN(maxAge)) {
cookieToAdd += `max-age=${maxAge};`;
}
document.cookie = cookieToAdd;
}
/* /*
Gets current domain without the subdomain for cookie. Gets current domain without the subdomain for cookie.
*/ */
function getDomainWithoutSubdomain() { function getDomainWithoutSubdomain() {
let url = location.hostname; let url = location.hostname;
if (url.includes("localhost")) { if (isLocalhost()) {
return "localhost"; return "localhost";
} }
@ -167,4 +192,8 @@ function getCookieValueByName(name) {
return parts.pop().split(";").shift(); return parts.pop().split(";").shift();
} }
return ""; return "";
}
function isLocalhost() {
return location.hostname.includes("localhost");
} }

View file

@ -21,7 +21,8 @@ describe("cookies", () => {
ReferralDate: testReferralDate, ReferralDate: testReferralDate,
ReferralCorrelationId: testReferralCorrelationId, ReferralCorrelationId: testReferralCorrelationId,
ShouldResetState: testShouldResetState, ShouldResetState: testShouldResetState,
DidHeritageFunnelUpdateLast: testDidHeritageFunnelUpdateLast DidHeritageFunnelUpdateLast: testDidHeritageFunnelUpdateLast,
SuppressConceptFunnel: true
} }
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) }); setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });

View file

@ -16,7 +16,7 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') { if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
// If they have an existing order, return 'heritage' for the page name. // If they have an existing order, return 'heritage' for the page name.
if (existingHeritageOrder) { if (existingHeritageOrder) {
return 'heritage'; return fmgPageValues.HERITAGE;
} }
return await getLatestPageForRedirection(); return await getLatestPageForRedirection();
@ -39,9 +39,11 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
Used to navigate to the heritage funnel with the correct query string and url. Used to navigate to the heritage funnel with the correct query string and url.
*/ */
export async function navigateToHeritageFunnel() { export async function navigateToHeritageFunnel(shouldSaveOrder = true) {
// Create the order (or save existing order) when navigating to Heritage Funnel. // Create the order (or save existing order) when navigating to Heritage Funnel.
await saveOrder(); if (shouldSaveOrder) {
await saveOrder();
}
router.navigateToExternalUrl( router.navigateToExternalUrl(
externalUrls.HERITAGE_FUNNEL, externalUrls.HERITAGE_FUNNEL,
@ -59,11 +61,16 @@ async function getLatestPageForRedirection() {
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites. // If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order. // This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible. // That shouldn't happen, but it's possible.
const vehicleMakeComponent = (await lazyLoadComponent('vehicle-make')()).default; const vehicleMakeComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_MAKE);
const vehicleModelComponent = (await lazyLoadComponent('vehicle-model')()).default; const vehicleModelComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_MODEL);
const vehicleStyleComponent = (await lazyLoadComponent('vehicle-style')()).default; const vehicleStyleComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_STYLE);
const vehicleDamageComponent = (await lazyLoadComponent('vehicle-damage')()).default; const vehicleDamageComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_DAMAGE);
const estimateComponent = await getLazyLoadedComponent(fmgPageValues.ESTIMATE);
const vinLookupComponent = await getLazyLoadedComponent(fmgPageValues.VIN_LOOKUP);
const partQuestionsComponent = await getLazyLoadedComponent(fmgPageValues.PART_QUESTIONS);
const vehiclePartsComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_PARTS);
const moldingQuestionsComponent = await getLazyLoadedComponent(fmgPageValues.MOLDING_QUESTIONS);
const capabilityQuestionsComponent = await getLazyLoadedComponent(fmgPageValues.CAPABILITY_QUESTIONS);
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) { if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_YEAR; return fmgPageValues.VEHICLE_YEAR;
@ -73,15 +80,28 @@ async function getLatestPageForRedirection() {
return fmgPageValues.VEHICLE_MODEL; return fmgPageValues.VEHICLE_MODEL;
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) { } else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_STYLE; return fmgPageValues.VEHICLE_STYLE;
} else if (store.getters.damage.isRepair == null || !store.getters.vehicle.carId) { } else if (!estimateComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_DAMAGE; return fmgPageValues.VEHICLE_DAMAGE;
} else { } else {
if (store.getters.vehicle.vin) { if (capabilityQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.CAPABILITY_QUESTIONS;
}
else if (moldingQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.MOLDING_QUESTIONS;
}
else if (vehiclePartsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_PARTS;
}
else if (partQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.PART_QUESTIONS;
}
else if (vinLookupComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VIN_LOOKUP; return fmgPageValues.VIN_LOOKUP;
} else { }
else {
return fmgPageValues.ESTIMATE; return fmgPageValues.ESTIMATE;
} }
} }
} }
/* /*
@ -124,4 +144,8 @@ function isVinRelatedPage(toRoute) {
fmgPageValue === fmgPageValues.ADDRESS_LOOKUP || fmgPageValue === fmgPageValues.ADDRESS_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_VEHICLES || fmgPageValue === fmgPageValues.ADDRESS_VEHICLES ||
fmgPageValue === fmgPageValues.ESTIMATE; fmgPageValue === fmgPageValues.ESTIMATE;
}
async function getLazyLoadedComponent(pageName) {
return (await lazyLoadComponent(pageName)()).default;
} }

View file

@ -5,6 +5,7 @@ import { storeActions } from "@/constants/store-actions";
import { setupMocksForJsFiles, getMockOrderInfo } from "@/helpers/unit-test-helper.js"; import { setupMocksForJsFiles, getMockOrderInfo } from "@/helpers/unit-test-helper.js";
import { externalUrls } from "@/router/router-constants/externalUrl-values"; import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import store from "@/store"; import store from "@/store";
import router from "@/router"; import router from "@/router";
@ -15,278 +16,269 @@ jest.mock("@/router/dynamic-routing/component-loader.js", () => ({
})); }));
describe("getPageToRouteExistingOrderTo", () => { describe("getPageToRouteExistingOrderTo", () => {
test("should return vehicle-year", async () => {
test("getPageToRouteExistingOrderTo, should return vehicle-year", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: {} query: {}
}; };
// Mock out the lazy load calls for all components. // Mock out the lazy load calls for all components.
lazyLoadComponent mockLazyLoadComponentReturnValues({
.mockReturnValueOnce(() => { [fmgPageValues.VEHICLE_MAKE]: false
return { })
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
});
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
expect(result).toBe('vehicle-year'); expect(result).toBe(fmgPageValues.VEHICLE_YEAR);
}); });
test("getPageToRouteExistingOrderTo, should return vehicle-model", async () => { test("should return vehicle-make", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: {} query: {}
}; };
// Mock out the lazy load calls for all components. // Mock out the lazy load calls for all components.
lazyLoadComponent mockLazyLoadComponentReturnValues({
.mockReturnValueOnce(() => { [fmgPageValues.VEHICLE_MAKE]: true,
return { [fmgPageValues.VEHICLE_MODEL]: false
default: { })
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(false)
}
}
}
});
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
expect(result).toBe('vehicle-model'); expect(result).toBe(fmgPageValues.VEHICLE_MAKE);
}); });
test("getPageToRouteExistingOrderTo, should return vehicle-damage", async () => { test("should return vehicle-model", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: {} query: {}
}; };
// Mock out the lazy load calls for all components. // Mock out the lazy load calls for all components.
lazyLoadComponent mockLazyLoadComponentReturnValues({
.mockReturnValueOnce(() => { [fmgPageValues.VEHICLE_MAKE]: true,
return { [fmgPageValues.VEHICLE_MODEL]: true,
default: { [fmgPageValues.VEHICLE_STYLE]: false,
methods: { })
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = undefined;
store.getters.vehicle.carId = 'C00000';
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
expect(result).toBe('vehicle-damage'); expect(result).toBe(fmgPageValues.VEHICLE_MODEL);
}); });
test("getPageToRouteExistingOrderTo, should return vin-lookup", async () => { test("should return vehicle-style", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: {} query: {}
}; };
// Mock out the lazy load calls for all components. // Mock out the lazy load calls for all components.
lazyLoadComponent mockLazyLoadComponentReturnValues({
.mockReturnValueOnce(() => { [fmgPageValues.VEHICLE_MAKE]: true,
return { [fmgPageValues.VEHICLE_MODEL]: true,
default: { [fmgPageValues.VEHICLE_STYLE]: true,
methods: { [fmgPageValues.VEHICLE_DAMAGE]: false,
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true) })
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = true;
store.getters.vehicle.carId = 'C00000';
store.getters.vehicle.vin = "1FADP3F26DL212886"
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
expect(result).toBe('vin-lookup'); expect(result).toBe(fmgPageValues.VEHICLE_STYLE);
}); });
test("getPageToRouteExistingOrderTo, should return estimate", async () => { test("should return vehicle-damage", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: {} query: {}
}; };
// Mock out the lazy load calls for all components. // Mock out the lazy load calls for all components.
lazyLoadComponent mockLazyLoadComponentReturnValues({
.mockReturnValueOnce(() => { [fmgPageValues.VEHICLE_MAKE]: true,
return { [fmgPageValues.VEHICLE_MODEL]: true,
default: { [fmgPageValues.VEHICLE_STYLE]: true,
methods: { [fmgPageValues.VEHICLE_DAMAGE]: true,
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true) [fmgPageValues.ESTIMATE]: false
} })
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
})
.mockReturnValueOnce(() => {
return {
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(true)
}
}
}
});
store.getters.damage.isRepair = true;
store.getters.vehicle.carId = 'C00000';
store.getters.vehicle.vin = null;
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert //Assert
expect(result).toBe('estimate'); expect(result).toBe(fmgPageValues.VEHICLE_DAMAGE);
}); });
test("getPageToRouteExistingOrderTo, existing order, should return heritage", async () => { test("user has YMMS and no vehicle questions > should return vin-lookup", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: true,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VIN_LOOKUP);
});
test("user has YMMS but no questions or carId > should return estimate", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: false,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.ESTIMATE);
});
test("user has capability questions and molding questions > should return capability questions", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: true,
[fmgPageValues.MOLDING_QUESTIONS]: true,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: false,
[fmgPageValues.VIN_LOOKUP]: false,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.CAPABILITY_QUESTIONS);
});
test("user has molding questions and part questions > should return molding questions", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: true,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.MOLDING_QUESTIONS);
});
test("user has vehicle parts questions > should return vehicle-parts", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: true,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_PARTS);
});
test("user has part questions > should return part-questions", async () => {
// Arrange
const toRoute = {
query: {}
};
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
[fmgPageValues.MOLDING_QUESTIONS]: false,
[fmgPageValues.VEHICLE_PARTS]: false,
[fmgPageValues.PART_QUESTIONS]: true,
[fmgPageValues.VIN_LOOKUP]: false,
})
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.PART_QUESTIONS);
});
test("existing order > should return heritage", async () => {
// Arrange // Arrange
const toRoute = { const toRoute = {
query: { query: {
@ -298,7 +290,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, true); const result = await getPageToRouteExistingOrderTo(toRoute, true);
// Assert // Assert
expect(result).toBe("heritage"); expect(result).toBe(fmgPageValues.HERITAGE);
}) })
}); });
@ -309,10 +301,10 @@ describe("navigateToHeritageFunnel", () => {
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "167132"; const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx"; const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx" const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSavedSessionId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{
@ -331,7 +323,7 @@ describe("navigateToHeritageFunnel", () => {
// Assert // Assert
expect(saveOrderFunction).toHaveBeenCalled(); expect(saveOrderFunction).toHaveBeenCalled();
// Should alway save before we navigate to heritage // Should save before we navigate to heritage by default
const saveOrderFunctionCallOrder = saveOrderFunction.mock.invocationCallOrder[0]; const saveOrderFunctionCallOrder = saveOrderFunction.mock.invocationCallOrder[0];
const routerNavigateFunctionCallOrder = router.navigateToExternalUrl.mock.invocationCallOrder[0]; const routerNavigateFunctionCallOrder = router.navigateToExternalUrl.mock.invocationCallOrder[0];
expect(saveOrderFunctionCallOrder).toBeLessThan(routerNavigateFunctionCallOrder); expect(saveOrderFunctionCallOrder).toBeLessThan(routerNavigateFunctionCallOrder);
@ -344,10 +336,10 @@ describe("navigateToHeritageFunnel", () => {
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "167132"; const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx"; const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx" const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSavedSessionId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{
@ -373,4 +365,50 @@ describe("navigateToHeritageFunnel", () => {
}) })
); );
}); });
});
test("should not save order, but should still navigate", async () => {
// Arrange
const mockReferralNumber = "2";
const mockCorrelationId = "55";
const mockReferralDate = "2022";
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate);
const mockData = {
actionList: [{
actionName: storeActions.SAVE_ORDER,
data: mockOrderInfo,
}]
}
setupMocksForJsFiles(mockData);
const saveOrderFunction = jest.spyOn(orderHelper, "saveOrder");
router.navigateToExternalUrl = jest.fn();
// Act
await navigateToHeritageFunnel(false);
// Assert
expect(saveOrderFunction).not.toHaveBeenCalled();
expect(router.navigateToExternalUrl).toHaveBeenCalled();
saveOrderFunction.mockRestore();
});
});
/**
* `arePagePrerequisitesValidObject` is an object where the keys are fmgPageValue names and the values are booleans that indicate
* whether arePagePrerequisitesValid is true or false
*/
function mockLazyLoadComponentReturnValues(arePagePrerequisitesValidObject = {}) {
lazyLoadComponent.mockImplementation((pageName) => {
return async () => {
return Promise.resolve({
default: {
methods: {
arePagePrerequisitesValid: jest.fn().mockReturnValueOnce(arePagePrerequisitesValidObject[pageName])
}
}
})
}
})
}

View file

@ -60,6 +60,8 @@ export async function saveOrder() {
and returns the response. and returns the response.
*/ */
async function loadOrder(referralNumber, referralDate, referralCorrelationId, accountNumber) { async function loadOrder(referralNumber, referralDate, referralCorrelationId, accountNumber) {
// await the saveOrderPromise in the store to make sure we're loading up to date information
await store.getters.applicationUser.saveOrderPromise;
const response = await baseMixin.methods.dispatchStoreAction(storeActions.LOAD_ORDER, const response = await baseMixin.methods.dispatchStoreAction(storeActions.LOAD_ORDER,
{ {
referralNumber: referralNumber.toString(), referralNumber: referralNumber.toString(),
@ -82,7 +84,7 @@ async function saveOrderHelper() {
referralCorrelationId: savedOrderInfo.data.referralCorrelationId, referralCorrelationId: savedOrderInfo.data.referralCorrelationId,
referralDate: savedOrderInfo.data.referralDate, referralDate: savedOrderInfo.data.referralDate,
accountNumber: savedOrderInfo.data.accountNumber.toString(), accountNumber: savedOrderInfo.data.accountNumber.toString(),
saveQuoteId: savedOrderInfo.data.saveQuoteId, savedSessionId: savedOrderInfo.data.savedSessionId,
crmCustomerId: savedOrderInfo.data.crmCustomerId.toString(), crmCustomerId: savedOrderInfo.data.crmCustomerId.toString(),
}, false); }, false);

View file

@ -112,10 +112,10 @@ describe("saveOrder", () => {
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "167132"; const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx"; const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx" const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSavedSessionId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [ actionList: [
@ -141,7 +141,7 @@ describe("saveOrder", () => {
referralDate: mockReferralDate, referralDate: mockReferralDate,
referralCorrelationId: mockCorrelationId, referralCorrelationId: mockCorrelationId,
accountNumber: mockAccountNumber, accountNumber: mockAccountNumber,
saveQuoteId: mockSaveQuoteId, savedSessionId: mockSavedSessionId,
crmCustomerId: mockCrmCustomerId, crmCustomerId: mockCrmCustomerId,
}, false); }, false);
}); });
@ -152,10 +152,10 @@ describe("saveOrder", () => {
const mockReferralDate = "2022-03-15T10:56:24.597"; const mockReferralDate = "2022-03-15T10:56:24.597";
const mockReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060"; const mockReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060";
const mockAccountNumber = "167132"; const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx"; const mockSavedSessionId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx" const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockReferralCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockReferralCorrelationId, mockReferralDate, mockAccountNumber, mockSavedSessionId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{

View file

@ -28,19 +28,15 @@ export function isAnalyticsSessionStillActive() {
*/ */
export function isSavedSessionStillActive() { export function isSavedSessionStillActive() {
if (getFunnelCookie() !== null) { if (getFunnelCookie() !== null) {
const savedSessionTimeStamp = new Date(getFunnelCookie().SavedQuoteTimeoutDate); const savedSessionTimeStamp = new Date(getFunnelCookie().SavedSessionTimeoutDate);
const isSavedSessionTimedOut = (new Date(new Date().toUTCString()) > savedSessionTimeStamp); const isSavedSessionTimedOut = (new Date(new Date().toUTCString()) > savedSessionTimeStamp);
if (isSavedSessionTimedOut) { return !isSavedSessionTimedOut;
return false;
}
return true;
} }
} }
/* /*
Function to get the date for the saved session timeout. Function to calculate the date for the saved session timeout.
*/ */
export function getDateForSavedSessionTimeout() { export function getDateForSavedSessionTimeout() {

View file

@ -41,7 +41,7 @@ describe("isSavedSessionStillActive", () => {
mockDate.setDate(mockDate.getDate() + 1); mockDate.setDate(mockDate.getDate() + 1);
cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie") cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValue({ SavedQuoteTimeoutDate: mockDate }); .mockReturnValue({ SavedSessionTimeoutDate: mockDate });
// Act // Act
const result = isSavedSessionStillActive(); const result = isSavedSessionStillActive();
@ -56,7 +56,7 @@ describe("isSavedSessionStillActive", () => {
mockDate.setDate(mockDate.getDate() - 1) mockDate.setDate(mockDate.getDate() - 1)
cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie") cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie")
.mockReturnValue({ SavedQuoteTimeoutDate: mockDate }); .mockReturnValue({ SavedSessionTimeoutDate: mockDate });
// Act // Act
const result = isSavedSessionStillActive(); const result = isSavedSessionStillActive();

View file

@ -6,8 +6,8 @@ import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { cookieNames } from "@/constants/cookie-names"; import { cookieNames } from "@/constants/cookie-names";
import { Form } from "vee-validate"; import { Form } from "vee-validate";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import { getCookieDomainValue } from "@/helpers/heritage-integration/cookie-helper"; import { getCookieDomainValue, setCookieProperties } from "@/helpers/heritage-integration/cookie-helper";
import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics"; import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes } from "@/constants/analytics";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { routerParams } from "@/router/router-constants/router-params"; import { routerParams } from "@/router/router-constants/router-params";
@ -48,6 +48,7 @@ export function getMountOptions(mockData) {
mocks.GaActions = GaActions; mocks.GaActions = GaActions;
mocks.GaLabels = GaLabels; mocks.GaLabels = GaLabels;
mocks.GaEvents = GaEvents; mocks.GaEvents = GaEvents;
mocks.ValueToLogTypes = ValueToLogTypes;
mocks.queryStrings = queryStrings; mocks.queryStrings = queryStrings;
mocks.routerParams = routerParams; mocks.routerParams = routerParams;
@ -90,13 +91,13 @@ export function removeAllTestCookies() {
}); });
} }
export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, accountNumber = "0", saveQuoteId, crmCustomerId) { export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, accountNumber = "0", savedSessionId, crmCustomerId) {
return { return {
referralNumber: mockReferralNumber, referralNumber: mockReferralNumber,
referralCorrelationId: mockCorrelationId, referralCorrelationId: mockCorrelationId,
referralDate: mockReferralDate, referralDate: mockReferralDate,
accountNumber: accountNumber, accountNumber: accountNumber,
saveQuoteId: saveQuoteId, savedSessionId: savedSessionId,
crmCustomerId: crmCustomerId, crmCustomerId: crmCustomerId,
} }
} }
@ -105,7 +106,7 @@ export function setupCookies({ funnelCookieValue = "", includeHeritageCookie = t
Object.keys(cookies).forEach(key => { Object.keys(cookies).forEach(key => {
const cookieValue = key == cookieNames.FUNNEL_SESSION_INFO ? funnelCookieValue : cookies[key]; const cookieValue = key == cookieNames.FUNNEL_SESSION_INFO ? funnelCookieValue : cookies[key];
if (includeHeritageCookie || key != cookieNames.FUNNEL_SESSION_INFO) if (includeHeritageCookie || key != cookieNames.FUNNEL_SESSION_INFO)
document.cookie = `${key}=${cookieValue}; path=/; ${getCookieDomainValue()}`; setCookieProperties({ [key]: cookieValue }, { isSecure: false });
}); });
} }

View file

@ -351,10 +351,9 @@ describe("address-lookup.vue", () => {
addressQuestions: mockRegistrationAddress addressQuestions: mockRegistrationAddress
}, },
isCarIdDifferent: true, isCarIdDifferent: true,
isGlassAvailableForCarId: false, isSelectedGlassAvailableForVehicle: false,
}) })
let carsFound = [{ let carsFound = [{
vin: "TEST_VIN2", vin: "TEST_VIN2",
vehicle: { vehicle: {
@ -499,38 +498,6 @@ describe("address-lookup.vue", () => {
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true); expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true);
}); });
// test.only("if registration address is provided user clicks continue => show service zip field on continue click", async () => {
// // Arrange
// const mockRegistrationAddress = {
// streetAddress: "1234 Main St",
// city: "Columbus",
// state: "OH",
// zipCode: "43215"
// }
// const { wrapper } = setupMocks({
// isZipServiceable: false
// }
// );
// expect(wrapper.vm.showServiceZipField).toBeFalsy();
// expect(wrapper.findComponent({ ref: "serviceZip" }).exists()).toBe(false);
// store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
// await wrapper.setData({
// customerQuestions: {
// addressQuestions: mockRegistrationAddress
// }
// })
// // Act
// await wrapper.vm.forwardButtonAction();
// // Assert
// expect(wrapper.vm.showServiceZipField).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
const mockRegistrationAddress = { const mockRegistrationAddress = {

View file

@ -127,7 +127,7 @@ export default {
displayVinLookupByHomeAddressNotAllowedAlert: false, displayVinLookupByHomeAddressNotAllowedAlert: false,
previouslyEnteredCarId: "", previouslyEnteredCarId: "",
isCarIdDifferent: false, isCarIdDifferent: false,
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: true,
customAlertData: {}, customAlertData: {},
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
showServiceZipField: this.getServiceZipFromStore(), showServiceZipField: this.getServiceZipFromStore(),
@ -230,9 +230,6 @@ export default {
const carFound = carsFound[0].vehicle; const carFound = carsFound[0].vehicle;
this.isCarIdDifferent = carFound.carId !== this.$store.getters.vehicle.carId; this.isCarIdDifferent = carFound.carId !== this.$store.getters.vehicle.carId;
console.log(this.isCarIdDifferent);
console.log(carFound.carId);
console.log(this.$store.getters.vehicle.carId);
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) { if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
// Display Alert // Display Alert
this.previouslyEnteredCarId = carFound.carId; this.previouslyEnteredCarId = carFound.carId;
@ -261,7 +258,6 @@ console.log(this.$store.getters.vehicle.carId);
} else { } else {
// No VINS found. // No VINS found.
console.log("no vins");
this.displayVinNotFoundAlert = true; this.displayVinNotFoundAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.funnelFooter.removeLoader();

View file

@ -56,13 +56,15 @@
</div> </div>
</div> </div>
</transition> </transition>
<alert ref="alertVerificationWarning" v-if="displayVerificationWarning" <alert ref="alertVerificationWarning"
v-if="displayVerificationWarning"
class="mb-4" class="mb-4"
cmsWidgetName="AlertVerificationWarningWidget" cmsWidgetName="AlertVerificationWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertNoMatchWarning" v-if="displayNoMatchWarning" <alert ref="alertNoMatchWarning"
v-if="displayNoMatchWarning"
class="mb-4" class="mb-4"
cmsWidgetName="AlertNoMatchWarningWidget" cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"

View file

@ -1,5 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question"; import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import { ValueToLogTypes } from "@/constants/analytics";
describe("addressVehiclesQuestion.vue", () => { describe("addressVehiclesQuestion.vue", () => {
@ -54,9 +56,15 @@ const mockMixin = {
return 'FoundWindshieldTestReturn'; return 'FoundWindshieldTestReturn';
} }
return null; return null;
}), }),
vehicles: jest.fn(() => { vehicles: jest.fn(() => {
return [{ vehicle: "test" }]; return [{ vehicle: "test" }];
}) })
} },
computed: {
ValueToLogTypes() {
return ValueToLogTypes;
}
},
} }

View file

@ -8,14 +8,15 @@
v-model="selectedVehicleVinAsArray" v-model="selectedVehicleVinAsArray"
isRequired isRequired
:validation-rules="validationRules" :validation-rules="validationRules"
:valueToLogType="ValueToLogTypes.LAST_5"
/> />
<alert <alert ref="differentVehicleAlert"
ref="differentVehicleAlert"
v-if="isCarIdDifferent" v-if="isCarIdDifferent"
class="my-3" class="my-3"
alertClass="alert-warning" cmsWidgetName="FoundWindshield"
:manualHeadline="differentVehicleAlertHeader" :manualHeadline="differentVehicleAlertHeader"
:manualCopy="differentVehicleAlertBody" :manualCopy="differentVehicleAlertBody"
alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
</template> </template>

View file

@ -125,8 +125,8 @@ export default {
// Map API result data, to address-vehicles data structure // Map API result data, to address-vehicles data structure
const mappedData = this.VehiclesFromApi.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 - 6);
const vinEnd = v.vin.substring(v.vin.length-4); const vinEnd = v.vin.substring(v.vin.length - 6);
return { return {
vin: v.vin, vin: v.vin,
vehicle: v.vehicle, vehicle: v.vehicle,

View file

@ -0,0 +1,159 @@
<template>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="page-container-grouped-styles capability-questions">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<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"
/>
<questionChain
ref="questionChain"
v-model="selectedAnswer"
:questionData="capabilityQuestionsData"
:partIndex="i"
validationRules="questions-required"
/>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
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 funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
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";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "capability-questions",
mixins: [vehicleQuestionsMixin],
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);
});
},
data() {
return {
selectedAnswer: [],
// TODO This shouldn't be an object with property `partQuestions`
capabilityQuestionsData: {
partQuestions: store.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS).capabilityQuestions
}
};
},
computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent("AdditionalPartsQuestionsAlert", "HeadlineText");
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AdditionalPartsQuestionsAlert", "BodyText");
},
windshieldPart() {
return this.pageData.partsOrQuestions.find(x => x.glassLocation === damageLocationsSelected.WINDSHIELD);
},
windshieldPartInfo() {
return this.windshieldPart.parts[0];
},
pageData() {
return this.$store.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS);
}
},
methods: {
arePagePrerequisitesValid() {
const capabilityQuestionsPageData = store.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS);
return capabilityQuestionsPageData && Object.keys(capabilityQuestionsPageData).length > 0;
},
async forwardButtonAction() {
const selectedAnswerResult1 = this.selectedAnswer.answerResult;
const selectedAnswerResult2 = this.pageData.capabilityQuestions[0].answers.find(x => x.answerResult1 === selectedAnswerResult1).answerResult2;
this.dispatchStoreAction(storeActions.SAVE_CAPABILITY_QUESTION_ANSWERS, {
glassName: this.windshieldPart.glassName,
glassLocation: this.windshieldPart.glassLocation,
result1: selectedAnswerResult1,
result2: selectedAnswerResult2
});
const partFromCapabilityQuestionAnswer = (await this.dispatchStoreAction(storeActions.GET_PART_FROM_CAPABILITY_QUESTION_ANSWER)).data;
let partsOrQuestions = this.pageData.partsOrQuestions;
partsOrQuestions.find(partOrQuestion => partOrQuestion.glassLocation === damageLocationsSelected.WINDSHIELD).parts = partFromCapabilityQuestionAnswer;
this.navigateForward(partsOrQuestions);
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
Form,
},
};
</script>
<style lang="scss">
.capability-questions {
.question-text {
margin-bottom: .5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -11,11 +11,10 @@
<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">
<alert <alert
class="vinLookupMethodHeading" class="vinLookupMethodHeading"
v-model="customAlertData" cmsWidgetName="AlertVinLookupQuestion"
alertClass="" alertClass=""
cmsWidgetName="AlertVinLookupQuestion" />
/>
<buttonQuestion <buttonQuestion
cmsWidgetName="VinLookupMethod" cmsWidgetName="VinLookupMethod"
:answers="answersFromCms" :answers="answersFromCms"
@ -81,7 +80,7 @@ export default {
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
if(store.getters.damage.isRepair!=null){ if(store.getters.damage.isRepair != null){
return true; return true;
} }
return false; return false;

View file

@ -235,7 +235,7 @@ describe("license-plate-lookup.vue", () => {
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
test("navigateToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => { test("navigateForwardWithSingleCarMatch should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -247,11 +247,11 @@ describe("license-plate-lookup.vue", () => {
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ''; return '';
}); });
navigateToHeritage.navigateToHeritageFunnel = jest.fn(); wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
//Assert //Assert
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled(); expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalled();
}); });
test("carId matches returned vehicle => navigateForwardWithSingleCarMatch", async () => { test("carId matches returned vehicle => navigateForwardWithSingleCarMatch", async () => {

View file

@ -163,7 +163,7 @@ export default {
displayVinNotFoundAlert: false, displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false, displayMatchedDifferentVehicleAlert: false,
previouslyEnteredCarId: "", previouslyEnteredCarId: "",
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: true,
isCarIdDifferent: false, isCarIdDifferent: false,
customAlertData: {}, customAlertData: {},
displayInvalidZipAlert: false, displayInvalidZipAlert: false,

View file

@ -0,0 +1,202 @@
<template>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="page-container-grouped-styles molding-questions">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<h1>MOLDING QUESTIONS TEMPORARY PLACEHOLDER</h1>
<alert
ref="alertFewMoreQuestions"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader"
:manualCopy="AlertFewMoreQuestionsCopy"
v-bind:isDismissible="false"
/>
<div v-for="(part, i) in moldingQuestionsData" :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="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
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 funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
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";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "molding-questions",
mixins: [vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// 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);
});
},
data() {
return {
selectedModel: [],
partsQuestionsData: this.$store.getters.pageData(fmgPageValues.MOLDING_QUESTIONS).partsOrQuestions.filter((p) => {
if (Array.isArray(p.partQuestions) && p.partQuestions.length > 0) {
return {
glassName: p.glassName,
glassLocation: p.glassLocation,
partQuestions: p.partQuestions,
};
}
}),
currentPartNum: 0,
};
},
computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent("AlertPartsQuestions", "HeadlineText");
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AlertPartsQuestions", "BodyText");
},
},
methods: {
arePagePrerequisitesValid() {
return false; // TODO - DO TRUE TEST OF PAGEDATA
// return Object.keys(store.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)).length > 0;
},
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() {
// route to move backwards
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
async forwardButtonAction() {
const partQuestionAnswersArray = this.partsQuestionsData.map((item) => {
return {
glassLocation: item.glassLocation,
glassName: item.glassName,
result: item.answerData.answerResult,
answeredQuestions: item.answerData.answeredQuestions,
};
});
// save to vuex store as order.damage.partQuestionAnswers (array)
await this.dispatchStoreAction(this.storeActions.SAVE_PART_QUESTION_ANSWERS, partQuestionAnswersArray, false);
// call API parts method
const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS)
.catch(() => {
return this.$refs.funnelFooter.removeLoader();
});
const glassNameAndPartsForStore = partsLookup.data.glassNameAndPartsForStore;
const hasCapabilityQuestions = this.hasCapabilityQuestions(glassNameAndPartsForStore);
if (hasCapabilityQuestions) {
// if has capability questions
// go to capability-questions page
this.$router.navigate(this.navigationScenarios.HAS_CAPABILITY_QUESTIONS,this.$route,{},{},{partsOrQuestions: glassNameAndPartsForStore});
} else {
// if single parts only
const collectedGlassParts = this.reducedGlassPartsArray(glassNameAndPartsForStore);
// save to store lineItems.glassParts
this.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
// go to heritage quote page
this.$refs.loadingModal.showModal();
navigateToHeritageFunnel();
}
},
},
watch: {
selectedModel(model) {
this.moldingQuestionsData[model.partIndex].answerData = {
answerResult: model.answerResult,
answeredQuestions: model.answeredQuestions,
}
this.currentPartNum = model.partIndex + 1;
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
Form,
},
};
</script>
<style lang="scss">
.molding-questions {
.question-text {
margin-bottom: .5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -6,12 +6,14 @@
v-slot="{ meta }" v-slot="{ meta }"
> >
<div class="page-container-grouped-styles part-questions"> <div class="page-container-grouped-styles part-questions">
<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">
<alert <alert
ref="alertFewMoreQuestions" ref="alertFewMoreQuestions"
cmsWidgetName="AlertFewMoreQuestionsWidget"
class="my-5" class="my-5"
alertClass="alert-warning" alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader" :manualHeadline="AlertFewMoreQuestionsHeader"
@ -21,8 +23,10 @@
<div v-for="(part, i) in partsQuestionsData" :key="i"> <div v-for="(part, i) in partsQuestionsData" :key="i">
<questionChain <questionChain
ref="questionChain" ref="questionChain"
v-model="selectedModel" :key="part.key"
:questionData="part" :keyString="part.key"
v-model="selectedAnswers[part.glassLocation + '-' + part.glassName]"
:questionData="part.partQuestions"
:partIndex="i" :partIndex="i"
v-if="showThisPartQuestionChain(part, i)" v-if="showThisPartQuestionChain(part, i)"
validationRules="questions-required" validationRules="questions-required"
@ -31,7 +35,7 @@
<funnel-footer <funnel-footer
ref="funnelFooter" ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="waitingForLoad || !meta.valid" :isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
/> />
@ -48,6 +52,7 @@ import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain"; 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";
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";
@ -59,6 +64,8 @@ import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED)); defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -86,12 +93,14 @@ export default {
}, },
data() { data() {
return { return {
selectedModel: [], partsQuestionsFromApi: this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS)?.partsOrQuestions.filter((p) => {
partsQuestionsData: [], return Array.isArray(p.partQuestions) && p.partQuestions.length > 0;
waitingForLoad: true, }),
selectedAnswers: {},
currentPartNum: 0, currentPartNum: 0,
glassNameAndParts: [], newAnswersArray: [],
hasMultipleParts: false, partsQuestionsData: [],
foundDuplicateQuestions: [],
}; };
}, },
computed: { computed: {
@ -102,35 +111,16 @@ export default {
return this.getCmsContent("AlertPartsQuestions", "BodyText"); return this.getCmsContent("AlertPartsQuestions", "BodyText");
}, },
}, },
async mounted() { mixins: [vehicleQuestionsMixin],
const questionData = await this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS); mounted() {
this.LoadInitialPartsData();
// 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) { showThisPartQuestionChain(part, i) {
if (part.partQuestions?.length < 1) { return false; } // return false if only one partQuestion if (part.partQuestions?.length < 1 || part.isSuppressedPart) { return false; } // return false if no partQuestions or if suppressed
if (this.currentPartNum === i || part.answerData?.answerResult.length > 0) { return true; } if (this.currentPartNum === i || part.answerData?.answerResult?.length > 0) { return true; }
return false; return false;
}, },
backButtonAction() {
// route to move backwards
this.$router.navigate(
this.navigationScenarios.CLICKED_BACK,
this.$route
);
},
async forwardButtonAction() { async forwardButtonAction() {
const partQuestionAnswersArray = this.partsQuestionsData.map((item) => { const partQuestionAnswersArray = this.partsQuestionsData.map((item) => {
return { return {
@ -138,65 +128,367 @@ export default {
glassName: item.glassName, glassName: item.glassName,
result: item.answerData.answerResult, result: item.answerData.answerResult,
answeredQuestions: item.answerData.answeredQuestions, answeredQuestions: item.answerData.answeredQuestions,
isSuppressedPart: item.isSuppressedPart,
}; };
}); });
// clear out answerData for future page loads; must occur prior to store save
this.partsQuestionsData.forEach((part) => {
part.answerData = {};
});
// save to vuex store as order.damage.partQuestionAnswers (array) // save to vuex store as order.damage.partQuestionAnswers (array)
await this.dispatchStoreAction(this.storeActions.SAVE_PART_QUESTION_ANSWERS, partQuestionAnswersArray, false); await this.dispatchStoreAction(this.storeActions.SAVE_PART_QUESTION_ANSWERS, partQuestionAnswersArray, false);
// call API parts method // call API parts method
const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS) const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS)
.catch(() => { .catch(() => {
this.$refs.funnelFooter.removeLoader(); return this.$refs.funnelFooter.removeLoader();
}); });
if (!partsLookup) { return } const glassNameAndPartsForStore = partsLookup.data.glassNameAndParts;
this.navigateForward(glassNameAndPartsForStore);
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() { arePagePrerequisitesValid() {
return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length > 0; const partQuestionsFromPageData = store.getters.pageData(fmgPageValues.PART_QUESTIONS);
return partQuestionsFromPageData && Object.keys(partQuestionsFromPageData).length > 0;
}, },
}, handleAnswerUpdates(key, answer) { // only runs when all questions in a question-chain have been answered
watch: { // when selectedAnswers updates, user has completed this part's question chain and has a final answer
selectedModel(model) { // (does not get run for each invididual question's answer, only when
this.partsQuestionsData[model.partIndex].answerData = { // all relevent questions for the current part have been answered)
answerResult: model.answerResult, const glassPartWithAnswer = this.partsQuestionsData[answer.partIndex];
answeredQuestions: model.answeredQuestions, const completeAnsweredQuestions = answer.answeredQuestions ? [...answer.answeredQuestions] : [];
const answeredQuestionIndexes = [];
// since user has answered a question differently than anything that was preloaded,
// then clear out the preloaded answers
this.selectedAnswers = {};
// examine all the answers returned that were part of the user's journey through question-chain
// loop through every answered question on currently answered glass part
answer.answeredQuestions?.forEach((aq) => {
// gather all the question numbers of the answered questions
answeredQuestionIndexes.push(aq.questionNum);
const answeredQuestionText = aq.questionText.toUpperCase();
const answeredQuestionAnswer = aq.selectedAnswerText.toUpperCase();
// HANDLE DUPLICATE QUESTIONS
// loop through all glass parts data (but only examining parts after currently being answered part)
this.partsQuestionsData.forEach((glassPart, i) => {
// restrict duplicate logic to only parts that follow the currently being answered part
if (i > answer.partIndex) {
// loop through this glass part's part questions, looking for a questionText match
glassPart.partQuestions.forEach((pq, pqIndex) => {
// clear out any previously set answers
//delete pq.answerSelected;
pq.answerSelected = null;
// does pq.questionText match answeredQuestionText? (aka do we have a duplicate question?)
if (pq.questionText.toUpperCase() === answeredQuestionText) {
// which one of this partQuestions' answers matches our answer?
let matchedAnswer;
pq.answers.forEach((ans, ansIndex) => {
// delete pq.answers[ansIndex].selected;
pq.answers[ansIndex].selected = null;
if (ans.answerText.toUpperCase() === answeredQuestionAnswer) {
matchedAnswer = ans;
pq.answers[ansIndex].selected = true;
}
});
if (matchedAnswer) {
const thisAnsweredPartQuestion = glassPart.partQuestions[pqIndex];
// remove answerData from this glass part
// delete glassPart.answerData;
glassPart.answerData = null;
// delete glassPart.isSuppressedPart;
glassPart.isSuppressedPart = null;
// Update the key to re-render this part's question-chain component
this.partsQuestionsData[i].key = this.partsQuestionsData[i].glassLocation + this.partsQuestionsData[i].glassName + Date.now().toString();
// handle suppressing downstream in this question chain
const rejectedAnswer = thisAnsweredPartQuestion.answers.filter((ans) => {
return !ans.selected;
});
if (rejectedAnswer[0].nextQuestionSequence) {
glassPart.partQuestions[rejectedAnswer[0].nextQuestionSequence - 1].suppressDuplicateQuestion = true;
}
if (matchedAnswer.nextQuestionSequence) {
// ensure that accepted answer is NOT suppressed
glassPart.partQuestions[matchedAnswer.nextQuestionSequence - 1].suppressDuplicateQuestion = null;
}
// handle suppressing upstream in this question chain
glassPart.partQuestions.forEach((q) => {
q.answers.forEach((thisAns) => {
// restore any of the answers that formerly led to the duplicated question
if (thisAns.originalNextQuestionSequence === pq.questionSequence) {
// restore original nextQuestionSequence
thisAns.nextQuestionSequence = thisAns.originalNextQuestionSequence;
this.originalNextQuestionSequence = null;
// restore original answerResult
if (thisAns.originalAnswerResult) {
thisAns.answerResult = thisAns.originalAnswerResult;
thisAns.originalAnswerResult = null;
}
}
// search for any of the answers that lead to the duplicated question
if (thisAns.nextQuestionSequence === pq.questionSequence) {
// update either the nextQuestionSequence or the answerResult
if (matchedAnswer.nextQuestionSequence) {
thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = matchedAnswer.nextQuestionSequence;
} else {
thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = null;
thisAns.originalAnswerResult = thisAns.originalAnswerResult || thisAns.answerResult;
thisAns.answerResult = matchedAnswer.answerResult;
}
}
});
});
// suppress current question
thisAnsweredPartQuestion.suppressDuplicateQuestion = true;
const thisGlassPart = "glassPart" + i;
if (this.foundDuplicateQuestions[thisGlassPart]) {
if (!this.foundDuplicateQuestions[thisGlassPart].includes(thisAnsweredPartQuestion.questionSequence)) {
this.foundDuplicateQuestions[thisGlassPart].push(thisAnsweredPartQuestion.questionSequence);
}
} else {
this.foundDuplicateQuestions[thisGlassPart] = [thisAnsweredPartQuestion.questionSequence];
}
// are there any questions left that are not suppressed?
const remainingQuestions = glassPart.partQuestions.filter((q) => {
return !q.suppressDuplicateQuestion;
});
if (remainingQuestions.length < 1) {
// this is the final answer for this glass part
// mark this part as completely answered by adding answerData
const answeredQuestionObj = {
questionText: pq.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: pq.questionSequence,
isDuplicateQuestion: pq.suppressDuplicateQuestion,
};
// set the answerData as 'already answered'
glassPart.answerData = {
answerResult: matchedAnswer.nextQuestionSequence ? matchedAnswer.nextQuestionSequence : matchedAnswer.answerResult,
answeredQuestions: [answeredQuestionObj],
};
// suppress this glassPart because it has an answer
glassPart.isSuppressedPart = true;
}
} // END of if (matchedAnswer)
}
});
// Update the key to re-render this part's question-chain component
this.partsQuestionsData[i].key = this.partsQuestionsData[i].glassLocation + this.partsQuestionsData[i].glassName + Date.now().toString();
}
});
});
// look through all (this part's) part questions for any duplicates that were suppressed;
// add them to the list of answered questions if found
// EX answeredQuestionIndexes: [1,5,11,13]
// EX this.foundDuplicateQuestions = {
// "glassPart1": [1],
// "glassPart2": [7, 10]
// };
const thisPartsDupes = this.foundDuplicateQuestions["glassPart" + answer.partIndex];
thisPartsDupes?.forEach((dupe) => {
// dupe is a single integer
const dupeQuestion = glassPartWithAnswer.partQuestions[dupe - 1];
const dupeQuestionAnswer = dupeQuestion.answers.find((q) => q.selected === true);
glassPartWithAnswer.partQuestions.forEach((q) => {
let includeThisDupeInAnsweredQuestions = false;
// did one of the answers of this question point to the duplicated question?
q.answers.forEach((a) => {
if ((dupe === a.originalNextQuestionSequence) &&
(answeredQuestionIndexes.includes(q.questionSequence)) &&
(a.answerText.toUpperCase() === dupeQuestionAnswer.answerText.toUpperCase())) {
includeThisDupeInAnsweredQuestions = true;
}
});
// is this q.questionSequence listed as the duplicated question's nextQuestionSequence?
if ((q.questionSequence === dupeQuestionAnswer.nextQuestionSequence) && (answeredQuestionIndexes.includes(q.questionSequence))) {
includeThisDupeInAnsweredQuestions = true;
}
if (includeThisDupeInAnsweredQuestions) {
completeAnsweredQuestions.push({
questionNum: dupeQuestion.questionSequence,
questionText: dupeQuestion.questionText,
selectedAnswerText: dupeQuestionAnswer.answerText,
isDuplicateQuestion: dupeQuestion.suppressDuplicateQuestion,
});
}
});
});
// make sure there are no duplicated dupes in the list...
const foundInCompleteAnsweredQuestions = new Set();
let filteredCompleteAnsweredQuestions = completeAnsweredQuestions.filter(el => {
const duplicate = foundInCompleteAnsweredQuestions.has(el.questionText);
foundInCompleteAnsweredQuestions.add(el.questionText);
return !duplicate;
});
filteredCompleteAnsweredQuestions = filteredCompleteAnsweredQuestions.sort((a,b) => a.questionNum - b.questionNum);
// set final answer data for the current answered glass part
glassPartWithAnswer.answerData = {
answerResult: answer.answerResult,
answeredQuestions: filteredCompleteAnsweredQuestions,
} }
this.currentPartNum = model.partIndex + 1;
// this part has been fully answered, so advance to next part's question chain
for (let i = answer.partIndex + 1; i < this.partsQuestionsData.length; i++) {
// if this part has not yet been fully answered, then make it the current part
if (!this.partsQuestionsData[i].answerData?.answerResult) {
this.currentPartNum = i;
break;
}
}
},
LoadInitialPartsData() {
// are there alreadyAnsweredQuestions?
const alreadyAnsweredQuestions = store.getters.damage.partQuestionAnswers;
this.partsQuestionsData = this.partsQuestionsFromApi.map((part, i) => {
part.key = part.glassLocation + "-" + part.glassName;
this.selectedAnswers[part.key] = [];
alreadyAnsweredQuestions?.forEach((savedPart) => {
if (!savedPart.glassLocation || !savedPart.glassName || !savedPart.answeredQuestions || !savedPart.result) {
return;
}
if (part.glassLocation === savedPart.glassLocation && part.glassName === savedPart.glassName) {
let answerString = "";
// matches; loop through answeredQuestions for matches
savedPart.answeredQuestions.forEach((aq) => {
if (!aq.questionNum || !aq.selectedAnswerText) {
return;
}
// determine which answer was previously chosen
const theAns = part.partQuestions[aq.questionNum-1].answers.find((a) => {
return a.answerText.toUpperCase() === aq.selectedAnswerText.toUpperCase();
});
if (theAns.nextQuestionSequence) {
answerString = `${aq.questionNum}|nextQuestion|${theAns.nextQuestionSequence}|${theAns.answerText}`
} else {
answerString = `${aq.questionNum}|answer|${theAns.answerResult}|${theAns.answerText}`
}
// mark this partQuestion as answered (question-chain will read this)
part.partQuestions[aq.questionNum-1].answerSelected = answerString;
// mark this partQuestion as duplicate if it is (question-chain will read this)
if (aq.isDuplicateQuestion) {
part.partQuestions[aq.questionNum-1].isDuplicateQuestion = true;
}
});
// advance the currentPartNum
this.currentPartNum = i;
// add answerData to current part
part.answerData = {
answerResult: savedPart.result,
answeredQuestions: savedPart.answeredQuestions
}
}
});
// Set up watch for each set of part questions, which gets updated when all questions for a part have been answered
this.$watch("selectedAnswers." + part.key, (newValue) => {
if (newValue) {
this.handleAnswerUpdates(part.key, newValue);
}
}, {deep: true})
return part;
});
// this block is purely for handling duplicate questions
alreadyAnsweredQuestions?.forEach((savedPart, partIndex) => {
savedPart.answeredQuestions.forEach((aq) => {
if (aq.isDuplicateQuestion) {
// find this one in partsQuestionData
const dupedPartsQuestion = this.partsQuestionsData[partIndex].partQuestions[aq.questionNum - 1];
dupedPartsQuestion.suppressDuplicateQuestion = true;
const dupedPartsQuestionAnswer = dupedPartsQuestion.answers.filter((ans) => {
return ans.answerText.toUpperCase() === aq.selectedAnswerText.toUpperCase();
})[0];
let isDupedPartsQuestionFirst;
if (aq.questionNum === 1) { isDupedPartsQuestionFirst = true }
this.partsQuestionsData.forEach((glassPart, i) => {
// loop through this glass part's part questions, looking for a questionText match
glassPart.partQuestions.forEach((pq, pqIndex) => {
// if this dupedQ is the first in the array then
// suppress all questions for this part up until the answer's nextQuestionSequence
if (isDupedPartsQuestionFirst &&
dupedPartsQuestionAnswer.nextQuestionSequence &&
pq.questionSequence < dupedPartsQuestionAnswer.nextQuestionSequence) {
pq.suppressDuplicateQuestion = true;
}
pq.answers.forEach((thisAns) => {
if (thisAns.nextQuestionSequence === aq.questionNum) {
// update either the nextQuestionSequence or the answerResult
if (dupedPartsQuestionAnswer.nextQuestionSequence) {
thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = dupedPartsQuestionAnswer.nextQuestionSequence;
// update this change to answerSelected
pq.answerSelected = `${pq.questionSequence}|nextQuestion|${thisAns.nextQuestionSequence}|${thisAns.answerText}`;
} else {
thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = null;
thisAns.originalAnswerResult = thisAns.originalAnswerResult || thisAns.answerResult;
thisAns.answerResult = dupedPartsQuestionAnswer.answerResult;
}
}
})
});
});
}
})
});
}, },
}, },
components: { components: {
@ -206,18 +498,19 @@ export default {
questionChain, questionChain,
funnelSubHeader, funnelSubHeader,
funnelFooter, funnelFooter,
loadingModal,
Form, Form,
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.part-questions { .part-questions {
.question-text { .question-text {
margin-bottom: .5rem; margin-bottom: .5rem;
span { span {
text-align: left; text-align: left;
} }
} }
} }
</style> </style>

View file

@ -1,6 +1,6 @@
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div v-if="isAvailable && this.answersToDisplay.length > 0" class="replace-options-question" :class="this.answersToDisplay.length < 2 ? 'd-none' : ''" aria-live="polite"> <div v-if="shouldDisplayReplaceOptionsQuestion" class="replace-options-question" :class="this.answersToDisplay.length < 2 ? 'd-none' : ''" aria-live="polite">
<buttonQuestion <buttonQuestion
isWide isWide
:questionText="questionText" :questionText="questionText"
@ -80,11 +80,19 @@ export default ({
return ans; return ans;
}); });
}, },
shouldDisplayReplaceOptionsQuestion() {
return this.isAvailable && this.answersToDisplay.length > 0
}
}, },
watch: { watch: {
isAvailable(val) { isAvailable(val) {
// CHECK TO UPDATE SELECTED VALUES WHEN ISAVAILABLE IS TRUE // CHECK TO UPDATE SELECTED VALUES WHEN ISAVAILABLE IS TRUE
val && this.updateSelectedValues(); val && this.updateSelectedValues();
},
shouldDisplayReplaceOptionsQuestion(shouldDisplayReplaceOptionsQuestion) {
if (!shouldDisplayReplaceOptionsQuestion) {
this.selectedValues = [];
}
} }
}, },
components: { components: {

View file

@ -46,6 +46,7 @@ import store from "@/store";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("damage-side-required", required(errorMessages.DAMAGE_SIDE_REQUIRED)); defineRule("damage-side-required", required(errorMessages.DAMAGE_SIDE_REQUIRED));
@ -112,7 +113,7 @@ export default ({
this.selectedValues = this.getSideDoorReplacementOptions(this.selectedValues.selectedDoorSides, this.selectedValues.selectedDriverSideReplaceOptions, newValue); this.selectedValues = this.getSideDoorReplacementOptions(this.selectedValues.selectedDoorSides, this.selectedValues.selectedDriverSideReplaceOptions, newValue);
} }
}, },
answersToDisplay(){ answersToDisplay(){
const filteredAnswers = Array.isArray(this.answersFromCms) const filteredAnswers = Array.isArray(this.answersFromCms)
? this.answersFromCms.filter(ans => ? this.answersFromCms.filter(ans =>
{ {
@ -128,10 +129,10 @@ export default ({
}); });
}, },
isDriverSideReplaceOptionsQuestionAvailable(){ isDriverSideReplaceOptionsQuestionAvailable(){
return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes("DriverSide") && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes("SideDoor"))); return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes(damageLocationsSelected.DRIVERSIDE) && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes(damageLocationsSelected.SIDEDOOR)));
}, },
isPassengerSideReplaceOptionsQuestionAvailable(){ isPassengerSideReplaceOptionsQuestionAvailable(){
return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes("PassengerSide") && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes("SideDoor"))); return (Array.isArray(this.selectedDoorSidesValues) && this.selectedDoorSidesValues.includes(damageLocationsSelected.PASSENGERSIDE) && (Array.isArray(this.selectedDamageLocations) && this.selectedDamageLocations.includes(damageLocationsSelected.SIDEDOOR)));
}, },
}, },
components: { components: {

View file

@ -257,7 +257,7 @@ describe("vehicle-damage.vue", () => {
}); });
describe("alert", () => { describe("alert", () => {
test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be visible", () => { test("when displayVehicleChangeAlert router params is true, the alert: 'vehicleChangeAlert' should be rendered", () => {
// Arrange & Act // Arrange & Act
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
@ -269,10 +269,10 @@ describe("vehicle-damage.vue", () => {
} }
}); });
// Assert // Assert
expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).isVisible()).toBe(true); expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).exists()).toBe(true);
}); });
test("when displayVehicleChangeAlert router params is false, the alert: 'vehicleChangeAlert' should not be visible", () => { test("when displayVehicleChangeAlert router params is false, the alert: 'vehicleChangeAlert' should not be rendered", () => {
// Arrange & Act // Arrange & Act
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
@ -284,10 +284,10 @@ describe("vehicle-damage.vue", () => {
} }
}); });
// Assert // Assert
expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).isVisible()).toBe(false); expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).exists()).toBe(false);
}); });
test("when displayVehicleChangeAlert router params is undefined, the alert: 'vehicleChangeAlert' should not be visible", () => { test("when displayVehicleChangeAlert router params is undefined, the alert: 'vehicleChangeAlert' should not be rendered", () => {
// Arrange & Act // Arrange & Act
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
@ -299,7 +299,7 @@ describe("vehicle-damage.vue", () => {
} }
}); });
// Assert // Assert
expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).isVisible()).toBe(false); expect(wrapper.findComponent({ ref: 'vehicleChangeAlert' }).exists()).toBe(false);
}) })
}); });

View file

@ -10,11 +10,10 @@
<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">
<alert <alert ref="vehicleChangeAlert"
ref="vehicleChangeAlert" v-if="shouldDisplayVehicleChangeAlert"
class="mt-5 mb-0" class="mt-5 mb-0"
cmsWidgetName="VehicleChangeAlert" cmsWidgetName="VehicleChangeAlert"
v-show="shouldDisplayVehicleChangeAlert"
alertClass="alert-warning" alertClass="alert-warning"
:isDismissible="false" :isDismissible="false"
/> />
@ -32,9 +31,9 @@
:selectedDamageLocations="selectedDamageLocations" :selectedDamageLocations="selectedDamageLocations"
/> />
<alert <alert
v-if="hasRepairReplaceConflict"
class="my-5" class="my-5"
cmsWidgetName="HasReplacementConflict" cmsWidgetName="HasReplacementConflict"
v-show="hasRepairReplaceConflict"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />
@ -156,7 +155,7 @@ export default {
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
if(store.getters.vehicle.carId){ if (store.getters.vehicle.carId) {
return true; return true;
} }
return false; return false;

View file

@ -17,7 +17,6 @@
<script> <script>
import buttonQuestion from "@/common-components/button-question/button-question"; import buttonQuestion from "@/common-components/button-question/button-question";
import store from "@/store";
export default ({ export default ({
name: "windshieldDamageTypeQuestion", name: "windshieldDamageTypeQuestion",

View file

@ -1,19 +1,19 @@
<template> <template>
<div class="windshield-options"> <div class="windshield-options">
<windshieldDamageTypeQuestion cmsWidgetName="WindshieldDamageTypeQuestion" <windshieldDamageTypeQuestion cmsWidgetName="WindshieldDamageTypeQuestion"
:isAvailable=isWindshieldDamageLocation :isAvailable="isWindshieldDamageLocation"
:suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError" :suppressError="hasRepairReplaceConflict || showNoReplacementAvailableError"
groupName="WindshieldDamageTypeQuestion" groupName="WindshieldDamageTypeQuestion"
v-model="selectedWindshieldDamageTypeValue" v-model="selectedWindshieldDamageTypeValue"
:validationRules="windshieldDamageTypeQuestionValidationRules" :validationRules="windshieldDamageTypeQuestionValidationRules"
/> />
<alert <alert
v-if="showNoReplacementAvailableError"
class="my-3" class="my-3"
cmsWidgetName="NoReplacementAvailableError" cmsWidgetName="NoReplacementAvailableError"
v-if="showNoReplacementAvailableError"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />
<windshieldChipCountQuestion cmsWidgetName="WindshieldChipCountQuestion" <windshieldChipCountQuestion cmsWidgetName="WindshieldChipCountQuestion"
:isAvailable="isRepairOptionSelected && !hasRepairReplaceConflict" :isAvailable="isRepairOptionSelected && !hasRepairReplaceConflict"
groupName="WindshieldChipCountQuestion" groupName="WindshieldChipCountQuestion"
@ -30,9 +30,9 @@
isRequired isRequired
/> />
<alert <alert
v-if="hasSplitSingleConflict"
class="mt-5" class="mt-5"
cmsWidgetName="SplitSingleConflict" cmsWidgetName="SplitSingleConflict"
v-if="hasSplitSingleConflict"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />
@ -55,7 +55,7 @@ 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", (selectedWindshieldDamageType, [selectedDamageLocations]) => { defineRule("check-for-repair-and-replace", (selectedWindshieldDamageType, selectedDamageLocations) => {
return selectedWindshieldDamageType.toString() != damageLocationsSelected.REPAIR || return selectedWindshieldDamageType.toString() != damageLocationsSelected.REPAIR ||
!selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD) || !selectedDamageLocations.includes(damageLocationsSelected.WINDSHIELD) ||
selectedDamageLocations.length === 1; selectedDamageLocations.length === 1;
@ -137,15 +137,16 @@ export default ({
} }
}, },
isWindshieldDamageLocation() { isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages => return this.selectedDamageLocations.some(selectedDamageLocation => selectedDamageLocation === damageLocationsSelected.WINDSHIELD);
{
return Boolean(selectedDamages.toUpperCase() === "WINDSHIELD");
});
}, },
isRepairOptionSelected(){ isRepairOptionSelected(){
if (!this.selectedWindshieldDamageTypeValue) return false;
return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPAIR && this.isWindshieldDamageLocation; return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPAIR && this.isWindshieldDamageLocation;
}, },
isReplaceOptionSelected(){ isReplaceOptionSelected(){
if (!this.selectedWindshieldDamageTypeValue) return false;
return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPLACE && this.isWindshieldDamageLocation; return this.selectedWindshieldDamageTypeValue === damageLocationsSelected.REPLACE && this.isWindshieldDamageLocation;
}, },
isWindshieldReplaceAvailable() { isWindshieldReplaceAvailable() {

View file

@ -76,7 +76,7 @@ export default {
); );
}, },
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
if (store.getters.vehicle.year){ if (store.getters.vehicle.year){
return true; return true;
} }
return false; return false;

View file

@ -9,7 +9,10 @@ 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 baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
@ -21,6 +24,10 @@ jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(), fetchCmsContentForPage: jest.fn(),
})); }));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn()
}));
// Mock store // Mock store
jest.mock("@/store", () => { return {}; }, { virtual: true }); jest.mock("@/store", () => { return {}; }, { virtual: true });
@ -140,7 +147,7 @@ describe("vehicle-parts.vue", () => {
test("Initial data, should populate this.glassParts", async () => { test("Initial data, should populate this.glassParts", async () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse); store.getters.pageData.mockReturnValue(basePartResponse);
store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM' }] } store.getters.lineItems = { glassParts: [{ partNumber: 'DB12209YPYNOEM' }] }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
@ -173,12 +180,8 @@ describe("vehicle-parts.vue", () => {
expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } }); expect(wrapper.vm.glassParts).toEqual({ "Rear-Stationary": { "Rear": ['DB12209YPYNOEM'] } });
}); });
test("BackButtonAction triggers a router.navigate change", async () => { test("User had part questions > BackButtonAction triggers a router.navigate change with correct scenario", async () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: null }
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
@ -191,7 +194,25 @@ describe("vehicle-parts.vue", () => {
} }
}, },
store: { store: {
getters: store.getters getters: {
pageData: () => {
return {
partsOrQuestions: [
{
glassName: "Stationary",
glassLocation: "Rear",
parts: null,
partQuestions: [{
testProperty: "some value"
}]
}
]
}
},
lineItems: {
glassParts: null
}
}
}, },
} }
}); });
@ -207,17 +228,12 @@ describe("vehicle-parts.vue", () => {
wrapper.vm.backButtonAction(); wrapper.vm.backButtonAction();
//Assert //Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.CLICKED_BACK_TO_GO_TO_PART_QUESTIONS, wrapper.vm.$route);
}); });
test("ForwardButtonAction triggers a router.navigate change and saves selected parts to store", async () => { test("User did not have part questions > BackButtonAction triggers a router.navigate change with correct scenario", async () => {
//Arrange //Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: {} }
store.commit = jest.fn();
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mountOptionsMockData: { mountOptionsMockData: {
router: { router: {
@ -229,6 +245,85 @@ describe("vehicle-parts.vue", () => {
fmgPage: 'vehicle-parts', fmgPage: 'vehicle-parts',
} }
}, },
store: {
getters: {
pageData: () => basePartResponse,
lineItems: {
glassParts: null
}
}
},
}
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP, wrapper.vm.$route);
});
test("ForwardButtonAction triggers a router.navigate change if there are child part questions", async () => {
//Arrange
store.getters.pageData.mockReturnValueOnce({
partsOrQuestions: [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
"partNumber": "FW03861GTYN",
"description": "rain sensor, heated glass, auto dimming mirror, solar, 3rd visor band, condensation sensor",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"recalibrationType": null,
"childParts": null,
"childPartQuestions": [
{
"questionSequence": 1,
"questionText": "Does the rubber seal around your windshield have a chrome strip running through it?",
"answers": [
{
"answerResult": "WKT D1106 C",
"answerText": "Yes",
"nextQuestionSequence": null
},
{
"answerResult": "WKT D1106 B",
"answerText": "No",
"nextQuestionSequence": null
}
]
}
]
}
],
partQuestions: null
}
]
});
store.getters.lineItems = { glassParts: {} }
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn()
},
route: {
query: {
fmgPage: 'vehicle-parts',
}
},
store: { store: {
getters: store.getters, getters: store.getters,
commit: store.commit commit: store.commit
@ -236,6 +331,70 @@ describe("vehicle-parts.vue", () => {
} }
}); });
wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'FW03861GTYN' } } });
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
test("ForwardButtonAction triggers a router.navigate change if there are capability questions", async () => {
//Arrange
store.getters.pageData.mockReturnValueOnce({
partsOrQuestions: [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
partNumber: "DB12209GTYN",
description: "heated glass, solar, 1 hole",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: true,
childParts: null
}
],
partQuestions: null
}
]
});
store.getters.lineItems = { glassParts: {} }
store.getters.vehicle = { carId: "TEST_CAR_ID" }
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn()
},
route: {
query: {
fmgPage: 'vehicle-parts',
}
},
store: {
getters: store.getters,
commit: store.commit
},
actionList: [
{
actionName: storeActions.GET_CAPABILITY_QUESTIONS,
data: []
}
]
}
});
wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'DB12209GTYN' } } }); wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'DB12209GTYN' } } });
//Act //Act
@ -251,6 +410,51 @@ describe("vehicle-parts.vue", () => {
//Assert //Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
test("ForwardButtonAction saves selected parts to store if no molding or capability questions", async () => {
//Arrange
store.getters.pageData.mockReturnValueOnce(basePartResponse);
store.getters.lineItems = { glassParts: {} }
store.getters.vehicle = { carId: "TEST_CAR_ID" }
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn()
},
route: {
query: {
fmgPage: 'vehicle-parts',
}
},
store: {
getters: store.getters,
commit: store.commit
},
navigateToHeritageFunnel: jest.fn()
}
});
wrapper.vm.$store.commit = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
wrapper.setData({ glassParts: { "Rear-Stationary": { partNumber: 'DB12209GTYN' } } });
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$store.commit).toHaveBeenCalled();
expect(navigateToHeritageFunnel).toHaveBeenCalled();
});
}); });
function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} }) { function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} }) {
@ -288,6 +492,7 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -1,6 +1,7 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles vehicle-parts"> <div class="page-container-grouped-styles vehicle-parts">
<loadingModal ref="loadingModal"/>
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" /> <funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner <vehicleBanner
ref="vehicleBanner" ref="vehicleBanner"
@ -49,217 +50,182 @@
</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 import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper"; // Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper"; import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form } from "vee-validate"; import { Form } from "vee-validate";
import { storeActions } from "@/constants/store-actions"; import store from "@/store";
import { storeMutations } from "@/constants/store-mutations.js";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { assertParenthesizedExpression } from "@babel/types";
export default { export default {
name: "vehicle-parts", name: "vehicle-parts",
mixins: [vehicleQuestionsMixin],
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);
// Glass Part Question dynamic component // Glass Part Question dynamic component
Object.keys(vm.$refs) Object.keys(vm.$refs)
.filter( .filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
(r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined .forEach((c) =>
)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({ vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget, ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget, FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
}) })
); );
}); });
}, },
data() { data() {
return { return {
glassParts: {}, glassParts: {},
alertWidgetData: Object, alertWidgetData: Object,
alreadyPopulatedPartsData: {}, alreadyPopulatedPartsData: {},
}; };
},
components: {
Form,
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
}, },
computed: { computed: {
isForwardActionDisabled() { isForwardActionDisabled() {
return ( return (
Object.keys(this.matchedParts).length !== this.selectedGlassPartNumbers.length !== this.PartsFromApi.partsOrQuestions.length
this.PartsFromApi.partsOrQuestions.length );
); },
}, selectedGlassPartNumbers () {
matchedParts() { // Compile all selected parts from the page.
const matchedParts = []; const numberArray = [];
for (let glassPart of Object.values(this.glassParts)) {
if (glassPart?.partNumber) { numberArray.push(glassPart.partNumber) }
}
return numberArray;
},
PartsOrQuestions() {
const partsData = this.PartsFromApi;
// Match them to the parts from the API. // Map API result data, to vehicle-parts data structure
this.PartsFromApi.partsOrQuestions.forEach((part) => { const mappedData = partsData.partsOrQuestions.map((g) => {
const selectedPartForGlassLocationAndName = return {
this.glassParts[`${part.glassLocation}-${part.glassName}`]; glassName: g.glassName,
const selectedPartData = part.parts.filter( glassLocation: g.glassLocation,
(part) => colorAnswers: g.parts?.reduce((arr, p) => {
selectedPartForGlassLocationAndName && arr.push({
part.partNumber == selectedPartForGlassLocationAndName?.partNumber ColorAnswerText: p.color,
)[0]; FeatureAnswers: [
{
FeatureAnswerText: p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
};
});
if (selectedPartData) matchedParts.push(selectedPartData); return mappedData;
}); },
return matchedParts; PartsFromApi() {
}, return this.$store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
PartsOrQuestions() { },
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure RefPrefix() {
const mappedData = partsData.partsOrQuestions.map((g) => { return "partQuestion";
return { },
glassName: g.glassName,
glassLocation: g.glassLocation,
colorAnswers: g.parts.reduce((arr, p) => {
arr.push({
ColorAnswerText: p.color,
FeatureAnswers: [
{
FeatureAnswerText:
p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
};
});
return mappedData;
},
PartsFromApi() {
return store.getters.pageData(fmgPageValues.VEHICLE_PARTS);
},
RefPrefix() {
return "partQuestion";
},
}, },
methods: { methods: {
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data) // Check if isRepair is populated and if the pageData we need is here (Parts data)
if ( return store.getters.damage.isRepair != null && store.getters.pageData(fmgPageValues.VEHICLE_PARTS) &&
store.getters.damage.isRepair != null && Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)).length !== 0;
Object.keys(store.getters.pageData(fmgPageValues.VEHICLE_PARTS)) },
.length !== 0 async forwardButtonAction() {
) { const matchedParts = [];
return true;
}
return false; // Match them to the parts from the API.
}, for (let [key, value] of Object.entries(
backButtonAction() { this.PartsFromApi.partsOrQuestions
this.$router.navigate( )) {
this.navigationScenarios.CLICKED_BACK, for (let [partKey, partValue] of Object.entries(value.parts)) {
this.$route const currentPart = this.PartsFromApi.partsOrQuestions[key].parts[partKey];
);
},
async forwardButtonAction() {
const selectedGlassPartNumbers = [];
const matchedParts = [];
// Compile all selected parts from the page. const isMatched = this.selectedGlassPartNumbers.some(
for (let [key, value] of Object.entries(this.glassParts)) { (p) => p === currentPart.partNumber
for (let [glassKey, glassValue] of Object.entries(value)) { );
selectedGlassPartNumbers.push(glassValue[0]);
}
}
// Match them to the parts from the API. if (isMatched) {
for (let [key, value] of Object.entries( matchedParts.push({
this.PartsFromApi.partsOrQuestions glassLocation: value.glassLocation,
)) { glassName: value.glassName,
for (let [partKey, partValue] of Object.entries(value.parts)) { parts: [currentPart]
const currentPart = });
this.PartsFromApi.partsOrQuestions[key].parts[partKey]; }
const isMatched = selectedGlassPartNumbers.some( }
(p) => p === currentPart.partNumber }
); // If no parts could be matched, throw an error (isForwardActionDisabled is based off of matchedParts)
if (this.isForwardActionDisabled) {
if (isMatched) { this.$refs.funnelFooter.removeLoader();
matchedParts.push(currentPart); throw new Error("Could not match any parts to the selected parts");
} }
}
}
// If no parts could be matched, throw an error. // Navigate to the next page
if (this.isForwardActionDisabled) { this.navigateForward(matchedParts);
this.$refs.funnelFooter.removeLoader(); },
throw new Error("Could not match any parts to the selected parts");
}
// Save parts to the store. LoadInitialPartsData() {
await this.dispatchStoreAction( const partsData = this.PartsFromApi;
storeActions.SAVE_GLASS_PARTS, const alreadyPopulatedPartsData =
matchedParts this.$store.getters.lineItems.glassParts === null
); ? {}
: this.$store.getters.lineItems.glassParts;
// Navigate to the next page. partsData.partsOrQuestions.map((g) => {
this.$router.navigate( // If the part is already populated, use the value from the store and populate the v-model.
this.navigationScenarios.SELECTED_PARTS, Object.keys(alreadyPopulatedPartsData).forEach((key) => {
this.$route const partNumber = alreadyPopulatedPartsData[key].partNumber;
); g.parts.forEach((p) => {
}, if (p.partNumber === partNumber) {
this.glassParts[g.glassLocation + "-" + g.glassName] = {
LoadInitialPartsData() { [g.glassLocation]: [partNumber],
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() { mounted() {
this.LoadInitialPartsData(); this.LoadInitialPartsData();
}, },
}; components: {
Form,
glassPartQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
funnelFooter,
alert,
loadingModal,
},
};
</script> </script>

View file

@ -27,6 +27,15 @@ jest.mock("@/store", () => ({
vehicle: { vehicle: {
model: "TL", model: "TL",
}, },
applicationUser:{
pageData: {
"part-questions": null,
"vehicle-make": {},
"vehicle-model": {},
"vehicle-style": {},
"vehicle-damage": {}
}
}
}, },
})); }));

View file

@ -3,14 +3,21 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car"> <div class="select-car">
<div class="select-car-form rounded text-center"> <div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage /> <vehicleBanner
cmsWidgetName="VehicleBannerWidget"
displayGenericVehicleImage
/>
<funnelSubHeader <funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget" cmsWidgetName="FunnelSubHeaderWidget"
:hasBackButton="true" :hasBackButton="true"
@click-event="backButtonAction" @click-event="backButtonAction"
/> />
<div class="fade-on-route-transition"> <div class="fade-on-route-transition">
<styleQuestion v-model="selectedStyle" ref="styleQuestion" cmsWidgetName="VehicleStyleQuestion" /> <styleQuestion
v-model="selectedStyle"
ref="styleQuestion"
cmsWidgetName="VehicleStyleQuestion"
/>
</div> </div>
</div> </div>
</div> </div>
@ -29,14 +36,19 @@ 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";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import router from "@/router";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
export default { export default {
name: "vehicle-style", name: "vehicle-style",
data() { data() {
return { return {
selectedStyle: null, selectedStyle: null
}; };
}, },
computed: {}, computed: {},
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -58,14 +70,31 @@ export default {
]; ];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
const visitedVehicleDamage = JSON.stringify(store.getters.applicationUser.pageData).indexOf(fmgPageValues.VEHICLE_DAMAGE) < 0 ? false : true;
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent); // If we have exactly one style then navigate directly to vehicle-damage
vm.$refs.styleQuestion.initializeComponent( if(resultMap.styleQuestionInitialData.length === 1 && !visitedVehicleDamage) {
resultMap.styleQuestionInitialData store.commit(storeMutations.UPDATE_STYLE, resultMap.styleQuestionInitialData[0]);
);
}); await store.dispatch(storeActions.SET_VEHICLE,
{
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
style: store.getters.vehicle.style,
});
//emulate selecting the vehicle style
router.overrideNavigation(navigationScenarios.SELECTED_STYLE, to, next);
} else{
next( (vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.styleQuestion.initializeComponent(resultMap.styleQuestionInitialData)
});
}
}, },
methods: { methods: {

View file

@ -15,6 +15,11 @@ import store from "@/store";
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
commit: jest.fn(), commit: jest.fn(),
dispatch: jest.fn(), dispatch: jest.fn(),
getters: {
applicationUser: {
experiments: []
}
}
})); }));
// Mock our module for promises. // Mock our module for promises.

View file

@ -31,6 +31,7 @@ import { settleAllPromises } from "@/helpers/layout-helper";
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 store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -48,14 +49,19 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData(); const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
// Log experiment exposure const experimentForLogging = store.getters.applicationUser.experiments.find(e => e.universeName === experimentUniverses.CONCEPT_FUNNEL);
const logExperimentExposurePromise = baseMixin.methods.dispatchStoreAction(storeActions.LOG_EXPERIMENT_EXPOSURE,
{ // If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
userId: getDeviceIdValue(), if (experimentForLogging !== undefined) {
sessionKey: getSessionKeyValue(), // Log experiment exposure
pageName: to.query.fmgPage, baseMixin.methods.dispatchStoreAction(storeActions.LOG_EXPERIMENT_EXPOSURE,
universeName: experimentUniverses.CONCEPT_FUNNEL {
}, false); userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
experiment: experimentForLogging
}, false);
}
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [ const promiseResultMap = [
@ -67,10 +73,6 @@ export default {
resultKey: "yearQuestionInitialData", resultKey: "yearQuestionInitialData",
promise: yearQuestionInitialDataPromise, promise: yearQuestionInitialDataPromise,
}, },
{
resultKey: "logExperimentExposure",
promise: logExperimentExposurePromise,
},
]; ];
let resultMap = await settleAllPromises(promiseResultMap); let resultMap = await settleAllPromises(promiseResultMap);

View file

@ -4,7 +4,9 @@ import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information
describe("vinInformation.vue", () => { describe("vinInformation.vue", () => {
it("Should return input class active if isActive is true", async () => { it("Should return input class active if isActive is true", async () => {
// Act // Act
const wrapper = shallowMount(vinInformation); const wrapper = shallowMount(vinInformation, {
mixins: [mockMixin]
});
await wrapper.setData({ await wrapper.setData({
isActive: true, isActive: true,
@ -16,3 +18,9 @@ describe("vinInformation.vue", () => {
expect(wrapper.vm.isActive).toEqual(false); expect(wrapper.vm.isActive).toEqual(false);
}); });
}); });
const mockMixin = {
methods: {
getCmsContent: jest.fn()
}
}

View file

@ -1,11 +1,15 @@
<template> <template>
<div class="vin-information"> <div class="vin-information">
<div class="vin-toggle d-inline-flex" :class="[isActive ? 'active' : '']" @click="toggleClass()"> <div class="vin-toggle d-inline-flex mt-2" :class="[isActive ? 'active' : '']" @click="toggleClass()">
<textLink :text="HeadlineText" linkType="text" href="#!" /> <textLink
linkType="text"
href="#!"
:text="WhereCanIFindMyVINHeadline"
/>
</div> </div>
<div class="vin-info"> <div class="vin-info">
<div class="mt-2" v-html="BodyText"></div> <div class="mt-2" v-html="WhereCanIFindMyVINBodyCopy"></div>
<img :src="OptionalImage" alt="" /> <img :src="WhereCanIFindMyVINImage" alt="" />
</div> </div>
</div> </div>
</template> </template>
@ -13,6 +17,8 @@
<script> <script>
import textLink from "@/ux-components/text-link/text-link"; import textLink from "@/ux-components/text-link/text-link";
//Supporting files
import { settleAllPromises } from "@/helpers/layout-helper";
export default { export default {
name: "vinInformation", name: "vinInformation",
components: { components: {
@ -21,15 +27,23 @@ export default {
data() { data() {
return { return {
isActive: false, isActive: false,
HeadlineText: "Where can I find my VIN?",
BodyText: "<p class='mb-2'>You can find your VIN in a few places:</p> <ol> <li>Driver side windshield</li> <li>Driver side doorjamb</li> <li>Vehicle registration card</li> <li>Auto insurance card or app</li> </ol>",
OptionalImage: "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/vin_location.svg?sfvrsn=63a9a2cf_3",
}; };
}, },
methods: { methods: {
toggleClass: function(event){ toggleClass: function(event){
this.isActive = !this.isActive; this.isActive = !this.isActive;
} }
},
computed: {
WhereCanIFindMyVINHeadline(){
return this.getCmsContent("WhereCanIFindMyVINToggle", "HeaderText");
},
WhereCanIFindMyVINBodyCopy(){
return this.getCmsContent("WhereCanIFindMyVINToggle", "BodyText");
},
WhereCanIFindMyVINImage(){
return this.getCmsContent("WhereCanIFindMyVINToggle", "Image");
},
} }
} }
@ -65,6 +79,7 @@ export default {
max-height: 500px; max-height: 500px;
transition: all 250ms ease-in; transition: all 250ms ease-in;
opacity: 1; opacity: 1;
visibility: visible;
} }
} }
.vin-info { .vin-info {
@ -72,8 +87,12 @@ export default {
transition: all 250ms ease-out; transition: all 250ms ease-out;
overflow: hidden; overflow: hidden;
opacity: 0; opacity: 0;
visibility: hidden;
p {
margin-bottom: .5rem;
}
img { img {
max-width: 420px; max-width: 100%;
width: 117%; width: 117%;
height: auto; height: auto;
} }

View file

@ -178,7 +178,7 @@ export default {
customAlertData: {}, customAlertData: {},
previouslyEnteredCarId: '', previouslyEnteredCarId: '',
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0, vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: true,
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
displayNonServiceableZipAlert: false, displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false, displayVinNotFoundAlert: false,

View file

@ -2,8 +2,9 @@ import { storeActions } from "@/constants/store-actions";
import { setCookieProperties, getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper"; import { setCookieProperties, getDeviceIdValue, getSessionIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics"; import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes } from "@/constants/analytics";
import { cookieNames } from "@/constants/cookie-names"; import { cookieNames } from "@/constants/cookie-names";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -19,6 +20,7 @@ export default {
action: '', action: '',
event: pageEvent, event: pageEvent,
shouldUseSessionId: false, shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false); baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
@ -37,18 +39,20 @@ export default {
label: label, label: label,
value: value, value: value,
shouldUseSessionId: false, shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false); baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false);
}, },
pushEventToGA(category, action, label, pushToLogApp = false) { pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const labelToLog = getValueToLog(label, valueToLogType);
const eventToBePushed = { const eventToBePushed = {
'event': GaEvents.GENERIC_EVENT, 'event': GaEvents.GENERIC_EVENT,
'category': category, 'category': category,
'action': action, 'action': action,
'label': label, 'label': labelToLog,
'value': undefined, 'value': undefined,
'path': `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}` 'path': `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}`
} }
@ -56,7 +60,7 @@ export default {
pushToDataLayerIfDefined(eventToBePushed); pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) { if (pushToLogApp) {
this.logCustomEvent(category, action, label, undefined); this.logCustomEvent(category, action, labelToLog, undefined);
} }
}, },
@ -74,7 +78,8 @@ export default {
this.logPageView(analyticsPageEvents.ENTRY); this.logPageView(analyticsPageEvents.ENTRY);
}, },
pushExperimentsToDataLayer(experiments) { pushExperimentsToDataLayer() {
const experiments = store.getters.applicationUser.experiments;
experiments?.forEach(exp => { experiments?.forEach(exp => {
// Set Google Dimension Index based on experiment settings. // Set Google Dimension Index based on experiment settings.
@ -97,7 +102,7 @@ export default {
pushToDataLayerIfDefined(experimentWithDimension); pushToDataLayerIfDefined(experimentWithDimension);
}); });
}, },
prependActionToMethod(object, method, actionToPrepend) { prependActionToMethod(object, method, actionToPrepend) {
const baseMethodName = method.name.startsWith('bound ') ? method.name.substring(6) : method.name ; const baseMethodName = method.name.startsWith('bound ') ? method.name.substring(6) : method.name ;
const baseMethod = object[baseMethodName]; const baseMethod = object[baseMethodName];
@ -121,10 +126,14 @@ export default {
if (response.data) { if (response.data) {
if (response.data.sessionKey && skey === 0) { if (response.data.sessionKey && skey === 0) {
setCookieProperties({ [cookieNames.SESSION_KEY]: response.data.sessionKey}); setCookieProperties({ [cookieNames.SESSION_KEY]: response.data.sessionKey}, {
useDefaultFunnelCookieAttributes: false
});
} }
if (response.data.sessionId && sid === '00000000-0000-0000-0000-000000000000') { if (response.data.sessionId && sid === '00000000-0000-0000-0000-000000000000') {
setCookieProperties({ [cookieNames.SESSION_ID]: response.data.sessionId}); setCookieProperties({ [cookieNames.SESSION_ID]: response.data.sessionId}, {
maxAge: 60 * 30 // 30 minutes
});
} }
} }
}, },
@ -145,6 +154,9 @@ export default {
}, },
GaLabels() { GaLabels() {
return GaLabels; return GaLabels;
},
ValueToLogTypes() {
return ValueToLogTypes;
} }
}, },
}; };
@ -163,4 +175,11 @@ function getPageNameByQueryString() {
} else { } else {
return ''; return '';
} }
}
function getValueToLog(value, valueToLogType) {
if (valueToLogType != null && valueToLogType === ValueToLogTypes.LAST_5 ) {
return value.slice(-5);
}
return value;
} }

View file

@ -1,7 +1,8 @@
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js"; import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents } from "@/constants/analytics"; import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes } from "@/constants/analytics";
import store from "@/store";
describe("analyticsMixin.js", () => { describe("analyticsMixin.js", () => {
test("logPageView: calls dispatch with type and payload", () => { test("logPageView: calls dispatch with type and payload", () => {
@ -39,21 +40,71 @@ describe("analyticsMixin.js", () => {
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("pushEventToGA, should call logEvent too", () => { test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => {
// Arrange // Arrange
window.dataLayer = [];
const mockData = { const mockData = {
actionList: [{ actionList: [{
actionName: storeActions.LOG_CUSTOM_EVENT actionName: storeActions.LOG_CUSTOM_EVENT
}], }],
} }
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
var mockDataLayer = [];
mockDataLayer.push({
event: 'event',
category: 'category',
action: 'action',
label: 'label',
value: undefined,
path: '/fmg/?fmgPage='
});
// Act // Act
analyticsMixin.methods.pushEventToGA('category', 'action', 'label', true); analyticsMixin.methods.pushEventToGA('category', 'action', 'label', true);
// Assert // Assert
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
});
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", () => {
// Arrange
window.dataLayer = [];
var expectedDataLayer = [];
expectedDataLayer.push({
event: 'event',
category: 'category',
action: 'action',
label: '33333',
value: undefined,
path: '/fmg/?fmgPage='
});
// Act
analyticsMixin.methods.pushEventToGA('category', 'action', '1111122222333333', false, ValueToLogTypes.LAST_5);
// Assert
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
});
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 logs only the last 3 characters for a 3 character string", () => {
// Arrange
window.dataLayer = [];
var expectedDataLayer = [];
expectedDataLayer.push({
event: 'event',
category: 'category',
action: 'action',
label: '111',
value: undefined,
path: '/fmg/?fmgPage='
});
// Act
analyticsMixin.methods.pushEventToGA('category', 'action', '111', false, ValueToLogTypes.LAST_5);
// Assert
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
}); });
test("Experiments, should push to dataLayer with default Google Custom Dimension Index", () => { test("Experiments, should push to dataLayer with default Google Custom Dimension Index", () => {
@ -69,6 +120,21 @@ describe("analyticsMixin.js", () => {
} }
] ]
// Mock store
jest.mock("@/store", () => { return {}; }, { virtual: true });
store.getters = {
applicationUser: {
experiments: [
{
settings: {},
variationName: 'test',
universeName: 'testUniverse'
}
]
}
};
// Act // Act
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData); analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);
@ -97,6 +163,20 @@ describe("analyticsMixin.js", () => {
} }
] ]
// Mock store
jest.mock("@/store", () => { return {}; }, { virtual: true });
store.getters = {
applicationUser: {
experiments: [
{
settings: { "Google Custom Dimension Index": "5" },
variationName: 'test',
universeName: 'testUniverse'
}
]
}
};
// Act // Act
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData); analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);

View file

@ -69,7 +69,10 @@ export default {
}, },
dynamicStrings(){ dynamicStrings(){
return dynamicStrings; return dynamicStrings;
} },
cssClassNameForCmsWidget(){
return "widget-name-" + this.cmsWidgetName;
},
}, },
}; };

View file

@ -0,0 +1,15 @@
import store from "@/store";
export default {
methods: {
hasSettingEqualTo(settingName, settingValue) {
return store.getters.experimentSettings[settingName] === settingValue;
},
hasSetting(settingName) {
return store.getters.experimentSettings.hasOwnProperty(settingName);
},
getSettingValue(settingName) {
return this.hasSetting(settingName) ? store.getters.experimentSettings[settingName] : null;
}
},
}

View file

@ -0,0 +1,148 @@
import experimentMixin from "@/mixins/experiment-mixin";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
describe("experiment-mixin", () => {
describe("hasSettingEqualTo", () => {
test("setting exists and value matches => return true", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.hasSettingEqualTo("Setting2", "Value2");
// Assert
expect(result).toEqual(true);
});
test("setting exists and value does not match => return false", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.hasSettingEqualTo("Setting2", "Value3");
// Assert
expect(result).toEqual(false);
});
test("setting does not exist => return false", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.hasSettingEqualTo("SettingBoogly", "Woogly");
// Assert
expect(result).toEqual(false);
});
test("there are no settings => return false", () => {
// Arrange
const { wrapper } = setupMocks({ experimentSettings: {} });
// Act
const result = wrapper.vm.hasSettingEqualTo("Setting2", "Value3");
// Assert
expect(result).toEqual(false);
});
});
describe("hasSetting", () => {
test("has setting => return true", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.hasSetting("Setting4");
// Assert
expect(result).toEqual(true);
});
test("does not have setting => return false", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.hasSetting("BooglyWoogly");
// Assert
expect(result).toEqual(false);
});
test("experimentSettings is empty => return false", () => {
// Arrange
const { wrapper } = setupMocks({ experimentSettings: {} });
// Act
const result = wrapper.vm.hasSetting("Setting4");
// Assert
expect(result).toEqual(false);
});
});
describe("getSettingValue", () => {
test("has setting => return correct value", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.getSettingValue("Setting3");
// Assert
expect(result).toEqual("Value3")
});
test("does not have setting => return null", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const result = wrapper.vm.getSettingValue("Hello");
// Assert
expect(result).toBeNull();
});
test("experimentSettings is empty => return null", () => {
// Arrange
const { wrapper } = setupMocks({ experimentSettings: {} });
// Act
const result = wrapper.vm.getSettingValue("Hello");
// Assert
expect(result).toBeNull();
});
});
});
function setupMocks({ experimentSettings }) {
const mocks = getMountOptions({});
const testExperimentSettings = {
"Setting1": "Value1",
"Setting2": "Value2",
"Setting3": "Value3",
"Setting4": "Value1",
"Setting5": "Value2",
"Setting6": "Value3"
};
store.getters = {
experimentSettings: experimentSettings ?? testExperimentSettings
};
const mockComponent = {
template: "<div></div>",
mixins: [experimentMixin]
};
const wrapper = shallowMount(mockComponent, mocks);
return { wrapper };
}

View file

@ -0,0 +1,148 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeMutations } from "@/constants/store-mutations.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { navigationScenarios } from "../router/router-constants/navigation-scenarios";
import { storeActions } from "@/constants/store-actions";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin.js";
import store from "@/store";
export default {
methods: {
hasPartQuestions(partsOrQuestions) {
return partsOrQuestions?.some(pq => pq.partQuestions?.length > 0);
},
hasGlassLocationWithMultipleParts(partsOrQuestions) {
return partsOrQuestions?.some(pq => pq.parts?.length > 1);
},
hasChildPartQuestions(partsOrQuestions) {
return partsOrQuestions?.some(pq => {
return pq.parts?.some(part => part.childPartQuestions?.length > 0);
});
},
hasCapabilityQuestions(partsOrQuestions) {
return partsOrQuestions?.some(pq => {
return pq.parts?.some(part => part.requiresCapabilityQuestions === true);
});
},
// method to only include keys listed for lineItems.glassParts in
// https://safelite.atlassian.net/wiki/spaces/DC/pages/17137665/Catalog+Front-End+State#glassParts
reducedGlassPartsArray(glassParts) {
const reducedGlassParts = [];
glassParts.forEach((glass) => {
if (Array.isArray(glass.parts) && glass.parts.length === 1) {
const singlePart = glass.parts[0];
reducedGlassParts.push({
partNumber: singlePart.partNumber,
description: singlePart.description,
color: singlePart.color,
requiresRecalibration: singlePart.requiresRecalibration,
requiresCapabilityQuestions: singlePart.requiresCapabilityQuestions,
recalibrationType: singlePart.recalibrationType,
childParts: singlePart.childParts,
price: singlePart.price,
});
}
});
return reducedGlassParts;
},
comparePageIndices(currentPage, fmgPage) {
const orderedVehicleQuestionPages = [
fmgPageValues.PART_QUESTIONS,
fmgPageValues.VEHICLE_PARTS,
fmgPageValues.MOLDING_QUESTIONS,
fmgPageValues.CAPABILITY_QUESTIONS,
fmgPageValues.QUOTE
]
return orderedVehicleQuestionPages.indexOf(currentPage) - orderedVehicleQuestionPages.indexOf(fmgPage);
},
currentPageComesBeforePage(currentPage = this.$route.query.fmgPage, fmgPage) {
return this.comparePageIndices(currentPage, fmgPage) < 0;
},
currentPageComesAfterPage(currentPage = this.$route.query.fmgPage, fmgPage) {
return this.comparePageIndices(currentPage, fmgPage) > 0;
},
// Can't use `this` because navigateForward is also called from vin-pages-mixin
async navigateForward(partsOrQuestions, vm) {
const self = vm ?? this;
const currentPage = self.$route.query.fmgPage;
const hasPartQuestions = this.hasPartQuestions(partsOrQuestions)
const hasGlassLocationWithMultipleParts = this.hasGlassLocationWithMultipleParts(partsOrQuestions);
const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions);
const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions);
if (hasPartQuestions && this.currentPageComesBeforePage(currentPage, fmgPageValues.PART_QUESTIONS)) {
self.$router.navigate(self.navigationScenarios.HAS_PART_QUESTIONS, self.$route, {}, {}, {partsOrQuestions: partsOrQuestions});
}
else if (hasGlassLocationWithMultipleParts && this.currentPageComesBeforePage(currentPage, fmgPageValues.VEHICLE_PARTS)) {
// if multiple parts on any glass
// go to vehicle-parts page and pass the partsData
self.$router.navigate(self.navigationScenarios.HAS_MULTIPLE_PARTS_TO_CHOOSE,self.$route,{},{},{partsOrQuestions: partsOrQuestions});
} else if (hasChildPartQuestions && this.currentPageComesBeforePage(currentPage, fmgPageValues.MOLDING_QUESTIONS)) {
// if any childpart questions
// go to molding-questions page and pass the partsData
self.$router.navigate(self.navigationScenarios.HAS_MOLDING_QUESTIONS,self.$route,{},{},{partsOrQuestions: partsOrQuestions});
} else if (hasCapabilityQuestions && this.currentPageComesBeforePage(currentPage, fmgPageValues.CAPABILITY_QUESTIONS)) {
// if has capability questions
// go to capability-questions page and pass the partsData
const windshieldPart = partsOrQuestions.filter(x => x.glassLocation === damageLocationsSelected.WINDSHIELD)[0].parts[0];
let capabilityQuestions = (await baseMixin.methods.dispatchStoreAction(storeActions.GET_CAPABILITY_QUESTIONS, {
carId: store.getters.vehicle.carId,
partNumber: windshieldPart.partNumber
})).data;
capabilityQuestions.forEach(question => {
question.answers = question.answers.map(answer => {
return {
...answer,
answerResult: answer.answerResult1
}
})
});
self.$router.navigate(self.navigationScenarios.HAS_CAPABILITY_QUESTIONS, self.$route, {}, {}, { partsOrQuestions, capabilityQuestions });
} else {
// if single parts only
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
// save to store lineItems.glassParts
self.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
self.$refs.loadingModal.showModal();
navigateToHeritageFunnel();
// For quote pages MVP release
// self.$router.navigate(self.navigationScenarios.ANSWERED_ALL_QUESTIONS, self.$route);
}
},
backButtonAction() {
const partsOrQuestions = this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS)?.partsOrQuestions;
const hasPartQuestions = this.hasPartQuestions(partsOrQuestions);
const hasGlassLocationWithMultipleParts = this.hasGlassLocationWithMultipleParts(partsOrQuestions);
const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions);
const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions);
let backNavigationScenario = navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP;
const currentPage = this.$route.query.fmgPage;
if (hasCapabilityQuestions && this.currentPageComesAfterPage(currentPage, fmgPageValues.CAPABILITY_QUESTIONS)) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_TO_GO_TO_CAPABILITY_QUESTIONS;
}
else if (hasChildPartQuestions && this.currentPageComesAfterPage(currentPage, fmgPageValues.MOLDING_QUESTIONS)) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_TO_GO_TO_MOLDING_QUESTIONS;
}
else if (hasGlassLocationWithMultipleParts && this.currentPageComesAfterPage(currentPage, fmgPageValues.VEHICLE_PARTS)) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_TO_GO_TO_VEHICLE_PARTS;
}
else if (hasPartQuestions && this.currentPageComesAfterPage(currentPage, fmgPageValues.PART_QUESTIONS)) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_TO_GO_TO_PART_QUESTIONS;
}
this.$router.navigate(
backNavigationScenario,
this.$route
);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,13 @@
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 vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
export default { export default {
methods: { methods: {
async navigateForwardWithSingleCarMatch() { async navigateForwardWithSingleCarMatch() {
const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS); const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS);
const partsOrQuestions = result.data.partsOrQuestions; const partsOrQuestions = result.data.partsOrQuestions;
const hasPartsQuestions = partsOrQuestions.some(pq => pq.partQuestions?.length > 0);
const hasGlassLocationWithMultipleParts = partsOrQuestions.some(pq => pq.parts?.length > 1);
if (hasPartsQuestions) { vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this);
this.$router.navigate(this.navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, this.$route, {}, {}, result.data); },
}
else if (hasGlassLocationWithMultipleParts) {
this.$router.navigate(this.navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, this.$route, {}, {}, result.data);
}
else {
store.commit(storeMutations.UPDATE_GLASS_PARTS, result.data);
this.$refs.loadingModal.showModal();
navigateToHeritageFunnel();
}
}
} }
} }

View file

@ -2,14 +2,11 @@ import vinPagesMixin from "@/mixins/vin-pages-mixin";
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import { setupMocksForJsFiles, getMountOptions } from "@/helpers/unit-test-helper.js"; import { setupMocksForJsFiles, 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 { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
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 { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn() navigateForward: jest.fn()
})); }));
describe("vin-pages-mixin", () => { describe("vin-pages-mixin", () => {
@ -18,864 +15,17 @@ describe("vin-pages-mixin", () => {
}) })
describe("navigateForwardWithSingleCarMatch", () => { describe("navigateForwardWithSingleCarMatch", () => {
describe("should go to parts-questions", () => { test("should navigateForward", async () => {
test("single glass location has part question => go to parts-questions", async () => { // Arrange
// Arrange const { wrapper } = setupMocks({});
const partsOrQuestions = [ vehicleQuestionsMixin.methods.navigateForward = jest.fn();
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
}
];
const { wrapper } = setupMocks({ // Act
partsOrQuestions: partsOrQuestions await wrapper.vm.navigateForwardWithSingleCarMatch();
});
// Act // Assert
await wrapper.vm.navigateForwardWithSingleCarMatch(); expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
})
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
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 () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": null,
"partQuestions": [
{
"questionSequence": 2,
"questionText": "Is this a super awesome question?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144000"
},
{
"answerText": "Super yes",
"nextQuestionSequence": null,
"answerResult": "DW01143001"
}
]
}
]
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
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 () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08160GTYN",
"description": "driver side, body side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
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 () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ08162YPYN",
"description": "driver side, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08160GTYN",
"description": "driver side, body side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD08160YPYN",
"description": "driver side, body side, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08165YPNN",
"description": "heated glass, stationary",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08166GTNN",
"description": "stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08167GTNN",
"description": "heated glass, movable, 8 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08167YPNN",
"description": "heated glass, movable, 8 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
});
describe("should go to vehicle-parts", () => {
test("single glass location has multiple parts => go to vehicle-parts", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25724GTYN",
"description": "heated glass, solar, antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "FB25759GTYN",
"description": "heated glass, solar, antenna, w/diversity antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
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 () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW03647GTNN",
"description": "solar, 3rd visor band",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "MWF03647",
"partType": "MOULDING",
"description": "Upper "
}
]
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25747GTYN",
"description": "solar, driver side, rear, ex models and above",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25719GTYN",
"description": "solar, driver side, front, ex models and above",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Vent",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FV25749GTNN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25724GTYN",
"description": "heated glass, solar, antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "FB25759GTYN",
"description": "heated glass, solar, antenna, w/diversity antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
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 () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "DW02101GTYN",
"description": "solar",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD12202GTYN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD12202YPYN",
"description": "solar, driver side, rear",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD12198GTYN",
"description": "solar, driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD12200GTYN",
"description": "solar, driver side, front, laminated, soundproofing",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ12204GTYNOEM",
"description": "solar, driver side, encap",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12204YPYNOEM",
"description": "solar, driver side, encap",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12205GTYNOEM",
"description": "solar, antenna, driver side, encap",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12205YPYNOEM",
"description": "solar, antenna, driver side, encap",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12207GTYN",
"description": "solar, driver side, encap, chrome molding",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12207YPYNOEM",
"description": "solar, driver side, encap, chrome molding",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12208GTYNOEM",
"description": "solar, antenna, driver side, encap, chrome molding",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12208YPYNOEM",
"description": "solar, antenna, driver side, encap, chrome molding",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB12209GTYN",
"description": "heated glass, solar, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB12209YPYN",
"description": "heated glass, solar, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
});
describe("should go to heritage funnel", () => {
test("single glass location selected, has no part questions and has one part => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW04186GTYN",
"description": "solar, soundproofing, lane keep assist",
"color": "Green Tint",
"requiresRecalibration": true,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "GGG 3563 KIT",
"partType": "MOULDING",
"description": "Kit, Top & Sides "
}
]
}
],
"partQuestions": null
}
]
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(navigateToHeritageFunnel).toHaveBeenCalledTimes(1);
});
test("multiple glass locations selected, each has one part and no part questions => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW04186GTYN",
"description": "solar, soundproofing, lane keep assist",
"color": "Green Tint",
"requiresRecalibration": true,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "GGG 3563 KIT",
"partType": "MOULDING",
"description": "Kit, Top & Sides "
}
]
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25457GTYN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD27090GTYN",
"description": "solar, driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Vent",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FV25459GTNN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25460GTYN",
"description": "heated glass, solar",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
store.commit = jest.fn();
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(store.commit).toHaveBeenCalledTimes(1);
expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_GLASS_PARTS, { partsOrQuestions })
expect(wrapper.vm.$refs.loadingModal.showModal).toHaveBeenCalledTimes(1);
expect(navigateToHeritageFunnel).toHaveBeenCalledTimes(1);
});
});
}); });
}); });
@ -891,27 +41,14 @@ function setupMocks({ partsOrQuestions = [] }) {
], ],
}); });
const mocks = getMountOptions({ const mocks = getMountOptions({});
router: {
navigate: jest.fn()
},
});
const mockVinComponent = { const mockVinComponent = {
components: { loadingModal }, components: { loadingModal },
template: '<loadingModal ref="loadingModal" />', template: '<loadingModal ref="loadingModal" />',
// render() {
// return '<div ref="loadingModal"></div>'
// },
mixins: [vinPagesMixin, baseMixin.baseMixin] mixins: [vinPagesMixin, baseMixin.baseMixin]
}; };
// const mockVinComponent = Vue.component("mockcomponent", {
// template: '<loadingModal ref="loadingModal" />',
// mixins: [vinPagesMixin, baseMixin.baseMixin]
// })
const wrapper = shallowMount(mockVinComponent, mocks); const wrapper = shallowMount(mockVinComponent, mocks);
wrapper.vm.$refs.loadingModal.showModal = jest.fn(); wrapper.vm.$refs.loadingModal.showModal = jest.fn();

View file

@ -10,7 +10,7 @@ import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper";
// Heritage integration // Heritage integration
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper"; import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
import { updateOrCreateFunnelCookie, getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { updateOrCreateFunnelCookie, getFunnelCookie, updateSessionIdCookie } from "@/helpers/heritage-integration/cookie-helper";
import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper"; import { loadOrderIfPresent, saveOrder } from "@/helpers/heritage-integration/order-helper";
import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { getPageToRouteExistingOrderTo, navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
@ -18,6 +18,8 @@ import baseMixin from "@/mixins/base-mixin";
import eventBus from "@/helpers/event-bus/event-bus"; import eventBus from "@/helpers/event-bus/event-bus";
import store from "@/store"; import store from "@/store";
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import { experimentTriggers } from "../constants/experiments";
import { applicationConfig } from "../constants/application-config";
// Components // Components
import quote from "@/layouts/quote/quote.vue"; import quote from "@/layouts/quote/quote.vue";
@ -37,6 +39,14 @@ const routes = [
if (analyticsMixin.methods.noSession()) { if (analyticsMixin.methods.noSession()) {
await analyticsMixin.methods.initSession(); await analyticsMixin.methods.initSession();
} }
else {
updateSessionIdCookie();
}
if (getFunnelCookie()?.SuppressConceptFunnel) {
await navigateToHeritageFunnel(false);
return next(false);
}
// If the saved session has timed out, clear the session, execute 404 logic. // If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) { if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
@ -46,6 +56,9 @@ const routes = [
// On entering the funnel "fresh", read cookie information, decide what to do next. // On entering the funnel "fresh", read cookie information, decide what to do next.
if (from.redirectedFrom === undefined) { if (from.redirectedFrom === undefined) {
// clear the saveOrderPromise - if it exists in the vuex store but a new instance was created
// the saveOrderPromise will no longer point to a valid promise
baseMixin.methods.dispatchStoreAction(storeActions.RESET_SAVE_ORDER_PROMISE);
const loadOrderResponse = await loadOrderIfPresent(); const loadOrderResponse = await loadOrderIfPresent();
const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse); const pageToRedirectTo = await getPageToRouteExistingOrderTo(to, loadOrderResponse);
@ -56,11 +69,12 @@ const routes = [
return next(false); return next(false);
} }
// Assign our fmgPage so it will load normally like the other pages. // Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo; to.query.fmgPage = pageToRedirectTo;
} }
await runExperiments(to.query.fmgPage);
// Process funnel cookie. // Process funnel cookie.
updateOrCreateFunnelCookie(); updateOrCreateFunnelCookie();
@ -125,29 +139,33 @@ const router = createRouter({
router.afterEach((to, from) => { router.afterEach((to, from) => {
// Update lastPageVisited in the store // Update lastPageVisited in the store
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name); store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
// Push page view to GA
analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
baseMixin.methods.dispatchStoreAction(storeActions.GET_EXPERIMENTS_BY_USER, { userId: getDeviceIdValue() }) // Push page view to GA
.then( (response) => { analyticsMixin.methods.pushPageViewToGA();
analyticsMixin.methods.pushExperimentsToDataLayer(response.data);
}); // Push experiments to Data Layer
analyticsMixin.methods.pushExperimentsToDataLayer();
}); });
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => { router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData) => {
navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData); navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData);
} }
router.navigateToExternalUrl = (url, optionalQuery = {}) => { router.navigateToExternalUrl = (url, optionalQuery = {}) => {
navigateToUrl(url, optionalQuery); navigateToUrl(url, optionalQuery);
} }
//Use this navigation when you need to call next() explicitly. beforeRouteEnter is a good example.
router.overrideNavigation = (scenario, currentRoute, next, optionalQuery = {}, optionalParams = {}, optionalPageData) => {
router.navigate(scenario, currentRoute, optionalQuery, optionalParams, optionalPageData);
next();
}
// 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, 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;
@ -160,8 +178,9 @@ async function navigate(scenario, currentRoute, optionalQuery = {}, optionalPara
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.
// Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided. // Update page data to the store for next page if provided. Otherwise, keep existing page data or set to empty object
baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData); const existingPageDataForPage = store.getters.pageData(destinationFmgPageValue);
baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData ? optionalPageData : existingPageDataForPage ?? {});
// if cookie and referralNumber/Date exists OR an emailAddress has been saved // if cookie and referralNumber/Date exists OR an emailAddress has been saved
if ((getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) || store.getters.order.customer?.emailAddress) { if ((getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) || store.getters.order.customer?.emailAddress) {
@ -183,15 +202,16 @@ async function navigate(scenario, currentRoute, optionalQuery = {}, optionalPara
// Get navigation map depending on the scenario and the current 'page' you're on. // Get navigation map depending on the scenario and the current 'page' you're on.
function getNavigationMap(scenario, currentRoute) { function getNavigationMap(scenario, currentRoute) {
const fmgPageValue = currentRoute.query.fmgPage; const fmgPageValue = currentRoute.query.fmgPage;
const matchedQueryValue = routingTable const matchedQueryValue = routingTable(store)
.filter( .filter(
(item) => (item) =>
item.fmgPageValue === fmgPageValue && item.fmgPageValue === fmgPageValue &&
item.maps.filter((map) => map.scenario === scenario).length > 0 item.maps.filter((map) => map.scenario === scenario).length > 0
) )
.map((m) => m.maps.filter((map) => map.scenario === scenario)); .map((m) => m.maps.filter((map) => map.scenario === scenario))[0]
.filter(x => x.filter === true || x.filter === undefined);
return matchedQueryValue[0][0]; return matchedQueryValue[0];
} }
//---------------------------------------------------------- Private Functions ---------------------------------------------------------- //---------------------------------------------------------- Private Functions ----------------------------------------------------------
@ -204,7 +224,7 @@ function navigateToUrl(url, optionalQuery = {}) {
for (const queryKey in optionalQuery) { for (const queryKey in optionalQuery) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
} }
window.location.assign(externalUrl); window.location.assign(externalUrl);
} }
@ -257,4 +277,21 @@ function arePagePrerequisitesValid(component) {
return component.default.methods.arePagePrerequisitesValid(); return component.default.methods.arePagePrerequisitesValid();
} }
// Run SiteEntry and PageEntry triggers for experiments
async function runExperiments(nextPage) {
if (!store.getters.applicationUser.triggeredSiteEntry) {
await baseMixin.methods.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, {
userId: getDeviceIdValue(),
triggerEvent: experimentTriggers.SITE_ENTRY,
triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE
})
}
await baseMixin.methods.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, {
userId: getDeviceIdValue(),
triggerEvent: experimentTriggers.PAGE_ENTRY,
triggerValue: nextPage
})
}
export default router; export default router;

View file

@ -8,11 +8,14 @@ const fmgPageValues = {
VIN_LOOKUP: "vin-lookup", VIN_LOOKUP: "vin-lookup",
VEHICLE_PARTS: "vehicle-parts", VEHICLE_PARTS: "vehicle-parts",
PART_QUESTIONS: "part-questions", PART_QUESTIONS: "part-questions",
MOLDING_QUESTIONS: "molding-questions",
CAPABILITY_QUESTIONS: "capability-questions",
LICENSE_PLATE_LOOKUP: "license-plate-lookup", LICENSE_PLATE_LOOKUP: "license-plate-lookup",
REVEAL: "reveal", REVEAL: "reveal",
ESTIMATE: "estimate", ESTIMATE: "estimate",
ADDRESS_VEHICLES: "address-vehicles", ADDRESS_VEHICLES: "address-vehicles",
QUOTE: "quote" QUOTE: "quote",
HERITAGE: "heritage"
}; };
export { fmgPageValues }; export { fmgPageValues };

View file

@ -8,8 +8,10 @@ const navigationScenarios = {
CLICKED_FORWARD: "CLICKED_FORWARD", CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN", CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
SELECTED_PARTS: "SELECTED_PARTS", SELECTED_PARTS: "SELECTED_PARTS",
SELECTED_VIN_WITH_PART_QUESTIONS: "SELECTED_VIN_WITH_PART_QUESTIONS", SELECTED_VIN_WITH_PART_QUESTIONS: "HAS_PART_QUESTIONS",
SELECTED_VIN_WITH_MULTIPLE_PARTS: "SELECTED_VIN_WITH_MULTIPLE_PARTS", SELECTED_VIN_WITH_MULTIPLE_PARTS: "HAS_MULTIPLE_PARTS_TO_CHOOSE",
SELECTED_VIN_WITH_MOLDING_QUESTIONS: "HAS_MOLDING_QUESTIONS",
SELECTED_VIN_WITH_CAPABILITY_QUESTIONS: "HAS_CAPABILITY_QUESTIONS",
CONTINUING_WITH_SINGLE_PART: "CONTINUING_WITH_SINGLE_PART", CONTINUING_WITH_SINGLE_PART: "CONTINUING_WITH_SINGLE_PART",
CONTINUING_WITH_MULTIPLE_VEHICLES: "CONTINUING_WITH_MULTIPLE_VEHICLES", CONTINUING_WITH_MULTIPLE_VEHICLES: "CONTINUING_WITH_MULTIPLE_VEHICLES",
SELECTED_VIN_HAS_MISMATCHED_GLASS: "SELECTED_VIN_HAS_MISMATCHED_GLASS", SELECTED_VIN_HAS_MISMATCHED_GLASS: "SELECTED_VIN_HAS_MISMATCHED_GLASS",
@ -19,7 +21,18 @@ const navigationScenarios = {
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_SINGLE_PART: "ANSWERED_QUESTIONS_WITH_SINGLE_PART",
ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS: "ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS", ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS: "ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS",
SELECTED_PROVIDE_VIN_DIFFERENT_WAY: "SELECTED_PROVIDE_VIN_DIFFERENT_WAY" SELECTED_PROVIDE_VIN_DIFFERENT_WAY: "SELECTED_PROVIDE_VIN_DIFFERENT_WAY",
CLICKED_BACK_WITH_PART_QUESTION_ANSWERS: "CLICKED_BACK_WITH_PART_QUESTION_ANSWERS",
CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS: "CLICKED_BACK_WITHOUT_PART_QUESTION_ANSWERS",
ANSWERED_ALL_QUESTIONS: "ANSWERED_ALL_QUESTIONS",
HAS_PART_QUESTIONS: "HAS_PART_QUESTIONS",
HAS_MULTIPLE_PARTS_TO_CHOOSE: "HAS_MULTIPLE_PARTS_TO_CHOOSE",
HAS_MOLDING_QUESTIONS: "HAS_MOLDING_QUESTIONS",
HAS_CAPABILITY_QUESTIONS: "HAS_CAPABILITY_QUESTIONS",
CLICKED_BACK_TO_GO_TO_VIN_LOOKUP: "CLICKED_BACK_TO_GO_TO_VIN_LOOKUP",
CLICKED_BACK_TO_GO_TO_PART_QUESTIONS: "CLICKED_BACK_TO_GO_TO_PART_QUESTIONS",
CLICKED_BACK_TO_GO_TO_VEHICLE_PARTS: "CLICKED_BACK_TO_GO_TO_VEHICLE_PARTS",
CLICKED_BACK_TO_GO_TO_MOLDING_QUESTIONS: "CLICKED_BACK_TO_GO_TO_MOLDING_QUESTIONS"
}; };
export { navigationScenarios }; export { navigationScenarios };

View file

@ -1,233 +1,345 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios"; import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
const routingTable = [ // Get store from router/index.js instead of importing it here to get updated values
{ const routingTable = function(store) {
fmgPageValue: fmgPageValues.VEHICLE_YEAR, return [
maps: [ {
{ fmgPageValue: fmgPageValues.VEHICLE_YEAR,
scenario: navigationScenarios.SELECTED_YEAR, maps: [
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE, {
}, scenario: navigationScenarios.SELECTED_YEAR,
], destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
}, },
{ ],
fmgPageValue: fmgPageValues.VEHICLE_MAKE, },
maps: [ {
{ fmgPageValue: fmgPageValues.VEHICLE_MAKE,
scenario: navigationScenarios.SELECTED_MAKE, maps: [
destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL, {
}, scenario: navigationScenarios.SELECTED_MAKE,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL,
scenario: navigationScenarios.CLICKED_BACK, },
destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR, {
}, scenario: navigationScenarios.CLICKED_BACK,
], destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
}, },
{ ],
fmgPageValue: fmgPageValues.VEHICLE_MODEL, },
maps: [ {
{ fmgPageValue: fmgPageValues.VEHICLE_MODEL,
scenario: navigationScenarios.SELECTED_MODEL, maps: [
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE, {
}, scenario: navigationScenarios.SELECTED_MODEL,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
scenario: navigationScenarios.CLICKED_BACK, },
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE, {
}, scenario: navigationScenarios.CLICKED_BACK,
], destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
}, },
{ ],
fmgPageValue: fmgPageValues.VEHICLE_STYLE, },
maps: [ {
{ fmgPageValue: fmgPageValues.VEHICLE_STYLE,
scenario: navigationScenarios.SELECTED_STYLE, maps: [
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, {
}, scenario: navigationScenarios.SELECTED_STYLE,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
scenario: navigationScenarios.CLICKED_BACK, },
destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL, {
}, scenario: navigationScenarios.CLICKED_BACK,
], destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL,
}, },
{ ],
fmgPageValue: fmgPageValues.VEHICLE_DAMAGE, },
maps: [ {
{ fmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
scenario: navigationScenarios.CLICKED_BACK, maps: [
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE, {
}, scenario: navigationScenarios.CLICKED_BACK,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN, },
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, {
}, scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
{ destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, },
destinationFmgPageValue: fmgPageValues.ESTIMATE, {
}, scenario: navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN,
], destinationFmgPageValue: fmgPageValues.ESTIMATE,
}, },
{ ],
fmgPageValue: fmgPageValues.VEHICLE_PARTS, },
maps: [ {
{ fmgPageValue: fmgPageValues.REVEAL,
scenario: navigationScenarios.CLICKED_BACK, maps: [
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, {
}, scenario: navigationScenarios.CLICKED_BACK,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
scenario: navigationScenarios.SELECTED_PARTS, },
destinationFmgPageValue: fmgPageValues.QUOTE, {
}, scenario: navigationScenarios.SELECTED_PARTS,
{ destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
scenario: navigationScenarios.CLICKED_FORWARD, },
destinationFmgPageValue: fmgPageValues.REVEAL, ],
}, },
], {
}, fmgPageValue: fmgPageValues.VIN_LOOKUP,
{ maps: [
fmgPageValue: fmgPageValues.REVEAL, {
maps: [ scenario: navigationScenarios.CLICKED_BACK,
{ destinationFmgPageValue: fmgPageValues.ESTIMATE,
scenario: navigationScenarios.CLICKED_BACK, },
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, {
}, scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
{ destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
scenario: navigationScenarios.SELECTED_PARTS, },
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, {
}, scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
], destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
}, },
{ {
fmgPageValue: fmgPageValues.VIN_LOOKUP, scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
maps: [ destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
{ },
scenario: navigationScenarios.CLICKED_BACK, {
destinationFmgPageValue: fmgPageValues.ESTIMATE, scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
}, destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
{ },
scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, {
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, scenario: navigationScenarios.SELECTED_VIN_WITH_MOLDING_QUESTIONS,
}, destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS
{ },
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN, {
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, scenario: navigationScenarios.SELECTED_VIN_WITH_CAPABILITY_QUESTIONS,
}, destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS
{ }
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, ],
//destinationFmgPageValue: fmgPageValues.PART_QUESTIONS },
destinationFmgPageValue: fmgPageValues.QUOTE, {
}, fmgPageValue: fmgPageValues.LICENSE_PLATE_LOOKUP,
{ maps: [
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, {
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS scenario: navigationScenarios.CLICKED_BACK,
}, destinationFmgPageValue: fmgPageValues.ESTIMATE,
], },
}, {
{ scenario: navigationScenarios.CLICKED_FORWARD,
fmgPageValue: fmgPageValues.LICENSE_PLATE_LOOKUP, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
maps: [ },
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.ESTIMATE, destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
}, },
{ {
scenario: navigationScenarios.CLICKED_FORWARD, scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, scenario: navigationScenarios.SELECTED_VIN_WITH_MOLDING_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, scenario: navigationScenarios.SELECTED_VIN_WITH_CAPABILITY_QUESTIONS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS
} }
], ],
}, },
{ {
fmgPageValue: fmgPageValues.ADDRESS_LOOKUP, fmgPageValue: fmgPageValues.ADDRESS_LOOKUP,
maps: [ maps: [
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.ESTIMATE, destinationFmgPageValue: fmgPageValues.ESTIMATE,
}, },
{ {
scenario: navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, scenario: navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES,
destinationFmgPageValue: fmgPageValues.ADDRESS_VEHICLES, destinationFmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
} },
], {
}, scenario: navigationScenarios.SELECTED_VIN_WITH_MOLDING_QUESTIONS,
{ destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS
fmgPageValue: fmgPageValues.ADDRESS_VEHICLES, },
maps: [ {
{ scenario: navigationScenarios.SELECTED_VIN_WITH_CAPABILITY_QUESTIONS,
scenario: navigationScenarios.CLICKED_BACK, destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP, }
}, ],
{ },
scenario: navigationScenarios.CLICKED_FORWARD, {
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, fmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
}, maps: [
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP,
}, },
{ {
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
} },
], {
}, scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
{ destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
fmgPageValue: fmgPageValues.ESTIMATE, },
maps: [ {
{ scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
scenario: navigationScenarios.CLICKED_BACK, destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE, },
}, {
{ scenario: navigationScenarios.SELECTED_VIN_WITH_MOLDING_QUESTIONS,
scenario: navigationScenarios.SELECTED_MANUAL_VIN, destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP, },
}, {
{ scenario: navigationScenarios.SELECTED_VIN_WITH_CAPABILITY_QUESTIONS,
scenario: navigationScenarios.SELECTED_LICENSE_PLATE, destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS
destinationFmgPageValue: fmgPageValues.LICENSE_PLATE_LOOKUP, },
}, {
{ scenario: navigationScenarios.SELECTED_PROVIDE_VIN_DIFFERENT_WAY,
scenario: navigationScenarios.SELECTED_HOME_ADDRESS, destinationFmgPageValue: fmgPageValues.ESTIMATE
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP, }
}, ],
], },
}, {
{ fmgPageValue: fmgPageValues.ESTIMATE,
fmgPageValue: fmgPageValues.PART_QUESTIONS, maps: [
maps: [ {
{ scenario: navigationScenarios.CLICKED_BACK,
scenario: navigationScenarios.CLICKED_BACK, destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP },
}, {
{ scenario: navigationScenarios.SELECTED_MANUAL_VIN,
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS, destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS },
}, {
{ scenario: navigationScenarios.SELECTED_LICENSE_PLATE,
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_SINGLE_PART, destinationFmgPageValue: fmgPageValues.LICENSE_PLATE_LOOKUP,
destinationFmgPageValue: fmgPageValues.QUOTE, },
}, {
] scenario: navigationScenarios.SELECTED_HOME_ADDRESS,
}, destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP,
]; },
],
},
{
fmgPageValue: fmgPageValues.PART_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.HAS_MULTIPLE_PARTS_TO_CHOOSE,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.HAS_MOLDING_QUESTIONS,
destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.HAS_CAPABILITY_QUESTIONS,
destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.ANSWERED_ALL_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
]
},
{
fmgPageValue: fmgPageValues.VEHICLE_PARTS,
maps: [
{
scenario: navigationScenarios.SELECTED_PARTS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.HAS_MOLDING_QUESTIONS,
destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.HAS_CAPABILITY_QUESTIONS,
destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.ANSWERED_ALL_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
],
},
{
fmgPageValue: fmgPageValues.MOLDING_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VEHICLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.HAS_CAPABILITY_QUESTIONS,
destinationFmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.ANSWERED_ALL_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE,
}
]
},
{
fmgPageValue: fmgPageValues.CAPABILITY_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_VEHICLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.CLICKED_BACK_TO_GO_TO_MOLDING_QUESTIONS,
destinationFmgPageValue: fmgPageValues.MOLDING_QUESTIONS
},
{
scenario: navigationScenarios.ANSWERED_ALL_QUESTIONS,
destinationFmgPageValue: fmgPageValues.QUOTE
},
]
},
];
}
export { routingTable }; export { routingTable };

View file

@ -4,7 +4,11 @@ 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"; import { storeActions } from "@/constants/store-actions";
import { applicationConfig } from "@/constants/application-config";
import { experimentTriggers } from "@/constants/experiments";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
@ -45,10 +49,10 @@ const getDefaultState = () => {
numberOfChips: null, numberOfChips: null,
glassToReplace: null, glassToReplace: null,
partQuestionAnswers: null, partQuestionAnswers: null,
capabilityQuestionAnswers: null
}, },
lineItems: { lineItems: {
glassParts: null, glassParts: null
otherParts: null
}, },
payment: { payment: {
isInsurance: null, isInsurance: null,
@ -60,15 +64,18 @@ const getDefaultState = () => {
referralDate: null, referralDate: null,
referralCorrelationId: null, referralCorrelationId: null,
accountNumber: 0, accountNumber: 0,
eon: null
}, },
applicationUser: { applicationUser: {
eventBus: [], eventBus: [],
pageData: {}, pageData: {},
savedSessionTimeout: getDateForSavedSessionTimeout(), savedSessionTimeout: getDateForSavedSessionTimeout(),
saveOrderPromise: null, saveOrderPromise: null,
saveQuoteId: null, savedSessionId: null,
crmCustomerId: null, crmCustomerId: null,
lastPageVisited: null, lastPageVisited: null,
experiments: [],
triggeredSiteEntry: false
}, },
} }
}; };
@ -118,11 +125,17 @@ export const mutations = {
state.order.damage.glassToReplace = glassToReplace; state.order.damage.glassToReplace = glassToReplace;
}, },
updatePartQuestionAnswers(state, answersArray) { updatePartQuestionAnswers(state, answersArray) {
state.order.damage.partQuestionAnswers = answersArray; state.order.damage.partQuestionAnswers = answersArray;
},
updateCapabilityQuestionAnswers(state, answersArray) {
state.order.damage.capabilityQuestionAnswers = answersArray;
}, },
updateGlassParts(state, partsData) { updateGlassParts(state, partsData) {
state.order.lineItems.glassParts = partsData; state.order.lineItems.glassParts = partsData;
}, },
updateOtherParts(state, partsData) {
state.order.lineItems.otherParts = partsData;
},
updatePageData(state, pageData) { updatePageData(state, pageData) {
state.applicationUser.pageData[pageData.page] = pageData.data; state.applicationUser.pageData[pageData.page] = pageData.data;
}, },
@ -138,6 +151,9 @@ export const mutations = {
updateParentAcctNumber(state, parentAcctNumber) { updateParentAcctNumber(state, parentAcctNumber) {
state.order.accountNumber = parentAcctNumber; state.order.accountNumber = parentAcctNumber;
}, },
updateEON(state, eon) {
state.order.eon = eon;
},
updateIsInsurance(state, isInsurance) { updateIsInsurance(state, isInsurance) {
state.order.payment.isInsurance = isInsurance; state.order.payment.isInsurance = isInsurance;
}, },
@ -208,8 +224,8 @@ export const mutations = {
updateSaveOrderPromise(state, saveOrderPromise){ updateSaveOrderPromise(state, saveOrderPromise){
state.applicationUser.saveOrderPromise = saveOrderPromise; state.applicationUser.saveOrderPromise = saveOrderPromise;
}, },
updateSaveQuoteId(state, saveQuoteId) { updateSavedSessionId(state, savedSessionId) {
state.applicationUser.saveQuoteId = saveQuoteId; state.applicationUser.savedSessionId = savedSessionId;
}, },
updateCrmCustomerId(state, crmCustomerId) { updateCrmCustomerId(state, crmCustomerId) {
state.applicationUser.crmCustomerId = crmCustomerId; state.applicationUser.crmCustomerId = crmCustomerId;
@ -264,16 +280,32 @@ export const mutations = {
}, },
resetGlassPartsState(state) { resetGlassPartsState(state) {
state.order.lineItems.glassParts = null; state.order.lineItems.glassParts = null;
state.order.damage.partQuestionAnswers = null;
state.order.damage.capabilityQuestionAnswers = null;
state.applicationUser.pageData[fmgPageValues.PART_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.VEHICLE_PARTS] = null;
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null;
}, },
resetState(state) { resetState(state) {
Object.assign(state, getDefaultState()); Object.assign(state, getDefaultState());
}, },
resetSaveOrderPromise(state) {
state.applicationUser.saveOrderPromise = null;
},
// Misc Mutations // Misc Mutations
updateStateWithOrderInformation(state, orderInformation) { updateStateWithOrderInformation(state, orderInformation) {
state.order.referralNumber = orderInformation.referralNumber; state.order.referralNumber = orderInformation.referralNumber;
state.order.referralDate = orderInformation.referralDate; state.order.referralDate = orderInformation.referralDate;
state.order.referralCorrelationId = orderInformation.referralCorrelationId; state.order.referralCorrelationId = orderInformation.referralCorrelationId;
state.order.eon = orderInformation.eon;
if (state.order.vehicle.vin !== orderInformation.vehicle?.vin) {
state.applicationUser.pageData[fmgPageValues.PART_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.VEHICLE_PARTS] = null;
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.CAPABILITY_QUESTIONS] = null;
}
state.order.vehicle = Object.assign(state.order.vehicle, { state.order.vehicle = Object.assign(state.order.vehicle, {
year: orderInformation.vehicle?.year, year: orderInformation.vehicle?.year,
@ -312,7 +344,14 @@ export const mutations = {
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;
state.applicationUser.experiments = orderInformation.experiments;
}, },
updateExperiments(state, experiments) {
state.applicationUser.experiments = experiments;
},
updateTriggeredSiteEntry(state, wasSiteEntryTriggered) {
state.applicationUser.triggeredSiteEntry = wasSiteEntryTriggered;
}
} }
// Export Getters // Export Getters
@ -333,6 +372,35 @@ export const getters = {
applicationUser: (state) => state.applicationUser, applicationUser: (state) => state.applicationUser,
order: (state) => state.order, order: (state) => state.order,
payment: (state) => state.order.payment, payment: (state) => state.order.payment,
experimentOrder: (state) => {
return {
vehicleYear: state.order.vehicle.year,
vehicleMake: state.order.vehicle.make,
vehicleModel: state.order.vehicle.model,
vehicleStyle: state.order.vehicle.style,
isRepair: state.order.damage.isRepair,
numberOfChips: state.order.damage.numberOfChips,
carId: state.order.vehicle.carId,
serviceCity: state.order.serviceLocation.city,
serviceState: state.order.serviceLocation.state,
serviceZipCode: state.order.serviceLocation.zipCode,
parentAccountNumber: state.order.accountNumber,
isCoverageVerified: state.order.payment.insuranceCoverage.isVerified,
orderPartNumbers: [...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "partNumber"), ...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "partNumber")],
orderPartTypes: [...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "recalibrationType"), ...getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.otherParts, "recalibrationType")],
hasRecalibrationPart: getAllValuesOfPropertyInArrayOfObjects(state.order.lineItems.glassParts, "requiresRecalibration")?.length > 0,
selectedMultiGlass: state.order.damage.glassToReplace?.length > 1,
selectedWindshieldGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.WINDSHIELD),
selectedBackGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.REAR),
selectedDriverSideGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.DRIVER),
selectedPassengerSideGlass: getAllValuesOfPropertyInArrayOfObjects(state.order.damage.glassToReplace, "glassLocation").includes(damageLocationsSelected.PASSENGER)
}
},
experimentSettings: (state) => state.applicationUser.experiments.map(x => x.settings).reduce((r, c) => Object.assign(r, c), {})
}
function getAllValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map(x => x[propertyName]).filter(x => x);
} }
// Export Actions // Export Actions
@ -455,6 +523,9 @@ export const actions = {
resetState(context) { resetState(context) {
context.commit(storeMutations.RESET_STATE); context.commit(storeMutations.RESET_STATE);
}, },
resetSaveOrderPromise(context) {
context.commit(storeMutations.RESET_SAVE_ORDER_PROMISE);
},
// Content API Actions // Content API Actions
getRouteInfo(context, { pageName }) { getRouteInfo(context, { pageName }) {
@ -481,39 +552,51 @@ export const actions = {
}, },
// Analytics Actions // Analytics Actions
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) { logExperimentExposure(context, { userId, sessionKey, pageName, experiment }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LogExperimentExposureIfAssigned.method, method: endpoints.LogExperimentExposureIfAssigned.method,
endpoint: endpoints.LogExperimentExposureIfAssigned.url, endpoint: endpoints.LogExperimentExposureIfAssigned.url,
payload: { payload: {
userId: userId, experimentForLogging: {
sessionKey: sessionKey, userId: userId,
pageName: pageName, experimentUniverseId: experiment.universeId,
universeName: universeName experimentUniverseName: experiment.universeName,
experimentTestId: experiment.testId,
experimentTestName: experiment.testName,
experimentVariationId: experiment.variationId,
experimentVariationName: experiment.variationName,
enabled: experiment.isActive,
isExposed: experiment.isExposed,
userPartitionNumber: experiment.userPartitionNumber,
assignmentId: experiment.assignmentId,
sessionKey: sessionKey,
pageName: pageName,
}
} }
}); });
}, },
// Misc Actions // Misc Actions
updateStoreWithSaveOrderResponse(context, { referralNumber, referralDate, referralCorrelationId, accountNumber, saveQuoteId, crmCustomerId }) { updateStoreWithSaveOrderResponse(context, { referralNumber, referralDate, referralCorrelationId, eon, accountNumber, savedSessionId, crmCustomerId }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
context.commit(storeMutations.UPDATE_EON, eon);
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, accountNumber); context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, accountNumber);
context.commit(storeMutations.UPDATE_SAVE_QUOTE_ID, saveQuoteId); context.commit(storeMutations.UPDATE_SAVED_SESSION_ID, savedSessionId);
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId); context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
}, },
logPageView(context, { userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId, experimentsForUser }) {
logPageView(context, { userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId }) {
var payload = { var payload = {
userId: userId, userId: userId,
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: 'SafeliteDotCom', applicationName: applicationConfig.APPLICATION_NAME,
action: action, action: action,
event: event, event: event,
shouldUseSessionId: shouldUseSessionId shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -523,18 +606,19 @@ 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, experimentsForUser }) {
var payload = { var payload = {
userId: userId, userId: userId,
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: 'SafeliteDotCom', applicationName: applicationConfig.APPLICATION_NAME,
category: category, category: category,
action: action, action: action,
label: label, label: label,
value: value, value: value,
shouldUseSessionId: shouldUseSessionId shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser
}; };
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
@ -546,7 +630,7 @@ export const actions = {
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) { initializeSession(context, { userId, sessionId, userAgent, referrer }) {
var payload = { var payload = {
applicationName: 'SafeliteDotCom', applicationName: applicationConfig.APPLICATION_NAME,
userId: userId, userId: userId,
deviceId: userId, deviceId: userId,
sessionId: sessionId, sessionId: sessionId,
@ -565,10 +649,11 @@ export const actions = {
}, },
// Misc Actions // Misc Actions
setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId }) { setReferralInformation(context, { referralNumber, referralDate, referralCorrelationId, eon }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber); context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate); context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId); context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
context.commit(storeMutations.UPDATE_EON, eon);
}, },
GetExperimentsByUser(context, { userId }) { GetExperimentsByUser(context, { userId }) {
@ -579,6 +664,28 @@ export const actions = {
}); });
}, },
async runExperimentsForTrigger(context, { userId, triggerEvent, triggerValue }) {
if (triggerEvent == experimentTriggers.SITE_ENTRY) {
context.commit(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true);
}
var payload = {
applicationName: applicationConfig.APPLICATION_NAME,
userId: userId,
triggerEvent: triggerEvent,
triggerValue: triggerValue,
experimentOrder: context.getters.experimentOrder
};
const response = await globalMethods.callHttpClient({
method: endpoints.RunExperimentsForTrigger.method,
endpoint: endpoints.RunExperimentsForTrigger.url,
payload: payload,
});
context.commit(storeMutations.UPDATE_EXPERIMENTS, response.data.experiments);
},
getEvoxImage(context, { relativeUrl }) { getEvoxImage(context, { relativeUrl }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetPageData.method, method: endpoints.GetPageData.method,
@ -635,12 +742,36 @@ export const actions = {
}); });
}, },
getCapabilityQuestions(context, { carId, partNumber }) {
return globalMethods.callHttpClient({
method: endpoints.GetCapabilityQuestions.method,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`,
})
},
getPartFromCapabilityQuestionAnswer(context, selectedAnswerResult1) {
const pageData = context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS);
const part = pageData.partsOrQuestions.find(x => x.glassLocation === damageLocationsSelected.WINDSHIELD).parts[0];
const capabilityQuestionAnswers = context.getters.damage.capabilityQuestionAnswers;
return globalMethods.callHttpClient({
method: endpoints.ApplyCapabilityAnswerToPart.method,
endpoint: endpoints.ApplyCapabilityAnswerToPart.url,
payload: {
part,
capabilityAnswerResults: capabilityQuestionAnswers
}
})
},
// Order API Actions // Order API Actions
saveOrder(context) { saveOrder(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order; const order = context.state.order;
const applicationUser = context.getters.applicationUser; const applicationUser = context.getters.applicationUser;
const lineItems = context.state.order.lineItems;
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.SaveOrder.method, method: endpoints.SaveOrder.method,
@ -671,6 +802,9 @@ export const actions = {
customer: { customer: {
emailAddress: order.customer.emailAddress, emailAddress: order.customer.emailAddress,
}, },
lineItems: {
glassParts: lineItems.glassParts
},
serviceLocation: { serviceLocation: {
streetAddress: order.serviceLocation.address, streetAddress: order.serviceLocation.address,
city: order.serviceLocation.city, city: order.serviceLocation.city,
@ -683,8 +817,8 @@ export const actions = {
existingPromoCode: null, existingPromoCode: null,
lastPage: applicationUser.lastPageVisited, lastPage: applicationUser.lastPageVisited,
crmCustomerId: applicationUser.crmCustomerId, crmCustomerId: applicationUser.crmCustomerId,
saveQuoteId: applicationUser.saveQuoteId, savedSessionId: applicationUser.savedSessionId,
experiments: applicationUser.experiments,
}, },
}); });
}, },
@ -699,7 +833,10 @@ export const actions = {
accountNumber: accountNumber?.toString() accountNumber: accountNumber?.toString()
}, },
}).then((response) => { }).then((response) => {
context.commit(storeMutations.RESET_STATE); // clear the state if the existing EON does not equal what is returned from loadOrder
if (context.state.order.eon && context.state.order.eon != response.data.eon) {
context.commit(storeMutations.RESET_STATE);
}
context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data); context.commit(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, response.data);
return response; return response;
}); });
@ -724,6 +861,7 @@ export const actions = {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values //Save new values
context.commit(storeMutations.UPDATE_YEAR, year); context.commit(storeMutations.UPDATE_YEAR, year);
@ -744,6 +882,7 @@ export const actions = {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values //Save new values
context.commit(storeMutations.UPDATE_MAKE, make); context.commit(storeMutations.UPDATE_MAKE, make);
@ -763,6 +902,7 @@ export const actions = {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values //Save new values
context.commit(storeMutations.UPDATE_MODEL, model); context.commit(storeMutations.UPDATE_MODEL, model);
@ -780,6 +920,7 @@ export const actions = {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values //Save new values
context.commit(storeMutations.UPDATE_STYLE, style); context.commit(storeMutations.UPDATE_STYLE, style);
@ -810,6 +951,7 @@ export const actions = {
//Reset dependent state when changing //Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) { if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
@ -828,6 +970,7 @@ export const actions = {
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
} }
//Save new values //Save new values
@ -843,6 +986,7 @@ export const actions = {
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
} }
//Save new values //Save new values
@ -851,10 +995,22 @@ export const actions = {
} }
}, },
savePartQuestionAnswers(context, partQuestionAnswersArray) { savePartQuestionAnswers(context, partQuestionAnswersArray) {
// if part question answers have changed, reset subsequent question answers
const previousResultsArray = context.getters.damage.partQuestionAnswers;
const havePartQuestionAnswersChanged = previousResultsArray.length !== partQuestionAnswersArray.length ||
!previousResultsArray.every((x, i) => x.result === partQuestionAnswersArray[i].result);
if (havePartQuestionAnswersChanged) {
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, null);
}
//Save new values //Save new values
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray); context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
}, },
saveCapabilityQuestionAnswers(context, capabilityQuestionAnswers) {
//Save new values
context.commit(storeMutations.UPDATE_CAPABILITY_QUESTION_ANSWERS, capabilityQuestionAnswers);
},
// Misc order actions // Misc order actions
saveServiceLocation(context, serviceLocationInfo) { saveServiceLocation(context, serviceLocationInfo) {
context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo); context.commit(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocationInfo);
@ -868,6 +1024,7 @@ export const actions = {
if (!isSelectedGlassAvailableForVehicle) { if (!isSelectedGlassAvailableForVehicle) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
} }
//Save new values //Save new values

View file

@ -2,6 +2,8 @@ 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"; import { storeActions } from "@/constants/store-actions";
import { experimentTriggers } from "@/constants/experiments";
// Mock global method // Mock global method
globalMethods.callHttpClient = jest.fn(); globalMethods.callHttpClient = jest.fn();
@ -249,6 +251,36 @@ describe("Mutations", () => {
expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true); expect(storeState.order.payment.insuranceCoverage.isVerified).toEqual(true);
}); });
it("updateExperiments, should set experiments", () => {
// Arrange
const storeState = state;
const mockExperimentsList = [
{
universeName: "XYZ",
settings: {
ExperimentSetting: "ExperimentValue"
}
}
]
// Act
mutations.updateExperiments(storeState, mockExperimentsList);
// Assert
expect(storeState.applicationUser.experiments).toEqual(mockExperimentsList);
});
it("updateTriggeredSiteEntry, should set triggeredSiteEntry", () => {
// Arrange
const storeState = state;
// Act
mutations.updateTriggeredSiteEntry(storeState, true);
// Assert
expect(storeState.applicationUser.triggeredSiteEntry).toEqual(true);
});
}); });
describe("Actions", () => { describe("Actions", () => {
@ -574,13 +606,14 @@ describe("Actions", () => {
applicationUser: { applicationUser: {
lastPageVisited: "test-page", lastPageVisited: "test-page",
crmCustomerId: "xxx-xxx-xxx", crmCustomerId: "xxx-xxx-xxx",
saveQuoteId: "xxx-xxx-xxx" savedSessionId: "xxx-xxx-xxx"
} },
}; };
context.state = { context.state = {
order: { order: {
serviceLocation: {}, serviceLocation: {},
customer: {} customer: {},
lineItems: {}
}, },
}; };
@ -630,7 +663,7 @@ describe("Actions", () => {
referralDate: new Date().toUTCString(), referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx", referralCorrelationId: "xxx-xxx-xxx",
accountNumber: "167132", accountNumber: "167132",
saveQuoteId: "xxx-xxx-xxx", savedSessionId: "xxx-xxx-xxx",
crmCustomerId: "xxx-xxx-xxx", crmCustomerId: "xxx-xxx-xxx",
}); });
@ -639,7 +672,7 @@ describe("Actions", () => {
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString()); expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString());
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx"); expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx");
expect(commit).toBeCalledWith(storeMutations.UPDATE_PARENT_ACCT_NUMBER, "167132"); expect(commit).toBeCalledWith(storeMutations.UPDATE_PARENT_ACCT_NUMBER, "167132");
expect(commit).toBeCalledWith(storeMutations.UPDATE_SAVE_QUOTE_ID, "xxx-xxx-xxx"); expect(commit).toBeCalledWith(storeMutations.UPDATE_SAVED_SESSION_ID, "xxx-xxx-xxx");
expect(commit).toBeCalledWith(storeMutations.UPDATE_CRM_CUSTOMER_ID, "xxx-xxx-xxx"); expect(commit).toBeCalledWith(storeMutations.UPDATE_CRM_CUSTOMER_ID, "xxx-xxx-xxx");
}); });
@ -793,13 +826,27 @@ describe("Actions", () => {
// Act // Act
const payload = { isCarIdDifferent: true, isSelectedGlassAvailableForVehicle: false, vehicleInfo: { carId: 'C010101', vin: "XXXXX" }, registrationInfo: { zipCode: "80020" }, serviceLocationInfo: { state: "CO" }, customerEmail: "test@safleite.com" }; 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); actions.saveVinLookup(context, payload);
// Assert // Assert
expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); expect(dispatch).toHaveBeenNthCalledWith(1, storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); expect(dispatch).toHaveBeenNthCalledWith(2, storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
expect(dispatch).toHaveBeenNthCalledWith(3, storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo); expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo);
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo); expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
@ -1041,6 +1088,72 @@ describe("Actions", () => {
}); });
describe("runExperimentsForTrigger", () => {
beforeEach(() => {
mutations.resetState(state);
globalMethods.callHttpClient = jest.fn().mockReturnValue({
data: {
experiments: [
{
mockProperty: "mockValue"
}
]
}
});
})
test("triggerEvent is SiteEntry => set triggeredSiteEntry to true in store", async () => {
// Arrange
const context = state;
context.commit = jest.fn().mockImplementation((storeMutation, value) => mutations[storeMutation](context, value));
context.getters = {
...getters,
applicationUser: getters.applicationUser(context)
};
// Act
await actions.runExperimentsForTrigger(context, {
triggerEvent: experimentTriggers.SITE_ENTRY,
});
// Assert
expect(context.commit).toHaveBeenNthCalledWith(1, storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true);
expect(context.getters.applicationUser.triggeredSiteEntry).toBe(true);
expect(globalMethods.callHttpClient).toHaveBeenCalled();
expect(context.commit).toHaveBeenNthCalledWith(2, storeMutations.UPDATE_EXPERIMENTS, [
{
mockProperty: "mockValue"
}
]);
});
test("triggerEvent is not SiteEntry => triggeredSiteEntry is false in store", async () => {
// Arrange
const context = state;
context.commit = jest.fn().mockImplementation((storeMutation, value) => mutations[storeMutation](context, value));
context.getters = {
...getters,
applicationUser: getters.applicationUser(context)
};
expect(context.commit).toHaveBeenCalledTimes(0);
// Act
await actions.runExperimentsForTrigger(context, {
triggerEvent: "NotSiteEntry",
});
// Assert
expect(context.commit).not.toHaveBeenCalledWith(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, expect.any);
expect(context.getters.applicationUser.triggeredSiteEntry).toBe(false);
expect(globalMethods.callHttpClient).toHaveBeenCalledTimes(1);
expect(context.commit).toHaveBeenNthCalledWith(1, storeMutations.UPDATE_EXPERIMENTS, [
{
mockProperty: "mockValue"
}
]);
});
})
}); });
@ -1138,4 +1251,321 @@ describe("Getters", () => {
expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true); expect(getters.payment(storeState).insuranceCoverage.isVerified).toEqual(true);
}); });
describe("experimentOrder", () => {
test("glassToReplace, glassParts, and otherParts are null > return correct experimentOrder values", () => {
// Arrange
const storeState = state;
const mockStateValues = {
vehicleYear: 1000,
vehicleMake: "CarMake",
vehicleModel: "CarModel",
vehicleStyle: "SuperCoolStyle",
isRepair: true,
numberOfChips: 9999999,
carId: "Gibberish",
serviceCity: "Columbus",
serviceState: "OH-IO",
serviceZipCode: 43215,
parentAccountNumber: "999999",
isCoverageVerified: true,
glassParts: null,
otherParts: null,
glassToReplace: null
}
//Act
mutations.updateYear(storeState, mockStateValues.vehicleYear);
mutations.updateMake(storeState, mockStateValues.vehicleMake);
mutations.updateModel(storeState, mockStateValues.vehicleModel);
mutations.updateStyle(storeState, mockStateValues.vehicleStyle);
mutations.updateIsRepair(storeState, mockStateValues.isRepair);
mutations.updateNumberOfChips(storeState, mockStateValues.numberOfChips);
mutations.updateCarId(storeState, mockStateValues.carId);
mutations.updateServiceLocation(storeState, {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateOtherParts(storeState, mockStateValues.otherParts);
mutations.updateGlassToReplace(storeState, mockStateValues.glassToReplace);
//Assert
expect(getters.experimentOrder(storeState)).toEqual({
vehicleYear: mockStateValues.vehicleYear,
vehicleMake: mockStateValues.vehicleMake,
vehicleModel: mockStateValues.vehicleModel,
vehicleStyle: mockStateValues.vehicleStyle,
isRepair: mockStateValues.isRepair,
numberOfChips: mockStateValues.numberOfChips,
carId: mockStateValues.carId,
serviceCity: mockStateValues.serviceCity,
serviceState: mockStateValues.serviceState,
serviceZipCode: mockStateValues.serviceZipCode,
parentAccountNumber: mockStateValues.parentAccountNumber,
isCoverageVerified: mockStateValues.isCoverageVerified,
orderPartNumbers: [],
orderPartTypes: [],
hasRecalibrationPart: false,
selectedMultiGlass: false,
selectedWindshieldGlass: false,
selectedBackGlass: false,
selectedDriverSideGlass: false,
selectedPassengerSideGlass: false
});
});
test("glassToReplace, glassParts, and otherParts are empty > return correct experimentOrder values", () => {
// Arrange
const storeState = state;
const mockStateValues = {
vehicleYear: 1000,
vehicleMake: "CarMake",
vehicleModel: "CarModel",
vehicleStyle: "SuperCoolStyle",
isRepair: true,
numberOfChips: 9999999,
carId: "Gibberish",
serviceCity: "Columbus",
serviceState: "OH-IO",
serviceZipCode: 43215,
parentAccountNumber: "999999",
isCoverageVerified: true,
glassParts: [],
otherParts: [],
glassToReplace: []
}
//Act
mutations.updateYear(storeState, mockStateValues.vehicleYear);
mutations.updateMake(storeState, mockStateValues.vehicleMake);
mutations.updateModel(storeState, mockStateValues.vehicleModel);
mutations.updateStyle(storeState, mockStateValues.vehicleStyle);
mutations.updateIsRepair(storeState, mockStateValues.isRepair);
mutations.updateNumberOfChips(storeState, mockStateValues.numberOfChips);
mutations.updateCarId(storeState, mockStateValues.carId);
mutations.updateServiceLocation(storeState, {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateOtherParts(storeState, mockStateValues.otherParts);
mutations.updateGlassToReplace(storeState, mockStateValues.glassToReplace);
//Assert
expect(getters.experimentOrder(storeState)).toEqual({
vehicleYear: mockStateValues.vehicleYear,
vehicleMake: mockStateValues.vehicleMake,
vehicleModel: mockStateValues.vehicleModel,
vehicleStyle: mockStateValues.vehicleStyle,
isRepair: mockStateValues.isRepair,
numberOfChips: mockStateValues.numberOfChips,
carId: mockStateValues.carId,
serviceCity: mockStateValues.serviceCity,
serviceState: mockStateValues.serviceState,
serviceZipCode: mockStateValues.serviceZipCode,
parentAccountNumber: mockStateValues.parentAccountNumber,
isCoverageVerified: mockStateValues.isCoverageVerified,
orderPartNumbers: [],
orderPartTypes: [],
hasRecalibrationPart: false,
selectedMultiGlass: false,
selectedWindshieldGlass: false,
selectedBackGlass: false,
selectedDriverSideGlass: false,
selectedPassengerSideGlass: false
});
});
test("Single windshield requiring recalibration is selected > return correct experimentOrder values", () => {
// Arrange
const storeState = state;
const mockStateValues = {
vehicleYear: 1000,
vehicleMake: "CarMake",
vehicleModel: "CarModel",
vehicleStyle: "SuperCoolStyle",
isRepair: false,
numberOfChips: 0,
carId: "Gibberish",
serviceCity: "Columbus",
serviceState: "OH-IO",
serviceZipCode: 43215,
parentAccountNumber: "999999",
isCoverageVerified: false,
glassParts: [
{
partNumber: "WINDSHIELDPARTNUMBER",
description: "This is a windshield",
recalibrationType: "ADAS, maybe",
requiresRecalibration: true,
requiresCapabilityQuestions: false
}
],
otherParts: [
],
glassToReplace: [
{
glassLocation: "Windshield",
glassName: "Single"
}
]
}
//Act
mutations.updateYear(storeState, mockStateValues.vehicleYear);
mutations.updateMake(storeState, mockStateValues.vehicleMake);
mutations.updateModel(storeState, mockStateValues.vehicleModel);
mutations.updateStyle(storeState, mockStateValues.vehicleStyle);
mutations.updateIsRepair(storeState, mockStateValues.isRepair);
mutations.updateNumberOfChips(storeState, mockStateValues.numberOfChips);
mutations.updateCarId(storeState, mockStateValues.carId);
mutations.updateServiceLocation(storeState, {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateOtherParts(storeState, mockStateValues.otherParts);
mutations.updateGlassToReplace(storeState, mockStateValues.glassToReplace);
//Assert
expect(getters.experimentOrder(storeState)).toEqual({
vehicleYear: mockStateValues.vehicleYear,
vehicleMake: mockStateValues.vehicleMake,
vehicleModel: mockStateValues.vehicleModel,
vehicleStyle: mockStateValues.vehicleStyle,
isRepair: mockStateValues.isRepair,
numberOfChips: mockStateValues.numberOfChips,
carId: mockStateValues.carId,
serviceCity: mockStateValues.serviceCity,
serviceState: mockStateValues.serviceState,
serviceZipCode: mockStateValues.serviceZipCode,
parentAccountNumber: mockStateValues.parentAccountNumber,
isCoverageVerified: mockStateValues.isCoverageVerified,
orderPartNumbers: ["WINDSHIELDPARTNUMBER"],
orderPartTypes: ["ADAS, maybe"],
hasRecalibrationPart: true,
selectedMultiGlass: false,
selectedWindshieldGlass: true,
selectedBackGlass: false,
selectedDriverSideGlass: false,
selectedPassengerSideGlass: false
});
});
test("Select multiglass > return correct experimentOrder values", () => {
// Arrange
const storeState = state;
const mockStateValues = {
vehicleYear: 1000,
vehicleMake: "CarMake",
vehicleModel: "CarModel",
vehicleStyle: "SuperCoolStyle",
isRepair: false,
numberOfChips: 0,
carId: "Gibberish",
serviceCity: "Columbus",
serviceState: "OH-IO",
serviceZipCode: 43215,
parentAccountNumber: "999999",
isCoverageVerified: false,
glassParts: [
{
partNumber: "BACKGLASS_PN",
description: "This is a back glass",
recalibrationType: null,
requiresRecalibration: false,
requiresCapabilityQuestions: false
},
{
partNumber: "DRIVERGLASS_PN",
description: "This is a driver side glass",
recalibrationType: null,
requiresRecalibration: false,
requiresCapabilityQuestions: false
},
{
partNumber: "PASSENGERGLASS_PN",
description: "This is a passenger side glass",
recalibrationType: null,
requiresRecalibration: false,
requiresCapabilityQuestions: false
}
],
otherParts: [
],
glassToReplace: [
{
glassLocation: "Rear",
glassName: "Stationary"
},
{
glassLocation: "Driver",
glassName: "Front"
},
{
glassLocation: "Passenger",
glassName: "Front"
},
{
glassLocation: "Passenger",
glassName: "Quarter"
}
]
}
//Act
mutations.updateYear(storeState, mockStateValues.vehicleYear);
mutations.updateMake(storeState, mockStateValues.vehicleMake);
mutations.updateModel(storeState, mockStateValues.vehicleModel);
mutations.updateStyle(storeState, mockStateValues.vehicleStyle);
mutations.updateIsRepair(storeState, mockStateValues.isRepair);
mutations.updateNumberOfChips(storeState, mockStateValues.numberOfChips);
mutations.updateCarId(storeState, mockStateValues.carId);
mutations.updateServiceLocation(storeState, {
city: mockStateValues.serviceCity,
state: mockStateValues.serviceState,
zipCode: mockStateValues.serviceZipCode
});
mutations.updateParentAcctNumber(storeState, mockStateValues.parentAccountNumber);
mutations.updateInsuranceVerifiedStatus(storeState, mockStateValues.isCoverageVerified);
mutations.updateGlassParts(storeState, mockStateValues.glassParts);
mutations.updateOtherParts(storeState, mockStateValues.otherParts);
mutations.updateGlassToReplace(storeState, mockStateValues.glassToReplace);
//Assert
expect(getters.experimentOrder(storeState)).toEqual({
vehicleYear: mockStateValues.vehicleYear,
vehicleMake: mockStateValues.vehicleMake,
vehicleModel: mockStateValues.vehicleModel,
vehicleStyle: mockStateValues.vehicleStyle,
isRepair: mockStateValues.isRepair,
numberOfChips: mockStateValues.numberOfChips,
carId: mockStateValues.carId,
serviceCity: mockStateValues.serviceCity,
serviceState: mockStateValues.serviceState,
serviceZipCode: mockStateValues.serviceZipCode,
parentAccountNumber: mockStateValues.parentAccountNumber,
isCoverageVerified: mockStateValues.isCoverageVerified,
orderPartNumbers: ["BACKGLASS_PN", "DRIVERGLASS_PN", "PASSENGERGLASS_PN"],
orderPartTypes: [],
hasRecalibrationPart: false,
selectedMultiGlass: true,
selectedWindshieldGlass: false,
selectedBackGlass: true,
selectedDriverSideGlass: true,
selectedPassengerSideGlass: true
});
});
})
}); });

View file

@ -2,5 +2,5 @@
//Blue gradient background mixin //Blue gradient background mixin
@mixin blue-gradient { @mixin blue-gradient {
background: linear-gradient(270deg, $blue-500 0%, $blue-700 100%); background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
} }

View file

@ -2,7 +2,7 @@
<div <div
class="alert fade show text-center mb-0 py-2 px-4" class="alert fade show text-center mb-0 py-2 px-4"
role="alert" role="alert"
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]" :class="[isDismissible ? 'alert-dismissible' : '', this.alertClass, this.cssClassNameForCmsWidget]"
> >
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p> <p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph"> <template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
@ -54,7 +54,15 @@ export default {
alert-info (blue) alert-info (blue)
*/ */
alertClass: String, alertClass: String,
cmsWidgetName: String, cmsWidgetName: {
type: String,
default(rawProps) {
if (!rawProps.cmsWidgetName) {
console.log('Error: Missing a CMS Widget Name (required field)');
}
return 'widgetUndefined';
},
},
manualHeadline: String, manualHeadline: String,
manualCopy: String, manualCopy: String,
shouldScrollToOnMount: { shouldScrollToOnMount: {
@ -64,10 +72,10 @@ export default {
}, },
computed: { computed: {
alertHeadline(){ alertHeadline(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'HeadlineText') : this.manualHeadline; return this.manualHeadline ? this.manualHeadline : this.getCmsContent(this.cmsWidgetName, 'HeadlineText');
}, },
alertCopy(){ alertCopy(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy; return this.manualCopy ? this.manualCopy : this.getCmsContent(this.cmsWidgetName, 'BodyText');
}, },
splitAlertCopyForParagraphTag(){ splitAlertCopyForParagraphTag(){
return splitCMSCopyOnParagraphTag(this.alertCopy); return splitCMSCopyOnParagraphTag(this.alertCopy);

View file

@ -54,14 +54,14 @@ export default {
.btn { .btn {
&.btn-primary { &.btn-primary {
position: relative; position: relative;
background: linear-gradient(270deg, $blue-500 0%, $blue-700 100%); background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
border: none; border: none;
border-radius: $border-radius-lg; border-radius: $border-radius-lg;
color: $white; color: $white;
justify-content: center; justify-content: center;
font-weight: 500; font-weight: 500;
@media (hover: hover) { @media (hover: hover) {
background: linear-gradient(270deg, $blue-500 0%, $blue-700 100%); background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
} }
&:focus { &:focus {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700; box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;

View file

@ -1,54 +1,24 @@
<template> <template>
<div <div class="list-group list-button-horizontal d-flex flex-column w-100"
class="list-group list-button-horizontal d-flex flex-column w-100" :class="[(errors.length > 0 || hasError) ? 'has-error' : '', isCashOrInsurance ? 'radio-fancy' : '']"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '' , isCashOrInsurance ? 'radio-fancy' : '']" @keyup.space="triggerButton()" @keyup.up="handleKeyupArrow()" @keyup.down="handleKeyupArrow()"
@keyup.space="triggerButton()" @keyup.left="handleKeyupArrow()" @keyup.right="handleKeyupArrow()">
@keyup.up="handleKeyupArrow()" <input :type="isMultiSelect ? 'checkbox' : 'radio'" :id="buttonID" :name="groupName" :value="value"
@keyup.down="handleKeyupArrow()" :aria-required="isRequired" v-model="checkValue" @change="handleInputChange()" />
@keyup.left="handleKeyupArrow()" <label tabindex="-1" :for="buttonID" :aria-labelledby="buttonID"
@keyup.right="handleKeyupArrow()" class="d-flex flex-column justify-content-center p-3" @mouseup="triggerButton()">
> <span class="m-0" :class="textPosition">
<input {{ buttonLabel }}
:type="isMultiSelect ? 'checkbox' : 'radio'" </span>
:id="buttonID" <span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
:name="groupName" {{ buttonLabelSubCopy }}
:value="value" </span>
:aria-required="isRequired" <span v-if="screenReaderOnlyText" class="sr-only">
v-model="checkValue" {{ screenReaderOnlyText }}
@change="handleInputChange()" </span>
/> <loader v-if="isLoaderDisplayed && selectingInitiatesLoad" :class="[loaderColor, loaderPosition]" />
<label </label>
tabindex="-1" </div>
:for="buttonID"
:aria-labelledby="buttonID"
class="d-flex flex-column justify-content-center p-3"
@mouseup="triggerButton()"
>
<span
class="m-0"
:class="textPosition"
>
{{buttonLabel}}
</span>
<span
v-if="buttonLabelSubCopy"
class="m-0 small"
:class="textPosition"
>
{{ buttonLabelSubCopy }}
</span>
<span
v-if="screenReaderOnlyText"
class="sr-only"
>
{{ screenReaderOnlyText }}
</span>
<loader
v-if="isLoaderDisplayed && selectingInitiatesLoad"
:class="[loaderColor, loaderPosition]"
/>
</label>
</div>
</template> </template>
<script> <script>
@ -79,10 +49,7 @@ export default {
validationRules: String, validationRules: String,
selectedValues: [Array, String], selectedValues: [Array, String],
hasError: Boolean, hasError: Boolean,
clearOnUnmount: { valueToLogType: String,
type: Boolean,
default: true
}
}, },
data() { data() {
return { return {
@ -93,14 +60,8 @@ export default {
created() { created() {
if (Array.isArray(this.selectedValues)) { if (Array.isArray(this.selectedValues)) {
this.checkValue = this.isMultiSelect this.checkValue = this.isMultiSelect
? this.selectedValues.includes(this.value) ? this.selectedValues.includes(this.value)
: this.selectedValues[0]; : this.selectedValues[0];
}
},
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync
if (this.clearOnUnmount) {
this.checkValue = false;
this.handleCheckChange();
} }
}, },
methods: { methods: {
@ -108,7 +69,7 @@ export default {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleInputChange() { handleInputChange() {
if(!this.selectingInitiatesLoad) { if (!this.selectingInitiatesLoad) {
this.handleCheckChange(); this.handleCheckChange();
} }
}, },
@ -117,17 +78,17 @@ export default {
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) { if (!this.selectingInitiatesLoad) {
this.handleCheckChange(); this.handleCheckChange();
} }
}, },
triggerButton() { triggerButton() {
if(this.selectingInitiatesLoad) { if (this.selectingInitiatesLoad) {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
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, this.valueToLogType);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {
@ -175,6 +136,7 @@ export default {
<style lang="scss"> <style lang="scss">
.list-button-horizontal { .list-button-horizontal {
input[type="radio"], input[type="radio"],
input[type="checkbox"] { input[type="checkbox"] {
position: absolute; position: absolute;
@ -182,27 +144,32 @@ export default {
opacity: 0; opacity: 0;
width: 0; width: 0;
&:focus-visible + label { &:focus-visible+label {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
z-index: 2; z-index: 2;
} }
&:focus + label {
&:focus+label {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
z-index: 3; z-index: 3;
} }
&:checked + label {
&:checked+label {
background: $blue-100; background: $blue-100;
box-shadow: 0 0 0 1px $blue; box-shadow: 0 0 0 1px $blue;
outline: none; outline: none;
z-index: 2; z-index: 2;
} }
&:checked:focus + label {
&:checked:focus+label {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
} }
&:checked + label p:first-child {
&:checked+label p:first-child {
font-weight: 500; font-weight: 500;
} }
} }
label { label {
outline: none; outline: none;
position: relative; position: relative;
@ -212,6 +179,7 @@ export default {
border-radius: 0; border-radius: 0;
width: 100%; width: 100%;
color: $gray-600; color: $gray-600;
&:hover { &:hover {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-300; box-shadow: 0 0 0 4px $blue-300;
@ -219,9 +187,11 @@ export default {
z-index: 4 !important; z-index: 4 !important;
} }
} }
+ p {
+p {
display: none; display: none;
} }
span { span {
font-size: .875rem; font-size: .875rem;
} }
@ -233,16 +203,19 @@ export default {
border: 1px solid $blue-700; border: 1px solid $blue-700;
z-index: 2; z-index: 2;
color: $blue; color: $blue;
span { span {
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
} }
} }
label:hover { label:hover {
background-color: $blue-700; background-color: $blue-700;
color: $white; color: $white;
box-shadow: none; box-shadow: none;
} }
input[type="radio"], input[type="radio"],
input[type="checkbox"] { input[type="checkbox"] {
position: absolute; position: absolute;
@ -250,14 +223,16 @@ export default {
opacity: 0; opacity: 0;
width: 0; width: 0;
&:focus-visible + label { &:focus-visible+label {
border-radius: 0.5rem; border-radius: 0.5rem;
z-index: 2; z-index: 2;
} }
&:focus + label {
&:focus+label {
z-index: 3; z-index: 3;
} }
&:checked + label {
&:checked+label {
outline: none; outline: none;
box-shadow: none; box-shadow: none;
color: $white; color: $white;
@ -265,10 +240,12 @@ export default {
border-radius: 0.5rem; border-radius: 0.5rem;
z-index: 5; z-index: 5;
} }
&:checked:focus + label {
&:checked:focus+label {
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700; box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
} }
&:checked + label p:first-child {
&:checked+label p:first-child {
font-weight: 500; font-weight: 500;
} }
} }
@ -298,11 +275,12 @@ export default {
&:first-of-type { &:first-of-type {
.list-button-horizontal.radio-fancy { .list-button-horizontal.radio-fancy {
input[type="radio"] { input[type="radio"] {
&:checked + label { &:checked+label {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
} }
&:checked:focus + label {
&:checked:focus+label {
border-bottom-right-radius: 0.5rem; border-bottom-right-radius: 0.5rem;
border-top-right-radius: 0.5rem; border-top-right-radius: 0.5rem;
} }
@ -313,11 +291,12 @@ export default {
&:last-of-type { &:last-of-type {
.list-button-horizontal.radio-fancy { .list-button-horizontal.radio-fancy {
input[type="radio"] { input[type="radio"] {
&:checked + label { &:checked+label {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-top-left-radius: 0; border-top-left-radius: 0;
} }
&:checked:focus + label {
&:checked:focus+label {
border-bottom-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem; border-top-left-radius: 0.5rem;
} }

View file

@ -187,7 +187,7 @@ describe("list-button.vue", () => {
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: Boolean, buttonId: 'list-card-id'}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: false, buttonId: 'list-card-id'}]);
}); });
@ -204,11 +204,11 @@ describe("list-button.vue", () => {
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], modelValue: ["List Card Checkbox"],
selectedValues: ["Car-Front"] selectedValues: "Car-Front"
}, },
}); });
// Assert // Assert
expect(wrapper.componentVM.checkValue).toEqual("Car-Front"); expect(wrapper.componentVM.checkValue).toEqual(false);
}); });
it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => { it("Should run handleCheckChange if selectingInitiatesLoad is false and handleInputChange is triggered", async () => {

View file

@ -3,6 +3,7 @@
class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2" class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']" :class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@keyup.space="triggerButton" @keyup.space="triggerButton"
@keyup.enter="triggerButton"
@keyup.up="handleKeyupArrow" @keyup.up="handleKeyupArrow"
@keyup.down="handleKeyupArrow" @keyup.down="handleKeyupArrow"
@keyup.left="handleKeyupArrow" @keyup.left="handleKeyupArrow"
@ -14,12 +15,14 @@
:value="value" :value="value"
:aria-required="isRequired" :aria-required="isRequired"
v-model="checkValue" v-model="checkValue"
:checked="checkValue"
@change="handleInputChange" @change="handleInputChange"
:aria-label="value"
> >
<label <label
tabindex="-1" tabindex="-1"
:for="buttonID" :for="buttonID"
:aria-labelledby="buttonID" :aria-label="buttonLabel"
class="d-flex flex-column justify-content-center py-3 px-4" class="d-flex flex-column justify-content-center py-3 px-4"
@mouseup="triggerButton" @mouseup="triggerButton"
> >
@ -52,6 +55,7 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import { toRef } from "vue";
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
@ -73,35 +77,38 @@ export default {
// Field initial value // Field initial value
type: [String, Number], type: [String, Number],
default: "", default: "",
}, },
validationRules: String, validationRules: String,
selectedValues: [Array, String], selectedValues: [Array, String],
hasError: Boolean, hasError: Boolean,
clearOnUnmount: { valueToLogType: String,
type: Boolean,
default: true
}
}, },
data() { data() {
return { return {
isLoaderDisplayed: false, isLoaderDisplayed: false,
checkValue: Boolean, checkValue: false,
}; };
}, },
created() { mounted() {
if (Array.isArray(this.selectedValues)) { if (Array.isArray(this.validateValue)) {
this.checkValue = this.isMultiSelect this.checkValue = this.isValueSelectedByArray(this.selectedValues);
? this.selectedValues.includes(this.value) const isSelectedByValidator = this.isValueSelectedByArray(this.validateValue);
: this.selectedValues[0];
if (this.checkValue != isSelectedByValidator) {
this.handleChange(this.value);
}
} }
}, else {
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync this.checkValue = this.selectedValues == this.value;
if (this.clearOnUnmount) {
this.checkValue = false;
this.handleCheckChange();
} }
}, },
methods: { methods: {
isValueSelectedByArray(arr) {
return this.isMultiSelect
? arr.includes(this.value)
: arr[0];
},
displayLoader() { displayLoader() {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
@ -120,7 +127,7 @@ export default {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
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, this.valueToLogType);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {
@ -154,11 +161,14 @@ export default {
const { const {
handleChange, handleChange,
errors, errors,
} = useField(props.groupName, props.validationRules, fieldOptions); value
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
const validateValue = value;
return { return {
handleChange, handleChange,
errors, errors,
validateValue,
fieldOptions, // only need to expose this for unit test purposes fieldOptions, // only need to expose this for unit test purposes
}; };
}, },

View file

@ -14,7 +14,6 @@ describe("list-card.vue", () => {
groupID: "checkbox-demo-1", groupID: "checkbox-demo-1",
groupName: "Checkbox 1", groupName: "Checkbox 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -33,7 +32,6 @@ describe("list-card.vue", () => {
groupID: "radio-demo-1", groupID: "radio-demo-1",
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -53,7 +51,6 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test", buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -73,7 +70,6 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test", buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -93,7 +89,6 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
buttonLabelSubCopy: "Test", buttonLabelSubCopy: "Test",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -113,7 +108,6 @@ describe("list-card.vue", () => {
groupName: "radio 1", groupName: "radio 1",
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -135,7 +129,6 @@ describe("list-card.vue", () => {
isRequired: true, isRequired: true,
isWide: true, isWide: true,
buttonLabelSubCopy: "", buttonLabelSubCopy: "",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -157,7 +150,6 @@ describe("list-card.vue", () => {
isRequired: true, isRequired: true,
isWide: true, isWide: true,
buttonLabelSubCopy: "Button Subcopy", buttonLabelSubCopy: "Button Subcopy",
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -178,7 +170,6 @@ describe("list-card.vue", () => {
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"],
}, },
}); });
@ -199,13 +190,13 @@ describe("list-card.vue", () => {
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], buttonID: 'list-card-id',
buttonID: 'list-card-id' selectedValues: "List Card Checkbox"
}, },
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: "List Card Checkbox", buttonId: 'list-card-id'}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{value: "List Card Checkbox", checkValue: true, 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 () => {
@ -220,12 +211,11 @@ describe("list-card.vue", () => {
buttonImage: "windshield-damage.svg", buttonImage: "windshield-damage.svg",
isRequired: true, isRequired: true,
isWide: false, isWide: false,
modelValue: ["List Card Checkbox"], selectedValues: "Car-Front"
selectedValues: ["Car-Front"]
}, },
}); });
// Assert // Assert
expect(wrapper.componentVM.checkValue).toEqual("Car-Front"); expect(wrapper.componentVM.checkValue).toEqual(false);
}); });
it("Should set an initial value for validation if selectedValues include the value", async () => { it("Should set an initial value for validation if selectedValues include the value", async () => {
@ -234,7 +224,6 @@ describe("list-card.vue", () => {
propsData: { propsData: {
value: "Windshield", value: "Windshield",
groupName: "radio 1", groupName: "radio 1",
modelValue: ["Windshield"],
selectedValues: ["Windshield"], selectedValues: ["Windshield"],
}, },
}); });

View file

@ -7,11 +7,11 @@
isWide ? 'horizontal' : '', isWide ? 'horizontal' : '',
(errors.length > 0 || hasError) ? 'has-error' : '', (errors.length > 0 || hasError) ? 'has-error' : '',
]" ]"
@keyup.space="triggerButton()" @keyup.space="triggerButton"
@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'"
@ -26,7 +26,7 @@
<label <label
tabindex="-1" tabindex="-1"
:for="buttonID" :for="buttonID"
:aria-labelledby="buttonID" :aria-label="buttonLabel"
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"
@ -87,37 +87,25 @@ export default {
colLength: String, colLength: String,
validationRules: String, validationRules: String,
selectedValues: [Array, String], selectedValues: [Array, String],
modelValue: Object,
hasError: Boolean, hasError: Boolean,
clearOnUnmount: { valueToLogType: String,
type: Boolean,
default: true
}
}, },
data() { data() {
return { return {
checkValue: null, checkValue: null,
} }
}, },
created() { mounted() {
if (Array.isArray(this.selectedValues)) { if (Array.isArray(this.validateValue)) {
this.checkValue = this.isMultiSelect this.checkValue = this.isValueSelectedByArray(this.selectedValues);
? this.selectedValues.includes(this.value) const isSelectedByValidator = this.isValueSelectedByArray(this.validateValue);
: this.selectedValues[0];
} if (this.checkValue != isSelectedByValidator) {
else if (Array.isArray(this.modelValue)) { this.handleChange(this.value);
this.checkValue = this.isMultiSelect }
? this.modelValue.includes(this.value)
: this.modelValue[0];
} }
else { else {
this.checkValue = this.selectedValues == this.value || this.modelValue == this.value; this.checkValue = this.selectedValues == this.value;
}
},
unmounted() { // needed to clear this button's selectedValues if it is removed to keep validation in sync
if (this.clearOnUnmount) {
this.checkValue = false;
this.handleCheckChange();
} }
}, },
computed: { computed: {
@ -134,10 +122,15 @@ export default {
}, },
}, },
methods: { methods: {
isValueSelectedByArray(arr) {
return this.isMultiSelect
? arr.includes(this.value)
: arr[0];
},
handleInputChange() { handleInputChange() {
if(!this.selectingInitiatesLoad) { if(!this.selectingInitiatesLoad) {
this.handleCheckChange(); this.handleCheckChange();
} }
}, },
handleKeyupArrow() { handleKeyupArrow() {
if (this.isMultiSelect) { if (this.isMultiSelect) {
@ -154,7 +147,7 @@ export default {
this.handleCheckChange(); this.handleCheckChange();
} }
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, this.valueToLogType);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {
@ -165,20 +158,13 @@ export default {
this.handleChange(this.value); this.handleChange(this.value);
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent);
}, },
}, },
watch: { watch: {
// Changing this will impact pre-selection data loads on vehicle-parts. // Changing this will impact pre-selection data loads on vehicle-parts.
// If changed, please regression test that vehicle-parts data still loads correctly with previous selections. // If changed, please regression test that vehicle-parts data still loads correctly with previous selections.
modelValue(newVal) {
if (newVal !== undefined) {
this.checkValue = newVal.value;
}
},
selectedValues(newVal) { selectedValues(newVal) {
if (typeof newVal === "string") { if (typeof newVal === "string") {
this.handleChange(newVal);
this.checkValue = newVal == this.value; this.checkValue = newVal == this.value;
} }
else if (newVal !== undefined) { else if (newVal !== undefined) {
@ -204,11 +190,16 @@ export default {
const { const {
handleChange, handleChange,
errors, errors,
value
} = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions); } = useField(toRef(props, "groupName"), toRef(props, "validationRules"), fieldOptions);
// First land on the blank, unselected page, no handleChange
// Land on page with initial values, handleChange
const validateValue = value;
return { return {
handleChange, handleChange,
errors, errors,
validateValue,
fieldOptions, // only need to expose this for unit test purposes fieldOptions, // only need to expose this for unit test purposes
}; };
}, },

View file

@ -1,6 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import radio from "./radio"; import radio from "./radio";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
describe("radio.vue", () => { describe("radio.vue", () => {
it("Should return group name", async () => { it("Should return group name", async () => {
@ -64,6 +65,13 @@ describe("radio.vue", () => {
it("Should emit button value on click", async () => { it("Should emit button value on click", async () => {
// Act // Act
const wrapper = shallowMount(radio, { const wrapper = shallowMount(radio, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
buttonLabel: "Windshield", buttonLabel: "Windshield",
value: "List Card Checkbox", value: "List Card Checkbox",
@ -77,12 +85,20 @@ describe("radio.vue", () => {
}); });
wrapper.vm.handleCheckChange(); wrapper.vm.handleCheckChange();
// Assert // Assert
expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{"buttonID": "List Card Checkbox", value: "List Card Checkbox", checkValue: false}]); expect(wrapper.emitted()["isCheckedChanged"][0]).toEqual([{"buttonID": "List Card Checkbox", value: "List Card Checkbox", checkValue: false}]);;
expect(wrapper.vm.pushEventToGA).toHaveBeenCalled();
}); });
it("Should set checkValue data if selectedButtonIDs has value(s)", async () => { it("Should set checkValue data if selectedButtonIDs has value(s)", async () => {
// Act // Act
const wrapper = shallowMount(radio, { const wrapper = shallowMount(radio, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
buttonLabel: "Windshield", buttonLabel: "Windshield",
buttonID: "List Card Checkbox", buttonID: "List Card Checkbox",

View file

@ -25,6 +25,8 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
name: "radio", name: "radio",
props: { props: {
@ -39,7 +41,8 @@ export default {
screenReaderOnlyText: String, screenReaderOnlyText: String,
selectedValues: String, selectedValues: String,
hasError: Boolean, hasError: Boolean,
validationRules: String validationRules: String,
valueToLogType: String,
}, },
data() { data() {
return { return {
@ -65,6 +68,8 @@ export default {
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent); this.$emit("update:modelValue", emitEvent);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true, this.valueToLogType);
}, },
}, },
setup(props) { setup(props) {