Merged from develop
This commit is contained in:
commit
19888602da
56 changed files with 4134 additions and 265 deletions
|
|
@ -26,7 +26,7 @@ module.exports = {
|
||||||
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: 76,
|
statements: 75,
|
||||||
// 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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ const errorMessages = {
|
||||||
VEHICLE_REQUIRED: "Please select a vehicle",
|
VEHICLE_REQUIRED: "Please select a vehicle",
|
||||||
MOBILE_LOCATION_REQUIRED: "Please enter your service address",
|
MOBILE_LOCATION_REQUIRED: "Please enter your service address",
|
||||||
DATE_REQUIRED: "Please select a date",
|
DATE_REQUIRED: "Please select a date",
|
||||||
|
PHONE_REQUIRED: "Please enter your phone number",
|
||||||
|
PHONE_FORMAT: "Phone number must be 10 digits",
|
||||||
|
YEAR_REQUIRED: "Please select your vehicle year",
|
||||||
|
MAKE_REQUIRED: "Please select your vehicle make",
|
||||||
|
MODEL_REQUIRED: "Please select your vehicle model",
|
||||||
|
STYLE_REQUIRED: "Please select your vehicle style",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { errorMessages };
|
export { errorMessages };
|
||||||
|
|
|
||||||
5
src/constants/package-names.js
Normal file
5
src/constants/package-names.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export const packageNames = {
|
||||||
|
TIER_ONE: "TierOne",
|
||||||
|
TIER_TWO: "TierTwo",
|
||||||
|
TIER_THREE: "TierThree",
|
||||||
|
};
|
||||||
|
|
@ -84,6 +84,7 @@ const storeActions = {
|
||||||
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING:
|
||||||
"saveSupportingItemsSuppressingStateResetting",
|
"saveSupportingItemsSuppressingStateResetting",
|
||||||
SAVE_VAPS: "saveVaps",
|
SAVE_VAPS: "saveVaps",
|
||||||
|
SAVE_CUSTOMER_DETAILS: "saveCustomerDetails",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { storeActions };
|
export { storeActions };
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ const storeMutations = {
|
||||||
|
|
||||||
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
|
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
|
||||||
|
|
||||||
|
//CUSTOMER MUTATIONS
|
||||||
|
UPDATE_CUSTOMER_DETAILS: "updateCustomerDetails",
|
||||||
|
|
||||||
// ORDER MUTATIONS
|
// ORDER MUTATIONS
|
||||||
UPDATE_REFERRAL_NUMBER: "updateReferralNumber",
|
UPDATE_REFERRAL_NUMBER: "updateReferralNumber",
|
||||||
UPDATE_REFERRAL_DATE: "updateReferralDate",
|
UPDATE_REFERRAL_DATE: "updateReferralDate",
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ export default {
|
||||||
classes = "d-flex flex-row p-0";
|
classes = "d-flex flex-row p-0";
|
||||||
break;
|
break;
|
||||||
case "listCard":
|
case "listCard":
|
||||||
classes = "row g-2 justify-content-center";
|
classes = "row g-2 justify-content-center mb-1";
|
||||||
if (this.isWide) {
|
if (this.isWide) {
|
||||||
classes += " flex-column";
|
classes += " flex-column";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
import checkbox from "./checkbox";
|
import checkboxQuestion from "./checkbox-question";
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
|
|
||||||
describe("checkbox.vue", () => {
|
// Mock CMS content
|
||||||
|
const questionText = "Question Text";
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn().mockImplementation(() => {
|
||||||
|
return questionText;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("checkbox-question.vue", () => {
|
||||||
it("Should return checkbox name", async () => {
|
it("Should return checkbox name", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(checkbox, {
|
const wrapper = shallowMount(checkboxQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
checkboxName: "Checkbox",
|
checkboxName: "Checkbox",
|
||||||
},
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -20,10 +31,11 @@ describe("checkbox.vue", () => {
|
||||||
|
|
||||||
it("Should return checkbox id", async () => {
|
it("Should return checkbox id", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(checkbox, {
|
const wrapper = shallowMount(checkboxQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
buttonID: "Checkbox ID",
|
buttonID: "Checkbox ID",
|
||||||
},
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -35,10 +47,11 @@ describe("checkbox.vue", () => {
|
||||||
|
|
||||||
it("Should return tabindex value", async () => {
|
it("Should return tabindex value", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(checkbox, {
|
const wrapper = shallowMount(checkboxQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
tabIndex: "1",
|
tabIndex: "1",
|
||||||
},
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -50,24 +63,11 @@ describe("checkbox.vue", () => {
|
||||||
|
|
||||||
it("Should return label text", async () => {
|
it("Should return label text", async () => {
|
||||||
// Act
|
// Act
|
||||||
const wrapper = shallowMount(checkbox, {
|
const wrapper = shallowMount(checkboxQuestion, {
|
||||||
propsData: {
|
|
||||||
checkboxLabel: "label text",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
const paragraph = wrapper.find("p");
|
|
||||||
|
|
||||||
expect(paragraph.text()).toEqual("label text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should return label text", async () => {
|
|
||||||
// Act
|
|
||||||
const wrapper = shallowMount(checkbox, {
|
|
||||||
propsData: {
|
propsData: {
|
||||||
screenReaderOnlyText: "screenreader text",
|
screenReaderOnlyText: "screenreader text",
|
||||||
},
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
|
<!-- Checkbox groups MUST be wrapped in a <fieldset> and <legend> tag -->
|
||||||
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
|
<div class="form-check ui-checkbox" :class="[hasError ? 'has-error' : '']">
|
||||||
<input
|
<input
|
||||||
|
v-model="value"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
aria-checked="false"
|
aria-checked="false"
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
:tabindex="tabIndex"
|
:tabindex="tabIndex"
|
||||||
:aria-required="isRequired" />
|
:aria-required="isRequired" />
|
||||||
<label class="d-flex align-items-start" :for="buttonID">
|
<label class="d-flex align-items-start" :for="buttonID">
|
||||||
<p v-if="checkboxLabel" class="m-0">{{ checkboxLabel }}</p>
|
<p v-html="checkboxLabelCopy" class="m-0"></p>
|
||||||
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
<span v-if="screenReaderOnlyText" class="sr-only">{{ screenReaderOnlyText }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -18,15 +19,32 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "checkbox",
|
name: "checkboxQuestion",
|
||||||
|
computed: {
|
||||||
|
checkboxLabelCopy() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
checkboxName: String,
|
checkboxName: String,
|
||||||
buttonID: String,
|
buttonID: String,
|
||||||
tabIndex: Number,
|
tabIndex: Number,
|
||||||
checkboxLabel: String,
|
|
||||||
screenReaderOnlyText: String,
|
screenReaderOnlyText: String,
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
hasError: Boolean,
|
hasError: Boolean,
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -956,6 +956,9 @@ export default {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-underline-offset: 4px;
|
text-underline-offset: 4px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.past {
|
.past {
|
||||||
|
|
@ -1018,7 +1021,7 @@ export default {
|
||||||
.btn-link {
|
.btn-link {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 3rem;
|
height: 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -1029,7 +1032,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-test-error {
|
.form-test-error {
|
||||||
margin: 0 auto 2rem auto;
|
margin: 0 auto;
|
||||||
max-width: 414px;
|
max-width: 414px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export default {
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
isRequired: Boolean,
|
isRequired: Boolean,
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
|
placeHolderText: String,
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
hasError: Boolean,
|
hasError: Boolean,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -175,8 +175,7 @@ export default {
|
||||||
}
|
}
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
background-color: $gray-100;
|
|
||||||
box-shadow: 0px -1px 0px rgba(179, 180, 181, 0.3);
|
|
||||||
button {
|
button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// Components
|
||||||
|
import phoneNumberQuestion from "./phone-number-question";
|
||||||
|
|
||||||
|
// Supporting Files
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
|
||||||
|
describe("phone-number-question.vue", () => {
|
||||||
|
it("Should render phoneNumberQuestion sub-component (textbox-question)", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(phoneNumberQuestion, {});
|
||||||
|
|
||||||
|
wrapper.getCmsContent = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(phoneNumber.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit new value when modelValue is changed", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||||
|
propsData: {
|
||||||
|
modelValue: "val",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||||
|
await phoneNumber.setValue("val2");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit new value when modelValue is changed", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||||
|
propsData: {
|
||||||
|
modelValue: "val",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const phoneNumber = wrapper.findComponent({ ref: "phoneNumber" });
|
||||||
|
await phoneNumber.setValue("val2");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should combine external and internal validation rules to pass to textbox-question", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(phoneNumberQuestion, {
|
||||||
|
propsData: {
|
||||||
|
validationRules: "outsideValidation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.validationRulesForTextBoxQuestion).toEqual(
|
||||||
|
"outsideValidation|phone-number-format"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="phone-number-question d-flex flex-column">
|
||||||
|
<textboxQuestion
|
||||||
|
ref="phoneNumber"
|
||||||
|
type="text"
|
||||||
|
:max-length="12"
|
||||||
|
v-model="selectedValue"
|
||||||
|
:mask="mask"
|
||||||
|
:isRequired="isRequired"
|
||||||
|
:validationRules="validationRulesForTextBoxQuestion"
|
||||||
|
cmsWidgetName="PhoneNumberQuestionWidget" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { regex } from "@/helpers/validation-rules";
|
||||||
|
import { defineRule } from "vee-validate";
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
defineRule(
|
||||||
|
"phone-number-format",
|
||||||
|
regex(/^(?=(?:.*\d){10})(?=(?:.*-){2})[\d-]{12}$/, errorMessages.PHONE_FORMAT)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "phoneNumberQuestion",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
validationRules: String,
|
||||||
|
hasError: Boolean,
|
||||||
|
centerErrorMessage: Boolean,
|
||||||
|
modelValue: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
phoneNumber: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedValue: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validationRulesForTextBoxQuestion() {
|
||||||
|
if (!this.validationRules || this.validationRules.length === 0) {
|
||||||
|
return "phone-number-format";
|
||||||
|
} else {
|
||||||
|
return this.validationRules + "|phone-number-format";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mask() {
|
||||||
|
return {
|
||||||
|
mask: "x##-###-####",
|
||||||
|
tokens: {
|
||||||
|
x: {
|
||||||
|
pattern: /[2-9]/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
textboxQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.phone-number-question {
|
||||||
|
label {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
height: 48px;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 0 4px $blue-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -91,5 +91,8 @@ export default {
|
||||||
&.bold {
|
&.bold {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
&.dark {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Components
|
||||||
|
import textareaQuestion from "./textarea-question";
|
||||||
|
|
||||||
|
// Supporting Files
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
|
||||||
|
const maska = jest.fn();
|
||||||
|
|
||||||
|
const questionText = "textareaQuestionText";
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn().mockImplementation(() => {
|
||||||
|
return questionText;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("textarea-question.vue", () => {
|
||||||
|
it("Should render a textarea", async () => {
|
||||||
|
// Arrange
|
||||||
|
const wrapper = shallowMount(textareaQuestion, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
maska: maska,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
modelValue: "",
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.getCmsContent = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const textarea = wrapper.find("textarea");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(textarea.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should emit new value when modelValue is changed", async () => {
|
||||||
|
// Act
|
||||||
|
const wrapper = shallowMount(textareaQuestion, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
maska: maska,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
modelValue: "val",
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
await wrapper.find("textarea").setValue("val2");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.emitted("update:modelValue")).toEqual([["val2"]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
112
src/digital-components/textarea-question/textarea-question.vue
Normal file
112
src/digital-components/textarea-question/textarea-question.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<div class="textarea-question">
|
||||||
|
<div class="label-wrapper mb-1" :aria-label="questionText">
|
||||||
|
<!-- Wrap label and span because v-html prevents v-if from displaying if v-if <span> is inside <label>-->
|
||||||
|
<label for="textarea-question" class="fw-bold" v-html="questionText"></label>
|
||||||
|
<span v-if="!isRequired" class="fw-normal ms-1">(Optional)</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="textareaQuestion"
|
||||||
|
ref="textarea"
|
||||||
|
v-model="value"
|
||||||
|
v-maska="mask"
|
||||||
|
@keyup="updateCount"
|
||||||
|
class="p-4"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
:aria-required="isRequired">
|
||||||
|
</textarea>
|
||||||
|
<p
|
||||||
|
tabindex="0"
|
||||||
|
class="caption mt-2 mb-0"
|
||||||
|
id="charactersRemaining"
|
||||||
|
:class="[urgentCountdown ? 'urgent-countdown' : '']">
|
||||||
|
{{ remainingCount }}/{{ maxLength }} characters remaining
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "textareaQuestion",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
isRequired: Boolean,
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 250,
|
||||||
|
},
|
||||||
|
modelValue: String,
|
||||||
|
},
|
||||||
|
// TODO: At some point in the future we should probably add the tie in to validation here in case the field must be populated for some other use cases
|
||||||
|
setup() {},
|
||||||
|
computed: {
|
||||||
|
questionText() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
get: function () {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set: function (newValue) {
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remainingCount() {
|
||||||
|
return this.maxLength - this.value.length;
|
||||||
|
},
|
||||||
|
urgentCountdown() {
|
||||||
|
return this.remainingCount <= this.maxLength * 0.1 ? true : false;
|
||||||
|
},
|
||||||
|
mask() {
|
||||||
|
// Allow any character but only the max length number of times.
|
||||||
|
return {
|
||||||
|
mask: `x*${this.maxLength}`,
|
||||||
|
tokens: {
|
||||||
|
x: {
|
||||||
|
pattern: /.|\n|\r/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.textarea-question {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
label {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
|
.label-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
label {
|
||||||
|
span {
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid $gray-500;
|
||||||
|
height: 88px;
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 0 0 2.5px $blue;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 0 4px $blue-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
color: $gray-500;
|
||||||
|
&.urgent-countdown {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -44,7 +44,8 @@
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
@blur="handleChange"
|
@blur="handleChange"
|
||||||
:maxlength="maxLength ? maxLength : '999'"
|
:maxlength="maxLength ? maxLength : '999'"
|
||||||
@focus="$emit('focus', $event.target.value)" />
|
@focus="$emit('focus', $event.target.value)"
|
||||||
|
@keydown="keyDownHandler" />
|
||||||
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
|
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
|
||||||
<template v-if="includeImageQuestion">
|
<template v-if="includeImageQuestion">
|
||||||
<template v-if="!isDisabled">
|
<template v-if="!isDisabled">
|
||||||
|
|
@ -118,6 +119,7 @@ export default {
|
||||||
maxFileSize: Number,
|
maxFileSize: Number,
|
||||||
hideInput: Boolean,
|
hideInput: Boolean,
|
||||||
centerErrorMessage: Boolean,
|
centerErrorMessage: Boolean,
|
||||||
|
keyDownHandler: Function,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const uuid = uuidv4();
|
const uuid = uuidv4();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row" :style="`padding-bottom: ${paddingHeight}px`"></div>
|
<div class="row"></div>
|
||||||
<footer class="footer container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
|
<footer class="footer container-fluid g-5 my-5 px-0" id="infoBox">
|
||||||
<div class="row d-flex flex-row-reverse align-items-center vw-100">
|
<div class="row d-flex flex-row-reverse align-items-center vw-100">
|
||||||
<div class="col button-col d-flex" id="stacked">
|
<div class="col button-col d-flex" id="stacked">
|
||||||
<buttonMain
|
<buttonMain
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
:isDisabled="isForwardActionDisabled"
|
:isDisabled="isForwardActionDisabled"
|
||||||
@click-event="buttonClick"
|
@click-event="buttonClick"
|
||||||
data-bs-target="#footerModal"
|
data-bs-target="#footerModal"
|
||||||
|
data-test-id="funnel-footer-main-button"
|
||||||
data-bs-dismiss="modal" />
|
data-bs-dismiss="modal" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
||||||
|
|
@ -46,19 +47,10 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
paddingHeight: 0,
|
|
||||||
customButtontext: "",
|
customButtontext: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
|
|
||||||
this.$nextTick(() => {
|
|
||||||
window.addEventListener("resize", this.onResize);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
window.removeEventListener("resize", this.onResize);
|
|
||||||
},
|
|
||||||
unmounted() {
|
unmounted() {
|
||||||
document.onkeydown = null;
|
document.onkeydown = null;
|
||||||
},
|
},
|
||||||
|
|
@ -73,9 +65,6 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onResize() {
|
|
||||||
this.paddingHeight = this.getFooterInfoBoxHeight();
|
|
||||||
},
|
|
||||||
updateButtonText(newText) {
|
updateButtonText(newText) {
|
||||||
this.customButtontext = newText;
|
this.customButtontext = newText;
|
||||||
},
|
},
|
||||||
|
|
@ -101,6 +90,7 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.footer {
|
.footer {
|
||||||
|
overflow: visible;
|
||||||
display: flex;
|
display: flex;
|
||||||
a {
|
a {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
239
src/helpers/service-package-helper.js
Normal file
239
src/helpers/service-package-helper.js
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
||||||
|
import { packageNames } from "@/constants/package-names";
|
||||||
|
|
||||||
|
export function containsLineItemWithPartType(typeToFind, itemsToSearch) {
|
||||||
|
const partTypeMatches = findLineItemsWithPartType(typeToFind, itemsToSearch);
|
||||||
|
return !!partTypeMatches?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findLineItemsWithPartType(typeToFind, itemsToSearch) {
|
||||||
|
const partTypeMatches = itemsToSearch?.filter(
|
||||||
|
(lineItem) => lineItem.partType.toUpperCase() === typeToFind.toUpperCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return partTypeMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVapsLineItems(availableLineItems, vapTypes) {
|
||||||
|
let lineItems = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < vapTypes.length; i++) {
|
||||||
|
lineItems.push(...findLineItemsWithPartType(vapTypes[i], availableLineItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function containsGlassPieceWithLocation(locationToFind, glassPiecesToSearch) {
|
||||||
|
const glassLocationMatches = glassPiecesToSearch?.filter(
|
||||||
|
(glassPiece) => glassPiece.glassLocation.toUpperCase() === locationToFind.toUpperCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
return !!glassLocationMatches?.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailablePackages(glassToReplace, availableLineItems, isRepair) {
|
||||||
|
let tierOneVaps = getPackageContents(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_ONE
|
||||||
|
);
|
||||||
|
let tierTwoVaps = getPackageContents(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_TWO
|
||||||
|
);
|
||||||
|
let tierThreeVaps = getPackageContents(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_THREE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tierTwoVaps.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
packageName: packageNames.TIER_ONE,
|
||||||
|
vapTypes: tierOneVaps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: packageNames.TIER_THREE,
|
||||||
|
vapTypes: tierThreeVaps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
packageName: packageNames.TIER_ONE,
|
||||||
|
vapTypes: tierOneVaps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: packageNames.TIER_TWO,
|
||||||
|
vapTypes: tierTwoVaps,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageName: packageNames.TIER_THREE,
|
||||||
|
vapTypes: tierThreeVaps,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPackageContents(glassToReplace, availableLineItems, isRepair, targetTier) {
|
||||||
|
let vaps = [];
|
||||||
|
|
||||||
|
if (shouldFrontWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
|
||||||
|
vaps.push(partTypeStrings.FRONT_WIPER);
|
||||||
|
}
|
||||||
|
if (shouldRearWipersBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
|
||||||
|
vaps.push(partTypeStrings.REAR_WIPER);
|
||||||
|
}
|
||||||
|
if (shouldRainDefenseBeAvailable(glassToReplace, availableLineItems, isRepair, targetTier)) {
|
||||||
|
vaps.push(partTypeStrings.RAIN_DEFENSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldFrontWipersBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
targetTier
|
||||||
|
) {
|
||||||
|
const frontWiperIsAvailable = containsLineItemWithPartType(
|
||||||
|
partTypeStrings.FRONT_WIPER,
|
||||||
|
availableLineItems
|
||||||
|
);
|
||||||
|
const isFrontWindshieldTask =
|
||||||
|
isRepair || containsGlassPieceWithLocation(glassLocations.WINDSHIELD, glassToReplace);
|
||||||
|
|
||||||
|
switch (targetTier) {
|
||||||
|
case packageNames.TIER_TWO:
|
||||||
|
return frontWiperIsAvailable && isFrontWindshieldTask;
|
||||||
|
case packageNames.TIER_THREE:
|
||||||
|
return frontWiperIsAvailable;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldRearWipersBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
targetTier
|
||||||
|
) {
|
||||||
|
const rearWiperIsAvailable = containsLineItemWithPartType(
|
||||||
|
partTypeStrings.REAR_WIPER,
|
||||||
|
availableLineItems
|
||||||
|
);
|
||||||
|
const frontWiperIsAvailable = containsLineItemWithPartType(
|
||||||
|
partTypeStrings.FRONT_WIPER,
|
||||||
|
availableLineItems
|
||||||
|
);
|
||||||
|
const isRearWindshieldTask = containsGlassPieceWithLocation(
|
||||||
|
glassLocations.REAR,
|
||||||
|
glassToReplace
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (targetTier) {
|
||||||
|
case packageNames.TIER_TWO:
|
||||||
|
return rearWiperIsAvailable && isRearWindshieldTask;
|
||||||
|
case packageNames.TIER_THREE:
|
||||||
|
return rearWiperIsAvailable && (isRearWindshieldTask || !frontWiperIsAvailable);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldRainDefenseBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
targetTier
|
||||||
|
) {
|
||||||
|
const isTierThree = targetTier === packageNames.TIER_THREE;
|
||||||
|
const frontWipersInTierTwo = shouldFrontWipersBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_TWO
|
||||||
|
);
|
||||||
|
const frontWipersInTierThree = shouldFrontWipersBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_THREE
|
||||||
|
);
|
||||||
|
const rearWipersInTierTwo = shouldRearWipersBeAvailable(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
packageNames.TIER_TWO
|
||||||
|
);
|
||||||
|
|
||||||
|
return isTierThree && !(rearWipersInTierTwo && !frontWipersInTierTwo && frontWipersInTierThree);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLowestTierForType(glassToReplace, availableLineItems, isRepair, partType) {
|
||||||
|
const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair);
|
||||||
|
for (let i = 0; i < packages.length; i++) {
|
||||||
|
for (let j = 0; j < packages[i].vapTypes.length; j++) {
|
||||||
|
if (packages[i].vapTypes[j].toUpperCase() === partType.toUpperCase()) {
|
||||||
|
return packages[i].packageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageNames.TIER_ONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHighestRequiredTier(glassToReplace, availableLineItems, isRepair, vaps) {
|
||||||
|
let currentHighestTier = packageNames.TIER_ONE;
|
||||||
|
for (let i = 0; i < vaps.length; i++) {
|
||||||
|
const lowestTierForItem = getLowestTierForType(
|
||||||
|
glassToReplace,
|
||||||
|
availableLineItems,
|
||||||
|
isRepair,
|
||||||
|
vaps[i].partType
|
||||||
|
);
|
||||||
|
|
||||||
|
currentHighestTier = maxTier(currentHighestTier, lowestTierForItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentHighestTier;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHighestFullySatisfiedTier(glassToReplace, availableLineItems, isRepair, vaps) {
|
||||||
|
const packages = getAvailablePackages(glassToReplace, availableLineItems, isRepair);
|
||||||
|
let highestSatisfiedPackage = packageNames.TIER_ONE;
|
||||||
|
|
||||||
|
for (let i = 0; i < packages.length; i++) {
|
||||||
|
let isPackageSatisfied = true;
|
||||||
|
for (let j = 0; j < packages[i].vapTypes.length; j++) {
|
||||||
|
isPackageSatisfied =
|
||||||
|
isPackageSatisfied && containsLineItemWithPartType(packages[i].vapTypes[j], vaps);
|
||||||
|
}
|
||||||
|
if (isPackageSatisfied) {
|
||||||
|
highestSatisfiedPackage = packages[i].packageName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return highestSatisfiedPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxTier(tierA, tierB) {
|
||||||
|
if (tierA === packageNames.TIER_THREE || tierB === packageNames.TIER_THREE) {
|
||||||
|
return packageNames.TIER_THREE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tierA === packageNames.TIER_TWO || tierB === packageNames.TIER_TWO) {
|
||||||
|
return packageNames.TIER_TWO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageNames.TIER_ONE;
|
||||||
|
}
|
||||||
1023
src/helpers/service-package-helper.spec.js
Normal file
1023
src/helpers/service-package-helper.spec.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -53,7 +53,7 @@
|
||||||
v-bind:isDismissible="false" />
|
v-bind:isDismissible="false" />
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
|
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
|
||||||
<div class="row mb-4">
|
<div class="row mt-4 mb-1">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<textboxQuestion
|
<textboxQuestion
|
||||||
cmsWidgetName="ServiceZipQuestionWidget"
|
cmsWidgetName="ServiceZipQuestionWidget"
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
validationRules="email-address-required|email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-4">
|
<div class="row mb-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
|
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
58
src/layouts/customer-details/customer-details.spec.js
Normal file
58
src/layouts/customer-details/customer-details.spec.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Components
|
||||||
|
import customerDetails from "@/layouts/customer-details/customer-details.vue";
|
||||||
|
|
||||||
|
// Supporting Files
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
||||||
|
navigateToHeritageFunnel: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock our module for promises.
|
||||||
|
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||||
|
settleAllPromises: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("customer-details.vue", () => {
|
||||||
|
describe("navigation", () => {
|
||||||
|
test("if the back button is clicked, navigate back", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.backButtonAction();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("if the continue button is clicked, navigate forward", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(navigateToHeritage.navigateToHeritageFunnel).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks() {
|
||||||
|
const wrapper = shallowMount(
|
||||||
|
customerDetails,
|
||||||
|
getMountOptions({
|
||||||
|
router: {
|
||||||
|
navigate: jest.fn(),
|
||||||
|
navigate: jest.fn(),
|
||||||
|
navigateWithSaving: jest.fn(),
|
||||||
|
navigateWithoutSaving: jest.fn(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
144
src/layouts/customer-details/customer-details.vue
Normal file
144
src/layouts/customer-details/customer-details.vue
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||||
|
<div class="page-container-grouped-styles">
|
||||||
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||||
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="my-5" />
|
||||||
|
<textboxQuestion
|
||||||
|
class="mb-4"
|
||||||
|
cmsWidgetName="FirstNameWidget"
|
||||||
|
v-model="firstName"
|
||||||
|
ref="firstName"
|
||||||
|
customInputId="firstName"
|
||||||
|
validationRules="first-name-required" />
|
||||||
|
<textboxQuestion
|
||||||
|
class="mb-4"
|
||||||
|
cmsWidgetName="LastNameWidget"
|
||||||
|
v-model="lastName"
|
||||||
|
ref="lastName"
|
||||||
|
customInputId="lastName"
|
||||||
|
validationRules="last-name-required" />
|
||||||
|
<textboxQuestion
|
||||||
|
class="mb-4"
|
||||||
|
cmsWidgetName="emailQuestionWidget"
|
||||||
|
v-model="emailAddress"
|
||||||
|
inputId="email"
|
||||||
|
validationRules="email-address-required|email-address-format" />
|
||||||
|
<phoneNumberQuestion
|
||||||
|
class="mb-4"
|
||||||
|
cmsWidgetName="phoneNumberQuestionWidget"
|
||||||
|
v-model="phoneNumber"
|
||||||
|
isRequired
|
||||||
|
validationRules="phone-number-required" />
|
||||||
|
<checkboxQuestion
|
||||||
|
class="mb-5"
|
||||||
|
cmsWidgetName="TextMeQuestionWidget"
|
||||||
|
v-model="textMeUpdates" />
|
||||||
|
<textareaQuestion
|
||||||
|
class="mb-4"
|
||||||
|
v-model="techNotes"
|
||||||
|
cmsWidgetName="TextAreaContentWidget"
|
||||||
|
maxLength="250" />
|
||||||
|
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
|
||||||
|
<funnel-footer
|
||||||
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
ref="funnelFooter"
|
||||||
|
:isForwardActionDisabled="!meta.valid"
|
||||||
|
@back-clicked="backButtonAction"
|
||||||
|
@ForwardClicked="forwardButtonAction" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
|
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||||
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
|
import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
|
||||||
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||||
|
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||||
|
//Supporting files
|
||||||
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import { routerParams } from "@/router/router-constants/router-params";
|
||||||
|
import { required, regex } from "@/helpers/validation-rules";
|
||||||
|
import { Form, defineRule } from "vee-validate";
|
||||||
|
import { useField, validate } from "vee-validate";
|
||||||
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
|
|
||||||
|
// DEFINE VALIDATION RULES
|
||||||
|
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||||
|
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||||
|
defineRule("phone-number-required", required(errorMessages.PHONE_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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "customer-details",
|
||||||
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
|
{
|
||||||
|
resultKey: "cmsContent",
|
||||||
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
// Call the "next" function to complete the transition to this page.
|
||||||
|
next((vm) => {
|
||||||
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
techNotes: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
emailAddress: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
textMeUpdates: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
textAreaLabelCopy() {
|
||||||
|
return this.getCmsContent("TextAreaContentWidget", "QuestionText");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
backButtonAction() {
|
||||||
|
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||||
|
},
|
||||||
|
forwardButtonAction() {
|
||||||
|
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
funnelHeader,
|
||||||
|
funnelSubHeader,
|
||||||
|
textareaQuestion,
|
||||||
|
textboxQuestion,
|
||||||
|
funnelFooter,
|
||||||
|
Form,
|
||||||
|
textBlock,
|
||||||
|
phoneNumberQuestion,
|
||||||
|
checkboxQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -54,10 +54,9 @@
|
||||||
:manualCopy="AlertNonServiceableZipBody"
|
:manualCopy="AlertNonServiceableZipBody"
|
||||||
alertClass="alert-danger"
|
alertClass="alert-danger"
|
||||||
v-bind:isDismissible="false" />
|
v-bind:isDismissible="false" />
|
||||||
<div class="row my-2">
|
<div class="row mt-2" v-if="showServiceZipField">
|
||||||
<div class="col">
|
<div class="col mb-1">
|
||||||
<textboxQuestion
|
<textboxQuestion
|
||||||
v-if="showServiceZipField"
|
|
||||||
cmsWidgetName="ServiceZipQuestionWidget"
|
cmsWidgetName="ServiceZipQuestionWidget"
|
||||||
v-model="serviceZipCode"
|
v-model="serviceZipCode"
|
||||||
inputId="serviceZipCode"
|
inputId="serviceZipCode"
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,11 @@ describe("quote.vue", () => {
|
||||||
},
|
},
|
||||||
referralNumber: "1234567",
|
referralNumber: "1234567",
|
||||||
},
|
},
|
||||||
|
payment: {
|
||||||
|
insuranceCoverage: {
|
||||||
|
isVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||||
|
|
@ -185,6 +190,11 @@ describe("quote.vue", () => {
|
||||||
glassParts: ["item", "item2"],
|
glassParts: ["item", "item2"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
payment: {
|
||||||
|
insuranceCoverage: {
|
||||||
|
isVerified: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ export default {
|
||||||
(store.getters.order.damage.isRepair ||
|
(store.getters.order.damage.isRepair ||
|
||||||
(store.getters.order.lineItems?.glassParts != null &&
|
(store.getters.order.lineItems?.glassParts != null &&
|
||||||
store.getters.order.lineItems.glassParts.length > 0)) &&
|
store.getters.order.lineItems.glassParts.length > 0)) &&
|
||||||
store.getters.order.referralNumber?.length !== 6
|
!store.getters.payment.insuranceCoverage.isVerified
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getDefaultIsInsuranceSelectedValue(availableLineItems) {
|
getDefaultIsInsuranceSelectedValue(availableLineItems) {
|
||||||
|
|
@ -239,3 +239,8 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.text-block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,17 @@ import { processIfStatements } from "@/helpers/cms-content-helper";
|
||||||
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
import { damageLocationsSelected as glassLocations } from "@/constants/damage-locations-selected";
|
||||||
import servicePackageRadio from "./service-package-radio/service-package-radio";
|
import servicePackageRadio from "./service-package-radio/service-package-radio";
|
||||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
import { packageNames } from "@/constants/package-names";
|
||||||
const packageNames = {
|
import {
|
||||||
TIER_ONE: "TierOne",
|
shouldFrontWipersBeAvailable,
|
||||||
TIER_TWO: "TierTwo",
|
shouldRearWipersBeAvailable,
|
||||||
TIER_THREE: "TierThree",
|
shouldRainDefenseBeAvailable,
|
||||||
};
|
getAvailablePackages,
|
||||||
|
getHighestRequiredTier,
|
||||||
|
getPackageContents,
|
||||||
|
containsLineItemWithPartType,
|
||||||
|
findLineItemsWithPartType,
|
||||||
|
} from "@/helpers/service-package-helper";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "servicePackageQuestion",
|
name: "servicePackageQuestion",
|
||||||
|
|
@ -65,11 +70,17 @@ export default {
|
||||||
if (!cmsAnswersContent) {
|
if (!cmsAnswersContent) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this.shouldDisplayTierTwoPackage) {
|
|
||||||
cmsAnswersContent = cmsAnswersContent.filter(
|
const availablePackages = getAvailablePackages(
|
||||||
(answer) => answer.Name != packageNames.TIER_TWO
|
this.glassToReplace,
|
||||||
);
|
this.nullSafeAvailableLineItems,
|
||||||
}
|
this.isRepair
|
||||||
|
);
|
||||||
|
|
||||||
|
cmsAnswersContent = cmsAnswersContent.filter((answer) =>
|
||||||
|
availablePackages.some((tier) => answer.Name === tier.packageName)
|
||||||
|
);
|
||||||
|
|
||||||
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
|
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
|
||||||
value: answer.Name,
|
value: answer.Name,
|
||||||
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
|
buttonLabel: this.getHeaderTextFromCms(answer.SubWidgetName),
|
||||||
|
|
@ -81,67 +92,56 @@ export default {
|
||||||
return modifiedAnswers;
|
return modifiedAnswers;
|
||||||
},
|
},
|
||||||
isRecalibrationOnOrder() {
|
isRecalibrationOnOrder() {
|
||||||
return this.lineItemsContainsPartType(partTypeStrings.RECALIBRATION);
|
return containsLineItemWithPartType(
|
||||||
|
partTypeStrings.RECALIBRATION,
|
||||||
|
this.nullSafeAvailableLineItems
|
||||||
|
);
|
||||||
},
|
},
|
||||||
frontWipersApplicableForTierTwo() {
|
frontWipersApplicableForTierTwo() {
|
||||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
return shouldFrontWipersBeAvailable(
|
||||||
partTypeStrings.FRONT_WIPER
|
this.glassToReplace,
|
||||||
|
this.nullSafeAvailableLineItems,
|
||||||
|
this.isRepair,
|
||||||
|
packageNames.TIER_TWO
|
||||||
);
|
);
|
||||||
const isRepair = this.$store.getters.order.damage.isRepair;
|
|
||||||
const glassToReplaceContainsWindshield = this.glassToReplaceContainsGlassLocation(
|
|
||||||
glassLocations.WINDSHIELD
|
|
||||||
);
|
|
||||||
if (frontWipersAreAvailable) {
|
|
||||||
if (isRepair) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
if (glassToReplaceContainsWindshield) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
rearWiperApplicableForTierTwo() {
|
rearWiperApplicableForTierTwo() {
|
||||||
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
|
return shouldRearWipersBeAvailable(
|
||||||
return (
|
this.glassToReplace,
|
||||||
this.glassToReplaceContainsGlassLocation(glassLocations.REAR) &&
|
this.nullSafeAvailableLineItems,
|
||||||
rearWiperIsAvailable
|
this.isRepair,
|
||||||
|
packageNames.TIER_TWO
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
frontWipersApplicableForTierThree() {
|
frontWipersApplicableForTierThree() {
|
||||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
return shouldFrontWipersBeAvailable(
|
||||||
partTypeStrings.FRONT_WIPER
|
this.glassToReplace,
|
||||||
|
this.nullSafeAvailableLineItems,
|
||||||
|
this.isRepair,
|
||||||
|
packageNames.TIER_THREE
|
||||||
);
|
);
|
||||||
return frontWipersAreAvailable;
|
|
||||||
},
|
},
|
||||||
rearWiperApplicableForTierThree() {
|
rearWiperApplicableForTierThree() {
|
||||||
const rearWiperIsAvailable = this.lineItemsContainsPartType(partTypeStrings.REAR_WIPER);
|
return shouldRearWipersBeAvailable(
|
||||||
const frontWipersAreAvailable = this.lineItemsContainsPartType(
|
this.glassToReplace,
|
||||||
partTypeStrings.FRONT_WIPER
|
this.nullSafeAvailableLineItems,
|
||||||
);
|
this.isRepair,
|
||||||
return (
|
packageNames.TIER_THREE
|
||||||
rearWiperIsAvailable &&
|
|
||||||
(this.glassToReplaceContainsGlassLocation(glassLocations.REAR) ||
|
|
||||||
!frontWipersAreAvailable)
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
rainDefenseApplicableForTierThree() {
|
rainDefenseApplicableForTierThree() {
|
||||||
if (
|
return shouldRainDefenseBeAvailable(
|
||||||
this.rearWiperApplicableForTierTwo &&
|
this.glassToReplace,
|
||||||
!this.frontWipersApplicableForTierTwo &&
|
this.nullSafeAvailableLineItems,
|
||||||
this.frontWipersApplicableForTierThree
|
this.isRepair,
|
||||||
) {
|
packageNames.TIER_THREE
|
||||||
return false;
|
);
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
shouldDisplayTierTwoPackage() {
|
glassToReplace() {
|
||||||
return this.frontWipersApplicableForTierTwo || this.rearWiperApplicableForTierTwo;
|
return this.$store.getters.order.damage.glassToReplace;
|
||||||
|
},
|
||||||
|
isRepair() {
|
||||||
|
return this.$store.getters.order.damage.isRepair;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -170,65 +170,31 @@ export default {
|
||||||
: baseMixin.methods.getTierOnePackagePrice(
|
: baseMixin.methods.getTierOnePackagePrice(
|
||||||
baseMixin.methods.filterOutFees(this.nullSafeAvailableLineItems)
|
baseMixin.methods.filterOutFees(this.nullSafeAvailableLineItems)
|
||||||
);
|
);
|
||||||
if (packageName === packageNames.TIER_TWO) {
|
|
||||||
priceFloat += this.getTierTwoPackageVapsPrice();
|
priceFloat += this.getVapsPrice(packageName);
|
||||||
} else if (packageName === packageNames.TIER_THREE) {
|
|
||||||
priceFloat += this.getTierThreePackageVapsPrice();
|
|
||||||
}
|
|
||||||
return priceFloat;
|
return priceFloat;
|
||||||
},
|
},
|
||||||
getTierTwoPackageVapsPrice() {
|
getVapsPrice(packageName) {
|
||||||
let vapsPrice = 0;
|
const vapsItems = this.getVapsLineItemsForSelectedPackage(packageName);
|
||||||
const priceFrontWipers = this.frontWipersApplicableForTierTwo;
|
|
||||||
const priceRearWipers = this.rearWiperApplicableForTierTwo;
|
let price = 0;
|
||||||
this.nullSafeAvailableLineItems.forEach((item) => {
|
|
||||||
if (
|
vapsItems.forEach((item) => {
|
||||||
(priceFrontWipers &&
|
price += baseMixin.methods.getTotalLineItemPrice(item);
|
||||||
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
|
|
||||||
(priceRearWipers && item.partType.toUpperCase() === partTypeStrings.REAR_WIPER)
|
|
||||||
) {
|
|
||||||
vapsPrice += baseMixin.methods.getTotalLineItemPrice(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return vapsPrice;
|
|
||||||
},
|
return price;
|
||||||
getTierThreePackageVapsPrice() {
|
|
||||||
let vapsPrice = 0;
|
|
||||||
const priceFrontWipers = this.frontWipersApplicableForTierThree;
|
|
||||||
const priceRearWipers = this.rearWiperApplicableForTierThree;
|
|
||||||
const priceRainDefense = this.rainDefenseApplicableForTierThree;
|
|
||||||
this.nullSafeAvailableLineItems.forEach((item) => {
|
|
||||||
if (
|
|
||||||
(priceFrontWipers &&
|
|
||||||
item.partType.toUpperCase() === partTypeStrings.FRONT_WIPER) ||
|
|
||||||
(priceRearWipers &&
|
|
||||||
item.partType.toUpperCase() === partTypeStrings.REAR_WIPER) ||
|
|
||||||
(priceRainDefense &&
|
|
||||||
item.partType.toUpperCase() === partTypeStrings.RAIN_DEFENSE)
|
|
||||||
) {
|
|
||||||
vapsPrice += baseMixin.methods.getTotalLineItemPrice(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return vapsPrice;
|
|
||||||
},
|
},
|
||||||
selectDefaultPackage() {
|
selectDefaultPackage() {
|
||||||
const vapsFromStore = this.$store.getters.lineItems.vaps;
|
const vapsFromStore = this.$store.getters.lineItems.vaps ?? [];
|
||||||
let lowestTierForPackage = packageNames.TIER_ONE;
|
|
||||||
if (vapsFromStore?.length > 0) {
|
this.selectedPackageName = getHighestRequiredTier(
|
||||||
vapsFromStore.every((vapsItem) => {
|
this.glassToReplace,
|
||||||
let lowestTierForThisItem = this.getLowestTierForThisItem(vapsItem);
|
this.nullSafeAvailableLineItems,
|
||||||
if (lowestTierForThisItem === packageNames.TIER_THREE) {
|
this.isRepair,
|
||||||
lowestTierForPackage = packageNames.TIER_THREE;
|
vapsFromStore
|
||||||
return false;
|
);
|
||||||
} else if (lowestTierForThisItem === packageNames.TIER_TWO) {
|
|
||||||
lowestTierForPackage = packageNames.TIER_TWO;
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.selectedPackageName = lowestTierForPackage;
|
|
||||||
},
|
},
|
||||||
allGlassPartsAndSupportingItemsHavePrices(lineItems) {
|
allGlassPartsAndSupportingItemsHavePrices(lineItems) {
|
||||||
if (lineItems?.glassParts) {
|
if (lineItems?.glassParts) {
|
||||||
|
|
@ -254,63 +220,22 @@ export default {
|
||||||
(lineItem.sellingPrice == null || lineItem.sellingPrice == 0)
|
(lineItem.sellingPrice == null || lineItem.sellingPrice == 0)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
getLowestTierForThisItem(vapsItem) {
|
|
||||||
let lowestTierForThisItem = null;
|
|
||||||
switch (vapsItem.partType) {
|
|
||||||
case partTypeStrings.FRONT_WIPER:
|
|
||||||
if (this.frontWipersApplicableForTierThree) {
|
|
||||||
lowestTierForThisItem = packageNames.TIER_THREE;
|
|
||||||
}
|
|
||||||
if (this.frontWipersApplicableForTierTwo) {
|
|
||||||
lowestTierForThisItem = packageNames.TIER_TWO;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case partTypeStrings.REAR_WIPER:
|
|
||||||
if (this.rearWiperApplicableForTierThree) {
|
|
||||||
lowestTierForThisItem = packageNames.TIER_THREE;
|
|
||||||
}
|
|
||||||
if (this.rearWiperApplicableForTierTwo) {
|
|
||||||
lowestTierForThisItem = packageNames.TIER_TWO;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case partTypeStrings.RAIN_DEFENSE:
|
|
||||||
if (this.rainDefenseApplicableForTierThree) {
|
|
||||||
lowestTierForThisItem = packageNames.TIER_THREE;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return lowestTierForThisItem;
|
|
||||||
},
|
|
||||||
getVapsLineItemsForSelectedPackage(packageName) {
|
getVapsLineItemsForSelectedPackage(packageName) {
|
||||||
const vapsLineItemsForSelectedPackage = [];
|
const packageContentTypes = getPackageContents(
|
||||||
if (packageName === packageNames.TIER_TWO) {
|
this.glassToReplace,
|
||||||
if (this.frontWipersApplicableForTierTwo) {
|
this.nullSafeAvailableLineItems,
|
||||||
vapsLineItemsForSelectedPackage.push(
|
this.isRepair,
|
||||||
...this.getLineItemsContainingPartType(partTypeStrings.FRONT_WIPER)
|
packageName
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if (this.rearWiperApplicableForTierTwo) {
|
let vapsLineItemsForSelectedPackage = [];
|
||||||
vapsLineItemsForSelectedPackage.push(
|
|
||||||
...this.getLineItemsContainingPartType(partTypeStrings.REAR_WIPER)
|
packageContentTypes.forEach((vapType) => {
|
||||||
);
|
vapsLineItemsForSelectedPackage.push(
|
||||||
}
|
...findLineItemsWithPartType(vapType, this.nullSafeAvailableLineItems)
|
||||||
} else if (packageName === packageNames.TIER_THREE) {
|
);
|
||||||
if (this.frontWipersApplicableForTierThree) {
|
});
|
||||||
vapsLineItemsForSelectedPackage.push(
|
|
||||||
...this.getLineItemsContainingPartType(partTypeStrings.FRONT_WIPER)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.rearWiperApplicableForTierThree) {
|
|
||||||
vapsLineItemsForSelectedPackage.push(
|
|
||||||
...this.getLineItemsContainingPartType(partTypeStrings.REAR_WIPER)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (this.rainDefenseApplicableForTierThree) {
|
|
||||||
vapsLineItemsForSelectedPackage.push(
|
|
||||||
...this.getLineItemsContainingPartType(partTypeStrings.RAIN_DEFENSE)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return vapsLineItemsForSelectedPackage;
|
return vapsLineItemsForSelectedPackage;
|
||||||
},
|
},
|
||||||
getCustomValueFromString(str) {
|
getCustomValueFromString(str) {
|
||||||
|
|
@ -331,23 +256,6 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getLineItemsContainingPartType(partType) {
|
|
||||||
const partTypeMatches = this.nullSafeAvailableLineItems.filter(
|
|
||||||
(lineItem) => lineItem.partType.toUpperCase() === partType
|
|
||||||
);
|
|
||||||
return partTypeMatches;
|
|
||||||
},
|
|
||||||
lineItemsContainsPartType(partType) {
|
|
||||||
const partTypeMatches = this.getLineItemsContainingPartType(partType);
|
|
||||||
return !!partTypeMatches.length;
|
|
||||||
},
|
|
||||||
glassToReplaceContainsGlassLocation(glassLocation) {
|
|
||||||
const glassLocationMatches =
|
|
||||||
this.$store.getters.order.damage.glassToReplace?.filter(
|
|
||||||
(glassToReplace) => glassToReplace.glassLocation === glassLocation
|
|
||||||
) ?? [];
|
|
||||||
return !!glassLocationMatches.length;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
buttonQuestion,
|
buttonQuestion,
|
||||||
|
|
|
||||||
36
src/layouts/review/review-block/review-block.spec.js
Normal file
36
src/layouts/review/review-block/review-block.spec.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import store from "@/store";
|
||||||
|
import reviewBlock from "@/layouts/review/review-block/review-block";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||||
|
fetchCmsContentForPage: () => Promise.resolve("content"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Review Content Block", () => {
|
||||||
|
test("Displays one new line of content for each element in the content prop.", () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: {
|
||||||
|
headerCmsWidgetName: "TestWidget",
|
||||||
|
content: ["a", "b", "c", "d"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const contentLines = wrapper.findAll("[data-test='contentLine']");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(contentLines.length).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
mountOptions["attachTo"] = document.body;
|
||||||
|
|
||||||
|
const wrapper = shallowMount(reviewBlock, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
46
src/layouts/review/review-block/review-block.vue
Normal file
46
src/layouts/review/review-block/review-block.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div class="py-3">
|
||||||
|
<div class="d-flex">
|
||||||
|
<textBlock
|
||||||
|
:cmsWidgetName="headerCmsWidgetName"
|
||||||
|
typeStyle="body small bold dark"
|
||||||
|
margin="mt-0" />
|
||||||
|
<textLink
|
||||||
|
linkType="textSmall"
|
||||||
|
text="Edit"
|
||||||
|
class="ml-auto"
|
||||||
|
@click-event="linkClicked"
|
||||||
|
href="javascript:void(0)" />
|
||||||
|
</div>
|
||||||
|
<div class="small" v-for="item in content" :key="item" data-test="contentLine">
|
||||||
|
{{ item }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
import textLink from "@/ux-components/text-link/text-link";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-review",
|
||||||
|
props: {
|
||||||
|
headerCmsWidgetName: String,
|
||||||
|
// Specifically an array of strings.
|
||||||
|
content: Array,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
linkClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
components: {
|
||||||
|
textBlock,
|
||||||
|
textLink,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||||
|
fetchCmsContentForPage: () => Promise.resolve("content"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Damage Review Block", () => {
|
||||||
|
describe("Correctly assembles damage info into a display string", () => {
|
||||||
|
test("Basic Replace", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: {
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield crack"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Basic Repair", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: {
|
||||||
|
damage: {
|
||||||
|
isRepair: true,
|
||||||
|
numberOfChips: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield repair - 2 chips"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Basic Repair - no s with 1 chip", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: {
|
||||||
|
damage: {
|
||||||
|
isRepair: true,
|
||||||
|
numberOfChips: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toStrictEqual(["Windshield repair - 1 chip"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const wrapper = shallowMount(damageReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock
|
||||||
|
:headerCmsWidgetName="cmsWidgetName"
|
||||||
|
:content="displayContent"
|
||||||
|
@edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/review/review-block/review-block";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "damage-review",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
damage: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [
|
||||||
|
this.damage.isRepair
|
||||||
|
? `Windshield repair - ${this.damage.numberOfChips} chip${
|
||||||
|
this.damage.numberOfChips !== 1 ? "s" : ""
|
||||||
|
}`
|
||||||
|
: "Windshield crack",
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,969 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
|
||||||
|
|
||||||
|
import { packageNames } from "@/constants/package-names";
|
||||||
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
import { damageLocationsSelected as glassConstants } from "@/constants/damage-locations-selected";
|
||||||
|
|
||||||
|
const testConstants = {
|
||||||
|
cmsPropValues: {
|
||||||
|
servicePackageOptionsCmsName: "ServicePackageTitle",
|
||||||
|
defaultPackageItemsCmsName: "DefaultPackageItemDescriptions",
|
||||||
|
vapsItemsCmsName: "VapsItemDescriptions",
|
||||||
|
},
|
||||||
|
widgetNames: {
|
||||||
|
tierOneTitle: "EconomyServiceTitle",
|
||||||
|
tierTwoTitle: "StandardServiceTitle",
|
||||||
|
tierThreeTitle: "PremiumServiceTitle",
|
||||||
|
},
|
||||||
|
defaultItemCopy: {
|
||||||
|
itemOne: "Item Description 1",
|
||||||
|
itemTwo: "Item Description 2",
|
||||||
|
itemThree: "Item Description 3",
|
||||||
|
itemFour: "Item Description 4",
|
||||||
|
defaultItemCopyArray: ["Item Description 1", "Item Description 2", "Item Description 3"],
|
||||||
|
},
|
||||||
|
vapsCopy: {
|
||||||
|
frontWiperCopy: "Front Wiper copy",
|
||||||
|
rearWiperCopy: "Rear Wiper copy",
|
||||||
|
rainDefenseCopy: "Rain defense copy",
|
||||||
|
},
|
||||||
|
parts: {
|
||||||
|
frontWiperPart: {
|
||||||
|
partNumber: "SBB16",
|
||||||
|
description: "SAFELITE BEAM BLADE 16",
|
||||||
|
partType: "FRONT WIPER",
|
||||||
|
price: 32.64,
|
||||||
|
},
|
||||||
|
rearWiperPart: {
|
||||||
|
partNumber: "SBBR12A",
|
||||||
|
description: "SAFELITE REAR BLADE 12A",
|
||||||
|
partType: "REAR WIPER",
|
||||||
|
price: 24.48,
|
||||||
|
},
|
||||||
|
rainDefensePart: {
|
||||||
|
partNumber: "RAIN DEFENSE",
|
||||||
|
description: null,
|
||||||
|
partType: "RAIN DEFENSE",
|
||||||
|
price: 35.5,
|
||||||
|
},
|
||||||
|
recalPart: {
|
||||||
|
partNumber: "RECAL STATIC",
|
||||||
|
Description: "Recalibration",
|
||||||
|
partType: "recalibration",
|
||||||
|
Quantity: "1",
|
||||||
|
price: 150.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
damages: {
|
||||||
|
frontWindshield: {
|
||||||
|
glassLocation: glassConstants.WINDSHIELD,
|
||||||
|
glassName: glassConstants.SINGLE,
|
||||||
|
},
|
||||||
|
rearWindshield: {
|
||||||
|
glassLocation: glassConstants.REAR,
|
||||||
|
glassName: glassConstants.STATIONARY,
|
||||||
|
},
|
||||||
|
sideGlass: {
|
||||||
|
glassLocation: glassConstants.PASSENGER,
|
||||||
|
glassName: glassConstants.QUARTER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
imageId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
};
|
||||||
|
|
||||||
|
const figmaScenarios = [
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_Cash",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.frontWindshield],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard+RearWiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 05_01_CSR_Quote_Standard_Repair & 05_01_CSR_Quote_Recal have identical outcomes to above, but included in case that changes in the future.
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_Standard_Repair",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: true,
|
||||||
|
glassToReplace: [],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard+RearWiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_Recal",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.frontWindshield],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [testConstants.parts.recalPart],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard+RearWiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 05_01_CSR_Quote_RearGlass omitted as a duplicate of below.
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_RearGlass+NonWindshield",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.rearWindshield],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Frontwiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard+RainDefense",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_RearGlass+Windshield",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [
|
||||||
|
testConstants.damages.frontWindshield,
|
||||||
|
testConstants.damages.rearWindshield,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Frontwiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Rearwiper",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+RainDefense",
|
||||||
|
vapsCombo: [testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Frontwiper+RainDefense",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Rearwiper+RainDefense",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_RearGlassNoFrontFit",
|
||||||
|
params: {
|
||||||
|
availableVaps: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.rearWindshield],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Raindefense",
|
||||||
|
vapsCombo: [testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart, testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 05_01_CSR_Quote_Windshield+SideGlass has identical outcomes to 05_01_CSR_Quote_Cash, but included in case that changes in the future.
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_Windshield+SideGlass",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [
|
||||||
|
testConstants.damages.frontWindshield,
|
||||||
|
testConstants.damages.sideGlass,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Standard+RearWiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart, testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Has no standard package
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_SideGlass",
|
||||||
|
params: {
|
||||||
|
availableVaps: [
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.sideGlass],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Frontwiper",
|
||||||
|
vapsCombo: [testConstants.parts.frontWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+RainDefense",
|
||||||
|
vapsCombo: [testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Economy+Rearwiper",
|
||||||
|
vapsCombo: [testConstants.parts.rearWiperPart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium+Rearwiper",
|
||||||
|
vapsCombo: [
|
||||||
|
testConstants.parts.rainDefensePart,
|
||||||
|
testConstants.parts.frontWiperPart,
|
||||||
|
testConstants.parts.rearWiperPart,
|
||||||
|
],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Has no standard package
|
||||||
|
{
|
||||||
|
name: "05_01_CSR_Quote_NoWiperFit",
|
||||||
|
params: {
|
||||||
|
availableVaps: [testConstants.parts.rainDefensePart],
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.frontWindshield],
|
||||||
|
},
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
},
|
||||||
|
iterations: [
|
||||||
|
{
|
||||||
|
name: "Economy",
|
||||||
|
vapsCombo: [],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierOneTitle,
|
||||||
|
displayContent: testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Premium",
|
||||||
|
vapsCombo: [testConstants.parts.rainDefensePart],
|
||||||
|
expected: {
|
||||||
|
packageNameWidget: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
displayContent: [
|
||||||
|
...testConstants.defaultItemCopy.defaultItemCopyArray,
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let cmsContent;
|
||||||
|
|
||||||
|
describe("Service Package Review Block", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cmsContent = {
|
||||||
|
ServicePackageTitle: {
|
||||||
|
Answers: [
|
||||||
|
{
|
||||||
|
Name: packageNames.TIER_ONE,
|
||||||
|
Text: "",
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: testConstants.widgetNames.tierOneTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: packageNames.TIER_TWO,
|
||||||
|
Text: "",
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: testConstants.widgetNames.tierTwoTitle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: packageNames.TIER_THREE,
|
||||||
|
Text: "",
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: testConstants.widgetNames.tierThreeTitle,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
DefaultPackageItemDescriptions: {
|
||||||
|
Answers: [
|
||||||
|
{
|
||||||
|
Name: "Item1",
|
||||||
|
Text: testConstants.defaultItemCopy.itemOne,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Item2",
|
||||||
|
Text: testConstants.defaultItemCopy.itemTwo,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Item3",
|
||||||
|
Text: testConstants.defaultItemCopy.itemThree,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
VapsItemDescriptions: {
|
||||||
|
Answers: [
|
||||||
|
{
|
||||||
|
Name: partTypeStrings.FRONT_WIPER,
|
||||||
|
Text: testConstants.vapsCopy.frontWiperCopy,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: partTypeStrings.REAR_WIPER,
|
||||||
|
Text: testConstants.vapsCopy.rearWiperCopy,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: partTypeStrings.RAIN_DEFENSE,
|
||||||
|
Text: testConstants.vapsCopy.rainDefenseCopy,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
describe("General functionality", () => {
|
||||||
|
it('Should properly "Round Down" package tier', async () => {
|
||||||
|
// Slightly longer explanation:
|
||||||
|
// Should only return the highest tier where *every* offered VAP is part of the order.
|
||||||
|
// However, there may be vaps not offered in the qualifying tier. Hence rounding *down*.
|
||||||
|
//
|
||||||
|
// I.e. Economy=[], Standard=[front wipers], Premium=[front wipers, rain defense].
|
||||||
|
// Current vaps=[rain defense]. Though rain defense is in Premium, we don't satisfy it or standard.
|
||||||
|
// So our tier should still be Economy.
|
||||||
|
// Should still display extra vaps.
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
|
||||||
|
props.lineItems.vaps = [testConstants.parts.rainDefensePart];
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.packageNameWidget).toEqual(testConstants.widgetNames.tierOneTitle);
|
||||||
|
|
||||||
|
let containsRainDefenseCopy = wrapper.vm.displayContent.includes(
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy
|
||||||
|
);
|
||||||
|
expect(containsRainDefenseCopy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should display all default items from cms", async () => {
|
||||||
|
// Arrange
|
||||||
|
cmsContent.DefaultPackageItemDescriptions.Answers.push({
|
||||||
|
Name: "Item4",
|
||||||
|
Text: testConstants.defaultItemCopy.itemFour,
|
||||||
|
SubText: "",
|
||||||
|
ImageId: testConstants.imageId,
|
||||||
|
Image: "",
|
||||||
|
SubWidgetName: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
props.lineItems.vaps = [];
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let expectedResult = [
|
||||||
|
testConstants.defaultItemCopy.itemOne,
|
||||||
|
testConstants.defaultItemCopy.itemTwo,
|
||||||
|
testConstants.defaultItemCopy.itemThree,
|
||||||
|
testConstants.defaultItemCopy.itemFour,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(wrapper.vm.displayContent).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should display vaps if and only if they are added", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: generateDefaultProps(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const includesFrontWiperCopy = wrapper.vm.displayContent.includes(
|
||||||
|
testConstants.vapsCopy.frontWiperCopy
|
||||||
|
);
|
||||||
|
const includesRainDefenseCopy = wrapper.vm.displayContent.includes(
|
||||||
|
testConstants.vapsCopy.rainDefenseCopy
|
||||||
|
);
|
||||||
|
expect(includesFrontWiperCopy).toBe(true);
|
||||||
|
expect(includesRainDefenseCopy).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not error if cms content is missing (though may display poorly).", async () => {
|
||||||
|
// Arrange
|
||||||
|
cmsContent = {};
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: generateDefaultProps(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.packageNameWidget).toEqual("");
|
||||||
|
expect(wrapper.vm.displayContent).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Match Figma Scenarios", () => {
|
||||||
|
figmaScenarios.forEach((scenario) => {
|
||||||
|
scenario.iterations.forEach((iteration) => {
|
||||||
|
it(`Should match figma scenario "${scenario.name}", iteration "${iteration.name}"`, async () => {
|
||||||
|
// Arrange
|
||||||
|
let props = generateDefaultProps();
|
||||||
|
props.availableVaps = scenario.params.availableVaps;
|
||||||
|
props.damage = scenario.params.damage;
|
||||||
|
props.glassParts = scenario.params.glassParts;
|
||||||
|
props.lineItems.supportingItems = scenario.params.supportingItems;
|
||||||
|
props.lineItems.vaps = iteration.vapsCombo;
|
||||||
|
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: props,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.packageNameWidget).toEqual(
|
||||||
|
iteration.expected.packageNameWidget
|
||||||
|
);
|
||||||
|
expect(wrapper.vm.displayContent).toEqual(iteration.expected.displayContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function generateDefaultProps() {
|
||||||
|
return {
|
||||||
|
servicePackageOptionsCmsName: testConstants.cmsPropValues.servicePackageOptionsCmsName,
|
||||||
|
defaultPackageItemsCmsName: testConstants.cmsPropValues.defaultPackageItemsCmsName,
|
||||||
|
vapsItemsCmsName: testConstants.cmsPropValues.vapsItemsCmsName,
|
||||||
|
availableVaps: [testConstants.parts.frontWiperPart, testConstants.parts.rainDefensePart],
|
||||||
|
lineItems: {
|
||||||
|
glassParts: [],
|
||||||
|
supportingItems: [],
|
||||||
|
vaps: [testConstants.parts.frontWiperPart],
|
||||||
|
},
|
||||||
|
damage: {
|
||||||
|
isRepair: false,
|
||||||
|
glassToReplace: [testConstants.damages.frontWindshield],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||||
|
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.global.mixins = [mockMixin];
|
||||||
|
|
||||||
|
const wrapper = shallowMount(servicePackageReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock
|
||||||
|
:headerCmsWidgetName="packageNameWidget"
|
||||||
|
:content="displayContent"
|
||||||
|
@edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/review/review-block/review-block";
|
||||||
|
import {
|
||||||
|
getHighestFullySatisfiedTier,
|
||||||
|
containsLineItemWithPartType,
|
||||||
|
} from "@/helpers/service-package-helper";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "service-package-review",
|
||||||
|
props: {
|
||||||
|
servicePackageOptionsCmsName: String,
|
||||||
|
defaultPackageItemsCmsName: String,
|
||||||
|
vapsItemsCmsName: String,
|
||||||
|
availableVaps: Array,
|
||||||
|
lineItems: Object,
|
||||||
|
damage: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [...this.defaultPackageText, ...this.vapsItemText];
|
||||||
|
},
|
||||||
|
packageNameWidget() {
|
||||||
|
const servicePackageNames = this.getCmsContent(
|
||||||
|
this.servicePackageOptionsCmsName,
|
||||||
|
"Answers"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!servicePackageNames) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPackage = servicePackageNames.find(
|
||||||
|
(entry) => entry.Name === this.packageLevel
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentPackage.SubWidgetName;
|
||||||
|
},
|
||||||
|
defaultPackageText() {
|
||||||
|
const defaultItems = this.getCmsContent(this.defaultPackageItemsCmsName, "Answers");
|
||||||
|
|
||||||
|
if (!defaultItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultItems.map((answer) => answer.Text);
|
||||||
|
},
|
||||||
|
vapsItemText() {
|
||||||
|
const vapsDescriptions = this.getCmsContent(this.vapsItemsCmsName, "Answers");
|
||||||
|
|
||||||
|
if (!vapsDescriptions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const vapsDescriptionsOnOrder = vapsDescriptions.filter((answer) =>
|
||||||
|
containsLineItemWithPartType(answer.Name, this.vaps)
|
||||||
|
);
|
||||||
|
|
||||||
|
return vapsDescriptionsOnOrder.map((answer) => answer.Text);
|
||||||
|
},
|
||||||
|
packageLevel() {
|
||||||
|
return getHighestFullySatisfiedTier(
|
||||||
|
this.glassToReplace,
|
||||||
|
this.availableLineItems,
|
||||||
|
this.isRepair,
|
||||||
|
this.vaps
|
||||||
|
);
|
||||||
|
},
|
||||||
|
glassToReplace() {
|
||||||
|
return this.damage.glassToReplace;
|
||||||
|
},
|
||||||
|
isRepair() {
|
||||||
|
return this.damage.isRepair;
|
||||||
|
},
|
||||||
|
vaps() {
|
||||||
|
return this.lineItems?.vaps;
|
||||||
|
},
|
||||||
|
availableLineItems() {
|
||||||
|
return [
|
||||||
|
...(this.lineItems?.glassParts ?? []),
|
||||||
|
...(this.lineItems?.supportingItems ?? []),
|
||||||
|
...this.availableVaps,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import store from "@/store";
|
||||||
|
import vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehicle-review";
|
||||||
|
|
||||||
|
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||||
|
fetchCmsContentForPage: () => Promise.resolve("content"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Vehicle Review Block", () => {
|
||||||
|
test("Correctly assembles vehicle info into a display string", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({
|
||||||
|
propsData: {
|
||||||
|
vehicle: {
|
||||||
|
year: "2019",
|
||||||
|
make: "Honda",
|
||||||
|
model: "Odyssey",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(wrapper.vm.displayContent).toStrictEqual(["2019 Honda Odyssey"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks(customMountOptions) {
|
||||||
|
const mountOptions = getMountOptions(customMountOptions);
|
||||||
|
|
||||||
|
const wrapper = shallowMount(vehicleReview, mountOptions);
|
||||||
|
wrapper.vm.setCmsContent = jest.fn();
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<reviewBlock
|
||||||
|
:headerCmsWidgetName="cmsWidgetName"
|
||||||
|
:content="displayContent"
|
||||||
|
@edit-clicked="editClicked" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import reviewBlock from "@/layouts/review/review-block/review-block";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-review",
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
vehicle: Object,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editClicked() {
|
||||||
|
this.$emit("edit-clicked");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayContent() {
|
||||||
|
return [`${this.vehicle.year} ${this.vehicle.make} ${this.vehicle.model}`];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
reviewBlock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
3
src/layouts/review/review.spec.js
Normal file
3
src/layouts/review/review.spec.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
describe("Review Page", () => {
|
||||||
|
test.todo("Add more tests as specific functionality is added.");
|
||||||
|
});
|
||||||
202
src/layouts/review/review.vue
Normal file
202
src/layouts/review/review.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
<template>
|
||||||
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||||
|
<!-- When customer-details is added: v-slot="{ meta }" -->
|
||||||
|
<div class="page-container-grouped-styles">
|
||||||
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||||
|
<vehicleBanner
|
||||||
|
cmsWidgetName="VehicleBannerWidget"
|
||||||
|
:displayGenericVehicleImage="false" />
|
||||||
|
|
||||||
|
<textBlock
|
||||||
|
:customText="subHeaderTitle"
|
||||||
|
typeStyle="h5"
|
||||||
|
justifyText="justify-content-center"
|
||||||
|
margin="mt-1"
|
||||||
|
class="dark-header" />
|
||||||
|
<textBlock
|
||||||
|
:customText="subHeaderBody"
|
||||||
|
typeStyle="body"
|
||||||
|
justifyText="justify-content-left"
|
||||||
|
margin="mt-0 mb-2" />
|
||||||
|
|
||||||
|
<buttonMain
|
||||||
|
ref="buttonMain"
|
||||||
|
isPrimary
|
||||||
|
:buttonText="forwardButtonText"
|
||||||
|
loaderColor="white"
|
||||||
|
class="mb-2"
|
||||||
|
@click-event="forwardButtonAction" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textBlock
|
||||||
|
customText="Appointment Details"
|
||||||
|
typeStyle="label bold"
|
||||||
|
justifyText="justify-text-left"
|
||||||
|
margin="mt-0"
|
||||||
|
class="dark-header" />
|
||||||
|
|
||||||
|
<div class="px-4">
|
||||||
|
<vehicleReview
|
||||||
|
cmsWidgetName="VehicleReviewWidget"
|
||||||
|
:vehicle="vehicleInfo"
|
||||||
|
@edit-clicked="editVehicle" />
|
||||||
|
|
||||||
|
<hr class="my-0" />
|
||||||
|
|
||||||
|
<damageReview
|
||||||
|
cmsWidgetName="DamageReviewWidget"
|
||||||
|
:damage="damageInfo"
|
||||||
|
@edit-clicked="editDamage" />
|
||||||
|
|
||||||
|
<hr class="my-0" />
|
||||||
|
|
||||||
|
<servicePackageReview
|
||||||
|
servicePackageOptionsCmsName="ServicePackageTitle"
|
||||||
|
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
|
||||||
|
vapsItemsCmsName="VapsItemDescriptions"
|
||||||
|
:damage="damageInfo"
|
||||||
|
:availableVaps="availableVaps"
|
||||||
|
:lineItems="lineItems"
|
||||||
|
@edit-clicked="editServicePackage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<hr class="my-0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<funnelFooter
|
||||||
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
@back-clicked="backButtonAction"
|
||||||
|
@ForwardClicked="forwardButtonAction" />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
|
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||||
|
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||||
|
import buttonMain from "@/ux-components/button-main/button-main";
|
||||||
|
import textBlock from "@/digital-components/text-block/text-block";
|
||||||
|
|
||||||
|
import vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehicle-review";
|
||||||
|
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
|
||||||
|
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
|
||||||
|
|
||||||
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "review",
|
||||||
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
|
// Get rain defense and wiper availability for package review section.
|
||||||
|
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS);
|
||||||
|
const rainDefensePromise = baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.GET_RAIN_DEFENSE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
|
{
|
||||||
|
resultKey: "cmsContent",
|
||||||
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resultKey: "wipers",
|
||||||
|
promise: wipersPromise,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resultKey: "rainDefense",
|
||||||
|
promise: rainDefensePromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
next((vm) => {
|
||||||
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
|
|
||||||
|
vm.availableWipers = resultMap.wipers;
|
||||||
|
vm.availableRainDefense = [resultMap.rainDefense];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
availableWipers: null,
|
||||||
|
availableRainDefense: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
backButtonAction() {},
|
||||||
|
forwardButtonAction() {},
|
||||||
|
editVehicle() {
|
||||||
|
this.$router.navigateWithoutSaving(
|
||||||
|
this.navigationScenarios.CLICKED_VEHICLE_EDIT,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
|
},
|
||||||
|
editDamage() {
|
||||||
|
this.$router.navigateWithoutSaving(
|
||||||
|
this.navigationScenarios.CLICKED_DAMAGE_EDIT,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
|
},
|
||||||
|
editServicePackage() {
|
||||||
|
this.$router.navigateWithoutSaving(
|
||||||
|
this.navigationScenarios.CLICKED_SERVICE_PACKAGE_EDIT,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
subHeaderTitle() {
|
||||||
|
return this.getCmsContent("FunnelSubHeaderWidget", "HeaderText");
|
||||||
|
},
|
||||||
|
subHeaderBody() {
|
||||||
|
return this.getCmsContent("FunnelSubHeaderWidget", "BodyText");
|
||||||
|
},
|
||||||
|
forwardButtonText() {
|
||||||
|
return this.getCmsContent("FunnelFooterWidget", "ForwardButtonText");
|
||||||
|
},
|
||||||
|
vehicleInfo() {
|
||||||
|
return this.$store.getters.vehicle;
|
||||||
|
},
|
||||||
|
damageInfo() {
|
||||||
|
return this.$store.getters.damage;
|
||||||
|
},
|
||||||
|
availableVaps() {
|
||||||
|
return [...(this.availableWipers ?? []), ...(this.availableRainDefense ?? [])];
|
||||||
|
},
|
||||||
|
lineItems() {
|
||||||
|
return this.$store.getters.lineItems;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
funnelHeader,
|
||||||
|
funnelFooter,
|
||||||
|
vehicleBanner,
|
||||||
|
buttonMain,
|
||||||
|
textBlock,
|
||||||
|
vehicleReview,
|
||||||
|
damageReview,
|
||||||
|
servicePackageReview,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.dark-header {
|
||||||
|
color: $black;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -108,7 +108,7 @@ const getAvailableDates = async (
|
||||||
const timeSlotsData = {};
|
const timeSlotsData = {};
|
||||||
timeSlotsData.days = [];
|
timeSlotsData.days = [];
|
||||||
let apiStartDate = startDateString;
|
let apiStartDate = startDateString;
|
||||||
let apiEndDate = apiEndDateLimit;
|
let apiEndDate = endDateString;
|
||||||
|
|
||||||
for (let i = 1; i <= apiCallsCount; i++) {
|
for (let i = 1; i <= apiCallsCount; i++) {
|
||||||
let storeActionConfig;
|
let storeActionConfig;
|
||||||
|
|
@ -120,6 +120,10 @@ const getAvailableDates = async (
|
||||||
if (i === apiCallsCount) {
|
if (i === apiCallsCount) {
|
||||||
apiEndDate = endDateString;
|
apiEndDate = endDateString;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (apiEndDate > apiEndDateLimit) {
|
||||||
|
apiEndDate = apiEndDateLimit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
if (appointmentType === AppointmentTypeStrings.MOBILE) {
|
||||||
|
|
@ -141,13 +145,13 @@ const getAvailableDates = async (
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
storeActionConfigs.push(storeActionConfig);
|
if (apiStartDate < apiEndDate) storeActionConfigs.push(storeActionConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASYNC METHOD
|
|
||||||
const timeSlotsResponsesData = {
|
const timeSlotsResponsesData = {
|
||||||
days: [],
|
days: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
function compareDayStrings(a, b) {
|
function compareDayStrings(a, b) {
|
||||||
if (a.date < b.date) return -1;
|
if (a.date < b.date) return -1;
|
||||||
if (a.date > b.date) return 1;
|
if (a.date > b.date) return 1;
|
||||||
|
|
@ -397,7 +401,12 @@ export default {
|
||||||
forwardButtonAction() {
|
forwardButtonAction() {
|
||||||
this.updateSupportingItems();
|
this.updateSupportingItems();
|
||||||
this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false);
|
this.dispatchStoreAction(this.storeActions.SAVE_SCHEDULE, this.selectedTimeSlot, false);
|
||||||
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
|
||||||
|
//Temporary, still need to determine what needs to be saved before continuing.
|
||||||
|
this.$router.navigateWithoutSaving(
|
||||||
|
this.navigationScenarios.CLICKED_FORWARD,
|
||||||
|
this.$route
|
||||||
|
);
|
||||||
},
|
},
|
||||||
updateSupportingItems() {
|
updateSupportingItems() {
|
||||||
const supportingItems = this.getSupportingItems();
|
const supportingItems = this.getSupportingItems();
|
||||||
|
|
|
||||||
|
|
@ -359,16 +359,19 @@ export default {
|
||||||
return `${hours}:${minutes} ${meridianNotation}`;
|
return `${hours}:${minutes} ${meridianNotation}`;
|
||||||
},
|
},
|
||||||
getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
|
getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
|
||||||
let displayTextForDurationLength;
|
const isLongAppointment = durationMaximum >= 120;
|
||||||
if (durationMaximum >= 120) {
|
const isDurationRange = durationMinimum !== durationMaximum;
|
||||||
displayTextForDurationLength = `${durationMinimum / 60} - ${
|
|
||||||
durationMaximum / 60
|
|
||||||
} hours`;
|
|
||||||
} else {
|
|
||||||
displayTextForDurationLength = `${durationMinimum} - ${durationMaximum} minutes`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayTextForDurationLength;
|
const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum;
|
||||||
|
const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum;
|
||||||
|
|
||||||
|
const durationText = isDurationRange
|
||||||
|
? `${adjustedMinimum} - ${adjustedMaximum}`
|
||||||
|
: adjustedMinimum;
|
||||||
|
|
||||||
|
const unitText = isLongAppointment ? "hours" : "minutes";
|
||||||
|
|
||||||
|
return `${durationText} ${unitText}`;
|
||||||
},
|
},
|
||||||
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
|
||||||
selectedTimeSlotId,
|
selectedTimeSlotId,
|
||||||
|
|
|
||||||
|
|
@ -401,10 +401,7 @@ export default {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
this.$router.navigateWithSaving(
|
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||||
this.navigationScenarios.SELECTED_LOCATION,
|
|
||||||
this.$route
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
openModalAction(modalName) {
|
openModalAction(modalName) {
|
||||||
this.$refs[modalName].openModal();
|
this.$refs[modalName].openModal();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
<p class="mb-0 color-question-text">{{ colorQuestionText }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="nested-radio">
|
<div class="nested-radio">
|
||||||
<div class="row my-2">
|
<div class="row mt-2">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
v-model="selectedTint"
|
v-model="selectedTint"
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
isRequired
|
isRequired
|
||||||
:groupName="`${glassLocation}-${glassName}`"
|
:groupName="`${glassLocation}-${glassName}`"
|
||||||
:validationRules="tintValidationRules">
|
:validationRules="tintValidationRules">
|
||||||
<div class="row my-2" aria-live="polite">
|
<div class="row mt-2" aria-live="polite">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<buttonQuestion
|
<buttonQuestion
|
||||||
v-model="selectedPartNumber"
|
v-model="selectedPartNumber"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="fade-on-route-transition sub-container make-tall">
|
<div class="fade-on-route-transition sub-container make-tall">
|
||||||
<div class="prevent-squish my-5">
|
<div class="prevent-squish my-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col pb-0">
|
||||||
<alert
|
<alert
|
||||||
class="rounded border-0 shadow-sm"
|
class="rounded border-0 shadow-sm"
|
||||||
alertClass="alert-warning"
|
alertClass="alert-warning"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
|
||||||
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
import store from "@/store";
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
"@/store",
|
||||||
|
() => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("vehicle-question.vue", () => {
|
||||||
|
test("Selected value is emitted upon selection.", async () => {
|
||||||
|
//Arrange
|
||||||
|
const { wrapper } = setupMocks({ modelValueProp: "value" });
|
||||||
|
const valueToSelect = "newvalue";
|
||||||
|
|
||||||
|
//Act
|
||||||
|
wrapper.setValue({ selectedValue: valueToSelect });
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedValue: "newvalue" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function setupMocks({ modelValueProp = "", dataFromStoreApi = [] }) {
|
||||||
|
//Mock store
|
||||||
|
store.dispatch = jest.fn(() => dataFromStoreApi);
|
||||||
|
store.getters = { vehicle: { year: 2019, make: "honda", model: "civic", style: "4 Door" } };
|
||||||
|
|
||||||
|
const mountOptions = getMountOptions({
|
||||||
|
store: {
|
||||||
|
dispatch: store.dispatch,
|
||||||
|
getters: store.getters,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Mock props
|
||||||
|
const mockMixin = {
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.propsData = {
|
||||||
|
modelValue: modelValueProp,
|
||||||
|
};
|
||||||
|
|
||||||
|
mountOptions.mixins = [mockMixin];
|
||||||
|
const wrapper = shallowMount(vehicleQuestion, mountOptions);
|
||||||
|
|
||||||
|
return { wrapper };
|
||||||
|
}
|
||||||
62
src/layouts/vehicle/vehicle-question/vehicle-question.vue
Normal file
62
src/layouts/vehicle/vehicle-question/vehicle-question.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<dropdownQuestion
|
||||||
|
:options="values"
|
||||||
|
disableAutoFill
|
||||||
|
v-model="selectedValue"
|
||||||
|
:isDisabled="!values.length" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-question";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle-question",
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
values: [],
|
||||||
|
selectedIndex: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
updateValues: Function,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
dropdownQuestion,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
selectedValue: {
|
||||||
|
get() {
|
||||||
|
return this.selectedIndex?.toString();
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
this.selectedIndex = newValue;
|
||||||
|
newValue = newValue != null && newValue > -1 ? this.values[newValue] : null;
|
||||||
|
this.$emit("update:modelValue", newValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async getNewValues(selectedYMMS) {
|
||||||
|
const results = await this.updateValues();
|
||||||
|
this.values = results?.data;
|
||||||
|
if (this.values.length == 1) {
|
||||||
|
this.selectedValue = 0;
|
||||||
|
} else {
|
||||||
|
this.selectedValue =
|
||||||
|
selectedYMMS != null ? this.values.indexOf(selectedYMMS) : null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearValues() {
|
||||||
|
this.values = [];
|
||||||
|
this.selectedValue = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
91
src/layouts/vehicle/vehicle.spec.js
Normal file
91
src/layouts/vehicle/vehicle.spec.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Components
|
||||||
|
import vehicle from "@/layouts/vehicle/vehicle.vue";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { nextTick } from "vue";
|
||||||
|
import { mount } from "@vue/test-utils";
|
||||||
|
|
||||||
|
describe("vehicle.vue", () => {
|
||||||
|
test('"Continue" button is enabled after YMMS is selected.', async () => {
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
const continueButton = wrapper.get('[data-test-id="funnel-footer-main-button"]');
|
||||||
|
|
||||||
|
expect(continueButton.attributes()["aria-disabled"]).toBe("false");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("arePagePrerequisitesValid should be true ", async () => {
|
||||||
|
//Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
|
||||||
|
//Act
|
||||||
|
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(arePagePrerequisitesValid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("navigation", () => {
|
||||||
|
test("forwardButtonAction should trigger navigateForward", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks();
|
||||||
|
wrapper.vm.navigateForward = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.forwardButtonAction();
|
||||||
|
|
||||||
|
//Assert
|
||||||
|
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const FunnelFooterWidgetMockData = {
|
||||||
|
ForwardButtonText: "Continue",
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupMocks() {
|
||||||
|
const mockRoute = {
|
||||||
|
query: {
|
||||||
|
fmgPage: "vehicle",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockRouter = {
|
||||||
|
navigate: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = mount(vehicle, {
|
||||||
|
global: {
|
||||||
|
mixins: [
|
||||||
|
{
|
||||||
|
methods: {
|
||||||
|
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
|
||||||
|
if (cmsWidgetName === "FunnelFooterWidget") {
|
||||||
|
if (fieldName === "ForwardButtonText") {
|
||||||
|
return FunnelFooterWidgetMockData.ForwardButtonText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}),
|
||||||
|
getFooterInfoBoxHeight: jest.fn(() => 80),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
mocks: {
|
||||||
|
$route: mockRoute,
|
||||||
|
$router: mockRouter,
|
||||||
|
},
|
||||||
|
|
||||||
|
stubs: {
|
||||||
|
FunnelHeader: true,
|
||||||
|
FunnelSubHeader: true,
|
||||||
|
VehicleBanner: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { mockRoute, mockRouter, wrapper };
|
||||||
|
}
|
||||||
278
src/layouts/vehicle/vehicle.vue
Normal file
278
src/layouts/vehicle/vehicle.vue
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
<template>
|
||||||
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||||
|
<div class="page-container-grouped-styles position-relative">
|
||||||
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||||
|
<div class="select-car">
|
||||||
|
<div class="container-fluid pb-2">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="select-car-form rounded">
|
||||||
|
<funnelSubHeader
|
||||||
|
cmsWidgetName="FunnelSubHeaderWidget"
|
||||||
|
class="siteSubHeader" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleYearQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedYear"
|
||||||
|
cmsWidgetName="VehicleYearQuestion"
|
||||||
|
:updateValues="updateYearValues"
|
||||||
|
validationRules="year-required"
|
||||||
|
placeHolderText="Select year"
|
||||||
|
inputId="yearQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleMakeQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedMake"
|
||||||
|
cmsWidgetName="VehicleMakeQuestion"
|
||||||
|
:updateValues="updateMakeValues"
|
||||||
|
validationRules="make-required"
|
||||||
|
placeHolderText="Select make"
|
||||||
|
inputId="makeQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleModelQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedModel"
|
||||||
|
cmsWidgetName="VehicleModelQuestion"
|
||||||
|
:updateValues="updateModelValues"
|
||||||
|
validationRules="model-required"
|
||||||
|
placeHolderText="Select model"
|
||||||
|
inputId="modelQuestionField" />
|
||||||
|
|
||||||
|
<vehicleQuestion
|
||||||
|
ref="vehicleStyleQuestion"
|
||||||
|
class="mb-2 mt-4"
|
||||||
|
v-model="selectedStyle"
|
||||||
|
cmsWidgetName="VehicleStyleQuestion"
|
||||||
|
:updateValues="updateStyleValues"
|
||||||
|
validationRules="style-required"
|
||||||
|
placeHolderText="Select style"
|
||||||
|
inputId="styleQuestionField" />
|
||||||
|
|
||||||
|
<vehicleBanner
|
||||||
|
cmsWidgetName="VehicleBannerWidget"
|
||||||
|
:displayGenericVehicleImage="displayGeneric"
|
||||||
|
class="mt-5 mb-3"
|
||||||
|
ref="banner" />
|
||||||
|
|
||||||
|
<funnelFooter
|
||||||
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
|
@ForwardClicked="forwardButtonAction"
|
||||||
|
:isForwardActionDisabled="!meta.valid"
|
||||||
|
ref="funnelFooter" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Components
|
||||||
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||||
|
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||||
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||||
|
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
||||||
|
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
|
||||||
|
|
||||||
|
// Supporting files
|
||||||
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||||
|
import { experimentUniverses } from "@/constants/experiments";
|
||||||
|
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
|
||||||
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||||
|
import { Form, defineRule } from "vee-validate";
|
||||||
|
import { required } from "@/helpers/validation-rules";
|
||||||
|
import { errorMessages } from "@/constants/error-messages";
|
||||||
|
import store from "@/store";
|
||||||
|
import { storeActions } from "@/constants/store-actions.js";
|
||||||
|
import baseMixin from "@/mixins/base-mixin.js";
|
||||||
|
|
||||||
|
//define validation rules
|
||||||
|
defineRule("year-required", required(errorMessages.YEAR_REQUIRED));
|
||||||
|
defineRule("make-required", required(errorMessages.MAKE_REQUIRED));
|
||||||
|
defineRule("model-required", required(errorMessages.MODEL_REQUIRED));
|
||||||
|
defineRule("style-required", required(errorMessages.STYLE_REQUIRED));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "vehicle",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedYear: null,
|
||||||
|
selectedMake: null,
|
||||||
|
selectedModel: null,
|
||||||
|
selectedStyle: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
cmsWidgetName: String,
|
||||||
|
validationRules: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore);
|
||||||
|
},
|
||||||
|
|
||||||
|
async beforeRouteEnter(to, from, next) {
|
||||||
|
// Call APIs
|
||||||
|
|
||||||
|
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||||
|
const experimentForLogging = store.getters.applicationUser.experiments.find(
|
||||||
|
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
|
||||||
|
if (experimentForLogging !== undefined) {
|
||||||
|
// Log experiment exposure
|
||||||
|
baseMixin.methods.dispatchStoreAction(
|
||||||
|
storeActions.LOG_EXPERIMENT_EXPOSURE,
|
||||||
|
{
|
||||||
|
userId: getDeviceIdValue(),
|
||||||
|
sessionKey: getSessionKeyValue(),
|
||||||
|
pageName: to.query.fmgPage,
|
||||||
|
experiment: experimentForLogging,
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settle promises and get results
|
||||||
|
const promiseResultMap = [
|
||||||
|
{
|
||||||
|
resultKey: "cmsContent",
|
||||||
|
promise: cmsContentPromise,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let resultMap = await settleAllPromises(promiseResultMap);
|
||||||
|
|
||||||
|
// Call the "next" function to complete the transition to this page.
|
||||||
|
next((vm) => {
|
||||||
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selectedYear(year) {
|
||||||
|
const parsedYear = parseInt(year);
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
|
||||||
|
if (year) {
|
||||||
|
this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleMakeQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedMake(make) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
|
||||||
|
if (make) {
|
||||||
|
this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleModelQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedModel(model) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
|
||||||
|
if (model) {
|
||||||
|
this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore);
|
||||||
|
} else {
|
||||||
|
this.$refs["vehicleStyleQuestion"].clearValues();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedStyle(style) {
|
||||||
|
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
|
||||||
|
this.setVehicle();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setVehicle() {
|
||||||
|
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
|
||||||
|
year: this.$store.getters.vehicle.year,
|
||||||
|
make: this.$store.getters.vehicle.make,
|
||||||
|
model: this.$store.getters.vehicle.model,
|
||||||
|
style: this.$store.getters.vehicle.style,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
arePagePrerequisitesValid() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async forwardButtonAction() {
|
||||||
|
return this.navigateForward();
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateForward() {
|
||||||
|
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateYearValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMakeValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateModelValues() {
|
||||||
|
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
|
||||||
|
make: store.getters.vehicle.make,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateStyleValues() {
|
||||||
|
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
|
||||||
|
year: store.getters.vehicle.year,
|
||||||
|
make: store.getters.vehicle.make,
|
||||||
|
model: store.getters.vehicle.model,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
displayGeneric() {
|
||||||
|
return !this.selectedStyle;
|
||||||
|
},
|
||||||
|
selectedYearfromStore() {
|
||||||
|
return store.getters.vehicle.year;
|
||||||
|
},
|
||||||
|
selectedMakefromStore() {
|
||||||
|
return store.getters.vehicle.make;
|
||||||
|
},
|
||||||
|
selectedModelfromStore() {
|
||||||
|
return store.getters.vehicle.model;
|
||||||
|
},
|
||||||
|
selectedStylefromStore() {
|
||||||
|
return store.getters.vehicle.style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
funnelHeader,
|
||||||
|
funnelFooter,
|
||||||
|
funnelSubHeader,
|
||||||
|
vehicleBanner,
|
||||||
|
Form,
|
||||||
|
vehicleQuestion,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.select-car-form {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.siteSubHeader {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
validationRules="email-address-required|email-address-format" />
|
validationRules="email-address-required|email-address-format" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-2">
|
<div class="row mb-0">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
|
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -446,9 +446,7 @@ export default {
|
||||||
|
|
||||||
const payment = store.getters.payment;
|
const payment = store.getters.payment;
|
||||||
|
|
||||||
if (store.getters.order.referralNumber?.length === 6) {
|
if (payment.isInsurance && payment.insuranceCoverage.isVerified) {
|
||||||
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal });
|
|
||||||
} else if (payment.isInsurance && payment.insuranceCoverage.isVerified) {
|
|
||||||
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal });
|
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal });
|
||||||
} else {
|
} else {
|
||||||
self.$router.navigateWithSaving(
|
self.$router.navigateWithSaving(
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,13 @@ import analyticsMixin from "@/mixins/analytics-mixin";
|
||||||
import { experimentTriggers } from "../constants/experiments";
|
import { experimentTriggers } from "../constants/experiments";
|
||||||
import { applicationConfig } from "../constants/application-config";
|
import { applicationConfig } from "../constants/application-config";
|
||||||
|
|
||||||
|
import review from "@/layouts/review/review";
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: "/review", // This is a temporary route for testing.
|
||||||
|
name: "review",
|
||||||
|
component: review,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "root",
|
name: "root",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const fmgPageValues = {
|
||||||
VEHICLE_MAKE: "vehicle-make",
|
VEHICLE_MAKE: "vehicle-make",
|
||||||
VEHICLE_MODEL: "vehicle-model",
|
VEHICLE_MODEL: "vehicle-model",
|
||||||
VEHICLE_STYLE: "vehicle-style",
|
VEHICLE_STYLE: "vehicle-style",
|
||||||
|
VEHICLE: "vehicle",
|
||||||
VEHICLE_DAMAGE: "vehicle-damage",
|
VEHICLE_DAMAGE: "vehicle-damage",
|
||||||
ADDRESS_LOOKUP: "address-lookup",
|
ADDRESS_LOOKUP: "address-lookup",
|
||||||
VIN_LOOKUP: "vin-lookup",
|
VIN_LOOKUP: "vin-lookup",
|
||||||
|
|
@ -17,6 +18,8 @@ const fmgPageValues = {
|
||||||
SERVICE_LOCATION: "service-location",
|
SERVICE_LOCATION: "service-location",
|
||||||
HERITAGE: "heritage",
|
HERITAGE: "heritage",
|
||||||
SCHEDULE: "schedule",
|
SCHEDULE: "schedule",
|
||||||
|
CUSTOMER_DETAILS: "customer-details",
|
||||||
|
REVIEW: "review",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { fmgPageValues };
|
export { fmgPageValues };
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,10 @@ const navigationScenarios = {
|
||||||
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
|
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
|
||||||
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
|
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
|
||||||
|
|
||||||
// Scheduling
|
// Review
|
||||||
SELECTED_LOCATION: "SELECTED_LOCATION",
|
CLICKED_VEHICLE_EDIT: "CLICKED_VEHICLE_EDIT",
|
||||||
|
CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT",
|
||||||
|
CLICKED_SERVICE_PACKAGE_EDIT: "CLICKED_SERVICE_PACKAGE_EDIT",
|
||||||
};
|
};
|
||||||
|
|
||||||
export { navigationScenarios };
|
export { navigationScenarios };
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,15 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
|
||||||
// Get store from router/index.js instead of importing it here to get updated values
|
// Get store from router/index.js instead of importing it here to get updated values
|
||||||
const routingTable = function (store) {
|
const routingTable = function (store) {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
fmgPageValue: fmgPageValues.VEHICLE,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||||
|
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
|
||||||
maps: [
|
maps: [
|
||||||
|
|
@ -57,7 +66,7 @@ const routingTable = function (store) {
|
||||||
maps: [
|
maps: [
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_BACK,
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
|
destinationFmgPageValue: fmgPageValues.VEHICLE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
scenario: navigationScenarios.CLICKED_FORWARD_WITH_VIN,
|
||||||
|
|
@ -421,7 +430,7 @@ const routingTable = function (store) {
|
||||||
destinationFmgPageValue: fmgPageValues.QUOTE,
|
destinationFmgPageValue: fmgPageValues.QUOTE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: navigationScenarios.SELECTED_LOCATION,
|
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||||
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -433,6 +442,44 @@ const routingTable = function (store) {
|
||||||
scenario: navigationScenarios.CLICKED_BACK,
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
|
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||||
|
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
|
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||||
|
destinationFmgPageValue: fmgPageValues.REVIEW,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fmgPageValue: fmgPageValues.REVIEW,
|
||||||
|
maps: [
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_VEHICLE_EDIT,
|
||||||
|
destinationFmgPageValue: fmgPageValues.VEHICLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_DAMAGE_EDIT,
|
||||||
|
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_SERVICE_PACKAGE_EDIT,
|
||||||
|
destinationFmgPageValue: fmgPageValues.QUOTE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: navigationScenarios.CLICKED_BACK,
|
||||||
|
destinationFmgPageValue: fmgPageValues.CUSTOMER_DETAILS,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||||
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
|
||||||
import { deepEqual } from "@/helpers/object-helper";
|
import { deepEqual } from "@/helpers/object-helper";
|
||||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||||
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
|
||||||
// Export State
|
// Export State
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
|
|
@ -280,6 +281,16 @@ export const mutations = {
|
||||||
state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
|
state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateCustomerDetails(state, detailsInfo) {
|
||||||
|
if (detailsInfo) {
|
||||||
|
state.order.customerDetails.firstName = detailsInfo.firstName;
|
||||||
|
state.order.customerDetails.lastName = detailsInfo.lastName;
|
||||||
|
state.order.customerDetails.email = detailsInfo.email;
|
||||||
|
state.order.customerDetails.telephone = detailsInfo.telephone;
|
||||||
|
state.order.customerDetails.textUpdates = detailsInfo.textUpdates;
|
||||||
|
state.order.customerDetails.techNotes = detailsInfo.techNotes;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// applicationUser MUTATIONS
|
// applicationUser MUTATIONS
|
||||||
updateSaveSessionPromise(state, saveSessionPromise) {
|
updateSaveSessionPromise(state, saveSessionPromise) {
|
||||||
|
|
@ -526,6 +537,14 @@ export const getters = {
|
||||||
isMobileAppointment: (state) => {
|
isMobileAppointment: (state) => {
|
||||||
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
|
||||||
},
|
},
|
||||||
|
isRecalibrationOnOrder: (state) => {
|
||||||
|
return getHasRecalibrationPart(state);
|
||||||
|
},
|
||||||
|
areRearWipersOnOrder: (state) => {
|
||||||
|
return !!state.order.lineItems.vaps?.some(
|
||||||
|
(vap) => vap.partType.toUpperCase() === partTypeStrings.REAR_WIPER.toUpperCase()
|
||||||
|
);
|
||||||
|
},
|
||||||
lineItems: (state) => state.order.lineItems,
|
lineItems: (state) => state.order.lineItems,
|
||||||
pageData: (state) => (page) => {
|
pageData: (state) => (page) => {
|
||||||
return state.applicationUser.pageData[page];
|
return state.applicationUser.pageData[page];
|
||||||
|
|
@ -1924,6 +1943,10 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
context.commit(storeMutations.UPDATE_SCHEDULE, scheduleInfo);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
saveDetails(context, scheduleInfo) {
|
||||||
|
context.commit(storeMutations.UPDATE_DETAILS, scheduleInfo);
|
||||||
|
},
|
||||||
|
|
||||||
saveServiceZipCodeInfo(context, serviceZipCodeInfo) {
|
saveServiceZipCodeInfo(context, serviceZipCodeInfo) {
|
||||||
if (
|
if (
|
||||||
context.state.order.serviceLocation &&
|
context.state.order.serviceLocation &&
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.textbox-question,
|
&.textbox-question,
|
||||||
&.dropdown-question {
|
&.dropdown-question,
|
||||||
|
&.phone-number-question {
|
||||||
p {
|
p {
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ export default {
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: $gray-600;
|
color: $gray-600;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue