Merge branch 'develop' into dependabot/npm_and_yarn/fast-uri-3.1.4

This commit is contained in:
Jeremy-Z 2026-07-28 12:25:50 -04:00 committed by GitHub
commit 3b695c5f11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 351 additions and 19 deletions

6
package-lock.json generated
View file

@ -18289,9 +18289,9 @@
} }
}, },
"node_modules/shell-quote": { "node_modules/shell-quote": {
"version": "1.8.4", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View file

@ -20,7 +20,7 @@ jest.mock('@/helpers/order-helper.js', () => ({
saveSession: jest.fn() saveSession: jest.fn()
})); }));
function setupMocks({ customMountOptions = {}, queryString }, mainInitialState = {}, customMixin = null) { function setupMocks({ customMountOptions = {}, queryString }, mainInitialState = {}, customMixin = null, mockCmsContent = '') {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
...customMountOptions, ...customMountOptions,
route: { query: { issPage: issPageValues.PAYMENT_METHOD, ...queryString }, params: {} }, route: { query: { issPage: issPageValues.PAYMENT_METHOD, ...queryString }, params: {} },
@ -51,7 +51,9 @@ function setupMocks({ customMountOptions = {}, queryString }, mainInitialState =
}; };
const cmsMixin = { const cmsMixin = {
methods: { methods: {
getCmsContent: jest.fn() getCmsContent: jest.fn((widget, field) => {
return mockCmsContent;
})
} }
} }
@ -320,4 +322,28 @@ describe('payment-method.vue', () => {
expect(submitWorkOrder).toHaveBeenCalledTimes(1); expect(submitWorkOrder).toHaveBeenCalledTimes(1);
}); });
}); });
describe('State Specific Text Rendering', () => {
test('State specific textBlock component exists when CMS content for it exists', () => {
// Arrange
const mockCmsContent = 'This is a test CMS content for state specific text.';
const wrapper = setupMocks({}, {}, null, mockCmsContent);
// Act
const stateSpecificTextBlock = wrapper.find('.state-specific-text');
// Assert
expect(stateSpecificTextBlock.exists()).toBe(true);
});
test('State specific textBlock component does not exist when CMS content for it does not exist', () => {
// Arrange
const wrapper = setupMocks({}, {}, null, '');
// Act
const stateSpecificTextBlock = wrapper.find('.state-specific-text');
// Assert
expect(stateSpecificTextBlock.exists()).toBe(false);
});
});
}); });

View file

@ -19,6 +19,10 @@
<siteSubHeader <siteSubHeader
class="subheader" class="subheader"
cmsWidgetName="SiteSubHeaderWidget" /> cmsWidgetName="SiteSubHeaderWidget" />
<textBlock
v-if="stateSpecificText"
class="state-specific-text"
:customText="stateSpecificText" />
<div class="main-content-container"> <div class="main-content-container">
<hr class="section-divider" /> <hr class="section-divider" />
<reviewDropdown <reviewDropdown
@ -138,7 +142,7 @@ import buttonMain from '@/ux-components/button-main/button-main.vue';
// Supporting Items // Supporting Items
import settleAllPromises from '@/helpers/layout-helper'; import settleAllPromises from '@/helpers/layout-helper';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import { fetchCmsContentForPage, processIfStatements } from '@/helpers/cms-content-helper';
import { paymentMethods } from '@/constants/payment-method-constants'; import { paymentMethods } from '@/constants/payment-method-constants';
import queryStrings from '@/constants/query-strings'; import queryStrings from '@/constants/query-strings';
import globalRules from '@/constants/global-rules'; import globalRules from '@/constants/global-rules';
@ -214,7 +218,8 @@ export default {
smsOptInQuestion: 'SMSOptInQuestionWidget', smsOptInQuestion: 'SMSOptInQuestionWidget',
smsPhoneNumber: 'SMSPhoneNumberWidget', smsPhoneNumber: 'SMSPhoneNumberWidget',
smsDisclaimer: 'SMSDisclaimerWidget', smsDisclaimer: 'SMSDisclaimerWidget',
wiperOffer: 'WiperOfferPanelWidget' wiperOffer: 'WiperOfferPanelWidget',
stateSpecificText: 'StateSpecificTextWidget'
}, },
rules: { rules: {
optionRequired: globalRules.OPTION_REQUIRED, optionRequired: globalRules.OPTION_REQUIRED,
@ -317,6 +322,13 @@ export default {
body: wiperBody, body: wiperBody,
edit: wiperEdit edit: wiperEdit
}; };
},
stateSpecificText() {
const rawStateSpecificText = this.getCmsContent(this.widget.stateSpecificText, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
if (rawStateSpecificText) {
return processIfStatements(rawStateSpecificText, 'custom', this.getStateSpecificText);
}
return null;
} }
}, },
watch: { watch: {
@ -566,9 +578,14 @@ export default {
action = 'Unverified'; action = 'Unverified';
label = 'Unverified'; label = 'Unverified';
break; break;
d
} }
this.pushEventToGA('Submit Appointment', action, label, true, null, 1); this.pushEventToGA('Submit Appointment', action, label, true, null, 1);
},
getStateSpecificText(value) {
if (value.includes('not')) {
return !value?.toLowerCase().includes(this.mainStore.order.customer.address.state?.toLowerCase());
}
return this.mainStore.order.customer.address.state?.toLowerCase() === value?.toLowerCase();
} }
} }
}; };

View file

@ -0,0 +1,151 @@
<template>
<div class="benefit-recal-modal-container">
<modal
class="benefit-recal-modal"
ref="benefitRecalModal"
:displayCloseButton="true"
:displayFooterButton="false"
:onModalClosedCallback="onModalClosed">
<img
:src="modalImage"
class="recal-image"
alt="" />
<h5
class="header-text"
v-html="modalHeaderText"></h5>
<div v-html="modalBodyText" class="recal-modal-body"></div>
<div class="recal-modal-footer">
<buttonMain
id="modalbtn"
ref="modalButtonMain"
class="w-100"
:buttonText="modalFooterButtonText"
@clickEvent="continueWithSafeliteClicked" />
<textLink
class="mt-4 mb-0"
linkType="text"
:text="modalCloseLinkText"
href="#!"
@click="scheduleElsewhereClicked" />
</div>
</modal>
</div>
</template>
<script>
import widgetFields from '@/constants/cms-widget-fields';
import modal from '@/digital-components/modal/modal.vue';
import buttonMain from '@/ux-components/button-main/button-main.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
export default {
name: 'benefit-recal-modal',
components: {
modal,
buttonMain,
textLink
},
emits: ['continue-safelite-clicked', 'schedule-elsewhere-clicked'],
data() {
return {
schedulingElsewhere: false
};
},
props: {
cmsWidgetName: {
type: String,
default: 'BenefitRecalModalWidget'
}
},
computed: {
modalBodyText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT);
},
modalCloseLinkText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.BODY_TEXT_2);
},
modalHeaderText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.HEADER_TEXT);
},
modalFooterButtonText() {
return this.getCmsContent(this.cmsWidgetName, widgetFields.CONTENT_GROUP_WIDGET.FOOTER_TEXT);
},
modalImage() {
return this.getCmsContent(this.cmsWidgetName, 'Image');
}
},
methods: {
continueWithSafeliteClicked() {
this.$refs.benefitRecalModal.closeModal();
},
scheduleElsewhereClicked() {
this.schedulingElsewhere = true;
this.$refs.benefitRecalModal.closeModal();
},
onModalClosed() {
this.pushEvent();
if (!this.schedulingElsewhere) {
this.$emit('continue-safelite-clicked');
}
else {
this.$emit('schedule-elsewhere-clicked');
}
},
openModal() {
this.$refs.benefitRecalModal.openModal();
},
pushEvent() {
const gaLabel = this.schedulingElsewhere ? 'schedule_elsewhere_clicked' : 'continue_with_safelite_clicked';
this.pushEventToGA(
'safelite_benefit_modal',
'modal_closed',
gaLabel,
true
);
}
}
};
</script>
<style lang="scss" scoped>
.benefit-recal-modal-container {
.recal-image {
margin: 1rem 0;
width: 23.5rem;
height: auto;
}
.recal-modal-footer {
border-top: none;
padding: .625rem 1.5rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
button {
margin: 0;
background-color: $green;
color: $white;
border-color: $blue;
font-weight: $font-weight-bold;
&:hover {
background-color: $green;
color: $white;
border-color: $heritage-blue-secondary;
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $green;
}
&:active,
&:focus {
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $green;
}
}
a {
margin: 1rem 0 0;
}
}
}
:deep(.modal) {
.modal-footer {
display: none;
}
}
</style>

View file

@ -13,6 +13,7 @@ jest.mock('@/helpers/cms-content-helper', () => ({
})); }));
const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal'; const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal';
const BENEFIT_RECAL_MODAL_REF_NAME = 'benefitRecalModal';
const mockMixin = { const mockMixin = {
methods: { methods: {
@ -74,6 +75,15 @@ const recalAckModalStub = {
} }
}; };
const benefitRecalModalStub = {
template: '<div></div>',
methods: {
openModal: jest.fn(),
footerButtonClick: jest.fn()
}
};
function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) { function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
const mountOptions = getMountOptions({ const mountOptions = getMountOptions({
router: { router: {
@ -85,7 +95,8 @@ function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
siteFooter: footerStub, siteFooter: footerStub,
loadingModal: loadingModalStub, loadingModal: loadingModalStub,
serviceLocation: serviceLocationStub, serviceLocation: serviceLocationStub,
recalAckModal: recalAckModalStub recalAckModal: recalAckModalStub,
benefitRecalModal: benefitRecalModalStub
}; };
methodToRun(); methodToRun();
@ -106,6 +117,16 @@ function getShallowMountedComponent(initialData = {}, methodToRun = () => {}) {
writable: false, writable: false,
configurable: true configurable: true
}); });
// Manually create the ref for benefitRecalModal since shallowMount doesn't populate it
Object.defineProperty(wrapper.vm.$refs, BENEFIT_RECAL_MODAL_REF_NAME, {
value: {
openModal: jest.fn(),
footerButtonClick: jest.fn()
},
writable: false,
configurable: true
});
// Manually create the ref for serviceLocation since shallowMount doesn't populate it // Manually create the ref for serviceLocation since shallowMount doesn't populate it
Object.defineProperty(wrapper.vm.$refs, 'serviceLocation', { Object.defineProperty(wrapper.vm.$refs, 'serviceLocation', {
@ -155,9 +176,8 @@ beforeEach(() => {
payment: { payment: {
isInsurance: true isInsurance: true
}, },
lineItems: { issConfig: {
glassParts: [], clientName: 'TestClient'
supportingItems: []
} }
} }
} }
@ -340,7 +360,7 @@ describe('schedule page methods...', () => {
test('recalAckModal should open when part requires recal, but recal not added to order', async () => { test('recalAckModal should open when part requires recal, but recal not added to order', async () => {
// Arrange // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.lineItems.glassParts = [ wrapper.vm.mainStore.order.lineItems.glassParts = [
{ {
partNumber: 'TEST123', partNumber: 'TEST123',
requiresRecalibration: true requiresRecalibration: true
@ -356,7 +376,7 @@ describe('schedule page methods...', () => {
test('navigation forward is blocked when recalAckModal is displayed, but checkbox has not been acknowledged', async () => { test('navigation forward is blocked when recalAckModal is displayed, but checkbox has not been acknowledged', async () => {
// Arrange // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.lineItems.glassParts = [ wrapper.vm.mainStore.order.lineItems.glassParts = [
{ {
partNumber: 'TEST123', partNumber: 'TEST123',
requiresRecalibration: true requiresRecalibration: true
@ -374,7 +394,7 @@ describe('schedule page methods...', () => {
test('navigation forward is allowed when checkbox has been acknowledged on recalAckModal', async () => { test('navigation forward is allowed when checkbox has been acknowledged on recalAckModal', async () => {
// Arrange // Arrange
const { wrapper } = getShallowMountedComponent(); const { wrapper } = getShallowMountedComponent();
wrapper.vm.mainStore.lineItems.glassParts = [ wrapper.vm.mainStore.order.lineItems.glassParts = [
{ {
partNumber: 'TEST123', partNumber: 'TEST123',
requiresRecalibration: true requiresRecalibration: true
@ -390,4 +410,88 @@ describe('schedule page methods...', () => {
// Assert // Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
}); });
}); test('backButtonAction opens benefitRecalModal when hasBenefitRecalModal returns true and benefitRecalModalAcknowledged is false', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.getCmsContent = jest.fn().mockReturnValue('Benefit Recalibration Modal Header');
wrapper.vm.benefitRecalModalAcknowledged = false;
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$refs[BENEFIT_RECAL_MODAL_REF_NAME].openModal).toHaveBeenCalled();
});
test('backButtonAction pushes non-recalibration_version event when opening benefitRecalModal and hasRecalibrationPart is false', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.getCmsContent = jest.fn().mockReturnValue('Benefit Recalibration Modal Header');
wrapper.vm.benefitRecalModalAcknowledged = false;
wrapper.vm.mainStore.order.lineItems.glassParts = [
{
partNumber: 'TEST123',
requiresRecalibration: false
}
];
wrapper.vm.pushEventToGA = jest.fn();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.pushEventToGA).toHaveBeenCalledWith(
'safelite_benefit_modal_displayed',
wrapper.vm.mainStore.accountNameForEvents,
'non-recalibration_version',
true
);
});
test('backButtonAction pushes recalibration_version event when opening benefitRecalModal and hasRecalibrationPart is true', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.getCmsContent = jest.fn().mockReturnValue('Benefit Recalibration Modal Header');
wrapper.vm.benefitRecalModalAcknowledged = false;
wrapper.vm.mainStore.order.lineItems.glassParts = [
{
partNumber: 'TEST123',
requiresRecalibration: true
}
];
wrapper.vm.pushEventToGA = jest.fn();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.pushEventToGA).toHaveBeenCalledWith(
'safelite_benefit_modal_displayed',
wrapper.vm.mainStore.accountNameForEvents,
'recalibration_version',
true
);
});
test('backButtonAction navigates back when hasBenefitRecalModal is true and benefitRecalModalAcknowledged is true', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.getCmsContent = jest.fn().mockReturnValue('Benefit Recalibration Modal Header');
wrapper.vm.benefitRecalModalAcknowledged = true;
wrapper.vm.navigateBack = jest.fn();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.navigateBack).toHaveBeenCalled();
});
test('backButtonAction navigates back when hasBenefitRecalModal is false', () => {
// Arrange
const { wrapper } = getShallowMountedComponent();
wrapper.vm.navigateBack = jest.fn();
// Act
wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.navigateBack).toHaveBeenCalled();
});
});

View file

@ -55,7 +55,7 @@
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardButtonHidden="!hasNeededServiceLocationData" :isForwardButtonHidden="!hasNeededServiceLocationData"
:isForwardButtonNavigationDisabled="!isFormValid" :isForwardButtonNavigationDisabled="!isFormValid"
@backClicked="navigateBack(this, navigateBackScenario)" @backClicked="backButtonAction"
@forwardClicked="forwardButtonAction" /> @forwardClicked="forwardButtonAction" />
</div> </div>
</div> </div>
@ -69,6 +69,11 @@
cmsWidgetName="RecalAckModalWidget" cmsWidgetName="RecalAckModalWidget"
:ackError="ackError" :ackError="ackError"
@recalAcknowledged="updateIsRecalAcknowledged"/> @recalAcknowledged="updateIsRecalAcknowledged"/>
<benefitRecalModal
:ref="BENEFIT_RECAL_MODAL_REF_NAME"
cmsWidgetName="BenefitRecalModalWidget"
@continue-safelite-clicked="continueWithSafeliteClicked"
@schedule-elsewhere-clicked="scheduleElsewhereClicked" />
</Form> </Form>
</template> </template>
<script> <script>
@ -81,6 +86,7 @@ import serviceLocation from '@/layouts/schedule-page/service-location/service-lo
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import suggestTimeslotModal from '@/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue'; import suggestTimeslotModal from '@/layouts/schedule-page/suggest-timeslot-modal/suggest-timeslot-modal.vue';
import recalAckModal from '@/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue'; import recalAckModal from '@/layouts/schedule-page/recal-ack-modal/recal-ack-modal.vue';
import benefitRecalModal from '@/layouts/schedule-page/benefit-recal-modal/benefit-recal-modal.vue';
// Supporting files // Supporting files
import { experimentSettings, experimentTest, experimentVariation, experimentUniverses } from '@/constants/experiments'; import { experimentSettings, experimentTest, experimentVariation, experimentUniverses } from '@/constants/experiments';
@ -118,6 +124,7 @@ import errorMessages from '@/constants/error-messages';
const SUGGEST_TIMESLOT_MODAL_REF_NAME = 'SuggestTimeslotModal'; const SUGGEST_TIMESLOT_MODAL_REF_NAME = 'SuggestTimeslotModal';
const TIME_SLOTS_CALL_DAYS_LIMIT = 15; const TIME_SLOTS_CALL_DAYS_LIMIT = 15;
const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal'; const RECAL_ACK_MODAL_REF_NAME = 'recalAckModal';
const BENEFIT_RECAL_MODAL_REF_NAME = 'benefitRecalModal';
const getAvailableDates = async ( const getAvailableDates = async (
startDateString, startDateString,
@ -240,7 +247,8 @@ export default {
siteFooter, siteFooter,
suggestTimeslotModal, suggestTimeslotModal,
Form, Form,
recalAckModal recalAckModal,
benefitRecalModal
}, },
mixins: [BaseFormMixin], mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -382,7 +390,9 @@ export default {
showDatePickerError: false, showDatePickerError: false,
SUGGEST_TIMESLOT_MODAL_REF_NAME, SUGGEST_TIMESLOT_MODAL_REF_NAME,
RECAL_ACK_MODAL_REF_NAME, RECAL_ACK_MODAL_REF_NAME,
isRecalAcknowledged: this.getIsRecalAcknowledgedForScheduling() isRecalAcknowledged: this.getIsRecalAcknowledgedForScheduling(),
BENEFIT_RECAL_MODAL_REF_NAME,
benefitRecalModalAcknowledged: false
}; };
}, },
computed: { computed: {
@ -439,6 +449,9 @@ export default {
}, },
ackError() { ackError() {
return errorMessages.ACKNOWLEDGEMENT_REQUIRED; return errorMessages.ACKNOWLEDGEMENT_REQUIRED;
},
hasBenefitRecalModal() {
return !!this.getCmsContent('BenefitRecalModalWidget', 'HeaderText');
} }
}, },
watch: { watch: {
@ -1017,6 +1030,27 @@ export default {
getIsRecalAcknowledgedForScheduling() { getIsRecalAcknowledgedForScheduling() {
return this.mainStore.order.isRecalAcknowledgedForScheduling; return this.mainStore.order.isRecalAcknowledgedForScheduling;
}, },
continueWithSafeliteClicked() {
this.benefitRecalModalAcknowledged = true;
},
scheduleElsewhereClicked() {
this.navigateBack(this, this.navigateBackScenario);
},
backButtonAction() {
if (this.hasBenefitRecalModal && !this.benefitRecalModalAcknowledged) {
this.$refs[BENEFIT_RECAL_MODAL_REF_NAME].openModal();
const gaLabel = this.mainStore.hasRecalibrationPart ? 'recalibration_version' : 'non-recalibration_version';
this.pushEventToGA(
'safelite_benefit_modal_displayed',
this.mainStore.accountNameForEvents,
gaLabel,
true
);
}
else {
this.navigateBack(this, this.navigateBackScenario);
}
}
} }
}; };
</script> </script>