Merge develop

This commit is contained in:
Adam Caouette 2023-09-28 14:03:41 -04:00
parent f9fdd405d9
commit 166978bc3a
69 changed files with 2508 additions and 1382 deletions

View file

@ -2,6 +2,7 @@ const queryStrings = {
FMG_PAGE: "fmgPage", FMG_PAGE: "fmgPage",
START_TYPE: "start_type", START_TYPE: "start_type",
ZIP_CODE: "zipcode", ZIP_CODE: "zipcode",
PROMO: "promo",
}; };
export { queryStrings }; export { queryStrings };

View file

@ -192,9 +192,9 @@ export default {
classes = "d-flex flex-row p-0"; classes = "d-flex flex-row p-0";
break; break;
case "listCard": case "listCard":
classes = "row g-2 justify-content-center mb-1"; classes = "row g-2 g-md-5 justify-content-center mb-1";
if (this.isWide) { if (this.isWide) {
classes += " flex-column"; classes += "flex-column";
} }
break; break;
case "radio": case "radio":

View file

@ -68,6 +68,10 @@ export default {
&:focus { &:focus {
box-shadow: 0 0 0 2.5px $blue; box-shadow: 0 0 0 2.5px $blue;
} }
&:checked[type="checkbox"] {
background-color: $blue;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
}
} }
&:hover { &:hover {
.form-check-input { .form-check-input {

View file

@ -148,12 +148,25 @@ export default {
} }
&.modal-component { &.modal-component {
.modal-dialog { .modal-dialog {
max-width: 576px; max-width: 767px;
margin: 0 auto; margin: 0 auto;
@include media-breakpoint-up(md) {
width: 376px;
position: absolute;
top: 0;
right: 0;
transform: translate(100%, 0);
}
.modal-content { .modal-content {
margin: 0 auto; margin: 1.5rem auto 0 auto;
box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25); box-shadow: 0px 16px 48px -16px rgba(0, 0, 0, 0.25);
border-radius: 1.5rem 1.5rem 0 0; border-radius: 1.5rem 1.5rem 0 0;
@include media-breakpoint-up(md) {
box-shadow: none;
border-radius: 0;
height: 100vh;
margin: 0 auto;
}
.modal-body { .modal-body {
.modal-sub-body { .modal-sub-body {
color: $gray-600; color: $gray-600;
@ -166,6 +179,9 @@ export default {
margin-bottom: 0; margin-bottom: 0;
} }
} }
@include media-breakpoint-up(md) {
overflow: scroll;
}
} }
} }
&.modal-dialog-centered { &.modal-dialog-centered {
@ -184,6 +200,11 @@ export default {
&.show .modal-dialog { &.show .modal-dialog {
transform: translateY(0); transform: translateY(0);
} }
@include media-breakpoint-up(md) {
&.fade {
transition: opacity 0.25s linear;
}
}
} }
body { body {
.modal-backdrop { .modal-backdrop {

View file

@ -158,7 +158,6 @@ export default {
} }
.modal { .modal {
&.menu-modal { &.menu-modal {
max-width: 576px;
left: auto; left: auto;
height: calc(100% - 72px); height: calc(100% - 72px);
top: 72px; top: 72px;
@ -175,7 +174,6 @@ export default {
} }
.modal-fullscreen { .modal-fullscreen {
width: 100vw; width: 100vw;
max-width: 576px;
} }
.navigation-link { .navigation-link {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
@ -188,7 +186,7 @@ export default {
position: absolute; position: absolute;
padding: 1.5rem 1rem 1.5rem 1.5rem; padding: 1.5rem 1rem 1.5rem 1.5rem;
right: 0; right: 0;
top: -4.25rem; top: -5rem;
button { button {
border: none; border: none;
&.menu-button { &.menu-button {

View file

@ -1,34 +1,48 @@
<template> <template>
<div :class="`page-container-grouped-styles questions-page`"> <div :class="`questions-page`">
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="container-fluid page-container-grouped-styles">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" /> <div class="row">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <div class="col">
<div class="fade-on-route-transition sub-container make-tall"> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<alert </div>
ref="alertFewMoreQuestions" </div>
cmsWidgetName="alertWidget" <div class="row justify-content-md-center">
class="my-5" <div class="col-md-6 col-xl-4">
alertClass="alert-warning" <vehicleBanner
:manualHeadline="alertFewMoreQuestionsHeader" cmsWidgetName="VehicleBannerWidget"
:manualCopy="alertFewMoreQuestionsCopy" :displayGenericVehicleImage="false" />
v-bind:isDismissible="false" />
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key"> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<questionChain
ref="questionChain" <alert
v-model="selectedAnswers[questionsDatum.answerKey]" ref="alertFewMoreQuestions"
:questionData="questionsDatum.questions" cmsWidgetName="alertWidget"
:index="i" class="my-5"
v-if="showThisQuestionChain(questionsDatum, i)" alertClass="alert-warning"
:answerKey="questionsDatum.answerKey" :manualHeadline="alertFewMoreQuestionsHeader"
:validationRules="validationRules" /> :manualCopy="alertFewMoreQuestionsCopy"
v-bind:isDismissible="false" />
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[questionsDatum.answerKey]"
:questionData="questionsDatum.questions"
:index="i"
v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey"
:validationRules="validationRules" />
</div>
<navbar
ref="navbar"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="handleBackButtonAction"
@ForwardClicked="handleForwardButtonAction" />
</div>
</div> </div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="handleBackButtonAction"
@ForwardClicked="handleForwardButtonAction" />
</div> </div>
</div> </div>
</template> </template>
@ -40,7 +54,7 @@ import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import questionChain from "@/digital-components/question-chain/question-chain"; import questionChain from "@/digital-components/question-chain/question-chain";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
export default { export default {
@ -88,7 +102,7 @@ export default {
alert, alert,
questionChain, questionChain,
funnelSubHeader, funnelSubHeader,
funnelFooter, navbar,
loadingModal, loadingModal,
}, },
}; };

View file

@ -1,7 +1,7 @@
<template> <template>
<div v-if="isModalVisible"> <div v-if="isModalVisible">
<div class="dimmer-overlay"></div> <div v-if="notFullScreen" class="dimmer-overlay"></div>
<div class="container-fluid modal-loader" :class="layoutClass"> <div class="modal-loader" :class="layoutClass">
<div class="row g-2 h-100 d-flex align-items-center"> <div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden"> <div class="container-fluid overflow-hidden">
<div class="row h-100"> <div class="row h-100">
@ -69,7 +69,7 @@ export default {
}, },
computed: { computed: {
layoutClass() { layoutClass() {
return this.notFullScreen ? "card-layout" : "full-screen"; return this.notFullScreen ? "card-layout" : "full-screen container-fluid";
}, },
}, },
methods: { methods: {
@ -123,12 +123,20 @@ export default {
} }
&.card-layout { &.card-layout {
height: 186px; position: fixed;
width: calc(100% - 3rem); top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
position: absolute;
top: calc(50% - 93px); & .text-container {
left: 1.5rem; display: none;
}
& .mb-4 {
margin-bottom: 0;
}
} }
.tagbg { .tagbg {

View file

@ -0,0 +1,59 @@
import { mount } from "@vue/test-utils";
import navbar from "./nav-bar";
describe("nav-bar.vue", () => {
test("Should emit ForwardClicked on button click", async () => {
// Act
const wrapper = mount(navbar, {
mixins: [mockMixin],
});
wrapper.vm.buttonClick();
// Assert
expect(wrapper.emitted()["ForwardClicked"][0]).toHaveBeenCalled;
});
test("Should emit BackClicked on link click", async () => {
// Act
const wrapper = mount(navbar, {
mixins: [mockMixin],
});
wrapper.vm.linkClick();
// Assert
expect(wrapper.emitted()["BackClicked"][0]).toHaveBeenCalled;
});
test("Should change button text when update button text is called", async () => {
// Act
const wrapper = mount(navbar, {
mixins: [mockMixin],
});
wrapper.vm.updateButtonText("newText");
// Assert
expect(wrapper.componentVM.customButtontext).toBe("newText");
});
test("should run removeLoader fn on buttonMain and return false for onkeydown fn", async () => {
// Arrange
const wrapper = mount(navbar, {
mixins: [mockMixin],
});
// Act
wrapper.vm.$refs.buttonMain.removeLoader = jest.fn();
wrapper.vm.removeLoader();
const spy = jest.spyOn(document, "onkeydown");
document.onkeydown();
// Assert
expect(wrapper.vm.$refs.buttonMain.removeLoader).toHaveBeenCalled();
expect(spy).toReturnWith(true);
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(() => 80),
},
};

View file

@ -0,0 +1,140 @@
<template>
<div class="row"></div>
<div class="nav-bar container-fluid g-5 my-5 px-0" id="infoBox">
<div class="row d-flex flex-row-reverse justify-content-between align-items-center vw-100">
<div
:class="['col button-col d-flex', buttonSize ? 'large-button' : 'medium-button']"
id="stacked">
<buttonMain
ref="buttonMain"
isPrimary
:buttonText="buttonText"
loaderColor="white"
:class="isForwardActionDisabled && 'form-test-invalid'"
:aria-disabled="isForwardActionDisabled"
:isDisabled="isForwardActionDisabled"
@click-event="buttonClick"
data-bs-target="#footerModal"
data-test-id="nav-bar-main-button"
data-bs-dismiss="modal" />
</div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
<textLink
linkType="navigation"
:text="backLink"
useLoadingModal
@click-event="linkClick"
href="javascript:void(0)"
data-bs-target="#footerModal"
data-bs-dismiss="modal" />
</div>
</div>
</div>
</template>
<script>
import textLink from "@/ux-components/text-link/text-link";
import buttonMain from "@/ux-components/button-main/button-main";
export default {
name: "navbar",
emits: ["BackClicked", "ForwardClicked"], // <--- should remove oodles of warnings in dev tools
props: {
isForwardActionDisabled: Boolean,
isBackButtonHidden: { type: Boolean, default: false },
cmsWidgetName: String,
buttonSize: { type: Boolean, default: false },
},
components: {
textLink,
buttonMain,
},
data() {
return {
customButtontext: "",
};
},
unmounted() {
document.onkeydown = null;
},
computed: {
backLink() {
return this.getCmsContent(this.cmsWidgetName, "BackButtonText");
},
buttonText() {
return this.customButtontext
? this.customButtontext
: this.getCmsContent(this.cmsWidgetName, "ForwardButtonText");
},
},
methods: {
updateButtonText(newText) {
this.customButtontext = newText;
},
removeLoader() {
this.$refs.buttonMain.removeLoader();
document.onkeydown = function (e) {
return true;
};
},
buttonClick() {
//prevent keyboard input after button click
document.onkeydown = function (e) {
return false;
};
this.$emit("ForwardClicked");
},
linkClick() {
this.$emit("BackClicked");
},
},
};
</script>
<style lang="scss" scoped>
.nav-bar {
overflow: visible;
display: flex;
padding: 0;
a {
display: flex;
justify-content: center;
}
.col,
.col-auto,
.col button {
width: 100%;
}
@media only screen and (min-width: 340px) {
.col,
.col-auto,
.col button {
width: auto;
justify-content: flex-end;
}
.btn-primary {
width: auto;
}
a {
display: flex;
justify-content: flex-start;
}
}
& > .container-fluid {
overflow-x: visible; // needed to fix hidden footer on some iphones
}
.medium-button {
// Define styles for a medium button (default size)
justify-content: flex-end;
width: auto;
}
.large-button {
justify-content: center;
.btn-primary {
width: 100%;
justify-content: center;
}
}
}
</style>

View file

@ -12,8 +12,8 @@ export default {
method, method,
endpoint, endpoint,
payload, payload,
logApiCall = true, logApiCall = false,
isFormData = false, pageNameToLog = null,
additionalSuccessEventDataHandler, additionalSuccessEventDataHandler,
}) { }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -37,13 +37,11 @@ export default {
if (additionalSuccessEventDataHandler) { if (additionalSuccessEventDataHandler) {
additionalEventData = "_" + additionalSuccessEventDataHandler(response); additionalEventData = "_" + additionalSuccessEventDataHandler(response);
} }
const pageName = analyticsMixIn.methods.getPageName();
const nextPageName = router.lastNavigationPage || pageName;
const endpointWithoutParams = const endpointWithoutParams =
analyticsMixIn.methods.removeParamsFromEndpoint(endpoint); analyticsMixIn.methods.removeParamsFromEndpoint(endpoint);
analyticsMixIn.methods.pushEventToGA( analyticsMixIn.methods.pushEventToGA(
GaCategories.API_RESPONSE, GaCategories.API_RESPONSE,
`${nextPageName}_${endpointWithoutParams}`, `${pageNameToLog}_${endpointWithoutParams}`,
`${GaLabels.SUCCESS}${additionalEventData}`, `${GaLabels.SUCCESS}${additionalEventData}`,
true true
); );

View file

@ -54,10 +54,11 @@ export function includesWindshieldReplacement() {
return windshieldMatches.length > 0; return windshieldMatches.length > 0;
} }
export async function isGlassAvailableForCarId(carId) { export async function isGlassAvailableForCarId(carId, pageNameToLog) {
const newGlassOptions = await baseMixin.methods.dispatchStoreAction( const newGlassOptions = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_DAMAGE_OPTIONS, storeActions.GET_DAMAGE_OPTIONS,
{ carId: carId } { carId: carId },
pageNameToLog
); );
const currentGlassOptions = store.getters.damage.glassToReplace; const currentGlassOptions = store.getters.damage.glassToReplace;

View file

@ -11,6 +11,13 @@ jest.mock("@/mixins/base-mixin.js", () => ({
}, },
}; };
}), }),
dispatchStoreActionWithLogging: jest.fn().mockImplementation(() => {
return {
data: {
windshieldOptions: { availableReplacementOptions: ["windshield"] },
},
};
}),
}, },
})); }));
@ -77,7 +84,7 @@ describe("damage-helper.js", () => {
]; ];
// Act // Act
const isGlassAvailable = await isGlassAvailableForCarId(); const isGlassAvailable = await isGlassAvailableForCarId("id", "testName");
// Assert // Assert
expect(isGlassAvailable).toEqual(true); expect(isGlassAvailable).toEqual(true);
@ -91,7 +98,7 @@ describe("damage-helper.js", () => {
{ glassLocation: "Windshield", glassName: "sideWindow" }, { glassLocation: "Windshield", glassName: "sideWindow" },
]; ];
const isGlassAvailable = await isGlassAvailableForCarId(); const isGlassAvailable = await isGlassAvailableForCarId("id", "testName");
// Assert // Assert
expect(isGlassAvailable).toEqual(false); expect(isGlassAvailable).toEqual(false);

View file

@ -0,0 +1,10 @@
export function getDateDifferenceInDays(startDate, endDate) {
var date1 = new Date(endDate);
date1.setHours(0, 0, 0, 0);
var date2 = new Date(startDate);
date2.setHours(0, 0, 0, 0);
// To calculate the time difference of two dates
var Difference_In_Time = date1.getTime() - date2.getTime();
// To calculate the no. of days between two dates
return Difference_In_Time / (1000 * 3600 * 24);
}

View file

@ -1,4 +1,5 @@
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { externalUrls } from "@/router/router-constants/externalUrl-values"; import { externalUrls } from "@/router/router-constants/externalUrl-values";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { saveSession } from "@/helpers/heritage-integration/order-helper.js"; import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
@ -16,15 +17,11 @@ import router from "@/router";
drop them so they don't start at the beginning again. This method will return 'heritage' if drop them so they don't start at the beginning again. This method will return 'heritage' if
the user has an existing order and they come back in from the Safelite.com CTA. the user has an existing order and they come back in from the Safelite.com CTA.
*/ */
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) { export async function getPageToRouteExistingOrderTo(toRoute = {}) {
// If the user is coming in via the Safelite.Com CTA // If the user is coming in via the Safelite.Com CTA
if (toRoute.query[queryStrings.START_TYPE] === "fmg") { if (toRoute.query[queryStrings.START_TYPE] === "fmg") {
// If they have an existing order, return 'heritage' for the page name. const latestPageRoute = await getLatestPageForRedirection();
if (existingHeritageOrder) { return latestPageRoute;
return fmgPageValues.HERITAGE;
}
return await getLatestPageForRedirection();
} }
// If navigating to a specific page, and that page is not part of the vin pages. // If navigating to a specific page, and that page is not part of the vin pages.
@ -41,7 +38,6 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
// If this is not a direct link to a page using fmgPage, not from Safelite.com CTA or this is a vin related page. // If this is not a direct link to a page using fmgPage, not from Safelite.com CTA or this is a vin related page.
// Get the latest page for redirection. // Get the latest page for redirection.
const latestPageRoute = await getLatestPageForRedirection(); const latestPageRoute = await getLatestPageForRedirection();
return latestPageRoute; return latestPageRoute;
} }
@ -49,31 +45,41 @@ export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHerita
Used to navigate to the heritage funnel with the correct query string and url. Used to navigate to the heritage funnel with the correct query string and url.
*/ */
export async function navigateToHeritageFunnel({ shouldSaveSession = true, loadingModal }) { export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLog, loadingModal }) {
// Create the order (or save existing order) when navigating to Heritage Funnel. // Create the order (or save existing order) when navigating to Heritage Funnel.
if (shouldSaveSession) { if (shouldSaveSession) {
await saveSession({ shouldAwaitSaveSessionQueue: true }); await saveSession({ pageNameToLog: pageNameToLog, shouldAwaitSaveSessionQueue: true });
} }
if (loadingModal && loadingModal.showModal) { if (loadingModal && loadingModal.showModal) {
loadingModal.showModal(); loadingModal.showModal();
} }
router.navigateToExternalUrl(externalUrls.HERITAGE_FUNNEL, { var heritageParms = {
corid: store.getters.order.referralCorrelationId, corid: store.getters.order.referralCorrelationId,
src: "concept-funnel", src: "concept-funnel",
conceptsqid: store.getters.applicationUser.savedSessionId, conceptsqid: store.getters.applicationUser.savedSessionId,
isInsurance: store.getters.payment.isInsurance, isInsurance: store.getters.payment.isInsurance,
}); };
const promo = getQuerystringParameter(queryStrings.PROMO);
if (promo) {
heritageParms["promo"] = promo;
}
router.navigateToExternalUrl(externalUrls.HERITAGE_FUNNEL, heritageParms);
} }
export async function skipVinLookup() { export async function skipVinLookup() {
if (store.getters.damage.isRepair) {
return true;
}
const isVinOptionalVehicle = store.getters.order.vehicle.make const isVinOptionalVehicle = store.getters.order.vehicle.make
? await store.dispatch(storeActions.IS_VIN_OPTIONAL_VEHICLE) ? await store.dispatch(storeActions.IS_VIN_OPTIONAL_VEHICLE)
: false; : false;
return ( return (
store.getters.damage.isRepair ||
isVinOptionalVehicle || isVinOptionalVehicle ||
!includesWindshieldReplacement() || !includesWindshieldReplacement() ||
experimentMixin.methods.hasSettingEqualTo(experimentSettings.SUPPRESS_VIN_CAPTURE, "true") experimentMixin.methods.hasSettingEqualTo(experimentSettings.SUPPRESS_VIN_CAPTURE, "true")
@ -81,18 +87,18 @@ export async function skipVinLookup() {
} }
export async function skipVinLookupNotRepair() { export async function skipVinLookupNotRepair() {
if (store.getters.damage.isRepair) {
return false;
}
const isVinOptionalVehicle = store.getters.order.vehicle.make const isVinOptionalVehicle = store.getters.order.vehicle.make
? await store.dispatch(storeActions.IS_VIN_OPTIONAL_VEHICLE) ? await store.dispatch(storeActions.IS_VIN_OPTIONAL_VEHICLE)
: false; : false;
return ( return (
!store.getters.damage.isRepair && isVinOptionalVehicle ||
(isVinOptionalVehicle || !includesWindshieldReplacement() ||
!includesWindshieldReplacement() || experimentMixin.methods.hasSettingEqualTo(experimentSettings.SUPPRESS_VIN_CAPTURE, "true")
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.SUPPRESS_VIN_CAPTURE,
"true"
))
); );
} }

View file

@ -36,7 +36,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.VEHICLE); expect(result).toBe(fmgPageValues.VEHICLE);
@ -59,7 +59,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.VEHICLE_DAMAGE); expect(result).toBe(fmgPageValues.VEHICLE_DAMAGE);
@ -97,7 +97,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.ESTIMATE); expect(result).toBe(fmgPageValues.ESTIMATE);
@ -125,7 +125,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.ESTIMATE); expect(result).toBe(fmgPageValues.ESTIMATE);
@ -153,7 +153,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.CAPABILITY_QUESTIONS); expect(result).toBe(fmgPageValues.CAPABILITY_QUESTIONS);
@ -181,7 +181,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.MOLDING_QUESTIONS); expect(result).toBe(fmgPageValues.MOLDING_QUESTIONS);
@ -209,7 +209,7 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.VEHICLE_PARTS); expect(result).toBe(fmgPageValues.VEHICLE_PARTS);
@ -237,29 +237,11 @@ describe("getPageToRouteExistingOrderTo", () => {
}); });
// Act // Act
const result = await getPageToRouteExistingOrderTo(toRoute, false); const result = await getPageToRouteExistingOrderTo(toRoute);
//Assert //Assert
expect(result).toBe(fmgPageValues.PART_QUESTIONS); expect(result).toBe(fmgPageValues.PART_QUESTIONS);
}); });
test("existing order > should return heritage", async () => {
// Arrange
const toRoute = {
query: {
[queryStrings.START_TYPE]: "fmg",
},
};
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
store.commit(storeMutations.UPDATE_MAKE, "acura");
// Act
const result = await getPageToRouteExistingOrderTo(toRoute, true);
// Assert
expect(result).toBe(fmgPageValues.HERITAGE);
});
}); });
describe("navigateToHeritageFunnel", () => { describe("navigateToHeritageFunnel", () => {
@ -295,7 +277,7 @@ describe("navigateToHeritageFunnel", () => {
router.navigateToExternalUrl = jest.fn(); router.navigateToExternalUrl = jest.fn();
// Act // Act
await navigateToHeritageFunnel({}); await navigateToHeritageFunnel({ loadingModal: null, shouldSaveSession: true });
// Assert // Assert
expect(saveSessionFunction).toHaveBeenCalled(); expect(saveSessionFunction).toHaveBeenCalled();

View file

@ -14,7 +14,7 @@ import { storeMutations } from "@/constants/store-mutations";
it will reset the state and go back to the start of the funnel. it will reset the state and go back to the start of the funnel.
*/ */
export async function loadSessionIfPresent(isConceptInsurance) { export async function loadSessionIfPresent(isConceptInsurance, pageNameToLog) {
const funnelCookie = getFunnelCookie(); const funnelCookie = getFunnelCookie();
// Do nothing if there is no cookie or session to use for loading. // Do nothing if there is no cookie or session to use for loading.
@ -42,7 +42,8 @@ export async function loadSessionIfPresent(isConceptInsurance) {
funnelCookie.ReferralDate, funnelCookie.ReferralDate,
funnelCookie.ReferralParentAccountNumber, funnelCookie.ReferralParentAccountNumber,
funnelCookie.ReferralCorrelationId, funnelCookie.ReferralCorrelationId,
isConceptInsurance isConceptInsurance,
pageNameToLog
) )
)?.data; )?.data;
} }
@ -52,17 +53,17 @@ export async function loadSessionIfPresent(isConceptInsurance) {
This will also set Referral information in the store after saving, and then This will also set Referral information in the store after saving, and then
update the cookie. To force synchronous behavior pass in 'true' for shouldAwaitSaveSessionQueue update the cookie. To force synchronous behavior pass in 'true' for shouldAwaitSaveSessionQueue
*/ */
export async function saveSession({ shouldAwaitSaveSessionQueue = false }) { export async function saveSession({ pageNameToLog, shouldAwaitSaveSessionQueue = false }) {
var saveSessionPromise; var saveSessionPromise;
if (store.getters.applicationUser.saveSessionPromise) { if (store.getters.applicationUser.saveSessionPromise) {
// queue newest request after current saveSessionPromise resolves // queue newest request after current saveSessionPromise resolves
saveSessionPromise = store.getters.applicationUser.saveSessionPromise.then(() => { saveSessionPromise = store.getters.applicationUser.saveSessionPromise.then(() => {
// get a new saveSessionPromise // get a new saveSessionPromise
return saveSessionHelper(); return saveSessionHelper(pageNameToLog);
}); });
} else { } else {
// create an initial saveSessionPromise // create an initial saveSessionPromise
saveSessionPromise = saveSessionHelper(); saveSessionPromise = saveSessionHelper(pageNameToLog);
} }
store.commit(storeMutations.UPDATE_SAVE_SESSION_PROMISE, saveSessionPromise); store.commit(storeMutations.UPDATE_SAVE_SESSION_PROMISE, saveSessionPromise);
// await here to allow for a caller to await and make the function synchronous // await here to allow for a caller to await and make the function synchronous
@ -83,12 +84,13 @@ async function loadSession(
referralDate, referralDate,
parentAccountNumber, parentAccountNumber,
referralCorrelationId, referralCorrelationId,
isConceptInsurance isConceptInsurance,
pageNameToLog
) { ) {
// await the saveSessionPromise in the store to make sure we're loading up to date information // await the saveSessionPromise in the store to make sure we're loading up to date information
await store.getters.applicationUser.saveSessionPromise; await store.getters.applicationUser.saveSessionPromise;
const response = await baseMixin.methods.dispatchStoreAction( const response = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.LOAD_SESSION, storeActions.LOAD_SESSION,
{ {
savedSessionId: savedSessionId?.toString(), savedSessionId: savedSessionId?.toString(),
@ -98,6 +100,7 @@ async function loadSession(
parentAccountNumber, parentAccountNumber,
isConceptInsurance, isConceptInsurance,
}, },
pageNameToLog,
false false
); );
@ -107,8 +110,12 @@ async function loadSession(
/* /*
Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing Encapsulates asynchronous Save Session logic inside a promise to allow for Save Session queuing
*/ */
async function saveSessionHelper() { async function saveSessionHelper(pageNameToLog) {
const savedSessionInfo = await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_SESSION); const savedSessionInfo = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.SAVE_SESSION,
null,
pageNameToLog
);
// Update the store with information received from the saveSession response // Update the store with information received from the saveSession response
await baseMixin.methods.dispatchStoreAction( await baseMixin.methods.dispatchStoreAction(
storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE, storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,

View file

@ -170,11 +170,13 @@ describe("saveSession", () => {
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
// Act // Act
await saveSession({}); await saveSession({ pageNameToLog: "test" });
// Assert // Assert
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith( expect(mocks.baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
storeActions.SAVE_SESSION storeActions.SAVE_SESSION,
null,
"test"
); );
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith( expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE, storeActions.UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE,
@ -231,7 +233,7 @@ describe("saveSession", () => {
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) }); setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
// Act // Act
await saveSession({}); await saveSession({ pageNameToLog: "test" });
// Assert // Assert
expect(cookieHelper.getFunnelCookie().DidHeritageFunnelUpdateLast).toEqual(false); expect(cookieHelper.getFunnelCookie().DidHeritageFunnelUpdateLast).toEqual(false);

View file

@ -44,6 +44,16 @@ export function getMountOptions(mockData) {
}); });
} }
}); });
mocks.dispatchStoreActionWithLogging = jest.fn();
mocks.dispatchStoreActionWithLogging.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList?.filter((x) => x.actionName == actionName);
if (actionFilterResult?.length === 1) {
return Promise.resolve({
data: actionFilterResult[0].data,
});
}
});
// Mock const files // Mock const files
mocks.storeActions = storeActions; mocks.storeActions = storeActions;
@ -141,5 +151,16 @@ function setupBaseMixinDispatchStoreAction(mockData) {
}); });
} }
}); });
baseMixin.methods.dispatchStoreActionWithLogging = jest.fn();
baseMixin.methods.dispatchStoreActionWithLogging.mockImplementation((actionName) => {
let actionFilterResult = mockData.actionList.filter((x) => x.actionName == actionName);
if (actionFilterResult.length > 0 && actionFilterResult.length === 1) {
return Promise.resolve({
data: actionFilterResult[0].data,
});
}
});
} }
} }

View file

@ -471,7 +471,7 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction(); await wrapper.vm.forwardButtonAction();
// Assert // Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith( expect(wrapper.vm.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
"lookupVinByAddress", "lookupVinByAddress",
{ {
licenseLastName: undefined, licenseLastName: undefined,
@ -479,12 +479,17 @@ describe("address-lookup.vue", () => {
licenseStreetAddress: "1234 Main St", licenseStreetAddress: "1234 Main St",
licenseZip: "43215", licenseZip: "43215",
}, },
"address-lookup",
false false
); );
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith("validateZip", { expect(wrapper.vm.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
zip: "43215", "validateZip",
}); {
zip: "43215",
},
"address-lookup"
);
}); });
}); });
@ -722,8 +727,8 @@ function setupMocks({
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
return { wrapper }; return { wrapper };
} }

View file

@ -5,75 +5,92 @@
ref="theForm" ref="theForm"
v-slot="{ meta }" v-slot="{ meta }"
autocomplete="off"> autocomplete="off">
<div class="page-container-grouped-styles"> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
ref="vehicleBanner" </div>
:displayGenericVehicleImage="false" /> </div>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" /> <div class="row justify-content-md-center">
<div class="fade-on-route-transition sub-container make-tall"> <div class="col-md-6 col-xl-4">
<customerQuestions ref="customerQuestions" v-model="customerQuestions" /> <vehicleBanner
<alert cmsWidgetName="VehicleBannerWidget"
ref="alertVinNotFound" ref="vehicleBanner"
v-if="displayVinNotFoundAlert" :displayGenericVehicleImage="false" />
class="mb-4"
cmsWidgetName="AlertVinNotFoundWidget" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
alertClass="alert-danger"
v-bind:isDismissible="false" /> <customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert
ref="alertMatchedDifferentVehicle" <alert
v-if="displayMatchedDifferentVehicleAlert" ref="alertVinNotFound"
class="mb-4" v-if="displayVinNotFoundAlert"
:manualHeadline="AlertMatchedDifferentVehicleHeader" class="mb-4"
:manualCopy="AlertMatchedDifferentVehicleBody" cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-warning" alertClass="alert-danger"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
<alert
ref="alertInvalidZip" <alert
v-if="displayInvalidZipAlert" ref="alertMatchedDifferentVehicle"
class="mb-4" v-if="displayMatchedDifferentVehicleAlert"
cmsWidgetName="AlertInvalidZipWidget" class="mb-4"
alertClass="alert-danger" :manualHeadline="AlertMatchedDifferentVehicleHeader"
v-bind:isDismissible="false" /> :manualCopy="AlertMatchedDifferentVehicleBody"
<alert alertClass="alert-warning"
ref="alertNonServiceableZip" v-bind:isDismissible="false" />
v-if="displayNonServiceableZipAlert"
class="mb-4" <alert
alertClass="alert-danger" ref="alertInvalidZip"
:manualHeadline="AlertNonServiceableZipHeader" v-if="displayInvalidZipAlert"
:manualCopy="AlertNonServiceableZipBody" class="mb-4"
v-bind:isDismissible="false" /> cmsWidgetName="AlertInvalidZipWidget"
<alert alertClass="alert-danger"
ref="alertVinLookupsByHomeAddressNotAllowed" v-bind:isDismissible="false" />
v-if="displayVinLookupByHomeAddressNotAllowedAlert"
class="mb-4" <alert
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget" ref="alertNonServiceableZip"
alertClass="alert-danger" v-if="displayNonServiceableZipAlert"
v-bind:isDismissible="false" /> class="mb-4"
<transition name="fade" mode="out-in"> alertClass="alert-danger"
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite"> :manualHeadline="AlertNonServiceableZipHeader"
<div class="row mt-4 mb-1"> :manualCopy="AlertNonServiceableZipBody"
<div class="col"> v-bind:isDismissible="false" />
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget" <alert
v-model="serviceZipCode" ref="alertVinLookupsByHomeAddressNotAllowed"
ref="serviceZip" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
customInputId="serviceZip" class="mb-4"
aria-haspopup="" cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
mask="#####" alertClass="alert-danger"
validationRules="service-zip-required|service-zip-format" /> v-bind:isDismissible="false" />
<transition name="fade" mode="out-in">
<div
class="service-zip-field"
v-if="showServiceZipField"
aria-live="polite">
<div class="row mt-4 mb-1">
<div class="col">
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
ref="serviceZip"
customInputId="serviceZip"
aria-haspopup=""
mask="#####"
validationRules="service-zip-required|service-zip-format" />
</div>
</div> </div>
</div> </div>
</div> </transition>
</transition> <navbar
<funnel-footer cmsWidgetName="FunnelFooterWidget"
cmsWidgetName="FunnelFooterWidget" ref="navbar"
ref="funnelFooter" :isDisabled="!meta.valid"
:isDisabled="!meta.valid" @ForwardClicked="forwardButtonAction"
@ForwardClicked="forwardButtonAction" @back-clicked="backButtonAction"
@back-clicked="backButtonAction" :isForwardActionDisabled="!meta.valid" />
:isForwardActionDisabled="!meta.valid" /> </div>
</div> </div>
</div> </div>
</Form> </Form>
@ -82,7 +99,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions"; import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
@ -207,7 +224,7 @@ export default {
// Vehicle info change in the flow, use a variable to keep track and commit to state at the end. // Vehicle info change in the flow, use a variable to keep track and commit to state at the end.
let vehicleInfoToCommit = {}; let vehicleInfoToCommit = {};
const vinLookupResponse = this.dispatchStoreAction( const vinLookupResponse = this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VIN_BY_ADDRESS, storeActions.LOOKUP_VIN_BY_ADDRESS,
{ {
licenseLastName: this.customerQuestions.lastName, licenseLastName: this.customerQuestions.lastName,
@ -215,6 +232,7 @@ export default {
licenseZip: this.customerQuestions.addressQuestions.zipCode, licenseZip: this.customerQuestions.addressQuestions.zipCode,
licenseState: this.customerQuestions.addressQuestions.state, licenseState: this.customerQuestions.addressQuestions.state,
}, },
"address-lookup",
false false
); );
@ -227,12 +245,20 @@ export default {
{ {
resultKey: "serviceZipValidationResponse", resultKey: "serviceZipValidationResponse",
promise: this.serviceZipCode promise: this.serviceZipCode
? this.dispatchStoreAction(storeActions.VALIDATE_ZIP, { ? this.dispatchStoreActionWithLogging(
zip: this.serviceZipCode, storeActions.VALIDATE_ZIP,
}) {
: this.dispatchStoreAction(storeActions.VALIDATE_ZIP, { zip: this.serviceZipCode,
zip: this.customerQuestions.addressQuestions.zipCode, },
}), "address-lookup"
)
: this.dispatchStoreActionWithLogging(
storeActions.VALIDATE_ZIP,
{
zip: this.customerQuestions.addressQuestions.zipCode,
},
"address-lookup"
),
}, },
]; ];
@ -241,14 +267,14 @@ export default {
if (!resultMap.vinLookupResponse.isStatePermissible) { if (!resultMap.vinLookupResponse.isStatePermissible) {
// State Restrictions forbid lookup by address // State Restrictions forbid lookup by address
this.displayVinLookupByHomeAddressNotAllowedAlert = true; this.displayVinLookupByHomeAddressNotAllowedAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// If a Service Zip is entered and it is an invalid zip code (ex. 11111) then show an alert // If a Service Zip is entered and it is an invalid zip code (ex. 11111) then show an alert
const isZipValid = resultMap.serviceZipValidationResponse.isValid; const isZipValid = resultMap.serviceZipValidationResponse.isValid;
if (this.serviceZipCode && !isZipValid) { if (this.serviceZipCode && !isZipValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
@ -267,14 +293,15 @@ export default {
this.displayMatchedDifferentVehicleAlert = true; this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId( this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
carFound.carId carFound.carId,
"address-lookup"
); );
// Update button "Continue with..." // Update button "Continue with..."
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
`Continue with ${carFound.year} ${carFound.make} ${carFound.model}` `Continue with ${carFound.year} ${carFound.make} ${carFound.model}`
); );
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// update data if the zip or service zip is serviceable // update data if the zip or service zip is serviceable
@ -294,7 +321,7 @@ export default {
} else { } else {
// No VINS found. // No VINS found.
this.displayVinNotFoundAlert = true; this.displayVinNotFoundAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// If the either the registration zip code or service zip code are not serviceable // If the either the registration zip code or service zip code are not serviceable
@ -302,7 +329,7 @@ export default {
if (!this.isZipServiceable) { if (!this.isZipServiceable) {
this.displayNonServiceableZipAlert = true; this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true; this.showServiceZipField = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// If the registration zip code is serviceable and nothing was entered for the service zip code // If the registration zip code is serviceable and nothing was entered for the service zip code
@ -320,7 +347,7 @@ export default {
Object.keys(vehicleInfoToCommit).length === 0 Object.keys(vehicleInfoToCommit).length === 0
? this.$store.getters.vehicle ? this.$store.getters.vehicle
: vehicleInfoToCommit, : vehicleInfoToCommit,
registrationInfo: { customerInfo: {
firstName: this.customerQuestions.firstName, firstName: this.customerQuestions.firstName,
lastName: this.customerQuestions.lastName, lastName: this.customerQuestions.lastName,
}, },
@ -420,7 +447,7 @@ export default {
customerQuestions: { customerQuestions: {
handler(newValue) { handler(newValue) {
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to "Get my personalized quote" // if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to "Get my personalized quote"
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
this.showServiceZipField = false; this.showServiceZipField = false;
@ -445,7 +472,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
customerQuestions, customerQuestions,

View file

@ -76,8 +76,8 @@ describe("addressVehicles.vue", () => {
}; };
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked // the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse)); wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigate = jest.fn(); wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(() => {}); wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(() => {});
@ -106,8 +106,8 @@ describe("addressVehicles.vue", () => {
}; };
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked // the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse)); wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse));
wrapper.vm.$router.navigateWithSaving = jest.fn(); wrapper.vm.$router.navigateWithSaving = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(() => {}); wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(() => {});
@ -128,7 +128,7 @@ describe("addressVehicles.vue", () => {
test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => { test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$router.navigateWithSaving = jest.fn(); wrapper.vm.$router.navigateWithSaving = jest.fn();
// Act // Act
@ -148,7 +148,7 @@ describe("addressVehicles.vue", () => {
test("carId is not different on navigateForward (car was found) => Should handle navigating forward with car match", async () => { test("carId is not different on navigateForward (car was found) => Should handle navigating forward with car match", async () => {
// Arrange // Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
navigateToHeritage.navigateToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
// Act // Act
@ -224,7 +224,7 @@ function setupMocks({}) {
const wrapper = shallowMount(addressVehicles, mountOptions); const wrapper = shallowMount(addressVehicles, mountOptions);
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn(); wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
return { wrapper }; return { wrapper };

View file

@ -1,38 +1,51 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" /> </div>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> </div>
<alert <div class="row justify-content-md-center">
ref="alertFoundMultipleVehicles" <div class="col-md-6 col-xl-4">
class="my-5" <vehicleBanner
alertClass="alert-warning" cmsWidgetName="VehicleBannerWidget"
:manualHeadline="AlertFoundMultipleVehiclesHeader" :displayGenericVehicleImage="false" />
manualCopy=""
v-bind:isDismissible="false" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<addressVehiclesQuestion
ref="addressVehiclesQuestion" <alert
cmsWidgetName="VehicleConfirmationQuestion" ref="alertFoundMultipleVehicles"
:vehicles="VehiclesForQuestions" class="my-5"
validationRules="vehicle-required" alertClass="alert-warning"
v-model="selectedVehicleVin" :manualHeadline="AlertFoundMultipleVehiclesHeader"
:isCarIdDifferent="isCarIdDifferent" /> manualCopy=""
<div class="alert-provide-vin" v-if="splitAlertProvideVinBodyForLink.length"> v-bind:isDismissible="false" />
<textBlock
:customText="AlertProvideVinBody" <addressVehiclesQuestion
justifyText="left" ref="addressVehiclesQuestion"
class="mb-3" cmsWidgetName="VehicleConfirmationQuestion"
marginTopSizeOverride="3" /> :vehicles="VehiclesForQuestions"
validationRules="vehicle-required"
v-model="selectedVehicleVin"
:isCarIdDifferent="isCarIdDifferent" />
<div class="alert-provide-vin" v-if="splitAlertProvideVinBodyForLink.length">
<textBlock
:customText="AlertProvideVinBody"
justifyText="left"
class="mb-3"
marginTopSizeOverride="3" />
</div>
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div> </div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</Form> </Form>
</template> </template>
@ -40,7 +53,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
@ -162,10 +175,14 @@ export default {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
async forwardButtonAction() { async forwardButtonAction() {
const vinLookup = await this.dispatchStoreAction(storeActions.LOOKUP_VEHICLE_BY_VIN, { const vinLookup = await this.dispatchStoreActionWithLogging(
vin: this.selectedVehicle.vin, storeActions.LOOKUP_VEHICLE_BY_VIN,
}).catch(() => { {
this.$refs.funnelFooter.removeLoader(); vin: this.selectedVehicle.vin,
},
"address-vehicles"
).catch(() => {
this.$refs.navbar.removeLoader();
}); });
if (!vinLookup) { if (!vinLookup) {
@ -173,7 +190,8 @@ export default {
} }
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId( this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
vinLookup.data.carId vinLookup.data.carId,
"address-vehicles"
); );
await this.dispatchStoreAction( await this.dispatchStoreAction(
@ -210,11 +228,11 @@ export default {
this.isCarIdDifferent = this.isCarIdDifferent =
this.selectedVehicle?.vehicle.carId !== store.getters.vehicle.carId; this.selectedVehicle?.vehicle.carId !== store.getters.vehicle.carId;
if (this.isCarIdDifferent) { if (this.isCarIdDifferent) {
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}` `Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`
); );
} else { } else {
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
} }
@ -229,7 +247,7 @@ export default {
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
alert, alert,
funnelFooter, navbar,
addressVehiclesQuestion, addressVehiclesQuestion,
textBlock, textBlock,
}, },

View file

@ -237,6 +237,11 @@ describe("capabilityQuestions.vue", () => {
data: [], data: [],
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: [],
};
});
// Act // Act
wrapper.vm.forwardButtonAction(); wrapper.vm.forwardButtonAction();
@ -272,6 +277,11 @@ describe("capabilityQuestions.vue", () => {
data: [], data: [],
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: [],
};
});
const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction"); const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction");
// Act // Act
@ -325,7 +335,12 @@ describe("capabilityQuestions.vue", () => {
data: [], data: [],
}; };
}); });
const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction"); wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: [],
};
});
const spy = jest.spyOn(wrapper.vm, "dispatchStoreActionWithLogging");
// Act // Act
wrapper.vm.forwardButtonAction(); wrapper.vm.forwardButtonAction();
@ -334,9 +349,10 @@ describe("capabilityQuestions.vue", () => {
//Assert //Assert
expect(spy).toHaveBeenNthCalledWith( expect(spy).toHaveBeenNthCalledWith(
2, 1,
"getPartFromCapabilityQuestionAnswer", "getPartFromCapabilityQuestionAnswer",
"Windshield", "Windshield",
"capability-questions",
false false
); );
@ -368,6 +384,11 @@ describe("capabilityQuestions.vue", () => {
data: [], data: [],
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: [],
};
});
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Act // Act

View file

@ -162,9 +162,10 @@ export default {
const partsOrQuestions = this.partsOrQuestionsData; const partsOrQuestions = this.partsOrQuestionsData;
for (let answer of questionAnswersArray) { for (let answer of questionAnswersArray) {
const partFromCapabilityQuestionAnswer = ( const partFromCapabilityQuestionAnswer = (
await this.dispatchStoreAction( await this.dispatchStoreActionWithLogging(
this.storeActions.GET_PART_FROM_CAPABILITY_QUESTION_ANSWER, this.storeActions.GET_PART_FROM_CAPABILITY_QUESTION_ANSWER,
answer.glassLocation, answer.glassLocation,
"capability-questions",
false false
) )
).data; ).data;

View file

@ -0,0 +1,101 @@
// Components
import confirmation from "@/layouts/confirmation/confirmation.vue";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { nextTick } from "vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
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: {
address: "",
address2: null,
city: "",
state: "AZ",
appointmentType: "Inshop",
zipCode: "12345",
zipCodeCtu: "01234",
provider: {
providerNumber: "123",
address: {
streetAddress: "test1",
city: "test",
state: "AZ",
zipCode: "12345",
zipCodeCtu: "01234",
},
},
},
damage: {
isRepair: false,
},
referralNumber: "1234567",
},
payment: {
isInsurance: true,
},
lineItems: {
glassParts: [],
supportingItems: [],
},
};
});
afterEach(() => {
store.getters = {};
jest.restoreAllMocks();
jest.clearAllMocks();
});
describe("confirmation.vue", () => {
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks();
//Act
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
function setupMocks() {
const wrapper = shallowMount(
confirmation,
getMountOptions({
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
})
);
return { wrapper };
}

View file

@ -0,0 +1,87 @@
<template>
<div class="page-container-grouped-styles position-relative">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div>
<div class="container-fluid pb-2">
<div class="row justify-content-md-center">
<div class="col-md-6 col-xl-4">
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
isBackButtonHidden="true"
@ForwardClicked="forwardButtonAction"
:buttonSize="true" />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
// Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import navbar from "@/fmg-components/nav-bar/nav-bar";
//Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { AppointmentTypeStrings } from "@/constants/schedule-constants";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
export default {
name: "confirmation",
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);
});
},
methods: {
arePagePrerequisitesValid() {
// Service Location
const serviceLocation = store.getters.order.serviceLocation;
const mobileReqs = !!(
serviceLocation.address &&
serviceLocation.city &&
serviceLocation.state &&
serviceLocation.zipCode
);
const providerLocation = serviceLocation.provider.address;
const dropOffInshopReqs = !!(
providerLocation.streetAddress &&
providerLocation.city &&
providerLocation.state &&
providerLocation.zipCode
);
const isMobile = serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE;
const serviceLocationReqs =
(isMobile && mobileReqs) || (!isMobile && dropOffInshopReqs);
// Schedule
const schedule = store.getters.order.schedule;
const scheduleReqs = !!(schedule.date && schedule.startTime); //removing schedule.endTime for testing
return serviceLocationReqs && scheduleReqs;
},
forwardButtonAction() {},
},
components: {
funnelHeader,
navbar,
},
};
</script>

View file

@ -1,57 +1,72 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <div class="container-fluid">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="my-5" /> <div class="col">
<textboxQuestion <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
class="mb-4" </div>
cmsWidgetName="FirstNameWidget" </div>
v-model="firstName" <div class="row justify-content-md-center">
ref="firstName" <div class="col-md-6 col-xl-4">
customInputId="firstName" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="my-5" />
validationRules="first-name-required" /> <textboxQuestion
<textboxQuestion class="mb-4"
class="mb-4" cmsWidgetName="FirstNameWidget"
cmsWidgetName="LastNameWidget" v-model="firstName"
v-model="lastName" ref="firstName"
ref="lastName" customInputId="firstName"
customInputId="lastName" validationRules="first-name-required" />
validationRules="last-name-required" />
<textboxQuestion <textboxQuestion
class="mb-4" class="mb-4"
cmsWidgetName="emailQuestionWidget" cmsWidgetName="LastNameWidget"
v-model="emailAddress" v-model="lastName"
inputId="email" ref="lastName"
validationRules="email-address-required|email-address-format" /> customInputId="lastName"
<phoneNumberQuestion validationRules="last-name-required" />
class="mb-4"
cmsWidgetName="PhoneNumberQuestionWidget" <textboxQuestion
v-model="phoneNumber" class="mb-4"
isRequired cmsWidgetName="emailQuestionWidget"
validationRules="phone-number-required" /> v-model="emailAddress"
<checkboxQuestion inputId="email"
class="mb-5" validationRules="email-address-required|email-address-format" />
cmsWidgetName="TextMeQuestionWidget"
v-model="isSmsOptIn" /> <phoneNumberQuestion
<textareaQuestion class="mb-4"
class="mb-4" cmsWidgetName="PhoneNumberQuestionWidget"
v-model="techNotes" v-model="phoneNumber"
cmsWidgetName="TextAreaContentWidget" isRequired
maxLength="250" /> validationRules="phone-number-required" />
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
<funnel-footer <checkboxQuestion
cmsWidgetName="FunnelFooterWidget" class="mb-5"
ref="funnelFooter" cmsWidgetName="TextMeQuestionWidget"
:isForwardActionDisabled="!meta.valid" v-model="isSmsOptIn" />
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> <textareaQuestion
class="mb-4"
v-model="techNotes"
cmsWidgetName="TextAreaContentWidget"
maxLength="250" />
<textBlock cmsWidgetName="DisclaimerCopyWidget" typeStyle="caption" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div> </div>
</Form> </Form>
</template> </template>
<script> <script>
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import textareaQuestion from "@/digital-components/textarea-question/textarea-question"; import textareaQuestion from "@/digital-components/textarea-question/textarea-question";
import textboxQuestion from "@/digital-components/textbox-question/textbox-question"; import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
@ -172,7 +187,7 @@ export default {
funnelSubHeader, funnelSubHeader,
textareaQuestion, textareaQuestion,
textboxQuestion, textboxQuestion,
funnelFooter, navbar,
Form, Form,
textBlock, textBlock,
phoneNumberQuestion, phoneNumberQuestion,

View file

@ -254,7 +254,7 @@ function setupMocks({
{ Name: "Provide my license plate # Most accurate VIN match" }, { Name: "Provide my license plate # Most accurate VIN match" },
{ Name: "Provide my home address Most convenient VIN match" }, { Name: "Provide my home address Most convenient VIN match" },
], ],
funnelFooterWidget = { ForwardButtonText: "test txt" }, FunnelFooterWidget = { ForwardButtonText: "test txt" },
mountOptionsMockData = { mountOptionsMockData = {
router: { router: {
navigate: jest.fn(), navigate: jest.fn(),
@ -271,7 +271,7 @@ function setupMocks({
groupName: groupName, groupName: groupName,
QuestionText: cmsQuestionText, QuestionText: cmsQuestionText,
Answers: cmsAnswers, Answers: cmsAnswers,
FunnelFooterWidget: funnelFooterWidget, FunnelFooterWidget: FunnelFooterWidget,
}; };
const apiPromise = Promise.resolve({ cmsContent }); const apiPromise = Promise.resolve({ cmsContent });

View file

@ -1,79 +1,84 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <loadingModal ref="loadingModal" />
<loadingModal ref="loadingModal" /> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" /> <div class="col">
<funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" /> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div v-if="!skipVin">
<div class="vinlookupquestion" v-html="this.VinLookupQuestionText"></div>
<buttonQuestion
cmsWidgetName="VinLookupMethod"
:answers="answersFromCms"
groupName="vinLookupMethodOption"
buttonTypeString="listButton"
v-model="selectedVinLookupMethod"
isRequired
validationRules="option-required"
:logDisplayedValuesEvent="true" />
</div> </div>
<div v-else> </div>
<alert <div class="row justify-content-md-center">
class="my-4" <div class="col-md-6 col-xl-4">
:cmsWidgetName="alertInfo" <vehicleBanner cmsWidgetName="VehicleBannerWidget" />
alertClass="alert-info" <funnelSubHeader class="pb-4" cmsWidgetName="FunnelSubHeaderWidget" />
v-bind:isDismissible="false" /> <div v-if="!skipVin">
<div class="row my-2"> <div class="vinlookupquestion" v-html="this.VinLookupQuestionText"></div>
<div class="col"> <buttonQuestion
<textboxQuestion cmsWidgetName="VinLookupMethod"
cmsWidgetName="ServiceZipQuestionWidget" :answers="answersFromCms"
v-model="serviceZipCode" groupName="vinLookupMethodOption"
inputId="serviceZipCode" buttonTypeString="listButton"
mask="#####" v-model="selectedVinLookupMethod"
isRequired isRequired
validationRules="zip-required|zip-format" /> validationRules="option-required"
</div> :logDisplayedValuesEvent="true" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
<div class="row mt-2">
<div class="col"> <div v-else>
<textboxQuestion <alert
cmsWidgetName="EmailAddressQuestionWidget" class="my-4"
v-model="emailAddress" :cmsWidgetName="alertInfo"
inputId="emailAddress" alertClass="alert-info"
isRequired v-bind:isDismissible="false" />
disableAutoFill
validationRules="email-address-required|email-address-format" /> <textboxQuestion
</div> cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
inputId="serviceZipCode"
mask="#####"
isRequired
validationRules="zip-required|zip-format" />
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
inputId="emailAddress"
isRequired
disableAutoFill
validationRules="email-address-required|email-address-format" />
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
class="my-4"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-model="customAlertData"
v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
<div class="row mb-2">
<div class="col">
<textBlock
cmsWidgetName="QuoteEmailTextBlockWidget"
typeStyle="caption" />
</div>
</div>
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
class="my-4"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-model="customAlertData"
v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" />
</div> </div>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</Form> </Form>
@ -82,7 +87,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import buttonQuestion from "@/digital-components/button-question/button-question"; import buttonQuestion from "@/digital-components/button-question/button-question";
@ -157,9 +162,10 @@ export default {
var vinByAddressPromise; var vinByAddressPromise;
if (zip) { if (zip) {
vinByAddressPromise = baseMixin.methods.dispatchStoreAction( vinByAddressPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.IS_VIN_BY_ADDRESS_PERMISSIBLE, storeActions.IS_VIN_BY_ADDRESS_PERMISSIBLE,
zip, { zip: zip },
"estimate",
false false
); );
} }
@ -238,13 +244,13 @@ export default {
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
if (!zipCodeData.isServiceable) { if (!zipCodeData.isServiceable) {
this.displayNonServiceableZipAlert = true; this.displayNonServiceableZipAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
@ -252,12 +258,18 @@ export default {
const vehicleChangedDuringPolicyLookupInHeritage = const vehicleChangedDuringPolicyLookupInHeritage =
payment.isInsurance && payment.insuranceCoverage.coverageStatus; payment.isInsurance && payment.insuranceCoverage.coverageStatus;
if (vehicleChangedDuringPolicyLookupInHeritage) { if (vehicleChangedDuringPolicyLookupInHeritage) {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: "estimate",
loadingModal: this.$refs.loadingModal,
});
} else if (this.$store.getters.order.referralNumber?.length === 6) { } else if (this.$store.getters.order.referralNumber?.length === 6) {
await this.navigateForwardWithSingleCarMatch(); await this.navigateForwardWithSingleCarMatch();
} else if (this.isRepair) { } else if (this.isRepair) {
const supportingItemsPromise = await this.dispatchStoreAction( const supportingItemsPromise = await this.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS storeActions.GET_SUPPORTING_ITEMS,
null,
"estimate"
); );
const promiseResultMap = [ const promiseResultMap = [
@ -276,7 +288,7 @@ export default {
); );
// call saveSession here - navigateWithSaving saves too late in the flow // call saveSession here - navigateWithSaving saves too late in the flow
await saveSession({}); await saveSession({ pageNameToLog: "estimate" });
return this.$router.navigateWithSaving( return this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_NO_QUESTIONS, this.navigationScenarios.CLICKED_FORWARD_WITH_NO_QUESTIONS,
this.$route this.$route
@ -343,7 +355,7 @@ export default {
funnelHeader, funnelHeader,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
funnelFooter, navbar,
buttonQuestion, buttonQuestion,
Form, Form,
alert, alert,
@ -358,6 +370,10 @@ export default {
.vinlookupquestion p { .vinlookupquestion p {
margin-bottom: 0; margin-bottom: 0;
} }
.vinlookupquestion strong {
font-weight: 500;
color: $black;
}
.vinlookupquestion p:nth-child(2) { .vinlookupquestion p:nth-child(2) {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 1rem; margin-bottom: 1rem;

View file

@ -120,7 +120,7 @@ describe("license-plate-lookup.vue", () => {
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -157,7 +157,7 @@ describe("license-plate-lookup.vue", () => {
}); });
// Mock store action call // Mock store action call
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -201,7 +201,7 @@ describe("license-plate-lookup.vue", () => {
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Mock store action call // Mock store action call
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -242,7 +242,7 @@ describe("license-plate-lookup.vue", () => {
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => { wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return ""; return "";
}); });
wrapper.vm.dispatchStoreAction = jest.fn(); wrapper.vm.dispatchStoreActionWithLogging = jest.fn();
await wrapper.vm.navigateForward(); await wrapper.vm.navigateForward();
@ -315,7 +315,7 @@ describe("license-plate-lookup.vue", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
//Assert //Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled(); expect(wrapper.vm.$refs.navbar.updateButtonText).toHaveBeenCalled();
}); });
test("Button Text should revert to initial value when registrationZip textfield has new text", async () => { test("Button Text should revert to initial value when registrationZip textfield has new text", async () => {
@ -330,7 +330,7 @@ describe("license-plate-lookup.vue", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
//Assert //Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled(); expect(wrapper.vm.$refs.navbar.updateButtonText).toHaveBeenCalled();
}); });
test("Button Text should revert to initial value when serviceZip textfield has new text", async () => { test("Button Text should revert to initial value when serviceZip textfield has new text", async () => {
@ -345,7 +345,7 @@ describe("license-plate-lookup.vue", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
//Assert //Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled(); expect(wrapper.vm.$refs.navbar.updateButtonText).toHaveBeenCalled();
}); });
}); });
@ -379,7 +379,7 @@ describe("license-plate-lookup.vue", () => {
return { data: { vehicle: { carId: "C00000" } } }; return { data: { vehicle: { carId: "C00000" } } };
}); });
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -417,7 +417,7 @@ describe("license-plate-lookup.vue", () => {
await wrapper.setData({ registrationZip: "00000" }); await wrapper.setData({ registrationZip: "00000" });
navigateToHeritage.navigateToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -449,7 +449,7 @@ describe("license-plate-lookup.vue", () => {
await wrapper.setData({ registrationZipCode: "00000" }); await wrapper.setData({ registrationZipCode: "00000" });
navigateToHeritage.navigateToHeritageFunnel = jest.fn(); navigateToHeritage.navigateToHeritageFunnel = jest.fn();
wrapper.vm.dispatchStoreAction = jest.fn().mockImplementation(() => wrapper.vm.dispatchStoreActionWithLogging = jest.fn().mockImplementation(() =>
Promise.resolve({ Promise.resolve({
data: { data: {
vehicle: { vehicle: {
@ -703,8 +703,8 @@ function setupMocks({
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -1,88 +1,89 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div class="row mt-2 mb-4">
<div class="col">
<textboxQuestion
cmsWidgetName="LicensePlateNumberQuestionWidget"
v-model="licensePlate"
isRequired
customInputId="licensePlate"
validationRules="license-plate-required" />
</div>
</div> </div>
<div class="row mt-0 mb-4"> </div>
<div class="col"> <div class="row justify-content-md-center">
<textboxQuestion <div class="col-md-6 col-xl-4">
cmsWidgetName="RegistrationZipQuestionWidget" <vehicleBanner
v-model="registrationZipCode" cmsWidgetName="VehicleBannerWidget"
customInputId="zip" :displayGenericVehicleImage="false" />
mask="#####"
validationRules="registration-zip-required|zip-format" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<textboxQuestion
cmsWidgetName="LicensePlateNumberQuestionWidget"
v-model="licensePlate"
isRequired
customInputId="licensePlate"
validationRules="license-plate-required" />
<textboxQuestion
cmsWidgetName="RegistrationZipQuestionWidget"
v-model="registrationZipCode"
customInputId="zip"
mask="#####"
validationRules="registration-zip-required|zip-format" />
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="email"
customInputId="email"
validationRules="email-address-required|email-address-format" />
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
<alert
v-if="displayInvalidZipAlert"
class="my-3"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
v-if="displayNonServiceableZipAlert"
class="my-3"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<div class="row mt-2" v-if="showServiceZipField">
<div class="col mb-1">
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
inputId="serviceZipCode"
validationRules="zip-required|zip-format"
mask="#####" />
</div>
</div> </div>
<alert
v-if="displayVinNotFoundAlert"
class="my-3"
cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
v-if="displayMatchedDifferentVehicleAlert"
class="my-3"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<navbar
ref="navbar"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
<div class="row mt-0">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="email"
customInputId="email"
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-2">
<div class="col">
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
</div>
</div>
<alert
v-if="displayInvalidZipAlert"
class="my-3"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
v-if="displayNonServiceableZipAlert"
class="my-3"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<div class="row mt-2" v-if="showServiceZipField">
<div class="col mb-1">
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
inputId="serviceZipCode"
validationRules="zip-required|zip-format"
mask="#####" />
</div>
</div>
<alert
v-if="displayVinNotFoundAlert"
class="my-3"
cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
v-if="displayMatchedDifferentVehicleAlert"
class="my-3"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<funnelFooter
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</Form> </Form>
@ -91,7 +92,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
@ -199,9 +200,10 @@ export default {
async forwardButtonAction() { async forwardButtonAction() {
this.resetWarningsAndErrors(); this.resetWarningsAndErrors();
const registrationZipValidationResponse = this.dispatchStoreAction( const registrationZipValidationResponse = this.dispatchStoreActionWithLogging(
storeActions.VALIDATE_ZIP, storeActions.VALIDATE_ZIP,
{ zip: this.registrationZipCode } { zip: this.registrationZipCode },
"license-plate-lookup"
); );
// Settle promises and get results // Settle promises and get results
@ -214,9 +216,13 @@ export default {
// If serviceZipCode is not set, then sets the response to the registrationZipValidationResponse. Calls VALIDATE_ZIP if the serviceZipCode is set. // If serviceZipCode is not set, then sets the response to the registrationZipValidationResponse. Calls VALIDATE_ZIP if the serviceZipCode is set.
resultKey: "serviceZipValidationResponse", resultKey: "serviceZipValidationResponse",
promise: this.serviceZipCode promise: this.serviceZipCode
? this.dispatchStoreAction(storeActions.VALIDATE_ZIP, { ? this.dispatchStoreActionWithLogging(
zip: this.serviceZipCode, storeActions.VALIDATE_ZIP,
}) {
zip: this.serviceZipCode,
},
"license-plate-lookup"
)
: registrationZipValidationResponse, : registrationZipValidationResponse,
}, },
]; ];
@ -230,7 +236,7 @@ export default {
).catch(() => { ).catch(() => {
// No VIN found. // No VIN found.
this.displayVinNotFoundAlert = true; this.displayVinNotFoundAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
}); });
// If no VIN was found, stop processing after displaying alert // If no VIN was found, stop processing after displaying alert
if (!vinLookup) { if (!vinLookup) {
@ -241,7 +247,7 @@ export default {
const isZipValid = resultMap.serviceZipValidationResponse.isValid; const isZipValid = resultMap.serviceZipValidationResponse.isValid;
if (this.serviceZipCode && !isZipValid) { if (this.serviceZipCode && !isZipValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
@ -260,14 +266,15 @@ export default {
this.displayMatchedDifferentVehicleAlert = true; this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId( this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
vinLookup.data.vehicle.carId vinLookup.data.vehicle.carId,
"license-plate-lookup"
); );
// Update button "Continue with..." // Update button "Continue with..."
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
`Continue with ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}` `Continue with ${vinLookup.data.vehicle.year} ${vinLookup.data.vehicle.make} ${vinLookup.data.vehicle.model}`
); );
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// If the either the registration zip code or service zip code are not serviceable // If the either the registration zip code or service zip code are not serviceable
@ -275,7 +282,7 @@ export default {
if (!this.isZipServiceable) { if (!this.isZipServiceable) {
this.displayNonServiceableZipAlert = true; this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true; this.showServiceZipField = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// If the registration zip code is serviceable and nothing was entered for the service zip code // If the registration zip code is serviceable and nothing was entered for the service zip code
@ -312,9 +319,10 @@ export default {
return await this.navigateForward(); return await this.navigateForward();
}, },
lookupVin(plate, state) { lookupVin(plate, state) {
return this.dispatchStoreAction( return this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VIN_BY_PLATE, storeActions.LOOKUP_VIN_BY_PLATE,
{ licensePlate: plate, licenseState: state }, { licensePlate: plate, licenseState: state },
"license-plate-lookup",
false false
); );
}, },
@ -367,26 +375,26 @@ export default {
}, },
watch: { watch: {
licensePlate() { licensePlate() {
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
}, },
registrationZipCode() { registrationZipCode() {
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
}, },
serviceZipCode() { serviceZipCode() {
// If they modify the service zip code, then hide the error message. // If they modify the service zip code, then hide the error message.
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
}, },
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
textboxQuestion, textboxQuestion,

View file

@ -248,6 +248,13 @@ describe("partQuestions.vue...", () => {
}, },
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: {
glassPieceParts: [],
},
};
});
// Act // Act
wrapper.vm.forwardButtonAction(); wrapper.vm.forwardButtonAction();
@ -281,6 +288,13 @@ describe("partQuestions.vue...", () => {
}, },
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: {
glassPieceParts: [],
},
};
});
const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction"); const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction");
// Act // Act
@ -328,7 +342,14 @@ describe("partQuestions.vue...", () => {
}, },
}; };
}); });
const spy = jest.spyOn(wrapper.vm, "dispatchStoreAction"); wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: {
glassPieceParts: [],
},
};
});
const spy = jest.spyOn(wrapper.vm, "dispatchStoreActionWithLogging");
// Act // Act
wrapper.vm.forwardButtonAction(); wrapper.vm.forwardButtonAction();
@ -336,7 +357,7 @@ describe("partQuestions.vue...", () => {
await nextTick(); await nextTick();
//Assert //Assert
expect(spy).toHaveBeenNthCalledWith(2, "getParts"); expect(spy).toHaveBeenNthCalledWith(1, "getParts", null, "part-questions");
wrapper.unmount(); wrapper.unmount();
}); });
@ -362,6 +383,13 @@ describe("partQuestions.vue...", () => {
}, },
}; };
}); });
wrapper.vm.dispatchStoreActionWithLogging = jest.fn(() => {
return {
data: {
glassPieceParts: [],
},
};
});
wrapper.vm.navigateForward = jest.fn(); wrapper.vm.navigateForward = jest.fn();
// Act // Act

View file

@ -145,7 +145,11 @@ export default {
); );
// call API parts method // call API parts method
const partsLookup = await this.dispatchStoreAction(this.storeActions.GET_PARTS); const partsLookup = await this.dispatchStoreActionWithLogging(
this.storeActions.GET_PARTS,
null,
"part-questions"
);
const glassPartsForStore = partsLookup.data.glassPieceParts; const glassPartsForStore = partsLookup.data.glassPieceParts;

View file

@ -52,7 +52,7 @@
@edit-clicked="editServiceLocation" /> @edit-clicked="editServiceLocation" />
</div> </div>
<funnel-footer <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton" :isBackButtonHidden="shouldHideBackButton"
@ -66,7 +66,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
@ -209,56 +209,9 @@ export default {
vapsItemsSelectedAction(vapsItemsSelected) { vapsItemsSelectedAction(vapsItemsSelected) {
this.selectedVaps = vapsItemsSelected; this.selectedVaps = vapsItemsSelected;
}, },
backButtonAction() { backButtonAction() {},
vehicleQuestionsMixin.methods.navigateBack(this);
},
forwardButtonAction() { forwardButtonAction() {
this.dispatchStoreAction( this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
this.storeActions.SAVE_PAYMENT_TYPE,
this.isInsuranceSelected,
false
);
if (!this.isInsuranceSelected) {
this.dispatchStoreAction(
this.storeActions.SAVE_PARENT_ACCOUNT_NUMBER,
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER,
false
);
}
if (
this.$store.getters.order.payment.parentAccountNumber !=
applicationConfig.CASH_PARENT_ACCOUNT_NUMBER
) {
this.supportingItems = this.filterOutFees(this.supportingItems);
}
if (this.pricedGlassParts.length > 0) {
this.dispatchStoreAction(
this.storeActions.SAVE_GLASS_PART_PRICES,
this.pricedGlassParts,
false
);
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
this.supportingItems,
false
);
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
const payment = this.$store.getters.payment;
if (payment.isInsurance) {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_CASH,
this.$route
);
}
}, },
}, },
computed: { computed: {
@ -286,7 +239,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
funnelSubHeader, funnelSubHeader,
Form, Form,
loadingModal, loadingModal,

View file

@ -36,6 +36,10 @@ jest.mock("@/mixins/base-mixin", () => ({
if (action === mockPriceOrderStoreAction) return items; if (action === mockPriceOrderStoreAction) return items;
else return mockStoreActionResults[action]; else return mockStoreActionResults[action];
}, },
dispatchStoreActionWithLogging(action, items, pageName, encode) {
if (action === mockPriceOrderStoreAction) return items;
else return mockStoreActionResults[action];
},
getTierOnePackagePrice() { getTierOnePackagePrice() {
return mockTierOnePrice; return mockTierOnePrice;
}, },

View file

@ -1,45 +1,56 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <loadingModal ref="loadingModal" />
<loadingModal ref="loadingModal" /> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" /> </div>
<funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" /> </div>
<div class="fade-on-route-transition sub-container make-tall"> <div class="row justify-content-md-center">
<cashOrInsuranceQuestion <div class="col-md-6 col-xl-4">
ref="cashOrInsurance" <vehicleBanner
cmsWidgetName="CashOrInsuranceQuestionWidget" cmsWidgetName="VehicleBannerWidget"
v-model="isInsuranceSelected" :displayGenericVehicleImage="false" />
groupName="CashOrInsuranceQuestion" />
<servicePackageQuestion
ref="servicePackage"
cashCmsWidgetName="CashServicePackageQuestionWidget"
insuranceCmsWidgetName="InsuranceServicePackageQuestionWidget"
groupName="ServicePackageQuestion"
:availableLineItems="availableLineItems"
:isInsuranceSelected="isInsuranceSelected"
@vapsItemsSelected="vapsItemsSelectedAction"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
<textBlock <funnelSubHeader class="mb-5" cmsWidgetName="FunnelSubHeaderWidget" />
cmsWidgetName="quoteDisclaimer"
justifyText="left" <cashOrInsuranceQuestion
typeStyle="caption" ref="cashOrInsurance"
class="mt-4" /> cmsWidgetName="CashOrInsuranceQuestionWidget"
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" /> v-model="isInsuranceSelected"
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" /> groupName="CashOrInsuranceQuestion" />
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" /> <servicePackageQuestion
<funnel-footer ref="servicePackage"
cmsWidgetName="FunnelFooterWidget" cashCmsWidgetName="CashServicePackageQuestionWidget"
:isForwardActionDisabled="!meta.valid" insuranceCmsWidgetName="InsuranceServicePackageQuestionWidget"
:isBackButtonHidden="shouldHideBackButton" groupName="ServicePackageQuestion"
@back-clicked="backButtonAction" :availableLineItems="availableLineItems"
@ForwardClicked="forwardButtonAction" /> :isInsuranceSelected="isInsuranceSelected"
@vapsItemsSelected="vapsItemsSelectedAction"
v-on="{ 'buttonEvent.openModal': openModalAction }"
validationRules="option-required"
isRequired />
<textBlock
cmsWidgetName="quoteDisclaimer"
justifyText="left"
typeStyle="caption"
class="mt-4" />
<contentGroupModal ref="RainDefenseModal" cmsWidgetName="RainDefenseModal" />
<contentGroupModal ref="FrontWiperModal" cmsWidgetName="FrontWiperModal" />
<contentGroupModal ref="RearWiperModal" cmsWidgetName="RearWiperModal" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<navbar
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div> </div>
</div> </div>
</Form> </Form>
@ -48,7 +59,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question"; import cashOrInsuranceQuestion from "./cash-or-insurance-question/cash-or-insurance-question";
@ -75,12 +86,20 @@ export default {
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS); const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
const rainDefensePromise = baseMixin.methods.dispatchStoreAction( storeActions.GET_WIPERS,
storeActions.GET_RAIN_DEFENSE null,
"quote"
); );
const supportingItemsPromise = baseMixin.methods.dispatchStoreAction( const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS storeActions.GET_RAIN_DEFENSE,
null,
"quote"
);
const supportingItemsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS,
null,
"quote"
); );
const promiseResultMap = [ const promiseResultMap = [
@ -112,12 +131,19 @@ export default {
...resultMap.wipers, ...resultMap.wipers,
...clonedGlassParts, ...clonedGlassParts,
]; ];
console.log("Available line items:", availableLineItems);
const pricingResults = await baseMixin.methods.dispatchStoreAction( const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
availableLineItems: availableLineItems, availableLineItems: availableLineItems,
}, },
"quote",
false
);
baseMixin.methods.dispatchStoreAction(
storeActions.SAVE_SUPPORTING_ITEMS,
resultMap.supportingItems,
false false
); );
console.log("Pricing results:", pricingResults); console.log("Pricing results:", pricingResults);
@ -213,17 +239,15 @@ export default {
); );
} }
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS,
this.supportingItems,
false
);
this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false); this.dispatchStoreAction(this.storeActions.SAVE_VAPS, this.selectedVaps, false);
const payment = this.$store.getters.payment; const payment = this.$store.getters.payment;
if (payment.isInsurance) { if (payment.isInsurance) {
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal }); navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: "quote",
loadingModal: this.$refs.loadingModal,
});
} else { } else {
this.$router.navigateWithSaving( this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_CASH, this.navigationScenarios.CLICKED_FORWARD_WITH_CASH,
@ -234,7 +258,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
Form, Form,

View file

@ -31,9 +31,15 @@ export default {
}, },
methods: { methods: {
loadInitialData() { loadInitialData() {
const wipersPromise = baseMixin.methods.dispatchStoreAction(storeActions.GET_WIPERS); const wipersPromise = baseMixin.methods.dispatchStoreActionWithLogging(
const rainDefensePromise = baseMixin.methods.dispatchStoreAction( storeActions.GET_WIPERS,
storeActions.GET_RAIN_DEFENSE null,
"review"
);
const rainDefensePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_RAIN_DEFENSE,
null,
"review"
); );
const promiseResultMap = [ const promiseResultMap = [

View file

@ -1,105 +1,113 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<!-- When customer-details is added: v-slot="{ meta }" --> <!-- When customer-details is added: v-slot="{ meta }" -->
<div class="page-container-grouped-styles"> <div class="container-fluid">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" /> </div>
<textBlock
:customText="subHeaderTitle"
typeStyle="h5"
justifyText="center"
margin="mt-1"
class="dark-header" />
<textBlock
:customText="subHeaderBody"
typeStyle="body"
justifyText="left"
margin="mt-0 mb-2" />
<buttonMain
ref="buttonMain"
isPrimary
:buttonText="forwardButtonText"
loaderColor="white"
class="mb-2"
@click-event="forwardButtonAction" />
<div>
<hr />
</div> </div>
<div class="row justify-content-md-center">
<div class="col-md-6 col-xl-4">
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<textBlock <textBlock
customText="Appointment Details" :customText="subHeaderTitle"
typeStyle="label bold" typeStyle="h5"
justifyText="justify-text-left" justifyText="center"
margin="mt-0" margin="mt-1"
class="dark-header" /> class="dark-header" />
<textBlock
:customText="subHeaderBody"
typeStyle="body"
justifyText="left"
margin="mt-0 mb-2" />
<div class="px-4"> <buttonMain
<vehicleReview ref="buttonMain"
cmsWidgetName="VehicleReviewWidget" isPrimary
:vehicle="vehicleInfo" :buttonText="forwardButtonText"
@edit-clicked="editVehicle" /> loaderColor="white"
class="mb-2"
@click-event="forwardButtonAction" />
<hr class="my-0" /> <div>
<hr />
</div>
<damageReview <textBlock
cmsWidgetName="DamageReviewWidget" customText="Appointment Details"
damageLocationsWidgetName="DamageLocationsWidget" typeStyle="label bold"
:damage="damageInfo" justifyText="justify-text-left"
@edit-clicked="editDamage" /> margin="mt-0"
class="dark-header" />
<hr class="my-0" /> <div class="px-4">
<vehicleReview
cmsWidgetName="VehicleReviewWidget"
:vehicle="vehicleInfo"
@edit-clicked="editVehicle" />
<servicePackageReview <hr class="my-0" />
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:lineItems="lineItems"
@edit-clicked="editServicePackage" />
<hr class="my-0" /> <damageReview
cmsWidgetName="DamageReviewWidget"
damageLocationsWidgetName="DamageLocationsWidget"
:damage="damageInfo"
@edit-clicked="editDamage" />
<serviceLocationReview <hr class="my-0" />
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
<hr class="my-0" /> <servicePackageReview
ref="servicePackageReview"
servicePackageOptionsCmsName="ServicePackageTitle"
defaultPackageItemsCmsName="DefaultPackageItemDescriptions"
vapsItemsCmsName="VapsItemDescriptions"
:damage="damageInfo"
:lineItems="lineItems"
@edit-clicked="editServicePackage" />
<scheduleReview <hr class="my-0" />
cmsWidgetName="ScheduleWidget"
:appointmentType="appointmentType"
@edit-clicked="editSchedule" />
<hr class="my-0" /> <serviceLocationReview
cmsWidgetName="ServiceLocationTitleWidget"
:serviceLocation="serviceLocationInfo"
@edit-clicked="editServiceLocation" />
<customerReview <hr class="my-0" />
cmsWidgetName="CustomerReviewWidget"
:customer="customerInfo" <scheduleReview
@edit-clicked="editCustomerDetails" /> cmsWidgetName="ScheduleWidget"
:appointmentType="appointmentType"
@edit-clicked="editSchedule" />
<hr class="my-0" />
<customerReview
cmsWidgetName="CustomerReviewWidget"
:customer="customerInfo"
@edit-clicked="editCustomerDetails" />
</div>
<div>
<hr class="my-0" />
</div>
<navbar
cmsWidgetName="FunnelFooterWidget"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div> </div>
<div>
<hr class="my-0" />
</div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</Form> </Form>
</template> </template>
<script> <script>
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import buttonMain from "@/ux-components/button-main/button-main"; import buttonMain from "@/ux-components/button-main/button-main";
import textBlock from "@/digital-components/text-block/text-block"; import textBlock from "@/digital-components/text-block/text-block";
@ -292,7 +300,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
buttonMain, buttonMain,
textBlock, textBlock,

View file

@ -2,11 +2,12 @@ import { storeActions } from "@/constants/store-actions";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
export async function getAlertReasons(ctu) { export async function getAlertReasons(ctu) {
const alertReasons = await baseMixin.methods.dispatchStoreAction( const alertReasons = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_ALERT_REASONS_BY_CTU, storeActions.GET_ALERT_REASONS_BY_CTU,
{ {
ctu: ctu, ctu: ctu,
} },
"schedule"
); );
return Promise.resolve(alertReasons); return Promise.resolve(alertReasons);

View file

@ -70,6 +70,65 @@ jest.mock("@/mixins/base-mixin.js", () => ({
]); ]);
} }
}), }),
dispatchStoreActionWithLogging: 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,
},
]);
}
}),
}, },
})); }));
@ -226,10 +285,11 @@ describe("schedule.vue...", () => {
); );
// Assert // Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledTimes(3); expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledTimes(3);
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith( expect(baseMixin.methods.dispatchStoreActionWithLogging).toHaveBeenCalledWith(
"getShopTimeSlots", "getShopTimeSlots",
expect.anything(), expect.anything(),
expect.anything(),
expect.anything() expect.anything()
); );
}); });
@ -623,7 +683,7 @@ function setupMocks({ customMountOptions }) {
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.datePicker.initializeComponent = jest.fn(); wrapper.vm.$refs.datePicker.initializeComponent = jest.fn();
wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn(); wrapper.vm.$refs.locationAlerts.initializeComponent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.timeSlotModalQuestion.openModal = jest.fn(); wrapper.vm.$refs.timeSlotModalQuestion.openModal = jest.fn();
return { wrapper }; return { wrapper };

View file

@ -1,50 +1,62 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <loadingModal ref="loadingModal" />
<loadingModal ref="loadingModal" /> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" /> <div class="col">
<template v-if="ChangeShopLink.length"> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<textBlock </div>
cmsWidgetName="ChangeShopLink" </div>
justifyText="center" <div class="row justify-content-md-center">
class="mb-3 text-link-small" <div class="col-md-6 col-xl-4">
marginTopSizeOverride="1" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
</template> <template v-if="ChangeShopLink.length">
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" /> <textBlock
<datePicker cmsWidgetName="ChangeShopLink"
customComponentId="dateQuestion" justifyText="center"
selectableDatesSetting="custom" class="mb-3 text-link-small"
ref="datePicker" marginTopSizeOverride="1" />
v-model="selectedDate" </template>
class="text-link-small" <locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
:customSelectableDatesCallback="getAvailableDatesMethod" <datePicker
validationRules="date-required" customComponentId="dateQuestion"
@date-clicked="openInshopTimeSlotsModal" /> selectableDatesSetting="custom"
<timeSlotModalQuestion ref="datePicker"
ref="timeSlotModalQuestion" v-model="selectedDate"
customComponentId="timeSlotModalQuestion" class="text-link-small"
v-model="selectedTimeSlotInfo" :customSelectableDatesCallback="getAvailableDatesMethod"
cmsWidgetName="TimeSlotModalQuestion" validationRules="date-required"
mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal" @date-clicked="openInshopTimeSlotsModal" />
mobileCmsWidgetName="MobileTimeSlotModal" <timeSlotModalQuestion
dropoffCmsWidgetName="DropOffTimeSlotModal" ref="timeSlotModalQuestion"
sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal" customComponentId="timeSlotModalQuestion"
overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal" v-model="selectedTimeSlotInfo"
:selectedDate="selectedDate" cmsWidgetName="TimeSlotModalQuestion"
:appointmentType="appointmentType" mobilePremiumCmsWidgetName="MobilePremiumTimeSlotModal"
:premiumAppointmentFee="mobilePremiumAppointmentFee" mobileCmsWidgetName="MobileTimeSlotModal"
:timeSlotsForSelectedDate="timeSlotsForSelectedDate" dropoffCmsWidgetName="DropOffTimeSlotModal"
:estimatedServiceMinutesMinimum="selectableDatesData.estimatedServiceMinutesMinimum" sameDayDropOffCmsWidgetName="SameDayDropOffTimeSlotModal"
:estimatedServiceMinutesMaximum="selectableDatesData.estimatedServiceMinutesMaximum" overnightDropOffCmsWidgetName="OvernightDropOffTimeSlotModal"
@time-slot-modal-closed="timeSlotModalClosed" :selectedDate="selectedDate"
validationRules="time-slot-selection-required" /> :appointmentType="appointmentType"
<funnel-footer :premiumAppointmentFee="mobilePremiumAppointmentFee"
cmsWidgetName="FunnelFooterWidget" :timeSlotsForSelectedDate="timeSlotsForSelectedDate"
ref="funnelFooter" :estimatedServiceMinutesMinimum="
:isForwardActionDisabled="!meta.valid" selectableDatesData.estimatedServiceMinutesMinimum
@back-clicked="backButtonAction" "
@ForwardClicked="forwardButtonAction" /> :estimatedServiceMinutesMaximum="
selectableDatesData.estimatedServiceMinutesMaximum
"
@time-slot-modal-closed="timeSlotModalClosed"
validationRules="time-slot-selection-required" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div> </div>
</Form> </Form>
</template> </template>
@ -52,7 +64,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate"; import { Form, defineRule } from "vee-validate";
@ -156,9 +168,10 @@ const getAvailableDates = async (
const makeParallelCalls = async () => { const makeParallelCalls = async () => {
await Promise.all( await Promise.all(
storeActionConfigs.map(async (storeAction) => { storeActionConfigs.map(async (storeAction) => {
const timeSlotsResponse = await baseMixin.methods.dispatchStoreAction( const timeSlotsResponse = await baseMixin.methods.dispatchStoreActionWithLogging(
storeAction.storeAction, storeAction.storeAction,
storeAction.payload, storeAction.payload,
"schedule",
false false
); );
timeSlotsResponsesData.estimatedServiceMinutesMinimum = timeSlotsResponsesData.estimatedServiceMinutesMinimum =
@ -206,17 +219,20 @@ export default {
preSelectedDate: preSelectedDate, preSelectedDate: preSelectedDate,
}); });
const premiumFeePromise = baseMixin.methods.dispatchStoreAction( const premiumFeePromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_PREMIUM_FEE storeActions.GET_MOBILE_PREMIUM_FEE,
null,
"schedule"
); );
const premiumFeeWithPricePromise = premiumFeePromise.then((result) => { const premiumFeeWithPricePromise = premiumFeePromise.then((result) => {
if (result.data) { if (result.data) {
return baseMixin.methods.dispatchStoreAction( return baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
availableLineItems: [result.data], availableLineItems: [result.data],
}, },
"schedule",
false false
); );
} else { } else {
@ -352,23 +368,23 @@ export default {
} }
}, },
updateFooterButtonText(timeSlotInfo) { updateFooterButtonText(timeSlotInfo) {
let funnelFooterButtonText; let navbarButtonText;
if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) { if (!timeSlotInfo || !timeSlotInfo.timeSlot.date) {
funnelFooterButtonText = "Continue"; navbarButtonText = "Continue";
} else { } else {
funnelFooterButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay( navbarButtonText = `Select ${this.convertSelectedDateToShortMonthAndDay(
timeSlotInfo.timeSlot.date timeSlotInfo.timeSlot.date
)}`; )}`;
if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) { if (this.appointmentType === AppointmentTypeStrings.IN_SHOP) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime( navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime timeSlotInfo.timeSlot.startTime
)}`; )}`;
} else if ( } else if (
this.appointmentType === AppointmentTypeStrings.MOBILE && this.appointmentType === AppointmentTypeStrings.MOBILE &&
!timeSlotInfo.isPremiumAppointment !timeSlotInfo.isPremiumAppointment
) { ) {
funnelFooterButtonText += ` at ${this.getDisplayTextForMilitaryTime( navbarButtonText += ` at ${this.getDisplayTextForMilitaryTime(
timeSlotInfo.timeSlot.startTime, timeSlotInfo.timeSlot.startTime,
true true
)} - ${this.getDisplayTextForMilitaryTime( )} - ${this.getDisplayTextForMilitaryTime(
@ -377,7 +393,7 @@ export default {
)}`; )}`;
} }
} }
this.$refs.funnelFooter.updateButtonText(funnelFooterButtonText); this.$refs.navbar.updateButtonText(navbarButtonText);
}, },
convertSelectedDateToShortMonthAndDay(selectedDate) { convertSelectedDateToShortMonthAndDay(selectedDate) {
// This conversion ensures we don't get get GMT induced date changes // This conversion ensures we don't get get GMT induced date changes
@ -483,7 +499,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
funnelSubHeader, funnelSubHeader,
Form, Form,
loadingModal, loadingModal,

View file

@ -516,9 +516,6 @@ export default {
<style lang="scss"> <style lang="scss">
.time-slots-modal.modal.modal-component { .time-slots-modal.modal.modal-component {
> .modal-dialog > .modal-content {
margin-top: 24px;
}
.modal-header { .modal-header {
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 0 !important; margin-bottom: 0 !important;

View file

@ -97,8 +97,8 @@ export default {
<style lang="scss"> <style lang="scss">
.appointment-type-question { .appointment-type-question {
.list-card img { .list-card img {
height: auto; height: 1.5rem;
width: 3.417rem; width: auto;
} }
} }
</style> </style>

View file

@ -2,42 +2,45 @@ import { storeActions } from "@/constants/store-actions";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js"; import baseMixin from "@/mixins/base-mixin.js";
export async function getPricedMobileFeePart(serviceZipCode) { export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) {
if (!serviceZipCode) { if (!serviceZipCode) {
return Promise.resolve(null); return Promise.resolve(null);
} }
const zipCodeData = await baseMixin.methods.getZipCodeData(serviceZipCode); const zipCodeData = await baseMixin.methods.getZipCodeData(serviceZipCode, pageNameToLog);
// Get the Mobile Fee Part // Get the Mobile Fee Part
const mobileFeePart = await baseMixin.methods.dispatchStoreAction( const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_MOBILE_FEE_PART, storeActions.GET_MOBILE_FEE_PART,
null, null,
pageNameToLog,
false false
); );
// Get the Mobile Fee Part Price // Get the Mobile Fee Part Price
const pricingResults = await baseMixin.methods.dispatchStoreAction( const pricingResults = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA,
{ {
availableLineItems: [mobileFeePart.data], availableLineItems: [mobileFeePart.data],
serviceZipCode: serviceZipCode, serviceZipCode: serviceZipCode,
serviceZipCodeCtu: zipCodeData.zipCodeCtu, serviceZipCodeCtu: zipCodeData.zipCodeCtu,
}, },
pageNameToLog,
false false
); );
return Promise.resolve(pricingResults[0]); return Promise.resolve(pricingResults[0]);
} }
export async function getServiceabilityDetails(serviceZipCode, lineItems) { export async function getServiceabilityDetails(serviceZipCode, lineItems, pageNameToLog) {
// Get the Mobile Fee Part // Get the Mobile Fee Part
const serviceabilityDetails = await baseMixin.methods.dispatchStoreAction( const serviceabilityDetails = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SERVICEABILITY_DETAILS, storeActions.GET_SERVICEABILITY_DETAILS,
{ {
serviceZipCode: serviceZipCode, serviceZipCode: serviceZipCode,
lineItems: lineItems, lineItems: lineItems,
}, },
pageNameToLog,
false false
); );
@ -51,7 +54,7 @@ export async function getAvailabilityRating(
providerNumber providerNumber
) { ) {
// For a given shop provider number and date range, get the appointment time slots available // For a given shop provider number and date range, get the appointment time slots available
const shopTimeSlots = await baseMixin.methods.dispatchStoreAction( const shopTimeSlots = await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_SHOP_TIME_SLOTS, storeActions.GET_SHOP_TIME_SLOTS,
{ {
providerNumber: providerNumber, providerNumber: providerNumber,
@ -59,6 +62,7 @@ export async function getAvailabilityRating(
endDate: endDate, endDate: endDate,
shopAppointmentType: shopAppointmentType, shopAppointmentType: shopAppointmentType,
}, },
"service-location",
false false
); );

View file

@ -188,6 +188,47 @@ jest.mock("@/mixins/base-mixin.js", () => ({
}); });
} }
if (actionName === mockStoreActionGetShopTimeSlots) {
if (request.providerNumber == "0000001") {
return Promise.resolve(mockGetShopTimeSlotsGoodAvailability);
}
return Promise.resolve(mockGetShopTimeSlotsLowAvailability);
}
}),
dispatchStoreActionWithLogging: jest.fn().mockImplementation((actionName, request) => {
if (actionName === mockStoreActionGetMobileFeePart) {
return Promise.resolve({
data: {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
},
});
}
if (actionName === mockStoreActionPriceOrderItemsAndSaveServerData) {
return Promise.resolve([
{
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
partType: "FEE",
laborAmount: 49.99,
sellingPrice: 0,
kitPrice: 0,
},
]);
}
if (actionName === mockStoreActionGetServiceabilityDetails) {
return Promise.resolve({
isGlassServiceableInshop: true,
isRecalibrationServiceableInshop: true,
isGlassServiceableMobile: true,
isRecalibrationServiceableMobile: true,
});
}
if (actionName === mockStoreActionGetShopTimeSlots) { if (actionName === mockStoreActionGetShopTimeSlots) {
if (request.providerNumber == "0000001") { if (request.providerNumber == "0000001") {
return Promise.resolve(mockGetShopTimeSlotsGoodAvailability); return Promise.resolve(mockGetShopTimeSlotsGoodAvailability);

View file

@ -229,7 +229,8 @@ export default {
) { ) {
// Validate the Zip Code // Validate the Zip Code
const zipCodeData = await this.getZipCodeData( const zipCodeData = await this.getZipCodeData(
this.internalModel.addressQuestions.zipCode this.internalModel.addressQuestions.zipCode,
"service-location"
); );
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
@ -238,10 +239,17 @@ export default {
} else { } else {
// retrieve mobile fee part // retrieve mobile fee part
const serviceZipCode = this.internalModel.addressQuestions.zipCode; const serviceZipCode = this.internalModel.addressQuestions.zipCode;
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode); const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode,
"service-location"
);
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode); const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
// update content related to service zip code // update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart); this.$emit("updated-mobile-fee-part", mobileFeePart);

View file

@ -1,87 +1,112 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <loadingModal ref="loadingModal" />
<loadingModal ref="loadingModal" /> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" /> <div class="col">
<serviceZipModalQuestion <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
v-model="serviceZipCodeQuestion" </div>
ref="serviceZipCodeQuestion" </div>
:mobileFeePart="mobileFeePart" <div class="row justify-content-md-center">
@updated-mobile-fee-part="setMobileFeePart" <div class="col-md-6 col-xl-4">
@updated-serviceability="setServiceabilityDetails" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
@updated-contains-military-base="setContainsMilitaryBase"
linkWidgetName="ServiceZipLinkWidget" <serviceZipModalQuestion
modalWidgetName="ServiceZipModalWidget" v-model="serviceZipCodeQuestion"
:onZipUpdateCallback="reloadShopData" /> ref="serviceZipCodeQuestion"
<alert :mobileFeePart="mobileFeePart"
ref="alertMilitaryBaseZip" @updated-mobile-fee-part="setMobileFeePart"
class="my-5" @updated-serviceability="setServiceabilityDetails"
cmsWidgetName="AlertMilitaryBaseZipWidget" @updated-contains-military-base="setContainsMilitaryBase"
v-if="displayMilitaryZipAlert" linkWidgetName="ServiceZipLinkWidget"
alertClass="alert-warning" /> modalWidgetName="ServiceZipModalWidget"
<alert :onZipUpdateCallback="reloadShopData" />
ref="alertMobileOnly"
class="my-5" <alert
cmsWidgetName="AlertMobileOnlyWidget" ref="alertMilitaryBaseZip"
v-if="displayServiceableMobileOnly" class="my-5"
alertClass="alert-warning" /> cmsWidgetName="AlertMilitaryBaseZipWidget"
<alert v-if="displayMilitaryZipAlert"
ref="alertRecalNoMobile" alertClass="alert-warning" />
class="my-5"
cmsWidgetName="AlertRecalNoMobileWidget" <alert
v-if="displayRecalibrationWarning" ref="alertMobileOnly"
@text-link-clicked="openModalAction" class="my-5"
alertClass="alert-warning" /> cmsWidgetName="AlertMobileOnlyWidget"
<alert v-if="displayServiceableMobileOnly"
ref="alertInshopOnly" alertClass="alert-warning" />
class="my-5"
cmsWidgetName="AlertInshopOnlyWidget" <alert
v-if="displayServiceableInshopOnly" ref="alertRecalNoMobile"
alertClass="alert-warning" /> class="my-5"
<alert cmsWidgetName="AlertRecalNoMobileWidget"
ref="alertNoShops" v-if="displayRecalibrationWarning"
class="my-5" @text-link-clicked="openModalAction"
cmsWidgetName="AlertNoShopsWidget" alertClass="alert-warning" />
v-if="displayNoShopsAlert"
alertClass="alert-warning" /> <alert
<appointmentTypeQuestion ref="alertInshopOnly"
v-model="selectedAppointmentType" class="my-5"
v-show="isAppointmentTypeDisplayed" cmsWidgetName="AlertInshopOnlyWidget"
:isServiceableMobile="isServiceableMobile" v-if="displayServiceableInshopOnly"
:isServiceableInshop="isServiceableInshop" alertClass="alert-warning" />
:isDisplayed="isAppointmentTypeDisplayed"
ref="appointmentTypeQuestion" <alert
groupName="appointmentTypeQuestion" ref="alertNoShops"
cmsWidgetName="AppointmentTypeQuestionWidget" class="my-5"
validationRules="option-required" /> cmsWidgetName="AlertNoShopsWidget"
<mobileLocationModalQuestions v-if="displayNoShopsAlert"
customComponentId="mobileLocationQuestions" alertClass="alert-warning" />
v-if="selectedAppointmentType === 'Mobile'" </div>
v-model="mobileLocationQuestions" </div>
:mobileFeePart="mobileFeePart" <div class="row justify-content-md-center appointment-type">
@updated-mobile-fee-part="setMobileFeePart" <div class="col-md-8">
@updated-serviceability="setServiceabilityDetails" <appointmentTypeQuestion
@updated-contains-military-base="setContainsMilitaryBase" v-model="selectedAppointmentType"
validationRules="mobile-location-required" v-show="isAppointmentTypeDisplayed"
ref="mobileLocationQuestions" :isServiceableMobile="isServiceableMobile"
linkWidgetName="MobileLocationLinkWidget" :isServiceableInshop="isServiceableInshop"
modalWidgetName="MobileLocationModalWidget" :isDisplayed="isAppointmentTypeDisplayed"
:onZipUpdateCallback="reloadShopData" /> ref="appointmentTypeQuestion"
<shopQuestion groupName="appointmentTypeQuestion"
ref="shopQuestion" cmsWidgetName="AppointmentTypeQuestionWidget"
v-show="isShopQuestionDisplayed" validationRules="option-required" />
v-model="selectedProvider" </div>
:selectedAppointmentType="selectedAppointmentType" </div>
:isDisplayed="isShopQuestionDisplayed" <div class="row justify-content-md-center">
cmsWidgetName="ShopQuestionWidget" /> <div class="col-md-6 col-xl-4">
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" /> <mobileLocationModalQuestions
<funnel-footer customComponentId="mobileLocationQuestions"
cmsWidgetName="FunnelFooterWidget" v-if="selectedAppointmentType === 'Mobile'"
ref="funnelFooter" v-model="mobileLocationQuestions"
:isForwardActionDisabled="!meta.valid || displayNoShopsAlert" :mobileFeePart="mobileFeePart"
@back-clicked="backButtonAction" @updated-mobile-fee-part="setMobileFeePart"
@ForwardClicked="forwardButtonAction" /> @updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
validationRules="mobile-location-required"
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
:onZipUpdateCallback="reloadShopData" />
<shopQuestion
ref="shopQuestion"
v-show="isShopQuestionDisplayed"
v-model="selectedProvider"
:selectedAppointmentType="selectedAppointmentType"
:isDisplayed="isShopQuestionDisplayed"
cmsWidgetName="ShopQuestionWidget" />
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid || displayNoShopsAlert"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</div> </div>
</Form> </Form>
</template> </template>
@ -95,7 +120,7 @@ import appointmentTypeQuestion from "@/layouts/service-location/appointment-type
import shopQuestion from "@/layouts/service-location/shop-question/shop-question"; import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form } from "vee-validate"; import { Form } from "vee-validate";
@ -116,6 +141,8 @@ import store from "@/store";
import { defineRule } from "vee-validate"; import { defineRule } from "vee-validate";
import { errorMessages } from "@/constants/error-messages"; import { errorMessages } from "@/constants/error-messages";
const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => { defineRule("mobile-location-required", (value) => {
if ( if (
@ -156,11 +183,15 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const serviceZipCode = store.getters.order.serviceLocation.zipCode; const serviceZipCode = store.getters.order.serviceLocation.zipCode;
const getZipCodeData = baseMixin.methods.getZipCodeData(serviceZipCode); const getZipCodeData = baseMixin.methods.getZipCodeData(serviceZipCode, "service-location");
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode); const serviceabilityDetailsPromise = getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode); const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode, "service-location");
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode); const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData(serviceZipCode);
@ -375,6 +406,43 @@ export default {
backButtonAction() { backButtonAction() {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route); this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
}, },
updateAndSaveSupportingItems() {
const supportingItems = store.getters.lineItems.supportingItems;
// if we have a mobile fee, then save/update supporting items
if (this.selectedAppointmentType == "Mobile") {
const mobileFeeIndex = supportingItems.findIndex(
(item) => item.partType == MOBILE_FEE_PART_TYPE
);
// If it already exists, update the price with latest data
if (mobileFeeIndex >= 0) {
supportingItems[mobileFeeIndex].laborAmount = this.mobileFeePart.laborAmount;
supportingItems[mobileFeeIndex].selingPrice = this.mobileFeePart.selingPrice;
supportingItems[mobileFeeIndex].kitPrice = this.mobileFeePart.kitPrice;
} else {
supportingItems.push(this.mobileFeePart);
}
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
} else {
// if it's not a mobile, then make sure we remove any that may have been added
const removeMobileFeeIndex = supportingItems.findIndex(
(item) => item.partType == MOBILE_FEE_PART_TYPE
);
if (removeMobileFeeIndex >= 0) {
supportingItems.splice(removeMobileFeeIndex, 1);
this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
supportingItems,
false
);
}
}
},
async forwardButtonAction() { async forwardButtonAction() {
await this.dispatchStoreAction( await this.dispatchStoreAction(
this.storeActions.SAVE_SERVICE_LOCATION, this.storeActions.SAVE_SERVICE_LOCATION,
@ -401,6 +469,7 @@ export default {
false false
); );
this.updateAndSaveSupportingItems();
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route); this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
}, },
openModalAction(modalName) { openModalAction(modalName) {
@ -416,7 +485,7 @@ export default {
appointmentTypeQuestion, appointmentTypeQuestion,
mobileLocationModalQuestions, mobileLocationModalQuestions,
funnelHeader, funnelHeader,
funnelFooter, navbar,
funnelSubHeader, funnelSubHeader,
Form, Form,
loadingModal, loadingModal,
@ -427,6 +496,14 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.appointment-type {
@include media-breakpoint-up(md) {
.col {
flex: 0 0 auto;
width: 33.33333333%;
}
}
}
.question-text { .question-text {
& > span { & > span {
text-align: center; text-align: center;

View file

@ -133,7 +133,10 @@ export default {
if (this.internalModel.zipCode !== this.modelValue.zipCode) { if (this.internalModel.zipCode !== this.modelValue.zipCode) {
this.resetAlerts(); this.resetAlerts();
const zipCodeData = await this.getZipCodeData(this.internalModel.zipCode); const zipCodeData = await this.getZipCodeData(
this.internalModel.zipCode,
"service-location"
);
if (!zipCodeData.isValid) { if (!zipCodeData.isValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
@ -144,10 +147,17 @@ export default {
// retrieve mobile fee part // retrieve mobile fee part
const serviceZipCode = this.internalModel.zipCode; const serviceZipCode = this.internalModel.zipCode;
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode); const mobileFeePart = await getPricedMobileFeePart(
serviceZipCode,
"service-location"
);
// retrieve serviceability details // retrieve serviceability details
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode); const serviceabilityDetails = await getServiceabilityDetails(
serviceZipCode,
null,
"service-location"
);
// update content related to service zip code // update content related to service zip code
this.$emit("updated-mobile-fee-part", mobileFeePart); this.$emit("updated-mobile-fee-part", mobileFeePart);

View file

@ -311,8 +311,7 @@ describe("shop-question.vue", () => {
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" }); const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
// Assert // Assert
expect(showMoreShopsLink.exists()).toBe(true); expect(showMoreShopsLink.exists()).toBe(false);
expect(showMoreShopsLink.isVisible()).toBe(false);
}); });
it("Should display the next three shops when the 'Show more location' link is clicked", async () => { it("Should display the next three shops when the 'Show more location' link is clicked", async () => {
@ -375,6 +374,12 @@ describe("shop-question.vue", () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
await wrapper.setData({
displaySeeMoreLocationsLink: true,
});
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" }); const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();

View file

@ -22,7 +22,7 @@
validationRules="option-required" validationRules="option-required"
:additionalButtonData="additionalButtonData" /> :additionalButtonData="additionalButtonData" />
<textLink <textLink
v-show="displaySeeMoreLocationsLink" v-if="displaySeeMoreLocationsLink"
ref="showMoreShopsLink" ref="showMoreShopsLink"
class="show-more-shops-link" class="show-more-shops-link"
id="showMoreShopsId" id="showMoreShopsId"
@ -126,9 +126,13 @@ export default {
return this.loadData(serviceZipCode); return this.loadData(serviceZipCode);
}, },
loadData(serviceZipCode) { loadData(serviceZipCode) {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_PROVIDERS, { return baseMixin.methods.dispatchStoreActionWithLogging(
serviceZipCode: serviceZipCode, storeActions.GET_PROVIDERS,
}); {
serviceZipCode: serviceZipCode,
},
"service-location"
);
}, },
initializeComponent(shopQuestionInitialData) { initializeComponent(shopQuestionInitialData) {
this.shopProviders = shopQuestionInitialData.shopProviders; this.shopProviders = shopQuestionInitialData.shopProviders;

View file

@ -173,9 +173,10 @@ describe("vehicle-damage.vue", () => {
//Assert //Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled(); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( expect(baseMixin.methods.dispatchStoreActionWithLogging).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS, storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000" } { carId: "C00000000" },
"vehicle-damage"
); );
}); });
@ -268,9 +269,10 @@ describe("vehicle-damage.vue", () => {
//Assert //Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled(); expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalled();
expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace); expect(wrapper.vm.selectedGlassToReplace()).toEqual(expectedGlassToReplace);
expect(baseMixin.methods.dispatchStoreAction).toBeCalledWith( expect(baseMixin.methods.dispatchStoreActionWithLogging).toBeCalledWith(
storeActions.GET_DAMAGE_OPTIONS, storeActions.GET_DAMAGE_OPTIONS,
{ carId: "C00000000" } { carId: "C00000000" },
"vehicle-damage"
); );
}); });
}); });
@ -789,6 +791,7 @@ function setupMocks({ pageHeaderWidgetHeaderText, mountOptionsMockData, funnelCo
mountOptionsMockData = Object.assign(mountOptionsMockDataDefault, mountOptionsMockData); mountOptionsMockData = Object.assign(mountOptionsMockDataDefault, mountOptionsMockData);
//Mock api responses //Mock api responses
baseMixin.methods.dispatchStoreAction = jest.fn(); baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreActionWithLogging = jest.fn();
const apiResponses = { const apiResponses = {
cmsContent: { cmsContent: {
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText, FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,

View file

@ -1,56 +1,77 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <div class="container-fluid page-container-grouped-styles vehicle-damage">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" /> </div>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> </div>
<div class="fade-on-route-transition sub-container make-tall"> <div class="row justify-content-md-center">
<alert <div class="col-md-6 col-xl-4">
ref="vehicleChangeAlert" <vehicleBanner
v-if="shouldDisplayVehicleChangeAlert" cmsWidgetName="VehicleBannerWidget"
class="mt-5 mb-0" :displayGenericVehicleImage="false" />
cmsWidgetName="VehicleChangeAlert"
alertClass="alert-warning" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
:isDismissible="false" />
<damageLocationQuestion <alert
ref="damageLocation" ref="vehicleChangeAlert"
cmsWidgetName="DamageLocationQuestion" v-if="shouldDisplayVehicleChangeAlert"
v-model="selectedDamageLocations" class="mt-5 mb-0"
groupName="DamageLocationQuestion" /> cmsWidgetName="VehicleChangeAlert"
<windshieldOptions alertClass="alert-warning"
ref="windshieldOptions" :isDismissible="false" />
v-model="selectedWindshieldOptions" </div>
:hasRepairReplaceConflict="hasRepairReplaceConflict" </div>
:hasSplitSingleConflict="hasSplitSingleConflict" <div class="row justify-content-md-center">
:selectedDamageLocations="selectedDamageLocations" /> <div class="col-md-8 col-xl-6">
<alert <damageLocationQuestion
v-if="hasRepairReplaceConflict" ref="damageLocation"
class="my-5" cmsWidgetName="DamageLocationQuestion"
cmsWidgetName="HasReplacementConflict" v-model="selectedDamageLocations"
alertClass="alert-danger" groupName="DamageLocationQuestion" />
:isDismissible="false" />
<sideDoorOptions <windshieldOptions
ref="sideDoorOptions" class="windshield-options"
cmsWidgetName="SideDoorSideQuestion" ref="windshieldOptions"
groupName="SideDoorSideQuestion" v-model="selectedWindshieldOptions"
v-model="sideDoorOptionsData" :hasRepairReplaceConflict="hasRepairReplaceConflict"
v-show="!hasRepairReplaceConflict" :hasSplitSingleConflict="hasSplitSingleConflict"
:selectedDamageLocations="selectedDamageLocations" /> :selectedDamageLocations="selectedDamageLocations" />
<replaceOptionsQuestion
ref="backGlassOptions" <alert
cmsWidgetName="RearReplaceOptionsQuestion" v-if="hasRepairReplaceConflict"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict" class="my-5"
v-model="selectedRearReplaceOptions" cmsWidgetName="HasReplacementConflict"
groupName="BackGlassReplaceOptionsQuestion" alertClass="alert-danger"
validationRules="replace-options-required" /> :isDismissible="false" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget" <sideDoorOptions
:isForwardActionDisabled="!meta.valid" ref="sideDoorOptions"
:isBackButtonHidden="shouldHideBackButton" cmsWidgetName="SideDoorSideQuestion"
@back-clicked="backButtonAction" groupName="SideDoorSideQuestion"
@ForwardClicked="forwardButtonAction" /> v-model="sideDoorOptionsData"
v-show="!hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations" />
<replaceOptionsQuestion
ref="backGlassOptions"
cmsWidgetName="RearReplaceOptionsQuestion"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
validationRules="replace-options-required" />
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-md-6 col-xl-4">
<navbar
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden="shouldHideBackButton"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div> </div>
</div> </div>
</Form> </Form>
@ -59,7 +80,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options"; import sideDoorOptions from "@/layouts/vehicle-damage/side-door-options/side-door-options";
@ -90,9 +111,10 @@ export default {
// Call APIs // Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const damageOptionsPromise = baseMixin.methods.dispatchStoreAction( const damageOptionsPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_DAMAGE_OPTIONS, storeActions.GET_DAMAGE_OPTIONS,
{ carId: store.getters.vehicle.carId } { carId: store.getters.vehicle.carId },
"vehicle-damage"
); );
// Settle promises and get results // Settle promises and get results
@ -325,8 +347,10 @@ export default {
); );
if (this.isWindshieldRepair) { if (this.isWindshieldRepair) {
const supportingItems = await this.dispatchStoreAction( const supportingItems = await this.dispatchStoreActionWithLogging(
storeActions.GET_SUPPORTING_ITEMS storeActions.GET_SUPPORTING_ITEMS,
null,
"vehicle-damage"
); );
this.dispatchStoreAction( this.dispatchStoreAction(
this.storeActions.SAVE_SUPPORTING_ITEMS, this.storeActions.SAVE_SUPPORTING_ITEMS,
@ -509,7 +533,7 @@ export default {
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
sideDoorOptions, sideDoorOptions,
@ -521,3 +545,16 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss">
.vehicle-damage {
.windshield-options {
@include media-breakpoint-up(md) {
.col {
flex: 0 0 auto;
width: 33.33333333%;
}
}
}
}
</style>

View file

@ -542,7 +542,7 @@ function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {}
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent; partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn(); wrapper.vm.$refs.loadingModal.showModal = jest.fn();
// wrapper.vm.$refs.onSubmit = jest.fn(); // wrapper.vm.$refs.onSubmit = jest.fn();
// wrapper.vm.$refs.onInvalidSubmit = jest.fn(); // wrapper.vm.$refs.onInvalidSubmit = jest.fn();

View file

@ -1,42 +1,54 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles vehicle-parts small-question-text"> <div class="vehicle-parts small-question-text">
<loadingModal ref="loadingModal" /> <loadingModal ref="loadingModal" />
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" /> <div class="container-fluid page-container-grouped-styles">
<vehicleBanner <div class="row">
ref="vehicleBanner" <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" />
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div class="prevent-squish my-5">
<div class="row">
<div class="col pb-0">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false" />
</div>
</div> </div>
</div> </div>
<div v-for="(item, i) in PartsOrQuestions" :key="i"> <div class="row justify-content-md-center">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) --> <div class="col-md-6 col-lg-4">
<hr v-if="i > 0" /> <vehicleBanner
<glassPartQuestion ref="vehicleBanner"
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`" cmsWidgetName="VehicleBannerWidget"
v-model="selectedGlassParts[item.glassLocation + '-' + item.glassName]" :displayGenericVehicleImage="false" />
:glassLocation="item.glassLocation" <funnelSubHeader
:glassName="item.glassName" ref="funnelSubHeader"
:colorAnswers="item.colorAnswers" cmsWidgetName="FunnelSubHeaderWidget" />
:alreadyPopulatedPartsData="alreadyPopulatedPartsData" /> <div class="prevent-squish my-5">
<div class="row">
<div class="col pb-0">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false" />
</div>
</div>
</div>
<div v-for="(item, i) in PartsOrQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<hr v-if="i > 0" />
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="
selectedGlassParts[item.glassLocation + '-' + item.glassName]
"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
:alreadyPopulatedPartsData="alreadyPopulatedPartsData" />
</div>
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="isForwardActionDisabled"
@back-clicked="navigateBack"
@ForwardClicked="forwardButtonAction" />
</div>
</div> </div>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="isForwardActionDisabled"
@back-clicked="navigateBack"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</Form> </Form>
@ -48,7 +60,7 @@ import glassPartQuestion from "@/layouts/vehicle-parts/glass-part-question/glass
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue"; import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
// Supporting Files // Supporting Files
@ -178,7 +190,7 @@ export default {
} }
// If no parts could be matched, throw an error (isForwardActionDisabled is based off of matchedParts) // If no parts could be matched, throw an error (isForwardActionDisabled is based off of matchedParts)
if (this.isForwardActionDisabled) { if (this.isForwardActionDisabled) {
this.$refs.funnelFooter.removeLoader(); this.$refs.navbar.removeLoader();
throw new Error("Could not match any parts to the selected parts"); throw new Error("Could not match any parts to the selected parts");
} }
@ -221,9 +233,19 @@ export default {
funnelHeader, funnelHeader,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
funnelFooter, navbar,
alert, alert,
loadingModal, loadingModal,
}, },
}; };
</script> </script>
<style lang="scss">
.vehicle-parts {
label {
&.list-card {
height: auto;
}
}
}
</style>

View file

@ -1,73 +1,68 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles position-relative"> <div class="container-fluid">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<div class="select-car"> <div class="col">
<div class="container-fluid pb-2"> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="row"> </div>
<div class="col"> </div>
<div class="select-car-form rounded"> <div class="row justify-content-md-center">
<funnelSubHeader <div class="col-md-6 col-xl-4">
cmsWidgetName="FunnelSubHeaderWidget" <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="siteSubHeader" />
class="siteSubHeader" /> <vehicleQuestion
ref="vehicleYearQuestion"
v-model="selectedYear"
:isDisabled="!yearOptions.length"
:options="yearOptions"
cmsWidgetName="VehicleYearQuestion"
validationRules="year-required"
placeHolderText="Select year"
inputId="yearQuestionField" />
<vehicleQuestion <vehicleQuestion
ref="vehicleYearQuestion" ref="vehicleMakeQuestion"
v-model="selectedYear" v-model="selectedMake"
:isDisabled="!yearOptions.length" :isDisabled="!makeOptions.length"
:options="yearOptions" :options="makeOptions"
cmsWidgetName="VehicleYearQuestion" cmsWidgetName="VehicleMakeQuestion"
validationRules="year-required" validationRules="make-required"
placeHolderText="Select year" placeHolderText="Select make"
inputId="yearQuestionField" /> inputId="makeQuestionField" />
<vehicleQuestion <vehicleQuestion
ref="vehicleMakeQuestion" ref="vehicleModelQuestion"
v-model="selectedMake" v-model="selectedModel"
:isDisabled="!makeOptions.length" cmsWidgetName="VehicleModelQuestion"
:options="makeOptions" :isDisabled="!modelOptions.length"
cmsWidgetName="VehicleMakeQuestion" :options="modelOptions"
validationRules="make-required" validationRules="model-required"
placeHolderText="Select make" placeHolderText="Select model"
inputId="makeQuestionField" /> inputId="modelQuestionField" />
<vehicleQuestion <vehicleQuestion
ref="vehicleModelQuestion" ref="vehicleStyleQuestion"
v-model="selectedModel" v-model="selectedStyle"
cmsWidgetName="VehicleModelQuestion" cmsWidgetName="VehicleStyleQuestion"
:isDisabled="!modelOptions.length" :isDisabled="!styleOptions.length"
:options="modelOptions" :options="styleOptions"
validationRules="model-required" validationRules="style-required"
placeHolderText="Select model" placeHolderText="Select style"
inputId="modelQuestionField" /> inputId="styleQuestionField" />
<div class="col-md-3">&nbsp;</div>
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
:displayVehicleImage="imageUrl"
:vehicleCategory="category"
:displayUnmatchedVehicleIcon="unmatchedVehicleIcon"
class="mt-5 vehicle-footer"
ref="banner" />
<vehicleQuestion <navbar
ref="vehicleStyleQuestion" cmsWidgetName="FunnelFooterWidget"
v-model="selectedStyle" @ForwardClicked="forwardButtonAction"
cmsWidgetName="VehicleStyleQuestion" :isForwardActionDisabled="!meta.valid"
:isDisabled="!styleOptions.length" ref="navbar" />
:options="styleOptions"
validationRules="style-required"
placeHolderText="Select style"
inputId="styleQuestionField" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
:displayVehicleImage="imageUrl"
:vehicleCategory="category"
:displayUnmatchedVehicleIcon="unmatchedVehicleIcon"
class="mt-5 vehicle-footer"
ref="banner" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
ref="funnelFooter" />
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -77,7 +72,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question"; import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
@ -127,9 +122,10 @@ export default {
const experimentForLogging = store.getters.applicationUser.experiments.find( const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL (e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
); );
const yearQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( const yearQuestionInitialDataPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_VEHICLE_YEARS, storeActions.GET_VEHICLE_YEARS,
{} {},
"vehicle"
); );
var makeQuestionInitialDataPromise = null; var makeQuestionInitialDataPromise = null;
var modelQuestionInitialDataPromise = null; var modelQuestionInitialDataPromise = null;
@ -140,35 +136,38 @@ export default {
store.getters.order.vehicle.model && store.getters.order.vehicle.model &&
store.getters.order.vehicle.style store.getters.order.vehicle.style
) { ) {
makeQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( makeQuestionInitialDataPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_VEHICLE_MAKES, storeActions.GET_VEHICLE_MAKES,
{ {
year: store.getters.order.vehicle.year, year: store.getters.order.vehicle.year,
} },
"vehicle"
); );
modelQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( modelQuestionInitialDataPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_VEHICLE_MODELS, storeActions.GET_VEHICLE_MODELS,
{ {
year: store.getters.order.vehicle.year, year: store.getters.order.vehicle.year,
make: store.getters.order.vehicle.make, make: store.getters.order.vehicle.make,
} },
"vehicle"
); );
styleQuestionInitialDataPromise = baseMixin.methods.dispatchStoreAction( styleQuestionInitialDataPromise = baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_VEHICLE_STYLES, storeActions.GET_VEHICLE_STYLES,
{ {
year: store.getters.order.vehicle.year, year: store.getters.order.vehicle.year,
make: store.getters.order.vehicle.make, make: store.getters.order.vehicle.make,
model: store.getters.order.vehicle.model, model: store.getters.order.vehicle.model,
} },
"vehicle"
); );
} }
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure. // If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
if (experimentForLogging !== undefined) { if (experimentForLogging !== undefined) {
// Log experiment exposure // Log experiment exposure
baseMixin.methods.dispatchStoreAction( baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.LOG_EXPERIMENT_EXPOSURE, storeActions.LOG_EXPERIMENT_EXPOSURE,
{ {
userId: getDeviceIdValue(), userId: getDeviceIdValue(),
@ -176,6 +175,7 @@ export default {
pageName: to.query.fmgPage, pageName: to.query.fmgPage,
experiment: experimentForLogging, experiment: experimentForLogging,
}, },
"vehicle",
false false
); );
} }
@ -264,8 +264,13 @@ export default {
model model
); );
this.styleOptions = result?.data; this.styleOptions = result?.data;
if (this.styleOptions.length === 1) this.selectedStyle = this.styleOptions[0]; if (this.styleOptions.length === 1) {
else this.selectedStyle = null; const sameStyle = this.selectedStyle === this.styleOptions[0];
this.selectedStyle = this.styleOptions[0];
if (sameStyle) {
this.getVehicleDetails();
}
} else this.selectedStyle = null;
} else { } else {
this.styleOptions = []; this.styleOptions = [];
this.selectedStyle = null; this.selectedStyle = null;
@ -273,18 +278,7 @@ export default {
}, },
async selectedStyle(style) { async selectedStyle(style) {
if (style) { if (style) {
this.carId = null; this.getVehicleDetails();
const result = await this.getVehicle(
this.selectedYear,
this.selectedMake,
this.selectedModel,
style
);
this.carId = result?.data.carId;
this.category = result?.data.category;
this.imageUrl = result?.data.imageUrl;
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
} else { } else {
this.imageUrl = null; this.imageUrl = null;
} }
@ -304,16 +298,34 @@ export default {
}, },
methods: { methods: {
getVehicle(year, make, model, style) { getVehicle(year, make, model, style) {
return this.dispatchStoreAction(this.storeActions.GET_VEHICLE, { return this.dispatchStoreActionWithLogging(
year: year, this.storeActions.GET_VEHICLE,
make: make, {
model: model, year: year,
style: style, make: make,
}); model: model,
style: style,
},
"vehicle"
);
}, },
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return true; return true;
}, },
async getVehicleDetails() {
this.carId = null;
const result = await this.getVehicle(
this.selectedYear,
this.selectedMake,
this.selectedModel,
this.selectedStyle
);
this.carId = result?.data.carId;
this.category = result?.data.category;
this.imageUrl = result?.data.imageUrl;
this.imageVifNumber = result?.data.imageVifNumber;
this.imageVifColor = result?.data.imageVifColor;
},
async forwardButtonAction() { async forwardButtonAction() {
this.dispatchStoreAction( this.dispatchStoreAction(
@ -354,24 +366,36 @@ export default {
return store.getters.vehicle.style; return store.getters.vehicle.style;
}, },
async getMakeOptions(year) { async getMakeOptions(year) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, { return await baseMixin.methods.dispatchStoreActionWithLogging(
year: year, storeActions.GET_VEHICLE_MAKES,
}); {
year: year,
},
"vehicle"
);
}, },
async getModelOptions(year, make) { async getModelOptions(year, make) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, { return await baseMixin.methods.dispatchStoreActionWithLogging(
year: year, storeActions.GET_VEHICLE_MODELS,
make: make, {
}); year: year,
make: make,
},
"vehicle"
);
}, },
async getStyleOptions(year, make, model) { async getStyleOptions(year, make, model) {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, { return await baseMixin.methods.dispatchStoreActionWithLogging(
year: year, storeActions.GET_VEHICLE_STYLES,
make: make, {
model: model, year: year,
}); make: make,
model: model,
},
"vehicle"
);
}, },
getImagefromStore() { getImagefromStore() {
return store.getters.vehicle.imageUrl; return store.getters.vehicle.imageUrl;
@ -392,7 +416,7 @@ export default {
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
funnelSubHeader, funnelSubHeader,
vehicleBanner, vehicleBanner,
Form, Form,
@ -402,10 +426,6 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
.select-car-form {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.vehicle-footer { .vehicle-footer {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -47,14 +47,14 @@ jest.mock("@/helpers/damage-helper", () => ({
})); }));
describe("vin-lookup.vue", () => { describe("vin-lookup.vue", () => {
it("Should update the funnel-footer forward button when VIN is changed", (done) => { it("Should update the nav-bar forward button when VIN is changed", (done) => {
//Arrange //Arrange
const { wrapper } = setupMocks({}); const { wrapper } = setupMocks({});
//Act //Act
wrapper.setData({ vin: "newValue" }); wrapper.setData({ vin: "newValue" });
//Assert //Assert
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toBeCalled(); expect(wrapper.vm.$refs.navbar.updateButtonText).toBeCalled();
done(); done();
}); });
}); });
@ -368,8 +368,8 @@ function mockOutPromises({ carId, isZipValid = true, isZipServiceable = true })
} }
function mockOutStubFunctions(wrapper) { function mockOutStubFunctions(wrapper) {
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
wrapper.vm.getZipCodeData = jest wrapper.vm.getZipCodeData = jest
.fn() .fn()
.mockReturnValue({ isValid: true, isServiceable: true, state: "OH" }); .mockReturnValue({ isValid: true, isServiceable: true, state: "OH" });

View file

@ -1,131 +1,125 @@
<template> <template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }"> <Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles"> <loadingModal ref="loadingModal" />
<loadingModal ref="loadingModal" /> <div class="container-fluid page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> <div class="row">
<vehicleBanner <div class="col">
cmsWidgetName="VehicleBannerWidget" <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div class="row mb-2">
<div class="col">
<textboxQuestion
cmsWidgetName="VinNumberQuestionWidget"
v-model="vin"
customInputId="vin"
isRequired
:validationRules="
!vinPopulatedOnPageLoad ? 'vin-required|vin-format' : ''
"
:isDisabled="vinPopulatedOnPageLoad"
maxLength="17"
:mask="vinMask"
includeImageQuestion
:imageQuestionSubmitHandler="getVinFromImage"
:maxFileSize="imageUploadMaxFileSize"
@image-lookup-error="displayVinScanAlert"
@image-validity-error="displayVinScanAlert"
data-test="vin-lookup-field"
ref="vinLookupQuestion" />
</div>
</div> </div>
<div class="row"> </div>
<div class="col"> <div class="row justify-content-md-center">
<alert <div class="col-md-6 col-xl-4">
cmsWidgetName="AlertVinScanFailed" <vehicleBanner
v-if="displayVinScanFailedAlert" cmsWidgetName="VehicleBannerWidget"
alertClass="alert-danger" /> :displayGenericVehicleImage="false" />
</div>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<textboxQuestion
cmsWidgetName="VinNumberQuestionWidget"
v-model="vin"
customInputId="vin"
isRequired
:validationRules="!vinPopulatedOnPageLoad ? 'vin-required|vin-format' : ''"
:isDisabled="vinPopulatedOnPageLoad"
maxLength="17"
:mask="vinMask"
includeImageQuestion
:imageQuestionSubmitHandler="getVinFromImage"
:maxFileSize="imageUploadMaxFileSize"
@image-lookup-error="displayVinScanAlert"
@image-validity-error="displayVinScanAlert"
data-test="vin-lookup-field"
ref="vinLookupQuestion" />
<alert
cmsWidgetName="AlertVinScanFailed"
v-if="displayVinScanFailedAlert"
alertClass="alert-danger" />
<vinInformation />
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
customInputId="serviceZipCode"
mask="#####"
isRequired
validationRules="zip-required|zip-format" />
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
customInputId="emailAddress"
isRequired
validationRules="email-address-required|email-address-format" />
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
class="my-4"
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
v-model="customAlertData"
v-if="
vinPopulatedOnPageLoad &&
isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<alert
class="my-4"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
v-model="customAlertData"
v-if="displayMatchedDifferentVehicleAlert"
alertClass="alert-warning" />
<alert
class="my-4"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-model="customAlertData"
v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" />
<alert
class="my-4"
v-model="customAlertData"
v-if="displayVinNotFoundAlert"
alertClass="alert-danger"
cmsWidgetName="AlertVinNotFoundWidget" />
<alert
class="my-4"
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
v-model="customAlertData"
v-if="
vinPopulatedOnPageLoad &&
!isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<navbar
cmsWidgetName="FunnelFooterWidget"
ref="navbar"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
<div class="row">
<div class="col">
<vinInformation />
</div>
</div>
<div class="row mb-0 mt-4">
<div class="col">
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
customInputId="serviceZipCode"
mask="#####"
isRequired
validationRules="zip-required|zip-format" />
</div>
</div>
<div class="row mt-4">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="emailAddress"
customInputId="emailAddress"
isRequired
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-0">
<div class="col">
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
</div>
</div>
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
class="my-4"
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
v-model="customAlertData"
v-if="
vinPopulatedOnPageLoad &&
isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<alert
class="my-4"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
v-model="customAlertData"
v-if="displayMatchedDifferentVehicleAlert"
alertClass="alert-warning" />
<alert
class="my-4"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-model="customAlertData"
v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" />
<alert
class="my-4"
v-model="customAlertData"
v-if="displayVinNotFoundAlert"
alertClass="alert-danger"
cmsWidgetName="AlertVinNotFoundWidget" />
<alert
class="my-4"
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
v-model="customAlertData"
v-if="
vinPopulatedOnPageLoad &&
!isInsuranceVerified &&
!displayInvalidZipAlert &&
!displayNonServiceableZipAlert
"
alertClass="alert-success" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</Form> </Form>
@ -134,7 +128,7 @@
<script> <script>
// Components // Components
import funnelHeader from "@/fmg-components/funnel-header/funnel-header"; import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer"; import navbar from "@/fmg-components/nav-bar/nav-bar";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner"; import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header"; import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert"; import alert from "@/ux-components/alert/alert";
@ -255,9 +249,10 @@ export default {
// If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation // If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation
if (!this.vinPopulatedOnPageLoad) { if (!this.vinPopulatedOnPageLoad) {
const vehicleLookupResponse = this.dispatchStoreAction( const vehicleLookupResponse = this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VEHICLE_BY_VIN, storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin: this.vin } { vin: this.vin },
"vin-lookup"
); );
// Settle promises and get results // Settle promises and get results
@ -278,7 +273,7 @@ export default {
const isZipValid = resultMap.zipCodeData.isValid; const isZipValid = resultMap.zipCodeData.isValid;
if (this.serviceZipCode && !isZipValid) { if (this.serviceZipCode && !isZipValid) {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
@ -295,7 +290,7 @@ export default {
} }
// Remove loader and stop processing the page. // Remove loader and stop processing the page.
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// Check if the CarId is different from the lookup vs what is in state currently. // Check if the CarId is different from the lookup vs what is in state currently.
@ -311,14 +306,15 @@ export default {
this.displayMatchedDifferentVehicleAlert = true; this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId( this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
resultMap.vehicleLookupResponse.carId resultMap.vehicleLookupResponse.carId,
"vin-lookup"
); );
// Update button "Continue with..." // Update button "Continue with..."
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
`Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}` `Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}`
); );
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
} }
// Save vin, vehicle, customer and service information // Save vin, vehicle, customer and service information
@ -380,7 +376,7 @@ export default {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
} }
return this.$refs.funnelFooter.removeLoader(); return this.$refs.navbar.removeLoader();
}, },
async navigateForward() { async navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) { if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
@ -399,7 +395,11 @@ export default {
}, },
getVinFromImage(image) { getVinFromImage(image) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_IMAGE, image) this.dispatchStoreActionWithLogging(
storeActions.LOOKUP_VIN_BY_IMAGE,
image,
"vin-lookup"
)
.then((response) => { .then((response) => {
if (response.data.length > 0) { if (response.data.length > 0) {
resolve(response.data[0]); resolve(response.data[0]);
@ -488,7 +488,7 @@ export default {
vin() { vin() {
this.displayVinNotFoundAlert = false; this.displayVinNotFoundAlert = false;
this.displayVinScanFailedAlert = false; this.displayVinScanFailedAlert = false;
this.$refs.funnelFooter.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );
}, },
@ -498,7 +498,7 @@ export default {
}, },
components: { components: {
funnelHeader, funnelHeader,
funnelFooter, navbar,
vehicleBanner, vehicleBanner,
funnelSubHeader, funnelSubHeader,
textboxQuestion, textboxQuestion,

View file

@ -31,6 +31,19 @@ export default {
return store.dispatch(type, payload); return store.dispatch(type, payload);
}, },
dispatchStoreActionWithLogging(type, payload, pageNameToLog, encodePayload = true) {
// Encode the payload if required
if (encodePayload) {
encodeUriData(payload);
}
const wrappedParams = {
payload: payload,
pageNameToLog: pageNameToLog,
};
return store.dispatch(type, wrappedParams);
},
savePageDataToStore(page, data) { savePageDataToStore(page, data) {
store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data }); store.commit(storeMutations.UPDATE_PAGE_DATA, { page: page, data: data });
}, },
@ -50,10 +63,16 @@ export default {
const footerInfoBox = document.querySelector(".footer#infoBox"); const footerInfoBox = document.querySelector(".footer#infoBox");
return footerInfoBox ? footerInfoBox.offsetHeight : 0; return footerInfoBox ? footerInfoBox.offsetHeight : 0;
}, },
async getZipCodeData(zipCode) { async getZipCodeData(zipCode, pageNameToLog = null) {
const serviceZipValidationResponse = await this.dispatchStoreAction( const pageName = pageNameToLog ?? this.$options?.name;
if (pageName == null) {
console.log("Missed a spot.");
}
const serviceZipValidationResponse = await this.dispatchStoreActionWithLogging(
storeActions.VALIDATE_ZIP, storeActions.VALIDATE_ZIP,
{ zip: zipCode } { zip: zipCode },
pageName
); );
return { return {

View file

@ -101,8 +101,8 @@ describe("baseMixin.js", () => {
test("getZipCodeData calls dispatch", () => { test("getZipCodeData calls dispatch", () => {
const mixIn = getMixInInstance({}); const mixIn = getMixInInstance({});
mixIn.methods.dispatchStoreAction = jest.fn(); mixIn.methods.dispatchStoreActionWithLogging = jest.fn();
mixIn.methods.dispatchStoreAction.mockReturnValue({ mixIn.methods.dispatchStoreActionWithLogging.mockReturnValue({
data: { isValid: true, isServiceable: true, state: "OH" }, data: { isValid: true, isServiceable: true, state: "OH" },
}); });
const type = ""; const type = "";
@ -118,7 +118,7 @@ describe("baseMixin.js", () => {
mixIn.methods.getZipCodeData(type, payload); mixIn.methods.getZipCodeData(type, payload);
expect(mixIn.methods.dispatchStoreAction).toBeCalled(); expect(mixIn.methods.dispatchStoreActionWithLogging).toBeCalled();
}); });
}); });

View file

@ -408,12 +408,13 @@ export default {
for (let partOrQuestion of partsOrQuestions) { for (let partOrQuestion of partsOrQuestions) {
if (this.hasCapabilityQuestions([partOrQuestion])) { if (this.hasCapabilityQuestions([partOrQuestion])) {
let capabilityQuestionsForGlassLocation = ( let capabilityQuestionsForGlassLocation = (
await baseMixin.methods.dispatchStoreAction( await baseMixin.methods.dispatchStoreActionWithLogging(
storeActions.GET_CAPABILITY_QUESTIONS, storeActions.GET_CAPABILITY_QUESTIONS,
{ {
carId: store.getters.vehicle.carId, carId: store.getters.vehicle.carId,
partNumber: partOrQuestion.parts[0].partNumber, partNumber: partOrQuestion.parts[0].partNumber,
} },
currentPage
) )
).data; ).data;
@ -447,9 +448,17 @@ export default {
const payment = store.getters.payment; const payment = store.getters.payment;
if (store.getters.order.referralNumber?.length === 6) { if (store.getters.order.referralNumber?.length === 6) {
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal }); navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: currentPage,
loadingModal: self.$refs.loadingModal,
});
} else if (payment.isInsurance && payment.insuranceCoverage.isVerified) { } else if (payment.isInsurance && payment.insuranceCoverage.isVerified) {
navigateToHeritageFunnel({ loadingModal: self.$refs.loadingModal }); navigateToHeritageFunnel({
shouldSaveSession: true,
pageNameToLog: currentPage,
loadingModal: self.$refs.loadingModal,
});
} else { } else {
self.$router.navigateWithSaving( self.$router.navigateWithSaving(
self.navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, self.navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,

View file

@ -6,12 +6,16 @@ import { saveSession } from "@/helpers/heritage-integration/order-helper.js";
export default { export default {
methods: { methods: {
async navigateForwardWithSingleCarMatch() { async navigateForwardWithSingleCarMatch() {
const pageName = this.$options?.name;
// If we have not already saved a session, we need to save one now before the lengthy call to getPartsOrQuestions // If we have not already saved a session, we need to save one now before the lengthy call to getPartsOrQuestions
if (!store.getters.applicationUser.savedSessionId) { if (!store.getters.applicationUser.savedSessionId) {
await saveSession({}); await saveSession({ pageNameToLog: pageName });
} }
const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS); const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS, {
pageNameToLog: pageName,
});
const partsOrQuestions = result.data.partsOrQuestions; const partsOrQuestions = result.data.partsOrQuestions;
vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this); vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this);

View file

@ -6,6 +6,7 @@ import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"
import { routingTable } from "@/router/router-constants/routing-table.js"; import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events"; import { globalEvents, globalEventTypes } from "@/constants/events";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { getQuerystringParameter } from "@/helpers/querystring-helper";
import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper"; import { getDeviceIdValue } from "@/helpers/heritage-integration/cookie-helper";
// Heritage integration // Heritage integration
@ -71,20 +72,11 @@ const routes = [
? to.query.isInsurance == "true" ? to.query.isInsurance == "true"
? true ? true
: false : false
: null : null,
to.query.fmgPage
); );
const pageToRedirectTo = await getPageToRouteExistingOrderTo( const pageToRedirectTo = await getPageToRouteExistingOrderTo(to);
to,
loadSessionResponse
);
// If getPageToRouteExistingOrderTo determines that the return user needs to
// go back to heritage funnel, send them there and stop our current navigation.
if (pageToRedirectTo === "heritage") {
await navigateToHeritageFunnel();
return next(false);
}
// Assign our fmgPage so it will load normally like the other pages. // Assign our fmgPage so it will load normally like the other pages.
to.query.fmgPage = pageToRedirectTo; to.query.fmgPage = pageToRedirectTo;
@ -181,7 +173,7 @@ router.afterEach(async (to, from) => {
store.getters.applicationUser.savedSessionId || store.getters.applicationUser.savedSessionId ||
store.getters.order.customer?.emailAddress store.getters.order.customer?.emailAddress
) { ) {
await saveSession({}); await saveSession({ pageNameToLog: to.query.fmgPage });
} }
} }
@ -284,14 +276,9 @@ async function navigate(
fmgPage: destinationFmgPageValue, fmgPage: destinationFmgPageValue,
}; };
const queryString = window.location.search; const hasZip = getQuerystringParameter(queryStrings.ZIP_CODE);
const urlParams = new URLSearchParams(queryString); const zip = getQuerystringParameter(queryStrings.ZIP_CODE);
const lowerCaseParams = new URLSearchParams(); const promo = getQuerystringParameter(queryStrings.PROMO);
for (const [name, value] of urlParams) {
lowerCaseParams.append(name.toLowerCase(), value);
}
const hasZip = lowerCaseParams.has(queryStrings.ZIP_CODE);
const zip = lowerCaseParams.get(queryStrings.ZIP_CODE);
if ( if (
hasZip && hasZip &&
@ -303,6 +290,10 @@ async function navigate(
queryStringsObject[queryStrings.ZIP_CODE] = zip; queryStringsObject[queryStrings.ZIP_CODE] = zip;
} }
if (promo) {
queryStringsObject[queryStrings.PROMO] = promo;
}
router.push({ router.push({
name: "root", name: "root",
query: Object.assign(optionalQuery, queryStringsObject), query: Object.assign(optionalQuery, queryStringsObject),
@ -394,18 +385,26 @@ function arePagePrerequisitesValid(component) {
// Run SiteEntry and PageEntry triggers for experiments // Run SiteEntry and PageEntry triggers for experiments
async function runExperiments(nextPage) { async function runExperiments(nextPage) {
if (!store.getters.applicationUser.triggeredSiteEntry) { if (!store.getters.applicationUser.triggeredSiteEntry) {
await baseMixin.methods.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { await baseMixin.methods.dispatchStoreActionWithLogging(
userId: getDeviceIdValue(), storeActions.RUN_EXPERIMENTS_FOR_TRIGGER,
triggerEvent: experimentTriggers.SITE_ENTRY, {
triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE, userId: getDeviceIdValue(),
}); triggerEvent: experimentTriggers.SITE_ENTRY,
triggerValue: applicationConfig.SITE_ENTRY_TRIGGER_VALUE,
},
nextPage
);
} }
await baseMixin.methods.dispatchStoreAction(storeActions.RUN_EXPERIMENTS_FOR_TRIGGER, { await baseMixin.methods.dispatchStoreActionWithLogging(
userId: getDeviceIdValue(), storeActions.RUN_EXPERIMENTS_FOR_TRIGGER,
triggerEvent: experimentTriggers.PAGE_ENTRY, {
triggerValue: nextPage, userId: getDeviceIdValue(),
}); triggerEvent: experimentTriggers.PAGE_ENTRY,
triggerValue: nextPage,
},
nextPage
);
} }
export default router; export default router;

View file

@ -17,6 +17,7 @@ const fmgPageValues = {
CUSTOMER_DETAILS: "customer-details", CUSTOMER_DETAILS: "customer-details",
REVIEW: "review", REVIEW: "review",
PAYMENT_METHOD: "payment-method", PAYMENT_METHOD: "payment-method",
CONFIRMATION: "confirmation",
}; };
export { fmgPageValues }; export { fmgPageValues };

View file

@ -467,6 +467,18 @@ const routingTable = function (store) {
}, },
], ],
}, },
{
fmgPageValue: fmgPageValues.PAYMENT_METHOD,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.CONFIRMATION,
},
],
},
{
fmgPageValue: fmgPageValues.CONFIRMATION,
},
]; ];
}; };

View file

@ -22,6 +22,7 @@ import {
getDisplayTextForDurationLength, getDisplayTextForDurationLength,
} from "@/layouts/schedule/helpers/schedule-helper"; } from "@/layouts/schedule/helpers/schedule-helper";
import { getDateDifferenceInDays } from "@/helpers/date-helper";
// Export State // Export State
const getDefaultState = () => { const getDefaultState = () => {
return { return {
@ -234,8 +235,6 @@ export const mutations = {
}, },
updateRegistration(state, registrationInfo) { updateRegistration(state, registrationInfo) {
state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate; state.order.vehicle.registration.licensePlate = registrationInfo?.licensePlate;
state.order.customer.firstName = registrationInfo?.firstName;
state.order.customer.lastName = registrationInfo?.lastName;
}, },
updateServiceZip(state, serviceZipInfo) { updateServiceZip(state, serviceZipInfo) {
state.order.serviceLocation.state = serviceZipInfo.state; state.order.serviceLocation.state = serviceZipInfo.state;
@ -636,40 +635,62 @@ function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map((x) => x[propertyName]).filter((x) => x); return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
} }
function provisionalTriggersToString(provisionalTriggers) { function getTimeSlotsAdditionalEventData(
return "ProvisionalTriggers:" + provisionalTriggers.join(","); provisionalTriggers,
zipCode,
firstAvailableAppointmentDateString,
shopAppointmentType
) {
var numberOfDays = null;
if (firstAvailableAppointmentDateString)
numberOfDays = getDateDifferenceInDays(new Date(), firstAvailableAppointmentDateString);
if (shopAppointmentType)
return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ShopAppointmentType:${shopAppointmentType},ProvisionalTriggers:${provisionalTriggers.join(
","
)}`;
else
return `FirstAvailableAppointment:${numberOfDays},Zip:${zipCode},ProvisionalTriggers:${provisionalTriggers.join(
","
)}`;
} }
// Export Actions // Export Actions
export const actions = { export const actions = {
// Vehicle API Actions // Vehicle API Actions
getVehicleYears(context) { getVehicleYears(context, { pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetVehicleYears.method, method: endpoints.GetVehicleYears.method,
endpoint: endpoints.GetVehicleYears.url, endpoint: endpoints.GetVehicleYears.url,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
lookupVehicleByYmms(context, { year, make, model, style }) { lookupVehicleByYmms(context, { payload: { year, make, model, style }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByYmms.method, method: endpoints.LookupVehicleByYmms.method,
endpoint: `${endpoints.LookupVehicleByYmms.url}/${year}/${make}/${model}/${style}`, endpoint: `${endpoints.LookupVehicleByYmms.url}/${year}/${make}/${model}/${style}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
lookupVehicleByVin(context, { vin }) { lookupVehicleByVin(context, { payload: { vin }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LookupVehicleByVin.method, method: endpoints.LookupVehicleByVin.method,
endpoint: endpoints.LookupVehicleByVin.url, endpoint: endpoints.LookupVehicleByVin.url,
payload: { payload: {
vin: vin, // EX "1J4GW58S4XC541166" vin: vin, // EX "1J4GW58S4XC541166"
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
lookupVinByPlate(context, { licensePlate, licenseState }) { lookupVinByPlate(context, { payload: { licensePlate, licenseState }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LookupVinByPlate.method, method: endpoints.LookupVinByPlate.method,
endpoint: endpoints.LookupVinByPlate.url, endpoint: endpoints.LookupVinByPlate.url,
@ -677,12 +698,17 @@ export const actions = {
licensePlate: licensePlate, licensePlate: licensePlate,
licenseState: licenseState, licenseState: licenseState,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
lookupVinByAddress( lookupVinByAddress(
context, context,
{ licenseLastName, licenseStreetAddress, licenseZip, licenseState } {
payload: { licenseLastName, licenseStreetAddress, licenseZip, licenseState },
pageNameToLog,
}
) { ) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LookupVinByAddress.method, method: endpoints.LookupVinByAddress.method,
@ -693,10 +719,14 @@ export const actions = {
licenseZip: licenseZip, licenseZip: licenseZip,
licenseState: licenseState, licenseState: licenseState,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
lookupVinByImage(context, image) { lookupVinByImage(context, { payload, pageNameToLog }) {
const image = payload;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
@ -718,68 +748,84 @@ export const actions = {
method: endpoints.LookupVinByImage.method, method: endpoints.LookupVinByImage.method,
endpoint: endpoints.LookupVinByImage.url, endpoint: endpoints.LookupVinByImage.url,
payload: data, payload: data,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}); });
}, },
isVinByAddressPermissible(context, zip) { isVinByAddressPermissible(context, { payload: { zip }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.IsVinByAddressPermissible.method, method: endpoints.IsVinByAddressPermissible.method,
endpoint: `${endpoints.IsVinByAddressPermissible.url}?zipcode=${zip}`, endpoint: `${endpoints.IsVinByAddressPermissible.url}?zipcode=${zip}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getVehicleMakes(context, { year }) { getVehicleMakes(context, { payload: { year }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetVehicleMakes.method, method: endpoints.GetVehicleMakes.method,
endpoint: `${endpoints.GetVehicleMakes.url}/${year}`, endpoint: `${endpoints.GetVehicleMakes.url}/${year}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getVehicleModels(context, { year, make }) { getVehicleModels(context, { payload: { year, make }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetVehicleModels.method, method: endpoints.GetVehicleModels.method,
endpoint: `${endpoints.GetVehicleModels.url}/${year}/${make}`, endpoint: `${endpoints.GetVehicleModels.url}/${year}/${make}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getVehicleStyles(context, { year, make, model }) { getVehicleStyles(context, { payload: { year, make, model }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetVehicleStyles.method, method: endpoints.GetVehicleStyles.method,
endpoint: `${endpoints.GetVehicleStyles.url}/${year}/${make}/${model}`, endpoint: `${endpoints.GetVehicleStyles.url}/${year}/${make}/${model}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getVehicle(context, { year, make, model, style }) { getVehicle(context, { payload: { year, make, model, style }, pageNameToLog }) {
return globalMethods return globalMethods
.callHttpClient({ .callHttpClient({
methods: endpoints.GetVehicle.method, methods: endpoints.GetVehicle.method,
endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`, endpoint: `${endpoints.GetVehicle.url}/${year}/${make}/${model}/${style}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}) })
.then((response) => { .then((response) => {
return response; return response;
}); });
}, },
getDamageOptions(context, { carId }) { getDamageOptions(context, { payload: { carId }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.GetDamageOptions.method, methods: endpoints.GetDamageOptions.method,
endpoint: `${endpoints.GetDamageOptions.url}/${carId}`, endpoint: `${endpoints.GetDamageOptions.url}/${carId}`,
payload: {}, payload: {},
additionalSuccessEventDataHandler: (response) => additionalSuccessEventDataHandler: (response) =>
"QueryStringZip: " + getQuerystringParameter(queryStrings.ZIP_CODE), "QueryStringZip: " + getQuerystringParameter(queryStrings.ZIP_CODE),
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
validateZip(context, { zip }) { validateZip(context, { payload: { zip }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
methods: endpoints.ValidateZip.method, methods: endpoints.ValidateZip.method,
endpoint: `${endpoints.ValidateZip.url}/${zip}`, endpoint: `${endpoints.ValidateZip.url}/${zip}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
@ -828,6 +874,8 @@ export const actions = {
payload: { payload: {
pageName: pageName, pageName: pageName,
}, },
logApiCall: true,
pageNameToLog: pageName,
}); });
}, },
@ -846,11 +894,16 @@ export const actions = {
pageName pageName
), ),
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageName,
}); });
}, },
// Analytics Actions // Analytics Actions
logExperimentExposure(context, { userId, sessionKey, pageName, experiment }) { logExperimentExposure(
context,
{ payload: { userId, sessionKey, pageName, experiment }, pageNameToLog }
) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.LogExperimentExposureIfAssigned.method, method: endpoints.LogExperimentExposureIfAssigned.method,
endpoint: endpoints.LogExperimentExposureIfAssigned.url, endpoint: endpoints.LogExperimentExposureIfAssigned.url,
@ -871,15 +924,19 @@ export const actions = {
pageName: pageName, pageName: pageName,
}, },
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
// Location API Actions // Location API Actions
getAlertReasonsByCtu(context, { ctu }) { getAlertReasonsByCtu(context, { payload: { ctu }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetAlertReasons.method, method: endpoints.GetAlertReasons.method,
endpoint: `${endpoints.GetAlertReasons.url}/${ctu}`, endpoint: `${endpoints.GetAlertReasons.url}/${ctu}`,
payload: {}, payload: {},
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
@ -1047,7 +1104,10 @@ export const actions = {
}); });
}, },
async runExperimentsForTrigger(context, { userId, triggerEvent, triggerValue }) { async runExperimentsForTrigger(
context,
{ payload: { userId, triggerEvent, triggerValue }, pageNameToLog }
) {
if (triggerEvent == experimentTriggers.SITE_ENTRY) { if (triggerEvent == experimentTriggers.SITE_ENTRY) {
context.commit(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true); context.commit(storeMutations.UPDATE_TRIGGERED_SITE_ENTRY, true);
} }
@ -1064,6 +1124,8 @@ export const actions = {
method: endpoints.RunExperimentsForTrigger.method, method: endpoints.RunExperimentsForTrigger.method,
endpoint: endpoints.RunExperimentsForTrigger.url, endpoint: endpoints.RunExperimentsForTrigger.url,
payload: payload, payload: payload,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
context.commit(storeMutations.UPDATE_EXPERIMENTS, response.data.experiments); context.commit(storeMutations.UPDATE_EXPERIMENTS, response.data.experiments);
@ -1078,7 +1140,7 @@ export const actions = {
}, },
// PartsOrQuestions API Actions // PartsOrQuestions API Actions
async getPartsOrQuestions(context) { async getPartsOrQuestions(context, { pageNameToLog }) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order; const order = context.state.order;
@ -1100,6 +1162,8 @@ export const actions = {
zip: zipCode, zip: zipCode,
vin: vin, vin: vin,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
// Flatten location and name properties // Flatten location and name properties
@ -1111,7 +1175,7 @@ export const actions = {
}, },
// Parts API Actions // Parts API Actions
async getParts(context) { async getParts(context, { pageNameToLog }) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order; const order = context.state.order;
@ -1136,6 +1200,8 @@ export const actions = {
zip: zipCode, zip: zipCode,
vin: vin, vin: vin,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
// Flatten location and name properties // Flatten location and name properties
@ -1146,13 +1212,15 @@ export const actions = {
return response; return response;
}, },
getWipers(context) { getWipers(context, { pageNameToLog }) {
const carId = context.getters.vehicle.carId; const carId = context.getters.vehicle.carId;
const serviceZipCode = context.getters.order.serviceLocation.zipCode; const serviceZipCode = context.getters.order.serviceLocation.zipCode;
return globalMethods return globalMethods
.callHttpClient({ .callHttpClient({
method: endpoints.GetWipers.method, method: endpoints.GetWipers.method,
endpoint: `${endpoints.GetWipers.url}/${carId}/${serviceZipCode}`, endpoint: `${endpoints.GetWipers.url}/${carId}/${serviceZipCode}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}) })
.catch((error) => { .catch((error) => {
// The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow // The wiper service sometimes returns 500s on legitimate carId/zipCode combination - return empty array instead of breaking flow
@ -1160,14 +1228,16 @@ export const actions = {
}); });
}, },
getRainDefense(context) { getRainDefense(context, { pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetRainDefense.method, method: endpoints.GetRainDefense.method,
endpoint: endpoints.GetRainDefense.url, endpoint: endpoints.GetRainDefense.url,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getMobileFeePart(context) { getMobileFeePart(context, { pageNameToLog }) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const parentAccountNumber = context.getters.payment.parentAccountNumber; const parentAccountNumber = context.getters.payment.parentAccountNumber;
const billToAccountNumber = 87291; // TODO: MAKE THIS REAL const billToAccountNumber = 87291; // TODO: MAKE THIS REAL
@ -1175,10 +1245,12 @@ export const actions = {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetMobileFeePart.method, method: endpoints.GetMobileFeePart.method,
endpoint: `${endpoints.GetMobileFeePart.url}/${damageType}/${parentAccountNumber}/${billToAccountNumber}`, endpoint: `${endpoints.GetMobileFeePart.url}/${damageType}/${parentAccountNumber}/${billToAccountNumber}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getServiceabilityDetails(context, { serviceZipCode }) { getServiceabilityDetails(context, { payload: { serviceZipCode }, pageNameToLog }) {
const lineItemsWithOnlyPartNumbers = context.getters.order.lineItems.supportingItems.map( const lineItemsWithOnlyPartNumbers = context.getters.order.lineItems.supportingItems.map(
(lineItem) => ({ (lineItem) => ({
partNumber: lineItem.partNumber, partNumber: lineItem.partNumber,
@ -1202,20 +1274,24 @@ export const actions = {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetServiceabilityDetails.method, method: endpoints.GetServiceabilityDetails.method,
endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&${lineItems}&${glassPieces}`, endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&carId=${carId}&${lineItems}&${glassPieces}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getProviders(context, { serviceZipCode }) { getProviders(context, { payload: { serviceZipCode }, pageNameToLog }) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const shopRadiusInMiles = 100; const shopRadiusInMiles = 100;
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetProviders.method, method: endpoints.GetProviders.method,
endpoint: `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}`, endpoint: `${endpoints.GetProviders.url}/${serviceZipCode}/${damageType}/${shopRadiusInMiles}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getSupportingItems(context) { getSupportingItems(context, { pageNameToLog }) {
const glassPartsArray = context.getters.lineItems.glassParts ?? []; const glassPartsArray = context.getters.lineItems.glassParts ?? [];
const carId = context.getters.vehicle.carId; const carId = context.getters.vehicle.carId;
const isRepair = context.getters.damage.isRepair; const isRepair = context.getters.damage.isRepair;
@ -1231,17 +1307,22 @@ export const actions = {
parts: glassPartsArray, parts: glassPartsArray,
numberOfRepairChips: isRepair ? numberOfChips : 0, numberOfRepairChips: isRepair ? numberOfChips : 0,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getCapabilityQuestions(context, { carId, partNumber }) { getCapabilityQuestions(context, { payload: { carId, partNumber }, pageNameToLog }) {
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetCapabilityQuestions.method, method: endpoints.GetCapabilityQuestions.method,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`, endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getPartFromCapabilityQuestionAnswer(context, glassLocation) { getPartFromCapabilityQuestionAnswer(context, { payload, pageNameToLog }) {
const glassLocation = payload;
const pageData = context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS); const pageData = context.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS);
const part = pageData.partsOrQuestions.find((x) => x.glassLocation === glassLocation) const part = pageData.partsOrQuestions.find((x) => x.glassLocation === glassLocation)
@ -1258,10 +1339,15 @@ export const actions = {
part, part,
capabilityAnswerResults: capabilityQuestionAnswersForPart, capabilityAnswerResults: capabilityQuestionAnswersForPart,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
getShopTimeSlots(context, { startDate, endDate, shopAppointmentType, providerNumber }) { getShopTimeSlots(
context,
{ payload: { startDate, endDate, shopAppointmentType, providerNumber }, pageNameToLog }
) {
const order = context.state.order; const order = context.state.order;
const vehicle = context.state.order.vehicle; const vehicle = context.state.order.vehicle;
@ -1316,12 +1402,19 @@ export const actions = {
method: endpoints.GetShopTimeSlots.method, method: endpoints.GetShopTimeSlots.method,
endpoint: endpoints.GetShopTimeSlots.url, endpoint: endpoints.GetShopTimeSlots.url,
payload: payload, payload: payload,
logApiCall: true,
pageNameToLog: pageNameToLog,
additionalSuccessEventDataHandler: (response) => additionalSuccessEventDataHandler: (response) =>
provisionalTriggersToString(response.data.provisionalTriggers), getTimeSlotsAdditionalEventData(
response.data.provisionalTriggers,
order.serviceLocation.zipCode,
response.data.days?.[0]?.date,
shopAppointmentType
),
}); });
}, },
getMobileTimeSlots(context, { startDate, endDate }) { getMobileTimeSlots(context, { payload: { startDate, endDate }, pageNameToLog }) {
const order = context.state.order; const order = context.state.order;
const vehicle = context.state.order.vehicle; const vehicle = context.state.order.vehicle;
let lineItems = [ let lineItems = [
@ -1373,23 +1466,31 @@ export const actions = {
method: endpoints.GetMobileTimeSlots.method, method: endpoints.GetMobileTimeSlots.method,
endpoint: endpoints.GetMobileTimeSlots.url, endpoint: endpoints.GetMobileTimeSlots.url,
payload: payload, payload: payload,
logApiCall: true,
pageNameToLog: pageNameToLog,
additionalSuccessEventDataHandler: (response) => additionalSuccessEventDataHandler: (response) =>
provisionalTriggersToString(response.data.provisionalTriggers), getTimeSlotsAdditionalEventData(
response.data.provisionalTriggers,
order.serviceLocation.zipCode,
response.data.days?.[0]?.date
),
}); });
}, },
getMobilePremiumFee(context) { getMobilePremiumFee(context, { pageNameToLog }) {
const damageType = context.getters.damage.isRepair ? "Repair" : "Replace"; const damageType = context.getters.damage.isRepair ? "Repair" : "Replace";
const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash"; const paymentType = context.getters.order.payment.isInsurance ? "Insurance" : "Cash";
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.GetMobilePremiumFee.method, method: endpoints.GetMobilePremiumFee.method,
endpoint: `${endpoints.GetMobilePremiumFee.url}/${paymentType}/${damageType}`, endpoint: `${endpoints.GetMobilePremiumFee.url}/${paymentType}/${damageType}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
}, },
// Session API Actions // Session API Actions
saveSession(context) { saveSession(context, { pageNameToLog }) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order; const order = context.state.order;
@ -1495,6 +1596,8 @@ export const actions = {
eon: order.eon, eon: order.eon,
}, },
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
additionalSuccessEventDataHandler: (response) => additionalSuccessEventDataHandler: (response) =>
"Email provided: " + (order.customer.emailAddress ? "true" : "false"), "Email provided: " + (order.customer.emailAddress ? "true" : "false"),
}); });
@ -1503,12 +1606,15 @@ export const actions = {
loadSession( loadSession(
context, context,
{ {
savedSessionId, payload: {
referralNumber, savedSessionId,
referralDate, referralNumber,
parentAccountNumber, referralDate,
referralCorrelationId, parentAccountNumber,
isConceptInsurance, referralCorrelationId,
isConceptInsurance,
},
pageNameToLog,
} }
) { ) {
const order = context.state.order; const order = context.state.order;
@ -1524,6 +1630,8 @@ export const actions = {
parentAccountNumber: parentAccountNumber, parentAccountNumber: parentAccountNumber,
referralCorrelationId: referralCorrelationId, referralCorrelationId: referralCorrelationId,
}, },
logApiCall: true,
pageNameToLog: pageNameToLog,
}) })
.then( .then(
async (response) => { async (response) => {
@ -1546,7 +1654,7 @@ export const actions = {
response.data response.data
); );
await resetScheduleIfUnavailable(context, response.data.order); await resetScheduleIfUnavailable(context, response.data.order, pageNameToLog);
return response; return response;
}, },
(error) => { (error) => {
@ -1567,26 +1675,25 @@ export const actions = {
context, context,
{ year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor } { year, make, model, style, carId, category, imageUrl, imageVifNumber, imageVifColor }
) { ) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); if (
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES); context.state.order.vehicle.year != year ||
context.state.order.vehicle.make != make ||
if (context.state.order.vehicle.year !== year) { context.state.order.vehicle.model != model ||
context.state.order.vehicle.style != style
) {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
context.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
context.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
context.commit(storeMutations.UPDATE_YEAR, year); context.commit(storeMutations.UPDATE_YEAR, year);
}
if (context.state.order.vehicle.make !== make) {
context.commit(storeMutations.UPDATE_MAKE, make); context.commit(storeMutations.UPDATE_MAKE, make);
}
if (context.state.order.vehicle.model !== model) {
context.commit(storeMutations.UPDATE_MODEL, model); context.commit(storeMutations.UPDATE_MODEL, model);
}
if (context.state.order.vehicle.style !== style) {
context.commit(storeMutations.UPDATE_STYLE, style); context.commit(storeMutations.UPDATE_STYLE, style);
context.commit(storeMutations.UPDATE_CAR_ID, carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
} }
context.commit(storeMutations.UPDATE_CAR_ID, carId);
context.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, category);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, imageUrl);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, imageVifNumber);
context.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, imageVifColor);
}, },
saveVehicleDamage( saveVehicleDamage(
@ -1662,15 +1769,25 @@ export const actions = {
context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES); context.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
} }
//Save new values
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo); context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
if (registrationInfo) {
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
// only update name information if there isn't a value in state already
if (!context.getters.order.customer.firstName) {
context.commit(storeMutations.UPDATE_CUSTOMER_DETAILS, {
firstName: registrationInfo.firstName,
lastName: registrationInfo.lastName,
});
}
}
} }
}, },
saveRegistrationAddressLookup( saveRegistrationAddressLookup(
context, context,
{ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo } { isSelectedGlassAvailableForVehicle, vehicleInfo, customerInfo }
) { ) {
//Reset dependent state when changing //Reset dependent state when changing
if (vehicleInfo.vin !== context.state.order.vehicle.vin) { if (vehicleInfo.vin !== context.state.order.vehicle.vin) {
@ -1681,9 +1798,14 @@ export const actions = {
} }
} }
if (registrationInfo) { context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_VEHICLE, vehicleInfo);
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo); // only update name information if there isn't a value in state already
if (!context.getters.order.customer.firstName && customerInfo) {
context.commit(storeMutations.UPDATE_CUSTOMER_DETAILS, {
firstName: customerInfo.firstName,
lastName: customerInfo.lastName,
});
} }
}, },
@ -1857,7 +1979,7 @@ export const actions = {
// Price order actions // Price order actions
async priceOrderItemsAndSaveServerData( async priceOrderItemsAndSaveServerData(
context, context,
{ availableLineItems, serviceZipCode, serviceZipCodeCtu } { payload: { availableLineItems, serviceZipCode, serviceZipCodeCtu }, pageNameToLog }
) { ) {
const zipCodeToUse = serviceZipCode const zipCodeToUse = serviceZipCode
? serviceZipCode ? serviceZipCode
@ -1897,6 +2019,8 @@ export const actions = {
const response = await globalMethods.callHttpClient({ const response = await globalMethods.callHttpClient({
method: endpoints.PriceOrderItems.method, method: endpoints.PriceOrderItems.method,
endpoint: `${endpoints.PriceOrderItems.url}?${queryString}`, endpoint: `${endpoints.PriceOrderItems.url}?${queryString}`,
logApiCall: true,
pageNameToLog: pageNameToLog,
}); });
context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData); context.commit(storeMutations.UPDATE_LINE_ITEMS_SERVER_DATA, response.data.serverData);
@ -2175,7 +2299,7 @@ function providersEqual(providerA, providerB) {
// check to see if we have an appointment date on the order object. // check to see if we have an appointment date on the order object.
// if so, make sure it's not in the past. if in the past, clear schedule info in store. // if so, make sure it's not in the past. if in the past, clear schedule info in store.
// if date not in past, then call schedule service to verify appointment is still available. // if date not in past, then call schedule service to verify appointment is still available.
async function resetScheduleIfUnavailable(context, order) { async function resetScheduleIfUnavailable(context, order, pageNameToLog) {
if (!order.schedule?.date) { if (!order.schedule?.date) {
return; return;
} }
@ -2214,8 +2338,11 @@ async function resetScheduleIfUnavailable(context, order) {
newTimeSlotsResponse = await context.dispatch( newTimeSlotsResponse = await context.dispatch(
storeActions.GET_MOBILE_TIME_SLOTS, storeActions.GET_MOBILE_TIME_SLOTS,
{ {
startDate: order.schedule.date, payload: {
endDate: endDate, startDate: order.schedule.date,
endDate: endDate,
},
pageNameToLog: pageNameToLog,
}, },
false false
); );
@ -2257,10 +2384,13 @@ async function resetScheduleIfUnavailable(context, order) {
newTimeSlotsResponse = await context.dispatch( newTimeSlotsResponse = await context.dispatch(
storeActions.GET_SHOP_TIME_SLOTS, storeActions.GET_SHOP_TIME_SLOTS,
{ {
startDate: order.schedule.date, payload: {
endDate: endDate, startDate: order.schedule.date,
shopAppointmentType: order.serviceLocation.appointmentType, endDate: endDate,
providerNumber: order.serviceLocation.provider.providerNumber, shopAppointmentType: order.serviceLocation.appointmentType,
providerNumber: order.serviceLocation.provider.providerNumber,
},
pageNameToLog: pageNameToLog,
}, },
false false
); );

View file

@ -370,7 +370,7 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.getVehicleYears(context); const response = await actions.getVehicleYears(context, { pageNameToLog: "test" });
expect(response.data).toEqual([2023, 2022, 2021]); expect(response.data).toEqual([2023, 2022, 2021]);
}); });
@ -385,13 +385,15 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.lookupVehicleByYmms( const response = await actions.lookupVehicleByYmms(context, {
context, payload: {
"2019", year: "2019",
"Acura", make: "Acura",
"ILX", model: "ILX",
"4 DOOR SEDAN" style: "4 DOOR SEDAN",
); },
pageNameToLog: "test",
});
expect(response.data).toEqual({ carId: "C00000001" }); expect(response.data).toEqual({ carId: "C00000001" });
}); });
@ -406,7 +408,12 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.lookupVehicleByVin(context, "12345678901234567"); const response = await actions.lookupVehicleByVin(context, {
payload: {
vin: "12345678901234567",
},
pageNameToLog: "test",
});
expect(response.data).toEqual({ carId: "C0000001" }); expect(response.data).toEqual({ carId: "C0000001" });
}); });
@ -421,7 +428,10 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.lookupVinByPlate(context, "12345678901234567"); const response = await actions.lookupVinByPlate(context, {
payload: { licensePlate: "12345678901234567", licenseState: "OH" },
pageNameToLog: "test",
});
expect(response.data).toEqual({ carId: "C00000001" }); expect(response.data).toEqual({ carId: "C00000001" });
}); });
@ -438,7 +448,10 @@ describe("Actions", () => {
}); });
// Act // Act
const response = await actions.lookupVinByImage(context, image); const response = await actions.lookupVinByImage(context, {
payload: image,
pageNameToLog: "test",
});
// Assert // Assert
expect(response.data).toEqual(["1C6JJTAG3NL134044"]); expect(response.data).toEqual(["1C6JJTAG3NL134044"]);
@ -458,7 +471,9 @@ describe("Actions", () => {
// Act // Act
// Assert // Assert
await expect(actions.lookupVinByImage(context, image)).rejects.toEqual("An error occurred"); await expect(
actions.lookupVinByImage(context, { payload: image, pageNameToLog: "test" })
).rejects.toEqual("An error occurred");
}); });
it("getVehicleMakes action, should return makes list", async () => { it("getVehicleMakes action, should return makes list", async () => {
@ -471,7 +486,10 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.getVehicleMakes(context, "2019"); const response = await actions.getVehicleMakes(context, {
payload: { year: "2019" },
pageNameToLog: "test",
});
expect(response.data).toEqual(["Acura", "Honda"]); expect(response.data).toEqual(["Acura", "Honda"]);
}); });
@ -486,7 +504,10 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.getVehicleModels(context, "2019", "Acura"); const response = await actions.getVehicleModels(context, {
payload: { year: "2019", make: "Acura" },
pageNameToLog: "test",
});
expect(response.data).toEqual(["ILX", "RDX"]); expect(response.data).toEqual(["ILX", "RDX"]);
}); });
@ -501,7 +522,10 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.getVehicleStyles(context, "2019", "Acura", "ILX"); const response = await actions.getVehicleStyles(context, {
payload: { year: "2019", make: "Acura", model: "ILX" },
pageNameToLog: "test",
});
expect(response.data).toEqual({ style: "4 DOOR SEDAN" }); expect(response.data).toEqual({ style: "4 DOOR SEDAN" });
}); });
@ -516,7 +540,10 @@ describe("Actions", () => {
}); });
// Assert // Assert
const response = await actions.getVehicle(context, "C00000000"); const response = await actions.getVehicle(context, {
payload: { year: "2019", make: "Acura", model: "IDX", style: "4-door sedan" },
pageNameToLog: "test",
});
expect(response.data).toEqual({ carId: "C00000000", category: "CAR" }); expect(response.data).toEqual({ carId: "C00000000", category: "CAR" });
}); });
@ -530,7 +557,10 @@ describe("Actions", () => {
return Promise.resolve({ data: ["Windshield", "DriversFrontDoor"] }); return Promise.resolve({ data: ["Windshield", "DriversFrontDoor"] });
}); });
const response = await actions.getDamageOptions(context, "C00000000"); const response = await actions.getDamageOptions(context, {
payload: { carId: "C00000000" },
pageNameToLog: "test",
});
// Assert // Assert
expect(response.data).toEqual(["Windshield", "DriversFrontDoor"]); expect(response.data).toEqual(["Windshield", "DriversFrontDoor"]);
@ -552,7 +582,10 @@ describe("Actions", () => {
}); });
}); });
const response = await actions.validateZip(context, "43212"); const response = await actions.validateZip(context, {
payload: { zip: "43212" },
pageNameToLog: "test",
});
// Assert // Assert
expect(response.data).toEqual({ expect(response.data).toEqual({
@ -730,7 +763,7 @@ describe("Actions", () => {
}); });
// Act // Act
const response = await actions.saveSession(context); const response = await actions.saveSession(context, { pageNameToLog: "test" });
// Assert // Assert
expect(response.data).toEqual({ referralNumber: 123 }); expect(response.data).toEqual({ referralNumber: 123 });
@ -750,7 +783,10 @@ describe("Actions", () => {
// Act // Act
const response = await actions.loadSession(context, { const response = await actions.loadSession(context, {
savedSessionId: "", payload: {
savedSessionId: "",
},
pageNameToLog: "test",
}); });
// Assert // Assert
@ -776,7 +812,10 @@ describe("Actions", () => {
// Act // Act
const response = await actions.loadSession(context, { const response = await actions.loadSession(context, {
savedSessionId: "", payload: {
savedSessionId: "",
},
pageNameToLog: "test",
}); });
// Assert // Assert
@ -801,7 +840,10 @@ describe("Actions", () => {
// Act // Act
const response = await actions.loadSession(context, { const response = await actions.loadSession(context, {
savedSessionId: "", payload: {
savedSessionId: "",
},
pageNameToLog: "test",
}); });
// Assert // Assert
@ -1492,9 +1534,18 @@ describe("Actions", () => {
licensePlate: "ABC123", licensePlate: "ABC123",
}, },
}, },
customer: {
firstName: "test",
lastName: "test",
},
}, },
}; };
context.getters = {
...getters,
order: getters.order(context),
};
const commit = jest.fn(); const commit = jest.fn();
const dispatch = jest.fn(); const dispatch = jest.fn();
@ -1538,9 +1589,18 @@ describe("Actions", () => {
address: "123 Main St", address: "123 Main St",
}, },
}, },
customer: {
firstName: "test",
lastName: "test",
},
}, },
}; };
context.getters = {
...getters,
order: getters.order(context),
};
const commit = jest.fn(); const commit = jest.fn();
const dispatch = jest.fn(); const dispatch = jest.fn();
@ -1552,7 +1612,7 @@ describe("Actions", () => {
isCarIdDifferent: true, isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false, isSelectedGlassAvailableForVehicle: false,
vehicleInfo: { carId: "C010101", vin: "XXXXX" }, vehicleInfo: { carId: "C010101", vin: "XXXXX" },
registrationInfo: { firstName: "abc", lastName: "123" }, customerInfo: { firstName: "abc", lastName: "123" },
serviceLocationInfo: { state: "CO" }, serviceLocationInfo: { state: "CO" },
customerEmail: "test@safelite.com", customerEmail: "test@safelite.com",
}; };
@ -1570,7 +1630,6 @@ describe("Actions", () => {
); );
expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo); expect(commit).toBeCalledWith(storeMutations.UPDATE_VEHICLE, payload.vehicleInfo);
expect(commit).toBeCalledWith(storeMutations.UPDATE_REGISTRATION, payload.registrationInfo);
}); });
it("saveVehicle, should save vehicle info", () => { it("saveVehicle, should save vehicle info", () => {
@ -1701,7 +1760,10 @@ describe("Actions", () => {
// Act // Act
await actions.runExperimentsForTrigger(context, { await actions.runExperimentsForTrigger(context, {
triggerEvent: experimentTriggers.SITE_ENTRY, payload: {
triggerEvent: experimentTriggers.SITE_ENTRY,
},
pageNameToLog: "test",
}); });
// Assert // Assert
@ -1735,7 +1797,10 @@ describe("Actions", () => {
// Act // Act
await actions.runExperimentsForTrigger(context, { await actions.runExperimentsForTrigger(context, {
triggerEvent: "NotSiteEntry", payload: {
triggerEvent: "NotSiteEntry",
},
pageNameToLog: "test",
}); });
// Assert // Assert

View file

@ -5,18 +5,16 @@ body {
background-color: #fff; background-color: #fff;
color: #4d5151; color: #4d5151;
.container-fluid { .container-fluid {
max-width: 576px; //Remove once desktop app is complete max-width: 1400px;
&.container-shadow { padding: 0 1.5rem;
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.15); //Use instead of Bootstrap's helper .prevent-squish {
overflow-x: unset;
} }
&.make-tall { &.page-container-grouped-styles {
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.prevent-squish {
overflow-x: unset;
}
} }
.pointer { .pointer {
cursor: pointer; cursor: pointer;
@ -43,18 +41,6 @@ body {
height: 1px; height: 1px;
overflow: hidden; overflow: hidden;
} }
.page-container-grouped-styles {
@extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5;
}
//Footer modal backdrop adjustments for positioning
.modal-backdrop {
left: 50%;
transform: translateX(-50%);
max-width: 576px;
height: calc(100% - 72px);
}
} }
//Disable scroll of main container when modal is open //Disable scroll of main container when modal is open
.modal-open { .modal-open {

View file

@ -170,15 +170,6 @@ $spacers: (
//Enable negative spacing (does NOT work on padding) //Enable negative spacing (does NOT work on padding)
$enable-negative-margins: true; $enable-negative-margins: true;
//Grid breakpoints
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 838px,
lg: 1074px,
xl: 1416px,
);
//Shadow //Shadow
$box-shadow: 0 0.5rem 1rem rgba($black, 0.15); $box-shadow: 0 0.5rem 1rem rgba($black, 0.15);
$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075); $box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075);

View file

@ -82,8 +82,8 @@ export default {
img { img {
// svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples. // svg's should be constructed on the same canvas size/viewbox to ensure they occupy the same space in the DOM. This will allow easy/proper alignment of elements. See exisitng svg's for examples.
height: auto; height: 2rem;
width: 6.5rem; width: auto;
margin-bottom: 2.2rem; margin-bottom: 2.2rem;
max-width: 100%; max-width: 100%;
} }
@ -96,7 +96,7 @@ export default {
outline: none; outline: none;
display: flex; display: flex;
position: relative; position: relative;
justify-content: space-between; justify-content: flex-start;
p { p {
color: $gray-600; color: $gray-600;