Merge pull request #1309 from Safelite/feature/CSR-1373
Feature/csr 1373
This commit is contained in:
commit
9b7114111d
8 changed files with 529 additions and 9 deletions
|
|
@ -8,7 +8,7 @@
|
|||
:mask="mask"
|
||||
:isRequired="isRequired"
|
||||
:validationRules="validationRulesForTextBoxQuestion"
|
||||
cmsWidgetName="PhoneNumberQuestionWidget" />
|
||||
:cmsWidgetName="cmsWidgetName" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
validationRules="email-address-required|email-address-format" />
|
||||
<phoneNumberQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="phoneNumberQuestionWidget"
|
||||
cmsWidgetName="PhoneNumberQuestionWidget"
|
||||
v-model="phoneNumber"
|
||||
isRequired
|
||||
validationRules="phone-number-required" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
import customerDetailsModalQuestion from "@/layouts/review/review-sections/customer-review/customer-details-modal-question/customer-details-modal-question";
|
||||
|
||||
const testConstants = {
|
||||
previousCustomerValues: {
|
||||
firstName: "First",
|
||||
lastName: "Last",
|
||||
emailAddress: "builddigitaltest@safelite.com",
|
||||
phoneNumber: "111-111-1111",
|
||||
isSmsOptIn: false,
|
||||
},
|
||||
newValues: {
|
||||
firstName: "New First",
|
||||
lastName: "New Last",
|
||||
emailAddress: "builddigitaltest2@safelite.com",
|
||||
phoneNumber: "222-222-2222",
|
||||
isSmsOptIn: true,
|
||||
},
|
||||
};
|
||||
|
||||
let cmsContent;
|
||||
|
||||
describe("Customer Details Modal", () => {
|
||||
beforeEach(() => {
|
||||
cmsContent = {};
|
||||
});
|
||||
|
||||
describe("Submit", () => {
|
||||
test("Should push to store if a field has changed", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.onModalOpened();
|
||||
|
||||
wrapper.vm.firstName = testConstants.newValues.firstName;
|
||||
|
||||
await wrapper.vm.setContactDetails();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
|
||||
expect(wrapper.vm.closeModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("Should not push to store if no fields have changed", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.onModalOpened();
|
||||
|
||||
await wrapper.vm.setContactDetails();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.dispatchStoreAction).not.toHaveBeenCalled();
|
||||
expect(wrapper.vm.closeModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Default values", () => {
|
||||
test("Should populate on open", () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.onModalOpened();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.firstName).toBe(testConstants.previousCustomerValues.firstName);
|
||||
expect(wrapper.vm.lastName).toBe(testConstants.previousCustomerValues.lastName);
|
||||
expect(wrapper.vm.emailAddress).toBe(testConstants.previousCustomerValues.emailAddress);
|
||||
expect(wrapper.vm.phoneNumber).toBe(testConstants.previousCustomerValues.phoneNumber);
|
||||
expect(wrapper.vm.isSmsOptIn).toBe(testConstants.previousCustomerValues.isSmsOptIn);
|
||||
});
|
||||
|
||||
test("Should replace old values on open", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.firstName = testConstants.newValues.firstName;
|
||||
wrapper.vm.lastName = testConstants.newValues.lastName;
|
||||
wrapper.vm.emailAddress = testConstants.newValues.emailAddress;
|
||||
wrapper.vm.phoneNumber = testConstants.newValues.phoneNumber;
|
||||
wrapper.vm.isSmsOptIn = testConstants.newValues.isSmsOptIn;
|
||||
|
||||
wrapper.vm.onModalOpened();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.firstName).toBe(testConstants.previousCustomerValues.firstName);
|
||||
expect(wrapper.vm.lastName).toBe(testConstants.previousCustomerValues.lastName);
|
||||
expect(wrapper.vm.emailAddress).toBe(testConstants.previousCustomerValues.emailAddress);
|
||||
expect(wrapper.vm.phoneNumber).toBe(testConstants.previousCustomerValues.phoneNumber);
|
||||
expect(wrapper.vm.isSmsOptIn).toBe(testConstants.previousCustomerValues.isSmsOptIn);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function generateDefaultProps() {
|
||||
return {
|
||||
previousCustomerValues: {
|
||||
firstName: testConstants.previousCustomerValues.firstName,
|
||||
lastName: testConstants.previousCustomerValues.lastName,
|
||||
emailAddress: testConstants.previousCustomerValues.emailAddress,
|
||||
phoneNumber: testConstants.previousCustomerValues.phoneNumber,
|
||||
isSmsOptIn: testConstants.previousCustomerValues.isSmsOptIn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setupMocks(customMountOptions) {
|
||||
const mountOptions = getMountOptions(customMountOptions);
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mountOptions.global.mixins = [mockMixin];
|
||||
|
||||
const wrapper = shallowMount(customerDetailsModalQuestion, mountOptions);
|
||||
wrapper.vm.setCmsContent = jest.fn();
|
||||
wrapper.vm.openModal = jest.fn();
|
||||
wrapper.vm.closeModal = jest.fn();
|
||||
wrapper.vm.dispatchStoreAction = jest.fn();
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
<template>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="customer-details-question">
|
||||
<modal
|
||||
ref="CustomerDetailsModal"
|
||||
headerText="Contact details"
|
||||
footerButtonText="Save contact details"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setContactDetails">
|
||||
<template v-if="isModalOpened">
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="CustomerFirstNameWidget"
|
||||
v-model="firstName"
|
||||
ref="firstName"
|
||||
customInputId="firstName"
|
||||
validation-rules="first-name-required" />
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="CustomerLastNameWidget"
|
||||
v-model="lastName"
|
||||
ref="lastName"
|
||||
customInputId="lastName"
|
||||
validation-rules="last-name-required" />
|
||||
<textboxQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="CustomerEmailWidget"
|
||||
v-model="emailAddress"
|
||||
ref="emailAddress"
|
||||
customInputId="emailAddress"
|
||||
validation-rules="email-address-required|email-address-format" />
|
||||
<phoneNumberQuestion
|
||||
class="mb-4"
|
||||
cmsWidgetName="CustomerPhoneWidget"
|
||||
v-model="phoneNumber"
|
||||
ref="phoneNumber"
|
||||
isRequired
|
||||
validation-rules="phone-number-required" />
|
||||
<checkboxQuestion
|
||||
class="mb-5"
|
||||
cmsWidgetName="CustomerTextMeWidget"
|
||||
v-model="isSmsOptIn" />
|
||||
<textBlock cmsWidgetName="CustomerTextDisclaimerWidget" typeStyle="caption" />
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
import phoneNumberQuestion from "@/digital-components/phone-number-question/phone-number-question";
|
||||
import checkboxQuestion from "@/digital-components/checkbox-question/checkbox-question";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { defineRule } from "vee-validate";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||
defineRule("phone-number-required", required(errorMessages.PHONE_REQUIRED));
|
||||
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
||||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
||||
export default {
|
||||
name: "customer-details-modal-question",
|
||||
data() {
|
||||
return {
|
||||
isModalOpened: false,
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
emailAddress: "",
|
||||
phoneNumber: "",
|
||||
isSmsOptIn: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
previousCustomerValues: Object,
|
||||
},
|
||||
computed: {
|
||||
modal() {
|
||||
return this.$refs.CustomerDetailsModal;
|
||||
},
|
||||
haveCustomerDetailsChanged() {
|
||||
return (
|
||||
this.firstName !== this.previousCustomerValues?.firstName ||
|
||||
this.lastName !== this.previousCustomerValues?.lastName ||
|
||||
this.emailAddress !== this.previousCustomerValues?.emailAddress ||
|
||||
this.phoneNumber !== this.previousCustomerValues?.phoneNumber ||
|
||||
this.isSmsOptIn !== this.previousCustomerValues?.isSmsOptIn
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.firstName = this.previousCustomerValues?.firstName ?? "";
|
||||
this.lastName = this.previousCustomerValues?.lastName ?? "";
|
||||
this.emailAddress = this.previousCustomerValues?.emailAddress ?? "";
|
||||
this.phoneNumber = this.previousCustomerValues?.phoneNumber ?? "";
|
||||
this.isSmsOptIn = this.previousCustomerValues?.isSmsOptIn ?? false;
|
||||
},
|
||||
onModalClosed() {},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
async setContactDetails() {
|
||||
if (this.haveCustomerDetailsChanged) {
|
||||
await this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_CUSTOMER_DETAILS,
|
||||
{
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
emailAddress: this.emailAddress,
|
||||
phoneNumber: this.phoneNumber,
|
||||
isSmsOptIn: this.isSmsOptIn,
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
this.closeModal();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
modal,
|
||||
textboxQuestion,
|
||||
phoneNumberQuestion,
|
||||
checkboxQuestion,
|
||||
textBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
import customerReview from "@/layouts/review/review-sections/customer-review/customer-review";
|
||||
|
||||
const testConstants = {
|
||||
cms: {
|
||||
header: {
|
||||
text: "Header",
|
||||
},
|
||||
sms: {
|
||||
template: "Test {custom:smsOptInJoiner}",
|
||||
expected: {
|
||||
ifTrue: "Test in to",
|
||||
ifFalse: "Test out of",
|
||||
},
|
||||
},
|
||||
},
|
||||
customer: {
|
||||
firstName: "First",
|
||||
lastName: "Last",
|
||||
phoneNumber: "111-111-1111",
|
||||
emailAddress: "builddigitaltest@safelite.com",
|
||||
isSmsOptIn: false,
|
||||
},
|
||||
displayContent: {
|
||||
fullName: "First Last",
|
||||
phoneNumber: "111-111-1111",
|
||||
emailAddress: "builddigitaltest@safelite.com",
|
||||
smsOptIn: "Test out of",
|
||||
},
|
||||
};
|
||||
|
||||
let cmsContent;
|
||||
|
||||
describe("Customer Review Block", () => {
|
||||
beforeEach(() => {
|
||||
cmsContent = {
|
||||
CustomerWidget: {
|
||||
HeaderText: testConstants.cms.header.text,
|
||||
SubheaderText: testConstants.cms.sms.template,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("Should display header text from cms", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.header).toEqual(testConstants.cms.header.text);
|
||||
});
|
||||
|
||||
describe("SMS Opt In Text", () => {
|
||||
test("Should render correctly when opt in is true:", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
props.customer.isSmsOptIn = true;
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifTrue);
|
||||
});
|
||||
test("Should render correctly when opt in is false:", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
props.customer.isSmsOptIn = false;
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifFalse);
|
||||
});
|
||||
test("Should render correctly when opt in is null:", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
props.customer.isSmsOptIn = null;
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.smsOptIn).toEqual(testConstants.cms.sms.expected.ifFalse);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render correct display content", async () => {
|
||||
// Arrange
|
||||
let props = generateDefaultProps();
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
// Act
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.displayContent).toEqual([
|
||||
testConstants.displayContent.fullName,
|
||||
testConstants.displayContent.emailAddress,
|
||||
testConstants.displayContent.phoneNumber,
|
||||
testConstants.displayContent.smsOptIn,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function generateDefaultProps() {
|
||||
return {
|
||||
cmsWidgetName: "CustomerWidget",
|
||||
customer: {
|
||||
firstName: testConstants.customer.firstName,
|
||||
lastName: testConstants.customer.lastName,
|
||||
phoneNumber: testConstants.customer.phoneNumber,
|
||||
emailAddress: testConstants.customer.emailAddress,
|
||||
isSmsOptIn: testConstants.customer.isSmsOptIn,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function setupMocks(customMountOptions) {
|
||||
const mountOptions = getMountOptions(customMountOptions);
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
return cmsContent?.[widgetName]?.[cmsFieldName] ?? "";
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
mountOptions.global.mixins = [mockMixin];
|
||||
|
||||
const wrapper = shallowMount(customerReview, mountOptions);
|
||||
wrapper.vm.setCmsContent = jest.fn();
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<reviewBlock :customHeaderText="header" :content="displayContent" @edit-clicked="editClicked" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import reviewBlock from "@/layouts/review/review-block/review-block";
|
||||
|
||||
export default {
|
||||
name: "customer-review",
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
customer: Object,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
methods: {
|
||||
editClicked() {
|
||||
this.$emit("edit-clicked");
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayContent() {
|
||||
return [this.fullName, this.email, this.phoneNumber, this.smsOptIn];
|
||||
},
|
||||
header() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
|
||||
},
|
||||
fullName() {
|
||||
return `${this.customer?.firstName} ${this.customer?.lastName}`;
|
||||
},
|
||||
email() {
|
||||
return this.customer?.emailAddress;
|
||||
},
|
||||
phoneNumber() {
|
||||
return this.customer?.phoneNumber;
|
||||
},
|
||||
smsOptIn() {
|
||||
const rawCmsText = this.getCmsContent(this.cmsWidgetName, "SubheaderText");
|
||||
const joinerText = this.customer?.isSmsOptIn ? "in to" : "out of";
|
||||
|
||||
return rawCmsText.replace("{custom:smsOptInJoiner}", joinerText);
|
||||
},
|
||||
},
|
||||
components: {
|
||||
reviewBlock,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -76,12 +76,23 @@
|
|||
cmsWidgetName="ScheduleWidget"
|
||||
:appointmentType="appointmentType"
|
||||
@edit-clicked="editSchedule" />
|
||||
|
||||
<hr class="my-0" />
|
||||
|
||||
<customerReview
|
||||
cmsWidgetName="CustomerReviewWidget"
|
||||
:customer="customerInfo"
|
||||
@edit-clicked="editCustomerDetails" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<hr class="my-0" />
|
||||
</div>
|
||||
|
||||
<customerDetailsModalQuestion
|
||||
ref="customerDetailsModalQuestion"
|
||||
:previousCustomerValues="customerInfo" />
|
||||
|
||||
<funnelFooter
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
@back-clicked="backButtonAction"
|
||||
|
|
@ -102,6 +113,9 @@ import damageReview from "@/layouts/review/review-sections/damage-review/damage-
|
|||
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
|
||||
import serviceLocationReview from "@/layouts/review/review-sections/service-location-review/service-location-review";
|
||||
import scheduleReview from "@/layouts/review/review-sections/schedule-review/schedule-review";
|
||||
import customerReview from "@/layouts/review/review-sections/customer-review/customer-review";
|
||||
|
||||
import customerDetailsModalQuestion from "@/layouts/review/review-sections/customer-review/customer-details-modal-question/customer-details-modal-question.vue";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
|
@ -175,6 +189,9 @@ export default {
|
|||
this.$route
|
||||
);
|
||||
},
|
||||
editCustomerDetails() {
|
||||
this.$refs.customerDetailsModalQuestion.openModal();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
subHeaderTitle() {
|
||||
|
|
@ -201,8 +218,8 @@ export default {
|
|||
appointmentType() {
|
||||
return this.$store.getters.order.serviceLocation.appointmentType;
|
||||
},
|
||||
exposeCms() {
|
||||
return this.$root.cmsContentByWidget;
|
||||
customerInfo() {
|
||||
return this.$store.getters.order.customer;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
|
|
@ -216,6 +233,8 @@ export default {
|
|||
servicePackageReview,
|
||||
serviceLocationReview,
|
||||
scheduleReview,
|
||||
customerReview,
|
||||
customerDetailsModalQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -31,11 +31,6 @@ import { applicationConfig } from "../constants/application-config";
|
|||
import review from "@/layouts/review/review";
|
||||
import paymentMethod from "@/layouts/payment-method/payment-method";
|
||||
const routes = [
|
||||
{
|
||||
path: "/review", // This is a temporary route for testing.
|
||||
name: "review",
|
||||
component: review,
|
||||
},
|
||||
{
|
||||
path: "/payment-method", // This is a temporary route for testing.
|
||||
name: "payment-method",
|
||||
|
|
|
|||
Loading…
Reference in a new issue