Merge remote-tracking branch 'origin/develop' into feature/CSR-1137

This commit is contained in:
Scott Kiener 2023-05-18 14:34:05 -04:00
commit 8967676496
12 changed files with 234 additions and 261 deletions

View file

@ -15,10 +15,12 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/reveal/**/*.vue",
"!src/ux-components/text-link/**/*.vue",
"!src/common-components/date-picker/**/*.vue", // Temp until unit tests completed
"!src/digital-components/date-picker/**/*.vue", // Temp until unit tests completed
"!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
"!src/common-components/funnel-header/menu-modal/**/*.vue",
"!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
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -120,7 +120,7 @@ export default {
isRequired: Boolean,
isOverflowScrollable: Boolean,
isWide: Boolean,
modelValue: [Array, Number, String, Object],
modelValue: [Array, Number, String],
value: [Number, String],
validationRules: String,
suppressError: Boolean,
@ -140,7 +140,7 @@ export default {
const fieldOptions = {
value: modelValue,
initialValue: modelValue,
initialValue: null,
};
const { errorMessage, handleBlur, handleChange, meta, validate, errors, resetField } =
@ -255,10 +255,8 @@ export default {
},
},
watch: {
modelValue(newValue) {
this.resetField({
value: newValue,
});
modelValue(newValue, oldValue) {
this.resetField();
},
answers() {
//once we get the answers to display from parent, see if we need a GA event to log what we showed
@ -295,16 +293,16 @@ export default {
</script>
<style lang="scss">
// .button-question-overflow {
// height: calc(100vh - 274px);
.button-question-overflow {
height: calc(100vh - 274px);
// .overflow-scroll {
// // Height will be determined by overall height of content above list
// height: calc(100% - 314px);
// overflow-x: hidden !important;
// -webkit-overflow-scrolling: touch;
// }
// }
.overflow-scroll {
// Height will be determined by overall height of content above list
height: calc(100% - 314px);
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
}
}
.button-question {
color: $black;

View file

@ -6,15 +6,55 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js";
describe("date-picker.vue", () => {
describe("initial setup", () => {
test("Creates a date object from today", () => {
test("Creates a date object from today", async () => {
// Arrange
const { wrapper } = setupMocks({});
const { wrapper } = await setupMocks({
propsData: {
selectableDatesSetting: "custom"
},
});
// Act
const testResult = wrapper.vm.todayDate instanceof Date;
wrapper.vm.loadInitialData = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
resolve({
data: [responseValue],
});
})
);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.vm.loadInitialData).toHaveBeenCalled();
// console.log("wrapper.vm.today: ", wrapper.vm.today)
// // Act
// const testResult = wrapper.vm.today instanceof Date;
// console.log("testResult: ", testResult)
// Assert
expect(testResult).toEqual(true);
// // Assert
// expect(testResult).toEqual(true);
wrapper.unmount();
});

View file

@ -9,18 +9,18 @@ import { headerKeys } from "@/constants/header-keys";
export default {
callHttpClient({ method, endpoint, payload, logApiCall = true, isFormData = false }) {
return new Promise((resolve, reject) => {
let cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "FixMyGlass" });
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
let payloadAndAnalyticsData = {};
if (isFormData) {
payloadAndAnalyticsData = payload;
payloadAndAnalyticsData.append("AppName", "FixMyGlass");
} else {
Object.assign(payloadAndAnalyticsData, payload, { AppName: "FixMyGlass" });
}
const headers = {
[headerKeys.EXPERIMENT]: JSON.stringify(store.getters.experimentSettings),
};
if (endpoint.toLowerCase().includes("/analytics/")) {
cfDistroUrl = "";
endpoint = "";
method = "GET";
}
axios({
method: method,
url: cfDistroUrl + endpoint,

View file

@ -1,6 +1,6 @@
<template>
<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
ref="buttonQuestion"
customButtonQuestionId="appointmentTypeQuestion"
@ -25,6 +25,7 @@ export default {
modelValue: String,
groupName: String,
isAvailable: Boolean,
isDisplayed: Boolean,
suppressError: Boolean,
validationRules: String,
cmsWidgetName: String,

View file

@ -45,4 +45,18 @@ describe("object-cloning-helper.js", () => {
// Assert
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";
const mockStoreActionGetMobileFeePart = storeActions.GET_MOBILE_FEE_PART;
const mockStoreActionPriceOrderItemsAndSaveServerData =
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA;
const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS;
jest.mock("@/mixins/base-mixin.js", () => ({
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", () => {
it("Should return null if no service zip code is passed in", async () => {
// Arrange
const serviceZipCode = null;
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = null;
describe("getPricedMobileFeePart", () => {
it("Should return null if no service zip code is passed in", async () => {
// Arrange
const serviceZipCode = null;
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
const expected = null;
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Assert
expect(result).toEqual(expected);
// Assert
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 () => {
// Arrange
const serviceZipCode = "43235";
const damageType = "Replace";
const parentAccountNumber = 167132;
const billToAccountNumber = 1234;
describe("getServiceabilityDetails", () => {
it("Should return the serviceability details", async () => {
// Arrange
const serviceZipCode = "43235";
const expected = {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
laborAmount: 49.99,
sellingPrice: 0,
kitPrice: 0,
};
const expected = {
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
};
// Act
const result = await getPricedMobileFeePart(
serviceZipCode,
damageType,
parentAccountNumber,
billToAccountNumber
);
// Act
const result = await getServiceabilityDetails(serviceZipCode);
// Assert
expect(result).toEqual(expected);
// Assert
expect(result).toEqual(expected);
});
});
});

View file

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

View file

@ -505,36 +505,6 @@ describe("service-location.vue", () => {
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 () => {
// Arrange
const { wrapper } = setupMocks({});

View file

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

View file

@ -434,7 +434,7 @@ describe("shop-question.vue", () => {
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: selectedProvider.providerNumber,
modelValue: selectedProvider,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
@ -460,18 +460,7 @@ describe("shop-question.vue", () => {
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",
modelValue: null,
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
@ -481,99 +470,22 @@ describe("shop-question.vue", () => {
});
// Act
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
expect(wrapper.vm.answers.length).toEqual(3);
await wrapper.setProps({
selectedAppointmentType: "Inshop",
});
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
// Assert
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 }) {

View file

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