Merge branch 'develop' into feature/CSR-1063

This commit is contained in:
Chloe Herd 2023-04-06 13:30:37 -04:00
commit f458384aa8
11 changed files with 490 additions and 240 deletions

View file

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

View file

@ -26,6 +26,7 @@ const errorMessages = {
OPTION_REQUIRED: "Please select an option", OPTION_REQUIRED: "Please select an option",
VEHICLE_REQUIRED: "Please select a vehicle", VEHICLE_REQUIRED: "Please select a vehicle",
MOBILE_LOCATION_REQUIRED: "Please enter your service address", MOBILE_LOCATION_REQUIRED: "Please enter your service address",
DATE_REQUIRED: "Please select a date",
}; };
export { errorMessages }; export { errorMessages };

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="date-picker" :class="calendarViewDirection"> <div class="date-picker text-center" :class="calendarViewDirection">
<fieldset v-if="months"> <fieldset v-if="months">
<legend class="sr-only">Date Picker</legend> <legend class="sr-only">Date Picker</legend>
<div <div
@ -51,7 +51,7 @@
<button <button
v-if="calendarViewDirection === 'future'" v-if="calendarViewDirection === 'future'"
type="button" type="button"
class="btn-link" class="btn btn-link"
@click="goForward"> @click="goForward">
View more dates View more dates
</button> </button>
@ -371,6 +371,7 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.date-picker {
.calendar-grid-container { .calendar-grid-container {
margin: 0 auto; margin: 0 auto;
max-width: 414px; max-width: 414px;
@ -575,6 +576,10 @@ export default {
} }
} }
} }
.btn-link {
font-weight: 500;
text-underline-offset: 4px;
}
.past { .past {
.calendar-grid-container { .calendar-grid-container {
.month-year { .month-year {
@ -631,4 +636,5 @@ export default {
} }
} }
} }
}
</style> </style>

View file

@ -0,0 +1,136 @@
<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 cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
<div class="text-center mt-1 mb-3" v-if="ChangeShopLink.length">
<span v-for="copy in ChangeShopLink" :key="copy">
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
<router-link
:to="{
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
</div>
<date-picker
selectableDates="custom"
v-model="selectedDate"
:customSelectableDatesCallback="getAvailableDates" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</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 loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate";
import datePicker from "@/digital-components/date-picker/date-picker";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
export default {
name: "schedule",
data() {
return {
selectedDate: null,
mockSelectableDatesData: [
{ year: 2023, month: 4, date: 13 },
{ year: 2023, month: 4, date: 14 },
{ year: 2023, month: 4, date: 26 },
{ year: 2023, month: 5, date: 4 },
{ year: 2023, month: 5, date: 5 },
{ year: 2023, month: 5, date: 14 },
{ year: 2023, month: 5, date: 21 },
{ year: 2023, month: 5, date: 23 },
{ year: 2023, month: 5, date: 25 },
{ year: 2023, month: 6, date: 11 },
],
};
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results
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);
});
},
computed: {
ChangeShopLinkText() {
return this.getCmsContent("ChangeShopLink", "Text");
},
ChangeShopLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
},
},
methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
arePagePrerequisitesValid() {
return true;
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
},
getAvailableDates(startDate, endDate) {
return this.mockSelectableDatesData;
},
backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
},
},
components: {
funnelHeader,
funnelFooter,
funnelSubHeader,
Form,
loadingModal,
datePicker,
},
};
</script>
<style lang="scss"></style>

View file

@ -324,6 +324,30 @@ describe("service-location.vue", () => {
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions); expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
}); });
test("resets appointment type selection when service zip code is updated by service zip modal", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
wrapper.vm.$refs.serviceZipCodeQuestion.resetMobileFeePart = jest.fn();
const newServiceZipCodeQuestion = {
zipCode: "61606",
state: "IL",
};
wrapper.vm.selectedAppointmentType = "Inshop";
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
});
// Act
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual(null);
});
test("displays military zip message when zip is updated", () => { test("displays military zip message when zip is updated", () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
@ -438,6 +462,61 @@ describe("service-location.vue", () => {
// Assert // Assert
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo); expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
}); });
test("resets appointment type selection when service zip code is updated by mobile location modal when Mobile is not selected", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
wrapper.vm.$refs.serviceZipCodeQuestion.resetMobileFeePart = jest.fn();
const newServiceZipCodeQuestion = {
zipCode: "61606",
state: "IL",
};
wrapper.vm.selectedAppointmentType = "Dropoff";
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
});
// Act
serviceZipCodeComponent.vm.$emit("update:modelValue", newServiceZipCodeQuestion);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual(null);
});
test("does not reset appointment type selection when service zip code is updated by mobile location modal when Mobile is selected", () => {
// Arrange
const { wrapper } = setupMocks({});
const appointmentTypeMobile = "Mobile";
wrapper.vm.$refs.mobileLocationModalQuestions.resetComponent = jest.fn();
wrapper.vm.$refs.serviceZipCodeQuestion.resetMobileFeePart = jest.fn();
const mobileLocationQuestions = {
addressQuestions: {
streetAddress: "555 Some Street",
apartmentNumberOrBusinessName: "",
city: "Westerville",
state: "OH",
zipCode: "43081",
},
isVehicleProtected: "YesAnswer",
};
wrapper.vm.selectedAppointmentType = "Mobile";
const mobileLocationComponent = wrapper.findComponent({
ref: "mobileLocationModalQuestions",
});
// Act
mobileLocationComponent.vm.$emit("update:modelValue", mobileLocationQuestions);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual(appointmentTypeMobile);
});
}); });
describe("serviceability logic", () => { describe("serviceability logic", () => {

View file

@ -184,6 +184,7 @@ export default {
set: function (newValue) { set: function (newValue) {
if (newValue.zipCode !== this.zipCode) { if (newValue.zipCode !== this.zipCode) {
this.resetMobileLocation(); this.resetMobileLocation();
this.selectedAppointmentType = null;
} }
this.state = newValue.state; this.state = newValue.state;
@ -213,11 +214,15 @@ export default {
this.isVehicleProtected = newValue.isVehicleProtected; this.isVehicleProtected = newValue.isVehicleProtected;
this.mobileLocationValidationField = "isValid"; this.mobileLocationValidationField = "isValid";
if (
newValue.zipCode !== this.zipCode &&
!this.selectedAppointmentType == "Mobile"
) {
this.selectedAppointmentType = null;
}
}, },
}, },
displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile;
},
isServiceableMobile() { isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) { if (this.isRecalibrationServiceableMobile !== null) {
return this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile; return this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile;
@ -249,6 +254,9 @@ export default {
!this.isServiceableMobile !this.isServiceableMobile
); );
}, },
displayMilitaryZipAlert() {
return this.zipContainsMilitaryBase && this.isServiceableMobile;
},
displayServiceableMobileOnly() { displayServiceableMobileOnly() {
return this.isServiceableMobile && !this.isServiceableInshop; return this.isServiceableMobile && !this.isServiceableInshop;
}, },
@ -316,7 +324,10 @@ export default {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
forwardButtonAction() { forwardButtonAction() {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); this.$router.navigateWithoutSaving(
this.navigationScenarios.SELECTED_LOCATION,
this.$route
);
}, },
openModalAction(modalName) { openModalAction(modalName) {
this.$refs[modalName].openModal(); this.$refs[modalName].openModal();

View file

@ -29,7 +29,6 @@ import { experimentTriggers } from "../constants/experiments";
import { applicationConfig } from "../constants/application-config"; import { applicationConfig } from "../constants/application-config";
// Components // Components
import quote from "@/layouts/quote/quote.vue";
import datePicker from "@/digital-components/date-picker/date-picker.vue"; import datePicker from "@/digital-components/date-picker/date-picker.vue";
import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue"; import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue";

View file

@ -16,6 +16,7 @@ const fmgPageValues = {
QUOTE: "quote", QUOTE: "quote",
SERVICE_LOCATION: "service-location", SERVICE_LOCATION: "service-location",
HERITAGE: "heritage", HERITAGE: "heritage",
SCHEDULE: "schedule",
}; };
export { fmgPageValues }; export { fmgPageValues };

View file

@ -45,6 +45,9 @@ const navigationScenarios = {
CLICKED_BACK_WITH_CAPABILITY_QUESTIONS: "CLICKED_BACK_WITH_CAPABILITY_QUESTIONS", CLICKED_BACK_WITH_CAPABILITY_QUESTIONS: "CLICKED_BACK_WITH_CAPABILITY_QUESTIONS",
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS", CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS", CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
// Scheduling
SELECTED_LOCATION: "SELECTED_LOCATION",
}; };
export { navigationScenarios }; export { navigationScenarios };

View file

@ -420,6 +420,19 @@ const routingTable = function (store) {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.QUOTE, destinationFmgPageValue: fmgPageValues.QUOTE,
}, },
{
scenario: navigationScenarios.SELECTED_LOCATION,
destinationFmgPageValue: fmgPageValues.SCHEDULE,
},
],
},
{
fmgPageValue: fmgPageValues.SCHEDULE,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
},
], ],
}, },
]; ];

View file

@ -932,9 +932,9 @@ export const actions = {
method: endpoints.GetServiceabilityDetails.method, method: endpoints.GetServiceabilityDetails.method,
//TODO: Remove Mocky Endpoints //TODO: Remove Mocky Endpoints
//endpoint: "https://run.mocky.io/v3/59e1a644-cf16-4f08-8069-1ab2a1e38f79", // NoShopsAvailable //endpoint: "https://run.mocky.io/v3/59e1a644-cf16-4f08-8069-1ab2a1e38f79", // NoShopsAvailable
endpoint: "https://run.mocky.io/v3/4fe1fb89-dd56-4e4a-9af2-96bd1ab77847", // ForcedInshop //endpoint: "https://run.mocky.io/v3/4fe1fb89-dd56-4e4a-9af2-96bd1ab77847", // ForcedInshop
//endpoint: "https://run.mocky.io/v3/e2eaa097-6ea5-4906-af53-901edaa94939", // ForcedMobile //endpoint: "https://run.mocky.io/v3/e2eaa097-6ea5-4906-af53-901edaa94939", // ForcedMobile
//endpoint: "https://run.mocky.io/v3/1811a1fe-12a7-48f3-939e-d10a9b77dd25", // All Options endpoint: "https://run.mocky.io/v3/1811a1fe-12a7-48f3-939e-d10a9b77dd25", // All Options
}); });
// TODO: Restore this when CSR-1104 is 100% complete // TODO: Restore this when CSR-1104 is 100% complete