Merge branch 'develop' into feature/CSR-1564

This commit is contained in:
CarlNation 2023-08-14 15:22:42 -04:00
commit 4307b67988
39 changed files with 2374 additions and 873 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

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

View file

@ -35,16 +35,15 @@ const storeMutations = {
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

@ -14,7 +14,7 @@
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
:href="getExternalLink(copy)"
@click-event="$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
</span>
@ -34,6 +34,7 @@ import {
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
} from "@/helpers/cms-content-helper";
import { applicationConfig } from "@/constants/application-config";
@ -53,6 +54,7 @@ export default {
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
getExternalLink,
},
computed: {
pageQueryString() {

View file

@ -1,7 +1,8 @@
<template>
<div class="funnel-sub-header">
<div
class="d-flex align-items-center justify-content-center container-fluid overflow-hidden">
class="d-flex align-items-center container-fluid overflow-hidden"
:class="[textAlignment]">
<h5 class="text-center fw-normal mb-0" :class="headerColor">
<span>
{{ text }}
@ -36,6 +37,10 @@ export default {
type: String,
default: "dark-header",
},
leftAlignHeader: {
type: Boolean,
default: false,
},
},
components: {
buttonBack,
@ -44,6 +49,9 @@ export default {
text() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
textAlignment() {
return this.leftAlignHeader ? "justify-content-start" : "justify-content-center";
},
subText() {
return this.getCmsContent(this.cmsWidgetName, "HeaderSubText");
},
@ -82,4 +90,8 @@ p.small {
.sub-text {
margin: 0;
}
.justify-content-start {
padding-left: 0;
}
</style>

View file

@ -13,13 +13,20 @@ export default {
required: true,
},
cmsWidgetName: String,
displayVehicleImage: {
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"

View file

@ -310,6 +310,16 @@ export function getRouterLinkRouteFromCopy(copy) {
return copy.split(":")[1].split(",")[0];
}
export function getExternalLink(copy) {
let url = copy.split(":")[1].split(",")[0];
if (url.includes("https-")) {
const prefixAdded = url.replace("https-", "https://");
return prefixAdded;
}
return "#!";
}
/**
* Returns string3 of input following this pattern: {string1:string2,string3}
* @returns string

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

@ -32,7 +32,7 @@
<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

@ -356,8 +356,12 @@ export default {
<style lang="scss">
.vinLookupMethodHeading p {
font-size: 1rem;
font-weight: 500;
font-size: 14px;
font-weight: 400;
color: $black;
strong {
font-size: 1rem;
font-weight: 500;
}
}
</style>

View file

@ -0,0 +1 @@
test.todo("some test to be written in the future");

View file

@ -0,0 +1,73 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" leftAlignHeader />
<hr class="mt-0" />
<div class="fade-on-route-transition sub-container make-tall">
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
// Validation
import { Form } from "vee-validate";
export default {
name: "payment-method",
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// TODO: Data fetching Promise declarations
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
data() {
return {};
},
methods: {
arePagePrerequisitesValid() {
// TODO
return true;
},
backButtonAction() {},
forwardButtonAction() {},
},
components: {
funnelHeader,
funnelFooter,
funnelSubHeader,
Form,
},
};
</script>
<style lang="scss"></style>

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,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,6 +69,13 @@
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
<hr class="my-0" />
<scheduleReview
cmsWidgetName="ScheduleWidget"
:appointmentType="appointmentType"
@edit-clicked="editSchedule" />
</div>
<div>
@ -94,22 +101,16 @@ 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 { 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 +169,12 @@ export default {
this.$route
);
},
editSchedule() {
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_SCHEDULE_EDIT,
this.$route
);
},
},
computed: {
subHeaderTitle() {
@ -191,6 +198,12 @@ export default {
serviceLocationInfo() {
return this.$store.getters.order.serviceLocation;
},
appointmentType() {
return this.$store.getters.order.serviceLocation.appointmentType;
},
exposeCms() {
return this.$root.cmsContentByWidget;
},
},
components: {
funnelHeader,
@ -202,6 +215,7 @@ export default {
damageReview,
servicePackageReview,
serviceLocationReview,
scheduleReview,
},
};
</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,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,7 @@
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
:displayVehicleImage="vehicle.imageUrl"
class="mt-5 mb-3"
ref="banner" />
@ -109,6 +110,13 @@ export default {
selectedMake: this.selectedMakefromStore(),
selectedModel: this.selectedModelfromStore(),
selectedStyle: this.selectedStylefromStore(),
vehicle: {
carId: null,
category: null,
imageUrl: null,
imageVifNumber: null,
imageVifColor: null,
},
yearOptions: [],
makeOptions: [],
modelOptions: [],
@ -116,11 +124,6 @@ export default {
};
},
props: {
cmsWidgetName: String,
validationRules: String,
},
async beforeRouteEnter(to, from, next) {
// Call APIs
@ -244,7 +247,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 +264,40 @@ 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.vehicle = result?.data;
}
},
},
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,
});
},
arePagePrerequisitesValid() {
return true;
},
async forwardButtonAction() {
return this.navigateForward();
},
navigateForward() {
this.dispatchStoreAction(
storeActions.SAVE_VEHICLE,
{
@ -297,12 +305,12 @@ export default {
make: this.selectedMake,
model: this.selectedModel,
style: this.selectedStyle,
vehicle: this.vehicle,
},
false
);
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
initializeYearComponent(initialData) {
this.yearOptions = initialData;
},

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

@ -29,12 +29,18 @@ import { experimentTriggers } from "../constants/experiments";
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",
component: paymentMethod,
},
{
path: "/",
name: "root",

View file

@ -20,6 +20,7 @@ const fmgPageValues = {
SCHEDULE: "schedule",
CUSTOMER_DETAILS: "customer-details",
REVIEW: "review",
PAYMENT_METHOD: "payment-method",
};
export { fmgPageValues };

View file

@ -51,6 +51,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

@ -484,6 +484,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 = () => {
@ -66,7 +73,7 @@ const getDefaultState = () => {
lastName: null,
emailAddress: null,
phoneNumber: null,
isSmsOptin: null,
isSmsOptIn: null,
},
damage: {
isRepair: null,
@ -96,8 +103,10 @@ const getDefaultState = () => {
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null,
},
referralNumber: null,
referralSequenceNumber: null,
referralDate: null,
referralCorrelationId: null,
eon: null,
@ -190,6 +199,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;
},
@ -283,6 +295,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 +304,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;
}
},
@ -368,6 +381,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 +425,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;
@ -493,7 +508,7 @@ export const mutations = {
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.isSmsOptIn = sessionInformation.order.customer.isSmsOptIn;
state.order.existingPromoCode = sessionInformation.order.existingPromoCode;
state.applicationUser.experiments = sessionInformation.applicationUser.experiments;
@ -506,6 +521,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 +556,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 +567,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 +782,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 +790,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 +916,7 @@ export const actions = {
context,
{
referralNumber,
referralSequenceNumber,
referralDate,
referralCorrelationId,
eon,
@ -892,6 +926,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 +946,8 @@ export const actions = {
event,
shouldUseSessionId,
experimentsForUser,
referralSequenceNumber,
parentAccountNumber,
}
) {
var payload = {
@ -923,6 +960,8 @@ export const actions = {
event: event,
shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser,
referralSequenceNumber: referralSequenceNumber,
parentAccountNumber: parentAccountNumber,
};
return globalMethods
@ -955,6 +994,8 @@ export const actions = {
value,
shouldUseSessionId,
experimentsForUser,
referralSequenceNumber,
parentAccountNumber,
}
) {
var payload = {
@ -969,6 +1010,8 @@ export const actions = {
value: value,
shouldUseSessionId: shouldUseSessionId,
experimentsForUser: experimentsForUser,
referralSequenceNumber: referralSequenceNumber,
parentAccountNumber: parentAccountNumber,
};
return globalMethods
@ -1417,7 +1460,7 @@ export const actions = {
emailAddress: order.customer.emailAddress,
firstName: order.customer.firstName,
lastName: order.customer.lastName,
isSmsOptin: order.customer.isSmsOptin,
isSmsOptIn: order.customer.isSmsOptIn,
phoneNumber: order.customer.phoneNumber,
},
damage: {
@ -1469,12 +1512,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,
},
},
@ -1624,9 +1668,7 @@ export const actions = {
}
},
saveVehicle(context, { year, make, model, style }) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
saveVehicle(context, { year, make, model, style, vehicle }) {
context.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
if (context.state.order.vehicle.year !== year) {
@ -1641,6 +1683,11 @@ export const actions = {
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_STYLE, style);
}
context.commit(storeMutations.UPDATE_CAR_ID, vehicle.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicle.category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicle.imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicle.imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicle.imageVifColor);
},
saveVehicleDamage(
@ -2064,12 +2111,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 = {
@ -1729,18 +1762,14 @@ describe("Actions", () => {
make: "Honda",
model: "Civic",
style: "Sedan",
vehicle: {
carId: "C00000000",
category: "CAR",
},
};
actions.saveVehicle(context, payload);
//Assert
expect(dispatch).toHaveBeenNthCalledWith(
1,
storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES
);
expect(dispatch).toHaveBeenNthCalledWith(
2,
storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES
);
if (context.state.order.vehicle.year !== payload.year) {
expect(commit).toBeCalledWith(storeMutations.UPDATE_YEAR, payload.year);
}
@ -1753,6 +1782,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.vehicle.carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, payload.vehicle.category);
});
it("saveVehicleDamage, should wipe out damage if different", () => {
@ -3291,8 +3322,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],