Merge branch 'develop' into feature/CSR-347

This commit is contained in:
Adam Caouette 2022-05-24 09:50:50 -04:00
commit ee7f4843c8
49 changed files with 810 additions and 377 deletions

View file

@ -127,4 +127,45 @@ stages:
indexDeployVariables: indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__) __VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__) __VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
# Prod Build/Deploy
- stage: Prod
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] )
variables:
- group: FixMyGlassProd
jobs:
- deployment: prodBuildDeployment
displayName: Build and Deploy FMG - Prod
environment: digitalCloud-prod
container: node
workspace:
clean: all
strategy:
runOnce:
deploy:
steps:
- checkout: self
clean: true
- template: templates/digital/step-build-vue.yml@AzureDevOps
parameters:
buildOutputDir: dist
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
parameters:
artifactName: vueDist
awsProfile: $(prodDeploymentProfile)
outputPath: /fmg/
deployBuckets:
safelite-prod-fmg-us-east-1:
clearFolder: true
deployFolder: ''
region: us-east-1
appDeployVariables:
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId) cfDistributionId: $(cfDistributionId)

View file

@ -27,13 +27,14 @@ module.exports = {
"!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue", "!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue",
"!src/common-components/dropdown-question/dropdown-question.vue", "!src/common-components/dropdown-question/dropdown-question.vue",
"!src/common-components/textbox-question/textbox-question.vue", "!src/common-components/textbox-question/textbox-question.vue",
"!src/ux-components/alert\alert.vue",
"!src/helpers/validation-rules.js", "!src/helpers/validation-rules.js",
// END // END
], // ! means exclude from coverage. ], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 85, statements: 84,
// Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90 // Got the go ahead from Mark to temporarily lower this. Taking out initialize component made the year,make,model and style coverage drop a bit. Once unit tests for license plate lookup, vin lookup and address lookup are in the coverage should go back up to 90
}, },
}, },

View file

@ -1,6 +1,7 @@
<template> <template>
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<transition :duration="{ enter: 800, leave: 300 }" name="route-fade" mode="out-in"> <transition :duration="{ enter: 200, leave: 200 }" name="route-fade" mode="out-in">
<!-- The above durations should be kept in sync with the global css class "fade-on-route-transition" -->
<component :is="Component" /> <component :is="Component" />
</transition> </transition>
</router-view> </router-view>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -5,8 +5,8 @@
<span class="fs-5 fw-bold w-100">{{ questionText }}</span> <span class="fs-5 fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''"> <fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName">
<legend class="sr-only" :data-focus-target="groupName" tabindex="-1"> <legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }} {{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
</legend> </legend>
<div :class="getComponentWrapperClasses"> <div :class="getComponentWrapperClasses">
@ -40,8 +40,8 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<div class="row form-test-error"> <div class="row form-test-error mt-1">
<error-message class="mt-2" :name="groupName" v-if="!suppressError"></error-message> <error-message :name="groupName" v-if="!suppressError"></error-message>
</div> </div>
</div> </div>
</template> </template>
@ -52,7 +52,6 @@ import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-bu
import listCard from "@/ux-components/list-card/list-card"; import listCard from "@/ux-components/list-card/list-card";
import { ErrorMessage } from 'vee-validate'; import { ErrorMessage } from 'vee-validate';
import radio from "@/ux-components/radio/radio"; import radio from "@/ux-components/radio/radio";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
name: "buttonQuestion", name: "buttonQuestion",
@ -134,8 +133,6 @@ export default {
return answer.Name ? answer.Name : answer; return answer.Name ? answer.Name : answer;
}, },
handleCheckedChanged(val) { handleCheckedChanged(val) {
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, val.value, true);
if(this.isMultiSelect && this.selectedValues) { if(this.isMultiSelect && this.selectedValues) {
// Add or remove item to array of data to emit // Add or remove item to array of data to emit

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="dropdown-question"> <div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<select v-model="selectedOption" <select v-model="selectedOption"
class="form-select" class="form-select"
@ -9,8 +9,7 @@
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
:validationRules="validationRules" :validationRules="validationRules"
@input="handleChange" >
@blur="handleBlur" >
<option v-for="(value, name, index) in options" :value="name" :key="index"> <option v-for="(value, name, index) in options" :value="name" :key="index">
{{ value }} {{ value }}
</option> </option>
@ -40,10 +39,23 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
break;
}
const fieldOptions = { const fieldOptions = {
type: "text", type: "select",
value: props.modelValue, value: props.modelValue,
initialValue: props.modelValue, initialValue: initialValue,
}; };
const { const {

View file

@ -62,6 +62,7 @@ describe("funnel-footer.vue", () => {
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn() getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=>80)
} }
} }

View file

@ -63,7 +63,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight + 24; this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
this.$nextTick(() => { this.$nextTick(() => {
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
}) })
@ -84,7 +84,7 @@ export default {
}, },
methods: { methods: {
onResize() { onResize() {
this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight; this.paddingHeight = this.getFooterInfoBoxHeight();
}, },
updateButtonText(newText) { updateButtonText(newText) {
this.customButtontext = newText; this.customButtontext = newText;

View file

@ -1,97 +1,84 @@
<template> <template>
<div v-show="isModalVisible" class="loading-modal-backdrop"> <div v-if="isModalVisible" class="container-fluid modal-loader">
<div class="loading-modal"> <div class="row g-2 h-100 d-flex align-items-center">
<section class="loading-modal-body"> <div class="container-fluid overflow-hidden">
<div class="modal-icon-container text-center"> <div class="row h-100">
<img class="loader-gif" alt="Loading" src="@/assets/img/loader.gif"> <div class="col text-center tagbg mb-2">
<img class="modal-icon" alt="" src="@/assets/img/windshield.png"> <div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div> </div>
<div class="text-center fw-bold fs-5 loading-modal-text"> <div class="row">
Please wait... <div class="col">
<p class="mb-0 text-center">We're processing your information.</p>
<p class="mb-0 text-center">This could take up to 30 seconds...</p>
</div>
</div> </div>
<div class="text-center fs-5 loading-modal-text"> </div>
This process can take up to 20 seconds.
</div>
</section>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'Modal', name: 'Modal',
data() { data() {
return { return {
isModalVisible: false, isModalVisible: false,
}; };
},
methods: {
showModal() {
//Display modal
this.isModalVisible = true;
//Force page reload on back button
window.addEventListener("pageshow", function(evt) {
if(evt.persisted){
setTimeout(function(){
window.location.reload();
}, 10);
}
}, false);
}, },
methods: { },
showModal() { };
this.isModalVisible = true;
},
},
};
</script> </script>
<style lang="scss"> <style lang="scss">
.loading-modal-backdrop { .modal-loader {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; left: 0;
left: 0; right: 0;
right: 0; bottom: 0;
background-color: #e5e5e5; z-index: 1055;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
outline: 0;
background: $gray-100;
p {
font-family: Roboto;
font-size: 1.125rem;
}
.tagbg {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: center center;
background-size: 96px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1050;
width: 100vw;
height: 100vh;
} }
.loading-modal { .spinner-border {
position: absolute; width: 6.5rem;
background: #ffffff; height: 6.5rem;
padding: 0 0 32px 0; border: 0.45em solid currentColor;
box-shadow: 2px 2px 20px 1px; border-right-color: transparent;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
border-radius: 4px;
top: 48px;
height: 246px;
width: 327px;
} }
}
.loading-modal-body { </style>
position: relative;
padding: 20px 10px;
}
.loading-modal-text {
padding: 0 24px 0 24px;
}
.modal-icon-container {
position: relative;
width: 75px;
height: 75px;
margin: 15px auto;
padding-bottom: 26px;
}
.modal-icon-container img {
position: absolute;
vertical-align: middle;
border: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.loader-gif {
width: 75px;
height: 75px;
}
</style>

View file

@ -2,8 +2,8 @@
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''"> <div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label> <label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" --> <!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" -->
<input <input
v-model="value" v-model.trim="value"
v-maska="mask" v-maska="mask"
:type="type" :type="type"
class="form-control" class="form-control"
@ -14,45 +14,49 @@
:aria-disabled="isDisabled" :aria-disabled="isDisabled"
:disabled="isDisabled" :disabled="isDisabled"
:aria-required="isRequired" :aria-required="isRequired"
autocomplete="do-not-autofill" autocomplete="do-not-autofill"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']" :class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules" :validationRules="validationRules"
@change="handleChange"
@blur="handleChange"
:maxlength="maxLength ? maxLength : '999'"
/> />
<div v-show="errorMessage" class="row mt-2 form-test-error"> <div v-show="errorMessage" class="row my-2 form-test-error">
<span role="alert">{{ errorMessage }}</span> <span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { useField, validate } from "vee-validate";
import { useField } from "vee-validate";
export default { export default {
name: "textbox-question", name: "textbox-question",
props: { props: {
type: { type: {
type: String, type: String,
default: 'text', default: "text",
}, },
placeholderText: { placeholderText: {
type: String, type: String,
default: '', default: "",
}, },
modelValue: String, modelValue: String,
inputId: String, inputId: String,
isDisabled: Boolean, isDisabled: Boolean,
isRequired: Boolean, isRequired: Boolean,
disableAutoFill: Boolean, disableAutoFill: Boolean,
hasIcon: Boolean, // If input has an icon hasIcon: Boolean, // If input has an icon
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
hasError: Boolean, hasError: Boolean,
mask: { mask: {
type: String, type: String,
default: '', default: "",
}, },
validationRules: String, validationRules: String,
cmsWidgetName: String semiAggressiveValidation: Boolean,
cmsWidgetName: String,
maxLength: String,
}, },
setup(props) { setup(props) {
const propsClone = Object.assign({}, props); const propsClone = Object.assign({}, props);
@ -60,7 +64,7 @@ export default {
let initialValue; let initialValue;
switch (typeof modelValue) { switch (typeof modelValue) {
case "number": case "number":
initialValue = modelValue; initialValue = modelValue;
break; break;
default: default:
@ -71,17 +75,11 @@ export default {
const fieldOptions = { const fieldOptions = {
type: "text", type: "text",
value: modelValue, value: modelValue,
initialValue: initialValue initialValue: initialValue,
}; };
const { const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
errorMessage, useField(props.inputId, props.validationRules, fieldOptions);
handleBlur,
handleChange,
meta,
validate,
errors,
} = useField(props.inputId, props.validationRules, fieldOptions);
return { return {
errorMessage, errorMessage,
@ -93,16 +91,16 @@ export default {
}; };
}, },
computed: { computed: {
questionText(){ questionText() {
return this.getCmsContent(this.cmsWidgetName, 'QuestionText'); return this.getCmsContent(this.cmsWidgetName, "QuestionText");
}, },
value: { value: {
get: function() { get: function () {
return this.modelValue; return this.modelValue;
}, },
set: function(newValue) { set: function (newValue) {
this.$emit("update:modelValue", newValue); this.$emit("update:modelValue", newValue);
} },
}, },
labelText: { labelText: {
get: function () { get: function () {
@ -112,7 +110,11 @@ export default {
var words = this.questionText.toString().split(/[ ]+/); var words = this.questionText.toString().split(/[ ]+/);
words.forEach(function (word) { words.forEach(function (word) {
const position = 1; const position = 1;
word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join(''); word = [
word.toString().slice(0, position),
noBreakChar,
word.toString().slice(position),
].join("");
questionText += `${word} `; questionText += `${word} `;
}); });
@ -122,14 +124,19 @@ export default {
} }
return questionText; return questionText;
} },
} },
}, },
watch: { watch: {
value(newValue) { async value(newValue) {
this.handleChange(newValue); if (this.semiAggressiveValidation) {
} const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
} if (result.valid) {
this.handleChange(newValue); // trigger full validation on this field only
}
}
},
},
}; };
</script> </script>
@ -159,6 +166,7 @@ export default {
border: 1px solid $gray-500; border: 1px solid $gray-500;
border-radius: .5rem; border-radius: .5rem;
min-height: 3rem; min-height: 3rem;
padding: 12px 16px;
&::placeholder { &::placeholder {
color: $gray-500; color: $gray-500;
} }
@ -182,4 +190,4 @@ export default {
display: none; display: none;
} }
} }
</style> </style>

View file

@ -26,6 +26,7 @@ const GaLabels = {
ERROR: 'Error', ERROR: 'Error',
LICENSE_PLATE_LOOKUP: 'License_Plate_Look_Up', LICENSE_PLATE_LOOKUP: 'License_Plate_Look_Up',
VIN_LOOKUP: 'Vin_Look_Up', VIN_LOOKUP: 'Vin_Look_Up',
ADDRESS_LOOKUP: 'Address_Look_up',
}; };

View file

@ -18,8 +18,8 @@ const errorMessages = {
LAST_NAME_REQUIRED: "Please enter your last name", LAST_NAME_REQUIRED: "Please enter your last name",
EMAIL_ADDRESS_REQUIRED: "Please enter your email address", EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address", EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP", SERVICE_ZIP_REQUIRED: "Please enter your service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP", SERVICE_ZIP_FORMAT: "Please enter a valid service ZIP",
VIN_REQUIRED: "Please enter your VIN", VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q", VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
OPTION_REQUIRED: "Please select an option", OPTION_REQUIRED: "Please select an option",

View file

@ -23,6 +23,7 @@ const storeMutations = {
UPDATE_REGISTRATION_FIRST_NAME: "updateRegistrationFirstName", UPDATE_REGISTRATION_FIRST_NAME: "updateRegistrationFirstName",
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName", UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
UPDATE_SERVICE_LOCATION_ZIP_CODE: "updateServiceLocationZipCode", UPDATE_SERVICE_LOCATION_ZIP_CODE: "updateServiceLocationZipCode",
UPDATE_SERVICE_LOCATION_STATE: "updateServiceLocationState",
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress", UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
// ORDER MUTATIONS // ORDER MUTATIONS

View file

@ -3,11 +3,19 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
export function getDamageString() { export function getDamageString() {
// If it's a repair it's always a windshield.
const isRepair = store.getters.damage.isRepair;
if(isRepair){
return "windshield"
}
const damageLocations = store.getters.damage.glassToReplace; const damageLocations = store.getters.damage.glassToReplace;
let returnString; let returnString;
if (!damageLocations) { if (!damageLocations) {
return; return;
} }
if (damageLocations.length > 1) { if (damageLocations.length > 1) {
returnString = "match" returnString = "match"
} else { } else {

View file

@ -6,6 +6,8 @@ import { applicationConfig } from "@/constants/application-config";
Will update the cookie if present, or create a new one if not. Will update the cookie if present, or create a new one if not.
*/ */
export function updateOrCreateFunnelCookie() { export function updateOrCreateFunnelCookie() {
const wasClaimRegistrationDelayed = getFunnelCookie()?.HasDelayedClaimRegistration;
// Create the cookie // Create the cookie
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}={}; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()};`; document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}={}; path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()};`;
@ -19,6 +21,7 @@ export function updateOrCreateFunnelCookie() {
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
}); });
} }

View file

@ -305,7 +305,7 @@ describe("getPageToRouteExistingOrderTo", () => {
describe("navigateToHeritageFunnel", () => { describe("navigateToHeritageFunnel", () => {
test("should save order", async () => { test("should save order", async () => {
// Arrange // Arrange
const mockReferralNumber = 2; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
@ -337,7 +337,7 @@ describe("navigateToHeritageFunnel", () => {
test("should go to heritage funnel", async () => { test("should go to heritage funnel", async () => {
// Arrange // Arrange
const mockReferralNumber = 2; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";

View file

@ -38,10 +38,10 @@ export async function saveOrder() {
// Save the referral information back from the store. // Save the referral information back from the store.
await baseMixin.methods.dispatchStoreAction(storeActions.SET_REFERRAL_INFORMATION, { await baseMixin.methods.dispatchStoreAction(storeActions.SET_REFERRAL_INFORMATION, {
referralNumber: savedOrderInfo.data.referralNumber, referralNumber: savedOrderInfo.data.referralNumber.toString(),
referralCorrelationId: savedOrderInfo.data.referralCorrelationId, referralCorrelationId: savedOrderInfo.data.referralCorrelationId,
referralDate: savedOrderInfo.data.referralDate, referralDate: savedOrderInfo.data.referralDate,
accountNumber: savedOrderInfo.data.accountNumber accountNumber: savedOrderInfo.data.accountNumber.toString()
}, false); }, false);
// Update the cookie with the referral information when saved. // Update the cookie with the referral information when saved.

View file

@ -103,11 +103,12 @@ describe("saveOrder", () => {
test("saveOrder => should set state order values", async () => { test("saveOrder => should set state order values", async () => {
// Arrange // Arrange
const mockReferralNumber = 2; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "5";
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber);
const mockData = { const mockData = {
actionList: [ actionList: [
@ -131,7 +132,8 @@ describe("saveOrder", () => {
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.SET_REFERRAL_INFORMATION, { expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.SET_REFERRAL_INFORMATION, {
referralNumber: mockReferralNumber, referralNumber: mockReferralNumber,
referralDate: mockReferralDate, referralDate: mockReferralDate,
referralCorrelationId: mockCorrelationId referralCorrelationId: mockCorrelationId,
accountNumber: mockAccountNumber
}, false); }, false);
}); });

View file

@ -89,11 +89,12 @@ export function removeAllTestCookies() {
}); });
} }
export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate) { export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, accountNumber = "0") {
return { return {
referralNumber: mockReferralNumber, referralNumber: mockReferralNumber,
referralCorrelationId: mockCorrelationId, referralCorrelationId: mockCorrelationId,
referralDate: mockReferralDate referralDate: mockReferralDate,
accountNumber: accountNumber
} }
} }

View file

@ -12,28 +12,28 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<customerQuestions ref="customerQuestions" v-model="customerQuestions" /> <customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert ref="alertVinNotFound" v-show="displayVinNotFoundAlert" <alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
class="my-3" class="mb-4"
cmsWidgetName="AlertVinNotFoundWidget" cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertMatchedDifferentVehicle" v-show="displayMatchedDifferentVehicleAlert" <alert ref="alertMatchedDifferentVehicle" v-if="displayMatchedDifferentVehicleAlert"
class="my-3" class="mb-4"
:manualHeadline="AlertMatchedDifferentVehicleHeader" :manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody" :manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertNonServiceableZip" v-show="displayNonServiceableZipAlert" <alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
class="my-3" class="mb-4"
alertClass="alert-danger" alertClass="alert-danger"
:manualHeadline="AlertNonServiceableZipHeader" :manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody" :manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-show="displayVinLookupByHomeAddressNotAllowedAlert" <alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
class="my-3" class="mb-4"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget" cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger" alertClass="alert-danger"
v-bind:isDismissible="false" v-bind:isDismissible="false"
@ -72,10 +72,8 @@ import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { Form } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { defineRule } from "vee-validate"; import { required, regex } from "@/helpers/validation-rules";
import { required } from "@/helpers/validation-rules";
import { regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
// Supporting files // Supporting files
@ -152,6 +150,16 @@ export default {
this.$route this.$route
); );
}, },
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED,
this.GaLabels.ADDRESS_LOOKUP,
true
);
});
},
getRegistrationAddressFromStore() { getRegistrationAddressFromStore() {
return store.getters.vehicle.registration.address; return store.getters.vehicle.registration.address;
}, },
@ -180,7 +188,7 @@ export default {
this.resetWarningsAndErrors(); this.resetWarningsAndErrors();
// Lookup VIN(s) with the provided address // Lookup VIN(s) with the provided address
const vinLookup = this.lookupVin( const vinLookupPromise = this.lookupVin(
this.customerQuestions.lastName, this.customerQuestions.lastName,
this.customerQuestions.addressQuestions.streetAddress, this.customerQuestions.addressQuestions.streetAddress,
this.customerQuestions.addressQuestions.zipCode, this.customerQuestions.addressQuestions.zipCode,
@ -188,10 +196,10 @@ export default {
); );
// Verify if the service zip code or registration zip code provided is serviceable // Verify if the service zip code or registration zip code provided is serviceable
const zipValidation = this.serviceZipCode ? this.validateZip(this.serviceZipCode) : this.validateZip(this.customerQuestions.addressQuestions.zipCode); const serviceZipValidationPromise = this.serviceZipCode ? this.validateZip(this.serviceZipCode) : this.validateZip(this.customerQuestions.addressQuestions.zipCode);
const vinLookupResponse = await vinLookup; const vinLookupResponse = await vinLookupPromise;
const zipValidationResponse = await zipValidation; const serviceZipValidationResponse = await serviceZipValidationPromise;
if (!vinLookupResponse.data.isStatePermissible) { if (!vinLookupResponse.data.isStatePermissible) {
// State Restrictions forbid lookup by address // State Restrictions forbid lookup by address
@ -201,7 +209,7 @@ export default {
} }
// if the neither the registration zip code or service zip code are not serviceable // if the neither the registration zip code or service zip code are not serviceable
this.isZipServicable = zipValidationResponse.data.isServiceable; this.isZipServicable = serviceZipValidationResponse.data.isServiceable;
if (!this.isZipServicable) { if (!this.isZipServicable) {
this.displayNonServiceableZipAlert = true; this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true; this.showServiceZipField = true;
@ -245,15 +253,24 @@ export default {
// update data if the zip or service zip is servicable // update data if the zip or service zip is servicable
this.updateVehicleInfo(carsFound[0].vin, carFound); this.updateVehicleInfo(carsFound[0].vin, carFound);
this.updateCustomerInfo(); this.updateCustomerInfo(serviceZipValidationResponse.data.state);
} else if (carsFound.length > 1) { } else if (carsFound.length > 1) {
if (!this.isZipServicable) { if (!this.isZipServicable) {
return; return;
} }
// if multiple cars were found
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
if (matchingCars.length === 1) {
// and one and only one of them matches the carId entered, save the vehicle info
// so we can go to the Heritage Funnel directly
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
}
// update data if the zip or service zip is servicable // update data if the zip or service zip is servicable
this.updateCustomerInfo(); this.updateCustomerInfo(serviceZipValidationResponse.data.state);
} }
this.navigateForward(carEntered, carsFound); this.navigateForward(carEntered, carsFound);
@ -285,13 +302,17 @@ export default {
navigateAfterSaveToHeritageFunnel(this.$route); navigateAfterSaveToHeritageFunnel(this.$route);
} }
} else if (carsFound.length > 1) { } else if (carsFound.length > 1) {
// if multiple cars were found // if multiple cars were found
if (carsFound.find(car => car.vehicle.carId === carEntered.carId)) { let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
// and one of them matches the car id entered if (matchingCars.length === 1) {
this.$refs.loadingModal.showModal(); this.$refs.loadingModal.showModal();
// and one and only of them matches the car id entered
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
navigateAfterSaveToHeritageFunnel(this.$route); navigateAfterSaveToHeritageFunnel(this.$route);
} else { } else {
// and there is no match, navigate to "address-vehicles" page // if there are no matches or there are multiple matches, navigate to "address-vehicles" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound); this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
} }
} }
@ -325,7 +346,7 @@ export default {
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageColor);
}, },
updateCustomerInfo() { updateCustomerInfo(serviceState) {
store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress); store.commit(storeMutations.UPDATE_REGISTRATION_ADDRESS, this.customerQuestions.addressQuestions.streetAddress);
store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city); store.commit(storeMutations.UPDATE_REGISTRATION_CITY, this.customerQuestions.addressQuestions.city);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, this.customerQuestions.addressQuestions.state); store.commit(storeMutations.UPDATE_REGISTRATION_STATE, this.customerQuestions.addressQuestions.state);
@ -333,9 +354,9 @@ export default {
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName); store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName); store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
}, },
updateServiceLocationIfNecessary() { updateServiceLocationIfNecessary() {
const serviceLocation = store.getters.order.serviceLocation; const serviceLocation = store.getters.order.serviceLocation;
@ -346,6 +367,9 @@ export default {
} }
} }
}, },
mounted() {
this.attachCustomEvents();
},
computed: { computed: {
AlertNonServiceableZipHeader(){ AlertNonServiceableZipHeader(){
const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode; const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode;
@ -360,13 +384,14 @@ export default {
return text; return text;
}, },
AlertMatchedDifferentVehicleBody(){ AlertMatchedDifferentVehicleBody(){
let content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText");
content = content.replaceAll("{custom:glassText}", getDamageString());
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
content = content.replaceAll("{custom:vinYmmFound}", vinYmmFound); const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
content = content.replaceAll("{custom:vinYmmExpected}", vinYmmExpected); const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
.replaceAll("{custom:glassText}", getDamageString())
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
return content; return content;
}, },

View file

@ -1,32 +1,70 @@
<template> <template>
<div class="row mt-2 mb-4"> <div class="row mt-2 mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="StreetAddressQuestionWidget" v-model="addressModel.streetAddress" ref="autocomplete" inputId="autocomplete" placeholderText="Search" aria-haspopup="" hasIcon disableAutoFill validationRules="street-address-required" /> <textboxQuestion
id="streetAddressField"
cmsWidgetName="StreetAddressQuestionWidget"
v-model="addressModel.streetAddress"
ref="autocomplete"
inputId="autocomplete"
placeholderText="Search"
aria-haspopup=""
hasIcon
disableAutoFill
validationRules="street-address-required"
/>
</div> </div>
</div> </div>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite"> <div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="CityQuestionWidget" v-model="addressModel.city" ref="city" inputId="cbf28188fdf2436688fd735915f7ee56" disableAutoFill validationRules="city-required"/> <textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</transition> </transition>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite"> <div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col"> <div class="col">
<dropdownQuestion cmsWidgetName="StateQuestionWidget" v-model="addressModel.state" ref="state" inputId="8fdf9dc2e13e430eb57529499dceb3eb" :options="stateOptions" disableAutoFill validationRules="state-required" /> <dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required"
/>
</div> </div>
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="ZipQuestionWidget" v-model="addressModel.zipCode" ref="zipCode" inputId="01a9a1c2de0b4c9da8e023c9ae3be498" mask="#####" disableAutoFill validationRules="zip-code-required|zip-code-format"/> <textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</transition> </transition>
<alert ref="alertVerificationWarning" v-show="displayVerificationWarning" <alert ref="alertVerificationWarning" v-if="displayVerificationWarning"
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-show="displayNoMatchWarning" <alert ref="alertNoMatchWarning" v-if="displayNoMatchWarning"
class="mb-4"
cmsWidgetName="AlertNoMatchWarningWidget" cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" v-bind:isDismissible="false"
@ -34,18 +72,19 @@
</template> </template>
<script> <script>
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question"; import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import { applicationConfig } from "@/constants/application-config.js"; import { applicationConfig } from "@/constants/application-config.js";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules"; import { required, regex } from "@/helpers/validation-rules";
import { regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED)); defineRule(
"street-address-required",
required(errorMessages.STREET_ADDRESS_REQUIRED)
);
defineRule("city-required", required(errorMessages.CITY_REQUIRED)); defineRule("city-required", required(errorMessages.CITY_REQUIRED));
defineRule("state-required", required(errorMessages.STATE_REQUIRED)); defineRule("state-required", required(errorMessages.STATE_REQUIRED));
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED)); defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
@ -152,7 +191,8 @@ export default ({
this.addressModel.state !== null & this.addressModel.state !== null &
this.addressModel.zipCode !== null) { this.addressModel.zipCode !== null) {
this.showAddressFields = true; this.showAddressFields = true;
return;
} }
const addressField1 = document.getElementById("autocomplete"); const addressField1 = document.getElementById("autocomplete");
@ -173,14 +213,22 @@ export default ({
); );
// Standard place_changed event handling // Standard place_changed event handling
autocomplete.addListener('place_changed', fillInAddress); const autocompleteListener = window.google.maps.event.addListener(autocomplete, 'place_changed', fillInAddress);
// Wrapping the addressField1 element in the Google Address Autocomplete object // Wrapping the addressField1 element in the Google Address Autocomplete object
// will cause "autocomplete='off'" which Chrome completely ignores. This event // will cause "autocomplete='off'" which Chrome completely ignores. This event
// handler will set the value to something arbitrary so autofill doesn't work. // handler will set the value to something arbitrary so autofill doesn't work.
// https://stackoverflow.com/a/30976223 // https://stackoverflow.com/a/30976223
addressField1.addEventListener("focus", () => { addressField1.addEventListener("focus", () => {
addressField1.setAttribute("autocomplete", "do-not-autofill"); addressField1.setAttribute("autocomplete", "do-not-autofill");
// Make place results box stick to the input on scroll
const streetAddressField = document.getElementById("streetAddressField");
const autocompleteResultsContainer = document.getElementsByClassName("pac-container")[0];
if (autocompleteResultsContainer) {
streetAddressField.appendChild(autocompleteResultsContainer);
}
}) })
addressField1.onchange = function() { addressField1.onchange = function() {
@ -224,13 +272,13 @@ export default ({
for (const component of place.address_components) { for (const component of place.address_components) {
const componentType = component.types[0]; const componentType = component.types[0];
switch (componentType) { switch (componentType) {
case "street_number": { case "street_number": {
self.addressModel.streetAddress = component.long_name; self.addressModel.streetAddress = component.long_name;
break; break;
} }
case "route": { case "route": {
self.addressModel.streetAddress += ' ' + component.short_name; self.addressModel.streetAddress += " " + component.short_name;
break; break;
} }
case "locality": { case "locality": {
@ -245,9 +293,8 @@ export default ({
self.addressModel.zipCode = component.long_name; self.addressModel.zipCode = component.long_name;
break; break;
} }
} }
} }
self.displayVerificationWarning = false; self.displayVerificationWarning = false;
self.displayNoMatchWarning = false; self.displayNoMatchWarning = false;
@ -255,7 +302,15 @@ export default ({
else { else {
self.displayVerificationWarning = true; self.displayVerificationWarning = true;
self.displayNoMatchWarning = false; self.displayNoMatchWarning = false;
} }
// after showing the address fields, disable the address autocomplete
window.google.maps.event.removeListener(autocompleteListener);
window.google.maps.event.clearInstanceListeners(autocomplete);
addressField1.onchange = null;
const pacContainer = document.querySelector(".pac-container");
pacContainer.remove();
} }
}) })
.catch(() => { .catch(() => {
@ -269,8 +324,17 @@ export default ({
}, },
watch: { watch: {
addressModel: { addressModel: {
handler(newValue){ handler(newValue) {
this.displayNoMatchWarning = false; // The first time the address model changes is w
if (!newValue.city &&
!newValue.state &&
!newValue.zipCode) {
return;
}
if (this.displayNoMatchWarning === true) {
this.displayNoMatchWarning = false;
}
}, },
deep: true deep: true
} }
@ -281,4 +345,15 @@ export default ({
alert, alert,
} }
}) })
</script> </script>
<style lang="scss">
#streetAddressField {
position: relative;
.pac-container {
top: 76px !important; // Height of #streetAddressField
left: 0 !important;
}
}
</style>

View file

@ -2,17 +2,39 @@
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" /> <addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" />
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="FirstNameQuestionWidget" v-model="customerModel.firstName" ref="firstName" inputId="08497a2efd9a4a73a70360ab47b4838d" disableAutoFill validationRules="first-name-required" /> <textboxQuestion
cmsWidgetName="FirstNameQuestionWidget"
v-model="customerModel.firstName"
ref="firstName"
inputId="08497a2efd9a4a73a70360ab47b4838d"
disableAutoFill
validationRules="first-name-required"
/>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="LastNameQuestionWidget" v-model="customerModel.lastName" ref="lastName" inputId="0030e56a57e74a4ab92de7fb8e97fec5" disableAutoFill validationRules="last-name-required" /> <textboxQuestion
cmsWidgetName="LastNameQuestionWidget"
v-model="customerModel.lastName"
ref="lastName"
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
disableAutoFill
validationRules="last-name-required"
/>
</div> </div>
</div> </div>
<div class="row mb-4"> <div class="row mb-5">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="EmailAddressQuestionWidget" v-model="customerModel.emailAddress" ref="emailAddress" inputId="00450a91b8964a768ce3992e6feb890f" disableAutoFill validationRules="email-address-required|email-address-format"/> <textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="customerModel.emailAddress"
ref="emailAddress"
inputId="00450a91b8964a768ce3992e6feb890f"
disableAutoFill
validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
</template> </template>
@ -29,7 +51,7 @@ import { errorMessages } from "@/constants/error-messages";
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED)); defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED)); defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED)); defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT)); defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
export default ({ export default ({
name: "customer-questions", name: "customer-questions",

View file

@ -10,7 +10,7 @@
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" /> <vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<alert <alert
ref="alertFoundMultipleVehicles" ref="alertFoundMultipleVehicles"
class="my-5" class="my-5"
alertClass="alert-warning" alertClass="alert-warning"
@ -20,13 +20,13 @@
/> />
<addressVehiclesQuestion <addressVehiclesQuestion
ref="addressVehiclesQuestion" ref="addressVehiclesQuestion"
cmsWidgetName="VehicleConfirmationQuestion" cmsWidgetName="VehicleConfirmationQuestion"
:vehicles="VehiclesForQuestions" :vehicles="VehiclesForQuestions"
validationRules="vehicle-required" validationRules="vehicle-required"
v-model="selectedVehicleVin" v-model="selectedVehicleVin"
:isCarIdDifferent="isCarIdDifferent" :isCarIdDifferent="isCarIdDifferent"
/> />
<div class="alert-provide-vin my-5" v-if="splitAlertProvideVinBodyForLink.length"> <div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy"> <span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="text-body"> <span v-if="copy.includes('routerLink:')" class="text-body">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link> <router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
@ -240,4 +240,4 @@ export default {
line-height: inherit; line-height: inherit;
} }
} }
</style> </style>

View file

@ -86,7 +86,6 @@ describe("license-plate-lookup.vue", () => {
//Assert //Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
}); });

View file

@ -8,28 +8,73 @@
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="LicensePlateNumber" v-model="licensePlate" isRequired inputId="license_plate" validationRules="license-plate-required" /> <textboxQuestion
cmsWidgetName="LicensePlateNumber"
v-model="licensePlate"
isRequired
inputId="license_plate"
validationRules="license-plate-required"
/>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="RegistrationZip" v-model="registrationZip" inputId="zip" mask="#####" validationRules="zip-required" /> <textboxQuestion
cmsWidgetName="RegistrationZip"
v-model="registrationZip"
inputId="zip"
mask="#####"
validationRules="zip-required|zip-format"
/>
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion cmsWidgetName="EmailAddress" v-model="email" inputId="email" validationRules="email-address-required|email-address-format" /> <textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
inputId="email"
validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
<alert class="my-3" :manualHeadline="NoServiceZipHeader" :manualCopy="NoServiceZipBody" v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent" alertClass="alert-danger" /> <alert
class="my-3"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent"
alertClass="alert-danger"
/>
<div class="row my-2"> <div class="row my-2">
<div class="col"> <div class="col">
<textboxQuestion v-if="!isRegistrationZipServicable" cmsWidgetName="ServiceZip" v-model="serviceZip" inputId="serviceZip" validationRules="zip-required" /> <textboxQuestion
v-if="!isRegistrationZipServicable"
cmsWidgetName="ServiceZip"
v-model="serviceZip"
inputId="serviceZip"
validationRules="zip-required|zip-format"
semiAggressiveValidation
/>
</div> </div>
</div> </div>
<alert class="my-3" cmsWidgetName="NoMatchAlertWidget" v-if="!isVinValid" /> <alert class="my-3"
<alert class="my-3" :manualHeadline="MatchedDifferentVehicleAlertHeader" :manualCopy="MatchedDifferentVehicleAlertBody" v-if="isCarIdDifferent" alertClass="alert-warning" /> cmsWidgetName="NoMatchAlertWidget"
<funnelFooter ref="funnelFooter" cmsWidgetName="FunnelFooterWidget" :isForwardActionDisabled="!meta.valid" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" /> v-if="!isVinValid"
alertClass="alert-warning" />
<alert class="my-3"
:manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-if="isCarIdDifferent"
alertClass="alert-warning"
/>
<funnelFooter
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div> </div>
</div> </div>
</Form> </Form>
@ -46,56 +91,25 @@ import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question"; import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue'; import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files // Supporting files
import { import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
fetchCmsContentForPage import { settleAllPromises } from "@/helpers/layout-helper";
} from "@/helpers/cms-content-helper";
import {
settleAllPromises
} from "@/helpers/layout-helper";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
import { import { storeActions } from "@/constants/store-actions";
storeActions import { storeMutations } from "@/constants/store-mutations";
} from "@/constants/store-actions"; import { errorMessages } from "@/constants/error-messages";
import { import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
storeMutations import { getDamageString, isGlassAvailableForCarId, } from "@/helpers/damage-helper";
} from "@/constants/store-mutations"; import { required, regex, } from "@/helpers/validation-rules";
import { import { Form, defineRule, } from "vee-validate";
errorMessages
} from "@/constants/error-messages";
import {
navigateAfterSaveToHeritageFunnel
} from "@/helpers/heritage-integration/navigation-helper";
import {
getDamageString,
isGlassAvailableForCarId,
} from "@/helpers/damage-helper";
import {
required,
regex,
} from "@/helpers/validation-rules";
import {
Form,
defineRule,
} from "vee-validate";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule( defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
"license-plate-required",
required(errorMessages.LICENSE_PLATE_REQUIRED)
);
defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
defineRule( defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
"email-address-required", defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
required(errorMessages.EMAIL_ADDRESS_REQUIRED) defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
);
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
export default { export default {
name: "license-plate-lookup", name: "license-plate-lookup",
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -188,12 +202,12 @@ export default {
}, },
attachCustomEvents() { attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA( this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE], this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED, this.GaActions.SUBMITTED,
this.GaLabels.LICENSE_PLATE_LOOKUP, this.GaLabels.LICENSE_PLATE_LOOKUP,
true true
); );
}); });
}, },
getLicensePlateFromStore() { getLicensePlateFromStore() {
@ -212,20 +226,29 @@ export default {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { async forwardButtonAction() {
const zipValidation = this.serviceZip ?
await this.validateZip(this.serviceZip) : //Call zip validation services
await this.validateZip(this.registrationZip); const registrationZipValidationPromise = this.validateZip(this.registrationZip);
if (!zipValidation.data.isServiceable) { const serviceZipValidationPromise = this.serviceZip ? this.validateZip(this.serviceZip) : null;
const registrationZipValidationResults = await registrationZipValidationPromise;
const serviceZipValidationResults = serviceZipValidationPromise !== null ? (await serviceZipValidationPromise) : registrationZipValidationResults;
//Handle service zip validations
if (!serviceZipValidationResults.data.isServiceable) {
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
this.isVinValid = true; this.isVinValid = true;
this.isRegistrationZipServicable = false; this.isRegistrationZipServicable = false;
this.isCarIdDifferent = false; this.isCarIdDifferent = false;
this.zipToDisplay = this.serviceZip ? this.serviceZip : this.registrationZip; this.zipToDisplay = this.serviceZip ? this.serviceZip : this.registrationZip;
return; return;
} } else if (!this.serviceZip) {
this.serviceZip = this.registrationZip;
}
//Lookup vin
const vinLookup = await this.lookupVin( const vinLookup = await this.lookupVin(
this.licensePlate, this.licensePlate,
zipValidation.data.state registrationZipValidationResults.data.state
).catch(() => { ).catch(() => {
this.$refs.funnelFooter.removeLoader(); this.$refs.funnelFooter.removeLoader();
this.isVinValid = false; this.isVinValid = false;
@ -235,6 +258,7 @@ export default {
this.isCarIdDifferent = this.isCarIdDifferent =
vinLookup.data.vehicle.carId !== store.getters.vehicle.carId; vinLookup.data.vehicle.carId !== store.getters.vehicle.carId;
//Handle changing car
if ( if (
this.isCarIdDifferent && this.isCarIdDifferent &&
vinLookup.data.vehicle.carId !== this.previouslyEnteredCarId vinLookup.data.vehicle.carId !== this.previouslyEnteredCarId
@ -251,12 +275,15 @@ export default {
return; return;
} }
//Save
this.updateCustomerInfo( this.updateCustomerInfo(
vinLookup.data.vin, vinLookup.data.vin,
vinLookup.data.vehicle, vinLookup.data.vehicle,
zipValidation.data.state registrationZipValidationResults.data.state,
serviceZipValidationResults.data.state
); );
//Navigate
this.navigateForward(); this.navigateForward();
}, },
navigateForward() { navigateForward() {
@ -281,12 +308,9 @@ export default {
}); });
}, },
lookupVin(plate, state) { lookupVin(plate, state) {
return baseMixin.methods.dispatchStoreAction( return baseMixin.methods.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_PLATE,{ licensePlate: plate, licenseState: state }, false);
storeActions.LOOKUP_VIN_BY_PLATE,
{ licensePlate: plate, licenseState: state }
);
}, },
updateCustomerInfo(vin, vehicleInfo, registrationState) { updateCustomerInfo(vin, vehicleInfo, registrationState, serviceState) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
} }
@ -304,6 +328,7 @@ export default {
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState); store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.registrationZip); store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.registrationZip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZip); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, serviceState);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
}, },
}, },
@ -335,4 +360,4 @@ export default {
loadingModal, loadingModal,
}, },
}; };
</script> </script>

View file

@ -18,6 +18,7 @@ import store from "@/store";
import { validate } from "vee-validate"; import { validate } from "vee-validate";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { routerParams } from "@/router/router-constants/router-params"; import { routerParams } from "@/router/router-constants/router-params";
import * as cookieHelper from "@/helpers/heritage-integration/cookie-helper";
// Mock our module for promises. // Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({ jest.mock("@/helpers/layout-helper.js", () => ({
@ -29,6 +30,11 @@ jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(), fetchCmsContentForPage: jest.fn(),
})); }));
// Mock getFunnelCookie
jest.mock("@/helpers/heritage-integration/cookie-helper", () => ({
getFunnelCookie: jest.fn(),
}));
// Mock Store // Mock Store
jest.mock("@/store", () => ({ jest.mock("@/store", () => ({
commit: jest.fn(), commit: jest.fn(),
@ -653,6 +659,10 @@ describe("vehicle-damage.vue", () => {
}) })
}); });
describe("vehicle-damage.vue wasDelayedClaimRegistration", () => {
test.todo("if wasDelayedClaimRegistration, should hide back button")
});
// THE FOLLOWING TEST IS NOT NECESSARILY REQUIRED FOR COVERAGE // THE FOLLOWING TEST IS NOT NECESSARILY REQUIRED FOR COVERAGE
// BUT KEEP FOR AN EXAMPLE OF A VALIDATION TEST // BUT KEEP FOR AN EXAMPLE OF A VALIDATION TEST
// //
@ -741,6 +751,7 @@ function setupMocks({pageHeaderWidgetHeaderText, mountOptionsMockData}) {
settleAllPromises.mockImplementation(() => apiPromise); settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve()); fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
cookieHelper.getFunnelCookie = jest.spyOn(cookieHelper, "getFunnelCookie").mockReturnValue({});
//Mock damage initialize methods //Mock damage initialize methods
damageLocationQuestion.methods = { damageLocationQuestion.methods = {

View file

@ -32,7 +32,7 @@
:selectedDamageLocations="selectedDamageLocations" :selectedDamageLocations="selectedDamageLocations"
/> />
<alert <alert
class="my-3" class="my-5"
cmsWidgetName="HasReplacementConflict" cmsWidgetName="HasReplacementConflict"
v-show="hasRepairReplaceConflict" v-show="hasRepairReplaceConflict"
alertClass="alert-danger" alertClass="alert-danger"
@ -87,6 +87,7 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
import { damageLocationsCms } from "@/constants/damage-locations-cms.js"; import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js"; import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -296,12 +297,12 @@ export default {
this.navigateForward(); this.navigateForward();
}, },
navigateForward(){ navigateForward(){
// If vin already exists, navigate directly to vin-lookup // If vin already exists, navigate directly to vin-lookup
if(store.getters.vehicle.vin) { if(store.getters.vehicle.vin) {
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route); this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITH_VIN, this.$route);
return; return;
} }
else { else {
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, this.$route); this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD_WITHOUT_VIN, this.$route);
@ -405,7 +406,7 @@ export default {
return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]; return this.$route.params[this.routerParams.DISPLAY_VEHICLE_CHANGE_ALERT];
}, },
shouldHideBackButton(){ shouldHideBackButton(){
return this.$store.getters.payment.insuranceCoverage.isVerified; return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
} }
}, },

View file

@ -10,11 +10,11 @@
<alert <alert
class="my-3" class="my-3"
cmsWidgetName="NoReplacementAvailableError" cmsWidgetName="NoReplacementAvailableError"
v-show="showNoReplacementAvailableError" 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"
v-model="selectedWindshieldChipCountValues" v-model="selectedWindshieldChipCountValues"
@ -30,9 +30,9 @@
isRequired isRequired
/> />
<alert <alert
class="my-3" class="mt-5"
cmsWidgetName="SplitSingleConflict" cmsWidgetName="SplitSingleConflict"
v-show="hasSplitSingleConflict" v-if="hasSplitSingleConflict"
alertClass="alert-danger" alertClass="alert-danger"
:isDismissible="false" :isDismissible="false"
/> />
@ -56,10 +56,10 @@ defineRule("windshield-chip-count-required", required(errorMessages.WINDSHIELD_C
defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED)); defineRule("windshield-replace-options-required", required(errorMessages.WINSHIELD_REPLACE_OPTIONS_REQUIRED));
defineRule("check-for-repair-and-replace", (value, [otherFieldValue]) => { defineRule("check-for-repair-and-replace", (value, [otherFieldValue]) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.REPAIR.toUpperCase()) && if (value.toString().toUpperCase().includes(damageLocationsSelected.REPAIR.toUpperCase()) &&
otherFieldValue.toString().toUpperCase().includes(damageLocationsSelected.WINDSHIELD.toUpperCase()) && otherFieldValue.toString().toUpperCase().includes(damageLocationsSelected.WINDSHIELD.toUpperCase()) &&
Array.isArray(otherFieldValue) && Array.isArray(otherFieldValue) &&
otherFieldValue.length > 1) otherFieldValue.length > 1)
{ {
return false; return false;
} }
@ -72,8 +72,8 @@ defineRule("repair-only", (value) => {
return false; return false;
}); });
defineRule("prevent-split-and-single-together", (value) => { defineRule("prevent-split-and-single-together", (value) => {
if (value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) && if (value.toString().toUpperCase().includes(damageLocationsSelected.SINGLE.toUpperCase()) &&
(value.toString().toUpperCase().includes(damageLocationsSelected.DRIVER.toUpperCase()) || (value.toString().toUpperCase().includes(damageLocationsSelected.DRIVER.toUpperCase()) ||
value.toString().toUpperCase().includes(damageLocationsSelected.PASSENGER.toUpperCase()))) value.toString().toUpperCase().includes(damageLocationsSelected.PASSENGER.toUpperCase())))
{ {
return false; return false;
@ -145,7 +145,7 @@ export default ({
} }
}, },
isWindshieldDamageLocation() { isWindshieldDamageLocation() {
return this.selectedDamageLocations.some(selectedDamages => return this.selectedDamageLocations.some(selectedDamages =>
{ {
return Boolean(selectedDamages.toUpperCase() === "WINDSHIELD"); return Boolean(selectedDamages.toUpperCase() === "WINDSHIELD");
}); });
@ -193,4 +193,4 @@ export default ({
alert, alert,
}, },
}) })
</script> </script>

View file

@ -88,6 +88,10 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);

View file

@ -88,6 +88,10 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);

View file

@ -101,6 +101,7 @@ export default {
store.commit(storeMutations.UPDATE_IS_REPAIR, null); store.commit(storeMutations.UPDATE_IS_REPAIR, null);
store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null); store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null);
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []); store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
}, },
}, },

View file

@ -107,8 +107,11 @@ export default {
store.commit(storeMutations.UPDATE_STYLE, null); store.commit(storeMutations.UPDATE_STYLE, null);
store.commit(storeMutations.UPDATE_CAR_ID, null); store.commit(storeMutations.UPDATE_CAR_ID, null);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null); store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
// Invokes // Invokes
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);

View file

@ -41,6 +41,9 @@ export default {
font-size: .875rem; font-size: .875rem;
color: $gray-600; color: $gray-600;
} }
li {
line-height: 26px;
}
a { a {
font-size: .875rem; font-size: .875rem;
} }
@ -70,6 +73,7 @@ export default {
overflow: hidden; overflow: hidden;
opacity: 0; opacity: 0;
img { img {
max-width: 420px;
width: 117%; width: 117%;
height: auto; height: auto;
} }

View file

@ -14,7 +14,7 @@
/> />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall"> <div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2"> <div class="row mt-2">
<div class="col"> <div class="col">
<textboxQuestion <textboxQuestion
cmsWidgetName="VinNumber" cmsWidgetName="VinNumber"
@ -24,10 +24,11 @@
disableAutoFill disableAutoFill
validationRules="vin-required|vin-format" validationRules="vin-required|vin-format"
:isDisabled="isVinFieldReadOnly" :isDisabled="isVinFieldReadOnly"
maxLength="17"
/> />
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row mb-2">
<div class="col"> <div class="col">
<vinInformation /> <vinInformation />
</div> </div>
@ -41,7 +42,7 @@
mask="#####" mask="#####"
isRequired isRequired
disableAutoFill disableAutoFill
validationRules="zip-required" validationRules="zip-required|zip-format"
/> />
</div> </div>
</div> </div>
@ -54,34 +55,35 @@
isRequired isRequired
disableAutoFill disableAutoFill
validationRules="email-address-required|email-address-format" validationRules="email-address-required|email-address-format"
semiAggressiveValidation
/> />
</div> </div>
</div> </div>
<alert <alert
class="my-3" class="my-4"
v-model="customAlertData" v-model="customAlertData"
v-if="noMatchAlert" v-if="noMatchAlert"
alertClass="alert-warning" alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget" cmsWidgetName="NoMatchAlertWidget"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader" :manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader"
:manualCopy="PerfectMatchNewVinAlertReadOnlyBody" :manualCopy="PerfectMatchNewVinAlertReadOnlyBody"
v-model="customAlertData" v-model="customAlertData"
v-if="isVinFieldReadOnly" v-if="isVinFieldReadOnly"
alertClass="alert-success" alertClass="alert-success"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="MatchedDifferentVehicleAlertHeader" :manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody" :manualCopy="MatchedDifferentVehicleAlertBody"
v-model="customAlertData" v-model="customAlertData"
v-if="isCarIdDifferent" v-if="isCarIdDifferent && !vinNotFound && !perfectMatchNewVinAlert"
alertClass="alert-warning" alertClass="alert-warning"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="NoServiceZipHeader" :manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody" :manualCopy="NoServiceZipBody"
v-model="customAlertData" v-model="customAlertData"
@ -89,18 +91,19 @@
alertClass="alert-danger" alertClass="alert-danger"
/> />
<alert <alert
class="my-3" class="my-4"
v-model="customAlertData" v-model="customAlertData"
v-if="vinNotFound" v-if="vinNotFound"
alertClass="alert-warning" alertClass="alert-danger"
cmsWidgetName="VinNotFound" cmsWidgetName="VinNotFound"
/> />
<alert <alert
class="my-3" class="my-4"
:manualHeadline="PerfectMatchNewVinAlertHeader"
:manualCopy="PerfectMatchNewVinAlertBody"
v-model="customAlertData" v-model="customAlertData"
v-if="perfectMatchNewVinAlert" v-if="perfectMatchNewVinAlert && !noServiceZip && !vinNotFound && meta.valid && !isVinFieldReadOnly"
alertClass="alert-success" alertClass="alert-success"
cmsWidgetName="PerfectMatchNewVinAlert"
/> />
<funnelFooter <funnelFooter
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
@ -137,25 +140,15 @@ import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "
import { required, regex } from "@/helpers/validation-rules"; import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper"; import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule( defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
"email-address-required", defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
required(errorMessages.EMAIL_ADDRESS_REQUIRED) defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
);
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
defineRule("vin-required", required(errorMessages.VIN_REQUIRED)); defineRule("vin-required", required(errorMessages.VIN_REQUIRED));
defineRule( defineRule("vin-format", regex(/^[a-hA-Hj-nJ-NpPr-zR-Z0-9]{17}$/, errorMessages.VIN_FORMAT));
"vin-format",
regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT)
);
export default { export default {
name: "vin-lookup", name: "vin-lookup",
@ -192,12 +185,21 @@ export default {
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0, vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
}; };
}, },
mounted() {
this.attachCustomEvents();
},
watch: {
vin() {
this.vinNotFound = false;
this.$refs.funnelFooter.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
);
}
},
computed: { computed: {
perfectMatchNewVinAlert() { perfectMatchNewVinAlert() {
const isVinPerfectMatch = this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore(); const isVinPerfectMatch = this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore();
if (isVinPerfectMatch) { this.updateIsCarIdDifferent(isVinPerfectMatch);
this.isCarIdDifferent = false;
}
return isVinPerfectMatch; return isVinPerfectMatch;
}, },
MatchedDifferentVehicleAlertHeader(){ MatchedDifferentVehicleAlertHeader(){
@ -236,7 +238,7 @@ export default {
getIsWindshieldOnly()) getIsWindshieldOnly())
}, },
isVinFieldReadOnly(){ isVinFieldReadOnly(){
return this.$store.getters.payment.insuranceCoverage.isVerified; return this.$store.getters.payment.insuranceCoverage.isVerified || getFunnelCookie().HasDelayedClaimRegistration;
}, },
}, },
methods: { methods: {
@ -259,16 +261,21 @@ export default {
getZipFromStore(){ getZipFromStore(){
return store.getters.order.serviceLocation.zipCode; return store.getters.order.serviceLocation.zipCode;
}, },
attachCustomEvents() { attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => { this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA( this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE], this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED, this.GaActions.SUBMITTED,
this.GaLabels.VINLOOKUP, this.GaLabels.VIN_LOOKUP,
true true
); );
}); });
}, },
updateIsCarIdDifferent(isVinPerfectMatch){
if (isVinPerfectMatch) {
this.isCarIdDifferent = false;
}
},
backButtonAction() { backButtonAction() {
if (store.getters.vehicle.vin) { if (store.getters.vehicle.vin) {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route); this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route);
@ -307,7 +314,7 @@ export default {
this.isCarIdDifferent = true; this.isCarIdDifferent = true;
return; return;
} }
this.updateStore(vehicleLookupResponse.data); this.updateStore(vehicleLookupResponse.data, zipValidationResponse.data);
this.navigateForward(); this.navigateForward();
}, },
navigateForward(){ navigateForward(){
@ -331,7 +338,7 @@ export default {
{ vin } { vin }
); );
}, },
updateStore(carInfo) { updateStore(carInfo, zipInfo) {
if(this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle){ if(this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle){
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
} }
@ -346,6 +353,7 @@ export default {
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, carInfo.imageVifNumber); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, carInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, carInfo.imageVifNumber); store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, carInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.zip); store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.zip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_STATE, zipInfo.state);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email); store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.email);
}, },
}, },

View file

@ -9,12 +9,18 @@ import baseMixin from "@/mixins/base-mixin";
export default { export default {
methods: { methods: {
logPageView(pageEvent) { logPageView(pageEvent) {
// if the user does not have a session id from the content site, do not log.
const sid = getSessionIdValue();
if (sid === '00000000-0000-0000-0000-000000000000' || sid == null) {
return;
}
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: sid,
action: '', action: '',
event: pageEvent, event: pageEvent,
shouldUseSessionId: true, shouldUseSessionId: true,
@ -24,12 +30,18 @@ export default {
}, },
logCustomEvent(category, action, label, value) { logCustomEvent(category, action, label, value) {
// if the user does not have a session id from the content site, do not log.
const sid = getSessionIdValue();
if (sid === '00000000-0000-0000-0000-000000000000' || sid == null) {
return;
}
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: sid,
category: category, category: category,
action: action, action: action,
label: label, label: label,
@ -97,8 +109,9 @@ export default {
}, },
prependActionToMethod(object, method, actionToPrepend) { prependActionToMethod(object, method, actionToPrepend) {
const baseMethod = object[method.name]; const baseMethodName = method.name.startsWith('bound ') ? method.name.substring(6) : method.name ;
object[method.name] = function () { const baseMethod = object[baseMethodName];
object[baseMethodName] = function () {
actionToPrepend.apply(this, arguments); actionToPrepend.apply(this, arguments);
return baseMethod.apply(object, arguments); return baseMethod.apply(object, arguments);
}; };

View file

@ -1,5 +1,5 @@
import analyticsMixin from "@/mixins/analytics-mixin"; import analyticsMixin from "@/mixins/analytics-mixin";
import { setupMocksForJsFiles } 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";
describe("analyticsMixin.js", () => { describe("analyticsMixin.js", () => {
@ -14,6 +14,12 @@ describe("analyticsMixin.js", () => {
} }
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
const testCookieValue = {
sid: '10000000-0000-0000-0000-000000000001'
}
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
analyticsMixin.methods.logPageView(type, payload); analyticsMixin.methods.logPageView(type, payload);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
@ -107,4 +113,17 @@ describe("analyticsMixin.js", () => {
}]); }]);
}); });
test("Obj is not null after action prepended", () => {
//Arrange
const obj = {baseMethodName:"testMethodName", data:"testData"};
const method = {name:"testMethodName", data:"testData" }
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(obj!=null);
});
}); });

View file

@ -42,6 +42,10 @@ export default {
el && el.focus(); el && el.focus();
} }
}, },
getFooterInfoBoxHeight() {
const footerInfoBox = document.querySelector(".footer #infoBox");
return footerInfoBox ? footerInfoBox.offsetHeight : 0;
}
}, },
computed: { computed: {
storeActions() { storeActions() {

View file

@ -21,6 +21,7 @@ import analyticsMixin from "@/mixins/analytics-mixin";
// Components // Components
import ComponentTest from "@/layouts/component-test/component-test.vue"; import ComponentTest from "@/layouts/component-test/component-test.vue";
import FormTest from "@/layouts/form-test/form-test.vue"; import FormTest from "@/layouts/form-test/form-test.vue";
import Modal from "@/common-components/loading-modal/loading-modal.vue";
const routes = [ const routes = [
@ -34,6 +35,11 @@ const routes = [
name: "FormTest", name: "FormTest",
component: FormTest, component: FormTest,
}, },
{
path: "/loading-modal", // This is a temporary route for testing.
name: "Modal",
component: Modal,
},
{ {
path: "/", path: "/",
name: "root", name: "root",
@ -126,7 +132,7 @@ const router = createRouter({
router.afterEach(async (to, from) => { router.afterEach(async (to, from) => {
// Push page view to GA // Push page view to GA
analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]); analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
baseMixin.methods.dispatchStoreAction(storeActions.GET_EXPERIMENTS_BY_USER, { userId: getDeviceIdValue() }) baseMixin.methods.dispatchStoreAction(storeActions.GET_EXPERIMENTS_BY_USER, { userId: getDeviceIdValue() })
.then( (response) => { .then( (response) => {
analyticsMixin.methods.pushExperimentsToDataLayer(response.data); analyticsMixin.methods.pushExperimentsToDataLayer(response.data);
@ -214,13 +220,6 @@ function navigateToUrl(url, optionalQuery = {}) {
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]); externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
} }
/////////////////////////////////////////////////////
// TEMP CODE FOR TESTING WITH SPECIFIC EXPERIMENTS //
/////////////////////////////////////////////////////
if (externalUrl.search.indexOf("corid=") != -1)
externalUrl.search = externalUrl.search + '&experiments=CollectEmailOnQuote=CollectEmailOnQuote_V1=YesCollectEmail_TEST1=true,RemoveServiceAreaPage=ServAreaRemoval_V7=ServAreaNoRemove_V7_TEST=true,VINeducationV2=VINeducation_V2=NoShowVINmodalV2_CONTROL=true,ServicePackages=ServicePackages_V1=NoShowPackages_CONTROL=true,PhotoUploadRedesign=PhotoUploadRedesign_V1=CurrentPhotoUpload_CONTROL=true,ScheduleDetailsServiceType=ScheduleBeforeServiceType_V1=ServTypeThenSched_CONTROL=true';
///////////// END TEMP CODE /////////////////////////
window.location.assign(externalUrl); window.location.assign(externalUrl);
} }
@ -278,4 +277,4 @@ function resetDependentState(component) {
return component.default.methods.resetDependentState(); return component.default.methods.resetDependentState();
} }
export default router; export default router;

View file

@ -153,6 +153,9 @@ export const mutations = {
updateServiceLocationZipCode(state, serviceLocationZip){ updateServiceLocationZipCode(state, serviceLocationZip){
state.order.serviceLocation.zipCode = serviceLocationZip; state.order.serviceLocation.zipCode = serviceLocationZip;
}, },
updateServiceLocationState(state, serviceLocationState){
state.order.serviceLocation.state = serviceLocationState;
},
updateRegistrationFirstName(state, firstName){ updateRegistrationFirstName(state, firstName){
state.order.vehicle.registration.firstName = firstName; state.order.vehicle.registration.firstName = firstName;
}, },
@ -190,6 +193,10 @@ export const mutations = {
state.order.vehicle.style = null; state.order.vehicle.style = null;
state.order.vehicle.carId = null; state.order.vehicle.carId = null;
state.order.vehicle.category = null; state.order.vehicle.category = null;
state.order.vehicle.vin = null;
state.order.vehicle.imageUrl = null;
state.order.vehicle.imageVifNumber = null;
state.order.vehicle.imageColor = null;
}, },
resetDamageState(state) { resetDamageState(state) {
state.order.damage.isRepair = null; state.order.damage.isRepair = null;
@ -566,9 +573,9 @@ export const actions = {
state: order.serviceLocation.state, state: order.serviceLocation.state,
zipCode: order.serviceLocation.zipCode zipCode: order.serviceLocation.zipCode
}, },
referralNumber: order.referralNumber, referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place
referralDate: order.referralDate, referralDate: order.referralDate,
accountNumber: order.accountNumber accountNumber: order.accountNumber?.toString()
}, },
}); });
}, },
@ -578,10 +585,10 @@ export const actions = {
method: endpoints.LoadOrder.method, method: endpoints.LoadOrder.method,
endpoint: endpoints.LoadOrder.url, endpoint: endpoints.LoadOrder.url,
payload: { payload: {
referralNumber: referralNumber, referralNumber: referralNumber?.toString(),
referralDate: referralDate, referralDate: referralDate,
referralCorrelationId: referralCorrelationId, referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber accountNumber: accountNumber?.toString()
}, },
}).then((response) => { }).then((response) => {
context.commit(storeMutations.RESET_STATE); context.commit(storeMutations.RESET_STATE);

View file

@ -12,11 +12,11 @@
} }
.route-fade-enter-active .fade-on-route-transition { .route-fade-enter-active .fade-on-route-transition {
transition: opacity 0.8s ease; transition: opacity 0.2s ease-out; //This duration should be kept in sync with the app.vue <transition> element attribute
} }
.route-fade-leave-active .fade-on-route-transition { .route-fade-leave-active .fade-on-route-transition {
transition: opacity 0.3s ease; transition: opacity 0.2s ease-in; //This duration should be kept in sync with the app.vue <transition> element attribute
} }
.route-fade-enter-from .fade-on-route-transition, .route-fade-enter-from .fade-on-route-transition,

View file

@ -1,8 +1,9 @@
html { html {
.has-error { .has-error {
&.list-button, &.list-button,
&.list-card, &.list-card,
&.list-card.list-button { &.list-card.list-button {
border: none;
color: $red; color: $red;
input[type=checkbox]:focus + label, input[type=checkbox]:focus + label,
input[type=radio]:focus + label { input[type=radio]:focus + label {
@ -13,15 +14,16 @@ html {
} }
&:hover { &:hover {
box-shadow: 0px 0px 0px 4px $red-200; box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px; border-radius: .5rem;
} }
label { label {
border: 1px solid $red; border: 1px solid $red;
border-radius: .5rem;
} }
label:hover { label:hover {
box-shadow: 0px 0px 0px 4px $red-200; box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px; border-radius: 10px;
border: 1px solid $red; border: 1px solid $red;
} }
} }
&.list-button-horizontal { &.list-button-horizontal {
@ -51,12 +53,21 @@ html {
} }
&.textbox-question, &.textbox-question,
&.dropdown-question { &.dropdown-question {
input:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: .5rem;
border: 1px solid $red;
}
input:focus {
box-shadow: 0 0 0 2.5px $red;
}
p { p {
color: $red; color: $red;
} }
input, input,
select { select {
border: 1px solid $red; border: 1px solid transparent;
box-shadow: 0 0 0 1px $red;
&:focus { &:focus {
border: 1px solid transparent; border: 1px solid transparent;
} }
@ -77,12 +88,14 @@ html {
border: 1px solid $gray-500; border: 1px solid $gray-500;
input:not(:focus) { input:not(:focus) {
+ label { + label {
border: none;
box-shadow: 0 0 0 1px $gray-500; box-shadow: 0 0 0 1px $gray-500;
border-radius: .5rem; border-radius: .5rem;
} }
} }
input:checked:focus { input:checked:focus {
+ label { + label {
border: none;
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
border-radius: .5rem; border-radius: .5rem;
} }
@ -114,8 +127,6 @@ html {
color: $red; color: $red;
font-size: .875rem; font-size: .875rem;
font-weight: 500; font-weight: 500;
height: 1.5rem;
margin-top: .25rem !important;
} }
.form-test-invalid { .form-test-invalid {
@ -129,7 +140,6 @@ html {
&.btn.btn-primary:hover, &.btn.btn-primary:hover,
&.btn.btn-primary:focus, &.btn.btn-primary:focus,
&.btn.btn-primary:focus-visible { &.btn.btn-primary:focus-visible {
color: $gray !important;
background: $gray-200; background: $gray-200;
box-shadow: none; box-shadow: none;
} }

View file

@ -66,6 +66,7 @@ describe("alert.vue", () => {
const mockMixin = { const mockMixin = {
methods: { methods: {
getCmsContent: jest.fn() getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=> 80),
} }
} }

View file

@ -5,15 +5,17 @@
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]" :class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
> >
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p> <p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
<p v-if="splitAlertCopyForLink.length"> <template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
<template v-for="copy in splitAlertCopyForLink" :key="copy"> <p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p>
<span v-if="copy.includes('routerLink:')" class="m-0 text-body"> <p class="m-0 text-body small" v-else>
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link> <template v-for="copy in splitCopyForRouterLink(paragraph)" :key="copy">
</span> <span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
<span v-else class="m-0 text-body" v-html="copy"></span> <span v-else>
</template> <router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
</p> </span>
<p v-else class="m-0 text-body" v-html="alertCopy"></p> </template>
</p>
</template>
<button <button
type="button" type="button"
class="btn-close p-2" class="btn-close p-2"
@ -50,6 +52,10 @@ export default {
cmsWidgetName: String, cmsWidgetName: String,
manualHeadline: String, manualHeadline: String,
manualCopy: String, manualCopy: String,
shouldScrollToOnMount: {
type: Boolean,
default: true
},
}, },
computed: { computed: {
alertHeadline(){ alertHeadline(){
@ -58,11 +64,60 @@ export default {
alertCopy(){ alertCopy(){
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy; return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
}, },
splitAlertCopyForLink(){ splitAlertCopyForParagraphTag(){
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed // splits the alertCopy on <p ... > (with or without attributes) and </p>
return this.alertCopy.split(/{(.*?)}/g); // filter removes empty strings that are a result of string.split with regex
} return this.alertCopy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
},
}, },
methods: {
doesCopyContainRouterLink(copy) {
return copy.includes('routerLink:');
},
splitCopyForRouterLink(copy){
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
},
getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
},
getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
},
ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != 'none') {
var footerHeight = this.getFooterInfoBoxHeight();
if (!this.isAlertInViewport(footerHeight)) {
this.scrollContainerToAlert(footerHeight);
}
}
},
isAlertInViewport(footerHeight) {
const rect = this.$el.getBoundingClientRect();
return (
rect.top >= 0 &&
// remove footerHeight from window height to avoid items being hidden behind footer
rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight)
);
},
scrollContainerToAlert(footerHeight) {
// alert position on page + height of alert + footer height
var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight;
// find the div wrapped by the form element - this is the scrollable container
// should be a more future-proof selector in case of CSS class changes
var pageContainerScrollable = document.querySelector('form > div');
pageContainerScrollable.scrollTo(0, scrollToHeight);
},
},
mounted() {
this.ensureAlertIsInViewPort();
}
}; };
</script> </script>

View file

@ -1,6 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import listButtonHorizontal from "./list-button-horizontal"; import listButtonHorizontal from "./list-button-horizontal";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
describe("list-button-horizontal.vue", () => { describe("list-button-horizontal.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
@ -90,6 +91,13 @@ describe("list-button-horizontal.vue", () => {
it("Should return loader enabled true", async () => { it("Should return loader enabled true", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
selectingInitiatesLoad: true, selectingInitiatesLoad: true,
}, },
@ -112,6 +120,13 @@ describe("list-button-horizontal.vue", () => {
it("Should return loader color", async () => { it("Should return loader color", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
loaderColor: "blue", loaderColor: "blue",
selectingInitiatesLoad: true, selectingInitiatesLoad: true,
@ -135,6 +150,13 @@ describe("list-button-horizontal.vue", () => {
it("Should return loader position", async () => { it("Should return loader position", async () => {
// Act // Act
const wrapper = shallowMount(listButtonHorizontal, { const wrapper = shallowMount(listButtonHorizontal, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
loaderPosition: "right", loaderPosition: "right",
selectingInitiatesLoad: true, selectingInitiatesLoad: true,

View file

@ -54,6 +54,7 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
name: "listButtonHorizontal", name: "listButtonHorizontal",
@ -91,6 +92,10 @@ export default {
: this.selectedValues[0]; : this.selectedValues[0];
} }
}, },
unmounted() { // needed to clear this button's selectedValues if it is removed
this.checkValue = false;
this.handleCheckChange();
},
methods: { methods: {
displayLoader() { displayLoader() {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
@ -116,6 +121,7 @@ export default {
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value); this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {

View file

@ -1,6 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import listButton from "./list-button"; import listButton from "./list-button";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
describe("list-button.vue", () => { describe("list-button.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
@ -90,6 +91,13 @@ describe("list-button.vue", () => {
it("Should return loader enabled true", async () => { it("Should return loader enabled true", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
selectingInitiatesLoad: true, selectingInitiatesLoad: true,
}, },
@ -109,6 +117,13 @@ describe("list-button.vue", () => {
it("Should return loader color", async () => { it("Should return loader color", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
loaderColor: "blue", loaderColor: "blue",
selectingInitiatesLoad: true, selectingInitiatesLoad: true,
@ -128,6 +143,13 @@ describe("list-button.vue", () => {
it("Should return loader position", async () => { it("Should return loader position", async () => {
// Act // Act
const wrapper = shallowMount(listButton, { const wrapper = shallowMount(listButton, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
loaderPosition: "right", loaderPosition: "right",
selectingInitiatesLoad: true, selectingInitiatesLoad: true,

View file

@ -54,6 +54,7 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import loader from "@/ux-components/loader/loader"; import loader from "@/ux-components/loader/loader";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
name: "listButton", name: "listButton",
@ -91,6 +92,10 @@ export default {
: this.selectedValues[0]; : this.selectedValues[0];
} }
}, },
unmounted() { // needed to clear this button's selectedValues if it is removed
this.checkValue = false;
this.handleCheckChange();
},
methods: { methods: {
displayLoader() { displayLoader() {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
@ -116,6 +121,7 @@ export default {
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value); this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {

View file

@ -1,6 +1,7 @@
import { shallowMount } from "@vue/test-utils"; import { shallowMount } from "@vue/test-utils";
import listCard from "./list-card"; import listCard from "./list-card";
import { nextTick } from "vue"; import { nextTick } from "vue";
import { GaActions } from "@/constants/analytics";
describe("list-card.vue", () => { describe("list-card.vue", () => {
it("Should return input type checkbox if isMultiSelect is true", async () => { it("Should return input type checkbox if isMultiSelect is true", async () => {
@ -292,8 +293,16 @@ describe("list-card.vue", () => {
}); });
it("Should run handleChange if triggerButton is triggered", async () => { it("Should run handleChange if triggerButton is triggered", async () => {
// Act // Act
const wrapper = shallowMount(listCard, { const wrapper = shallowMount(listCard, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
selectingInitiatesLoad: false, selectingInitiatesLoad: false,
}, },
@ -312,6 +321,13 @@ describe("list-card.vue", () => {
it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => { it("Should run handleCheckChange and displayLoader if triggerButton is triggered and seletingInitiatesLoad is true", async () => {
// Act // Act
const wrapper = shallowMount(listCard, { const wrapper = shallowMount(listCard, {
global: {
mocks: {
'$route': { query: { fmgPage: 'page-name' } },
GaActions: GaActions,
pushEventToGA: jest.fn(),
}
},
propsData: { propsData: {
selectingInitiatesLoad: true, selectingInitiatesLoad: true,
}, },

View file

@ -61,6 +61,7 @@
<script> <script>
import { useField } from "vee-validate"; import { useField } from "vee-validate";
import { queryStrings } from "@/constants/query-strings";
export default { export default {
name: "listCard", name: "listCard",
@ -98,6 +99,10 @@ export default {
: this.selectedValues[0]; : this.selectedValues[0];
} }
}, },
unmounted() { // needed to clear this button's selectedValues if it is removed
this.checkValue = false;
this.handleCheckChange();
},
computed: { computed: {
getLabelClasses() { getLabelClasses() {
if (this.isWide) { if (this.isWide) {
@ -133,6 +138,7 @@ export default {
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value); this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
const emitEvent = { const emitEvent = {