Merge pull request #986 from Safelite/feature/CSR-1012
Feature/csr 1012
This commit is contained in:
commit
cc13583d95
37 changed files with 2554 additions and 260 deletions
|
|
@ -74,6 +74,10 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/rain-defense",
|
||||
method: "GET",
|
||||
},
|
||||
GetMobileFeePart: {
|
||||
url: "/parts/api/v1/parts/mobile-fee",
|
||||
method: "GET",
|
||||
},
|
||||
GetSupportingItems: {
|
||||
url: "/parts/api/v1/parts/supporting-items",
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const storeActions = {
|
|||
GET_CAPABILITY_QUESTIONS: "getCapabilityQuestions",
|
||||
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
|
||||
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
||||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||
SAVE_SESSION: "saveSession",
|
||||
LOAD_SESSION: "loadSession",
|
||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
|
|||
:lastValuePushedToGa="lastValuePushedToGa"
|
||||
:setLastValuePushedToGa="setLastValuePushedToGa"
|
||||
:suppressError="suppressError"
|
||||
@buttonEvent="handleButtonEvent"
|
||||
v-model="selectedValues" />
|
||||
<!-- For nested questions -->
|
||||
<transition name="fade" mode="out-in">
|
||||
|
|
@ -220,6 +221,10 @@ export default {
|
|||
setLastValuePushedToGa(lastValuePushedToGa) {
|
||||
this.lastValuePushedToGa = lastValuePushedToGa;
|
||||
},
|
||||
handleButtonEvent(event) {
|
||||
const eventName = event.eventName;
|
||||
this.$emit(`buttonEvent.${eventName}`, event.args);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
listButton,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import dropdownQuestion from "./dropdown-question";
|
||||
import crypto from "crypto";
|
||||
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
|
|
@ -11,6 +12,8 @@ const mockMixin = {
|
|||
},
|
||||
};
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
// TODO: Remove the following from dropdown-question.vue -> :class="(errors && errors.length) || hasError ? 'has-error' : ''"
|
||||
// It is not being used.
|
||||
describe("dropdownQuestion.vue", () => {
|
||||
|
|
@ -46,23 +49,6 @@ describe("dropdownQuestion.vue", () => {
|
|||
expect(label.text()).toContain(questionText);
|
||||
});
|
||||
|
||||
it("Should return input id as the id of the select field", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(dropdownQuestion, {
|
||||
propsData: {
|
||||
inputId: "input ID",
|
||||
options: {},
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Act
|
||||
const select = wrapper.find("select");
|
||||
|
||||
// Assert
|
||||
expect(select.attributes().id).toEqual("input ID");
|
||||
});
|
||||
|
||||
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(dropdownQuestion, {
|
||||
|
|
@ -118,7 +104,7 @@ describe("dropdownQuestion.vue", () => {
|
|||
const wrapper = shallowMount(dropdownQuestion, {
|
||||
propsData: {
|
||||
options: {},
|
||||
modelValue: 0,
|
||||
modelValue: "0",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
:for="inputId"
|
||||
:aria-label="questionText"
|
||||
class="form-label"
|
||||
v-html="labelText"></label>
|
||||
v-html="questionText"></label>
|
||||
<select
|
||||
v-model="selectedOption"
|
||||
class="form-select"
|
||||
|
|
@ -35,20 +35,23 @@ export default {
|
|||
name: "dropdown-question",
|
||||
props: {
|
||||
modelValue: String,
|
||||
inputId: String,
|
||||
customInputId: String,
|
||||
options: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
disableAutoFill: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
hasError: Boolean,
|
||||
placeHolderText: String,
|
||||
},
|
||||
setup(props) {
|
||||
const inputId = !props.customInputId
|
||||
? `dropdown-${crypto.randomUUID()}`
|
||||
: props.customInputId;
|
||||
|
||||
const propsClone = Object.assign({}, props);
|
||||
const modelValue = propsClone.modelValue;
|
||||
let initialValue;
|
||||
|
|
@ -69,12 +72,13 @@ export default {
|
|||
};
|
||||
|
||||
const { errorMessage, handleBlur, handleChange, meta, errors } = useField(
|
||||
props.inputId,
|
||||
inputId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
inputId,
|
||||
errorMessage,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
|
|
@ -82,6 +86,9 @@ export default {
|
|||
errors,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$emit("dropdownQuestionEvent.inputIdAssigned", this.inputId);
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
|
|
@ -94,30 +101,6 @@ export default {
|
|||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
labelText: {
|
||||
get: function () {
|
||||
const noBreakChar = "⁠";
|
||||
var questionText = "";
|
||||
if (this.disableAutoFill) {
|
||||
var words = this.questionText.toString().split(/[ ]+/);
|
||||
words.forEach(function (word) {
|
||||
const position = 1;
|
||||
word = [
|
||||
word.toString().slice(0, position),
|
||||
noBreakChar,
|
||||
word.toString().slice(position),
|
||||
].join("");
|
||||
questionText += `${word} `;
|
||||
});
|
||||
|
||||
questionText = questionText.trimEnd();
|
||||
} else {
|
||||
questionText = this.questionText.toString();
|
||||
}
|
||||
|
||||
return questionText;
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedOption(newValue) {
|
||||
|
|
|
|||
|
|
@ -1,68 +1,199 @@
|
|||
jest.mock("vee-validate", () => ({
|
||||
useForm: jest.fn(),
|
||||
useIsFormTouched: jest.fn(),
|
||||
useIsFormDirty: jest.fn(),
|
||||
useIsFormValid: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockValidate = (returnValue) => jest.fn(async () => Promise.resolve({ valid: returnValue }));
|
||||
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import Modal from "./modal";
|
||||
import modal from "./modal";
|
||||
import crypto from "crypto";
|
||||
import { useForm, useIsFormDirty, useIsFormTouched, useIsFormValid } from "vee-validate";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const footerButtonText = "Sample footer text here.";
|
||||
const headerText = "Sample header text here.";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("modal.vue", () => {
|
||||
it("Should display header text when HeaderText is defined in the CMS", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(Modal, {
|
||||
mixins: [mockMixin],
|
||||
it("Should display modal header text when headerText is defined", async () => {
|
||||
// Arrange / Act
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
headerText: headerText,
|
||||
footerButtonText: footerButtonText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["HeaderText"]));
|
||||
|
||||
// Assert
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(headerText));
|
||||
});
|
||||
|
||||
it("Should display subheader text when SubheaderText is defined in the CMS", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(Modal, {
|
||||
mixins: [mockMixin],
|
||||
it("Should display footer button text when footerButtonText is defined", async () => {
|
||||
// Arrange / Act
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
footerButtonText: footerButtonText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["SubheaderText"]));
|
||||
|
||||
// Assert
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(footerButtonText));
|
||||
});
|
||||
|
||||
it("Should insert image url when Image is defined in the CMS", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(Modal, {
|
||||
mixins: [mockMixin],
|
||||
it("Should emit 'footer-button-event' if the form is valid", async () => {
|
||||
// Arrange
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["Image"]));
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
const buttonMain = wrapper.findComponent({ ref: "buttonMain" });
|
||||
await buttonMain.trigger("click-event");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("footer-button-event")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should display body text when BodyText is defined in the CMS", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(Modal, {
|
||||
mixins: [mockMixin],
|
||||
it("Should not emit 'footer-button-event' if the form is invalid", async () => {
|
||||
// Arrange
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"]));
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
const buttonMain = wrapper.findComponent({ ref: "buttonMain" });
|
||||
await buttonMain.trigger("click-event");
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("footer-button-event")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Should have a disabled footer button when the form has not been touched", async () => {
|
||||
// Arrange / Act
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
useIsFormTouched.mockReturnValue(false);
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isFooterButtonDisabled).toBe(true);
|
||||
});
|
||||
|
||||
it("Should have a disabled footer button when the form is invalid", async () => {
|
||||
// Arrange / Act
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
useIsFormTouched.mockReturnValue(true);
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(false);
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isFooterButtonDisabled).toBe(true);
|
||||
});
|
||||
|
||||
it("Should call bootstrap Modal method 'show' when calling 'openModal'", async () => {
|
||||
// Arrange
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(true);
|
||||
|
||||
const showMock = jest.spyOn(Modal.prototype, "show");
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
wrapper.vm.openModal();
|
||||
|
||||
// Assert
|
||||
expect(showMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Should call bootstrap Modal method 'hide' when calling 'closeModal'", async () => {
|
||||
// Arrange
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(true);
|
||||
|
||||
const hideMock = jest.spyOn(Modal.prototype, "hide");
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
footerButtonText: footerButtonText,
|
||||
headerText: headerText,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
wrapper.vm.openModal();
|
||||
wrapper.vm.closeModal();
|
||||
|
||||
// Assert
|
||||
expect(hideMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
return mockCmsContent[cmsFieldName];
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const mockCmsContent = {
|
||||
HeaderText: "Sample header text here.",
|
||||
SubheaderText: "Sample subheader text here.",
|
||||
Image: "https://www.sampleImage.sample",
|
||||
BodyText: "Sample body text here.",
|
||||
FooterText: "Sample footer text here.",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,39 +2,38 @@
|
|||
<!-- Modal -->
|
||||
<div
|
||||
class="modal fade modal-component"
|
||||
v-on="{ 'hidden.bs.modal': resetButtonStyle }"
|
||||
:id="cmsWidgetName"
|
||||
v-on="{ 'hidden.bs.modal': onModalClosed, 'shown.bs.modal': onModalOpened }"
|
||||
:id="modalId"
|
||||
tabindex="-1"
|
||||
aria-labelledby="ModalComponentLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header mb-2 mt-2">
|
||||
<label
|
||||
v-if="headerText"
|
||||
class="modal-title d-flex justify-content-center pb-0 w-100">
|
||||
{{ headerText }}
|
||||
</label>
|
||||
<button
|
||||
ref="closeButton"
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body ps-4 pe-4 pt-5 pb-4">
|
||||
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
|
||||
<h5 class="mb-4" v-html="ModalHeadline"></h5>
|
||||
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
|
||||
<p class="mb-0" v-html="ModalBodyText"></p>
|
||||
<p
|
||||
class="my-4 caption modal-sub-body"
|
||||
v-if="ModalSubBodyText"
|
||||
v-html="ModalSubBodyText"></p>
|
||||
<div class="modal-body ps-5 pe-5 pb-4 pt-0">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer px-5 py-4">
|
||||
<buttonMain
|
||||
isPrimary
|
||||
class="w-100"
|
||||
ref="buttonMain"
|
||||
suppressLoader
|
||||
:buttonText="ModalCloseButtonText"
|
||||
@click-event="buttonClick"
|
||||
data-bs-dismiss="modal" />
|
||||
loaderColor="white"
|
||||
:buttonText="footerButtonText"
|
||||
@click-event="validateAndEmit"
|
||||
:class="isFooterButtonDisabled && 'form-test-invalid'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -43,36 +42,71 @@
|
|||
|
||||
<script>
|
||||
import buttonMain from "@/ux-components/button-main/button-main";
|
||||
import { Modal } from "bootstrap";
|
||||
import { useForm, useIsFormTouched, useIsFormDirty, useIsFormValid } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "modal",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
headerText: String,
|
||||
footerButtonText: String,
|
||||
onModalOpenedCallback: {
|
||||
type: Function,
|
||||
},
|
||||
onModalClosedCallback: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
ModalHeadline() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||
},
|
||||
ModalSubheadertext() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||
},
|
||||
ModalBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
},
|
||||
ModalSubBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
},
|
||||
ModalImage() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Image");
|
||||
},
|
||||
ModalCloseButtonText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
setup() {
|
||||
const modalId = `modal-${crypto.randomUUID()}`;
|
||||
|
||||
const form = useForm();
|
||||
const isFormTouched = useIsFormTouched();
|
||||
const isFormDirty = useIsFormDirty();
|
||||
const isFormValid = useIsFormValid();
|
||||
|
||||
return {
|
||||
modalId,
|
||||
form,
|
||||
isFormTouched,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async validateAndEmit() {
|
||||
const validationResult = await this.form.validate();
|
||||
if (validationResult.valid) {
|
||||
this.$emit("footer-button-event");
|
||||
}
|
||||
this.resetButtonStyle();
|
||||
},
|
||||
resetButtonStyle() {
|
||||
this.$refs.buttonMain.resetButtonStyle();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.onModalOpenedCallback?.();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.resetButtonStyle();
|
||||
this.onModalClosedCallback?.();
|
||||
},
|
||||
openModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId));
|
||||
modal.show();
|
||||
},
|
||||
closeModal() {
|
||||
const modal = Modal.getInstance(document.getElementById(this.modalId));
|
||||
modal.hide();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isFooterButtonDisabled() {
|
||||
if (!this.isFormTouched) {
|
||||
return !this.isFormValid;
|
||||
}
|
||||
return !this.isFormDirty || !this.isFormValid;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonMain,
|
||||
|
|
@ -92,9 +126,17 @@ export default {
|
|||
.modal-header {
|
||||
border-bottom: none;
|
||||
.btn-close {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 1rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.6874 1.82179L9.50889 8L15.6874 14.1782C16.1042 14.595 16.1042 15.2707 15.6874 15.6875C15.4843 15.8907 15.2146 16 14.9328 16C14.6514 16 14.3818 15.8906 14.1787 15.6875L8.00017 9.50934L1.82171 15.6875C1.6182 15.8907 1.34876 16 1.06708 16C0.785733 16 0.516056 15.8906 0.312965 15.6875C-0.10429 15.2706 -0.10429 14.5952 0.312797 14.1784L6.03276 8.45867L6.49146 8L0.312945 1.82177C-0.10429 1.40483 -0.10429 0.729418 0.312797 0.31262C0.728936 -0.104145 1.40489 -0.104051 1.82185 0.31262L7.54152 6.03202L8.00017 6.49065L14.1786 0.312472C14.5955 -0.104107 15.2707 -0.104201 15.6874 0.312452C16.1042 0.729289 16.1042 1.40496 15.6874 1.82179Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-weight: 500;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
&.modal-component {
|
||||
.modal-dialog {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@ describe("modal.vue", () => {
|
|||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["Text"]));
|
||||
});
|
||||
|
||||
it("Should display the custom text when it is passed in", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(TextBlock, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
customText: "customText",
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining("customText"));
|
||||
});
|
||||
|
||||
it("Should contain the typeStyle class as defined by the prop", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(TextBlock, {
|
||||
|
|
@ -18,6 +30,7 @@ describe("modal.vue", () => {
|
|||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockProps["typeStyle"]));
|
||||
});
|
||||
|
||||
it("Should contain the justifyText class as defined by the prop", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(TextBlock, {
|
||||
|
|
@ -26,6 +39,7 @@ describe("modal.vue", () => {
|
|||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockProps["justifyText"]));
|
||||
});
|
||||
|
||||
it("Should contain the fontWeight class as defined by the prop", async () => {
|
||||
// Act
|
||||
const wrapper = shallowMount(TextBlock, {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
export default {
|
||||
name: "textBlock",
|
||||
props: {
|
||||
customText: String, // used to allow the insert of token values into textblock
|
||||
justifyText: String, // left, right, center
|
||||
typeStyle: String, // h1-h6, body, small, label, caption (see Figma or Confluence documentation)
|
||||
fontWeight: String, // bold=500, default is 400
|
||||
|
|
@ -16,6 +17,9 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
TextBlockCopy() {
|
||||
if (this.customText) {
|
||||
return this.customText;
|
||||
}
|
||||
return this.getCmsContent(this.cmsWidgetName, "Text");
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import textboxQuestion from "./textbox-question";
|
||||
import crypto from "crypto";
|
||||
|
||||
// Mock CMS content
|
||||
const questionText = "Question Text";
|
||||
|
|
@ -12,6 +13,8 @@ const mockMixin = {
|
|||
};
|
||||
const maska = jest.fn();
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("textboxQuestion.vue", () => {
|
||||
it("Should render a text input", async () => {
|
||||
// Arrange
|
||||
|
|
@ -73,45 +76,6 @@ describe("textboxQuestion.vue", () => {
|
|||
expect(paragraph.attributes("class")).toContain("form-control");
|
||||
});
|
||||
|
||||
it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
},
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Act
|
||||
const label = wrapper.find("label");
|
||||
|
||||
// Assert
|
||||
expect(label.text()).toContain(questionText);
|
||||
});
|
||||
|
||||
it("Should return input id as the id of the input field", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
global: {
|
||||
directives: {
|
||||
maska: maska,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
inputId: "input ID",
|
||||
},
|
||||
mixins: [mockMixin],
|
||||
});
|
||||
|
||||
// Act
|
||||
const input = wrapper.find("input");
|
||||
|
||||
// Assert
|
||||
expect(input.attributes().id).toEqual("input ID");
|
||||
});
|
||||
|
||||
it("Should render the 'questionText' data value as the aria-label attribute.", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(textboxQuestion, {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<template>
|
||||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<label
|
||||
v-if="displayQuestionText"
|
||||
:for="inputId"
|
||||
:aria-label="questionText"
|
||||
class="form-label"
|
||||
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
|
||||
v-html="labelText"></label>
|
||||
v-html="questionText"></label>
|
||||
<div class="input-wrapper" :class="[includeSearchIcon ? 'has-search-icon' : '']">
|
||||
<input
|
||||
class="form-control"
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
|
||||
<script>
|
||||
import { useField, validate } from "vee-validate";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
|
||||
export default {
|
||||
name: "textbox-question",
|
||||
|
|
@ -53,8 +53,12 @@ export default {
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
displayQuestionText: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
modelValue: String,
|
||||
inputId: String,
|
||||
customInputId: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
hasIcon: Boolean, // If input has an icon
|
||||
|
|
@ -72,6 +76,8 @@ export default {
|
|||
includeSearchIcon: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const inputId = !props.customInputId ? `input-${crypto.randomUUID()}` : props.customInputId;
|
||||
|
||||
const propsClone = Object.assign({}, props);
|
||||
const modelValue = propsClone.modelValue;
|
||||
let initialValue;
|
||||
|
|
@ -92,12 +98,13 @@ export default {
|
|||
};
|
||||
|
||||
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
|
||||
props.inputId,
|
||||
inputId,
|
||||
props.validationRules,
|
||||
fieldOptions
|
||||
);
|
||||
|
||||
return {
|
||||
inputId,
|
||||
errorMessage,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
|
|
@ -118,30 +125,9 @@ export default {
|
|||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
labelText: {
|
||||
get: function () {
|
||||
const noBreakChar = "⁠";
|
||||
var questionText = "";
|
||||
if (this.disableAutoFill) {
|
||||
var words = this.questionText.toString().split(/[ ]+/);
|
||||
words.forEach(function (word) {
|
||||
const position = 1;
|
||||
word = [
|
||||
word.toString().slice(0, position),
|
||||
noBreakChar,
|
||||
word.toString().slice(position),
|
||||
].join("");
|
||||
questionText += `${word} `;
|
||||
});
|
||||
|
||||
questionText = questionText.trimEnd();
|
||||
} else {
|
||||
questionText = this.questionText.toString();
|
||||
}
|
||||
|
||||
return questionText;
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
|
||||
},
|
||||
watch: {
|
||||
async value(newValue) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
import { mount } from "@vue/test-utils";
|
||||
import contentGroupModal from "./content-group-modal";
|
||||
import crypto from "crypto";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
describe("content-group-modal.vue", () => {
|
||||
it("Should display header text when HeaderText is defined in the CMS", async () => {
|
||||
const wrapper = mount(contentGroupModal, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["HeaderText"]));
|
||||
});
|
||||
|
||||
it("Should display subheader text when SubheaderText is defined in the CMS", async () => {
|
||||
const wrapper = mount(contentGroupModal, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["SubheaderText"]));
|
||||
});
|
||||
|
||||
it("Should insert image url when Image is defined in the CMS", async () => {
|
||||
const wrapper = mount(contentGroupModal, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["Image"]));
|
||||
});
|
||||
|
||||
it("Should display body text when BodyText is defined in the CMS", async () => {
|
||||
const wrapper = mount(contentGroupModal, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: "test",
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"]));
|
||||
});
|
||||
});
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
return mockCmsContent[cmsFieldName];
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const mockCmsContent = {
|
||||
HeaderText: "Sample header text here.",
|
||||
SubheaderText: "Sample subheader text here.",
|
||||
Image: "https://www.sampleImage.sample",
|
||||
BodyText: "Sample body text here.",
|
||||
FooterText: "Sample footer text here.",
|
||||
};
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<modal
|
||||
:ref="ModalName"
|
||||
:modalId="ModalName"
|
||||
:footerButtonText="ModalCloseButtonText"
|
||||
@footer-button-event="footerButtonClick">
|
||||
<img :src="ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
|
||||
<h5 class="mb-4" v-html="ModalHeadline"></h5>
|
||||
<p class="fw-bold mb-2 subheader-text" v-html="ModalSubheadertext"></p>
|
||||
<p class="mb-0" v-html="ModalBodyText"></p>
|
||||
<p
|
||||
class="my-4 caption modal-sub-body"
|
||||
v-if="ModalSubBodyText"
|
||||
v-html="ModalSubBodyText"></p>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
|
||||
export default {
|
||||
name: "content-group-modal",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
computed: {
|
||||
ModalName() {
|
||||
return this.cmsWidgetName;
|
||||
},
|
||||
ModalHeadline() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||
},
|
||||
ModalSubheadertext() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||
},
|
||||
ModalBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
},
|
||||
ModalSubBodyText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
},
|
||||
ModalImage() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Image");
|
||||
},
|
||||
ModalCloseButtonText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs[this.ModalName].openModal();
|
||||
},
|
||||
|
||||
footerButtonClick() {
|
||||
this.$refs[this.ModalName].closeModal();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
modal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal {
|
||||
&.modal-component {
|
||||
.modal-dialog {
|
||||
.modal-content {
|
||||
.modal-body {
|
||||
.modal-sub-body {
|
||||
color: $gray-600;
|
||||
}
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
src/helpers/object-cloning-helper.js
Normal file
27
src/helpers/object-cloning-helper.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// For nested objects, spread operator only creates new references to the top level fields,
|
||||
// the remaining nested fields actually reference the original object which can introduce problems.
|
||||
|
||||
// The purpose of this method is to deep clone the data in an object recursively, this is useful
|
||||
// for cloning modelValues to internal models when regular two-way binding is not an option.
|
||||
// See: mobile-location-modal-questions.vue
|
||||
|
||||
// Creates a deep clone of an object. Clones primitives, arrays and objects, excluding class instances.
|
||||
// https://www.30secondsofcode.org/js/s/deep-clone
|
||||
export function deepClone(object) {
|
||||
if (object === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let clone = Object.assign({}, object);
|
||||
Object.keys(clone).forEach(
|
||||
(key) =>
|
||||
(clone[key] = typeof object[key] === "object" ? deepClone(object[key]) : object[key])
|
||||
);
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
clone.length = object.length;
|
||||
return Array.from(clone);
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
48
src/helpers/object-cloning-helper.spec.js
Normal file
48
src/helpers/object-cloning-helper.spec.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { deepClone } from "./object-cloning-helper";
|
||||
|
||||
describe("object-cloning-helper.js", () => {
|
||||
it("Should return null if no object is passed in", async () => {
|
||||
// Arrange
|
||||
const expected = null;
|
||||
|
||||
// Act
|
||||
const result = deepClone(null);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("Should return a deep copy of the object", async () => {
|
||||
// Arrange
|
||||
|
||||
const object = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const expected = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = deepClone(object);
|
||||
|
||||
// Assert
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
30
src/helpers/service-location-helper.js
Normal file
30
src/helpers/service-location-helper.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { storeActions } from "@/constants/store-actions";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
export async function getPricedMobileFeePart(serviceZipCode) {
|
||||
if (!serviceZipCode) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const zipCodeData = await baseMixin.methods.getZipCodeData(serviceZipCode);
|
||||
|
||||
// Get the Mobile Fee Part
|
||||
const mobileFeePart = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_MOBILE_FEE_PART,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
// Get the Mobile Fee Part Price
|
||||
const pricingResults = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
|
||||
{
|
||||
availableLineItems: [mobileFeePart.data],
|
||||
serviceZipCode: serviceZipCode,
|
||||
ctu: zipCodeData.zipCodeCtu,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return Promise.resolve(pricingResults[0]);
|
||||
}
|
||||
78
src/helpers/service-location-helper.spec.js
Normal file
78
src/helpers/service-location-helper.spec.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { getPricedMobileFeePart } from "./service-location-helper";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
|
||||
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
|
||||
const mockStoreActionPriceOrderItemsAndSaveServerData =
|
||||
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
|
||||
|
||||
jest.mock("@/mixins/base-mixin.js", () => ({
|
||||
methods: {
|
||||
getZipCodeData: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: true,
|
||||
isValid: true,
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
});
|
||||
}),
|
||||
|
||||
dispatchStoreAction: jest.fn().mockImplementation((actionName) => {
|
||||
if (actionName === mockStoreActionGetMobileFeePart) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (actionName === mockStoreActionPriceOrderItemsAndSaveServerData) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("service-location-helper.js", () => {
|
||||
it("Should return null if no service zip code is passed in", async () => {
|
||||
// Arrange
|
||||
const serviceZipCode = null;
|
||||
const expected = null;
|
||||
|
||||
// Act
|
||||
const result = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("Should return the priced mobile fee part", async () => {
|
||||
// Arrange
|
||||
const serviceZipCode = "43235";
|
||||
const expected = {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
|
@ -59,6 +59,80 @@ describe("address-questions.vue", () => {
|
|||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should render apartmentNumberOrBusinessName textbox-question when captureApartmentNumberOrBusinessName = true", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
captureApartmentNumberOrBusinessName: true,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.setData({
|
||||
showAddressFields: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
const streetAddress = wrapper.findComponent({ ref: "autocomplete" });
|
||||
const apartmentNumberOrBusinessName = wrapper.findComponent({
|
||||
ref: "apartmentNumberOrBusinessName",
|
||||
});
|
||||
const city = wrapper.findComponent({ ref: "city" });
|
||||
const state = wrapper.findComponent({ ref: "state" });
|
||||
const zipCode = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
// Assert
|
||||
expect(streetAddress.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.isVisible()).toBe(true);
|
||||
expect(city.exists()).toBe(true);
|
||||
expect(state.exists()).toBe(true);
|
||||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should *not* render apartmentNumberOrBusinessName textbox-question when captureApartmentNumberOrBusinessName = false", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({
|
||||
props: {
|
||||
modelValue: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
captureApartmentNumberOrBusinessName: false,
|
||||
},
|
||||
});
|
||||
|
||||
await wrapper.setData({
|
||||
showAddressFields: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
const streetAddress = wrapper.findComponent({ ref: "autocomplete" });
|
||||
const apartmentNumberOrBusinessName = wrapper.findComponent({
|
||||
ref: "apartmentNumberOrBusinessName",
|
||||
});
|
||||
const city = wrapper.findComponent({ ref: "city" });
|
||||
const state = wrapper.findComponent({ ref: "state" });
|
||||
const zipCode = wrapper.findComponent({ ref: "zipCode" });
|
||||
|
||||
// Assert
|
||||
expect(streetAddress.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.exists()).toBe(true);
|
||||
expect(apartmentNumberOrBusinessName.isVisible()).toBe(false);
|
||||
expect(city.exists()).toBe(true);
|
||||
expect(state.exists()).toBe(true);
|
||||
expect(zipCode.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test("Should set this.showAddressFields to true when the model is prepopulated", async () => {
|
||||
// Arrange
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -1,21 +1,33 @@
|
|||
<template>
|
||||
<div role="application">
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="address-questions" role="application">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
id="streetAddressField"
|
||||
cmsWidgetName="StreetAddressQuestionWidget"
|
||||
v-model="addressModel.streetAddress"
|
||||
ref="autocomplete"
|
||||
inputId="autocomplete"
|
||||
customInputId="autocomplete"
|
||||
placeholderText="Search"
|
||||
aria-haspopup=""
|
||||
hasIcon
|
||||
disableAutoFill
|
||||
validationRules="street-address-required"
|
||||
@keydown.enter.prevent />
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
class="row mb-4"
|
||||
v-show="showAddressFields && showApartmentNumberOrBusinessNameField"
|
||||
aria-live="polite">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
cmsWidgetName="ApartmentNumberOrBusinessNameQuestionWidget"
|
||||
v-model="addressModel.apartmentNumberOrBusinessName"
|
||||
ref="apartmentNumberOrBusinessName" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" v-show="showAddressFields" aria-live="polite">
|
||||
<div class="col">
|
||||
|
|
@ -23,8 +35,6 @@
|
|||
cmsWidgetName="CityQuestionWidget"
|
||||
v-model="addressModel.city"
|
||||
ref="city"
|
||||
inputId="cbf28188fdf2436688fd735915f7ee56"
|
||||
disableAutoFill
|
||||
validationRules="city-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -36,9 +46,7 @@
|
|||
cmsWidgetName="StateQuestionWidget"
|
||||
v-model="addressModel.state"
|
||||
ref="state"
|
||||
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
|
||||
:options="stateOptions"
|
||||
disableAutoFill
|
||||
validationRules="state-required" />
|
||||
</div>
|
||||
<div class="col">
|
||||
|
|
@ -46,9 +54,7 @@
|
|||
cmsWidgetName="ZipQuestionWidget"
|
||||
v-model="addressModel.zipCode"
|
||||
ref="zipCode"
|
||||
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
|
||||
mask="#####"
|
||||
disableAutoFill
|
||||
validationRules="zip-code-required|zip-code-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -78,7 +84,6 @@ import { applicationConfig } from "@/constants/application-config.js";
|
|||
import { defineRule } from "vee-validate";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { fill } from "lodash";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED));
|
||||
|
|
@ -95,12 +100,17 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
}),
|
||||
},
|
||||
validationRules: String,
|
||||
captureApartmentNumberOrBusinessName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -180,9 +190,15 @@ export default {
|
|||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
showApartmentNumberOrBusinessNameField: {
|
||||
get: function () {
|
||||
return this.captureApartmentNumberOrBusinessName;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setupAddressLookup() {
|
||||
this.showAddressFields = false;
|
||||
if (
|
||||
this.addressModel.streetAddress &&
|
||||
this.addressModel.city &&
|
||||
|
|
@ -199,7 +215,7 @@ export default {
|
|||
const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY;
|
||||
|
||||
this.$loadScript(
|
||||
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`
|
||||
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`
|
||||
)
|
||||
.then(() => {
|
||||
// Script is loaded, initialize the autocomplete textbox
|
||||
|
|
@ -233,13 +249,17 @@ export default {
|
|||
});
|
||||
|
||||
addressField1.addEventListener("keydown", (e) => {
|
||||
if (e.code === "Enter" || e.code === "NumpadEnter") {
|
||||
const autocomplete = document.getElementById("autocomplete");
|
||||
const event = new Event("place_changed");
|
||||
|
||||
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
|
||||
const selectedItem = document.querySelector(
|
||||
".pac-container .pac-item-selected"
|
||||
);
|
||||
if (selectedItem !== null) {
|
||||
// Fill-in the address using selected item in the list.
|
||||
fillInAddress(selectedItem);
|
||||
autocomplete.dispatchEvent(event);
|
||||
//fillInAddress(selectedItem.textContent);
|
||||
} else {
|
||||
// Fill-in the address using first item in the list.
|
||||
fillInAddressUsingFirstItem();
|
||||
|
|
@ -297,7 +317,6 @@ export default {
|
|||
if (place && place.address_components) {
|
||||
self.matchFound = true;
|
||||
|
||||
self.addressModel.streetAddress = "";
|
||||
self.$nextTick(function () {
|
||||
self.showAddressFields = true;
|
||||
|
||||
|
|
@ -392,6 +411,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.address-questions {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#streetAddressField {
|
||||
position: relative;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
v-model="customerModel.firstName"
|
||||
ref="firstName"
|
||||
inputId="08497a2efd9a4a73a70360ab47b4838d"
|
||||
disableAutoFill
|
||||
validationRules="first-name-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -18,7 +17,6 @@
|
|||
v-model="customerModel.lastName"
|
||||
ref="lastName"
|
||||
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
|
||||
disableAutoFill
|
||||
validationRules="last-name-required" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -29,7 +27,6 @@
|
|||
v-model="customerModel.emailAddress"
|
||||
ref="emailAddress"
|
||||
inputId="00450a91b8964a768ce3992e6feb890f"
|
||||
disableAutoFill
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
inputId="serviceZipCode"
|
||||
mask="#####"
|
||||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="zip-required|zip-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -45,7 +44,6 @@
|
|||
v-model="emailAddress"
|
||||
inputId="emailAddress"
|
||||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
:availableLineItems="availableLineItems"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
@vapsItemsSelected="vapsItemsSelectedAction"
|
||||
v-on="{ 'buttonEvent.openModal': openModalAction }"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
|
||||
|
|
@ -28,11 +29,11 @@
|
|||
cmsWidgetName="quoteDisclaimer"
|
||||
justifyText="left"
|
||||
typeStyle="caption"
|
||||
class="mt-7" />
|
||||
<modal cmsWidgetName="RainDefenseModal" />
|
||||
<modal cmsWidgetName="FrontWiperModal" />
|
||||
<modal cmsWidgetName="RearWiperModal" />
|
||||
<modal cmsWidgetName="RecalModal" />
|
||||
class="mt-4" />
|
||||
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
|
||||
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
|
||||
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
|
|
@ -53,7 +54,7 @@ import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-heade
|
|||
import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question";
|
||||
import servicePackageQuestion from "./service-package-question/service-package-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import vehicleQuestionsMixin from "../../mixins/vehicle-questions-mixin";
|
||||
|
|
@ -137,6 +138,9 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
},
|
||||
arePagePrerequisitesValid() {
|
||||
return (
|
||||
store.getters.order.serviceLocation.zipCode &&
|
||||
|
|
@ -221,7 +225,7 @@ export default {
|
|||
textBlock,
|
||||
cashOrInsuranceQuestion,
|
||||
servicePackageQuestion,
|
||||
modal,
|
||||
contentGroupModal,
|
||||
loadingModal,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
ref="buttonQuestion"
|
||||
:answers="servicePackageAnswers"
|
||||
:groupName="groupName"
|
||||
buttonTypeString="servicePackageRadio"
|
||||
|
|
|
|||
|
|
@ -32,9 +32,21 @@
|
|||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
@click-event="
|
||||
$emit('buttonEvent', {
|
||||
eventName: 'openModal',
|
||||
args: getRouterLinkRouteFromCopy(copy),
|
||||
})
|
||||
"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
aria-label="Modal window" />
|
||||
<!-- <textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
aria-label="Modal window" /> -->
|
||||
</span>
|
||||
</template>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,352 @@
|
|||
import mobileLocationModalQuestions from "./mobile-location-modal-questions";
|
||||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
global.crypto = crypto;
|
||||
|
||||
const linkWidgetName = "linkWidgetName";
|
||||
const modalWidgetName = "modalWidgetName";
|
||||
const mobileFeeDisclaimerWidgetName = "MobileFeeDisclaimerWidget";
|
||||
|
||||
const mockLinkCmsContent = {
|
||||
BodyText: "Sample link body text here.",
|
||||
};
|
||||
|
||||
const mockModalCmsContent = {
|
||||
FooterText: "Sample modal footer text here.",
|
||||
};
|
||||
|
||||
const mockMobileFeeDisclaimerContent = {
|
||||
Text: "Sample mobile fee disclaimer text {custom:mobileFee}",
|
||||
};
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (widgetName === linkWidgetName) {
|
||||
return mockLinkCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === modalWidgetName) {
|
||||
return mockModalCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === mobileFeeDisclaimerWidgetName) {
|
||||
return mockMobileFeeDisclaimerContent[cmsFieldName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
getZipCodeData: jest.fn((zip) => {
|
||||
if (zip === "43235" || zip === "55555") {
|
||||
return Promise.resolve({
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: true,
|
||||
isValid: true,
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: false,
|
||||
isValid: false,
|
||||
state: null,
|
||||
zipCodeCtu: null,
|
||||
});
|
||||
}),
|
||||
|
||||
dispatchStoreAction: jest.fn((actionName) => {
|
||||
if (actionName === storeActions.GET_MOBILE_FEE_PART) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (actionName === storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}),
|
||||
|
||||
getTotalLineItemPrice: jest.fn((lineItem) => {
|
||||
return 49.99;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("mobile-location-modal-questions.vue", () => {
|
||||
it("Should copy the modelValue to the internalModel when the component is mounted", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.internalModel.addressQuestions.streetAddress).toEqual("555 Some St");
|
||||
expect(wrapper.vm.internalModel.addressQuestions.apartmentNumberOrBusinessName).toEqual(
|
||||
"Apt 1"
|
||||
);
|
||||
expect(wrapper.vm.internalModel.addressQuestions.city).toEqual("Funkytown");
|
||||
expect(wrapper.vm.internalModel.addressQuestions.state).toEqual("OH");
|
||||
expect(wrapper.vm.internalModel.addressQuestions.zipCode).toEqual("55555");
|
||||
expect(wrapper.vm.internalModel.isVehicleProtected).toEqual(true);
|
||||
expect(wrapper.vm.internalModel.serviceZipCode).toEqual("55555");
|
||||
});
|
||||
|
||||
it("Should open the modal when the 'Enter your service address' link is clicked", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.openModal = jest.fn();
|
||||
const mobileLocationLink = wrapper.findComponent({ ref: "mobileLocationLink" });
|
||||
|
||||
// // Act
|
||||
await mobileLocationLink.trigger("click-event");
|
||||
|
||||
// // Assert
|
||||
expect(wrapper.vm.$refs.MobileLocationModalWidget.openModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Should emit update:modelValue on setMobileLocation for a valid address and vehicle protection answer", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "",
|
||||
};
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(mobileLocationModalQuestions, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel = newMobileLocationQuestions;
|
||||
await wrapper.vm.setMobileLocation();
|
||||
|
||||
let expectedEmit = [[newMobileLocationQuestions]];
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual(expectedEmit);
|
||||
});
|
||||
|
||||
it("Should not emit update:modelValue on setMobileLocation for an invalid address zip code", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "",
|
||||
};
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "61000",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "61000",
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(mobileLocationModalQuestions, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel = newMobileLocationQuestions;
|
||||
await wrapper.vm.setMobileLocation();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should display invalid zip alert for invalid address zip inputs", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "",
|
||||
};
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "11111",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "11111",
|
||||
};
|
||||
|
||||
const wrapper = shallowMount(mobileLocationModalQuestions, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
wrapper.vm.internalModel = newMobileLocationQuestions;
|
||||
|
||||
// Act
|
||||
await wrapper.vm.setMobileLocation();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.displayInvalidZipAlert).toBe(true);
|
||||
});
|
||||
|
||||
it("Should clear the internal model when resetModel is called", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.resetModel();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.internalModel.addressQuestions.streetAddress).toEqual("");
|
||||
expect(wrapper.vm.internalModel.addressQuestions.apartmentNumberOrBusinessName).toEqual("");
|
||||
expect(wrapper.vm.internalModel.addressQuestions.city).toEqual("");
|
||||
expect(wrapper.vm.internalModel.isVehicleProtected).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
|
||||
const resultingMountOptions = getMountOptions({
|
||||
...mountOptions,
|
||||
mixins,
|
||||
});
|
||||
|
||||
if (props) resultingMountOptions.propsData = props;
|
||||
|
||||
const wrapper = isShallowMount
|
||||
? shallowMount(mobileLocationModalQuestions, resultingMountOptions)
|
||||
: mount(mobileLocationModalQuestions, resultingMountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<div class="update-mobile-location-text-link">
|
||||
<textLink
|
||||
ref="mobileLocationLink"
|
||||
id="mobileLocationLinkPromptId"
|
||||
linkType="text"
|
||||
:text="mobileLocationLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal"
|
||||
aria-label="Modal window" />
|
||||
</div>
|
||||
</div>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
:footerButtonText="modalFooterText"
|
||||
@footer-button-event="setMobileLocation">
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="internalModel.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="internalModel.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget" />
|
||||
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertInvalidZipWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
|
||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||
|
||||
// Helpers
|
||||
import { deepClone } from "@/helpers/object-cloning-helper";
|
||||
|
||||
export default {
|
||||
name: "mobile-location-modal-questions",
|
||||
emits: ["update:modelValue", "updated-zip-code"], // The component emits an event
|
||||
data() {
|
||||
return {
|
||||
internalModel: deepClone(this.modelValue),
|
||||
displayInvalidZipAlert: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
serviceZipCode: "",
|
||||
mobileFeePart: null,
|
||||
}),
|
||||
},
|
||||
isZipServiceableMobile: Boolean,
|
||||
isZipServiceableInShop: Boolean,
|
||||
linkWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
alertNonServiceableZipWidgetName: String,
|
||||
alertInvalidZipWidgetName: String,
|
||||
},
|
||||
computed: {
|
||||
mobileLocationLinkText() {
|
||||
if (
|
||||
this.addressModel.streetAddress !== null &&
|
||||
this.addressModel.streetAddress !== "" &&
|
||||
this.addressModel.city !== null &&
|
||||
this.addressModel.city !== "" &&
|
||||
this.addressModel.state !== null &&
|
||||
this.addressModel.state !== "" &&
|
||||
this.addressModel.zipCode !== null &&
|
||||
this.addressModel.zipCode !== ""
|
||||
) {
|
||||
return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`;
|
||||
}
|
||||
return this.getCmsContent(this.linkWidgetName, "BodyText");
|
||||
},
|
||||
modalName() {
|
||||
return "MobileLocationModalWidget";
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
mobileFeeText() {
|
||||
const cmsContentText = this.getCmsContent("MobileFeeDisclaimerWidget", "Text");
|
||||
return cmsContentText.replaceAll("{custom:mobileFee}", this.mobileFee);
|
||||
},
|
||||
mobileFee() {
|
||||
if (!this.modelValue.mobileFeePart) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
this.modelValue.mobileFeePart.laborAmount +
|
||||
this.modelValue.mobileFeePart.sellingPrice +
|
||||
this.modelValue.mobileFeePart.kitPrice
|
||||
);
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
|
||||
addressModel: {
|
||||
get: function () {
|
||||
return this.modelValue.addressQuestions;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
},
|
||||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
},
|
||||
resetComponent() {
|
||||
this.resetModel();
|
||||
|
||||
// Reset the validation form
|
||||
this.$refs[this.modalName].form.resetForm();
|
||||
|
||||
// Reinitialize the Address Auto Complete
|
||||
this.$refs.addressQuestions.setupAddressLookup();
|
||||
},
|
||||
resetModel() {
|
||||
// Address
|
||||
this.internalModel.addressQuestions.streetAddress = "";
|
||||
this.internalModel.addressQuestions.apartmentNumberOrBusinessName = "";
|
||||
this.internalModel.addressQuestions.city = "";
|
||||
|
||||
// Is Vehicle Protected
|
||||
this.internalModel.isVehicleProtected = null;
|
||||
},
|
||||
async setMobileLocation() {
|
||||
// Validate the Zip Code
|
||||
const zipCodeData = await this.getZipCodeData(
|
||||
this.internalModel.addressQuestions.zipCode
|
||||
);
|
||||
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
} else {
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.internalModel = deepClone(newValue);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
modal,
|
||||
addressQuestions,
|
||||
vehicleProtectedQuestion,
|
||||
textBlock,
|
||||
alert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.update-zip-text-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.address-questions {
|
||||
margin-top: 0em;
|
||||
}
|
||||
|
||||
#mobileLocationLinkPromptId {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import vehicleProtectedQuestion from "./vehicle-protected-question";
|
||||
|
||||
jest.mock("@/digital-components/textbox-question/textbox-question", () => ({
|
||||
getCmsContent: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (cmsFieldName === "Answers") {
|
||||
return [
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "YesAnswer",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Yes",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "NoAnswer",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "No",
|
||||
},
|
||||
];
|
||||
}
|
||||
return "QuestionText";
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("service-zip-question.vue", () => {
|
||||
it("Should get the modelValue", async () => {
|
||||
// Arrange
|
||||
const answer = "YesAnswer";
|
||||
const wrapper = shallowMount(vehicleProtectedQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: answer,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
const modelValueAnswer = wrapper.vm.selectedValue;
|
||||
|
||||
// Assert
|
||||
expect(modelValueAnswer).toEqual(answer);
|
||||
});
|
||||
|
||||
it("Should emit to set value", async () => {
|
||||
// Arrange
|
||||
const yesAnswer = "YesAnswer";
|
||||
const noAnswer = "NoAnswer";
|
||||
const wrapper = shallowMount(vehicleProtectedQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: yesAnswer,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.selectedValue = noAnswer;
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual([[noAnswer]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
class="radioQuestion"
|
||||
:questionText="questionText"
|
||||
:answers="answers"
|
||||
groupName="isVehicleProtected"
|
||||
textPosition="text-center"
|
||||
v-model="selectedValue"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
|
||||
// Supporting files
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "vehicle-protected-question",
|
||||
props: {
|
||||
modelValue: {
|
||||
isVehicleProtected: Boolean,
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
selectedValue: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
answers() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.625rem;
|
||||
|
||||
& > span {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,48 +4,114 @@ import serviceLocation from "@/layouts/service-location/service-location.vue";
|
|||
// Supporting files
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
import { nextTick } from "vue";
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper.js";
|
||||
|
||||
import store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
||||
// Define Mocks
|
||||
jest.mock("@/helpers/layout-helper.js", () => ({
|
||||
settleAllPromises: jest.fn(),
|
||||
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||
fetchCmsContentForPage: jest.fn(() => {
|
||||
return Promise.resolve("content");
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@/helpers/cms-content-helper", () => ({
|
||||
fetchCmsContentForPage: jest.fn(),
|
||||
const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
|
||||
let mobileFeePart = {};
|
||||
|
||||
if (mockServiceZipCode === "43235") {
|
||||
mobileFeePart = {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.resolve(mobileFeePart);
|
||||
};
|
||||
|
||||
jest.mock("@/helpers/service-location-helper", () => ({
|
||||
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedMobileFeePart(mockServiceZipCode);
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@/store", () => ({
|
||||
commit: jest.fn(),
|
||||
dispatch: jest.fn(),
|
||||
getters: {
|
||||
lineItems: {
|
||||
supportingItems: [
|
||||
{
|
||||
description: null,
|
||||
kitPrice: 0,
|
||||
laborAmount: 0,
|
||||
partNumber: "SUPPLIES-REPAIR",
|
||||
partType: "REPAIR FEE",
|
||||
sellingPrice: 7.99,
|
||||
},
|
||||
],
|
||||
},
|
||||
order: {
|
||||
serviceLocation: {
|
||||
zipCode: "43235",
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (widgetName === linkWidgetName) {
|
||||
return mockLinkCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === modalWidgetName) {
|
||||
return mockModalCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === mobileFeeDisclaimerWidgetName) {
|
||||
return mockMobileFeeDisclaimerContent[cmsFieldName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
getZipCodeData: jest.fn((zip) => {
|
||||
if (zip === "43235" || zip === "55555") {
|
||||
return Promise.resolve({
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: true,
|
||||
isValid: true,
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: false,
|
||||
isValid: false,
|
||||
state: null,
|
||||
zipCodeCtu: null,
|
||||
});
|
||||
}),
|
||||
|
||||
dispatchStoreAction: jest.fn((actionName) => {
|
||||
if (actionName === storeActions.GET_MOBILE_FEE_PART) {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (actionName === storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}),
|
||||
|
||||
getTotalLineItemPrice: jest.fn((lineItem) => {
|
||||
return 49.99;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store.getters = {
|
||||
lineItems: {
|
||||
|
|
@ -63,15 +129,52 @@ beforeEach(() => {
|
|||
order: {
|
||||
serviceLocation: {
|
||||
zipCode: "43235",
|
||||
state: "OH",
|
||||
isServiceable: true,
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
isInsurance: false,
|
||||
},
|
||||
vehicle: {
|
||||
registration: {
|
||||
address: "5555 Sulgrave Dr",
|
||||
city: "New Albany",
|
||||
state: "OH",
|
||||
zipCode: "43054",
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("service-location.vue", () => {
|
||||
describe("beforeRouteEnter", () => {
|
||||
test("on load sets the mobile fee part when an service zip code has already been provided", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
|
||||
const mobileFeePart = {
|
||||
partNumber: "MOBILE FEE",
|
||||
description: "MOBILE FEE",
|
||||
partType: "FEE",
|
||||
laborAmount: 49.99,
|
||||
sellingPrice: 0,
|
||||
kitPrice: 0,
|
||||
};
|
||||
|
||||
// Act
|
||||
await serviceLocation.beforeRouteEnter.call(
|
||||
wrapper.vm,
|
||||
{ query: { fmgPage: "serviceLocation" } },
|
||||
undefined,
|
||||
(c) => c(wrapper.vm)
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mobileLocationQuestions.mobileFeePart).toStrictEqual(mobileFeePart);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arePagePrerequisitesValid", () => {
|
||||
test("No prerequisites set: Should return false.", () => {
|
||||
const { wrapper } = setupMocks({});
|
||||
|
|
@ -113,6 +216,175 @@ describe("service-location.vue", () => {
|
|||
expect(arePagePrerequisitesValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updating service zip", () => {
|
||||
test("updates the page model after providing the service zip code", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
|
||||
|
||||
const newServiceZipCodeQuestion = {
|
||||
zipCode: "61606",
|
||||
state: "IL",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
|
||||
// Act
|
||||
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeQuestion);
|
||||
});
|
||||
|
||||
test("resets mobile location when service zip code is updated", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
|
||||
wrapper.vm.$refs.serviceZipCodeQuestion.resetMobileFeePart = jest.fn();
|
||||
|
||||
const newServiceZipCodeQuestion = {
|
||||
zipCode: "61606",
|
||||
state: "IL",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Sulgrave Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "New Albany",
|
||||
state: "OH",
|
||||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
|
||||
const serviceZipCodeComponent = wrapper.findComponent({
|
||||
ref: "serviceZipCodeQuestion",
|
||||
});
|
||||
|
||||
// Act
|
||||
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updating mobile location", () => {
|
||||
test("updates the page model after providing the mobile location", () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Sulgrave Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "New Albany",
|
||||
state: "OH",
|
||||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
|
||||
const mobileLocationComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationModalQuestions",
|
||||
});
|
||||
|
||||
// Act
|
||||
mobileLocationComponent.vm.$emit("update:modelValue", newMobileLocationQuestions);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
|
||||
});
|
||||
|
||||
test("resets service zip code when mobile location is updated", async () => {
|
||||
// Arrange
|
||||
const { wrapper } = setupMocks({});
|
||||
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
|
||||
wrapper.vm.$refs.serviceZipCodeQuestion.resetMobileFeePart = jest.fn();
|
||||
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "",
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
const newMobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "5555 Rustic Dr",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Westerville",
|
||||
state: "OH",
|
||||
zipCode: "43081",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
|
||||
const mobileLocationComponent = wrapper.findComponent({
|
||||
ref: "mobileLocationModalQuestions",
|
||||
});
|
||||
|
||||
wrapper.vm.serviceZipCodeQuestion = {
|
||||
zipCode: "61606",
|
||||
state: "IL",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const newServiceZipCodeInfo = {
|
||||
state: "OH",
|
||||
zipCode: "43081",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
// Act
|
||||
mobileLocationComponent.vm.$emit("update:modelValue", newMobileLocationQuestions);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ mountOptionsMockData = {} }) {
|
||||
|
|
@ -120,9 +392,6 @@ function setupMocks({ mountOptionsMockData = {} }) {
|
|||
|
||||
const apiPromise = Promise.resolve(apiResponses);
|
||||
|
||||
settleAllPromises.mockImplementation(() => apiPromise);
|
||||
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
|
||||
|
||||
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||
const wrapper = shallowMount(serviceLocation, mountOptions);
|
||||
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,27 @@
|
|||
<loadingModal ref="loadingModal" />
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<serviceZipModalQuestion
|
||||
v-model="serviceZipCodeQuestion"
|
||||
@updated-service-zip-code="resetMobileLocation"
|
||||
ref="serviceZipCodeQuestion"
|
||||
linkWidgetName="ServiceZipLinkWidget"
|
||||
modalWidgetName="ServiceZipModalWidget" />
|
||||
<div class="text-center">
|
||||
<label
|
||||
for="mobileLocationLinkPromptId"
|
||||
:aria-label="mobileLocationLinkPromptText"
|
||||
class="form-label fw-bold w-100 ps-4 pe-4 pt-4"
|
||||
v-html="mobileLocationLinkPromptText"></label>
|
||||
</div>
|
||||
<mobileLocationModalQuestions
|
||||
v-model="mobileLocationQuestions"
|
||||
ref="mobileLocationModalQuestions"
|
||||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget" />
|
||||
<div class="text-center">
|
||||
<textBlock :customText="mobileFeeDisclaimerText" typeStyle="caption" />
|
||||
</div>
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
|
|
@ -16,33 +37,52 @@
|
|||
|
||||
<script>
|
||||
// Components
|
||||
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
|
||||
import mobileLocationModalQuestions from "@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions";
|
||||
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 loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form } from "vee-validate";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import { getPricedMobileFeePart } from "@/helpers/service-location-helper";
|
||||
import store from "@/store";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
|
||||
export default {
|
||||
name: "service-location",
|
||||
data() {
|
||||
return {};
|
||||
return {
|
||||
streetAddress: this.getServiceAddressFromStore(),
|
||||
apartmentNumberOrBusinessName: "",
|
||||
city: this.getServiceCityFromStore(),
|
||||
state: this.getServiceStateFromStore(),
|
||||
zipCode: this.getServiceZipCodeFromStore(),
|
||||
isServiceable: false,
|
||||
isZipServiceableMobile: null,
|
||||
isZipServiceableInShop: null,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
|
||||
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
||||
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "mobileFeePart",
|
||||
promise: mobileFeePartPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
|
@ -50,8 +90,54 @@ export default {
|
|||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.setData(resultMap.mobileFeePart);
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
serviceZipCodeQuestion: {
|
||||
get: function () {
|
||||
return {
|
||||
state: this.state,
|
||||
zipCode: this.zipCode,
|
||||
isServiceable: this.isServiceable,
|
||||
};
|
||||
},
|
||||
set: function (newValue) {
|
||||
if (newValue.zipCode !== this.zipCode) {
|
||||
this.resetMobileLocation(this.zipCode);
|
||||
}
|
||||
|
||||
this.state = newValue.state;
|
||||
this.zipCode = newValue.zipCode;
|
||||
this.isServiceable = newValue.isServiceable;
|
||||
},
|
||||
},
|
||||
mobileLocationQuestions: {
|
||||
get: function () {
|
||||
return {
|
||||
addressQuestions: {
|
||||
streetAddress: this.streetAddress,
|
||||
apartmentNumberOrBusinessName: this.apartmentNumberOrBusinessName,
|
||||
city: this.city,
|
||||
state: this.state,
|
||||
zipCode: this.zipCode,
|
||||
},
|
||||
isVehicleProtected: this.isVehicleProtected,
|
||||
mobileFeePart: this.mobileFeePart,
|
||||
};
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.streetAddress = newValue.addressQuestions.streetAddress;
|
||||
this.apartmentNumberOrBusinessName =
|
||||
newValue.addressQuestions.apartmentNumberOrBusinessName;
|
||||
this.city = newValue.addressQuestions.city;
|
||||
this.state = newValue.addressQuestions.state;
|
||||
this.zipCode = newValue.addressQuestions.zipCode;
|
||||
this.isVehicleProtected = newValue.isVehicleProtected;
|
||||
this.mobileFeePart = newValue.mobileFeePart;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
return (
|
||||
|
|
@ -60,7 +146,41 @@ export default {
|
|||
store.getters.payment.isInsurance !== null
|
||||
);
|
||||
},
|
||||
setData(mobileFeePart) {
|
||||
if (mobileFeePart) {
|
||||
this.mobileFeePart = mobileFeePart;
|
||||
}
|
||||
},
|
||||
getServiceAddressFromStore() {
|
||||
return store.getters.order.serviceLocation.address;
|
||||
},
|
||||
getServiceCityFromStore() {
|
||||
return store.getters.order.serviceLocation.city;
|
||||
},
|
||||
getServiceStateFromStore() {
|
||||
return store.getters.order.serviceLocation.state;
|
||||
},
|
||||
getServiceZipCodeFromStore() {
|
||||
return store.getters.order.serviceLocation.zipCode;
|
||||
},
|
||||
getIsServiceableFromStore() {
|
||||
return store.getters.order.serviceLocation.isServiceable;
|
||||
},
|
||||
resetMobileFeePart(serviceZipCode) {
|
||||
getPricedMobileFeePart(serviceZipCode).then((pricedMobileFeePart) => {
|
||||
this.mobileFeePart = pricedMobileFeePart;
|
||||
});
|
||||
},
|
||||
resetMobileLocation(updatedServiceZipCode) {
|
||||
this.streetAddress = "";
|
||||
this.apartmentNumberOrBusinessName = "";
|
||||
this.city = "";
|
||||
this.isVehicleProtected = null;
|
||||
|
||||
this.$refs.mobileLocationModalQuestions.resetComponent();
|
||||
|
||||
this.resetMobileFeePart(updatedServiceZipCode);
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
|
|
@ -69,11 +189,14 @@ export default {
|
|||
},
|
||||
},
|
||||
components: {
|
||||
serviceZipModalQuestion,
|
||||
mobileLocationModalQuestions,
|
||||
funnelHeader,
|
||||
funnelFooter,
|
||||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,262 @@
|
|||
import { mount, shallowMount } from "@vue/test-utils";
|
||||
import serviceZipModalQuestion from "./service-zip-modal-question";
|
||||
import crypto from "crypto";
|
||||
global.crypto = crypto;
|
||||
|
||||
jest.mock("@/digital-components/textbox-question/textbox-question", () => ({
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
return widgetName[cmsFieldName];
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("@/digital-components/modal/modal", () => ({
|
||||
methods: {
|
||||
closeModal: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const linkWidgetName = "linkWidgetName";
|
||||
const modalWidgetName = "modalWidgetName";
|
||||
|
||||
const mockLinkCmsContent = {
|
||||
BodyText: "Sample link body text here.",
|
||||
};
|
||||
|
||||
const mockModalCmsContent = {
|
||||
FooterText: "Sample modal footer text here.",
|
||||
};
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (widgetName === linkWidgetName) {
|
||||
return mockLinkCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
if (widgetName === modalWidgetName) {
|
||||
return mockModalCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
getZipCodeData: jest.fn((zip) => {
|
||||
if (zip === "43235") {
|
||||
return {
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: true,
|
||||
isValid: true,
|
||||
state: "OH",
|
||||
zipCodeCtu: "01820",
|
||||
};
|
||||
}
|
||||
|
||||
if (zip === "61606") {
|
||||
return {
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: true,
|
||||
isValid: true,
|
||||
state: "IL",
|
||||
zipCodeCtu: "01526",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
containsMilitaryBase: false,
|
||||
isServiceable: false,
|
||||
isValid: false,
|
||||
state: null,
|
||||
zipCodeCtu: null,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("service-zip-modal-question.vue", () => {
|
||||
it("Initial state on load with no existing location info", async () => {
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "",
|
||||
zipCode: "",
|
||||
isServiceable: undefined,
|
||||
};
|
||||
|
||||
// Arrange
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.html()).toEqual(expect.stringContaining(mockLinkCmsContent["BodyText"]));
|
||||
});
|
||||
|
||||
it("Initial state on load with existing location info", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "OH",
|
||||
zipCode: "43235",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.html()).not.toEqual(expect.stringContaining(mockLinkCmsContent["BodyText"]));
|
||||
});
|
||||
|
||||
it("Should emit update:modelValue on setZipCode for a valid, serviceable zip", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const zip = "43235";
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel.zipCode = zip;
|
||||
await wrapper.vm.setZipCode();
|
||||
|
||||
let expectedEmit = [[{ isServiceable: true, state: "OH", zipCode: "43235" }]];
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual(expectedEmit);
|
||||
});
|
||||
|
||||
it("Should not emit update:modelValue on setZipCode for an invalid zip", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const zip = "";
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel.zipCode = zip;
|
||||
await wrapper.vm.setZipCode();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should display invalid zip alerts for invalid ip inputs", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const zip = "11111";
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel.zipCode = zip;
|
||||
await wrapper.vm.setZipCode();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.displayInvalidZipAlert).toBe(true);
|
||||
});
|
||||
|
||||
it("Should reset all alerts on onModalClosed", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const zip = "";
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel.zipCode = zip;
|
||||
await wrapper.vm.setZipCode();
|
||||
wrapper.vm.onModalClosed();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.displayInvalidZipAlert).toBe(false);
|
||||
});
|
||||
|
||||
it("Should prepopulate the zip on onModalOpened", async () => {
|
||||
// Arrange
|
||||
let serviceZipCodeQuestion = {
|
||||
state: "IL",
|
||||
zipCode: "61606",
|
||||
isServiceable: true,
|
||||
};
|
||||
|
||||
const zip = "123";
|
||||
|
||||
const wrapper = mount(serviceZipModalQuestion, {
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: serviceZipCodeQuestion,
|
||||
linkWidgetName: linkWidgetName,
|
||||
modalWidgetName: modalWidgetName,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel.zipCode = zip;
|
||||
wrapper.vm.onModalOpened();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.internalModel.zipCode).toEqual("61606");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<div class="text-center">
|
||||
<div class="update-zip-text-link">
|
||||
<textLink
|
||||
id="serviceZipLinkPromptId"
|
||||
linkType="text"
|
||||
:text="serviceZipLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal"
|
||||
aria-label="Modal window" />
|
||||
</div>
|
||||
</div>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
:footerButtonText="modalFooterText"
|
||||
@footer-button-event="setZipCode">
|
||||
<serviceZipQuestion
|
||||
ref="serviceZipQuestion"
|
||||
v-model="internalModel.zipCode"
|
||||
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
|
||||
:cmsWidgetName="textboxQuestionWidgetName" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
v-if="displayInvalidZipAlert"
|
||||
class="my-4"
|
||||
:cmsWidgetName="alertInvalidZipWidgetName"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import serviceZipQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
|
||||
export default {
|
||||
name: "service-zip-modal-question",
|
||||
data() {
|
||||
return {
|
||||
internalModel: this.copyModel(this.modelValue),
|
||||
serviceZipCodeTextInputId: "",
|
||||
displayInvalidZipAlert: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
state: "",
|
||||
zipCode: "",
|
||||
isServiceable: null,
|
||||
}),
|
||||
},
|
||||
linkWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
},
|
||||
setup() {
|
||||
const textboxQuestionWidgetName = "ServiceZipQuestionWidget";
|
||||
const alertInvalidZipWidgetName = "AlertInvalidZipWidget";
|
||||
|
||||
return {
|
||||
textboxQuestionWidgetName,
|
||||
alertInvalidZipWidgetName,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
serviceZipLinkText() {
|
||||
if (this.modelValue.zipCode && this.modelValue.zipCode.length > 0) {
|
||||
return this.modelValue.state + ", " + this.modelValue.zipCode;
|
||||
}
|
||||
return this.getCmsContent(this.linkWidgetName, "BodyText");
|
||||
},
|
||||
modalHeaderText() {
|
||||
return this.getCmsContent(this.textboxQuestionWidgetName, "QuestionText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
modalName() {
|
||||
return this.modalWidgetName;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetAlerts() {
|
||||
this.displayInvalidZipAlert = false;
|
||||
},
|
||||
|
||||
resetsOnZipInput() {
|
||||
this.resetAlerts();
|
||||
},
|
||||
|
||||
focusOnZipInput() {
|
||||
const input = document.getElementById(this.serviceZipCodeTextInputId);
|
||||
input?.focus();
|
||||
},
|
||||
|
||||
copyModel(modelToCopy) {
|
||||
return {
|
||||
state: modelToCopy.state,
|
||||
zipCode: modelToCopy.zipCode,
|
||||
isServiceable: modelToCopy.isServiceable,
|
||||
};
|
||||
},
|
||||
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
},
|
||||
|
||||
onInputIdAssigned(inputId) {
|
||||
this.serviceZipCodeTextInputId = inputId;
|
||||
},
|
||||
|
||||
onModalOpened() {
|
||||
this.internalModel.zipCode = this.modelValue.zipCode;
|
||||
this.focusOnZipInput();
|
||||
},
|
||||
|
||||
onModalClosed() {
|
||||
this.internalModel.zipCode = this.modelValue.zipCode;
|
||||
this.resetsOnZipInput();
|
||||
},
|
||||
|
||||
async setZipCode() {
|
||||
this.resetAlerts();
|
||||
|
||||
const zipCodeData = await this.getZipCodeData(this.internalModel.zipCode);
|
||||
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
this.focusOnZipInput();
|
||||
} else {
|
||||
this.internalModel.state = zipCodeData.state;
|
||||
this.internalModel.isServiceable = zipCodeData.isServiceable;
|
||||
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue) {
|
||||
this.internalModel = this.copyModel(newValue);
|
||||
},
|
||||
"internalModel.zipCode": {
|
||||
handler() {
|
||||
this.resetsOnZipInput();
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
serviceZipQuestion,
|
||||
modal,
|
||||
alert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.update-zip-text-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import serviceZipQuestion from "./service-zip-question";
|
||||
|
||||
describe("service-zip-question.vue", () => {
|
||||
it("Should get the modelValue", async () => {
|
||||
// Arrange
|
||||
const text = "test";
|
||||
const wrapper = shallowMount(serviceZipQuestion, {
|
||||
props: {
|
||||
modelValue: text,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
const modelValueText = wrapper.vm.value;
|
||||
wrapper.vm.value = "test also";
|
||||
|
||||
// Assert
|
||||
expect(modelValueText).toEqual("test");
|
||||
});
|
||||
|
||||
it("Should emit to set value", async () => {
|
||||
// Arrange
|
||||
const text = "test";
|
||||
const wrapper = shallowMount(serviceZipQuestion, {
|
||||
props: {
|
||||
modelValue: text,
|
||||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
|
||||
// Act
|
||||
const modelValueText = wrapper.vm.value;
|
||||
wrapper.vm.value = "test also";
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("update:modelValue")).toEqual([["test also"]]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<textboxQuestion
|
||||
ref="zipInputTextQuestion"
|
||||
:cmsWidgetName="cmsWidgetName"
|
||||
v-model="value"
|
||||
inputId="serviceZipCode"
|
||||
questionAlignment="center"
|
||||
cornerStyle="rounded"
|
||||
mask="#####"
|
||||
:displayQuestionText="false"
|
||||
isRequired
|
||||
validationRules="zip-required|zip-format" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
|
||||
//Supporting Files
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
// Define Validation Rules
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
||||
|
||||
export default {
|
||||
name: "service-zip-question",
|
||||
props: {
|
||||
modelValue: {
|
||||
serviceZipCode: String,
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
computed: {
|
||||
value: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
textboxQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
v-model="vin"
|
||||
inputId="vin"
|
||||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="vin-required|vin-format"
|
||||
:isDisabled="vinPopulatedOnPageLoad"
|
||||
maxLength="17"
|
||||
|
|
@ -35,7 +34,6 @@
|
|||
inputId="serviceZipCode"
|
||||
mask="#####"
|
||||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="zip-required|zip-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -46,7 +44,6 @@
|
|||
v-model="emailAddress"
|
||||
inputId="emailAddress"
|
||||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="email-address-required|email-address-format" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -914,6 +914,13 @@ export const actions = {
|
|||
});
|
||||
},
|
||||
|
||||
getMobileFeePart(context) {
|
||||
return globalMethods.callMockHttpClient({
|
||||
method: endpoints.GetMobileFeePart.method,
|
||||
endpoint: "https://run.mocky.io/v3/795de4cb-d014-48b4-9338-dab33261adce", //TODO: Remove Mocky Endpoint
|
||||
});
|
||||
},
|
||||
|
||||
getSupportingItems(context) {
|
||||
const glassPartsArray = context.getters.lineItems.glassParts ?? [];
|
||||
const carId = context.getters.vehicle.carId;
|
||||
|
|
@ -1423,24 +1430,34 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_VAPS, vaps);
|
||||
},
|
||||
// Price order actions
|
||||
async priceOrderItemsAndSaveServerData(context, availableLineItems) {
|
||||
async priceOrderItemsAndSaveServerData(context, { availableLineItems, serviceZipCode, ctu }) {
|
||||
const zipCodeToUse = serviceZipCode
|
||||
? serviceZipCode
|
||||
: context.getters.order.serviceLocation.zipCode;
|
||||
const ctuToUse = ctu ? ctu : context.getters.order.serviceLocation.zipCodeCtu;
|
||||
|
||||
const availableLineItemsFormattedForRequest =
|
||||
getLineItemQueryStringForPricing(availableLineItems);
|
||||
|
||||
const vehicle = context.getters.order.vehicle;
|
||||
|
||||
let queryString =
|
||||
`ParentAccountNumber=${applicationConfig.CASH_ACCOUNT_NUMBER}` +
|
||||
`&CTU=${context.getters.order.serviceLocation.zipCodeCtu}` +
|
||||
`&CTU=${ctuToUse}` +
|
||||
`&CarId=${vehicle.carId}` +
|
||||
`&Make=${vehicle.make}` +
|
||||
`&Model=${vehicle.model}` +
|
||||
`&Year=${vehicle.year}` +
|
||||
`&EON=${context.getters.order.eon}` +
|
||||
`&ZipCode=${context.getters.order.serviceLocation.zipCode}` +
|
||||
`&ZipCode=${zipCodeToUse}` +
|
||||
`${availableLineItemsFormattedForRequest}`;
|
||||
|
||||
const lineItemServerData = context.getters.order.lineItems.serverData;
|
||||
|
||||
if (lineItemServerData) {
|
||||
queryString += `&ServerData=${encodeURIComponent(lineItemServerData)}`;
|
||||
}
|
||||
|
||||
const response = await globalMethods.callHttpClient({
|
||||
method: endpoints.PriceOrderItems.method,
|
||||
endpoint: `${endpoints.PriceOrderItems.url}?${queryString}`,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,103 @@ describe("buttonMain.vue", () => {
|
|||
const loader = wrapper.find("loader-stub");
|
||||
expect(loader.attributes("class")).toContain("right");
|
||||
});
|
||||
|
||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
buttonMain,
|
||||
setupMocks({
|
||||
props: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
wrapper.setData({
|
||||
isLoaderDisplayed: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.removeLoader();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
const loader = wrapper.find("loader-stub");
|
||||
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
|
||||
});
|
||||
|
||||
it("Should set 'isLoaderDisplayed' to false when calling 'resetButtonStyle'", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
buttonMain,
|
||||
setupMocks({
|
||||
props: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
wrapper.setData({
|
||||
isLoaderDisplayed: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.resetButtonStyle();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
|
||||
});
|
||||
|
||||
it("Should emit 'click-event' event when clicking if the button is enabled", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
buttonMain,
|
||||
setupMocks({
|
||||
props: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
isDisabled: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const buttonElement = wrapper.find("button");
|
||||
|
||||
// Act
|
||||
buttonElement.trigger("click");
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("click-event")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should not emit 'click-event' event when clicking if the button is disabled", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
buttonMain,
|
||||
setupMocks({
|
||||
props: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const buttonElement = wrapper.find("button");
|
||||
|
||||
// Act
|
||||
buttonElement.trigger("click");
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("click-event")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks(mountOptionsMockData = {}) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue