Merge branch 'release/2025.05.08' into feature/bugfix/CASH-501

This commit is contained in:
AdamCaouetteSafelite 2025-04-30 11:13:26 -04:00 committed by GitHub
commit 02f317daee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 319 additions and 132 deletions

View file

@ -18,9 +18,11 @@ pr:
resources: resources:
containers: containers:
- container: node - container: node
image: packagerepository.sagaws.net:8082/safelite/node-build:18 image: ghcr.io/safelite/node-build:18
endpoint: GitHub Docker
- container: awscli - container: awscli
image: packagerepository.sagaws.net:8082/safelite/awscli2-build:3.9 image: ghcr.io/safelite/awscli2-build:3.9
endpoint: GitHub Docker
- container: prettier-node - container: prettier-node
image: ghcr.io/safelite/prettier-node-build:23 image: ghcr.io/safelite/prettier-node-build:23
endpoint: GitHub Docker endpoint: GitHub Docker

View file

@ -4,6 +4,9 @@ module.exports = {
testResultsProcessor: "jest-junit", testResultsProcessor: "jest-junit",
preset: "@vue/cli-plugin-unit-jest", preset: "@vue/cli-plugin-unit-jest",
transform: { "^.+\\.vue$": "@vue/vue3-jest" }, transform: { "^.+\\.vue$": "@vue/vue3-jest" },
transformIgnorePatterns: [
'/node_modules/(?!.*\\.mjs$)',
],
moduleFileExtensions: ["js", "vue"], moduleFileExtensions: ["js", "vue"],
modulePathIgnorePatterns: ["vin-lookup"], modulePathIgnorePatterns: ["vin-lookup"],
collectCoverageFrom: [ collectCoverageFrom: [

View file

@ -3,6 +3,7 @@ import salesforceWebchat from "./salesforce-webchat.vue";
import * as devHelper from "./salesforce-helper-dev"; import * as devHelper from "./salesforce-helper-dev";
import * as qaHelper from "./salesforce-helper-qa"; import * as qaHelper from "./salesforce-helper-qa";
import * as prodHelper from "./salesforce-helper-prod"; import * as prodHelper from "./salesforce-helper-prod";
import { applicationConfig } from "../../constants/application-config";
describe("salesforceWebchat.vue", () => { describe("salesforceWebchat.vue", () => {
it("renders correctly", () => { it("renders correctly", () => {
@ -21,19 +22,19 @@ describe("salesforceWebchat.vue", () => {
}); });
it("calls the correct initialization function based on environment", () => { it("calls the correct initialization function based on environment", () => {
process.env.VUE_APP_CURRENT_ENVIRONMENT = "Prod"; applicationConfig.CURRENT_ENVIRONMENT = "Prod";
let wrapper = shallowMount(salesforceWebchat); let wrapper = shallowMount(salesforceWebchat);
mockProd = jest.spyOn(prodHelper, "initializeSalesforceWebchatForProd"); mockProd = jest.spyOn(prodHelper, "initializeSalesforceWebchatForProd");
wrapper.vm.initializeSalesforceWebchat(); wrapper.vm.initializeSalesforceWebchat();
expect(mockProd).toHaveBeenCalled(); expect(mockProd).toHaveBeenCalled();
process.env.VUE_APP_CURRENT_ENVIRONMENT = "QA"; applicationConfig.CURRENT_ENVIRONMENT = "QA";
wrapper = shallowMount(salesforceWebchat); wrapper = shallowMount(salesforceWebchat);
mockQa = jest.spyOn(qaHelper, "initializeSalesforceWebchatForQa"); mockQa = jest.spyOn(qaHelper, "initializeSalesforceWebchatForQa");
wrapper.vm.initializeSalesforceWebchat(); wrapper.vm.initializeSalesforceWebchat();
expect(mockQa).toHaveBeenCalled(); expect(mockQa).toHaveBeenCalled();
process.env.VUE_APP_CURRENT_ENVIRONMENT = "Dev"; applicationConfig.CURRENT_ENVIRONMENT = "Dev";
wrapper = shallowMount(salesforceWebchat); wrapper = shallowMount(salesforceWebchat);
mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev"); mockDev = jest.spyOn(devHelper, "initializeSalesforceWebchatForDev");
wrapper.vm.initializeSalesforceWebchat(); wrapper.vm.initializeSalesforceWebchat();

View file

@ -7,6 +7,7 @@
import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev"; import { initializeSalesforceWebchatForDev } from "./salesforce-helper-dev";
import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa"; import { initializeSalesforceWebchatForQa } from "./salesforce-helper-qa";
import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod"; import { initializeSalesforceWebchatForProd } from "./salesforce-helper-prod";
import { applicationConfig } from "@/constants/application-config";
export default { export default {
name: "salesforceWebchat", name: "salesforceWebchat",
@ -28,7 +29,7 @@ export default {
}, },
methods: { methods: {
initializeSalesforceWebchat() { initializeSalesforceWebchat() {
switch (process.env.VUE_APP_CURRENT_ENVIRONMENT) { switch (applicationConfig.CURRENT_ENVIRONMENT) {
case "Prod": case "Prod":
initializeSalesforceWebchatForProd(); initializeSalesforceWebchatForProd();
break; break;

View file

@ -182,6 +182,23 @@ describe("address-lookup.vue", () => {
// Assert // Assert
expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true); expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true);
}); });
test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
const { wrapper } = setupMocks({
isZipServiceable: true,
displayNoServiceAlert: false,
lookupVinbyAddressResponse: {
isStatePermissible: true,
vinVehicles: [], // Return no vehicles
},
});
// Ensure displayNoServiceAlert is false
await wrapper.setData({ displayNoServiceAlert: false });
await wrapper.vm.forwardButtonAction();
// Check if the AlertNoService component is not rendered
const alertNoService = wrapper.findComponent({ ref: "AlertNoService" });
expect(alertNoService.exists()).toBe(false);
});
}); });
describe("navigation", () => { describe("navigation", () => {
@ -741,6 +758,17 @@ function setupMocks({
wrapper.vm.setCmsContent = jest.fn(); wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.navbar.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.navbar.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
const closestShops = {
data: {
providers: [
{ id: 1, name: "Shop 1" },
{ id: 2, name: "Shop 2" },
],
},
};
wrapper.vm.findClosestApplicableShops = jest.fn().mockImplementation(() => {
return new Promise((resolve) => resolve(closestShops));
});
return { wrapper }; return { wrapper };
} }

View file

@ -81,13 +81,20 @@
</div> </div>
</div> </div>
</transition> </transition>
<alert
ref="AlertNoService"
v-if="displayNoServiceAlert"
class="my-4"
cmsWidgetName="AlertNoServiceWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
:isDisabled="!meta.valid" :isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
:isForwardActionDisabled="!meta.valid" /> :isForwardActionDisabled="!meta.valid || displayNoServiceAlert" />
</div> </div>
</div> </div>
</div> </div>
@ -177,6 +184,7 @@ export default {
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
showServiceZipField: this.getServiceZipFromStore(), showServiceZipField: this.getServiceZipFromStore(),
isZipServiceable: false, isZipServiceable: false,
displayNoServiceAlert: false,
}; };
}, },
methods: { methods: {
@ -293,6 +301,17 @@ export default {
return this.$refs.navbar.removeLoader(); return this.$refs.navbar.removeLoader();
} }
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
if (this.serviceZipCode != null && this.isHeavyTruck) {
const closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
}
const carsFound = resultMap.vinLookupResponse.vinVehicles; const carsFound = resultMap.vinLookupResponse.vinVehicles;
// Handle cases for different amounts of VINS found for the address. // Handle cases for different amounts of VINS found for the address.
@ -444,6 +463,7 @@ export default {
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.displayMatchedDifferentVehicleAlert = false; this.displayMatchedDifferentVehicleAlert = false;
this.displayVinLookupByHomeAddressNotAllowedAlert = false; this.displayVinLookupByHomeAddressNotAllowedAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
mounted() { mounted() {
@ -495,6 +515,7 @@ export default {
handler(newValue) { handler(newValue) {
// If they modify the service zip code, then hide the error message. // If they modify the service zip code, then hide the error message.
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
showServiceZipField: { showServiceZipField: {

View file

@ -620,6 +620,8 @@ function setupMocks({
validateZipResponse = { zipCodeCtu: null, isServiceable: false, isValid: true, state: null }, validateZipResponse = { zipCodeCtu: null, isServiceable: false, isValid: true, state: null },
//Values to set in the state store //Values to set in the state store
carId = null, carId = null,
isHeavyTruck = true,
displayNoServiceAlert = true,
serviceLocationZipCode = null, serviceLocationZipCode = null,
registrationZipCode = null, registrationZipCode = null,
licensePlate = null, //"TESTPLATE", licensePlate = null, //"TESTPLATE",
@ -672,6 +674,8 @@ function setupMocks({
zipCode: registrationZipCode, zipCode: registrationZipCode,
}, },
carId: carId, carId: carId,
isBigTruck: isHeavyTruck,
canSafeliteService: true,
}, },
order: { order: {
customer: { customer: {
@ -716,5 +720,17 @@ function setupMocks({
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => ""); wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.$refs.navbar.updateButtonText = jest.fn(); wrapper.vm.$refs.navbar.updateButtonText = jest.fn();
wrapper.vm.$refs.navbar.removeLoader = jest.fn(); wrapper.vm.$refs.navbar.removeLoader = jest.fn();
const closestShops = {
data: {
providers: [
{ id: 1, name: "Shop 1" },
{ id: 2, name: "Shop 2" },
],
},
};
wrapper.vm.findClosestApplicableShops = jest.fn().mockImplementation(() => {
return new Promise((resolve) => resolve(closestShops));
});
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -76,6 +76,7 @@
:manualCopy="AlertMatchedDifferentVehicleBody" :manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning" alertClass="alert-warning"
v-bind:isDismissible="false" /> v-bind:isDismissible="false" />
<alert <alert
ref="AlertNoService" ref="AlertNoService"
v-if="displayNoServiceAlert" v-if="displayNoServiceAlert"
@ -87,9 +88,9 @@
<navbar <navbar
ref="navbar" ref="navbar"
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid || displayNoServiceAlert"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction || displayNoServiceAlert" /> @ForwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
</div> </div>
@ -177,6 +178,7 @@ export default {
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
showServiceZipField: this.getServiceZipFromStore(), showServiceZipField: this.getServiceZipFromStore(),
isZipServiceable: false, isZipServiceable: false,
displayNoServiceAlert: false,
}; };
}, },
methods: { methods: {
@ -279,6 +281,17 @@ export default {
this.displayInvalidZipAlert = true; this.displayInvalidZipAlert = true;
return this.$refs.navbar.removeLoader(); return this.$refs.navbar.removeLoader();
} }
if (this.isHeavyTruck) {
const closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
}
// Check if the CarId has changed. // Check if the CarId has changed.
this.isCarIdDifferent = this.isCarIdDifferent =
vinLookup.data.vehicle.carId !== this.$store.getters.vehicle.carId; vinLookup.data.vehicle.carId !== this.$store.getters.vehicle.carId;
@ -393,6 +406,7 @@ export default {
this.displayMatchedDifferentVehicleAlert = false; this.displayMatchedDifferentVehicleAlert = false;
this.displayVinLookupByHomeAddressNotAllowedAlert = false; this.displayVinLookupByHomeAddressNotAllowedAlert = false;
this.displayInvalidZipAlert = false; this.displayInvalidZipAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
mounted() { mounted() {
@ -437,6 +451,7 @@ export default {
serviceZipCode() { serviceZipCode() {
// If they modify the service zip code, then hide the error message. // If they modify the service zip code, then hide the error message.
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.displayNoServiceAlert = false;
this.$refs.navbar.updateButtonText( this.$refs.navbar.updateButtonText(
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText") this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
); );

View file

@ -3,18 +3,16 @@
<component :is="'script'" src="https://js.afterpay.com/afterpay-1.x.js" async></component> <component :is="'script'" src="https://js.afterpay.com/afterpay-1.x.js" async></component>
<div class="my-4 alert-heading text-center"> <div class="my-4 alert-heading text-center">
<span v-html="afterpayHeaderCopy" /> <span v-html="afterpayHeaderCopy" />
&nbsp;
<span class="afterpay-amount">{{ this.afterpayPrice }}</span> <span class="afterpay-amount">{{ this.afterpayPrice }}</span>
<br /> <br />
<span class="alert-sub-heading" v-html="afterpayHeaderSubHeaderCopy" /> <span class="alert-sub-heading" v-html="afterpayHeaderSubHeaderCopy" />
&nbsp; <img :src="afterPayImageUrl" class="afterpay-image" />
<img :src="afterPayImageUrl" />
&nbsp;
<a <a
id="afterpay-learnmore" id="afterpay-learnmore"
href="#" href="#"
data-afterpay-modal="en_US-safelite" data-afterpay-modal="en_US-safelite"
data-bind="click:afterpayLearnMore"> data-bind="click:afterpayLearnMore"
class="afterpay-learn-more">
<img :src="infoIcon" alt="Info Icon" class="info-icon" /> <img :src="infoIcon" alt="Info Icon" class="info-icon" />
</a> </a>
<div class="after-pay"> <div class="after-pay">
@ -151,8 +149,8 @@ export default {
&.alert-info { &.alert-info {
.alert-heading { .alert-heading {
color: #000000; color: $black;
font-weight: 600; font-weight: $font-weight-600;
margin-left: 0.5rem; margin-left: 0.5rem;
margin-right: 0.5rem; margin-right: 0.5rem;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
@ -162,11 +160,12 @@ export default {
} }
} }
.afterpay-amount { .afterpay-amount {
color: #0c7e47; color: $green;
margin-left: 0.25rem;
} }
.alert-sub-heading { .alert-sub-heading {
color: #4b4c4e; color: $gray-1100;
font-weight: 400; font-weight: $font-weight-normal;
} }
.after-pay { .after-pay {
.after-pay-toggle { .after-pay-toggle {
@ -180,7 +179,7 @@ export default {
background-position: right center; background-position: right center;
margin-left: 0.5rem; margin-left: 0.5rem;
width: 1rem; width: 1rem;
height: 9px; height: 0.5625rem;
display: inline-flex; display: inline-flex;
} }
&.expanded:after { &.expanded:after {
@ -194,8 +193,8 @@ export default {
} }
} }
.after-pay-toggle a { .after-pay-toggle a {
font-weight: 600; font-weight: $font-weight-600;
color: #0070d1; color: $blue;
text-decoration: none; text-decoration: none;
} }
.after-pay-details { .after-pay-details {
@ -218,27 +217,45 @@ export default {
.payment-card { .payment-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 6px; border-radius: 0.375rem;
border: 0.25px #b3b4b5; border: 0.25px $gray-1000;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0px 4px 8px -4px rgba(0, 0, 0, 0.15);
} }
.calendar-section { .calendar-section {
background-color: #fff; /* White background for calendar area */ background-color: $white; /* White background for calendar area */
padding: 0.5rem; padding: 0.5rem;
text-align: center; text-align: center;
gap: 2px; gap: 0.125rem;
font-weight: 700; font-weight: $font-weight-700;
white-space: nowrap; white-space: nowrap;
border-top-left-radius: 0.375rem;
border-top-right-radius: 0.375rem;
width: 4.375rem;
@include media-breakpoint-up(md) {
width: 5rem;
}
} }
.payment-amount-section { .payment-amount-section {
background-color: #e0f7e9; /* Light green background for payment area */ background-color: $green-600; /* Light green background for payment area */
padding: 0.5rem; padding: 0.5rem;
text-align: center; text-align: center;
font-weight: 700; font-weight: $font-weight-700;
border-bottom-left-radius: 0.375rem;
border-bottom-right-radius: 0.375rem;
width: 4.375rem;
@include media-breakpoint-up(md) {
width: 5rem;
}
}
.afterpay-learn-more {
margin-left: 0.25rem;
}
.afterpay-image {
margin-left: 0.25rem;
} }
.amount-due { .amount-due {
color: #00723e; /* Green text */ color: $green-1000; /* Green text */
} }
.legend { .legend {
display: flex; display: flex;
@ -249,11 +266,11 @@ export default {
.legend-item { .legend-item {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 0.125rem;
white-space: nowrap; white-space: nowrap;
} }
.legend-item span { .legend-item span {
font-weight: 400; font-weight: $font-weight-normal;
} }
} }
</style> </style>

View file

@ -1,72 +1,70 @@
<template> <template>
<transition name="fade" mode="out-in"> <div class="mobile-location-questions">
<div class="mobile-location-questions"> <div class="text-center" :id="componentId">
<div class="text-center" :id="componentId"> <label
<label for="mobileLocationLinkPromptId"
for="mobileLocationLinkPromptId" :aria-label="mobileLocationLinkPromptText"
:aria-label="mobileLocationLinkPromptText" class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black" v-html="mobileLocationLinkPromptText"></label>
v-html="mobileLocationLinkPromptText"></label> <div class="update-mobile-location-text-link">
<div class="update-mobile-location-text-link"> <textLink
<textLink ref="mobileLocationLink"
ref="mobileLocationLink" id="mobileLocationLinkPromptId"
id="mobileLocationLinkPromptId" linkType="text"
linkType="text" :text="mobileLocationLinkText"
:text="mobileLocationLinkText" href="#!"
href="#!" @click-event="openModal" />
@click-event="openModal" />
</div>
<div v-show="errorMessage" class="row my-1 form-test-error">
<span
class="d-inline-flex small mt-0 center-error-message"
aria-atomic="true"
aria-live="polite">
{{ errorMessage }}
</span>
</div>
<textBlock
v-if="mobileFeeApplies"
:customText="mobileFeeText"
cmsWidgetName="MobileFeeDisclaimerWidget"
typeStyle="caption" />
</div> </div>
<modal <div v-show="errorMessage" class="row my-1 form-test-error">
:ref="modalName" <span
:headerText="modalHeaderText" class="d-inline-flex small mt-0 center-error-message"
:footerButtonText="modalFooterText" aria-atomic="true"
:onModalOpenedCallback="onModalOpened" aria-live="polite">
:onModalClosedCallback="onModalClosed" {{ errorMessage }}
@isModalOpened="setModalStatus" </span>
@footer-button-event="setMobileLocation"> </div>
<template v-if="isModalOpened"> <textBlock
<addressQuestions v-if="mobileFeeApplies"
ref="addressQuestions" :customText="mobileFeeText"
v-model="internalModel.addressQuestions" cmsWidgetName="MobileFeeDisclaimerWidget"
captureApartmentNumberOrBusinessName="true" typeStyle="caption" />
preserveCityAndStateOnReset="true" />
<vehicleProtectedQuestion
ref="vehicleProtectedQuestion"
v-model="internalModel.isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget" />
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertMismatchStateAndZip"
v-if="displayMismatchStateAndZipAlert"
class="my-4"
cmsWidgetName="AlertMismatchStateAndZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
</template>
</modal>
</div> </div>
</transition> <modal
:ref="modalName"
:headerText="modalHeaderText"
:footerButtonText="modalFooterText"
:onModalOpenedCallback="onModalOpened"
:onModalClosedCallback="onModalClosed"
@isModalOpened="setModalStatus"
@footer-button-event="setMobileLocation">
<template v-if="isModalOpened">
<addressQuestions
ref="addressQuestions"
v-model="internalModel.addressQuestions"
captureApartmentNumberOrBusinessName="true"
preserveCityAndStateOnReset="true" />
<vehicleProtectedQuestion
ref="vehicleProtectedQuestion"
v-model="internalModel.isVehicleProtected"
cmsWidgetName="VehicleProtectedQuestionWidget" />
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="my-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertMismatchStateAndZip"
v-if="displayMismatchStateAndZipAlert"
class="my-4"
cmsWidgetName="AlertMismatchStateAndZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
</template>
</modal>
</div>
</template> </template>
<script> <script>
@ -150,6 +148,7 @@ export default {
zipCode: "", zipCode: "",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
}), }),
}, },
mobileFeePart: { mobileFeePart: {
@ -169,17 +168,7 @@ export default {
return this.getCmsContent(this.linkWidgetName, "HeaderText"); return this.getCmsContent(this.linkWidgetName, "HeaderText");
}, },
mobileLocationLinkText() { mobileLocationLinkText() {
if ( if (this.isMobileAddressComplete()) {
this.addressModel.streetAddress &&
this.addressModel.streetAddress !== "" &&
this.addressModel.city &&
this.addressModel.city !== "" &&
this.addressModel.state &&
this.addressModel.state !== "" &&
this.addressModel.zipCode &&
this.addressModel.zipCode !== "" &&
this.internalModel.isVehicleProtected !== null
) {
return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`; return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`;
} }
return this.getCmsContent(this.linkWidgetName, "BodyText"); return this.getCmsContent(this.linkWidgetName, "BodyText");
@ -218,6 +207,19 @@ export default {
}, },
}, },
methods: { methods: {
isMobileAddressComplete() {
return (
this.addressModel.streetAddress &&
this.addressModel.streetAddress !== "" &&
this.addressModel.city &&
this.addressModel.city !== "" &&
this.addressModel.state &&
this.addressModel.state !== "" &&
this.addressModel.zipCode &&
this.addressModel.zipCode !== "" &&
this.internalModel.isVehicleProtected !== null
);
},
openModal() { openModal() {
this.modal.openModal(); this.modal.openModal();
}, },

View file

@ -81,6 +81,16 @@ jest.mock(
}) })
); );
jest.mock(
"@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue",
() => ({
isMobileAddressComplete: jest.fn(() => {
return true;
}),
openModal: jest.fn(),
})
);
jest.spyOn(baseMixin.methods, "getZipCodeData").mockImplementation((serviceZipCode) => { jest.spyOn(baseMixin.methods, "getZipCodeData").mockImplementation((serviceZipCode) => {
return new Promise((resolve) => { return new Promise((resolve) => {
let mockContainsMilitaryBase = false; let mockContainsMilitaryBase = false;
@ -343,6 +353,7 @@ describe("service-location.vue", () => {
zipCode: "43054", zipCode: "43054",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -355,6 +366,7 @@ describe("service-location.vue", () => {
zipCode: "61606", zipCode: "61606",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
}; };
// Act // Act
@ -457,6 +469,7 @@ describe("service-location.vue", () => {
zipCode: "", zipCode: "",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
const newMobileLocationQuestions = { const newMobileLocationQuestions = {
@ -468,6 +481,7 @@ describe("service-location.vue", () => {
zipCode: "43054", zipCode: "43054",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true,
}; };
wrapper.vm.closeModalAction = jest.fn(); wrapper.vm.closeModalAction = jest.fn();
@ -517,6 +531,7 @@ describe("service-location.vue", () => {
zipCode: "", zipCode: "",
}, },
isVehicleProtected: null, isVehicleProtected: null,
isMobileSelected: false,
mobileFeePart: null, mobileFeePart: null,
}; };
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
@ -530,6 +545,7 @@ describe("service-location.vue", () => {
zipCode: "43081", zipCode: "43081",
}, },
isVehicleProtected: true, isVehicleProtected: true,
isMobileSelected: true,
mobileFeePart: null, mobileFeePart: null,
}; };
@ -593,6 +609,7 @@ describe("service-location.vue", () => {
zipCode: "43081", zipCode: "43081",
}, },
isVehicleProtected: "YesAnswer", isVehicleProtected: "YesAnswer",
isMobileSelected: true,
}; };
wrapper.vm.closeModalAction = jest.fn(); wrapper.vm.closeModalAction = jest.fn();
@ -1433,5 +1450,7 @@ function setupMocks({ mountOptionsMockData = {} }) {
const mountOptions = getMountOptions(mountOptionsMockData); const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(serviceLocation, mountOptions); const wrapper = shallowMount(serviceLocation, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent; wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.mobileLocationQuestions.isMobileAddressComplete = jest.fn();
wrapper.vm.$refs.mobileLocationQuestions.openModal = jest.fn();
return { wrapper, apiPromise }; return { wrapper, apiPromise };
} }

View file

@ -88,7 +88,7 @@
<div class="col-md-6 col-xl-4"> <div class="col-md-6 col-xl-4">
<mobileLocationModalQuestions <mobileLocationModalQuestions
customComponentId="mobileLocationQuestions" customComponentId="mobileLocationQuestions"
v-if="selectedAppointmentType === 'Mobile'" v-show="selectedAppointmentType === 'Mobile'"
v-model="mobileLocationQuestions" v-model="mobileLocationQuestions"
:mobileFeePart="mobileFeePart" :mobileFeePart="mobileFeePart"
:mobileFeeApplies="mobileFeeApplies" :mobileFeeApplies="mobileFeeApplies"
@ -171,11 +171,12 @@ const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
// DEFINE VALIDATION RULES // DEFINE VALIDATION RULES
defineRule("mobile-location-required", (value) => { defineRule("mobile-location-required", (value) => {
if ( if (
!value.addressQuestions.streetAddress || value.isMobileSelected &&
!value.addressQuestions.city || (!value.addressQuestions.streetAddress ||
!value.addressQuestions.state || !value.addressQuestions.city ||
!value.addressQuestions.zipCode || !value.addressQuestions.state ||
!value.isVehicleProtected !value.addressQuestions.zipCode ||
!value.isVehicleProtected)
) { ) {
return errorMessages.MOBILE_LOCATION_REQUIRED; return errorMessages.MOBILE_LOCATION_REQUIRED;
} }
@ -297,6 +298,7 @@ export default {
zipCode: this.zipCode, zipCode: this.zipCode,
}, },
isVehicleProtected: this.isVehicleProtected, isVehicleProtected: this.isVehicleProtected,
isMobileSelected: this.selectedAppointmentType === "Mobile",
}; };
}, },
}, },
@ -765,6 +767,10 @@ export default {
this.selectedProvider = new Provider( this.selectedProvider = new Provider(
this.shopProviderData.mobileProviderNumber this.shopProviderData.mobileProviderNumber
); );
var mobileLocationQs = this.$refs.mobileLocationQuestions;
if (!mobileLocationQs.isMobileAddressComplete()) {
mobileLocationQs.openModal();
}
} else { } else {
this.selectedProvider = new Provider(); this.selectedProvider = new Provider();
} }

View file

@ -415,6 +415,21 @@ describe("service-zip.vue", () => {
expect(wrapper.vm.displayInvalidZipAlert).toBe(false); expect(wrapper.vm.displayInvalidZipAlert).toBe(false);
expect(wrapper.vm.displayNonServiceableZipAlert).toBe(false); expect(wrapper.vm.displayNonServiceableZipAlert).toBe(false);
}); });
test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
// Arrange
// no changes to store
applyMockStoreDataToGetters();
const wrapper = setupMocks({});
wrapper.vm.serviceZipCode = "12345";
wrapper.vm.displayNoServiceAlert = false;
await wrapper.vm.forwardButtonAction();
// Check if the AlertNoService component is rendered
expect(wrapper.vm.displayNoServiceAlert).toBe(false);
});
}); });
}); });
@ -511,5 +526,15 @@ function setupMocks({ customMountOptions, customZipQuery, customZipDataResponse
]; ];
const wrapper = shallowMount(serviceZip, mountOptions); const wrapper = shallowMount(serviceZip, mountOptions);
wrapper.vm.findClosestApplicableShops = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {
providers: [
{ id: 1, name: "Shop 1" },
{ id: 2, name: "Shop 2" },
],
},
});
});
return wrapper; return wrapper;
} }

View file

@ -46,11 +46,18 @@
v-model="customAlertData" v-model="customAlertData"
v-if="displayNonServiceableZipAlert" v-if="displayNonServiceableZipAlert"
alertClass="alert-danger" /> alertClass="alert-danger" />
<alert
ref="AlertNoService"
v-if="displayNoServiceAlert"
class="my-4"
cmsWidgetName="AlertNoServiceWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid || displayNoServiceAlert"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
</div> </div>
@ -114,6 +121,7 @@ export default {
isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(), isHeavyTruck: this.getIsVehicleHeavyTruckServiceableFromStore(),
displayInvalidZipAlert: false, displayInvalidZipAlert: false,
displayNonServiceableZipAlert: false, displayNonServiceableZipAlert: false,
displayNoServiceAlert: false, // Initialize the property
}; };
}, },
@ -278,17 +286,6 @@ export default {
return this.$refs.navbar.removeLoader(); return this.$refs.navbar.removeLoader();
} }
} }
if (this.isHeavyTruck) {
const closestShops = await this.findClosestApplicableShops(
this.serviceZipCode,
this.carId
);
this.displayNoServiceAlert = !closestShops?.data?.providers?.length;
if (this.displayNoServiceAlert) {
return this.$refs.navbar.removeLoader();
}
}
const payment = this.$store.getters.payment; const payment = this.$store.getters.payment;
const policy = this.$store.getters.policy; const policy = this.$store.getters.policy;
@ -346,7 +343,6 @@ export default {
serviceZipCode() { serviceZipCode() {
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.displayNoServiceAlert = false; this.displayNoServiceAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
components: { components: {

View file

@ -258,6 +258,23 @@ describe("vin-lookup.vue", () => {
// Assert // Assert
expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1); expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1);
}); });
test("should hide the AlertNoService when displayNoServiceAlert is false", async () => {
const { wrapper } = setupMocks({
isZipServiceable: true,
displayNoServiceAlert: false,
lookupVinbyAddressResponse: {
isStatePermissible: true,
vinVehicles: [], // Return no vehicles
},
});
// Ensure displayNoServiceAlert is false
await wrapper.setData({ displayNoServiceAlert: false });
await wrapper.vm.forwardButtonAction();
// Check if the AlertNoService component is not rendered
const alertNoService = wrapper.findComponent({ ref: "AlertNoService" });
expect(alertNoService.exists()).toBe(false);
});
}); });
describe("getVinFromImage", () => { describe("getVinFromImage", () => {

View file

@ -111,10 +111,18 @@
" "
alertClass="alert-success" /> alertClass="alert-success" />
<alert
ref="AlertNoService"
v-if="displayNoServiceAlert"
class="my-4"
cmsWidgetName="AlertNoServiceWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<navbar <navbar
cmsWidgetName="FunnelFooterWidget" cmsWidgetName="FunnelFooterWidget"
ref="navbar" ref="navbar"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid || displayNoServiceAlert"
@back-clicked="backButtonAction" @back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
</div> </div>
@ -209,6 +217,7 @@ export default {
displayVinNotFoundAlert: false, displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false, displayMatchedDifferentVehicleAlert: false,
displayVinScanFailedAlert: false, displayVinScanFailedAlert: false,
displayNoServiceAlert: false,
}; };
}, },
methods: { methods: {
@ -247,6 +256,9 @@ export default {
); );
} }
}, },
getCarIdfromStore() {
return this.$store.getters.vehicle.carId;
},
getIsVehicleHeavyTruckServiceableFromStore() { getIsVehicleHeavyTruckServiceableFromStore() {
return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService; return store.getters.vehicle.isBigTruck && store.getters.vehicle.canSafeliteService;
}, },
@ -341,7 +353,7 @@ export default {
false false
); );
if (this.isHeavyTruck) { if (this.isHeavyTruck) {
closestShops = await this.findClosestApplicableShops( const closestShops = await this.findClosestApplicableShops(
this.serviceZipCode, this.serviceZipCode,
this.carId this.carId
); );
@ -499,7 +511,6 @@ export default {
this.displayVinNotFoundAlert = false; this.displayVinNotFoundAlert = false;
this.displayVinScanFailedAlert = false; this.displayVinScanFailedAlert = false;
this.displayNoServiceAlert = false; this.displayNoServiceAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
mounted() { mounted() {
@ -576,6 +587,7 @@ export default {
}, },
serviceZipCode() { serviceZipCode() {
this.displayNonServiceableZipAlert = false; this.displayNonServiceableZipAlert = false;
this.displayNoServiceAlert = false;
}, },
}, },
components: { components: {

View file

@ -33,9 +33,11 @@ $green-300: #94d7b6;
$green-400: #5abc8c; // Used in theme $green-400: #5abc8c; // Used in theme
$green-500: #2ca168; $green-500: #2ca168;
$green: #0c7e47; // Default Green $green: #0c7e47; // Default Green
$green-600: #e7f2ed;
$green-700: #006a36; $green-700: #006a36;
$green-800: #004f28; $green-800: #004f28;
$green-900: #00331a; $green-900: #00331a;
$green-1000: #00723e;
// Yellows // Yellows
$yellow-100: #fff5eb; $yellow-100: #fff5eb;
@ -59,6 +61,8 @@ $gray-600: #4d5151; // Used in theme
$gray-700: #303333; // Used in theme $gray-700: #303333; // Used in theme
$gray-800: #222424; // Used in theme $gray-800: #222424; // Used in theme
$gray-900: #181a1a; // Used in theme $gray-900: #181a1a; // Used in theme
$gray-1000: #b3b4b5;
$gray-1100: #4b4c4e;
//orange //orange
$orange-600: #e86421; $orange-600: #e86421;
@ -134,6 +138,8 @@ $font-weight-lighter: lighter;
$font-weight-light: 300; $font-weight-light: 300;
$font-weight-normal: 400; $font-weight-normal: 400;
$font-weight-bold: 500; $font-weight-bold: 500;
$font-weight-600: 600;
$font-weight-700: 700;
$font-weight-bolder: bolder; $font-weight-bolder: bolder;
//Headings //Headings