Merge branch 'develop' into nation/CASH-2844
This commit is contained in:
commit
91bcc37bf9
39 changed files with 480 additions and 186 deletions
13
src/App.vue
13
src/App.vue
|
|
@ -87,9 +87,22 @@ window.onerror = (msg, url, line, col, error) => {
|
||||||
return suppressErrorAlert;
|
return suppressErrorAlert;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isThirdPartyUnhandledRejection(reason) {
|
||||||
|
const stack = reason instanceof Error ? reason.stack : "";
|
||||||
|
if (typeof stack !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.includes("chrome-extension://") || stack.includes("quantummetric.com");
|
||||||
|
}
|
||||||
|
|
||||||
// Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async.
|
// Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async.
|
||||||
window.addEventListener("unhandledrejection", (event) => {
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
const reason = event.reason;
|
const reason = event.reason;
|
||||||
|
if (isThirdPartyUnhandledRejection(reason)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = reason instanceof Error ? reason.message : String(reason);
|
const message = reason instanceof Error ? reason.message : String(reason);
|
||||||
global.$logger.logError(`unhandledrejection: ${message}`, {
|
global.$logger.logError(`unhandledrejection: ${message}`, {
|
||||||
stack: reason instanceof Error ? reason.stack : undefined,
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||||||
|
|
|
||||||
|
|
@ -56,8 +56,8 @@ const endpoints = {
|
||||||
url: "/parts/api/v1/parts/parts-or-questions",
|
url: "/parts/api/v1/parts/parts-or-questions",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
GetPartsOrQuestionsV3: {
|
GetPartsOrQuestionsV2: {
|
||||||
url: "/parts/api/v3/parts/parts-or-questions",
|
url: "/parts/api/v2/parts/parts-or-questions",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
},
|
},
|
||||||
GetParts: {
|
GetParts: {
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,6 @@ const storeActions = {
|
||||||
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
|
UPDATE_HAS_TRIGGERED_ERROR: "updateHasTriggeredError",
|
||||||
GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey",
|
GET_VALID_IDEMPOTENCY_KEY: "getValidIdempotencyKey",
|
||||||
CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry",
|
CORRECT_IDEMPOTENCY_KEY_EXPIRY: "correctIdempotencyKeyExpiry",
|
||||||
|
|
||||||
SAVE_BAILOUT_CODE: "saveBailoutCode",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { storeActions };
|
export { storeActions };
|
||||||
|
|
|
||||||
|
|
@ -118,9 +118,6 @@ const storeMutations = {
|
||||||
UPDATE_EXPERIMENTS: "updateExperiments",
|
UPDATE_EXPERIMENTS: "updateExperiments",
|
||||||
UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry",
|
UPDATE_TRIGGERED_SITE_ENTRY: "updateTriggeredSiteEntry",
|
||||||
|
|
||||||
// BAILOUT MUTATIONS
|
|
||||||
UPDATE_BAILOUT_CODE: "updateBailoutCode",
|
|
||||||
|
|
||||||
// EXTERNAL_PARAMETER MUTATIONS
|
// EXTERNAL_PARAMETER MUTATIONS
|
||||||
UPDATE_IS_EXTERNAL_PARAMETER: "updateIsExternalParameter",
|
UPDATE_IS_EXTERNAL_PARAMETER: "updateIsExternalParameter",
|
||||||
UPDATE_EXTERNAL_PARAMETER_YEAR: "updateExternalParameterYear",
|
UPDATE_EXTERNAL_PARAMETER_YEAR: "updateExternalParameterYear",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,22 @@ const mockMixin = {
|
||||||
const maska = jest.fn();
|
const maska = jest.fn();
|
||||||
|
|
||||||
describe("textboxQuestion.vue", () => {
|
describe("textboxQuestion.vue", () => {
|
||||||
|
it("Should coerce non-string modelValue to empty string for v-model.trim", async () => {
|
||||||
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
maska: maska,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
modelValue: null,
|
||||||
|
},
|
||||||
|
mixins: [mockMixin],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.vm.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
it("Should render a text input", async () => {
|
it("Should render a text input", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const wrapper = shallowMount(textboxQuestion, {
|
const wrapper = shallowMount(textboxQuestion, {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<input
|
<input
|
||||||
class="form-control"
|
class="form-control"
|
||||||
v-model.trim="value"
|
v-model.trim="value"
|
||||||
v-maska="mask"
|
v-maska="effectiveMask"
|
||||||
:type="type"
|
:type="type"
|
||||||
:ref="inputId"
|
:ref="inputId"
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
|
|
@ -90,6 +90,19 @@ import loader from "@/ux-components/loader/loader.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
function coerceTextboxValue(modelValue) {
|
||||||
|
if (modelValue == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof modelValue === "number") {
|
||||||
|
return String(modelValue);
|
||||||
|
}
|
||||||
|
if (typeof modelValue === "string") {
|
||||||
|
return modelValue;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "textbox-question",
|
name: "textbox-question",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -149,18 +162,8 @@ export default {
|
||||||
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
|
const inputId = !props.customInputId ? `input-${uuid}` : props.customInputId;
|
||||||
|
|
||||||
const propsClone = Object.assign({}, props);
|
const propsClone = Object.assign({}, props);
|
||||||
const modelValue = propsClone.modelValue;
|
const modelValue = coerceTextboxValue(propsClone.modelValue);
|
||||||
let initialValue;
|
const initialValue = modelValue;
|
||||||
let isImageProcessing = ref(false);
|
|
||||||
|
|
||||||
switch (typeof modelValue) {
|
|
||||||
case "number":
|
|
||||||
initialValue = modelValue;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
initialValue = modelValue && modelValue.length > 0 ? modelValue : "";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldOptions = {
|
const fieldOptions = {
|
||||||
type: "text",
|
type: "text",
|
||||||
|
|
@ -168,6 +171,8 @@ export default {
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let isImageProcessing = ref(false);
|
||||||
|
|
||||||
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
|
const { errorMessage, handleBlur, handleChange, meta, validate, errors } = useField(
|
||||||
inputId,
|
inputId,
|
||||||
props.validationRules,
|
props.validationRules,
|
||||||
|
|
@ -228,18 +233,25 @@ export default {
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return this.modelValue;
|
return coerceTextboxValue(this.modelValue);
|
||||||
},
|
},
|
||||||
set: function (newValue) {
|
set: function (newValue) {
|
||||||
this.$emit("update:modelValue", newValue);
|
this.$emit("update:modelValue", newValue);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
effectiveMask() {
|
||||||
|
return this.mask ?? "";
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
|
this.$emit("textboxQuestionEvent.inputIdAssigned", this.inputId);
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
async value(newValue) {
|
async value(newValue) {
|
||||||
|
if (typeof this.handleChange !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||||
if (result.valid) {
|
if (result.valid) {
|
||||||
this.handleChange(newValue); // trigger full validation on this field only
|
this.handleChange(newValue); // trigger full validation on this field only
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
class="my-4 caption modal-sub-body"
|
class="my-4 caption modal-sub-body"
|
||||||
v-if="ModalSubBodyText"
|
v-if="ModalSubBodyText"
|
||||||
v-html="ModalSubBodyText"></p>
|
v-html="ModalSubBodyText"></p>
|
||||||
|
<p class="my-4 caption" v-if="ModalFooter2Text" v-html="ModalFooter2Text"></p>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
@ -58,6 +59,9 @@ export default {
|
||||||
ModalCloseButtonText() {
|
ModalCloseButtonText() {
|
||||||
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
return this.getCmsContent(this.cmsWidgetName, "FooterText");
|
||||||
},
|
},
|
||||||
|
ModalFooter2Text() {
|
||||||
|
return this.getCmsContent(this.cmsWidgetName, "FooterText2");
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openModal() {
|
openModal() {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,9 @@ export async function skipVinLookup() {
|
||||||
if (store.getters.damage.isRepair) {
|
if (store.getters.damage.isRepair) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (store.getters.vehicle.vinRequired) {
|
||||||
|
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;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { damageLocationsSelected as glassLocations } from "@/constants/damage-lo
|
||||||
|
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
import { skipVinLookup } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
|
|
||||||
const getPageToRouteExistingOrderTo = navigationHelper.getPageToRouteExistingOrderTo;
|
const getPageToRouteExistingOrderTo = navigationHelper.getPageToRouteExistingOrderTo;
|
||||||
const navigateToHeritageFunnel = navigationHelper.navigateToHeritageFunnel;
|
const navigateToHeritageFunnel = navigationHelper.navigateToHeritageFunnel;
|
||||||
|
|
@ -147,6 +148,20 @@ describe("navigateToHeritageFunnel", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("skipVinLookup", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
store.commit(storeMutations.UPDATE_VIN_REQUIRED, false);
|
||||||
|
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false when vehicle vin is required", async () => {
|
||||||
|
store.commit(storeMutations.UPDATE_VIN_REQUIRED, true);
|
||||||
|
store.commit(storeMutations.UPDATE_IS_REPAIR, false);
|
||||||
|
|
||||||
|
await expect(skipVinLookup()).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `arePagePrerequisitesValidObject` is an object where the keys are fmgPageValue names and the values are booleans that indicate
|
* `arePagePrerequisitesValidObject` is an object where the keys are fmgPageValue names and the values are booleans that indicate
|
||||||
* whether arePagePrerequisitesValid is true or false
|
* whether arePagePrerequisitesValid is true or false
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getBailoutCodeFromStore() {
|
getBailoutCodeFromStore() {
|
||||||
return store.getters.applicationUser.bailoutCode;
|
return store.getters.bailoutCode;
|
||||||
},
|
},
|
||||||
getFirstNameFromStore() {
|
getFirstNameFromStore() {
|
||||||
return store.getters.order.customer.firstName;
|
return store.getters.order.customer.firstName;
|
||||||
|
|
@ -233,7 +233,6 @@ export default {
|
||||||
this.navigationScenarios.CLICKED_FORWARD,
|
this.navigationScenarios.CLICKED_FORWARD,
|
||||||
this.pageName,
|
this.pageName,
|
||||||
{
|
{
|
||||||
bailoutCode: this.bailoutCode,
|
|
||||||
submit: true,
|
submit: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -244,7 +243,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
arePagePrerequisitesValid() {
|
arePagePrerequisitesValid() {
|
||||||
return this.getBailoutCodeFromStore() !== null;
|
return this.getBailoutCodeFromStore() != null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
class="keys-message" />
|
class="keys-message" />
|
||||||
<mobileAddressQuestions
|
<mobileAddressQuestions
|
||||||
ref="addressQuestions"
|
ref="addressQuestions"
|
||||||
v-model="this.addressQuestions"
|
v-model="addressQuestions"
|
||||||
captureApartmentNumberOrBusinessName="true"
|
captureApartmentNumberOrBusinessName="true"
|
||||||
preserveCityAndStateOnReset="true"
|
preserveCityAndStateOnReset="true"
|
||||||
labelBold="true"
|
labelBold="true"
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
isZipCodeDisabled="true" />
|
isZipCodeDisabled="true" />
|
||||||
<vehicleProtectedQuestion
|
<vehicleProtectedQuestion
|
||||||
ref="vehicleProtectedQuestion"
|
ref="vehicleProtectedQuestion"
|
||||||
v-model="this.isVehicleProtected"
|
v-model="isVehicleProtected"
|
||||||
cmsWidgetName="VehicleProtectedQuestionWidget"
|
cmsWidgetName="VehicleProtectedQuestionWidget"
|
||||||
labelBold="true" />
|
labelBold="true" />
|
||||||
<textBlock
|
<textBlock
|
||||||
|
|
@ -84,11 +84,11 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addressQuestions: {
|
addressQuestions: {
|
||||||
streetAddress: this.getServiceAddressFromStore(),
|
streetAddress: String(this.getServiceAddressFromStore() ?? ""),
|
||||||
apartmentNumberOrBusinessName: this.getServiceAddress2FromStore(),
|
apartmentNumberOrBusinessName: String(this.getServiceAddress2FromStore() ?? ""),
|
||||||
city: this.getServiceCityFromStore(),
|
city: String(this.getServiceCityFromStore() ?? ""),
|
||||||
state: this.getServiceStateFromStore(),
|
state: String(this.getServiceStateFromStore() ?? ""),
|
||||||
zipCode: this.getServiceZipCodeFromStore(),
|
zipCode: String(this.getServiceZipCodeFromStore() ?? ""),
|
||||||
},
|
},
|
||||||
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
isVehicleProtected: this.getIsVehicleProtectedFromStore(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,42 @@
|
||||||
class="date-picker__nav-icon date-picker__nav-icon--flipped"
|
class="date-picker__nav-icon date-picker__nav-icon--flipped"
|
||||||
alt="" />
|
alt="" />
|
||||||
</button>
|
</button>
|
||||||
<div class="date-picker__track d-flex flex-grow-1">
|
<div
|
||||||
<button
|
class="date-picker__track d-flex flex-grow-1"
|
||||||
v-for="date in visibleDates"
|
:style="isLoadingDates ? { '--card-count': windowSize } : null">
|
||||||
:key="date.value"
|
<template v-if="showLoadingPlaceholders">
|
||||||
class="date-picker__day-card d-flex flex-column align-items-center justify-content-center"
|
<div
|
||||||
:class="{
|
v-for="index in windowSize"
|
||||||
'date-picker__day-card--selected': isSelected(date.value),
|
:key="index"
|
||||||
'date-picker__day-card--disabled': !date.isAvailable,
|
class="date-picker__day-card date-picker__day-card--loading d-flex flex-column align-items-center justify-content-center"
|
||||||
}"
|
:style="{ '--card-index': index - 1 }"
|
||||||
:disabled="!date.isAvailable"
|
aria-hidden="true">
|
||||||
:aria-pressed="isSelected(date.value)"
|
<!-- Placeholder text is intentionally non-empty so the spans occupy the
|
||||||
:aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`"
|
same height as real content. The --loading CSS hides them via
|
||||||
@click="selectDate(date)">
|
visibility:hidden, so they are never visible to users. -->
|
||||||
<span class="date-picker__day-abbr">{{ date.dayAbbr }}</span>
|
<span class="date-picker__day-abbr">MON</span>
|
||||||
<span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span>
|
<span class="date-picker__day-date">Jan 00</span>
|
||||||
</button>
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-for="(date, index) in visibleDates"
|
||||||
|
:key="date.value"
|
||||||
|
class="date-picker__day-card d-flex flex-column align-items-center justify-content-center"
|
||||||
|
:class="{
|
||||||
|
'date-picker__day-card--selected': isSelected(date.value),
|
||||||
|
'date-picker__day-card--disabled': !date.isAvailable,
|
||||||
|
'date-picker__day-card--loading': isLoadingDates,
|
||||||
|
}"
|
||||||
|
:style="isLoadingDates ? { '--card-index': index } : null"
|
||||||
|
:disabled="!date.isAvailable || isLoadingDates"
|
||||||
|
:aria-pressed="isSelected(date.value)"
|
||||||
|
:aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`"
|
||||||
|
@click="selectDate(date)">
|
||||||
|
<span class="date-picker__day-abbr">{{ date.dayAbbr }}</span>
|
||||||
|
<span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="date-picker__nav-btn"
|
class="date-picker__nav-btn"
|
||||||
|
|
@ -110,6 +130,9 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showLoadingPlaceholders() {
|
||||||
|
return this.isLoadingDates && !this.allDates.length;
|
||||||
|
},
|
||||||
allDates() {
|
allDates() {
|
||||||
if (this.availableDates === null) return [];
|
if (this.availableDates === null) return [];
|
||||||
|
|
||||||
|
|
@ -308,6 +331,23 @@ export default {
|
||||||
color: $gray-300;
|
color: $gray-300;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--loading,
|
||||||
|
&--loading.date-picker__day-card--selected,
|
||||||
|
&--loading.date-picker__day-card--disabled {
|
||||||
|
border-color: $gray-200;
|
||||||
|
cursor: default;
|
||||||
|
background-color: $gray-200;
|
||||||
|
background-image: linear-gradient(90deg, $gray-200 25%, $gray-100 50%, $gray-200 75%);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: calc(var(--card-count) * 200%) 100%;
|
||||||
|
animation: date-picker-shimmer 1.5s ease-in-out infinite;
|
||||||
|
|
||||||
|
.date-picker__day-abbr,
|
||||||
|
.date-picker__day-date {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__day-abbr,
|
&__day-abbr,
|
||||||
|
|
@ -336,4 +376,13 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes date-picker-shimmer {
|
||||||
|
0% {
|
||||||
|
background-position-x: calc(var(--card-index) / (var(--card-count) - 1) * 100% + 100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position-x: calc(var(--card-index) / (var(--card-count) - 1) * 100% - 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@
|
||||||
import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
import { PREMIUM_TIME_SLOT_ID_FLAG, AppointmentTypeStrings } from "@/constants/schedule-constants";
|
||||||
import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper";
|
import { militaryToTwelveHourTime } from "@/layouts/schedule/helpers/schedule-helper";
|
||||||
import schedulingCardLoader from "@/layouts/scheduling/scheduling-card-loader/scheduling-card-loader.vue";
|
import schedulingCardLoader from "@/layouts/scheduling/scheduling-card-loader/scheduling-card-loader.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "mobile-scheduling-card",
|
name: "mobile-scheduling-card",
|
||||||
emits: ["update:modelValue", "zip-code-clicked"],
|
emits: ["update:modelValue", "zip-code-clicked"],
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,9 @@ function setupMocks() {
|
||||||
|
|
||||||
describe("scheduling.vue", () => {
|
describe("scheduling.vue", () => {
|
||||||
describe("intercept overlay", () => {
|
describe("intercept overlay", () => {
|
||||||
test("does not render interceptOverlay when isLoadingDates is false", () => {
|
test("does not render interceptOverlay when isLoadingDates is false", async () => {
|
||||||
const { wrapper } = setupMocks();
|
const { wrapper } = setupMocks();
|
||||||
|
await wrapper.setData({ isLoadingDates: false });
|
||||||
expect(wrapper.find("intercept-overlay-stub").exists()).toBe(false);
|
expect(wrapper.find("intercept-overlay-stub").exists()).toBe(false);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
|
||||||
<interceptOverlay v-if="isLoadingDates" />
|
<interceptOverlay v-if="isLoadingDates" />
|
||||||
<loadingModal notFullScreen ref="loadingModal" />
|
|
||||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
|
||||||
<div class="container page-container-grouped-styles">
|
<div class="container page-container-grouped-styles">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
@ -20,29 +19,35 @@
|
||||||
:availableDates="availableDates"
|
:availableDates="availableDates"
|
||||||
:isLoadingDates="isLoadingDates"
|
:isLoadingDates="isLoadingDates"
|
||||||
@requestMoreDates="handleRequestMoreDates" />
|
@requestMoreDates="handleRequestMoreDates" />
|
||||||
<mobileSchedulingCard
|
<Transition name="card-slide" mode="out-in">
|
||||||
v-if="showMobileSchedulingCard"
|
<div :key="selectedDate">
|
||||||
class="mt-4"
|
<mobileSchedulingCard
|
||||||
v-model="selectedScheduling"
|
v-if="showMobileSchedulingCard"
|
||||||
:providerNumber="mobileProviderAndTimeSlot.providerNumber"
|
class="mt-4"
|
||||||
:timeSlots="mobileTimeSlotsForSelectedDate"
|
v-model="selectedScheduling"
|
||||||
:premiumTimeSlotPrice="premiumTimeSlotPrice"
|
:providerNumber="mobileProviderAndTimeSlot.providerNumber"
|
||||||
:zipCode="serviceZipCode"
|
:timeSlots="mobileTimeSlotsForSelectedDate"
|
||||||
:showFreeFlag="showMobileFreeFlag"
|
:premiumTimeSlotPrice="premiumTimeSlotPrice"
|
||||||
:radioGroupName="schedulingRadioGroupName"
|
:zipCode="serviceZipCode"
|
||||||
:isLoading="isLoadingDates"
|
:showFreeFlag="showMobileFreeFlag"
|
||||||
@zip-code-clicked="onMobileZipCodeClicked" />
|
:radioGroupName="schedulingRadioGroupName"
|
||||||
<inshopSchedulingCard
|
:isLoading="isLoadingDates"
|
||||||
v-for="{ provider } in inShopProvidersAndTimeslots"
|
@zip-code-clicked="onMobileZipCodeClicked" />
|
||||||
v-show="selectedDate"
|
<inshopSchedulingCard
|
||||||
:key="provider.providerNumber"
|
v-for="{ provider } in inShopProvidersAndTimeslots"
|
||||||
class="mt-4"
|
v-show="showInshopSchedulingCards"
|
||||||
v-model="selectedScheduling"
|
:key="provider.providerNumber"
|
||||||
:provider="provider"
|
class="mt-4"
|
||||||
:timeSlots="getInshopTimeSlotsForSelectedDate(provider.providerNumber)"
|
v-model="selectedScheduling"
|
||||||
:radioGroupName="schedulingRadioGroupName"
|
:provider="provider"
|
||||||
:isLoading="isLoadingDates"
|
:timeSlots="
|
||||||
@address-clicked="onInshopAddressClicked(provider)" />
|
getInshopTimeSlotsForSelectedDate(provider.providerNumber)
|
||||||
|
"
|
||||||
|
:radioGroupName="schedulingRadioGroupName"
|
||||||
|
:isLoading="isLoadingDates"
|
||||||
|
@address-clicked="onInshopAddressClicked(provider)" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
<navbar
|
<navbar
|
||||||
cmsWidgetName="FunnelFooterWidget"
|
cmsWidgetName="FunnelFooterWidget"
|
||||||
ref="navbar"
|
ref="navbar"
|
||||||
|
|
@ -62,7 +67,6 @@ import { Form } from "vee-validate";
|
||||||
import datePicker from "@/layouts/scheduling/date-picker/date-picker";
|
import datePicker from "@/layouts/scheduling/date-picker/date-picker";
|
||||||
import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card";
|
import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mobile-scheduling-card";
|
||||||
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
|
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
|
||||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
|
||||||
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
|
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
|
||||||
|
|
@ -138,17 +142,6 @@ export default {
|
||||||
async beforeRouteEnter(to, from, next) {
|
async beforeRouteEnter(to, from, next) {
|
||||||
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
||||||
|
|
||||||
const shopProviderData = await store.dispatch("getProviders", {
|
|
||||||
payload: { serviceZipCode },
|
|
||||||
pageNameToLog: to.name,
|
|
||||||
});
|
|
||||||
// Get the first 3 providers from the shopProviderData
|
|
||||||
const providers = shopProviderData?.data?.shopProviders?.slice(0, 3) ?? [];
|
|
||||||
const mobileProviderNumber = shopProviderData?.data?.mobileProviderNumber ?? null;
|
|
||||||
|
|
||||||
const startDate = toDateString(0);
|
|
||||||
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
|
|
||||||
|
|
||||||
const promiseResultMap = [
|
const promiseResultMap = [
|
||||||
{
|
{
|
||||||
resultKey: "cmsContent",
|
resultKey: "cmsContent",
|
||||||
|
|
@ -160,45 +153,64 @@ export default {
|
||||||
pageNameToLog: to.name,
|
pageNameToLog: to.name,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...providers.map((provider, i) => ({
|
{
|
||||||
resultKey: `inshopTimeSlots_${i}`,
|
resultKey: "providers",
|
||||||
promise: fetchInshopTimeSlots({
|
promise: store.dispatch("getProviders", {
|
||||||
startDate,
|
payload: { serviceZipCode },
|
||||||
endDate,
|
|
||||||
providerNumber: provider.providerNumber,
|
|
||||||
pageNameToLog: to.name,
|
pageNameToLog: to.name,
|
||||||
}),
|
}),
|
||||||
})),
|
},
|
||||||
// Get the mobile time slots if a mobile provider number is available
|
|
||||||
...(mobileProviderNumber
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
resultKey: "mobileTimeSlots",
|
|
||||||
promise: fetchMobileTimeSlots({
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
zipCode: serviceZipCode,
|
|
||||||
pageNameToLog: to.name,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
const resultMap = await settleAllPromises(promiseResultMap);
|
const resultMap = await settleAllPromises(promiseResultMap);
|
||||||
next((vm) => {
|
// Get the first 3 providers from the shopProviderData
|
||||||
|
const providers = resultMap.providers?.shopProviders?.slice(0, 3) ?? [];
|
||||||
|
const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null;
|
||||||
|
next(async (vm) => {
|
||||||
vm.setCmsContent(resultMap.cmsContent);
|
vm.setCmsContent(resultMap.cmsContent);
|
||||||
vm.inShopProvidersAndTimeslots = providers.map((provider, i) => ({
|
vm.inShopProvidersAndTimeslots = providers.map((provider) => ({
|
||||||
provider,
|
provider,
|
||||||
timeSlots: resultMap[`inshopTimeSlots_${i}`] ?? null,
|
timeSlots: null,
|
||||||
}));
|
}));
|
||||||
vm.mobileProviderAndTimeSlot = mobileProviderNumber
|
vm.mobileProviderAndTimeSlot = mobileProviderNumber
|
||||||
? {
|
? { providerNumber: mobileProviderNumber, timeSlots: null }
|
||||||
providerNumber: mobileProviderNumber,
|
|
||||||
timeSlots: resultMap.mobileTimeSlots ?? null,
|
|
||||||
}
|
|
||||||
: null;
|
: null;
|
||||||
|
const startDate = toDateString(0);
|
||||||
|
const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
|
||||||
|
const timeSlotsPromiseResultMap = [
|
||||||
|
...providers.map((provider, i) => ({
|
||||||
|
resultKey: `inshopTimeSlots_${i}`,
|
||||||
|
promise: fetchInshopTimeSlots({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
providerNumber: provider.providerNumber,
|
||||||
|
pageNameToLog: to.name,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
// Get the mobile time slots if a mobile provider number is available
|
||||||
|
...(mobileProviderNumber
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
resultKey: "mobileTimeSlots",
|
||||||
|
promise: fetchMobileTimeSlots({
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
zipCode: serviceZipCode,
|
||||||
|
pageNameToLog: to.name,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
const timeSlotsResultMap = await settleAllPromises(timeSlotsPromiseResultMap);
|
||||||
|
vm.inShopProvidersAndTimeslots.forEach((entry, i) => {
|
||||||
|
entry.timeSlots = timeSlotsResultMap[`inshopTimeSlots_${i}`] ?? null;
|
||||||
|
});
|
||||||
|
if (vm.mobileProviderAndTimeSlot) {
|
||||||
|
vm.mobileProviderAndTimeSlot.timeSlots = timeSlotsResultMap.mobileTimeSlots ?? null;
|
||||||
|
}
|
||||||
vm.datesLoaded = true;
|
vm.datesLoaded = true;
|
||||||
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
|
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
|
||||||
|
vm.isLoadingDates = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -217,7 +229,12 @@ export default {
|
||||||
return store.getters.order.serviceLocation.zipCode;
|
return store.getters.order.serviceLocation.zipCode;
|
||||||
},
|
},
|
||||||
showMobileSchedulingCard() {
|
showMobileSchedulingCard() {
|
||||||
return Boolean(this.mobileProviderAndTimeSlot && this.selectedDate);
|
return Boolean(
|
||||||
|
this.mobileProviderAndTimeSlot && (this.selectedDate || this.isLoadingDates)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showInshopSchedulingCards() {
|
||||||
|
return this.selectedDate || this.isLoadingDates;
|
||||||
},
|
},
|
||||||
mobileTimeSlotsForSelectedDate() {
|
mobileTimeSlotsForSelectedDate() {
|
||||||
if (!this.selectedDate) {
|
if (!this.selectedDate) {
|
||||||
|
|
@ -251,7 +268,7 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedDate: null,
|
selectedDate: null,
|
||||||
isLoadingDates: false,
|
isLoadingDates: true,
|
||||||
datesLoaded: false,
|
datesLoaded: false,
|
||||||
datePickerStartDate: toDateString(0),
|
datePickerStartDate: toDateString(0),
|
||||||
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
|
datePickerEndDate: toDateString(SCHEDULE_FETCH_DAYS - 1),
|
||||||
|
|
@ -378,7 +395,6 @@ export default {
|
||||||
datePicker,
|
datePicker,
|
||||||
mobileSchedulingCard,
|
mobileSchedulingCard,
|
||||||
inshopSchedulingCard,
|
inshopSchedulingCard,
|
||||||
loadingModal,
|
|
||||||
interceptOverlay,
|
interceptOverlay,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -391,4 +407,20 @@ h5 {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-slide-enter-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.3s ease-out,
|
||||||
|
transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
.card-slide-leave-active {
|
||||||
|
transition: opacity 0.32s ease-in;
|
||||||
|
}
|
||||||
|
.card-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(24px);
|
||||||
|
}
|
||||||
|
.card-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ export default {
|
||||||
name: "vehicle-protected-question",
|
name: "vehicle-protected-question",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
isVehicleProtected: Boolean,
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
labelBold: {
|
labelBold: {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ export default {
|
||||||
name: "service-zip-question",
|
name: "service-zip-question",
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
serviceZipCode: String,
|
type: String,
|
||||||
|
default: "",
|
||||||
},
|
},
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
isRequired: {
|
isRequired: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
cmsWidgetName="FunnelHeaderWidget"
|
cmsWidgetName="FunnelHeaderWidget"
|
||||||
ref="funnelHeader"
|
ref="funnelHeader"
|
||||||
:overrideImageSrc="clientLogoImageSrc" />
|
:overrideImageSrc="clientLogoImageSrc" />
|
||||||
<div class="page-gradient"></div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ jest.mock("@/store", () => ({
|
||||||
commit: jest.fn(),
|
commit: jest.fn(),
|
||||||
dispatch: jest.fn(),
|
dispatch: jest.fn(),
|
||||||
getters: {
|
getters: {
|
||||||
|
bailoutCode: null,
|
||||||
applicationUser: {
|
applicationUser: {
|
||||||
bailoutCode: null,
|
pageData: {},
|
||||||
},
|
},
|
||||||
externalParameterState: {
|
externalParameterState: {
|
||||||
isExternalParameter: false,
|
isExternalParameter: false,
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ export default {
|
||||||
},
|
},
|
||||||
getRequiredVinNotFound() {
|
getRequiredVinNotFound() {
|
||||||
// Prevents success alert from showing
|
// Prevents success alert from showing
|
||||||
var bailoutCode = this.$store.getters.applicationUser.bailoutCode;
|
var bailoutCode = this.$store.getters.bailoutCode;
|
||||||
var vinRequired = this.$store.getters.vehicle.vinRequired;
|
var vinRequired = this.$store.getters.vehicle.vinRequired;
|
||||||
if (vinRequired && this.vin && bailoutCode == bailoutCodes.PART_NOT_FOUND) {
|
if (vinRequired && this.vin && bailoutCode == bailoutCodes.PART_NOT_FOUND) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||||
import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper";
|
import { containsRecalParts, getRecalPartNumbers } from "@/helpers/recal-helper";
|
||||||
import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js";
|
import { getAmountDue, getSubTotal, getSalesTax } from "@/helpers/pricing-helper.js";
|
||||||
import { partTypeStrings } from "@/constants/part-type-strings";
|
import { partTypeStrings } from "@/constants/part-type-strings";
|
||||||
|
import { bailoutCodes } from "@/constants/bailout-codes";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -271,6 +272,9 @@ export default {
|
||||||
sessionData.totalPrice = getAmountDue(order?.lineItems, true);
|
sessionData.totalPrice = getAmountDue(order?.lineItems, true);
|
||||||
sessionData.userAgent = navigator.userAgent;
|
sessionData.userAgent = navigator.userAgent;
|
||||||
sessionData.cashPriceSubTotal = order?.cashPriceSubTotal;
|
sessionData.cashPriceSubTotal = order?.cashPriceSubTotal;
|
||||||
|
sessionData.bailoutCode = this.getBailoutCodeEnum(
|
||||||
|
applicationUser?.pageData?.bailout?.bailoutCode
|
||||||
|
);
|
||||||
|
|
||||||
await baseMixin.methods.dispatchStoreAction(
|
await baseMixin.methods.dispatchStoreAction(
|
||||||
storeActions.LOG_FMG_SESSION_DATA,
|
storeActions.LOG_FMG_SESSION_DATA,
|
||||||
|
|
@ -279,6 +283,17 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getBailoutCodeEnum(bailoutCode) {
|
||||||
|
if (bailoutCode) {
|
||||||
|
const bailoutCodeEnum = Object.keys(bailoutCodes).find(
|
||||||
|
(key) => bailoutCodes[key] === bailoutCode
|
||||||
|
);
|
||||||
|
return bailoutCodeEnum ? bailoutCode + "_" + bailoutCodeEnum : bailoutCode;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
pushOrderToDataLayer() {
|
pushOrderToDataLayer() {
|
||||||
// helper check for if an object is defined (but maybe falsey)
|
// helper check for if an object is defined (but maybe falsey)
|
||||||
const isDefined = (x) => x !== null && x !== undefined;
|
const isDefined = (x) => x !== null && x !== undefined;
|
||||||
|
|
@ -622,7 +637,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
discount += productPrice * -1;
|
discount += productPrice * -1;
|
||||||
subTotal += isQuotePageDiscount ? productPrice : 0;
|
subTotal += productPrice;
|
||||||
} else {
|
} else {
|
||||||
products.push({
|
products.push({
|
||||||
productType: productType,
|
productType: productType,
|
||||||
|
|
@ -722,6 +737,9 @@ export default {
|
||||||
if (quotePageDiscount) {
|
if (quotePageDiscount) {
|
||||||
combinedLineItems = combinedLineItems.concat(quotePageDiscount);
|
combinedLineItems = combinedLineItems.concat(quotePageDiscount);
|
||||||
}
|
}
|
||||||
|
if (lineItems.promos) {
|
||||||
|
combinedLineItems = combinedLineItems.concat(lineItems.promos);
|
||||||
|
}
|
||||||
|
|
||||||
//Remove child Parts if any
|
//Remove child Parts if any
|
||||||
combinedLineItems.forEach((lineItem) => {
|
combinedLineItems.forEach((lineItem) => {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
getUserIdValue,
|
getUserIdValue,
|
||||||
} from "@/helpers/heritage-integration/cookie-helper";
|
} from "@/helpers/heritage-integration/cookie-helper";
|
||||||
import baseMixin from "./base-mixin";
|
import baseMixin from "./base-mixin";
|
||||||
|
import { bailoutCodes } from "@/constants/bailout-codes";
|
||||||
|
|
||||||
const parts = {
|
const parts = {
|
||||||
windshield: {
|
windshield: {
|
||||||
|
|
@ -746,6 +747,71 @@ describe("analyticsMixin.js", () => {
|
||||||
//Assert
|
//Assert
|
||||||
expect(gaLabels).toEqual(GaLabels);
|
expect(gaLabels).toEqual(GaLabels);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getBailoutCodeEnum", () => {
|
||||||
|
test("returns null when bailout code is not provided", () => {
|
||||||
|
expect(analyticsMixin.methods.getBailoutCodeEnum(null)).toBeNull();
|
||||||
|
expect(analyticsMixin.methods.getBailoutCodeEnum(undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns formatted value when bailout code matches a known enum", () => {
|
||||||
|
const result = analyticsMixin.methods.getBailoutCodeEnum(bailoutCodes.PART_NOT_FOUND);
|
||||||
|
|
||||||
|
expect(result).toBe(`${bailoutCodes.PART_NOT_FOUND}_PART_NOT_FOUND`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns original value when bailout code does not match a known enum", () => {
|
||||||
|
expect(analyticsMixin.methods.getBailoutCodeEnum(999)).toBe(999);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pushFmgSessionData", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
baseMixin.methods.hasSubmittedOrder = jest.fn().mockReturnValue(false);
|
||||||
|
baseMixin.methods.getSubmittedOrder = jest.fn();
|
||||||
|
baseMixin.methods.hasSubmittedApplicationUser = jest.fn().mockReturnValue(false);
|
||||||
|
baseMixin.methods.getSubmittedApplicationUser = jest.fn();
|
||||||
|
store.getters.order = {
|
||||||
|
vehicle: { vinRequired: false },
|
||||||
|
payment: {
|
||||||
|
isInsurance: false,
|
||||||
|
isClaimAndCoverage: false,
|
||||||
|
isPia: false,
|
||||||
|
},
|
||||||
|
damage: { isRepair: false },
|
||||||
|
lineItems: {},
|
||||||
|
serviceLocation: {},
|
||||||
|
customer: {},
|
||||||
|
policy: {},
|
||||||
|
};
|
||||||
|
store.getters.applicationUser = {
|
||||||
|
experiments: [],
|
||||||
|
pageData: {
|
||||||
|
bailout: {
|
||||||
|
bailoutCode: bailoutCodes.PART_NOT_FOUND,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dispatches session data with formatted bailout code", async () => {
|
||||||
|
const mocks = setupMocksForJsFiles({
|
||||||
|
actionList: [{ actionName: storeActions.LOG_FMG_SESSION_DATA }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await analyticsMixin.methods.pushFmgSessionData();
|
||||||
|
|
||||||
|
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(
|
||||||
|
storeActions.LOG_FMG_SESSION_DATA,
|
||||||
|
expect.objectContaining({
|
||||||
|
currentPage: "mockedPageName",
|
||||||
|
bailoutCode: `${bailoutCodes.PART_NOT_FOUND}_PART_NOT_FOUND`,
|
||||||
|
}),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("initSession", () => {
|
describe("initSession", () => {
|
||||||
test("Generates random values for userId and deviceId if not present", async () => {
|
test("Generates random values for userId and deviceId if not present", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
||||||
|
import { routeData } from "@/router/constants/routes";
|
||||||
|
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
navigateToBailoutPage(vm, bailoutCode) {
|
navigateToBailoutPage(vm, bailoutCode) {
|
||||||
const self = vm ?? this;
|
const self = vm ?? this;
|
||||||
|
|
||||||
self.dispatchStoreAction(self.storeActions.SAVE_BAILOUT_CODE, bailoutCode).then(() => {
|
return savePageData(routeData.BAILOUT.name, { bailoutCode }).then(() => {
|
||||||
self.$router.navigateWithoutSaving(navigationScenarios.BAILOUT, self.pageName);
|
return self.$router.navigateWithoutSaving(
|
||||||
|
navigationScenarios.BAILOUT,
|
||||||
|
self.pageName
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,34 @@
|
||||||
import bailoutMixin from "@/mixins/bailout-mixin";
|
import bailoutMixin from "@/mixins/bailout-mixin";
|
||||||
import { storeActions } from "@/constants/store-actions.js";
|
|
||||||
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
||||||
import { bailoutCodes } from "@/constants/bailout-codes.js";
|
import { bailoutCodes } from "@/constants/bailout-codes.js";
|
||||||
|
import { routeData } from "@/router/constants/routes";
|
||||||
|
|
||||||
|
jest.mock("@/router/methods/helpers/save-page-data", () => ({
|
||||||
|
savePageData: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||||
|
|
||||||
describe("bailout-mixin.js", () => {
|
describe("bailout-mixin.js", () => {
|
||||||
test("navigateToBailoutPage: dispatches SAVE_BAILOUT_CODE action with bailout code", async () => {
|
beforeEach(() => {
|
||||||
// Arrange
|
savePageData.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigateToBailoutPage: saves bailout code to pageData", async () => {
|
||||||
const mockVm = createMockVm();
|
const mockVm = createMockVm();
|
||||||
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
||||||
|
|
||||||
// Act
|
|
||||||
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
||||||
|
|
||||||
// Assert
|
expect(savePageData).toHaveBeenCalledWith(routeData.BAILOUT.name, { bailoutCode });
|
||||||
expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith(
|
|
||||||
storeActions.SAVE_BAILOUT_CODE,
|
|
||||||
bailoutCode
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigateToBailoutPage: navigates to bailout page after saving bailout code", async () => {
|
test("navigateToBailoutPage: navigates to bailout page after saving bailout code", async () => {
|
||||||
// Arrange
|
|
||||||
const mockVm = createMockVm();
|
const mockVm = createMockVm();
|
||||||
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
||||||
|
|
||||||
// Act
|
|
||||||
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
|
expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
|
||||||
navigationScenarios.BAILOUT,
|
navigationScenarios.BAILOUT,
|
||||||
mockVm.pageName
|
mockVm.pageName
|
||||||
|
|
@ -35,55 +36,39 @@ describe("bailout-mixin.js", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigateToBailoutPage: uses current context (this) when vm is not provided", async () => {
|
test("navigateToBailoutPage: uses current context (this) when vm is not provided", async () => {
|
||||||
// Arrange
|
|
||||||
const mockRouter = {
|
const mockRouter = {
|
||||||
navigateWithoutSaving: jest.fn().mockResolvedValue(undefined),
|
navigateWithoutSaving: jest.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
const mockThis = {
|
const mockThis = {
|
||||||
dispatchStoreAction: jest.fn().mockResolvedValue(undefined),
|
|
||||||
$router: mockRouter,
|
$router: mockRouter,
|
||||||
storeActions: storeActions,
|
|
||||||
pageName: "test-page",
|
pageName: "test-page",
|
||||||
};
|
};
|
||||||
|
|
||||||
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
||||||
|
|
||||||
// Act
|
|
||||||
await bailoutMixin.methods.navigateToBailoutPage.call(mockThis, undefined, bailoutCode);
|
await bailoutMixin.methods.navigateToBailoutPage.call(mockThis, undefined, bailoutCode);
|
||||||
|
|
||||||
// Assert
|
expect(savePageData).toHaveBeenCalledWith(routeData.BAILOUT.name, { bailoutCode });
|
||||||
expect(mockThis.dispatchStoreAction).toHaveBeenCalledWith(
|
|
||||||
storeActions.SAVE_BAILOUT_CODE,
|
|
||||||
bailoutCode
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigateToBailoutPage: passes correct bailout code to store", async () => {
|
test("navigateToBailoutPage: passes correct bailout code to pageData", async () => {
|
||||||
// Arrange
|
|
||||||
const mockVm = createMockVm();
|
const mockVm = createMockVm();
|
||||||
const customBailoutCode = 999;
|
const customBailoutCode = 999;
|
||||||
|
|
||||||
// Act
|
|
||||||
await bailoutMixin.methods.navigateToBailoutPage(mockVm, customBailoutCode);
|
await bailoutMixin.methods.navigateToBailoutPage(mockVm, customBailoutCode);
|
||||||
|
|
||||||
// Assert
|
expect(savePageData).toHaveBeenCalledWith(routeData.BAILOUT.name, {
|
||||||
expect(mockVm.dispatchStoreAction).toHaveBeenCalledWith(
|
bailoutCode: customBailoutCode,
|
||||||
storeActions.SAVE_BAILOUT_CODE,
|
});
|
||||||
customBailoutCode
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigateToBailoutPage: calls navigateWithoutSaving with correct parameters", async () => {
|
test("navigateToBailoutPage: calls navigateWithoutSaving with correct parameters", async () => {
|
||||||
// Arrange
|
|
||||||
const mockVm = createMockVm();
|
const mockVm = createMockVm();
|
||||||
const mockPageName = "vehicle-damage";
|
const mockPageName = "vehicle-damage";
|
||||||
mockVm.pageName = mockPageName;
|
mockVm.pageName = mockPageName;
|
||||||
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
const bailoutCode = bailoutCodes.PART_NOT_FOUND;
|
||||||
|
|
||||||
// Act
|
|
||||||
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
await bailoutMixin.methods.navigateToBailoutPage(mockVm, bailoutCode);
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
|
expect(mockVm.$router.navigateWithoutSaving).toHaveBeenCalledWith(
|
||||||
navigationScenarios.BAILOUT,
|
navigationScenarios.BAILOUT,
|
||||||
mockPageName
|
mockPageName
|
||||||
|
|
@ -93,11 +78,9 @@ describe("bailout-mixin.js", () => {
|
||||||
|
|
||||||
function createMockVm() {
|
function createMockVm() {
|
||||||
return {
|
return {
|
||||||
dispatchStoreAction: jest.fn().mockResolvedValue(undefined),
|
|
||||||
$router: {
|
$router: {
|
||||||
navigateWithoutSaving: jest.fn().mockResolvedValue(undefined),
|
navigateWithoutSaving: jest.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
storeActions,
|
|
||||||
pageName: "test-page",
|
pageName: "test-page",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { shallowMount } from "@vue/test-utils";
|
||||||
import { setupMocksForJsFiles, getMountOptions } from "@/helpers/unit-test-helper.js";
|
import { setupMocksForJsFiles, getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
import { storeActions } from "@/constants/store-actions";
|
import { storeActions } from "@/constants/store-actions";
|
||||||
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
|
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
|
||||||
|
import bailoutMixin from "@/mixins/bailout-mixin";
|
||||||
|
import { bailoutCodes } from "@/constants/bailout-codes";
|
||||||
|
import store from "@/store";
|
||||||
|
import { storeMutations } from "@/constants/store-mutations";
|
||||||
|
|
||||||
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
|
||||||
navigateForward: jest.fn(),
|
navigateForward: jest.fn(),
|
||||||
|
|
@ -15,6 +19,7 @@ jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
|
||||||
describe("vin-pages-mixin", () => {
|
describe("vin-pages-mixin", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
store.commit(storeMutations.UPDATE_VIN_REQUIRED, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("navigateForwardWithSingleCarMatch", () => {
|
describe("navigateForwardWithSingleCarMatch", () => {
|
||||||
|
|
@ -29,10 +34,46 @@ describe("vin-pages-mixin", () => {
|
||||||
// Assert
|
// Assert
|
||||||
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
|
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("navigate to bailout when parts are not found", async () => {
|
||||||
|
// Arrange
|
||||||
|
const { wrapper } = setupMocks({ partNotFound: true });
|
||||||
|
bailoutMixin.methods.navigateToBailoutPage = jest.fn();
|
||||||
|
vehicleQuestionsMixin.methods.navigateForward = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.navigateForwardWithSingleCarMatch();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(bailoutMixin.methods.navigateToBailoutPage).toHaveBeenCalledWith(
|
||||||
|
wrapper.vm,
|
||||||
|
bailoutCodes.PART_NOT_FOUND
|
||||||
|
);
|
||||||
|
expect(vehicleQuestionsMixin.methods.navigateForward).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("vinRequired vehicles navigate forward when parts are found", async () => {
|
||||||
|
// Arrange
|
||||||
|
store.commit(storeMutations.UPDATE_VIN_REQUIRED, true);
|
||||||
|
const partsOrQuestions = [{ partNumber: "123" }];
|
||||||
|
const { wrapper } = setupMocks({ partsOrQuestions });
|
||||||
|
bailoutMixin.methods.navigateToBailoutPage = jest.fn();
|
||||||
|
vehicleQuestionsMixin.methods.navigateForward = jest.fn();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await wrapper.vm.navigateForwardWithSingleCarMatch();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(bailoutMixin.methods.navigateToBailoutPage).not.toHaveBeenCalled();
|
||||||
|
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalledWith(
|
||||||
|
partsOrQuestions,
|
||||||
|
wrapper.vm
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupMocks({ partsOrQuestions = [] }) {
|
function setupMocks({ partsOrQuestions = [], partNotFound = false } = {}) {
|
||||||
const baseMixin = setupMocksForJsFiles({
|
const baseMixin = setupMocksForJsFiles({
|
||||||
actionList: [
|
actionList: [
|
||||||
{
|
{
|
||||||
|
|
@ -44,6 +85,14 @@ function setupMocks({ partsOrQuestions = [] }) {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (partNotFound) {
|
||||||
|
baseMixin.baseMixin.methods.dispatchStoreAction.mockImplementation((actionName) => {
|
||||||
|
if (actionName === storeActions.GET_PARTS_OR_QUESTIONS) {
|
||||||
|
return Promise.resolve({ PartNotFound: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const mocks = getMountOptions({
|
const mocks = getMountOptions({
|
||||||
router: {
|
router: {
|
||||||
navigate: jest.fn(),
|
navigate: jest.fn(),
|
||||||
|
|
|
||||||
|
|
@ -193,5 +193,5 @@ export async function beforeEach(to, from) {
|
||||||
|
|
||||||
function getIsBailout(submittedState) {
|
function getIsBailout(submittedState) {
|
||||||
const submittedStateObj = JSON.parse(submittedState);
|
const submittedStateObj = JSON.parse(submittedState);
|
||||||
return !!submittedStateObj?.applicationUser?.bailoutCode;
|
return !!submittedStateObj?.applicationUser?.pageData?.bailout?.bailoutCode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { buildManualUrl } from "@/router/methods/helpers/build-manual-url";
|
||||||
import { getDestination } from "@/router/methods/helpers/get-destination";
|
import { getDestination } from "@/router/methods/helpers/get-destination";
|
||||||
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
import { savePageData } from "@/router/methods/helpers/save-page-data";
|
||||||
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
import { navigationScenarios } from "@/router/constants/navigation-scenarios";
|
||||||
|
import { routeData } from "@/router/constants/routes";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import store from "@/store";
|
import store from "@/store";
|
||||||
|
|
||||||
|
|
@ -79,9 +80,13 @@ export async function navigateWithSaving(scenario, currentPageName) {
|
||||||
export async function navigateWithPageData(scenario, currentPageName, pageData = {}) {
|
export async function navigateWithPageData(scenario, currentPageName, pageData = {}) {
|
||||||
const nextPage = getDestination(currentPageName, scenario);
|
const nextPage = getDestination(currentPageName, scenario);
|
||||||
|
|
||||||
if (pageData && pageData.bailoutCode) {
|
if (currentPageName === routeData.BAILOUT.name) {
|
||||||
pageData.AppName = "FixMyGlass";
|
const existingPageData = store.getters.pageData(routeData.BAILOUT.name) ?? {};
|
||||||
await savePageData(currentPageName, pageData);
|
await savePageData(routeData.BAILOUT.name, {
|
||||||
|
...existingPageData,
|
||||||
|
...pageData,
|
||||||
|
AppName: "FixMyGlass",
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await savePageData(nextPage.name, pageData);
|
await savePageData(nextPage.name, pageData);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,6 @@ const getDefaultState = () => {
|
||||||
affiliateCookies: [],
|
affiliateCookies: [],
|
||||||
loggingOption: false,
|
loggingOption: false,
|
||||||
hasAlreadyTriggeredError: false,
|
hasAlreadyTriggeredError: false,
|
||||||
bailoutCode: null,
|
|
||||||
},
|
},
|
||||||
idempotencyKeyFields: {
|
idempotencyKeyFields: {
|
||||||
referralCorrelationId: null,
|
referralCorrelationId: null,
|
||||||
|
|
@ -506,6 +505,9 @@ export const mutations = {
|
||||||
state.order.vehicle.carId = vehicleInfo.carId;
|
state.order.vehicle.carId = vehicleInfo.carId;
|
||||||
state.order.vehicle.category = vehicleInfo.category;
|
state.order.vehicle.category = vehicleInfo.category;
|
||||||
state.order.vehicle.vin = vehicleInfo.vin;
|
state.order.vehicle.vin = vehicleInfo.vin;
|
||||||
|
if (vehicleInfo.vinRequired !== undefined) {
|
||||||
|
state.order.vehicle.vinRequired = vehicleInfo.vinRequired;
|
||||||
|
}
|
||||||
|
|
||||||
state.order.vehicle.imageUrl = vehicleInfo.imageUrl;
|
state.order.vehicle.imageUrl = vehicleInfo.imageUrl;
|
||||||
state.order.vehicle.imageVifNumber = vehicleInfo.imageVifNumber;
|
state.order.vehicle.imageVifNumber = vehicleInfo.imageVifNumber;
|
||||||
|
|
@ -1035,9 +1037,6 @@ export const mutations = {
|
||||||
state.idempotencyKeyFields.totalInCents = totalInCents;
|
state.idempotencyKeyFields.totalInCents = totalInCents;
|
||||||
state.idempotencyKeyFields.expiryTime = expiryTime;
|
state.idempotencyKeyFields.expiryTime = expiryTime;
|
||||||
},
|
},
|
||||||
updateBailoutCode(state, bailoutCode) {
|
|
||||||
state.applicationUser.bailoutCode = bailoutCode;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export Getters
|
// Export Getters
|
||||||
|
|
@ -1160,6 +1159,7 @@ export const getters = {
|
||||||
pageData: (state) => (page) => {
|
pageData: (state) => (page) => {
|
||||||
return state.applicationUser.pageData[page];
|
return state.applicationUser.pageData[page];
|
||||||
},
|
},
|
||||||
|
bailoutCode: (state) => state.applicationUser.pageData.bailout?.bailoutCode,
|
||||||
applicationUser: (state) => state.applicationUser,
|
applicationUser: (state) => state.applicationUser,
|
||||||
order: (state) => state.order,
|
order: (state) => state.order,
|
||||||
payment: (state) => state.order.payment,
|
payment: (state) => state.order.payment,
|
||||||
|
|
@ -1849,6 +1849,7 @@ export const actions = {
|
||||||
totalPrice,
|
totalPrice,
|
||||||
userAgent,
|
userAgent,
|
||||||
cashPriceSubTotal,
|
cashPriceSubTotal,
|
||||||
|
bailoutCode,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
var payload = {
|
var payload = {
|
||||||
|
|
@ -1903,6 +1904,7 @@ export const actions = {
|
||||||
userAgent: userAgent,
|
userAgent: userAgent,
|
||||||
cashPriceSubTotal: cashPriceSubTotal,
|
cashPriceSubTotal: cashPriceSubTotal,
|
||||||
billToAccountNumber: billToAccountNumber,
|
billToAccountNumber: billToAccountNumber,
|
||||||
|
bailoutCode: bailoutCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return globalMethods.callHttpClient({
|
return globalMethods.callHttpClient({
|
||||||
|
|
@ -2001,7 +2003,7 @@ export const actions = {
|
||||||
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
|
||||||
|
|
||||||
const partsOrQuestionsEndpoint = vehicle.vinRequired
|
const partsOrQuestionsEndpoint = vehicle.vinRequired
|
||||||
? endpoints.GetPartsOrQuestionsV3
|
? endpoints.GetPartsOrQuestionsV2
|
||||||
: endpoints.GetPartsOrQuestions;
|
: endpoints.GetPartsOrQuestions;
|
||||||
|
|
||||||
const response = await globalMethods
|
const response = await globalMethods
|
||||||
|
|
@ -3980,10 +3982,6 @@ export const actions = {
|
||||||
context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey);
|
context.commit(storeMutations.UPDATE_IDEMPOTENCY_KEY, newKey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
saveBailoutCode(context, bailoutCode) {
|
|
||||||
context.commit(storeMutations.UPDATE_BAILOUT_CODE, bailoutCode);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,3 @@ option,
|
||||||
.btn {
|
.btn {
|
||||||
letter-spacing: 0.03rem;
|
letter-spacing: 0.03rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-gradient {
|
|
||||||
height: 12px;
|
|
||||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,33 @@ describe("intercept-overlay.vue", () => {
|
||||||
expect(wrapper.text()).toBe("");
|
expect(wrapper.text()).toBe("");
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("keyboard interception", () => {
|
||||||
|
test("adds a capturing keydown listener on mount and removes it on unmount", () => {
|
||||||
|
const addSpy = jest.spyOn(document, "addEventListener");
|
||||||
|
const removeSpy = jest.spyOn(document, "removeEventListener");
|
||||||
|
|
||||||
|
const wrapper = shallowMount(interceptOverlay);
|
||||||
|
expect(addSpy).toHaveBeenCalledWith("keydown", expect.any(Function), true);
|
||||||
|
|
||||||
|
const [, handler] = addSpy.mock.calls.find(
|
||||||
|
([type, , capture]) => type === "keydown" && capture === true
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
expect(removeSpy).toHaveBeenCalledWith("keydown", handler, true);
|
||||||
|
|
||||||
|
addSpy.mockRestore();
|
||||||
|
removeSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blockKey prevents default and stops immediate propagation", () => {
|
||||||
|
const wrapper = shallowMount(interceptOverlay);
|
||||||
|
const event = { preventDefault: jest.fn(), stopImmediatePropagation: jest.fn() };
|
||||||
|
wrapper.vm.blockKey(event);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(event.stopImmediatePropagation).toHaveBeenCalled();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="intercept-overlay" aria-hidden="true"></div>
|
<div class="intercept-overlay" aria-hidden="true" @keydown.capture="blockKey"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "intercept-overlay",
|
name: "intercept-overlay",
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener("keydown", this.blockKey, true);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
document.removeEventListener("keydown", this.blockKey, true);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
blockKey(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue