Merge pull request #1103 from Safelite/feature/CSR-1112A

Feature/csr 1112 a
This commit is contained in:
Leah Schumann 2023-05-18 08:51:51 -04:00 committed by GitHub
commit 9083fd72f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 170 additions and 237 deletions

View file

@ -19,6 +19,8 @@ module.exports = {
"!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing "!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
"!src/common-components/funnel-header/menu-modal/**/*.vue", "!src/common-components/funnel-header/menu-modal/**/*.vue",
"!src/layouts/schedule/*.vue", // Temp test exclusion while in development "!src/layouts/schedule/*.vue", // Temp test exclusion while in development
"!src/layouts/schedule/helpers/schedule-helper.js", // Temp test exclusion while in development
"!src/layouts/review/*.vue", // Temp test exclusion while in development
// END // END
], // ! means exclude from coverage. ], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"], testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -121,7 +121,7 @@ export default {
isOverflowScrollable: Boolean, isOverflowScrollable: Boolean,
isWide: Boolean, isWide: Boolean,
isCashOrInsurance: Boolean, isCashOrInsurance: Boolean,
modelValue: [Array, Number, String, Object], modelValue: [Array, Number, String],
value: [Number, String], value: [Number, String],
validationRules: String, validationRules: String,
suppressError: Boolean, suppressError: Boolean,
@ -141,7 +141,7 @@ export default {
const fieldOptions = { const fieldOptions = {
value: modelValue, value: modelValue,
initialValue: modelValue, initialValue: null,
}; };
const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } = const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } =
@ -255,10 +255,8 @@ export default {
}, },
}, },
watch: { watch: {
modelValue(newValue) { modelValue(newValue, oldValue) {
this.resetField({ this.resetField();
value: newValue,
});
}, },
answers() { answers() {
//once we get the answers to display from parent, see if we need a GA event to log what we showed //once we get the answers to display from parent, see if we need a GA event to log what we showed

View file

@ -1,6 +1,6 @@
<template> <template>
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<div class="appointment-type-question" aria-live="polite"> <div class="appointment-type-question" aria-live="polite" v-if="isDisplayed">
<buttonQuestion <buttonQuestion
ref="buttonQuestion" ref="buttonQuestion"
customButtonQuestionId="appointmentTypeQuestion" customButtonQuestionId="appointmentTypeQuestion"
@ -25,6 +25,7 @@ export default {
modelValue: String, modelValue: String,
groupName: String, groupName: String,
isAvailable: Boolean, isAvailable: Boolean,
isDisplayed: Boolean,
suppressError: Boolean, suppressError: Boolean,
validationRules: String, validationRules: String,
cmsWidgetName: String, cmsWidgetName: String,

View file

@ -45,4 +45,18 @@ describe("object-cloning-helper.js", () => {
// Assert // Assert
expect(result).toStrictEqual(expected); expect(result).toStrictEqual(expected);
}); });
it("Should return a copy of the array", async () => {
// Arrange
const array = [9, 8, 7, 6, 5, 4, 3, 2, 1];
const expected = [9, 8, 7, 6, 5, 4, 3, 2, 1];
// Act
const result = deepClone(array);
// Assert
expect(result).toStrictEqual(expected);
});
}); });

View file

@ -1,9 +1,10 @@
import { getPricedMobileFeePart } from "./service-location-helper"; import { getPricedMobileFeePart, getServiceabilityDetails } from "./service-location-helper";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART; const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
const mockStoreActionPriceOrderItemsAndSaveServerData = const mockStoreActionPriceOrderItemsAndSaveServerData =
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA; storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
jest.mock("@/mixins/base-mixin.js", () => ({ jest.mock("@/mixins/base-mixin.js", () => ({
methods: { methods: {
@ -40,56 +41,87 @@ jest.mock("@/mixins/base-mixin.js", () => ({
}, },
]); ]);
} }
if (actionName === mockStoreActionGetServiceabilityDetails) {
return Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
});
}
}), }),
}, },
})); }));
describe("service-location-helper.js", () => { describe("service-location-helper.js", () => {
it("Should return null if no service zip code is passed in", async () => { describe("getPricedMobileFeePart", () => {
// Arrange it("Should return null if no service zip code is passed in", async () => {
const serviceZipCode = null; // Arrange
const damageType = "Replace"; const serviceZipCode = null;
const parentAccountNumber = 167132; const damageType = "Replace";
const billToAccountNumber = 1234; const parentAccountNumber = 167132;
const expected = null; const billToAccountNumber = 1234;
const expected = null;
// Act // Act
const result = await getPricedMobileFeePart( const result = await getPricedMobileFeePart(
serviceZipCode, serviceZipCode,
damageType, damageType,
parentAccountNumber, parentAccountNumber,
billToAccountNumber billToAccountNumber
); );
// Assert // Assert
expect(result).toEqual(expected); expect(result).toEqual(expected);
});
it("Should return the priced mobile fee part", async () => {
// Arrange
const serviceZipCode = "43235";
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
laborAmount: 49.99,
sellingPrice: 0,
kitPrice: 0,
};
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Assert
expect(result).toEqual(expected);
});
}); });
it("Should return the priced mobile fee part", async () => { describe("getServiceabilityDetails", () => {
// Arrange it("Should return the serviceability details", async () => {
const serviceZipCode = "43235"; // Arrange
const damageType = "Replace"; const serviceZipCode = "43235";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = { const expected = {
partNumber: "MOBILE FEE", isGlassServiceableInshop: true,
description: "MOBILE FEE", isRecalibrationServiceableInshop: true,
partType: "FEE", isGlassServiceableMobile: true,
laborAmount: 49.99, isRecalibrationServiceableMobile: true,
sellingPrice: 0, };
kitPrice: 0,
};
// Act // Act
const result = await getPricedMobileFeePart( const result = await getServiceabilityDetails(serviceZipCode);
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Assert // Assert
expect(result).toEqual(expected); expect(result).toEqual(expected);
});
}); });
}); });

View file

@ -236,35 +236,40 @@ export default {
}); });
}, },
async setMobileLocation() { async setMobileLocation() {
// Validate the Zip Code if (
const zipCodeData = await this.getZipCodeData( this.internalModel.addressQuestions.zipCode !==
this.internalModel.addressQuestions.zipCode this.modelValue.addressQuestions.zipCode
); ) {
// Validate the Zip Code
const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode
);
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
this.resetModalButtonStyle(); this.resetModalButtonStyle();
} else { } else {
// retrieve mobile fee part // retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode; const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode); const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode); const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
// update content related to service zip code // update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart); this.$emit("updated-mobile-fee-part", mobileFeePart);
this.$emit("updated-serviceability", serviceabilityDetails.data); this.$emit("updated-serviceability", serviceabilityDetails.data);
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase); this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
// Update the page level model // Update the page level model
this.$emit("update:modelValue", this.internalModel); this.$emit("update:modelValue", this.internalModel);
if (this.onZipUpdateCallback) { if (this.onZipUpdateCallback) {
await this.onZipUpdateCallback(serviceZipCode); await this.onZipUpdateCallback(serviceZipCode);
}
this.closeModal();
} }
this.closeModal();
} }
}, },
}, },

View file

@ -505,36 +505,6 @@ describe("service-location.vue", () => {
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo); expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
}); });
test("resets appointment type selection when service zip code is updated by mobile location modal when Mobile is not selected", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
selectedAppointmentType: "Dropoff",
});
const mobileLocationQuestionsComponent = wrapper.findComponent({
ref: "mobileLocationQuestions",
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
const newServiceZipCodeQuestion = {
zipCode: "61606",
state: "IL",
};
// Act
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual(null);
});
test("does not reset appointment type selection when service zip code is updated by mobile location modal when Mobile is selected", async () => { test("does not reset appointment type selection when service zip code is updated by mobile location modal when Mobile is selected", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});

View file

@ -47,9 +47,10 @@
alertClass="alert-warning" /> alertClass="alert-warning" />
<appointmentTypeQuestion <appointmentTypeQuestion
v-model="selectedAppointmentType" v-model="selectedAppointmentType"
v-show="!displayNoShopsAlert" v-show="isAppointmentTypeDisplayed"
:isServiceableMobile="isServiceableMobile" :isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop" :isServiceableInshop="isServiceableInshop"
:isDisplayed="isAppointmentTypeDisplayed"
ref="appointmentTypeQuestion" ref="appointmentTypeQuestion"
groupName="appointmentTypeQuestion" groupName="appointmentTypeQuestion"
cmsWidgetName="AppointmentTypeQuestionWidget" cmsWidgetName="AppointmentTypeQuestionWidget"
@ -71,9 +72,7 @@
<shopQuestion <shopQuestion
ref="shopQuestion" ref="shopQuestion"
v-show="isShopQuestionDisplayed" v-show="isShopQuestionDisplayed"
v-model="selectedProviderNumber" v-model="selectedProvider"
@providerSelected="onProviderSelected"
:serviceZipCode="zipCode"
:selectedAppointmentType="selectedAppointmentType" :selectedAppointmentType="selectedAppointmentType"
:isDisplayed="isShopQuestionDisplayed" :isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget" /> cmsWidgetName="ShopQuestionWidget" />
@ -134,16 +133,6 @@ defineRule("mobile-location-required", (value) => {
return true; return true;
}); });
const defaultProvider = {
providerNumber: null,
address: {
streetAddress: null,
city: null,
state: null,
zip: null,
},
};
export default { export default {
name: "service-location", name: "service-location",
data() { data() {
@ -160,7 +149,6 @@ export default {
isRecalibrationServiceableMobile: null, isRecalibrationServiceableMobile: null,
selectedAppointmentType: this.getSelectedAppointmentType(), selectedAppointmentType: this.getSelectedAppointmentType(),
selectedProvider: this.getSelectedProvider(), selectedProvider: this.getSelectedProvider(),
selectedProviderNumber: this.getSelectedProvider().providerNumber,
mobileFeePart: null, mobileFeePart: null,
zipContainsMilitaryBase: false, zipContainsMilitaryBase: false,
zipCodeCtu: null, zipCodeCtu: null,
@ -228,7 +216,7 @@ export default {
if (newValue.zipCode !== this.zipCode) { if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation(); this.resetMobileLocation();
this.selectedAppointmentType = null; this.selectedAppointmentType = null;
this.selectedProvider = defaultProvider; this.selectedProvider = null;
} }
this.state = newValue.state; this.state = newValue.state;
@ -263,7 +251,7 @@ export default {
if (!this.selectedAppointmentType == "Mobile") { if (!this.selectedAppointmentType == "Mobile") {
this.selectedAppointmentType = null; this.selectedAppointmentType = null;
} }
this.selectedProvider = defaultProvider; this.selectedProvider = null;
} }
}, },
}, },
@ -287,8 +275,11 @@ export default {
this.selectedAppointmentType === "Dropoff" this.selectedAppointmentType === "Dropoff"
); );
}, },
// Specifically check for isRecalibrationServiceableMobile === false, not null or true. isAppointmentTypeDisplayed() {
return this.zipCode && !this.displayNoShopsAlert;
},
requiresInshopRecalibration() { requiresInshopRecalibration() {
// Specifically check for isRecalibrationServiceableMobile === false, not null or true.
return ( return (
this.isServiceableInshop && this.isServiceableInshop &&
this.isGlassServiceableMobile && this.isGlassServiceableMobile &&
@ -342,6 +333,9 @@ export default {
this.zipContainsMilitaryBase = val; this.zipContainsMilitaryBase = val;
} }
}, },
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
},
getServiceAddressFromStore() { getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address; return store.getters.order.serviceLocation.address;
}, },
@ -358,10 +352,7 @@ export default {
return store.getters.order.serviceLocation.appointmentType; return store.getters.order.serviceLocation.appointmentType;
}, },
getSelectedProvider() { getSelectedProvider() {
return store.getters.order.serviceLocation.provider ?? defaultProvider; return store.getters.order.serviceLocation.provider;
},
setMobileFeePart(mobileFeePart) {
this.mobileFeePart = mobileFeePart;
}, },
resetMobileLocation() { resetMobileLocation() {
this.streetAddress = ""; this.streetAddress = "";
@ -417,9 +408,6 @@ export default {
async reloadShopData() { async reloadShopData() {
await this.$refs.shopQuestion.reloadShopData(this.zipCode); await this.$refs.shopQuestion.reloadShopData(this.zipCode);
}, },
onProviderSelected(selectedProvider) {
this.selectedProvider = selectedProvider;
},
}, },
components: { components: {
alert, alert,

View file

@ -434,7 +434,7 @@ describe("shop-question.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
modelValue: selectedProvider.providerNumber, modelValue: selectedProvider,
serviceZipCode: "43081", serviceZipCode: "43081",
selectedAppointmentType: "Dropoff", selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName, cmsWidgetName: cmsWidgetName,
@ -460,18 +460,7 @@ describe("shop-question.vue", () => {
const { wrapper } = setupMocks({ const { wrapper } = setupMocks({
mixins: [mockMixin], mixins: [mockMixin],
props: { props: {
modelValue: { modelValue: null,
address: {
city: "POWELL",
country: "US",
state: "OH",
streetAddress: "3938 POWELL RD",
zipCode: "43065",
},
distanceInMiles: 16.2690495685233,
providerNumber: "003341",
},
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff", selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName, cmsWidgetName: cmsWidgetName,
}, },
@ -481,99 +470,22 @@ describe("shop-question.vue", () => {
}); });
// Act // Act
wrapper.vm.initializeComponent(shopQuestionInitialData); await wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.vm.answers.length).toEqual(3);
await wrapper.setProps({ await wrapper.setProps({
selectedAppointmentType: "Inshop", selectedAppointmentType: "Inshop",
}); });
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
// Assert // Assert
expect(wrapper.vm.answers.length).toEqual(3); expect(wrapper.vm.answers.length).toEqual(3);
}); });
// it("Should reload the shops when the service zip code changes", async () => {
// // Arrange
// const { wrapper } = setupMocks({
// mixins: [mockMixin],
// props: {
// modelValue: {
// address: {
// city: "POWELL",
// country: "US",
// state: "OH",
// streetAddress: "3938 POWELL RD",
// zipCode: "43065",
// },
// distanceInMiles: 16.2690495685233,
// providerNumber: "003341",
// },
// serviceZipCode: "43081",
// selectedAppointmentType: "Dropoff",
// cmsWidgetName: cmsWidgetName,
// isDisplayed: true
// },
// mountOptions: {
// attachTo: document.body,
// },
// });
// wrapper.vm.$options.methods.loadInitialData = jest.fn().mockImplementation(() => {
// return new Promise((resolve) => {
// resolve(mockNewShopList);
// });
// });
// // Act
// wrapper.vm.initializeComponent(shopQuestionInitialData);
// await wrapper.vm.$options.watch.serviceZipCode.handler.call(wrapper.vm, "43054");
// // Assert
// expect(wrapper.vm.shopProviders.length).toEqual(3);
// expect(wrapper.vm.shopProviders).toEqual(mockNewShopList.shopProviders);
// });
// it("Should clear the existing answers when the service zip code changes", async () => {
// // Arrange
// const { wrapper } = setupMocks({
// mixins: [mockMixin],
// props: {
// modelValue: {
// address: {
// city: "POWELL",
// country: "US",
// state: "OH",
// streetAddress: "3938 POWELL RD",
// zipCode: "43065",
// },
// distanceInMiles: 16.2690495685233,
// providerNumber: "003341",
// },
// serviceZipCode: "43081",
// selectedAppointmentType: "Dropoff",
// cmsWidgetName: cmsWidgetName,
// },
// mountOptions: {
// attachTo: document.body,
// },
// });
// //Act
// wrapper.vm.initializeComponent(shopQuestionInitialData);
// wrapper.setProps({
// selectedAppointmentType: "Inshop",
// });
// await wrapper.vm.$nextTick();
// // Assert
// expect(wrapper.vm.answers.length).toEqual(3);
// });
}); });
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) { function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {

View file

@ -68,8 +68,8 @@ export default {
props: { props: {
modelValue: { modelValue: {
type: Object, type: Object,
default: () => null,
}, },
serviceZipCode: String,
selectedAppointmentType: String, selectedAppointmentType: String,
cmsWidgetName: String, cmsWidgetName: String,
validationRules: String, validationRules: String,
@ -81,16 +81,12 @@ export default {
}, },
selectedValue: { selectedValue: {
get: function () { get: function () {
return this.modelValue; return this.modelValue?.providerNumber;
}, },
set: function (newValue) { set: function (newValue) {
// Button Question only supports primitive values so we must get the full object to emit to the page // Button Question only supports Number, String data types so we must get the full object to emit
const provider = this.shopProviders?.find( const provider = this.getSelectedProviderObject(newValue);
(provider) => provider.providerNumber == newValue this.$emit("update:modelValue", provider);
);
this.$emit("update:modelValue", newValue);
this.$emit("providerSelected", provider);
}, },
}, },
displayDropoffInformation() { displayDropoffInformation() {
@ -170,11 +166,6 @@ export default {
resetAnswers() { resetAnswers() {
this.answers = []; this.answers = [];
this.shopIndex = 0; this.shopIndex = 0;
this.selectedValue = "";
if (this.$refs.buttonQuestion) {
this.$refs.buttonQuestion.resetField();
}
}, },
async reloadShopData(serviceZipCode) { async reloadShopData(serviceZipCode) {
const result = await this.loadData(serviceZipCode); const result = await this.loadData(serviceZipCode);
@ -185,6 +176,20 @@ export default {
await nextTick(); await nextTick();
await this.getNextShopsFromList(); await this.getNextShopsFromList();
}, },
getSelectedProviderObject(providerNumber) {
const provider = this.shopProviders?.find(
(provider) => provider.providerNumber == providerNumber
);
return provider;
},
getSelectedProviderIndex(providers, selectedProviderNumber) {
const index = providers.findIndex(
(provider) => provider.providerNumber == selectedProviderNumber
);
return index;
},
}, },
watch: { watch: {
selectedAppointmentType: { selectedAppointmentType: {
@ -193,6 +198,12 @@ export default {
await this.$nextTick(); await this.$nextTick();
this.selectedValue = null;
this.$refs.buttonQuestion?.resetField();
await this.$nextTick();
if (newValue !== "Mobile") { if (newValue !== "Mobile") {
await this.getNextShopsFromList(); await this.getNextShopsFromList();
} }
@ -200,19 +211,19 @@ export default {
}, },
shopProviders: { shopProviders: {
async handler(newValue) { async handler(newValue) {
//this.resetAnswers();
await this.$nextTick(); await this.$nextTick();
if (this.selectedAppointmentType) { if (this.selectedAppointmentType) {
const selectedShopIndex = newValue.findIndex( const selectedShopIndex = this.getSelectedProviderIndex(
(provider) => provider.providerNumber == this.modelValue newValue,
this.modelValue?.providerNumber
); );
if (selectedShopIndex >= 3) { if (selectedShopIndex >= 3) {
await this.getNextShopsFromList(selectedShopIndex + 1); await this.getNextShopsFromList(selectedShopIndex + 1);
} else { } else {
await this.getNextShopsFromList(); await this.getNextShopsFromList();
await this.$nextTick();
} }
} }
}, },