Merge branch 'release/2025.02.27' into rlsmerge/2025.02.27-to-develop

This commit is contained in:
CarlNation 2025-02-17 06:53:45 -05:00
commit 24286d2f27
33 changed files with 1697 additions and 150 deletions

View file

@ -1,5 +1,6 @@
{
"tabWidth": 4,
"htmlWhitespaceSensitivity": "ignore",
"bracketSameLine": true,
"printWidth": 100,
"trailingComma" : "es5"

View file

@ -28,6 +28,7 @@ module.exports = {
"!src/layouts/insurance/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/*.vue", // Temp test exclusion while in development
"!src/layouts/insurance-company/insurance-company-question/*.vue", // Temp test exclusion while in development
"!src/experiment-components/.vue",
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

View file

@ -18,6 +18,7 @@ const experimentSettings = {
INSURANCE_TAB_TO_DISPLAY_THRESHOLD_INTERNAL: "NextGen_InternalInsuranceTabDisplayThreshold",
INSURANCE_TAB_TO_DISPLAY_THRESHOLD_EXTERNAL: "NextGen_ExternalInsuranceTabDisplayThreshold",
DISPLAY_MSR: "DisplayMSR",
PRICING_BY_DAY: "DisplayPricingByDay",
};
const experimentTriggers = {

View file

@ -55,6 +55,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
:additionalButtonData="answer.additionalButtonData"
:lastValuePushedToGa="lastValuePushedToGa"
:setLastValuePushedToGa="setLastValuePushedToGa"
:isInsuranceSelected="isInsuranceSelected"
:suppressError="suppressError"
@buttonEvent="handleButtonEvent"
v-model="selectedValues" />
@ -139,6 +140,7 @@ export default {
type: Boolean,
default: false,
},
isInsuranceSelected: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);

File diff suppressed because it is too large Load diff

View file

@ -17,18 +17,40 @@
<div
v-if="calendarViewDirection === 'future'"
class="legend caption d-flex align-items-center justify-content-end">
<span class="legend-circle me-1"></span> &equals; Available
<span class="legend-circle me-1"></span>
&equals; Available
</div>
<div class="separator-line"></div>
<div class="nav-back ps-3"><button></button></div>
<div class="nav-forward pe-3"><button></button></div>
<div class="grid-item caption"><span class="sr-only">Sunday</span>S</div>
<div class="grid-item caption"><span class="sr-only">Monday</span>M</div>
<div class="grid-item caption"><span class="sr-only">Tuesday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Wednesday</span>W</div>
<div class="grid-item caption"><span class="sr-only">Thursday</span>T</div>
<div class="grid-item caption"><span class="sr-only">Friday</span>F</div>
<div class="grid-item caption"><span class="sr-only">Saturday</span>S</div>
<div class="grid-item caption">
<span class="sr-only">Sunday</span>
S
</div>
<div class="grid-item caption">
<span class="sr-only">Monday</span>
M
</div>
<div class="grid-item caption">
<span class="sr-only">Tuesday</span>
T
</div>
<div class="grid-item caption">
<span class="sr-only">Wednesday</span>
W
</div>
<div class="grid-item caption">
<span class="sr-only">Thursday</span>
T
</div>
<div class="grid-item caption">
<span class="sr-only">Friday</span>
F
</div>
<div class="grid-item caption">
<span class="sr-only">Saturday</span>
S
</div>
<div
v-for="date in month.dates"
:key="date.inputValue"

View file

@ -16,8 +16,7 @@
:maxlength="maxLength"
role="textbox"
aria-multiline="true"
:aria-required="isRequired">
</textarea>
:aria-required="isRequired"></textarea>
<p
tabindex="-1"
class="caption mt-2 mb-0 w-100"

View file

@ -75,9 +75,9 @@
class="d-inline-flex small mt-1"
aria-atomic="true"
aria-live="polite"
:class="[centerErrorMessage ? 'center-error-message' : '']"
>{{ errorMessage }}</span
>
:class="[centerErrorMessage ? 'center-error-message' : '']">
{{ errorMessage }}
</span>
</div>
</div>
</template>

View file

@ -0,0 +1,462 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div
class="package-label"
:class="[
this.buttonLabelSubCopy ? 'has-subheader' : '',
this.isInsuranceSelected ? 'pricing-by-day-pkg-lbl-ins' : 'pricing-by-day-pkg-lbl',
]"
for="testradio">
<div class="package-specs">
<div v-if="this.additionalButtonData.servicePackageDiscount" class="row">
<div class="col md-6">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<span class="col md-6 service-package-discount-pricing-by-day">
<div>
<span v-if="!isInsuranceSelected" class="caption pricing-by-day">
as low as
</span>
<div
class="strikethrough-discount-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></div>
<div class="discount-price" v-html="getDisplayPrice"></div>
</div>
</span>
</div>
<div v-else>
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info-pricing-by-day">
<span v-if="!isInsuranceSelected" class="caption pricing-by-day">
as low as
</span>
<span
class="strikethrough-price"
v-if="shouldDisplayStrikeThroughPrice"
v-html="this.additionalButtonData.strikeThroughPrice"></span>
<span v-html="getDisplayPrice"></span>
</span>
</p>
<p
class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
v-html="this.buttonLabelSubCopy"></p>
</div>
<div class="hide-when-closed">
<!-- Parse the body text containing <ul> with logic -->
<ul>
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem">
<!-- no textLink -->
<span
v-if="!doesCopyContainTextLink(listItem)"
v-html="listItem"></span>
<!-- with textLink -->
<template
v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink
linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="
$emit('buttonEvent', {
eventName: 'openModal',
args: getRouterLinkRouteFromCopy(copy),
})
"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)" />
</span>
</template>
</li>
</ul>
<!-- End of body text parsing -->
<div
class="package-footer fw-bold caption ms-n4 mt-4"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
<div
v-if="
this.additionalButtonData.servicePackageDiscount &&
this.additionalButtonData.Text
"
class="special-save-box">
<span v-html="this.additionalButtonData.Text"></span>
</div>
</div>
</div>
</baseInputButton>
</template>
<script>
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import textLink from "@/ux-components/text-link/text-link";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from "@/helpers/cms-content-helper";
import experimentMixin from "@/mixins/experiment-mixin";
import { experimentSettings } from "@/constants/experiments";
import store from "@/store";
export default {
name: "servicePackageRadio",
mixins: [inputButtonWrapperMixin],
props: {
isInsuranceSelected: Boolean,
},
components: {
baseInputButton,
textLink,
},
computed: {
arrayOfListItemsFromBodyText() {
return this.getArrayOfListItemsFromRawCmsCopy(this.buttonBodyCopy);
},
shouldDisplayStrikeThroughPrice() {
const displayPrice = this.buttonAuxillaryCopy.substring(
this.buttonAuxillaryCopy.indexOf("$")
);
return displayPrice != this.additionalButtonData.strikeThroughPrice;
},
getDisplayPrice() {
return this.buttonAuxillaryCopy.split(".")[0];
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
const regex = /(?:<ul(?:.*?)>)|(?:<\/ul>)/g;
return copy.replace(regex, "");
},
getArrayOfListItemsFromRawCmsCopy(copy) {
const withoutUlTags = this.stripUlTagFromCopy(copy);
return withoutUlTags
.split(/(?:<li(?:.*?)>)|(?:<\/li>)/g)
.filter((lineItem) => lineItem);
},
},
};
</script>
<style lang="scss">
.package-main {
.package-wrapper {
margin: 1rem 0;
&:first-child {
margin: 1rem 0 0 0;
}
&:last-child {
margin: 1rem 0 0 0;
}
@include media-breakpoint-up(md) {
margin: 1rem 0.5rem 0 0.5rem;
display: flex;
flex-direction: column;
&:first-child {
margin: 1rem 0.5rem 0 0;
}
&:last-child {
margin: 1rem 0 0 0.5rem;
}
}
label {
display: block;
@include media-breakpoint-up(md) {
display: flex;
height: 100%;
}
}
input[type="radio"] {
opacity: 0;
position: absolute;
left: -9999px;
+ .package-label.pricing-by-day-pkg-lbl {
/* Copying a component for an experiment will cause duplication of css classes.
This can cause problems if you want change the value of something only for the experiment
version of the component. So added a more specific selector for the parts I want to change
If this experiment wins, move this to package-label ::before/::after below
*/
&:before {
top: 15px;
}
&:after {
top: 30px;
}
}
+ .package-label.pricing-by-day-pkg-lbl-ins {
&:before {
top: 5px;
}
&:after {
top: 20px;
}
}
+ .package-label {
display: flex;
align-items: flex-start;
position: relative;
cursor: pointer;
width: 100%;
padding: 0.75rem;
border: 1px solid $gray-300;
box-shadow:
0px 4px 8px -4px rgba(0, 0, 0, 0.15),
0px 4px 24px -8px rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
overflow: hidden;
min-height: 52px;
max-height: 126px;
@include media-breakpoint-up(md) {
max-height: 500px;
padding-bottom: 3rem;
}
transition: max-height 0.25s ease-in;
&.has-subheader {
min-height: 72px;
}
&:before {
content: "";
position: relative;
margin-right: 0.5rem;
border-radius: 50%;
border: 1px solid $gray-500;
width: 16px;
height: 16px;
min-width: 16px;
}
&:after {
content: "";
position: absolute;
left: 15px;
border-radius: 50%;
width: 10px;
height: 10px;
min-width: 10px;
}
}
&:hover {
+ .package-label {
&:before {
border: 1px solid #8e9292;
box-shadow:
0px 0px 0px 4px #9fcee6,
0px 1px 4px rgba(0, 0, 0, 0.2);
}
}
}
&:checked {
+ .package-label {
.package-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
.hide-when-closed {
display: block;
}
}
}
+ .package-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
.special-save-box {
background-color: $green-200;
}
}
+ .package-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
&:after {
background: $blue;
}
}
}
&:focus {
+ .package-label {
&:before {
border: 2px solid $blue;
}
}
}
}
.package-footer {
color: $red;
font-weight: 600;
}
.package-specs {
display: flex;
flex-direction: column;
width: 100%;
transition: all 0.5s ease;
@include media-breakpoint-up(md) {
max-height: 500px;
}
span.pricing-by-day {
color: #4d5151;
display: block;
white-space: nowrap;
}
.row {
align-items: center;
}
p {
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
span {
&.pricing-info-pricing-by-day {
text-align: end;
color: $green;
font-size: 0.875rem;
span.strikethrough-price {
text-decoration: line-through;
font-size: 0.75rem;
font-weight: 400;
color: $gray-550;
padding-right: 8px;
}
}
}
&.sub-label {
color: $green;
text-transform: uppercase;
font-size: 0.75rem;
}
}
span {
&.service-package-discount-pricing-by-day {
color: $green;
text-align: end;
font-weight: 600;
font-size: 0.875rem;
display: flex;
justify-content: flex-end;
align-items: center;
align-self: flex-start;
div.strikethrough-discount-price {
display: inline;
text-decoration: line-through;
margin-right: 0.5rem;
font-weight: 400;
font-size: 0.75rem;
line-height: 20px;
color: $gray-550;
@include media-breakpoint-up(md) {
margin-right: 0.5rem;
font-size: 0.75rem;
}
}
div.discount-price {
color: $green;
display: inline;
}
}
}
ul {
margin: 1rem 0 0 -5px;
padding: 0;
li {
font-size: 0.875rem;
line-height: 1.714;
color: $gray-550;
a {
line-height: 1.714;
padding: 0;
}
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
.hide-when-closed {
display: none;
@include media-breakpoint-up(md) {
display: flex;
flex-direction: column;
}
}
}
.discount-text {
height: 2rem;
display: flex;
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
background-color: $green;
align-items: center;
justify-content: center;
width: 100%;
color: white;
text-transform: uppercase;
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.75px;
}
.special-save-box {
background-color: $green-100;
display: flex;
flex-direction: row;
margin: auto;
margin-left: -1.25rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
margin-top: 0.5rem;
span {
color: $green;
font-size: 0.875rem;
font-weight: 600;
}
}
}
}
</style>

View file

@ -10,9 +10,9 @@
href="javascript:void(0)"
class="col d-flex justify-content-between py-0">
<span class="label">{{ OrderDetailsText }}</span>
<span class="label amount-due">{{
getLineItemAmount(amountDue, showCoverageAsPending)
}}</span>
<span class="label amount-due">
{{ getLineItemAmount(amountDue, showCoverageAsPending) }}
</span>
</a>
</div>
<div class="cart-panel">
@ -48,11 +48,11 @@
v-for="(cartItem, i) in packageCartItems"
:key="i"
class="packaged-cart-item">
<span
><span :class="cartItem.category === 'promos' ? 'promo' : ''">{{
cartItem.name
}}</span></span
>
<span>
<span :class="cartItem.category === 'promos' ? 'promo' : ''">
{{ cartItem.name }}
</span>
</span>
<textLink
v-if="allowItemRemoval"
ref="removeLink"
@ -99,11 +99,9 @@
!showCoverageAsVerified ||
cartItem.name === 'Recycling' ||
(showCoverageAsVerified && !cartItem.isCoveredByInsurance)
"
>{{
getLineItemAmount(cartItem.subTotal, null, cartItem.category)
}}</span
>
">
{{ getLineItemAmount(cartItem.subTotal, null, cartItem.category) }}
</span>
</div>
<!-- Sub total, sales tax, total columns -->
@ -143,8 +141,10 @@
class="my-2 lh-1 applied-promo-tag"
v-for="(promoCode, i) in getPromoCodeList"
:key="i">
<span class="caption"
>Promo code <span class="promo-code">{{ promoCode }}</span> applied
<span class="caption">
Promo code
<span class="promo-code">{{ promoCode }}</span>
applied
</span>
</div>
<contentGroupModal ref="RecycleModal" cmsWidgetName="RecycleModal">

View file

@ -54,8 +54,10 @@
class="mt-4 d-flex align-items-center justify-content-between"
v-for="(promoCode, i) in this.getPromoCodeList()"
:key="i">
<span class="applied-promo"
>Promo code <span class="promo-code">{{ promoCode }}</span> applied
<span class="applied-promo">
Promo code
<span class="promo-code">{{ promoCode }}</span>
applied
</span>
<textLink
ref="removeLink"

View file

@ -4,8 +4,8 @@
<button v-on:click="openCalendarModal">
<span class="add-to-calendar-span">
<img src="@/assets/img/icons/add-to-calendar.svg" class="add-to-calendar-img" />
<span class="add-to-calendar-text">Add to calendar</span></span
>
<span class="add-to-calendar-text">Add to calendar</span>
</span>
</button>
</div>
<calendarModalQuestion
@ -14,8 +14,7 @@
cmsWidgetName="CalendarModalQuestion"
@calendar-modal-closed="calendarModalClosed"
:calendarOptionsData="calendarValues"
v-model="selectedCalendarOption">
</calendarModalQuestion>
v-model="selectedCalendarOption"></calendarModalQuestion>
</div>
</template>
<script>

View file

@ -22,7 +22,7 @@
linkType="text"
:text="payWithCashButtonCopy"
href="javascript:void(0)"
@click-event="backButtonAction(foreceCashSelectionOnQuote = true)" />
@click-event="backButtonAction((foreceCashSelectionOnQuote = true))" />
<navbar
cmsWidgetName="FunnelFooterWidget"
@ -102,7 +102,7 @@ export default {
},
payWithCashButtonCopy() {
return this.getCmsContent("PayWithCashWidget", "Text");
}
},
},
methods: {
isCashParentAccountNumber() {
@ -123,10 +123,14 @@ export default {
backButtonAction(forceCashSelectionOnQuote) {
// Go back to Quote page
if (forceCashSelectionOnQuote) {
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route, {"isCashSelected": true});
this.$router.navigateWithoutSaving(
this.navigationScenarios.CLICKED_BACK,
this.$route,
{ isCashSelected: true }
);
return;
}
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {

View file

@ -93,7 +93,7 @@ export default {
false
);
}
baseMixin.methods.ResetExternalParamsAndHideModal();
}
});

View file

@ -18,7 +18,7 @@
v-if="hasImage"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
<span v-else>{{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}

View file

@ -11,7 +11,7 @@
class="ms-2"
:src="buttonImage"
:alt="getInlineAltText(token)" />
<span v-else> {{ getInlineAltText(token) }}</span>
<span v-else>{{ getInlineAltText(token) }}</span>
</span>
<span v-else>
{{ token }}

View file

@ -41,8 +41,7 @@
id="card-frame"
seamless
scrolling="no"
allow="payment *; microphone *; geolocation *;">
</iframe>
allow="payment *; microphone *; geolocation *;"></iframe>
</div>
<hr />

View file

@ -612,14 +612,10 @@ describe("quote.vue", () => {
mockTierOnePrice = 505;
const { wrapper } = setupMocks({});
//Act
await quote.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "quote",
isInsuranceSelected: true,
isCashSelected: true,
} },
{ query: { fmgPage: "quote", isInsuranceSelected: true, isCashSelected: true } },
undefined,
(c) => c(wrapper.vm)
);

View file

@ -387,8 +387,7 @@ export default {
// prettier-ignore
{
const log = getQuerystringParameter(queryStrings.LOG);
if (log === "true") {
if (store.getters.applicationUser.loggingOption) {
console.log("------------- quote.vue thresholds start -----------------");
console.log(new Date() + " isExternalParameter: " + isExternalParameter);
console.log(new Date() + " internalThreshold: " + internalThreshold);

View file

@ -3,7 +3,7 @@
<div class="alert-heading">
<span>
<img v-if="hasImage" :src="imageUrl" />
<span class="px-2"> {{ recalHeaderText }}</span>
<span class="px-2">{{ recalHeaderText }}</span>
</span>
</div>
<div>

View file

@ -29,6 +29,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({
return true;
}
},
hasSettingEqualTo(settingName, settingValue) {
return true;
},
},
}));

View file

@ -4,11 +4,12 @@
:answers="servicePackageAnswers"
:groupName="groupName"
buttonTypeString="servicePackageRadio"
:buttonTypeObject="servicePackageRadio"
:buttonTypeObject="hasPricingByDay() ? servicePackageRadioExperiment : servicePackageRadio"
v-model="selectedPackageName"
:validationRules="validationRules"
:isRequired="isRequired"
:logDisplayedValuesEvent="true" />
:logDisplayedValuesEvent="true"
:isInsuranceSelected="isInsuranceSelected" />
</template>
<script>
@ -16,6 +17,7 @@ import buttonQuestion from "@/digital-components/button-question/button-question
import baseMixin from "@/mixins/base-mixin.js";
import { processIfStatements } from "@/helpers/cms-content-helper";
import servicePackageRadio from "./service-package-radio/service-package-radio";
import servicePackageRadioExperiment from "@/experiment-components/service-package-radio";
import { partTypeStrings } from "@/constants/part-type-strings";
import { packageNames } from "@/constants/package-names";
import {
@ -58,6 +60,7 @@ export default {
data() {
return {
servicePackageRadio: servicePackageRadio,
servicePackageRadioExperiment: servicePackageRadioExperiment,
selectedPackageName: null,
};
},
@ -223,6 +226,14 @@ export default {
},
methods: {
processIfStatements,
hasPricingByDay() {
return (
experimentMixin.methods.hasSettingEqualTo(
experimentSettings.PRICING_BY_DAY,
"true"
) && !this.isInsuranceSelected
);
},
getHeaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "HeaderText");
},

View file

@ -27,9 +27,10 @@
</div>
<div class="row justify-content-center">
<div class="col-md-6 col-xl-4 start-over">
<a @click="startOver" alt="Start Over Button"
><img v-bind:src="startOverImage" />{{ startOverText }}</a
>
<a @click="startOver" alt="Start Over Button">
<img v-bind:src="startOverImage" />
{{ startOverText }}
</a>
</div>
</div>
</div>

View file

@ -18,7 +18,7 @@
marginTopSizeOverride="1" />
</template>
<locationAlerts cmsWidgetPrefix="LocationAlert-" ref="locationAlerts" />
<datePicker
<datePicker2
customComponentId="dateQuestion"
selectableDatesSetting="custom"
ref="datePicker"
@ -69,7 +69,7 @@ import navbar from "@/fmg-components/nav-bar/nav-bar";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
import { Form, defineRule } from "vee-validate";
import datePicker from "@/digital-components/date-picker/date-picker";
import datePicker2 from "@/digital-components/date-picker/date-picker-2";
import locationAlerts from "@/layouts/schedule/location-alerts/location-alerts";
import timeSlotModalQuestion from "./time-slot-modal-question/time-slot-modal-question";
import textBlock from "@/digital-components/text-block/text-block";
@ -213,7 +213,7 @@ export default {
preSelectedDate = null;
}
const datePickerInitialDataPromise = await datePicker.methods.loadInitialData({
const datePickerInitialDataPromise = await datePicker2.methods.loadInitialData({
// setup config options for date-picker
selectableDatesSetting: "custom",
initialViewRowsToShow: 5,
@ -537,7 +537,7 @@ export default {
funnelSubHeader,
Form,
loadingModal,
datePicker,
datePicker2,
locationAlerts,
timeSlotModalQuestion,
textBlock,

View file

@ -203,7 +203,7 @@ describe("mobile-location-modal-questions.vue", () => {
expect(wrapper.vm.$refs.MobileLocationModalWidget.openModal).toHaveBeenCalled();
});
it("Should emit updated-mobile-location-questions on setMobileLocation for a valid address and vehicle protection answer", async () => {
it("Should emit setMobileLocationAndProceed on setMobileLocation for a valid address and vehicle protection answer", async () => {
// Arrange
const mobileLocationQuestions = {
addressQuestions: {
@ -240,26 +240,19 @@ describe("mobile-location-modal-questions.vue", () => {
});
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
// Mock the emitEvent function
wrapper.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
wrapper.vm.$emit(eventName, newMobileLocationQuestions, resolve);
resolve(newMobileLocationQuestions);
});
});
// Act
wrapper.vm.internalModel = newMobileLocationQuestions;
await wrapper.vm.setMobileLocation();
// Assert
expect(wrapper.emitted()["updated-mobile-location-questions"]).toBeTruthy();
expect(wrapper.emitted()["updated-mobile-location-questions"][0][0]).toBe(
expect(wrapper.emitted()["setMobileLocationAndProceed"]).toBeTruthy();
expect(wrapper.emitted()["setMobileLocationAndProceed"][0][0]).toStrictEqual(
newMobileLocationQuestions
);
});
it("Should not emit updated-mobile-location-questions on setMobileLocation for an invalid address zip code", async () => {
it("Should not emit setMobileLocationAndProceed on setMobileLocation for an invalid address zip code", async () => {
// Arrange
const mobileLocationQuestions = {
addressQuestions: {
@ -302,7 +295,7 @@ describe("mobile-location-modal-questions.vue", () => {
await wrapper.vm.setMobileLocation();
// Assert
expect(wrapper.emitted("updated-mobile-location-questions")).not.toBeTruthy();
expect(wrapper.emitted("setMobileLocationAndProceed")).not.toBeTruthy();
});
it("Should display invalid zip alert for invalid address zip inputs", async () => {

View file

@ -97,8 +97,7 @@ export default {
"updated-mobile-fee-part",
"updated-contains-military-base",
"updated-bill-to-account-number",
"mobileLocationSelected",
"updated-mobile-location-questions",
"setMobileLocationAndProceed",
],
data() {
return {
@ -287,15 +286,7 @@ export default {
this.$emit("updated-bill-to-account-number", billToAccountNumber);
}
// update the page level model
await this.emitEvent("updated-mobile-location-questions", this.internalModel);
this.$emit("mobileLocationSelected");
},
//creating emitEvent function to raise events with resolve callback to handle events sequencing
async emitEvent(eventName, payload) {
return new Promise((resolve) => {
this.$emit(eventName, payload, resolve);
});
this.$emit("setMobileLocationAndProceed", this.internalModel);
},
},
watch: {

View file

@ -435,6 +435,7 @@ describe("service-location.vue", () => {
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
const mobileLocationQuestions = {
addressQuestions: {
@ -447,7 +448,6 @@ describe("service-location.vue", () => {
isVehicleProtected: null,
};
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
const newMobileLocationQuestions = {
addressQuestions: {
streetAddress: "5555 Sulgrave Dr",
@ -461,21 +461,13 @@ describe("service-location.vue", () => {
// Act
// Mock the emitEvent function
mobileLocationQuestionsComponent.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
mobileLocationQuestionsComponent.vm.$emit(eventName, payload, resolve);
});
});
// Trigger the event
await mobileLocationQuestionsComponent.vm.emitEvent(
"updated-mobile-location-questions",
await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
newMobileLocationQuestions
);
await wrapper.vm.$nextTick(); // Wait for DOM updates
await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for promise to resolve
// Assert
expect(wrapper.vm.mobileLocationQuestions).toStrictEqual(newMobileLocationQuestions);
@ -502,7 +494,7 @@ describe("service-location.vue", () => {
ref: "serviceZipCodeQuestion",
});
serviceZipCodeComponent.resetMobileFeePart = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
const mobileLocationQuestions = {
addressQuestions: {
streetAddress: "",
@ -542,21 +534,13 @@ describe("service-location.vue", () => {
// Act
// Mock the emitEvent function
mobileLocationQuestionsComponent.vm.emitEvent = jest.fn(async (eventName, payload) => {
return new Promise((resolve) => {
mobileLocationQuestionsComponent.vm.$emit(eventName, payload, resolve);
});
});
// Trigger the event
await mobileLocationQuestionsComponent.vm.emitEvent(
"updated-mobile-location-questions",
await mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
newMobileLocationQuestions
);
await wrapper.vm.$nextTick(); // Wait for DOM updates
await new Promise((resolve) => setTimeout(resolve, 0)); // Wait for promise to resolve
// Assert
expect(wrapper.vm.serviceZipCodeQuestion).toStrictEqual(newServiceZipCodeInfo);
@ -578,6 +562,7 @@ describe("service-location.vue", () => {
ref: "mobileLocationQuestions",
});
mobileLocationQuestionsComponent.resetComponent = jest.fn();
wrapper.vm.forwardButtonAction = jest.fn();
const serviceZipCodeComponent = wrapper.findComponent({
ref: "serviceZipCodeQuestion",
@ -596,7 +581,10 @@ describe("service-location.vue", () => {
};
// Act
mobileLocationQuestionsComponent.vm.$emit("update:modelValue", mobileLocationQuestions);
mobileLocationQuestionsComponent.vm.$emit(
"setMobileLocationAndProceed",
mobileLocationQuestions
);
// Assert
expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile");
@ -1429,6 +1417,5 @@ function setupMocks({ mountOptionsMockData = {} }) {
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(serviceLocation, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
return { wrapper, apiPromise };
}

View file

@ -93,12 +93,11 @@
@updated-serviceability="setServiceabilityDetails"
@updated-contains-military-base="setContainsMilitaryBase"
@updated-mobile-ctu="setCtuForMobile"
@updated-mobile-location-questions="setMobileLocationQuestions"
validationRules="mobile-location-required"
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget"
@mobileLocationSelected="forwardButtonAction" />
@setMobileLocationAndProceed="setMobileLocationQuestions" />
<shopQuestion
ref="shopQuestion"
@ -295,19 +294,6 @@ export default {
isVehicleProtected: this.isVehicleProtected,
};
},
async set(newValue) {
if (newValue.addressQuestions.zipCode !== this.zipCode) {
await getShopProviderData(newValue.addressQuestions.zipCode).then((result) => {
this.shopProviderData = result.data;
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
});
}
this.setMobileLocation(newValue);
//handle promise
newValue.resolve?.();
},
},
isServiceableMobile() {
if (this.isRecalibrationServiceableMobile !== null) {
@ -488,8 +474,18 @@ export default {
this.mobileFeePart = mobileFeePart;
},
//creating async function as the computed property can not directly handle asynchronous operations or promises.
async setMobileLocationQuestions(newValue, resolve) {
this.mobileLocationQuestions = { ...newValue, resolve };
async setMobileLocationQuestions(newValue) {
if (newValue.addressQuestions.zipCode !== this.zipCode) {
await getShopProviderData(newValue.addressQuestions.zipCode).then((result) => {
this.shopProviderData = result.data;
this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber
);
});
}
this.setMobileLocation(newValue);
//proceed to next page
this.forwardButtonAction();
},
getServiceAddressFromStore() {
return store.getters.order.serviceLocation.address;

View file

@ -8,7 +8,7 @@
href="#!"
@click-event="openModal">
<template v-slot:after-text v-if="editScreenReaderTextCmsWidgetName">
<span class="sr-only"> {{ screenReaderOnlyText }} </span>
<span class="sr-only">{{ screenReaderOnlyText }}</span>
</template>
</textLink>
</div>

View file

@ -8,12 +8,12 @@
:aria-label="buttonLabel"
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4">
<div class="row-one">
<span class="m-0 button-label-copy text-truncate" :class="textPosition">{{
buttonLabel
}}</span>
<span class="m-0 caption mx-1 text-nowrap" :class="textPosition">{{
buttonLabelSubCopy
}}</span>
<span class="m-0 button-label-copy text-truncate" :class="textPosition">
{{ buttonLabel }}
</span>
<span class="m-0 caption mx-1 text-nowrap" :class="textPosition">
{{ buttonLabelSubCopy }}
</span>
<div
v-if="displayAvailabilityIndicators"
class="availability-indicator rounded-pill text-nowrap"
@ -22,9 +22,9 @@
v-if="!isLoaderDisplayed"
class="availability-badge"
:class="availabilityRating == 'high' ? 'green' : 'orange'"></div>
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">{{
badgeText
}}</span>
<span v-if="!isLoaderDisplayed" class="m-0 button-auxillary-copy">
{{ badgeText }}
</span>
<loader
v-if="isLoaderDisplayed"
:loaderPosition="left"

View file

@ -206,8 +206,7 @@ export default {
if (!this.emailOrSms) {
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, "", false);
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, "", false);
}
else {
} else {
if (vinPagesMixin.methods.isPhoneNumber(this.emailOrSms)) {
const phone = this.emailOrSms.replace(/[()]/g, "");
await this.dispatchStoreAction(storeActions.SAVE_PHONE_NUMBER, phone, false);

View file

@ -1016,8 +1016,12 @@ export const getters = {
return email;
}
// if we're here, then we have both email and phone so get the value from pageData to reflect what they entered on the page
return state.applicationUser.pageData[fmgPageValues.EMAIL_SMS_PAGES]?.emailOrSmsValue;
// if we're here, then we have both email and phone so get the value from pageData to reflect what they entered on the page.
// if they didn't enter anything on the page default to email
return (
state.applicationUser.pageData[fmgPageValues.EMAIL_SMS_PAGES]?.emailOrSmsValue ??
state.order.customer.emailAddress
);
},
};

View file

@ -1,22 +1,26 @@
<template>
<loadingModal v-if="useLoadingModal" notFullScreen ref="loadingModal" />
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
<a v-if="linkType === 'navigation'" @click="handleClick" class="navigation-link" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
<a
v-else-if="linkType === 'newWindowLink'"
@click="handleClick"
class="new-window-link"
:href="href"
target="_blank"
>{{ text }}<slot name="after-text"></slot
></a>
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href"
>{{ text }}<slot name="after-text"></slot
></a>
target="_blank">
{{ text }}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'textSmall'" @click="handleClick" class="small" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
<a v-else-if="linkType === 'text'" @click="handleClick" :href="href">
{{ text }}
<slot name="after-text"></slot>
</a>
</template>
<script>