Merge branch 'develop' into CSR-1382-order-quote-initial-setup

This commit is contained in:
Bryan Mauger 2023-08-22 09:24:30 -04:00
commit 48c2d28ab8
66 changed files with 2954 additions and 3231 deletions

View file

@ -15,10 +15,8 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/reveal/**/*.vue",
"!src/ux-components/text-link/**/*.vue",
"!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

View file

@ -12,7 +12,6 @@ const GaEvents = {
const GaCategories = {
API_RESPONSE: "Api_Response",
EVOX: "Evox",
FUNNEL_ENTRY: "funnel_entry",
};
const GaActions = {
@ -21,7 +20,6 @@ const GaActions = {
VIF: "vif",
SUBMITTED: "Submitted",
DISPLAYED: "Displayed",
ZIP_CODE_PROVIDED: "zip_code_provided",
};
const GaLabels = {

View file

@ -119,7 +119,7 @@ const endpoints = {
method: "GET",
},
SaveSession: {
url: "/order/api/v1/order/save-session",
url: "/order/api/v1/order/save-session/fmg",
method: "POST",
},
LoadSession: {

View file

@ -0,0 +1 @@
export const payWithInsuranceStates = ["AZ", "CT", "FL", "KY", "MA", "MN", "NY", "SC"];

View file

@ -9,7 +9,7 @@ const storeActions = {
GET_VEHICLE_MAKES: "getVehicleMakes",
GET_VEHICLE_MODELS: "getVehicleModels",
GET_VEHICLE_STYLES: "getVehicleStyles",
SET_VEHICLE: "setVehicle",
GET_VEHICLE: "getVehicle",
GET_DAMAGE_OPTIONS: "getDamageOptions",
GET_EVOX_IMAGE: "getEvoxImage",
IS_VIN_OPTIONAL_VEHICLE: "isVinOptionalVehicle",
@ -58,10 +58,6 @@ const storeActions = {
RESET_STATE: "resetState",
// SAVE COMPONENT STATE
SAVE_VEHICLE_YEAR: "saveVehicleYear",
SAVE_VEHICLE_MAKE: "saveVehicleMake",
SAVE_VEHICLE_MODEL: "saveVehicleModel",
SAVE_VEHICLE_STYLE: "saveVehicleStyle",
SAVE_VEHICLE: "saveVehicle",
SAVE_VEHICLE_DAMAGE: "saveVehicleDamage",
SAVE_VIN_LOOKUP: "saveVinLookup",

View file

@ -23,28 +23,20 @@ const storeMutations = {
UPDATE_SUPPORTING_ITEMS: "updateSupportingItems",
UPDATE_LINE_ITEMS_SERVER_DATA: "updateLineItemsServerData",
UPDATE_REGISTRATION_LICENSE_PLATE: "updateRegistrationLicensePlate",
UPDATE_REGISTRATION_ADDRESS: "updateRegistrationAddress",
UPDATE_REGISTRATION_CITY: "updateRegistrationCity",
UPDATE_REGISTRATION_STATE: "updateRegistrationState",
UPDATE_REGISTRATION_ZIP_CODE: "updateRegistrationZipCode",
UPDATE_REGISTRATION_FIRST_NAME: "updateRegistrationFirstName",
UPDATE_REGISTRATION_LAST_NAME: "updateRegistrationLastName",
UPDATE_REGISTRATION: "updateRegistration",
UPDATE_SERVICE_ZIP: "updateServiceZip",
UPDATE_SERVICE_LOCATION: "updateServiceLocation",
UPDATE_SERVICE_LOCATION_TECH_NOTES: "updateServiceLocationTechNotes",
UPDATE_TECH_NOTES: "updateTechNotes",
UPDATE_SCHEDULE: "updateSchedule",
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
// CUSTOMER MUTATIONS
UPDATE_CUSTOMER_EMAIL_ADDRESS: "updateCustomerEmailAddress",
UPDATE_CUSTOMER_DETAILS: "updateCustomerDetails",
// ORDER MUTATIONS
UPDATE_REFERRAL_NUMBER: "updateReferralNumber",
UPDATE_REFERRAL_SEQUENCE_NUMBER: "updateReferralSequenceNumber",
UPDATE_REFERRAL_DATE: "updateReferralDate",
UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId",
UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber",

File diff suppressed because it is too large Load diff

View file

@ -1,477 +0,0 @@
import datePicker from "./date-picker";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
describe("date-picker.vue", () => {
describe("initial setup", () => {
test("Creates a date object from today", async () => {
// Arrange
const { wrapper } = await setupMocks({
propsData: {
selectableDatesSetting: "custom"
},
});
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);
wrapper.unmount();
});
});
// describe("formatNumberToSetDigits", () => {
// const testScenarios = [
// [0, 3, "000"],
// [8, 2, "08"],
// [345, 2, "345"],
// [4567, 0, "4567"],
// ];
// test.each(testScenarios)(
// "when num is %s and digits is %s, formatNumberToSetDigits should return %s",
// async (num, digits, expectedReturn) => {
// // Arrange
// const { wrapper } = setupMocks({});
// // Act
// const fnReturn = wrapper.vm.formatNumberToSetDigits(num, digits);
// // Assert
// expect(fnReturn).toEqual(expectedReturn);
// }
// );
// });
// describe("formatDate", () => {
// test("Can format a date as MM/dd/yyyy", () => {
// // Arrange
// const { wrapper } = setupMocks({});
// const testDate = new Date("01-26-2023");
// // Act
// const testResult = wrapper.vm.formatDate(testDate);
// // Assert
// expect(testResult).toEqual("01/26/2023");
// wrapper.unmount();
// });
// // test("Can get correct initialFirstDate from today", () => {
// // // Arrange
// // const { wrapper } = setupMocks({
// // propsData: {
// // todayDate: "Thu Jan 26 2023",
// // },
// // });
// // // Act
// // const testResult = wrapper.vm.initialFirstDate;
// // const testResultAsDateString = testResult.toDateString();
// // // Assert
// // expect(testResultAsDateString).toEqual("Sun Jan 22 2023");
// // wrapper.unmount();
// // });
// });
// describe("calendarData", () => {
// test("should return an array", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// // Act
// const testResult = await wrapper.vm.calendarData;
// // Assert
// expect(Array.isArray(testResult)).toBe(true);
// wrapper.unmount();
// });
// describe("each month should have keys monthLabel, yearNum, dates, firstDateDayIndex", () => {
// const testScenarios = [
// ["monthLabel", true, "string"],
// ["yearNum", true, "number"],
// ["dates", true, "object"],
// ["firstDateDayIndex", true, "number"],
// ["monthClass", true, "string"],
// ];
// test.each(testScenarios)(
// "test for %s key should return %s and be type %s",
// async (label, test, type) => {
// // Arrange
// const { wrapper } = setupMocks({});
// const containsMonthLabel = (obj) => Object.hasOwn(obj, label);
// const isCorrectType = (obj) => typeof obj[label] === type;
// const isTruthyOrZero = (obj) => (obj[label] || obj[label] === 0 ? true : false);
// // Act
// const calendarData = await wrapper.vm.calendarData;
// // console.log("calendarData: ", calendarData);
// // Assert
// expect(calendarData.every(containsMonthLabel)).toBe(true);
// expect(calendarData.every(isCorrectType)).toBe(true);
// expect(calendarData.every(isTruthyOrZero)).toBe(true);
// wrapper.unmount();
// }
// );
// });
// describe("the current month's week which includes today", () => {
// const testScenarios = [
// ["2022-02-05T03:00:00", 1, 2, true],
// ["2024-02-05T03:00:00", 4, 0, true],
// ["2020-01-15T03:00:00", 12, 0, true],
// ];
// test.each(testScenarios)(
// "the first date of the week including the date %s should be the date number %s and be day index %s",
// async (todayDateString, startDate, dayOfWeekIndex) => {
// // Arrange
// const { wrapper } = setupMocks({
// propsData: {
// todayOverrideDateString: todayDateString,
// },
// });
// // Act
// const calendarData = await wrapper.vm.calendarData;
// const currentMonthIndex = calendarData.findIndex((month) => {
// console.log("month: ", month);
// return month.monthClass === "currentMonth";
// });
// console.log("calendarData ", calendarData);
// console.log("currentMonthIndex ", currentMonthIndex);
// // Assert
// expect(calendarData[currentMonthIndex].dates[0].dateNum === startDate).toBe(
// true
// );
// expect(
// calendarData[currentMonthIndex].dates[0].firstDateDayIndex ===
// dayOfWeekIndex
// ).toBe(true);
// wrapper.unmount();
// }
// );
// });
// test("the current month's dates array should start with the first day of the week which includes today", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// const hasAFirstInDates = (obj) => obj?.dates[0].dateNum === 1;
// // Act
// const calendarData = await wrapper.vm.calendarData;
// const currentMonthIndex = calendarData.findIndex((month) => {
// return month.monthClass === "currentMonth";
// });
// // Assert
// expect(calendarData[currentMonthIndex].dates[0].dateNum === 1).toBe(true);
// wrapper.unmount();
// });
// test("for each month (other than current month), the dates array should have the 1st of the month", async () => {
// // Arrange
// const { wrapper } = setupMocks({});
// const hasAFirstInDates = (obj) => obj?.dates[0].dateNum === 1;
// // Act
// const calendarData = await wrapper.vm.calendarData;
// // Assert
// expect(calendarData.every(hasAFirstInDates)).toBe(true);
// wrapper.unmount();
// });
// describe("for the current month in calendarData, the dates array should have correct days of month", () => {
// const testScenarios = [
// ["2022-02-05T03:00:00", 28, true],
// ["2024-02-05T03:00:00", 29, true],
// ["2020-01-15T03:00:00", 31, true],
// ];
// test.each(testScenarios)(
// "the current month including the date %s should have %s days in its dates array",
// async (dateString, noOfDays) => {
// // Arrange
// const { wrapper } = setupMocks({
// propsData: {
// todayOverrideDateString: dateString,
// },
// });
// // Act
// const calendarData = await wrapper.vm.calendarData;
// const currentMonthIndex = calendarData.findIndex((month) => {
// return month.monthClass === "currentMonth";
// });
// // Assert
// expect(calendarData[currentMonthIndex].dates.length === noOfDays).toBe(true);
// wrapper.unmount();
// }
// );
// // test.each(testScenarios)(
// // "the date %s should have %s days in dates array",
// // async (dateString, noOfDays) => {
// // // Arrange
// // const { wrapper } = setupMocks({
// // propsData: {
// // todayOverrideDateString: dateString,
// // },
// // });
// // const hasCorrectNumberOfDates = (obj) => obj?.dates.length === noOfDays;
// // // Act
// // const calendarData = await wrapper.vm.calendarData;
// // // Assert
// // expect(calendarData.every(hasCorrectNumberOfDates)).toBe(true);
// // wrapper.unmount();
// // }
// // );
// });
// // describe("for each object in array, the dates array should have correct days of month", () => {
// // const testScenarios = [
// // ["2022-02-05T03:00:00", 28, true],
// // ["2024-02-05T03:00:00", 29, true],
// // ["2020-01-15T03:00:00", 31, true],
// // ];
// // test.each(testScenarios)(
// // "the date %s should have %s days in dates array",
// // async (dateString, noOfDays) => {
// // // Arrange
// // const { wrapper } = setupMocks({
// // propsData: {
// // todayOverrideDateString: dateString,
// // },
// // });
// // const hasCorrectNumberOfDates = (obj) => obj?.dates.length === noOfDays;
// // // Act
// // const calendarData = await wrapper.vm.calendarData;
// // // Assert
// // expect(calendarData.every(hasCorrectNumberOfDates)).toBe(true);
// // wrapper.unmount();
// // }
// // );
// // });
// // test.only("each object in array should have a monthLabel", () => {
// // // Arrange
// // const { wrapper } = setupMocks({});
// // // const testTodayDate = new Date("01-26-2023");
// // // Act
// // const testResult = wrapper.vm.calendarData;
// // const containsMonthLabel = (month) => Object.hasOwn(month, 'monthLabel');
// // // Assert
// // expect(testResult.every(containsMonthLabel)).toBe(true);
// // });
// // test.only("array should have a month object with same month as today", () => {
// // // Arrange
// // const { wrapper } = setupMocks({});
// // // const testTodayDate = new Date("01-26-2023");
// // // Act
// // // const testResult = wrapper.vm.calendarData;
// // console.log("wrapper.vm.calendarData: ", wrapper.vm.calendarData)
// // // Assert
// // // expect(wrapper.vm.calendarData).toEqual("something");
// // // expect(wrapper.vm.calendarDataOld).toEqual("something");
// // expect(wrapper.vm.calendarData[1]).toMatchObject({
// // "dates": [
// // {
// // "dateNum": 1
// // },
// // {
// // "dateNum": 2
// // },
// // {
// // "dateNum": 3
// // },
// // {
// // "dateNum": 4
// // },
// // {
// // "dateNum": 5
// // },
// // {
// // "dateNum": 6
// // },
// // {
// // "dateNum": 7
// // },
// // {
// // "dateNum": 8
// // },
// // {
// // "dateNum": 9
// // },
// // {
// // "dateNum": 10
// // },
// // {
// // "dateNum": 11
// // },
// // {
// // "dateNum": 12
// // },
// // {
// // "dateNum": 13
// // },
// // {
// // "dateNum": 14
// // },
// // {
// // "dateNum": 15
// // },
// // {
// // "dateNum": 16
// // },
// // {
// // "dateNum": 17
// // },
// // {
// // "dateNum": 18
// // },
// // {
// // "dateNum": 19
// // },
// // {
// // "dateNum": 20
// // },
// // {
// // "dateNum": 21
// // },
// // {
// // "dateNum": 22
// // },
// // {
// // "dateNum": 23
// // },
// // {
// // "dateNum": 24
// // },
// // {
// // "dateNum": 25
// // },
// // {
// // "dateNum": 26
// // },
// // {
// // "dateNum": 27
// // },
// // {
// // "dateNum": 28
// // },
// // {
// // "dateNum": 29
// // },
// // {
// // "dateNum": 30
// // },
// // {
// // "dateNum": 31
// // }
// // ],
// // "monthLabel": "January",
// // "yearNum": 2023
// // });
// // wrapper.unmount();
// // });
// });
});
function setupMocks(mountOptionsMockData = {}) {
const initialMountOptionsMockData = {
// router: {
// navigate: jest.fn(),
// navigateWithSaving: jest.fn(),
// navigateWithoutSaving: jest.fn(),
// },
// actionList: [
// // {
// // actionName: storeActions.SAVE_PART_QUESTION_ANSWERS,
// // data: {},
// // },
// // {
// // actionName: storeActions.GET_PARTS,
// // data: {},
// // },
// ],
// store: {
// // getters: store.getters,
// // commit: store.commit,
// },
};
const mountOptions = getMountOptions(
Object.assign(initialMountOptionsMockData, mountOptionsMockData)
);
// console.log("mountOptions: ", mountOptions)
const wrapper = shallowMount(datePicker, mountOptions);
return { wrapper };
}

View file

@ -174,9 +174,8 @@ export default {
return this.todayDateNum + (6 - this.todayDayIndex);
},
calendarViewDirection() {
if (this.selectableDatesSetting === "past") return "past";
if (this.selectableDatesSetting === "custom") return "future";
return "none";
return "past";
},
selectedDate: {
get() {
@ -428,7 +427,7 @@ export default {
} else if (direction === "past") {
// first 0, then -1
for (let i = 0; i >= 0 - monthsBeforeToLoadOffset; i--) {
months.unshift(this.getMonthData(i, options));
months.unshift(await this.getMonthData(i, options));
}
} else {
// TODO - IF A CALENDAR WITH BOTH PAST AND FUTURE WAS EVER NEEDED
@ -610,7 +609,7 @@ export default {
// find the first day-hidden to become the next api call start date
monthStartDateNum =
monthToShow.dates.find(({ dayClasses }) => dayClasses.includes("day-hidden"))
.dateNum - 1; // TRY 2
.dateNum - 1;
} else {
if (this.calendarViewDirection === "future") {
monthToShow = this.months.find((month) =>
@ -646,11 +645,12 @@ export default {
this.$store.getters.order.serviceLocation.appointmentType,
this.$store.getters.order.serviceLocation.provider.providerNumber
);
moreSelectableDates.days.forEach((selectableDate) => {
const index = this.selectableDatesData.findIndex(
(dateObj) => dateObj.date === selectableDate.date
);
if (index === -1) this.selectableDatesData.push(selectableDate.date);
if (index === -1) this.selectableDatesData.push(selectableDate);
this.months.forEach((month) => {
// TODO: avoid checking all calendar dates; maybe only ones between monthStart and monthEnd as defined above?
month.dates.forEach((date) => {

View file

@ -8,7 +8,7 @@
:mask="mask"
:isRequired="isRequired"
:validationRules="validationRulesForTextBoxQuestion"
cmsWidgetName="PhoneNumberQuestionWidget" />
:cmsWidgetName="cmsWidgetName" />
</div>
</template>

View file

@ -18,6 +18,7 @@ describe("vehicleBanner", () => {
const { wrapper } = setupMocks({
displayGenericVehicleImageProp: true,
imageUrlValue: "NULL",
displayUnmatchedVehicleIconProp: true,
});
// Assert
@ -87,7 +88,12 @@ describe("vehicleBanner", () => {
);
});
function setupMocks({ displayGenericVehicleImageProp, imageUrlValue, categoryValue = "CAR" }) {
function setupMocks({
displayGenericVehicleImageProp,
displayUnmatchedVehicleIconProp,
imageUrlValue,
categoryValue = "CAR",
}) {
const mockGetCmsContent = jest.fn();
mockGetCmsContent((cmsWidget, field) => {
return field;
@ -108,7 +114,10 @@ function setupMocks({ displayGenericVehicleImageProp, imageUrlValue, categoryVal
});
//Mock props
mountOptions.propsData = { displayGenericVehicleImage: displayGenericVehicleImageProp };
mountOptions.propsData = {
displayGenericVehicleImage: displayGenericVehicleImageProp,
displayUnmatchedVehicleIconProp: displayUnmatchedVehicleIconProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(vehicleBanner, mountOptions);

View file

@ -13,16 +13,34 @@ export default {
required: true,
},
cmsWidgetName: String,
displayVehicleImage: {
type: String,
required: false,
default: null,
},
displayUnmatchedVehicleIcon: {
type: Boolean,
required: false,
default: false,
},
vehicleCategory: {
type: String,
required: false,
default: null,
},
},
computed: {
vehicleImageToDisplay() {
if (this.displayVehicleImage !== null) {
return this.displayVehicleImage;
}
if (this.displayGenericVehicleImage) {
return this.genericVehicleImage;
}
if (
this.$store.getters.vehicle.imageUrl === null ||
this.$store.getters.vehicle.imageUrl === "NULL"
this.$store.getters.vehicle.imageUrl === "NULL" ||
this.displayUnmatchedVehicleIcon
) {
return this.getUnmatchedVehicleIcon();
}
@ -50,7 +68,8 @@ export default {
},
methods: {
getUnmatchedVehicleIcon() {
switch (this.$store.getters.vehicle.category) {
const category = this.vehicleCategory ?? this.$store.getters.vehicle.category;
switch (category) {
case this.vehicleCategories.CAR:
return this.carUnmatchedVehicleIcon;
case this.vehicleCategories.SUV:

View file

@ -31,7 +31,8 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
// Return that page, so that it can navigate like normal.
if (
toRoute.query[queryStrings.FMG_PAGE] !== undefined &&
toRoute.query[queryStrings.FMG_PAGE] !== fmgPageValues.QUOTE &&
toRoute.query[queryStrings.FMG_PAGE] !== fmgPageValues.SERVICE_LOCATION &&
toRoute.query[queryStrings.FMG_PAGE] !== fmgPageValues.SCHEDULE &&
!isVinRelatedPage(toRoute)
) {
return overrideYmmsDirectionIfNeeded(toRoute);
@ -102,9 +103,6 @@ async function getLatestPageForRedirection() {
// If this is a non-CTA navigation, determine where to send the user based on page prerequisites.
// This also works if a user has a 'fmg' start_type query string but no current order.
// That shouldn't happen, but it's possible.
const vehicleMakeComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_MAKE);
const vehicleModelComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_MODEL);
const vehicleStyleComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_STYLE);
const vehicleDamageComponent = await getLazyLoadedComponent(fmgPageValues.VEHICLE_DAMAGE);
const estimateComponent = await getLazyLoadedComponent(fmgPageValues.ESTIMATE);
const vinLookupComponent = await getLazyLoadedComponent(fmgPageValues.VIN_LOOKUP);
@ -115,21 +113,21 @@ async function getLatestPageForRedirection() {
fmgPageValues.CAPABILITY_QUESTIONS
);
const quoteComponent = await getLazyLoadedComponent(fmgPageValues.QUOTE);
const serviceLocationComponent = await getLazyLoadedComponent(fmgPageValues.SERVICE_LOCATION);
const scheduleComponent = await getLazyLoadedComponent(fmgPageValues.SCHEDULE);
const skipVin = await skipVinLookup();
if (!vehicleMakeComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_YEAR;
} else if (!vehicleModelComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MAKE;
} else if (!vehicleStyleComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_MODEL;
} else if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_STYLE;
if (!vehicleDamageComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE;
} else if (!estimateComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.VEHICLE_DAMAGE;
} else {
if (quoteComponent.methods.arePagePrerequisitesValid()) {
if (scheduleComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.SCHEDULE;
} else if (serviceLocationComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.SERVICE_LOCATION;
} else if (quoteComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.QUOTE;
} else if (capabilityQuestionsComponent.methods.arePagePrerequisitesValid()) {
return fmgPageValues.CAPABILITY_QUESTIONS;
@ -163,10 +161,7 @@ function overrideYmmsDirectionIfNeeded(toRoute) {
if (store.getters.payment.insuranceCoverage.isVerified) {
switch (fmgPageValue) {
case fmgPageValues.VEHICLE_YEAR:
case fmgPageValues.VEHICLE_MAKE:
case fmgPageValues.VEHICLE_MODEL:
case fmgPageValues.VEHICLE_STYLE: {
case fmgPageValues.VEHICLE: {
return fmgPageValues.VEHICLE_DAMAGE;
}
default: {

View file

@ -20,7 +20,7 @@ jest.mock("@/router/dynamic-routing/component-loader.js", () => ({
}));
describe("getPageToRouteExistingOrderTo", () => {
test("should return vehicle-year", async () => {
test("should return vehicle", async () => {
// Arrange
const toRoute = {
query: {},
@ -31,75 +31,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_YEAR);
});
test("should return vehicle-make", async () => {
// Arrange
const toRoute = {
query: {},
};
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
store.commit(storeMutations.UPDATE_MAKE, "acura");
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_MAKE);
});
test("should return vehicle-model", async () => {
// Arrange
const toRoute = {
query: {},
};
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
store.commit(storeMutations.UPDATE_MAKE, "acura");
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: false,
});
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_MODEL);
});
test("should return vehicle-style", async () => {
// Arrange
const toRoute = {
query: {},
};
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
store.commit(storeMutations.UPDATE_MAKE, "acura");
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: false,
});
@ -107,7 +39,7 @@ describe("getPageToRouteExistingOrderTo", () => {
const result = await getPageToRouteExistingOrderTo(toRoute, false);
//Assert
expect(result).toBe(fmgPageValues.VEHICLE_STYLE);
expect(result).toBe(fmgPageValues.VEHICLE);
});
test("should return vehicle-damage", async () => {
@ -121,9 +53,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: false,
});
@ -156,9 +86,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
@ -186,9 +114,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
@ -216,9 +142,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: true,
@ -246,9 +170,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
@ -276,9 +198,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,
@ -306,9 +226,7 @@ describe("getPageToRouteExistingOrderTo", () => {
// Mock out the lazy load calls for all components.
mockLazyLoadComponentReturnValues({
[fmgPageValues.VEHICLE_MAKE]: true,
[fmgPageValues.VEHICLE_MODEL]: true,
[fmgPageValues.VEHICLE_STYLE]: true,
[fmgPageValues.VEHICLE]: true,
[fmgPageValues.VEHICLE_DAMAGE]: true,
[fmgPageValues.ESTIMATE]: true,
[fmgPageValues.CAPABILITY_QUESTIONS]: false,

View file

@ -114,6 +114,7 @@ async function saveSessionHelper() {
storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,
{
referralNumber: savedSessionInfo.data.referralNumber?.toString(),
referralSequenceNumber: savedSessionInfo.data.referralSequenceNumber,
referralCorrelationId: savedSessionInfo.data.referralCorrelationId,
referralDate: savedSessionInfo.data.referralDate,
parentAccountNumber: savedSessionInfo.data.parentAccountNumber?.toString(),

View file

@ -0,0 +1,10 @@
export function getQuerystringParameter(key) {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const lowerCaseParams = new URLSearchParams();
for (const [name, value] of urlParams) {
lowerCaseParams.append(name.toLowerCase(), value);
}
return lowerCaseParams.get(key) ? lowerCaseParams.get(key) : null;
}

View file

@ -681,7 +681,6 @@ function setupMocks({
},
],
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),

View file

@ -323,10 +323,6 @@ export default {
registrationInfo: {
firstName: this.customerQuestions.firstName,
lastName: this.customerQuestions.lastName,
address: this.customerQuestions.addressQuestions.streetAddress,
city: this.customerQuestions.addressQuestions.city,
state: resultMap.serviceZipValidationResponse.state,
zipCode: this.customerQuestions.addressQuestions.zipCode,
},
},
false

View file

@ -25,14 +25,14 @@
validationRules="email-address-required|email-address-format" />
<phoneNumberQuestion
class="mb-4"
cmsWidgetName="phoneNumberQuestionWidget"
cmsWidgetName="PhoneNumberQuestionWidget"
v-model="phoneNumber"
isRequired
validationRules="phone-number-required" />
<checkboxQuestion
class="mb-5"
cmsWidgetName="TextMeQuestionWidget"
v-model="isSmsOptin" />
v-model="isSmsOptIn" />
<textareaQuestion
class="mb-4"
v-model="techNotes"
@ -110,7 +110,7 @@ export default {
lastName: this.getLastNameFromStore(),
emailAddress: this.getEmailAddressFromStore(),
phoneNumber: this.getPhoneNumberFromStore(),
isSmsOptin: this.getIsSmsOptinFromStore(),
isSmsOptIn: this.getIsSmsOptInFromStore(),
techNotes: this.getTechNotesFromStore(),
};
},
@ -135,8 +135,8 @@ export default {
getPhoneNumberFromStore() {
return store.getters.order.customer.phoneNumber;
},
getIsSmsOptinFromStore() {
return store.getters.order.customer.isSmsOptin;
getIsSmsOptInFromStore() {
return store.getters.order.customer.isSmsOptIn;
},
getTechNotesFromStore() {
return store.getters.order.serviceLocation.techNotes;
@ -152,7 +152,7 @@ export default {
lastName: this.lastName,
emailAddress: this.emailAddress,
phoneNumber: this.phoneNumber,
isSmsOptin: this.isSmsOptin,
isSmsOptIn: this.isSmsOptIn,
},
false
);

View file

@ -7,10 +7,7 @@
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div v-if="!skipVin">
<alert
class="vinLookupMethodHeading"
cmsWidgetName="AlertVinLookupQuestion"
alertClass="" />
<div class="vinlookupquestion" v-html="this.VinLookupQuestionText"></div>
<buttonQuestion
cmsWidgetName="VinLookupMethod"
:answers="answersFromCms"
@ -333,6 +330,9 @@ export default {
alertInfo() {
return this.skipVinNotRepair ? "AlertQuoteVinOptional" : "AlertQuoteReady";
},
VinLookupQuestionText() {
return this.getCmsContent("VinLookupQuestion", "BodyText");
},
},
watch: {
serviceZipCode() {
@ -355,13 +355,11 @@ export default {
</script>
<style lang="scss">
.vinLookupMethodHeading p {
font-size: 14px;
font-weight: 400;
color: $black;
strong {
font-size: 1rem;
font-weight: 500;
}
.vinlookupquestion p {
margin-bottom: 0;
}
.vinlookupquestion p:nth-child(2) {
font-size: 0.875rem;
margin-bottom: 1rem;
}
</style>

View file

@ -292,8 +292,6 @@ export default {
vehicleInfo: Object.assign(vinLookup.data.vehicle, { vin: vinLookup.data.vin }),
registrationInfo: {
licensePlate: this.licensePlate,
state: resultMap.registrationZipValidationResponse.state,
zipCode: this.registrationZipCode,
},
},
false

View file

@ -263,6 +263,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null,
},
serviceLocation: {
state: null,
},
},
};
wrapper.vm.$route = { query: { isInsurance: "true" } };
@ -290,6 +293,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null,
},
serviceLocation: {
state: null,
},
},
};
wrapper.vm.$route = { query: { isInsurance: "false" } };
@ -317,6 +323,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: true,
},
serviceLocation: {
state: null,
},
},
};
// Ensure that query param isn't overriding selection
@ -345,6 +354,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: false,
},
serviceLocation: {
state: null,
},
},
};
// Ensure that query param isn't overriding selection
@ -374,6 +386,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
serviceLocation: {
state: null,
},
},
};
mockTierOnePrice = 200;
@ -404,6 +419,9 @@ describe("quote.vue", () => {
payment: {
isInsurance: null, // Ensure that previous selection isn't overriding selection
},
serviceLocation: {
state: null,
},
},
};
mockTierOnePrice = 505;
@ -421,6 +439,33 @@ describe("quote.vue", () => {
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(true);
});
test("Should default to insurence if user Service zip is from certain States", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
lineItems: {
glassParts: ["item", "item2"],
},
payment: {
isInsurance: null,
},
serviceLocation: {
state: "AZ",
},
},
};
wrapper.vm.$route = { query: null };
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
expect(wrapper.vm.isInsuranceSelected).toBe(true);
});
});
function setupMocks({ customMountOptions }) {

View file

@ -67,7 +67,7 @@ import { errorMessages } from "@/constants/error-messages";
import { Form, defineRule } from "vee-validate";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { applicationConfig } from "@/constants/application-config";
import { payWithInsuranceStates } from "@/constants/pay-with-insurance-states";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
@ -154,11 +154,18 @@ export default {
);
},
getDefaultIsInsuranceSelectedValue(availableLineItems) {
const serviceLocationState = store.getters.order.serviceLocation.state;
// Override if coming back from QuoteDetails. Remove override after Quote release
const isInsuranceOverrideValue = this.$route.query?.isInsurance;
const defaultIsInsuranceSelectedValue = this.$store.getters.order.payment.isInsurance;
if (isInsuranceOverrideValue != null) {
//Default to Insurance Tab if user Service zip is from certain States
if (
serviceLocationState != null &&
payWithInsuranceStates.find((item) => item === serviceLocationState)
)
return true;
else if (isInsuranceOverrideValue != null) {
return isInsuranceOverrideValue == "true";
} else if (defaultIsInsuranceSelectedValue != null) {
return defaultIsInsuranceSelectedValue;

View file

@ -135,64 +135,9 @@ describe("service-package-question.vue", () => {
expectedModifiedAnswers[packageNameKey].tierThree
);
});
it("should not select a default package if ANY glass part or supporting item has no prices", async () => {
it("should not select a default package if isInsurance is null", async () => {
// Note, only the first WSREPAIR item has a 0 for laborAmount, this is enough to not try and select a default package
const wrapper = setupMocks({
mountOptionsMockData: {
store: {
getters: {
lineItems: {
glassParts: [
{
partNumber: "FW02627GBYNOEE",
description: "solar, 3rd visor band",
color: "Green Tint, Blue Shade",
partType: "WINDSHIELD",
canSafeliteRecalibrate: false,
requiresRecalibration: false,
requiresCapabilityQuestions: false,
recalibrationType: null,
childParts: null,
laborAmount: 5.0,
},
],
supportingItems: [
{
partNumber: "SUPPLIES-REPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
],
},
order: {
damage: {
isRepair: false,
glassToReplace: [{ glassLocation: "Windshield" }],
},
},
},
},
},
});
const wrapper = setupMocks({});
// Act
wrapper.setProps({ availableLineItems: null });
@ -202,59 +147,24 @@ describe("service-package-question.vue", () => {
// Assert
expect(wrapper.vm.selectedPackageName).toBe(null);
});
it("should select a default package if ALL glass parts and supporting items have prices", async () => {
it("should select a default package if isInsurance is NOT null", async () => {
const wrapper = setupMocks({
mountOptionsMockData: {
store: {
getters: {
lineItems: {
glassParts: [
{
partNumber: "FW02627GBYNOEE",
description: "solar, 3rd visor band",
color: "Green Tint, Blue Shade",
partType: "WINDSHIELD",
canSafeliteRecalibrate: false,
requiresRecalibration: false,
requiresCapabilityQuestions: false,
recalibrationType: null,
childParts: null,
laborAmount: 5.0,
},
],
supportingItems: [
{
partNumber: "SUPPLIES-REPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
{
partNumber: "WSREPAIR",
description: null,
partType: "REPAIR FEE",
laborAmount: 5.0,
},
],
},
order: {
damage: {
isRepair: false,
glassToReplace: [{ glassLocation: "Windshield" }],
},
},
lineItems: {
vaps: [],
},
hasAnyNonWindshieldGlassParts: false,
payment: {
isInsurance: false,
},
},
},
},
@ -329,6 +239,9 @@ describe("service-package-question.vue", () => {
glassToReplace: [{ glassLocation: "Windshield" }],
},
},
payment: {
isInsurance: false,
},
},
},
},
@ -758,6 +671,9 @@ function setupMocks({ mountOptionsMockData, props = mockProps }) {
},
},
hasAnyNonWindshieldGlassParts: false,
payment: {
isInsurance: null,
},
},
},
};

View file

@ -49,7 +49,7 @@ export default {
},
watch: {
availableLineItems() {
if (this.allGlassPartsAndSupportingItemsHavePrices(this.$store.getters.lineItems)) {
if (this.$store.getters.payment.isInsurance != null) {
this.selectDefaultPackage();
}
},
@ -196,30 +196,6 @@ export default {
vapsFromStore
);
},
allGlassPartsAndSupportingItemsHavePrices(lineItems) {
if (lineItems?.glassParts) {
for (let i = 0; i < lineItems.glassParts.length; i++) {
if (this.priceIsNullOrZero(lineItems.glassParts[i])) {
return false;
}
}
}
if (lineItems?.supportingItems) {
for (let i = 0; i < lineItems.supportingItems.length; i++) {
if (this.priceIsNullOrZero(lineItems.supportingItems[i])) {
return false;
}
}
}
return true;
},
priceIsNullOrZero(lineItem) {
return (
(lineItem.kitPrice == null || lineItem.kitPrice == 0) &&
(lineItem.laborAmount == null || lineItem.laborAmount == 0) &&
(lineItem.sellingPrice == null || lineItem.sellingPrice == 0)
);
},
getVapsLineItemsForSelectedPackage(packageName) {
const packageContentTypes = getPackageContents(
this.glassToReplace,

View file

@ -3,6 +3,7 @@
<div class="d-flex">
<textBlock
:cmsWidgetName="headerCmsWidgetName"
:customText="customHeaderText"
typeStyle="body small bold dark"
marginTopSizeOverride="0" />
<textLink
@ -26,9 +27,10 @@ import textBlock from "@/digital-components/text-block/text-block";
import textLink from "@/ux-components/text-link/text-link";
export default {
name: "vehicle-review",
name: "review-block",
props: {
headerCmsWidgetName: String,
customHeaderText: String,
// Specifically an array of strings.
content: Array,
},

View file

@ -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 };
}

View file

@ -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>

View file

@ -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 };
}

View file

@ -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>

View file

@ -0,0 +1,40 @@
<template>
<reviewBlock
:customHeaderText="headerText"
:content="displayContent"
@edit-clicked="editClicked" />
</template>
<script>
import reviewBlock from "@/layouts/review/review-block/review-block";
export default {
name: "schedule-review",
props: {
cmsWidgetName: String,
appointmentType: String,
},
data() {
return {};
},
methods: {
editClicked() {
this.$emit("edit-clicked");
},
},
computed: {
displayContent() {
return [
this.getCmsContent(this.cmsWidgetName, "BodyText"),
this.getCmsContent(this.cmsWidgetName, "BodyText2"),
];
},
headerText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
},
components: {
reviewBlock,
},
};
</script>

View file

@ -10,7 +10,7 @@ import reviewBlock from "@/layouts/review/review-block/review-block";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
export default {
name: "service-package-review",
name: "service-location-review",
props: {
cmsWidgetName: String,
serviceLocation: Object,

View file

@ -10,13 +10,13 @@
<textBlock
:customText="subHeaderTitle"
typeStyle="h5"
justifyText="justify-content-center"
justifyText="center"
margin="mt-1"
class="dark-header" />
<textBlock
:customText="subHeaderBody"
typeStyle="body"
justifyText="justify-content-left"
justifyText="left"
margin="mt-0 mb-2" />
<buttonMain
@ -69,12 +69,30 @@
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
<hr class="my-0" />
<scheduleReview
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"
@ -94,22 +112,19 @@ import vehicleReview from "@/layouts/review/review-sections/vehicle-review/vehic
import damageReview from "@/layouts/review/review-sections/damage-review/damage-review";
import servicePackageReview from "@/layouts/review/review-sections/service-package-review/service-package-review";
import 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";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
export default {
name: "review",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Get rain defense and wiper availability for package review section.
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS);
const rainDefensePromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_RAIN_DEFENSE
);
const servicePackageInitialDataPromise = servicePackageReview.methods.loadInitialData();
@ -168,6 +183,15 @@ export default {
this.$route
);
},
editSchedule() {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_SCHEDULE_EDIT,
this.$route
);
},
editCustomerDetails() {
this.$refs.customerDetailsModalQuestion.openModal();
},
},
computed: {
subHeaderTitle() {
@ -191,6 +215,12 @@ export default {
serviceLocationInfo() {
return this.$store.getters.order.serviceLocation;
},
appointmentType() {
return this.$store.getters.order.serviceLocation.appointmentType;
},
customerInfo() {
return this.$store.getters.order.customer;
},
},
components: {
funnelHeader,
@ -202,6 +232,9 @@ export default {
damageReview,
servicePackageReview,
serviceLocationReview,
scheduleReview,
customerReview,
customerDetailsModalQuestion,
},
};
</script>

View file

@ -45,3 +45,33 @@ export function sumDateString(dateString, daysToAdd) {
date.setDate(date.getDate() + daysToAdd);
return convertDateToDateString(date);
}
export function militaryToTwelveHourTime(timeString) {
// Expected input: "HH:MM"
if (typeof timeString !== "string") return;
let hours = parseInt(timeString.split(":")[0]);
const minutes = timeString.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
return `${hours}:${minutes} ${meridianNotation}`;
}
export function getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
const isLongAppointment = durationMaximum >= 120;
const isDurationRange = durationMinimum !== durationMaximum;
const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum;
const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum;
const durationText = isDurationRange
? `${adjustedMinimum} - ${adjustedMaximum}`
: adjustedMinimum;
const unitText = isLongAppointment ? "hours" : "minutes";
return `${durationText} ${unitText}`;
}

View file

@ -1,15 +1,77 @@
// Components
import schedule from "@/layouts/schedule/schedule.vue";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { applicationConfig } from "@/constants/application-config";
import { storeActions } from "@/constants/store-actions";
import schedule from "@/layouts/schedule/schedule.vue";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
import router from "@/router";
import { nextTick } from "vue";
import baseMixin from "../../mixins/base-mixin";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
// Mock basemixin
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
dispatchStoreAction: jest.fn().mockImplementation((storeAction) => {
if (storeAction === "getShopTimeSlots") {
return {
data: {
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
days: [
{
date: "2023-12-01",
timeSlots: [
{
id: "06747-01820-S-B*20424*7 AM",
startTime: "07:00",
endTime: "08:00",
offerPremium: false,
},
],
},
],
},
};
}
if (storeAction === "getMobilePremiumFee") {
return Promise.resolve({
data: {
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
});
}
if (storeAction === "priceOrderItemsAndSaveServerData") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
if (storeAction === "saveSupportingItemsSuppressingStateResetting") {
return Promise.resolve([
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 14.99,
kitPrice: 0,
},
]);
}
}),
},
}));
// Mock fetchCmsContentForPage
@ -18,50 +80,402 @@ jest.mock("@/helpers/cms-content-helper", () => ({
splitCopyOnCMSPlaceHolder: jest.fn(() => ["A", "B"]),
}));
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
store.getters = {
order: {
schedule: {
date: "2020-01-01",
beforeEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
store.getters = {
order: {
schedule: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: "000",
},
lineItems: {
glassParts: [
{
partNumber: "ABC123",
},
],
supportingItems: [],
},
serviceLocation: {
appointmentType: "Inshop",
zipCode: "12345",
zipCodeCtu: "01234",
provider: {
providerNumber: "123",
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
glassParts: [],
supportingItems: [],
},
serviceLocation: {
appointmentType: "Inshop",
},
},
lineItems: {
glassParts: [],
supportingItems: [],
},
};
};
});
afterEach(() => {
store.getters = {};
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe("schedule.vue", () => {
test("Should navigateWithoutSaving", async () => {
//Arrange
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateWithSaving: jest.fn(),
},
route: { schedule },
},
describe("schedule.vue...", () => {
describe("initial load", () => {
test("should pass arePagePrerequisitesValid with a mobile order and no providerNumber", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.order.serviceLocation.provider = null;
//Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should pass arePagePrerequisitesValid with a inshop order and providerNumber", () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("should fail arePagePrerequisitesValid with a replace with no glass parts", async () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters.order.lineItems.glassParts = [];
//Act
const arePagePrerequisitesValid2 = await wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid2).toBe(false);
});
test("should fail arePagePrerequisitesValid without isInsurance", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters.payment.isInsurance = null;
//Act
const arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test("should return newShopTimeSlots when getAvailableDatesMethod is called", async () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
const newShopTimeSlots = await wrapper.vm.getAvailableDatesMethod(
"2023-01-01",
"2023-01-31"
);
//Assert
expect(newShopTimeSlots).toStrictEqual({
days: [
{
date: "2023-12-01",
timeSlots: [
{
endTime: "08:00",
id: "06747-01820-S-B*20424*7 AM",
offerPremium: false,
startTime: "07:00",
},
],
},
],
estimatedServiceMinutesMinimum: 90,
estimatedServiceMinutesMaximum: 120,
});
});
test("should call API service in day ranges of 34 or less when getAvailableDatesMethod is called with large date ranges", async () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
await wrapper.vm.getAvailableDates.call(
wrapper.vm,
"2023-01-01",
"2023-03-31",
"Inshop",
"123"
);
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledTimes(3);
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
"getShopTimeSlots",
expect.anything(),
expect.anything()
);
});
describe("beforeRouteEnter function... ", () => {
test("should call next() and call all functions within next", async () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
wrapper.vm.updateFooterButtonText = jest.fn();
const nextFunction = jest.fn((c) => {
c(wrapper.vm);
});
//Act
await schedule.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "schedule" } },
undefined,
nextFunction
);
//Assert
expect(nextFunction).toHaveBeenCalled();
expect(wrapper.vm.setCmsContent).toHaveBeenCalledWith("content");
expect(wrapper.vm.$refs.datePicker.initializeComponent).toHaveBeenCalledWith(
expect.objectContaining({
calendarViewDirection: "future",
})
);
expect(wrapper.vm.$refs.locationAlerts.initializeComponent).toHaveBeenCalled();
expect(wrapper.vm.selectableDatesData).toStrictEqual(
expect.objectContaining({
days: expect.any(Array),
estimatedServiceMinutesMaximum: expect.any(Number),
estimatedServiceMinutesMinimum: expect.any(Number),
})
);
expect(wrapper.vm.mobilePremiumAppointmentFee).toStrictEqual(
expect.objectContaining({
partNumber: expect.any(String),
})
);
expect(wrapper.vm.updateFooterButtonText).toHaveBeenCalled();
});
});
describe("computed properties...", () => {
test("timeSlotsForSelectedDate should return timeslots if selected date is available", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [
{
date: "2022-11-11",
timeSlots: [
{
id: "1820I-01820-M-I*20425*AM",
startTime: "08:00",
endTime: "12:00",
offerPremium: true,
},
{
id: "1820I-01820-M-I*20425*PM",
startTime: "12:00",
endTime: "17:00",
offerPremium: false,
},
],
},
],
};
wrapper.setData({
selectedDate: "2022-11-11",
});
//Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
//Assert
expect(testValue).toStrictEqual(
expect.objectContaining({
date: "2022-11-11",
})
);
});
test("timeSlotsForSelectedDate should be null if no date has been selected", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [
{
date: "2022-11-11",
timeSlots: [
{
id: "1820I-01820-M-I*20425*AM",
startTime: "08:00",
endTime: "12:00",
offerPremium: true,
},
{
id: "1820I-01820-M-I*20425*PM",
startTime: "12:00",
endTime: "17:00",
offerPremium: false,
},
],
},
],
};
wrapper.setData({
selectedDate: undefined,
});
//Act
const testValue = wrapper.vm.timeSlotsForSelectedDate;
//Assert
expect(testValue).toBe(null);
});
});
});
describe("schedule page methods...", () => {
test("getServiceZipCtuCodeFromStore should return zipCodeCtu", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
const testValue = wrapper.vm.getServiceZipCtuCodeFromStore();
//Assert
expect(testValue).toStrictEqual("01234");
});
test("openInshopTimeSlotsModal should trigger openModal method", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
wrapper.vm.openInshopTimeSlotsModal();
//Assert
expect(wrapper.vm.$refs.timeSlotModalQuestion.openModal).toBeCalled();
});
test("getSelectedRouteCode should return schedule routeCode", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
//Act
const testValue = wrapper.vm.getSelectedRouteCode();
//Assert
expect(testValue).toStrictEqual("000");
});
test("timeSlotModalClosed should null any selected date when there's no route code", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
wrapper.setData({
selectedDate: "1980-05-05",
});
wrapper.setData({
selectedTimeSlot: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: null,
},
});
//Act
wrapper.vm.timeSlotModalClosed();
//Assert
expect(wrapper.vm.selectedDate).toBe(null);
});
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
const timeInput1 = "15:00";
const timeInput2 = "15:30";
//Act
const testOutput1 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1);
const testOutput2 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2);
const testOutput3 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput1, true);
const testOutput4 = wrapper.vm.getDisplayTextForMilitaryTime(timeInput2, true);
//Assert
expect(testOutput1).toBe("3:00 PM");
expect(testOutput2).toBe("3:30 PM");
expect(testOutput3).toBe("3 PM");
expect(testOutput4).toBe("3:30 PM");
});
test("getDisplayTextForMilitaryTime should return the correctly formatted string", () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.selectableDatesData = {
days: [],
};
wrapper.vm.$router.navigateWithoutSaving = jest.fn();
wrapper.vm.$route = "testRoute";
//Act
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toBeCalledWith(
"CLICKED_BACK",
"testRoute"
);
});
});
test("forwardButtonAction should call route method navigateWithoutSaving", async () => {
//Arrange
const { wrapper } = setupMocks({});
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.$router.navigateWithSaving = jest.fn(() => {
return {};
});
//Act
await wrapper.vm.forwardButtonAction();
@ -69,136 +483,123 @@ describe("schedule.vue", () => {
//Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
test("should pass arePagePrerequisitesValid with a mobile order and no providerNumber", () => {
test("for mobile appts, updateSupportingItems should call store action to save supporting items", async () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.lineItems.supportingItems = [
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 0,
kitPrice: 0,
},
];
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Mobile",
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.mobilePremiumAppointmentFee = 14.99;
wrapper.setData({
selectedTimeSlot: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: null,
isPremiumAppointment: true,
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
},
};
});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Act
await wrapper.vm.updateSupportingItems();
expect(arePagePrerequisitesValid).toBe(true);
//Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
"saveSupportingItemsSuppressingStateResetting",
expect.arrayContaining([
expect.objectContaining({
partType: "EARLY BIRD",
}),
]),
expect.anything()
);
});
test("should pass arePagePrerequisitesValid with a inshop order and providerNumber", () => {
test("for Inshop appts, updateSupportingItems should call store action to save supporting items WITHOUT the EARLY BIRD supporting item", async () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = "Inshop";
store.getters.lineItems.supportingItems = [
{
partNumber: "EARLY BIRD",
description: null,
partType: "EARLY BIRD",
laborAmount: 0,
sellingPrice: 0,
kitPrice: 0,
},
];
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.mobilePremiumAppointmentFee = 14.99;
wrapper.setData({
selectedTimeSlot: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: null,
isPremiumAppointment: true,
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
},
};
});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Act
await wrapper.vm.updateSupportingItems();
expect(arePagePrerequisitesValid).toBe(true);
//Assert
expect(wrapper.vm.dispatchStoreAction).toBeCalledWith(
"saveSupportingItemsSuppressingStateResetting",
expect.not.arrayContaining([
expect.objectContaining({
partType: "EARLY BIRD",
}),
]),
expect.anything()
);
});
test("should fail arePagePrerequisitesValid with a replace with no glass parts", () => {
test("if no EARLY BIRD supporting item, then updateSupportingItems should NOT call store action", async () => {
//Arrange
store.getters.order.serviceLocation.appointmentType = "Mobile";
store.getters.lineItems.supportingItems = [];
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
wrapper.vm.dispatchStoreAction = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.mobilePremiumAppointmentFee = 14.99;
wrapper.setData({
selectedTimeSlot: {
date: "2019-01-01",
startTime: "09:00",
endTime: "10:00",
routeCode: null,
isPremiumAppointment: false,
},
payment: {
isInsurance: true,
},
lineItems: {
supportingItems: [],
glassParts: [],
},
};
});
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Act
await wrapper.vm.updateSupportingItems();
expect(arePagePrerequisitesValid).toBe(false);
});
test("should fail arePagePrerequisitesValid without isInsurance", () => {
//Arrange
const { wrapper } = setupMocks({});
store.getters = {
order: {
schedule: {
date: "2020-01-01",
},
serviceLocation: {
zipCode: "12345",
zipCodeCtu: "value",
appointmentType: "Inshop",
provider: {
providerNumber: "5",
},
},
damage: {
isRepair: true,
},
referralNumber: "1234567",
},
payment: {
isInsurance: null,
},
lineItems: {
supportingItems: [],
glassParts: [],
},
};
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
expect(arePagePrerequisitesValid).toBe(false);
//Assert
expect(wrapper.vm.dispatchStoreAction).not.toBeCalled();
});
});
@ -210,6 +611,7 @@ function setupMocks({ customMountOptions }) {
});
mountOptions.global.mocks["$store"] = store;
mountOptions.global.mocks["$router"] = router;
mountOptions["attachTo"] = document.body;
mountOptions.mixins = [
{
@ -223,6 +625,12 @@ function setupMocks({ customMountOptions }) {
];
const wrapper = shallowMount(schedule, mountOptions);
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.timeSlotModalQuestion.openModal = jest.fn();
return { wrapper };
}

View file

View file

@ -320,6 +320,7 @@ export default {
);
return newShopTimeSlots;
},
getAvailableDates,
getServiceZipCtuCodeFromStore() {
return store.getters.order.serviceLocation.zipCodeCtu;
},
@ -338,12 +339,6 @@ export default {
getSupportingItems() {
return store.getters.lineItems.supportingItems;
},
isMobilePremiumFeeOnOrderInVuex() {
const supportingItemsFromVuex = store.getters.lineItems.supportingItems;
return !!supportingItemsFromVuex.filter(
(lineItem) => lineItem.partType === PREMIUM_FEE_PART_TYPE
).length;
},
timeSlotModalClosed() {
// Clear the selectedDate if no timeSlot has been selected
if (this.selectedTimeSlot.routeCode == null) {
@ -459,6 +454,7 @@ export default {
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
};
}
},

View file

@ -16,6 +16,7 @@ describe("time-slot-modal-list-button-question.vue", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const expectedEmit = [
[
{
@ -24,6 +25,7 @@ describe("time-slot-modal-list-button-question.vue", () => {
startTime: startTime,
endTime: endTime,
jobMaxMinutes: jobMaxMinutes.toString(),
jobMinMinutes: jobMinMinutes.toString(),
},
],
];
@ -32,6 +34,7 @@ describe("time-slot-modal-list-button-question.vue", () => {
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -163,11 +166,13 @@ describe("time-slot-modal-list-button-question.vue Supplemental information", ()
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -201,11 +206,13 @@ describe("time-slot-modal-list-button-question.vue Disclaimer", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -288,11 +295,13 @@ describe("time-slot-modal-list-button-question.vue Duration", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -434,11 +443,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -470,11 +481,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -506,11 +519,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00 AM";
const endTime = "8:30 AM";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -542,11 +557,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [
@ -578,11 +595,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
premiumAppointmentFee: {
partType: PREMIUM_FEE_PART_TYPE,
},
@ -622,11 +641,13 @@ describe("time-slot-modal-list-button-question.vue Available Timeslots", () => {
const startTime = "8:00";
const endTime = "8:30";
const jobMaxMinutes = 100;
const jobMinMinutes = 50;
const { wrapper } = setupMocks({
customMountOptions: {
propsData: {
estimatedServiceMinutesMaximum: jobMaxMinutes,
estimatedServiceMinutesMinimum: jobMinMinutes,
dateAndTimeSlotData: {
date: date,
timeSlots: [

View file

@ -47,7 +47,11 @@ import buttonQuestion from "@/digital-components/button-question/button-question
import timeSlotModalListButton from "./time-slot-modal-list-button/time-slot-modal-list-button";
// Helpers
import { convertDateStringToDate } from "@/layouts/schedule/helpers/schedule-helper";
import {
convertDateStringToDate,
militaryToTwelveHourTime,
getDisplayTextForDurationLength,
} from "@/layouts/schedule/helpers/schedule-helper";
import { deepClone } from "@/helpers/object-helper";
// Validation - TODO: Move this somewhere more global?
@ -88,6 +92,7 @@ export default {
startTime: null,
endTime: null,
jobMaxMinutes: null,
jobMinMinutes: null,
}),
},
cmsWidgetName: String,
@ -263,7 +268,7 @@ export default {
cmsWidgetFieldMappings.DURATION
);
const inshopDurationTime = this.getDisplayTextForDurationLength(
const inshopDurationTime = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
);
@ -346,33 +351,6 @@ export default {
this.$emit("update:modelValue", this.selectedValue);
this.closeModal();
},
getDisplayTextForMilitaryTime(militaryTimeInput) {
// Expected input: "HH:MM"
let hours = parseInt(militaryTimeInput.split(":")[0]);
const minutes = militaryTimeInput.split(":")[1];
const meridianNotation = hours > 11 ? "PM" : "AM";
if (hours > 12) {
hours -= 12;
}
return `${hours}:${minutes} ${meridianNotation}`;
},
getDisplayTextForDurationLength(durationMinimum, durationMaximum) {
const isLongAppointment = durationMaximum >= 120;
const isDurationRange = durationMinimum !== durationMaximum;
const adjustedMinimum = isLongAppointment ? durationMinimum / 60 : durationMinimum;
const adjustedMaximum = isLongAppointment ? durationMaximum / 60 : durationMaximum;
const durationText = isDurationRange
? `${adjustedMinimum} - ${adjustedMaximum}`
: adjustedMinimum;
const unitText = isLongAppointment ? "hours" : "minutes";
return `${durationText} ${unitText}`;
},
getRelevantDropOffCmsWidgetNameForSelectedTimeSlot(
selectedTimeSlotId,
isSameDayRelevant = false
@ -387,7 +365,7 @@ export default {
},
getAvailableTimeSlotsForInshop(timeSlotsForSelectedDate) {
return timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = this.getDisplayTextForMilitaryTime(timeSlot.startTime);
const readableTime = militaryToTwelveHourTime(timeSlot.startTime);
return {
value: timeSlot.id,
buttonLabel: readableTime,
@ -415,9 +393,9 @@ export default {
},
getAvailableTimeSlotsForMobile(timeSlotsForSelectedDate) {
const availableTimeSlots = timeSlotsForSelectedDate.map((timeSlot) => {
const readableTime = `${this.getDisplayTextForMilitaryTime(
const readableTime = `${militaryToTwelveHourTime(
timeSlot.startTime
)} - ${this.getDisplayTextForMilitaryTime(timeSlot.endTime)}`;
)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
return {
value: timeSlot.id,
buttonLabel: readableTime,
@ -472,6 +450,7 @@ export default {
startTime: timeSlot.startTime,
endTime: timeSlot.endTime,
jobMaxMinutes: this.estimatedServiceMinutesMaximum.toString(),
jobMinMinutes: this.estimatedServiceMinutesMinimum.toString(),
};
}
@ -481,6 +460,7 @@ export default {
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
};
},
},

View file

@ -52,7 +52,6 @@ jest.mock("@/store", () => ({
damage: {
glassToReplace: [],
},
order: { serviceLocation: { zipCode: "11111" } },
},
}));
@ -134,7 +133,6 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -177,7 +175,7 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000", zipCode: "11111" }
{ carId: "C00000000" }
);
});
@ -243,7 +241,6 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -273,7 +270,7 @@ describe("vehicle-damage.vue", () => {
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000", zipCode: "11111" }
{ carId: "C00000000" }
);
});
});
@ -526,7 +523,6 @@ describe("vehicle-damage.vue", () => {
eventBusItem: jest.fn(),
damage: { glassToReplace: [{ glassLocation: damageLocation }] },
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getDamageLocationsFromStore();
@ -604,7 +600,6 @@ describe("vehicle-damage.vue", () => {
isRepair: isRepair,
numberOfChips: 2,
},
order: { serviceLocation: { zipCode: "11111" } },
};
var windshieldSelections = wrapper.vm.getWindshieldOptionsFromStore();
@ -641,7 +636,6 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getDriverSideReplaceOptionsFromStore();
@ -678,7 +672,6 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getPassengerSideReplaceOptionsFromStore();
@ -713,7 +706,6 @@ describe("vehicle-damage.vue", () => {
glassToReplace: [{ glassLocation: damageLocation, glassName: damageName }],
},
isRepair: true,
order: { serviceLocation: { zipCode: "11111" } },
};
var glassSelections = wrapper.vm.getRearReplaceOptionsFromStore();
@ -746,7 +738,6 @@ describe("vehicle-damage.vue", () => {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: true } },
order: { serviceLocation: { zipCode: "11111" } },
},
},
},
@ -787,7 +778,6 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo
isVerified: false,
},
},
order: { serviceLocation: { zipCode: "11111" } },
},
},
};

View file

@ -81,7 +81,6 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
import { queryStrings } from "@/constants/query-strings";
// DEFINE VALIDATION RULES
defineRule("replace-options-required", required(errorMessages.REPLACE_OPTIONS_REQUIRED));
@ -90,20 +89,10 @@ export default {
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const lowerCaseParams = new URLSearchParams();
for (const [name, value] of urlParams) {
lowerCaseParams.append(name.toLowerCase(), value);
}
const zip = lowerCaseParams.get(queryStrings.ZIP_CODE)
? lowerCaseParams.get(queryStrings.ZIP_CODE)
: store.getters.order.serviceLocation.zipCode;
const damageOptionsPromise = baseMixin.methods.dispatchStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId, zipCode: zip }
{ carId: store.getters.vehicle.carId }
);
// Settle promises and get results
@ -146,7 +135,6 @@ export default {
},
selectedWindshieldOptions: this.getWindshieldOptionsFromStore(),
selectedRearReplaceOptions: this.getRearReplaceOptionsFromStore(),
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
};
},
mounted() {
@ -169,18 +157,6 @@ export default {
true
);
}
if (this.serviceZipCode) {
this.pushEventToGA(
this.GaCategories.FUNNEL_ENTRY,
this.GaActions.ZIP_CODE_PROVIDED,
this.serviceZipCode,
true
);
}
},
getZipFromStore() {
return this.$store.getters.order.serviceLocation.zipCode;
},
backButtonAction() {

View file

@ -1,79 +0,0 @@
import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("make-question.vue", () => {
test("Selected make is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "honda" });
const makeToSelect = "ford";
//Act
wrapper.setValue({ selectedMake: makeToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedMake: "ford" }]);
});
});
describe("make-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["honda", "ford", "dodge"],
});
//Act
const initialData = makeQuestion.methods.loadInitialData.call(wrapper.vm);
makeQuestion.methods.initializeComponent.call(wrapper.vm, initialData);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe("honda,ford,dodge");
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { year: 2019 } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(makeQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}

View file

@ -1,59 +0,0 @@
<template>
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
selectingInitiatesLoad
:questionText="questionText"
:answers="makes"
groupName="ChooseVehicleMake"
textPosition="text-start"
v-model="selectedValue"
isRequired />
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
// Supporting files
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
export default {
name: "make-question",
data() {
return {
makes: [],
};
},
props: {
modelValue: String,
cmsWidgetName: String,
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
year: store.getters.vehicle.year,
});
},
initializeComponent(initialData) {
this.makes = initialData;
},
},
};
</script>

View file

@ -1,228 +0,0 @@
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import store from "@/store";
// Components
import vehicleMake from "@/layouts/vehicle-make/vehicle-make.vue";
import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
// getters: jest.fn().mockImplementation(() => ({
// vehicle: {
// year: 2019,
// },
// })),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
describe("vehicle-make.vue", () => {
test("Make question component is initized with api data", async (done) => {
//Arrange
const makeQuestionInitialData = ["honda", "ford", "dodge"];
const { wrapper, apiPromise } = setupMocks({
makeQuestionInitialData: makeQuestionInitialData,
});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(makeQuestion.methods.initializeComponent).toHaveBeenCalledWith(
makeQuestionInitialData
);
done();
});
});
});
describe("vehicle-make.vue", () => {
test("BackButtonAction triggers a router.navigateWithoutSaving change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a make to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
},
});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
//Assert
apiPromise.finally(() => {
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
done();
});
});
});
describe("vehicle-make.vue", () => {
describe("arePagePrerequisitesValue", () => {
test("Year set, arePagePrerequisitesValid should be true", async () => {
//Arrange
const { wrapper } = setupMocks({
vehicleData: {
year: 2019,
},
});
//Act
vehicleMake.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("Year not set, arePagePrerequisitesValid should be false", async () => {
//Arrange
store.getters.vehicle.year = jest.fn().mockReturnValueOnce(undefined);
const { wrapper } = setupMocks({});
//Act
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(false);
});
});
test("selectedMake changes => save make in store", async () => {
//Arrange
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
},
},
});
// Act
await wrapper.setData({
selectedMake: "Make",
});
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledTimes(1);
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith(
"saveVehicleMake",
"Make",
false
);
});
test("selectedMake changes => navigate with saving", async () => {
//Arrange
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
},
route: {
fmgPage: "test",
},
},
});
// Act
await wrapper.setData({
selectedMake: "Make",
});
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
"SELECTED_MAKE",
expect.anything()
);
});
});
function setupMocks({
vehicleMakeQuestionCmsContent = {},
makeQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
vehicleData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleMakeQuestion: vehicleMakeQuestionCmsContent,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
makeQuestionInitialData: makeQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
store.getters = {
vehicle: vehicleData,
};
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
settleAllPromises.mockImplementation(() => apiPromise);
//Mock make question methods
makeQuestion.methods = {
loadInitialData: jest.fn(),
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleMake, mountOptions);
const makeQuestionWrapper = wrapper.findComponent({ name: "makeQuestion" });
makeQuestionWrapper.vm.initializeComponent = makeQuestion.methods.initializeComponent;
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
return { wrapper, apiPromise };
}

View file

@ -1,95 +0,0 @@
<template>
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage />
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
:hasBackButton="true"
@click-event="backButtonAction"
headerColor="light-header" />
<div class="fade-on-route-transition">
<makeQuestion
v-model="selectedMake"
ref="makeQuestion"
cmsWidgetName="VehicleMakeQuestion" />
</div>
</div>
</div>
</div>
</template>
<script>
// Components
import makeQuestion from "@/layouts/vehicle-make/make-question/make-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
export default {
name: "vehicle-make",
data() {
return {
selectedMake: null,
};
},
computed: {},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const makeQuestionInitialDataPromise = makeQuestion.methods.loadInitialData();
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "makeQuestionInitialData",
promise: makeQuestionInitialDataPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.makeQuestion.initializeComponent(resultMap.makeQuestionInitialData);
});
},
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
arePagePrerequisitesValid() {
if (store.getters.vehicle.year) {
return true;
}
return false;
},
},
watch: {
selectedMake(make) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
this.$router.navigateWithSaving(this.navigationScenarios.SELECTED_MAKE, this.$route);
},
},
components: {
makeQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
},
};
</script>

View file

@ -1,79 +0,0 @@
import modelQuestion from "@/layouts/vehicle-model/model-question/model-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("model-question.vue", () => {
test("Selected model is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "Accord" });
const modelToSelect = "Civic";
//Act
wrapper.setValue({ selectedModel: modelToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedModel: "Civic" }]);
});
});
describe("model-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["accord", "civic", "insight"],
});
//Act
const initialData = modelQuestion.methods.loadInitialData.call(wrapper.vm);
modelQuestion.methods.initializeComponent.call(wrapper.vm, initialData);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe("accord,civic,insight");
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { year: 2019, make: "honda" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(modelQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}

View file

@ -1,60 +0,0 @@
<template>
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
selectingInitiatesLoad
:questionText="questionText"
:answers="models"
groupName="ChooseVehicleModel"
textPosition="text-start"
v-model="selectedValue"
isRequired />
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
// Supporting files
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
export default {
name: "model-question",
data() {
return {
models: [],
};
},
props: {
modelValue: String,
cmsWidgetName: String,
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
});
},
initializeComponent(initialData) {
this.models = initialData;
},
},
};
</script>

View file

@ -1,147 +0,0 @@
// Components
import vehicleModel from "@/layouts/vehicle-model/vehicle-model.vue";
import modelQuestion from "@/layouts/vehicle-model/model-question/model-question";
// Supporting files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock Store
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
make: "Acura",
},
},
}));
describe("vehicle-model.vue", () => {
test("Model question component is initized with api data", async (done) => {
//Arange
const modelQuestionInitialData = ["accord", "civic", "insight"];
const { wrapper, apiPromise } = setupMocks({
modelQuestionInitialData: modelQuestionInitialData,
});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(modelQuestion.methods.initializeComponent).toHaveBeenCalledWith(
modelQuestionInitialData
);
done();
});
});
});
describe("vehicle-model.vue", () => {
test("BackButtonAction triggers a router.navigateWithoutSaving change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a model to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
},
});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
//Assert
apiPromise.finally(() => {
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
done();
});
});
});
describe("vehicle-model.vue", () => {
test("Make set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleModel.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-model" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
function setupMocks({
buttonQuestionContent = {},
modelQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleModelQuestion: buttonQuestionContent,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
modelQuestionInitialData: modelQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock model question methods
modelQuestion.methods = {
loadInitialData: jest.fn(),
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleModel, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
const modelQuestionWrapper = wrapper.findComponent({ name: "modelQuestion" });
modelQuestionWrapper.vm.initializeComponent = modelQuestion.methods.initializeComponent;
return { wrapper, apiPromise };
}

View file

@ -1,98 +0,0 @@
<template>
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage />
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
:hasBackButton="true"
@click-event="backButtonAction"
headerColor="light-header" />
<div class="fade-on-route-transition">
<modelQuestion
v-model="selectedModel"
ref="modelQuestion"
cmsWidgetName="VehicleModelQuestion" />
</div>
</div>
</div>
</div>
</template>
<script>
// Components
import modelQuestion from "@/layouts/vehicle-model/model-question/model-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default {
name: "vehicle-model",
data() {
return {
selectedModel: null,
};
},
computed: {},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const modelQuestionInitialDataPromise = modelQuestion.methods.loadInitialData();
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "modelQuestionInitialData",
promise: modelQuestionInitialDataPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.modelQuestion.initializeComponent(resultMap.modelQuestionInitialData);
});
},
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
arePagePrerequisitesValid() {
if (store.getters.vehicle.make) {
return true;
}
return false;
},
},
watch: {
selectedModel(model) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
this.$router.navigateWithSaving(this.navigationScenarios.SELECTED_MODEL, this.$route);
},
},
components: {
modelQuestion,
funnelHeader,
funnelSubHeader,
vehicleBanner,
},
};
</script>

View file

@ -1,79 +0,0 @@
import styleQuestion from "@/layouts/vehicle-style/style-question/style-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("style-question.vue", () => {
test("Selected style is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "2 Door" });
const styleToSelect = "4 Door";
//Act
wrapper.setValue({ selectedStyle: styleToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedStyle: "4 Door" }]);
});
});
describe("style-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["2 Door", "4 Door"],
});
//Act
const initialData = styleQuestion.methods.loadInitialData.call(wrapper.vm);
styleQuestion.methods.initializeComponent.call(wrapper.vm, initialData);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe("2 Door,4 Door");
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { year: 2019, make: "honda", model: "civc" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(styleQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}

View file

@ -1,61 +0,0 @@
<template>
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
selectingInitiatesLoad
:questionText="questionText"
:answers="styles"
groupName="ChooseVehicleStyle"
textPosition="text-start"
v-model="selectedValue"
isRequired />
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
// Supporting files
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
export default {
name: "style-question",
data() {
return {
styles: [],
};
},
props: {
modelValue: String,
cmsWidgetName: String,
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
components: {
buttonQuestion,
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
});
},
initializeComponent(initialData) {
this.styles = initialData;
},
},
};
</script>

View file

@ -1,256 +0,0 @@
// Components
import vehicleStyle from "@/layouts/vehicle-style/vehicle-style.vue";
import styleQuestion from "@/layouts/vehicle-style/style-question/style-question";
// Supporting files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import router from "@/router";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/router", () => ({
overrideNavigation: jest.fn(),
}));
describe("vehicle-style.vue", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("Style question component is initized with api data", async (done) => {
//Arrange
const styleQuestionInitialData = ["2 Door", "4 Door"];
const { wrapper, apiPromise } = setupMocks({
styleQuestionInitialData: styleQuestionInitialData,
});
//Act
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(styleQuestion.methods.initializeComponent).toHaveBeenCalledWith(
styleQuestionInitialData
);
done();
});
});
test("BackButtonAction triggers a router.navigateWithoutSaving change", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a style to get started",
mountOptionsMockData: {
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
},
});
//Act
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
await nextTick();
//Assert
apiPromise.finally(() => {
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
done();
});
});
test("setVehicle triggers a dispatchStoreAction commit", async (done) => {
//Arrange
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: "Select a style to get started",
mountOptionsMockData: {
actionList: [
{
actionName: storeActions.SET_VEHICLE,
data: "mockData",
},
],
},
});
//Act
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setVehicle();
await nextTick();
//Assert
apiPromise.finally(() => {
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalled();
done();
});
});
test("Model set, arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("there is only one vehicle style => autoselect and move to vehicle damage", async () => {
//Arrange
const { wrapper } = setupMocks({
styleQuestionInitialData: ["2 door sedan"],
});
// Act
await vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_STYLE, "2 door sedan");
expect(router.overrideNavigation).toHaveBeenCalled();
});
test("there is only one vehicle style and vehicle-damage was visited => don't autoselect or move to vehicle damage", async () => {
//Arrange
const { wrapper } = setupMocks({
styleQuestionInitialData: ["2 door sedan"],
mountOptionsMockData: {
store: {
getters: {
applicationUser: {
pageData: {
"part-questions": null,
"vehicle-make": {},
"vehicle-model": {},
"vehicle-style": {},
"vehicle-damage": {},
},
},
},
},
},
});
// Act
await vehicleStyle.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-style" } },
undefined,
(c) => c(wrapper.vm)
);
// Assert
expect(store.commit).not.toHaveBeenCalledWith(storeMutations.UPDATE_STYLE, "2 door sedan");
expect(router.overrideNavigation).not.toHaveBeenCalled();
});
});
function setupMocks({
vehicleStyleQuestionCmsContent = {},
styleQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleStyleQuestion: vehicleStyleQuestionCmsContent,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
styleQuestionInitialData: styleQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock style question methods
styleQuestion.methods = {
loadInitialData: jest.fn(),
initializeComponent: jest.fn(),
};
store.commit = jest.fn();
store.dispatch = jest.fn();
store.getters = mountOptionsMockData.store?.getters ?? {
vehicle: {
model: "TL",
},
applicationUser: {
pageData: {
"part-questions": null,
"vehicle-make": {},
"vehicle-model": {},
"vehicle-style": {},
},
},
};
const mountOptions = getMountOptions({
...mountOptionsMockData,
store,
});
const wrapper = shallowMount(vehicleStyle, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
const styleQuestionWrapper = wrapper.findComponent({ name: "styleQuestion" });
styleQuestionWrapper.vm.initializeComponent = styleQuestion.methods.initializeComponent;
return { wrapper, apiPromise };
}

View file

@ -1,137 +0,0 @@
<template>
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage />
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
:hasBackButton="true"
@click-event="backButtonAction"
headerColor="light-header" />
<div class="fade-on-route-transition">
<styleQuestion
v-model="selectedStyle"
ref="styleQuestion"
cmsWidgetName="VehicleStyleQuestion" />
</div>
</div>
</div>
</div>
</template>
<script>
// Components
import baseMixin from "@/mixins/base-mixin.js";
import styleQuestion from "@/layouts/vehicle-style/style-question/style-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import router from "@/router";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
export default {
name: "vehicle-style",
data() {
return {
selectedStyle: null,
};
},
computed: {},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const styleQuestionInitialDataPromise = styleQuestion.methods.loadInitialData();
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "styleQuestionInitialData",
promise: styleQuestionInitialDataPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const visitedVehicleDamage =
JSON.stringify(store.getters.applicationUser.pageData).indexOf(
fmgPageValues.VEHICLE_DAMAGE
) < 0
? false
: true;
// If we have exactly one style then navigate directly to vehicle-damage
if (resultMap.styleQuestionInitialData.length === 1 && !visitedVehicleDamage) {
store.commit(storeMutations.UPDATE_STYLE, resultMap.styleQuestionInitialData[0]);
await baseMixin.methods.dispatchStoreAction(storeActions.SET_VEHICLE, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
style: store.getters.vehicle.style,
});
//emulate selecting the vehicle style
router.overrideNavigation(navigationScenarios.SELECTED_STYLE, to, next, true);
} else {
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.styleQuestion.initializeComponent(resultMap.styleQuestionInitialData);
});
}
},
methods: {
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
setVehicle() {
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
year: this.$store.getters.vehicle.year,
make: this.$store.getters.vehicle.make,
model: this.$store.getters.vehicle.model,
style: this.$store.getters.vehicle.style,
});
},
arePagePrerequisitesValid() {
if (store.getters.vehicle.model) {
return true;
}
return false;
},
},
watch: {
selectedStyle(style) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
this.setVehicle().then(() => {
this.$router.navigateWithSaving(
this.navigationScenarios.SELECTED_STYLE,
this.$route
);
});
},
},
components: {
styleQuestion,
funnelHeader,
funnelSubHeader,
vehicleBanner,
},
};
</script>

View file

@ -1,122 +0,0 @@
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { nextTick } from "vue";
import { storeMutations } from "@/constants/store-mutations";
import { storeActions } from "@/constants/store-actions";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "@/mixins/base-mixin.js";
import vehicleYear from "@/layouts/vehicle-year/vehicle-year.vue";
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import store from "@/store";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
applicationUser: {
experiments: [],
},
},
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
describe("vehicle-year.vue", () => {
test("Year question component is initized with api data", async (done) => {
//Arrange
const yearQuestionInitialData = ["2023", "2022", "2021"];
const { wrapper, apiPromise } = setupMocks({
yearQuestionInitialData: yearQuestionInitialData,
});
//Act
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-year" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(yearQuestion.methods.initializeComponent).toHaveBeenCalledWith(
yearQuestionInitialData
);
done();
});
});
});
describe("vehicle-year.vue", () => {
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
vehicleYear.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-make" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
function setupMocks({
vehicleYearQuestionCmsContent = {},
yearQuestionInitialData = {},
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {},
}) {
//Mock api responses
const apiResponses = {
cmsContent: {
FunnelSubHeaderWidget: { HeaderText: pageHeaderWidgetHeaderText },
VehicleYearQuestion: vehicleYearQuestionCmsContent,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
},
yearQuestionInitialData: yearQuestionInitialData,
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock year question methods
yearQuestion.methods = {
loadInitialData: jest.fn(),
initializeComponent: jest.fn(),
};
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleYear, mountOptions);
const yearQuestionWrapper = wrapper.findComponent({ name: "yearQuestion" });
yearQuestionWrapper.vm.initializeComponent = yearQuestion.methods.initializeComponent;
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
return { wrapper, apiPromise };
}

View file

@ -1,110 +0,0 @@
<template>
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" headerColor="light-header" />
<div class="fade-on-route-transition">
<yearQuestion
class="fade-on-route-transition"
v-model="selectedYear"
ref="yearQuestion"
cmsWidgetName="VehicleYearQuestion" />
</div>
</div>
</div>
</div>
</template>
<script>
// Components
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { storeActions } from "@/constants/store-actions";
import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin";
export default {
name: "vehicle-year",
data() {
return {
selectedYear: null,
};
},
computed: {},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const yearQuestionInitialDataPromise = yearQuestion.methods.loadInitialData();
const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
);
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
if (experimentForLogging !== undefined) {
// Log experiment exposure
baseMixin.methods.dispatchStoreAction(
storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
experiment: experimentForLogging,
},
false
);
}
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
{
resultKey: "yearQuestionInitialData",
promise: yearQuestionInitialDataPromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
vm.$refs.yearQuestion.initializeComponent(resultMap.yearQuestionInitialData);
});
},
watch: {
selectedYear(year) {
const parsedYear = parseInt(year);
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
this.$router.navigateWithSaving(this.navigationScenarios.SELECTED_YEAR, this.$route);
},
},
methods: {
arePagePrerequisitesValid() {
return true;
},
},
components: {
yearQuestion,
funnelHeader,
vehicleBanner,
funnelSubHeader,
},
};
</script>

View file

@ -1,78 +0,0 @@
import yearQuestion from "@/layouts/vehicle-year/year-question/year-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("year-question.vue", () => {
test("Selected year is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "2020" });
const yearToSelect = "2021";
//Act
wrapper.setValue({ modelValue: yearToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ modelValue: "2021" }]);
});
});
describe("year-question.vue", () => {
test("Data from store api are used as radio question answers.", async () => {
//Arrange
const { wrapper, cmsContent } = setupMocks({
dataFromStoreApi: ["2023", "2022", "2021"],
});
//Act
const initialData = yearQuestion.methods.loadInitialData.call(wrapper.vm);
yearQuestion.methods.initializeComponent.call(wrapper.vm, initialData);
//Assert
const buttonQuestionComponent = await wrapper.findComponent({
name: "buttonQuestion",
});
expect(buttonQuestionComponent.attributes("answers")).toBe("2023,2022,2021");
});
});
function setupMocks({
modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(yearQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}

View file

@ -1,55 +0,0 @@
<template>
<buttonQuestion
class="radioQuestion"
isOverflowScrollable
selectingInitiatesLoad
:questionText="questionText"
:answers="years"
groupName="ChooseVehicleYear"
textPosition="text-start"
v-model="selectedValue"
isRequired />
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
// Supporting files
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
export default {
name: "year-question",
data() {
return {
years: [],
};
},
props: {
modelValue: String,
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);
},
},
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
},
initializeComponent(initialData) {
this.years = initialData;
},
},
};
</script>

View file

@ -1,19 +1,17 @@
// Components
import vehicle from "@/layouts/vehicle/vehicle.vue";
// Supporting files
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { nextTick } from "vue";
import { mount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
describe("vehicle.vue", () => {
test('"Continue" button is enabled after YMMS is selected.', async () => {
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="funnel-footer-main-button"]');
expect(continueButton.attributes()["aria-disabled"]).toBe("false");
});
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks();
@ -26,66 +24,33 @@ describe("vehicle.vue", () => {
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("vehicle.vue", () => {
describe("navigation", () => {
test("if the continue button is clicked, navigate forward", async () => {
// Arrange
const { wrapper } = setupMocks();
describe("navigation", () => {
test("forwardButtonAction should trigger navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks();
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
});
});
});
const FunnelFooterWidgetMockData = {
ForwardButtonText: "Continue",
};
function setupMocks() {
const mockRoute = {
query: {
fmgPage: "vehicle",
},
};
const mockRouter = {
navigate: jest.fn(),
};
const wrapper = mount(vehicle, {
global: {
mixins: [
{
methods: {
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
if (cmsWidgetName === "FunnelFooterWidget") {
if (fieldName === "ForwardButtonText") {
return FunnelFooterWidgetMockData.ForwardButtonText;
}
}
return "";
}),
getFooterInfoBoxHeight: jest.fn(() => 80),
},
},
],
mocks: {
$route: mockRoute,
$router: mockRouter,
const wrapper = shallowMount(
vehicle,
getMountOptions({
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
})
);
stubs: {
FunnelHeader: true,
FunnelSubHeader: true,
VehicleBanner: true,
},
},
});
return { mockRoute, mockRouter, wrapper };
return { wrapper };
}

View file

@ -58,6 +58,9 @@
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
:displayVehicleImage="imageUrl"
:vehicleCategory="category"
:displayUnmatchedVehicleIcon="unmatchedVehicleIcon"
class="mt-5 mb-3"
ref="banner" />
@ -109,18 +112,20 @@ export default {
selectedMake: this.selectedMakefromStore(),
selectedModel: this.selectedModelfromStore(),
selectedStyle: this.selectedStylefromStore(),
carId: null,
category: null,
imageUrl: null,
imageVifNumber: null,
imageVifColor: null,
yearOptions: [],
makeOptions: [],
modelOptions: [],
styleOptions: [],
displayGeneric: this.displayGenericFromStore(),
unmatchedVehicleIcon: false,
};
},
props: {
cmsWidgetName: String,
validationRules: String,
},
async beforeRouteEnter(to, from, next) {
// Call APIs
@ -244,7 +249,8 @@ export default {
const result = await this.getModelOptions(this.selectedYear, make);
this.modelOptions = result?.data;
if (this.selectedMakefromStore() !== make) {
this.selectedModel = null;
if (this.modelOptions.length === 1) this.selectedModel = this.modelOptions[0];
else this.selectedModel = null;
this.selectedStyle = null;
}
} else {
@ -260,36 +266,60 @@ export default {
);
this.styleOptions = result?.data;
if (this.selectedModelfromStore() !== model) {
this.selectedStyle = null;
if (this.styleOptions.length === 1) this.selectedStyle = this.styleOptions[0];
else this.selectedStyle = null;
}
} else {
this.styleOptions = [];
}
},
selectedStyle(style) {
this.setVehicle(this.selectedYear, this.selectedMake, this.selectedModel, style);
async selectedStyle(style) {
if (style) {
const result = await this.getVehicle(
this.selectedYear,
this.selectedMake,
this.selectedModel,
style
);
this.carId = result?.data.carId;
this.category = result?.data.category;
this.imageUrl = result?.data.imageUrl;
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
if (this.imageUrl == null) {
this.displayGeneric = false;
this.unmatchedVehicleIcon = true;
} else this.unmatchedVehicleIcon = false;
} else {
this.imageUrl = null;
this.displayGeneric = true;
}
},
},
methods: {
setVehicle(year, make, model, style) {
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
getVehicle(year, make, model, style) {
return this.dispatchStoreAction(this.storeActions.GET_VEHICLE, {
year: year,
make: make,
model: model,
style: style,
});
},
displayGenericFromStore() {
if (store.getters.vehicle.imageUrl !== null) return false;
else if (
store.getters.vehicle.imageUrl === null &&
store.getters.vehicle.carId !== null
)
return false;
else return true;
},
arePagePrerequisitesValid() {
return true;
},
async forwardButtonAction() {
return this.navigateForward();
},
navigateForward() {
this.dispatchStoreAction(
storeActions.SAVE_VEHICLE,
{
@ -297,12 +327,16 @@ export default {
make: this.selectedMake,
model: this.selectedModel,
style: this.selectedStyle,
carId: this.carId,
category: this.category,
imageUrl: this.imageUrl,
imageVifNumber: this.imageVifNumber,
imageVifColor: this.imageVifColor,
},
false
);
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
initializeYearComponent(initialData) {
this.yearOptions = initialData;
},
@ -345,12 +379,6 @@ export default {
},
},
computed: {
displayGeneric() {
return !this.selectedStyle;
},
},
components: {
funnelHeader,
funnelFooter,

View file

@ -38,6 +38,8 @@ export default {
event: pageEvent,
shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments,
referralSequenceNumber: store.getters.order.referralSequenceNumber,
parentAccountNumber: store.getters.order.payment.parentAccountNumber,
};
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
@ -57,6 +59,8 @@ export default {
value: value,
shouldUseSessionId: false,
experimentsForUser: store.getters.applicationUser.experiments,
referralSequenceNumber: store.getters.order.referralSequenceNumber,
parentAccountNumber: store.getters.order.payment.parentAccountNumber,
};
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false);

View file

@ -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",

View file

@ -1,8 +1,4 @@
const fmgPageValues = {
VEHICLE_YEAR: "vehicle-year",
VEHICLE_MAKE: "vehicle-make",
VEHICLE_MODEL: "vehicle-model",
VEHICLE_STYLE: "vehicle-style",
VEHICLE: "vehicle",
VEHICLE_DAMAGE: "vehicle-damage",
ADDRESS_LOOKUP: "address-lookup",

View file

@ -12,12 +12,6 @@ const navigationScenarios = {
CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_CASH: "CLICKED_FORWARD_WITH_CASH",
// YMMS
SELECTED_YEAR: "SELECTED_YEAR",
SELECTED_MODEL: "SELECTED_MODEL",
SELECTED_MAKE: "SELECTED_MAKE",
SELECTED_STYLE: "SELECTED_STYLE",
// Vin selection
CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
@ -51,6 +45,7 @@ const navigationScenarios = {
CLICKED_DAMAGE_EDIT: "CLICKED_DAMAGE_EDIT",
CLICKED_SERVICE_PACKAGE_EDIT: "CLICKED_SERVICE_PACKAGE_EDIT",
CLICKED_SERVICE_LOCATION_EDIT: "CLICKED_SERVICE_LOCATION_EDIT",
CLICKED_SCHEDULE_EDIT: "CLICKED_SCHEDULE_EDIT",
};
export { navigationScenarios };

View file

@ -13,54 +13,7 @@ const routingTable = function (store) {
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
maps: [
{
scenario: navigationScenarios.SELECTED_YEAR,
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_MAKE,
maps: [
{
scenario: navigationScenarios.SELECTED_MAKE,
destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL,
},
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_YEAR,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_MODEL,
maps: [
{
scenario: navigationScenarios.SELECTED_MODEL,
destinationFmgPageValue: fmgPageValues.VEHICLE_STYLE,
},
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_MAKE,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_STYLE,
maps: [
{
scenario: navigationScenarios.SELECTED_STYLE,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_MODEL,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
maps: [
@ -484,6 +437,10 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_SERVICE_LOCATION_EDIT,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
{
scenario: navigationScenarios.CLICKED_SCHEDULE_EDIT,
destinationFmgPageValue: fmgPageValues.SCHEDULE,
},
],
},
];

View file

@ -13,7 +13,14 @@ import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import { deepEqual } from "@/helpers/object-helper";
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { queryStrings } from "@/constants/query-strings";
import { partTypeStrings } from "@/constants/part-type-strings";
import {
convertDateStringToDate,
militaryToTwelveHourTime,
getDisplayTextForDurationLength,
} from "@/layouts/schedule/helpers/schedule-helper";
// Export State
const getDefaultState = () => {
@ -32,12 +39,6 @@ const getDefaultState = () => {
imageColor: null,
registration: {
licensePlate: null,
address: null,
city: null,
state: null,
zipCode: null,
firstName: null,
lastName: null,
},
},
serviceLocation: {
@ -66,7 +67,7 @@ const getDefaultState = () => {
lastName: null,
emailAddress: null,
phoneNumber: null,
isSmsOptin: null,
isSmsOptIn: null,
},
damage: {
isRepair: null,
@ -96,8 +97,10 @@ const getDefaultState = () => {
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
referralNumber: null,
referralSequenceNumber: null,
referralDate: null,
referralCorrelationId: null,
eon: null,
@ -190,6 +193,9 @@ export const mutations = {
updateReferralNumber(state, referralNumber) {
state.order.referralNumber = referralNumber;
},
updateReferralSequenceNumber(state, referralSequenceNumber) {
state.order.referralSequenceNumber = referralSequenceNumber;
},
updateReferralDate(state, referralDate) {
state.order.referralDate = referralDate;
},
@ -205,27 +211,6 @@ export const mutations = {
updateInsuranceVerifiedStatus(state, isVerified) {
state.order.payment.insuranceCoverage.isVerified = isVerified;
},
updateRegistrationLicensePlate(state, licensePlate) {
state.order.vehicle.registration.licensePlate = licensePlate;
},
updateRegistrationAddress(state, registrationAddress) {
state.order.vehicle.registration.address = registrationAddress;
},
updateRegistrationCity(state, registrationCity) {
state.order.vehicle.registration.city = registrationCity;
},
updateRegistrationState(state, registrationState) {
state.order.vehicle.registration.state = registrationState;
},
updateRegistrationZipCode(state, registrationZipCode) {
state.order.vehicle.registration.zipCode = registrationZipCode;
},
updateRegistrationFirstName(state, firstName) {
state.order.vehicle.registration.firstName = firstName;
},
updateRegistrationLastName(state, lastName) {
state.order.vehicle.registration.lastName = lastName;
},
updateCustomerEmailAddress(state, customerEmailAddress) {
state.order.customer.emailAddress = customerEmailAddress;
},
@ -283,6 +268,7 @@ export const mutations = {
state.order.schedule.endTime = scheduleInfo.endTime;
state.order.schedule.routeCode = scheduleInfo.routeCode;
state.order.schedule.jobMaxMinutes = scheduleInfo.jobMaxMinutes;
state.order.schedule.jobMinMinutes = scheduleInfo.jobMinMinutes;
}
},
updateCustomerDetails(state, customerDetails) {
@ -291,7 +277,7 @@ export const mutations = {
state.order.customer.lastName = customerDetails.lastName;
state.order.customer.emailAddress = customerDetails.emailAddress;
state.order.customer.phoneNumber = customerDetails.phoneNumber;
state.order.customer.isSmsOptin = customerDetails.isSmsOptin;
state.order.customer.isSmsOptIn = customerDetails.isSmsOptIn;
}
},
@ -345,12 +331,8 @@ export const mutations = {
},
resetRegistrationState(state) {
state.order.vehicle.registration.licensePlate = null;
state.order.vehicle.registration.address = null;
state.order.vehicle.registration.city = null;
state.order.vehicle.registration.state = null;
state.order.vehicle.registration.zipCode = null;
state.order.vehicle.registration.firstName = null;
state.order.vehicle.registration.lastName = null;
state.order.customer.firstName = null;
state.order.customer.lastName = null;
},
resetGlassPartsState(state) {
state.order.lineItems.glassParts = null;
@ -368,6 +350,7 @@ export const mutations = {
state.order.schedule.endTime = null;
state.order.schedule.routeCode = null;
state.order.schedule.jobMaxMinutes = null;
state.order.schedule.jobMinMinutes = null;
//premium appointment fee used on schedule page also needs reset when schedule is reset
const supportingItems = state.order.lineItems.supportingItems;
@ -411,6 +394,7 @@ export const mutations = {
// Misc Mutations
updateStateWithOrderInformation(state, sessionInformation) {
state.order.referralNumber = sessionInformation.order.referralNumber;
state.order.referralSequenceNumber = sessionInformation.order.referralSequenceNumber;
state.order.referralDate = sessionInformation.order.referralDate;
state.order.referralCorrelationId = sessionInformation.order.referralCorrelationId;
state.order.eon = sessionInformation.order.eon;
@ -492,8 +476,8 @@ export const mutations = {
state.order.customer.emailAddress = sessionInformation.order.customer.emailAddress;
state.order.customer.firstName = sessionInformation.order.customer.firstName;
state.order.customer.lastName = sessionInformation.order.customer.lastName;
state.order.customer.phoneNumber = sessionInformation.order.customer.phoneNumber;
state.order.customer.isSmsOptin = sessionInformation.order.customer.isSmsOptin;
state.order.customer.phoneNumber = sessionInformation.order.customer.servicePhoneNumber;
state.order.customer.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn;
state.order.existingPromoCode = sessionInformation.order.existingPromoCode;
state.applicationUser.experiments = sessionInformation.applicationUser.experiments;
@ -506,6 +490,7 @@ export const mutations = {
state.order.schedule.endTime = sessionInformation.order.schedule?.endTime;
state.order.schedule.routeCode = sessionInformation.order.schedule?.routeCode;
state.order.schedule.jobMaxMinutes = sessionInformation.order.schedule?.jobMaxMinutes;
state.order.schedule.jobMinMinutes = sessionInformation.order.schedule?.jobMinMinutes;
},
updateExperiments(state, experiments) {
state.applicationUser.experiments = experiments;
@ -540,6 +525,9 @@ export const getters = {
isMobileAppointment: (state) => {
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
},
isDropOffAppointment: (state) => {
return state.order.serviceLocation.appointmentType === AppointmentTypeStrings.DROP_OFF;
},
isRecalibrationOnOrder: (state) => {
return getHasRecalibrationPart(state);
},
@ -548,6 +536,30 @@ export const getters = {
(vap) => vap.partType.toUpperCase() === partTypeStrings.REAR_WIPER.toUpperCase()
);
},
scheduleDisplayText: (state) => {
const dateModel = convertDateStringToDate(state.order.schedule.date);
const readableDate = dateModel?.toLocaleDateString("en-us", {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric",
});
const readableStartTime = militaryToTwelveHourTime(state.order.schedule.startTime);
const readableEndTime = militaryToTwelveHourTime(state.order.schedule.endTime);
const readableDuration = getDisplayTextForDurationLength(
state.order.schedule.jobMinMinutes,
state.order.schedule.jobMaxMinutes
);
return {
date: readableDate,
startTime: readableStartTime,
endTime: readableEndTime,
duration: readableDuration,
};
},
lineItems: (state) => state.order.lineItems,
pageData: (state) => (page) => {
return state.applicationUser.pageData[page];
@ -739,7 +751,7 @@ export const actions = {
});
},
setVehicle(context, { year, make, model, style }) {
getVehicle(context, { year, make, model, style }) {
return globalMethods
.callHttpClient({
methods: endpoints.GetVehicle.method,
@ -747,27 +759,17 @@ export const actions = {
payload: {},
})
.then((response) => {
context.commit(storeMutations.UPDATE_CAR_ID, response.data.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, response.data.category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, response.data.imageUrl);
context.commit(
storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER,
response.data.imageVifNumber
);
context.commit(
storeMutations.UPDATE_VEHICLE_IMAGE_COLOR,
response.data.imageVifColor
);
return response;
});
},
getDamageOptions(context, { carId, zipCode }) {
getDamageOptions(context, { carId }) {
return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {},
additionalSuccessEventDataHandler: (response) => "QueryStringZip: " + zipCode,
additionalSuccessEventDataHandler: (response) =>
"QueryStringZip: " + getQuerystringParameter(queryStrings.ZIP_CODE),
});
},
@ -883,6 +885,7 @@ export const actions = {
context,
{
referralNumber,
referralSequenceNumber,
referralDate,
referralCorrelationId,
eon,
@ -892,6 +895,7 @@ export const actions = {
}
) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_SEQUENCE_NUMBER, referralSequenceNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
context.commit(storeMutations.UPDATE_EON, eon);
@ -911,6 +915,8 @@ export const actions = {
event,
shouldUseSessionId,
experimentsForUser,
referralSequenceNumber,
parentAccountNumber,
}
) {
var payload = {
@ -923,6 +929,8 @@ export const actions = {
event: event,
shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser,
referralSequenceNumber: referralSequenceNumber,
parentAccountNumber: parentAccountNumber,
};
return globalMethods
@ -955,6 +963,8 @@ export const actions = {
value,
shouldUseSessionId,
experimentsForUser,
referralSequenceNumber,
parentAccountNumber,
}
) {
var payload = {
@ -969,6 +979,8 @@ export const actions = {
value: value,
shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser,
referralSequenceNumber: referralSequenceNumber,
parentAccountNumber: parentAccountNumber,
};
return globalMethods
@ -1417,8 +1429,8 @@ export const actions = {
emailAddress: order.customer.emailAddress,
firstName: order.customer.firstName,
lastName: order.customer.lastName,
isSmsOptin: order.customer.isSmsOptin,
phoneNumber: order.customer.phoneNumber,
isSmsOptIn: order.customer.isSmsOptIn,
servicePhoneNumber: order.customer.phoneNumber,
},
damage: {
numberOfChips: damage.numberOfChips,
@ -1438,7 +1450,7 @@ export const actions = {
InsuranceCoverage: {
isVerified: order.payment.insuranceCoverage.isVerified ?? false,
},
isInsurance: order.payment.isInsurance ?? false,
isInsurance: order.payment.isInsurance,
parentAccountNumber: order.payment.parentAccountNumber,
},
serviceLocation: {
@ -1469,12 +1481,13 @@ export const actions = {
endTime: order.schedule?.endTime,
routeCode: order.schedule?.routeCode,
jobMaxMinutes: order.schedule?.jobMaxMinutes,
jobMinMinutes: order.schedule?.jobMinMinutes,
},
existingPromoCode: null,
referralCorrelationId: order.referralCorrelationId,
referralDate: order.referralDate,
referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place
referralSequenceNumber: order.referralNumber?.toString(), // TODO Pass the referralSequence number once insurance flow creates it
referralNumber: order.referralNumber?.toString(),
referralSequenceNumber: order.referralSequenceNumber,
eon: order.eon,
},
},
@ -1542,92 +1555,12 @@ export const actions = {
// Business domain actions
// Vehicle
saveVehicleYear(context, year) {
//Reset dependent state when changing
if (context.state.order.vehicle.year !== year) {
context.commit(storeMutations.UPDATE_MAKE, null);
context.commit(storeMutations.UPDATE_MODEL, null);
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_YEAR, year);
}
},
saveVehicleMake(context, make) {
//Reset dependent state when changing
if (context.state.order.vehicle.make !== make) {
context.commit(storeMutations.UPDATE_MODEL, null);
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_MAKE, make);
}
},
saveVehicleModel(context, model) {
//Reset dependent state when changing
if (context.state.order.vehicle.model !== model) {
context.commit(storeMutations.UPDATE_STYLE, null);
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_MODEL, model);
}
},
saveVehicleStyle(context, style) {
//Reset dependent state when changing
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_CAR_ID, null);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
//Save new values
context.commit(storeMutations.UPDATE_STYLE, style);
}
},
saveVehicle(context, { year, make, model, style }) {
saveVehicle(
context,
{ year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor }
) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
if (context.state.order.vehicle.year !== year) {
context.commit(storeMutations.UPDATE_YEAR, year);
@ -1641,6 +1574,11 @@ export const actions = {
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_STYLE, style);
}
context.commit(storeMutations.UPDATE_CAR_ID, carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
},
saveVehicleDamage(
@ -1728,12 +1666,8 @@ export const actions = {
) {
//Reset dependent state when changing
if (
registrationInfo?.address !== context.state.order.vehicle.registration?.address ||
registrationInfo?.city !== context.state.order.vehicle.registration?.city ||
registrationInfo?.state !== context.state.order.vehicle.registration?.state ||
registrationInfo?.zipCode !== context.state.order.vehicle.registration?.zipCode ||
registrationInfo?.firstName !== context.state.order.vehicle.registration?.firstName ||
registrationInfo?.lastName !== context.state.order.vehicle.registration?.lastName
registrationInfo?.firstName !== context.state.order.customer?.firstName ||
registrationInfo?.lastName !== context.state.order.customer?.lastName
) {
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
@ -2064,12 +1998,6 @@ export const actions = {
return true;
default:
}
if (
context.state.order.vehicle.make.toLowerCase() === "ford" &&
context.state.order.vehicle.year >= 2018
) {
return true;
}
if (
context.state.order.vehicle.make.toLowerCase() === "bmw" &&
context.state.order.vehicle.year <= 2017

View file

@ -335,6 +335,28 @@ describe("Mutations", () => {
expect(storeState.order.serviceLocation.state).toEqual("OH");
expect(storeState.order.serviceLocation.zipCodeCtu).toEqual("01820");
});
it("updateCustomerDetails, should set customer details in state", () => {
// Arrange
const storeState = state;
const customerDetails = {
firstName: "foo",
lastName: "bar",
emailAddress: "foo@bar.com",
phoneNumber: "555-555-5555",
isSmsOptIn: true,
};
// Act
mutations.updateCustomerDetails(storeState, customerDetails);
// Assert
expect(state.order.customer.firstName).toEqual("foo");
expect(state.order.customer.lastName).toEqual("bar");
expect(state.order.customer.emailAddress).toEqual("foo@bar.com");
expect(state.order.customer.phoneNumber).toEqual("555-555-5555");
expect(state.order.customer.isSmsOptIn).toEqual(true);
});
});
describe("Actions", () => {
@ -484,12 +506,9 @@ describe("Actions", () => {
expect(response.data).toEqual({ style: "4 DOOR SEDAN" });
});
it("setVehicle action, should get vehicle data and set carId and vehicle category", async () => {
it("getVehicle action, should get vehicle data ", async () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
// Act
globalMethods.callHttpClient.mockImplementation(() => {
@ -497,10 +516,8 @@ describe("Actions", () => {
});
// Assert
const response = await actions.setVehicle(context, "C00000000");
const response = await actions.getVehicle(context, "C00000000");
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, "C00000000");
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, "CAR");
expect(response.data).toEqual({ carId: "C00000000", category: "CAR" });
});
@ -802,6 +819,7 @@ describe("Actions", () => {
// Act
actions.updateStoreWithSaveSessionResponse(context, {
referralNumber: "123",
referralSequenceNumber: 123,
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
parentAccountNumber: "167132",
@ -943,7 +961,7 @@ describe("Actions", () => {
);
});
it("saveServiceZipCodeInfo, should call mutation and save zip code to state", () => {
it("saveServiceZipCodeInfo, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
@ -958,7 +976,6 @@ describe("Actions", () => {
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_SERVICE_LOCATION, serviceZipCodeInfo);
expect(state.order.serviceLocation.zipCode).toEqual("43212");
});
it("saveServiceZipCodeInfo, should reset if zip code is different", () => {
@ -1027,7 +1044,7 @@ describe("Actions", () => {
);
});
it("saveServiceLocation, should call mutation and save service address to state", () => {
it("saveServiceLocation, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
@ -1046,11 +1063,6 @@ describe("Actions", () => {
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_SERVICE_LOCATION, serviceLocation);
expect(state.order.serviceLocation.address).toEqual("123 Test Lane");
expect(state.order.serviceLocation.city).toEqual("Columbus");
expect(state.order.serviceLocation.zipCode).toEqual("43212");
expect(state.order.serviceLocation.state).toEqual("OH");
expect(state.order.serviceLocation.zipCodeCtu).toEqual("01820");
});
it("saveServiceLocation, should reset if zipcode is different", () => {
@ -1297,6 +1309,27 @@ describe("Actions", () => {
expect(commit).not.toHaveBeenCalledWith(storeMutations.RESET_SCHEDULE);
});
it("saveCustomerDetails, should call mutation", () => {
// Arrange
const context = state;
const commit = jest.fn();
context.commit = commit;
const customerDetails = {
firstName: "foo",
lastName: "bar",
emailAddress: "foo@bar.com",
phoneNumber: "555-555-5555",
isSmsOptIn: true,
};
// Act
actions.saveCustomerDetails(context, customerDetails);
// Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_CUSTOMER_DETAILS, customerDetails);
});
it("saveGlassParts, should call mutation", () => {
// Arrange
const context = {
@ -1519,7 +1552,7 @@ describe("Actions", () => {
isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false,
vehicleInfo: { carId: "C010101", vin: "XXXXX" },
registrationInfo: { zipCode: "80020", address: "123 Marys Ave" },
registrationInfo: { firstName: "abc", lastName: "123" },
serviceLocationInfo: { state: "CO" },
customerEmail: "test@safelite.com",
};
@ -1540,168 +1573,6 @@ describe("Actions", () => {
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
});
it("saveVehicleYear, should wipe out vehicle info if year changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
year: "2015",
},
},
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleYear(context, "2016");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MAKE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicleMake, should wipe out vehicle info if make changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
make: "Honda",
},
},
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleMake(context, "Toyota");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
expect(commit).toBeCalledWith(storeMutations.UPDATE_MODEL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicle model, should wipe out vehicle info if model changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
model: "Civic",
},
},
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleModel(context, "Accord");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicleStyle, should wipe out vehicle info if style changes", () => {
// Arrange
const context = state;
context.state = {
order: {
vehicle: {
style: "Sedan",
},
},
};
const commit = jest.fn();
const dispatch = jest.fn();
context.commit = commit;
context.dispatch = dispatch;
// Act
actions.saveVehicleStyle(context, "SUV");
// Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
expect(commit).toBeCalledWith(storeMutations.UPDATE_CAR_ID, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_VIN, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
});
it("saveVehicle, should save vehicle info", () => {
// Arrange
const context = state;
@ -1729,6 +1600,8 @@ describe("Actions", () => {
make: "Honda",
model: "Civic",
style: "Sedan",
carId: "C00000000",
category: "CAR",
};
actions.saveVehicle(context, payload);
@ -1753,6 +1626,8 @@ describe("Actions", () => {
if (context.state.order.vehicle.style !== payload.style) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_STYLE, payload.style);
}
context.commit(storeMutations.UPDATE_CAR_ID, payload.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, payload.category);
});
it("saveVehicleDamage, should wipe out damage if different", () => {
@ -3291,8 +3166,6 @@ describe("Getters", () => {
describe("isVinOptionalVehicle", () => {
const testVehicles = [
["2017", "acura", false],
["2017", "ford", false],
["2018", "ford", true],
["2018", "bmw", false],
["2017", "bmw", true],
["2016", "bmw", true],