Merge branch 'develop' into feature/richardson/INSR-9658

This commit is contained in:
Jeremy-Z 2026-05-27 14:10:51 -04:00 committed by GitHub
commit fa67a6deb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 522 additions and 108 deletions

View file

@ -85,15 +85,10 @@ stages:
clean: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/dev'
SourceFolder: 'public/apple-pay'
Contents: '**'
TargetFolder: 'public/.well-known'
CleanTargetFolder: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/non-prod'
Contents: '**'
TargetFolder: 'public/.well-known'
- task: DeleteFiles@1
inputs:
SourceFolder: 'public/apple-pay'
@ -155,15 +150,10 @@ stages:
clean: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/test'
SourceFolder: 'public/apple-pay'
Contents: '**'
TargetFolder: 'public/.well-known'
CleanTargetFolder: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/non-prod'
Contents: '**'
TargetFolder: 'public/.well-known'
- task: DeleteFiles@1
inputs:
SourceFolder: 'public/apple-pay'
@ -225,15 +215,10 @@ stages:
clean: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/qa'
SourceFolder: 'public/apple-pay'
Contents: '**'
TargetFolder: 'public/.well-known'
CleanTargetFolder: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/non-prod'
Contents: '**'
TargetFolder: 'public/.well-known'
- task: DeleteFiles@1
inputs:
SourceFolder: 'public/apple-pay'
@ -295,7 +280,7 @@ stages:
clean: true
- task: CopyFiles@2
inputs:
SourceFolder: 'public/apple-pay/prod'
SourceFolder: 'public/apple-pay'
Contents: '**'
TargetFolder: 'public/.well-known'
CleanTargetFolder: true

View file

@ -1,74 +0,0 @@
MIIQcwYJKoZIhvcNAQcCoIIQZDCCEGACAQExCzAJBgUrDgMCGgUAMIGBBgkqhkiG9w0BBwGgdARy
eyJ0ZWFtSWQiOiJWOEhTWTM5Tlo1IiwiZG9tYWluIjoic2VsZnNlcnZpY2UudGVzdC5nbGFzc2Ns
YWltLmNvbSIsImRhdGVDcmVhdGVkIjoiMjAyNC0xMC0zMSwxOToxMzo1NSIsInZlcnNpb24iOjF9
oIINPzCCBDQwggMcoAMCAQICCGPwVvLPIlfzMA0GCSqGSIb3DQEBCwUAMHMxLTArBgNVBAMMJEFw
cGxlIGlQaG9uZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEgMB4GA1UECwwXQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTI0MDEzMDE4
MzIwNVoXDTI5MDEyODE4MzIwNFowWTE1MDMGA1UEAwwsQXBwbGUgaVBob25lIE9TIFByb3Zpc2lv
bmluZyBQcm9maWxlIFNpZ25pbmcxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtRV_5CUNXIVz4squ3G3qjRhp_qb_3P3MKS5N
qnAViQ-_R99YzlhZSggdsaENtvYgcVJ3B31kf0Jpiyh78sNAPbNwgRfJ5IYtmVc3dcDU47Y3HNg3
Ju5ALItx8O8UY0zkv9Evh1BFHacScZG0F_bDSUBwbeuPdty188Fh-SSsMVm-V_5KlsWKAJbHuQo5
mFHqgE4ChhHo7XjtR8ZleviwC9WHKX5mm-09veg5un1IZDvniAdfnnwhlpAUAFdAZSau26ODvI0b
96OFYzo39K7QzhRvEVQnUrBoy7RrPtIAxDkU6KxHcFJWNl-eKz5TmabjgNJTeDe7c9-ZDLcckpqv
ZQIDAQABo4HlMIHiMAwGA1UdEwEB_wQCMAAwHwYDVR0jBBgwFoAUb_GVGGJc4Mjxxe1sGMng02RS
mCAwQAYIKwYBBQUHAQEENDAyMDAGCCsGAQUFBzABhiRodHRwOi8vb2NzcC5hcHBsZS5jb20vb2Nz
cDAzLWFpcGNhMDcwLwYDVR0fBCgwJjAkoCKgIIYeaHR0cDovL2NybC5hcHBsZS5jb20vYWlwY2Eu
Y3JsMB0GA1UdDgQWBBQpAEPgxpMF9OJ7_zXSeQb6VJd2IzAOBgNVHQ8BAf8EBAMCB4AwDwYJKoZI
hvdjZAY6BAIFADANBgkqhkiG9w0BAQsFAAOCAQEAQOdTWwDSlgLvp2A-1tu4pTWPWEc7LZiAuf5z
s9aP0U0Ef2W6bLNRvG8zPudxejoC_DBSNrpplqV30i9pP21-mPxbQmJKMlnRfq1XsctFngpI3kYH
NRteAxsuaJ7b2qhsxDcIhaBAmKMSOHAQDUg1ZIWTVgc32dpWOHAlqOFPaFcsWAwaFknePyU2c1jc
5V95KI-5CwO74Zz1Fud4bBJPwbB-cddKsv7RATTij24eLwt6tMGxMoZOv1uYhGhPZ4dPwJ66tDH0
CbSKPdbeECpLxI3DTdTqxWKZmBnr3dF9x_fZ6b2neF4mFF9j5n9EaPAoYb3A4RXgQKv_Yeh7wDlK
DDCCBEQwggMsoAMCAQICCFxjyuRKN1PJMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRMw
EQYDVQQKEwpBcHBsZSBJbmMuMSYwJAYDVQQLEx1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0
eTEWMBQGA1UEAxMNQXBwbGUgUm9vdCBDQTAeFw0xNzA1MTAyMTI3MzBaFw0zMDEyMzEwMDAwMDBa
MHMxLTArBgNVBAMMJEFwcGxlIGlQaG9uZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEgMB4GA1UE
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYT
AlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyUVqAQ8-gwSGx_y_3F7wHoHuFzBz
YyYu3j16JM2TPk85R7p1vvPA0vFZoqsf_gqGPNktmgfyDmu5KZEaXyIKi_FyWAWuTEtExXmngDyw
iOCMDCeEXRnlhxk2y-PFdrew9EFyUfQFXINLom2mUbjxJt97Xq1lDMaymFGMu30bTMFOyAjH0u1k
C7TdG41PQH0bj0iWklvz0Jh-2bykGQ6ZYbtBXQHMW3d6fSTQ3NNT_8PcxZQstlpNjhgjOb3ZxlI-
0fL0JYqhKof92AxGKVH_7RdsiSVrh7-KaRSfd5_DFbdos4hFvYTmBgJBZA-tKii4FcngrKeKunIE
NLJ4jPiyhQIDAQABo4HsMIHpMA8GA1UdEwEB_wQFMAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70
a40uQKb3R01_CF4wRAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5hcHBs
ZS5jb20vb2NzcDAzLWFwcGxlcm9vdGNhMC4GA1UdHwQnMCUwI6AhoB-GHWh0dHA6Ly9jcmwuYXBw
bGUuY29tL3Jvb3QuY3JsMB0GA1UdDgQWBBRv8ZUYYlzgyPHF7WwYyeDTZFKYIDAOBgNVHQ8BAf8E
BAMCAQYwEAYKKoZIhvdjZAYCEgQCBQAwDQYJKoZIhvcNAQELBQADggEBADrPrJiNvpIgIQmtlfOx
XCH6Ni1XIER0c2SSCLOWrPdtl_pbNDgnzxJG0zwR8AfJmZCx0egRCaXjpWtsYwg_niX61ZmcTObl
zo6yTWjsi6ujok-KERU-3BQrHMZEtm9nxVtPlSkth1w_3IMed0_t2lSnLecTgcFjxFQLG0sKaigi
CNQ3knx_Zyhfrz0_t6xZHTg0ZFruM0oZQkQpxMoYa-HBUy0t9E3CFfYzMhh48SZvik3rlEyj6P8P
swOLZdrrLthlUJ_cn4rfMaiEVNxSUkHSshMdMUZHiF8-7sPyjCMEleusij6CbAafLuOLQ5piWzQN
9JnPLO66coYZI6X8jrUwggS7MIIDo6ADAgECAgECMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYT
AlVTMRMwEQYDVQQKEwpBcHBsZSBJbmMuMSYwJAYDVQQLEx1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eTEWMBQGA1UEAxMNQXBwbGUgUm9vdCBDQTAeFw0wNjA0MjUyMTQwMzZaFw0zNTAyMDky
MTQwMzZaMGIxCzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpBcHBsZSBJbmMuMSYwJAYDVQQLEx1BcHBs
ZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEWMBQGA1UEAxMNQXBwbGUgUm9vdCBDQTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOSRqQkfkdseR1DrBe1eeYQt6zaiV0xV7IsZid75S2z1
B6siMALoGD74UAnTf0GomPnRymacJGsR0KO75Bsqwx-VnnoMpEeLW9QWNzPLxA9NzhRp0ckZcvVd
DtV_X5vyJQO6VY9NXQ3xZDUjFUsVWR2zlPf2nJ7PULrBWFBnjwi0IPfLrCwgb3C2PwEwjLdDzw-d
PfMrSSgayP7OtbkO2V4c1ss9tTqt9A8OAJILsSEWLnTVPA3bYharo3GSR1NVwa8vQbP4--Nwzeaj
TEV-H0xrUJZBicR0YgsQg0GHM4qBsTBY7FoEMoxos48d3mVz_2deZbxJ2HafMxRloXeUyS0CAwEA
AaOCAXowggF2MA4GA1UdDwEB_wQEAwIBBjAPBgNVHRMBAf8EBTADAQH_MB0GA1UdDgQWBBQr0GlH
lHYJ_vRrjS5ApvdHTX8IXjAfBgNVHSMEGDAWgBQr0GlHlHYJ_vRrjS5ApvdHTX8IXjCCAREGA1Ud
IASCAQgwggEEMIIBAAYJKoZIhvdjZAUBMIHyMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5hcHBs
ZS5jb20vYXBwbGVjYS8wgcMGCCsGAQUFBwICMIG2GoGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZp
Y2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2Fi
bGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xp
Y3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wDQYJKoZIhvcNAQEFBQAD
ggEBAFw2mUwteLftjJvc83eb8nbSdzBPwR-Fg4UbmT1HN_Kpm0COLNSxkBLYvvRzm-7SZA_LeU80
2KI--Xj_a8gH7H05g4tTINM4xLG_mk8Ka_8r_FmnBQl8F0BWER5007eLIztHo9VvJOLr0bdw3w9F
4SfK8W147ee1Fxeo3H4iNcol1dkP1mvUoiQjEfehrI9zgWDGG1sJL5Ky-ERI8GA4nhX1PSZnIIoz
avcNgs_e66Mv-VNqW2TAYzN39zoHLFbr2g8hDtq6cxlPtdk2f8GHVdmnmbkyQvvY1XGefqFStxu9
k0IkEirHDx22TZxeY8hLgBdQqorV2uT80AkHN7B1dSExggKFMIICgQIBATB_MHMxLTArBgNVBAMM
JEFwcGxlIGlQaG9uZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEgMB4GA1UECwwXQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTAghj8FbyzyJX
8zAJBgUrDgMCGgUAoIHcMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
DTI0MTAzMTE5MTM1NVowIwYJKoZIhvcNAQkEMRYEFEzl9eeNJq1o4Ph1f_9i2uuUiotWMCkGCSqG
SIb3DQEJNDEcMBowCQYFKw4DAhoFAKENBgkqhkiG9w0BAQEFADBSBgkqhkiG9w0BCQ8xRTBDMAoG
CCqGSIb3DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG
9w0DAgIBKDANBgkqhkiG9w0BAQEFAASCAQB3LLlE3JjbMd_fLAvuLSxw33kWPis0DAtVXlCQ8jZD
Hl0tRCN7EEGj2s5VI2FDHaYj10jc6MZimTJxzX6yi0VzqzMlZj7VMhYI5WJlWvGjbldgIQyNURqh
xI4dMMt84jW39VjbxJ0nJYDTFnKaFhM8raJWg-S7uspc-BNn_TRgBY_YdRY-JdMLxAVAPf-9aTEp
jeYlj48CwyQaQTYYQXmKNaFVfBG1djgnVfRqZ1Pgrhdd1IYcWOxhEOYaAwnwQ94Pcl5HVmt271KL
v5QsNNTax_KpwRi9IHPaQ7r8QIKfc0qJ4Wau9Kl4hfBf5xfvQWsmhzwCRWcFK2jDGDADxC_8

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C6.41775 0 4.87104 0.469192 3.55544 1.34824C2.23985 2.22729 1.21447 3.47672 0.608967 4.93853C0.00346626 6.40034 -0.15496 8.00887 0.153721 9.56072C0.462403 11.1126 1.22433 12.538 2.34315 13.6569C3.46197 14.7757 4.88743 15.5376 6.43928 15.8463C7.99113 16.155 9.59966 15.9965 11.0615 15.391C12.5233 14.7855 13.7727 13.7602 14.6518 12.4446C15.5308 11.129 16 9.58225 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0V0ZM8.4672 11.4C8.4672 11.5485 8.4082 11.691 8.30318 11.796C8.19816 11.901 8.05572 11.96 7.9072 11.96C7.75868 11.96 7.61624 11.901 7.51122 11.796C7.4062 11.691 7.3472 11.5485 7.3472 11.4V7.08C7.3472 6.93148 7.4062 6.78904 7.51122 6.68402C7.61624 6.579 7.75868 6.52 7.9072 6.52C8.05572 6.52 8.19816 6.579 8.30318 6.68402C8.4082 6.78904 8.4672 6.93148 8.4672 7.08V11.4ZM7.9072 5.4C7.79645 5.4 7.68818 5.36716 7.59608 5.30562C7.50399 5.24409 7.43222 5.15663 7.38983 5.0543C7.34745 4.95198 7.33636 4.83938 7.35796 4.73075C7.37957 4.62212 7.43291 4.52234 7.51122 4.44402C7.58954 4.3657 7.68932 4.31237 7.79795 4.29076C7.90658 4.26915 8.01918 4.28024 8.12151 4.32263C8.22383 4.36501 8.31129 4.43679 8.37283 4.52888C8.43436 4.62097 8.4672 4.72924 8.4672 4.84C8.46741 4.9136 8.45307 4.98651 8.42501 5.05455C8.39694 5.12259 8.3557 5.18441 8.30365 5.23645C8.25161 5.28849 8.18979 5.32973 8.12176 5.3578C8.05372 5.38587 7.9808 5.40021 7.9072 5.4Z" fill="#0070D1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -2,6 +2,7 @@
exports[`cart-dropdown component initial data rendered as expected 1`] = `
Object {
"ADVANCED_MOBILE_MODAL_REF_NAME": "advancedMobileModal",
"cartItemType": Object {
"MOBILE_FEE": "MOBILE FEE",
"RECYCLE_FEE": "RECYCLE FEE",
@ -9,6 +10,8 @@ Object {
"WARRANTY": "WARRANTY",
},
"widget": Object {
"advancedMobileModal": "AdvancedMobileModal",
"advancedMobileTooltip": "AdvancedMobileTooltip",
"amountDue": "AmountDueTextWidget",
"amountPaid": "AmountPaidTextWidget",
"basePrice": "BasePriceWidget",

View file

@ -0,0 +1,176 @@
<template>
<div class="advanced-mobile-modal-container">
<modal
ref="advancedMobileModal">
<div>
<div class="header">{{ modalHeaderText }}</div>
<div class="subheader">{{ modalHeaderSubText }}</div>
</div>
<div>
<div class="message"
v-html="modalBodyText">
</div>
</div>
<div class="footer-container">
<div>
<button type="button" aria-live="polite" aria-label="Confirm Appointment" class="btn btn-danger change-appointment"
@click="changeAppointmentClicked">
{{ modalCloseButtonText }}
</button>
</div>
<div>
<button type="button" aria-live="polite" aria-label="See more options" class="btn btn-link cancel"
@click="footerButtonClick">
{{ modalFooterSubText }}
</button>
</div>
</div>
</modal>
</div>
</template>
<script>
import modal from '@/digital-components/modal/modal.vue';
export default {
name: 'advanced-mobile-modal',
components: {
modal
},
emits: ['change-appointment-clicked'],
props: {
cmsWidgetName: {
type: String,
default: 'AdvancedMobileModal'
}
},
computed: {
modalBodyText() {
return this.getCmsContent(this.cmsWidgetName, 'BodyText');
},
modalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, 'FooterText');
},
modalFooterSubText() {
return this.getCmsContent(this.cmsWidgetName, 'BodyText2');
},
modalHeaderSubText() {
return this.getCmsContent(this.cmsWidgetName, 'SubheaderText');
},
modalHeaderText() {
return this.getCmsContent(this.cmsWidgetName, 'HeaderText');
},
},
methods: {
openModal() {
this.$refs.advancedMobileModal.openModal();
},
footerButtonClick() {
this.$refs.advancedMobileModal.closeModal();
},
changeAppointmentClicked() {
this.$emit('change-appointment-clicked');
this.$refs.advancedMobileModal.closeModal();
},
}
};
</script>
<style lang="scss" scoped>
.advanced-mobile-modal-container {
.modal {
padding-right: 0;
}
:deep(.modal) {
.modal-dialog {
@include media-breakpoint-up(xs) {
width: auto;
max-width: unset;
}
@include media-breakpoint-up(xl) {
width: 37.5rem;
max-width: 37.5rem;
}
.modal-content {
width: 22rem;
max-width: 22rem;
padding: 1.5rem 0;
border-radius: 0.5rem;
line-height: 1.5rem;
.modal-header {
padding: 0;
}
.modal-body {
padding: 0 1rem;
}
.modal-footer {
display: none;
}
.header {
margin-top: 1rem;
color: #db0020;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.03em;
font-weight: 600;
font-size: 0.875rem;
}
.subheader {
color: black;
text-align: center;
letter-spacing: 0.03em;
font-weight: 400;
font-size: 1.25rem;
line-height: 2rem;
}
.message {
margin-top: 1.5rem;
color: #525656;
letter-spacing: 0.03em;
font-weight: 400;
font-size: 1rem;
line-height: 1.625rem;
.emphasize {
color: black;
font-weight: 600;
}
}
.footer-container {
margin-top: 1.5rem;
padding: 0 1.5rem;
border: none;
.change-appointment {
width: 100%;
padding: .75rem 0;
border-radius: 4.5rem;
letter-spacing: 0.03em;
font-weight: 600;
font-size: 1rem;
}
.cancel {
text-align: center;
width: 100%;
margin-top: 1.5rem;
letter-spacing: 0.03em;
font-weight: 600;
font-size: 1rem;
line-height: 1.625rem;
}
}
}
}
}
}
</style>

View file

@ -33,7 +33,13 @@
:key="i"
class="cart-item packaged">
<div class="price-row">
<span>{{ item?.name ?? '' }}</span>
<span>
{{ item?.name ?? '' }}
<toolTip
v-if="item.cartItemType === cartItemType.MOBILE_FEE"
:cmsWidgetName="widget.advancedMobileTooltip"
:customValueMap="advancedMobileTooltipCustomValueMap" />
</span>
<span>{{ formatAmountInDollars(item?.subTotal ?? 0) }}</span>
</div>
<textLink
@ -47,6 +53,17 @@
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
<textLink
v-if="item.cartItemType === cartItemType.MOBILE_FEE
&& !readOnly"
linkType="text"
text="Remove"
href="javascript:void(0)"
@clickEvent="openModal(ADVANCED_MOBILE_MODAL_REF_NAME)">
<template #after-text>
<span class="sr-only">{{ item?.name ?? '' }}</span>
</template>
</textLink>
</div>
</div>
<div
@ -99,14 +116,18 @@
<span class="deductible-label">{{ deductibleLabel }}:</span>
<span class="deductible-value">{{ getDisplayed(deductible) }}</span>
</div>
<advancedMobileModal
:ref="ADVANCED_MOBILE_MODAL_REF_NAME"
:cmsWidgetName="widget.advancedMobileModal"
@changeAppointmentClicked="switchToInShop" />
</template>
<script>
import { useMainStore } from '@/store';
import contentGroupModal from '@/iss-components/content-group-modal/content-group-modal.vue';
import textBlock from '@/digital-components/text-block/text-block.vue';
import textLink from '@/ux-components/text-link/text-link.vue';
import { formatAmountInDollars } from '@/helpers/text-helper.js';
import advancedMobileModal from '@/iss-components/cart-dropdown/advanced-mobile-modal/advanced-mobile-modal.vue';
import toolTip from '@/ux-components/tool-tip/tool-tip.vue';
// Constants
import { experimentSettings } from '@/constants/experiments';
@ -126,17 +147,21 @@ import { getPriceOfLineItem, getPriceOfLineItems, getTaxOfLineItems } from '@/he
import { processIfStatements } from '@/helpers/cms-content-helper';
const VERIFYING_COVERAGE = 'Verifying coverage';
const ADVANCED_MOBILE_MODAL_REF_NAME = 'advancedMobileModal';
export default {
name: 'confirmation-cart',
components: {
textLink
textLink,
advancedMobileModal,
toolTip
},
props: {
readOnly: Boolean,
showAsPaid: Boolean,
submittedOrder: Object
},
emits: ['switch-to-in-shop'],
data() {
return {
widget: {
@ -153,9 +178,12 @@ export default {
vapsItemDescriptions: 'VapsItemDescriptions',
warrantyText: 'WarrantyCartItemTextWidget',
guaranteeText: 'GuaranteeCartItemTextWidget',
total: 'TotalWidget'
total: 'TotalWidget',
advancedMobileTooltip: 'AdvancedMobileTooltip',
advancedMobileModal: 'AdvancedMobileModal'
},
cartItemType
cartItemType,
ADVANCED_MOBILE_MODAL_REF_NAME
};
},
computed: {
@ -237,9 +265,6 @@ export default {
if (this.recycleFeeCartItem && !isRecycleFeeHidden) {
items.push(this.recycleFeeCartItem);
}
if (this.mobileFeeCartItem && !isMobileFeeHidden && !this.mobileFeeCartItem.isInsurable) {
items.push(this.mobileFeeCartItem);
}
const vapPartTypesInOrder = Array.from(new Set(this.lineItems.vaps?.map((vap) => vap.partType) ?? []));
const vapsCartItems = vapPartTypesInOrder.map((partType) => {
switch (partType) {
@ -254,6 +279,9 @@ export default {
}
});
items.push(...vapsCartItems.filter((item) => item != null));
if (this.mobileFeeCartItem && !isMobileFeeHidden && !this.mobileFeeCartItem.isInsurable) {
items.push(this.mobileFeeCartItem);
}
return items;
},
frontWipersCartItem() {
@ -308,7 +336,7 @@ export default {
const mobileFeeLineItem = getMobileFeeLineItem(this.cartOrder);
const cartItem = mobileFeeLineItem
? this.getCartItem(
this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
mobileFeeLineItem.displayName ?? this.getCmsContent(this.widget.mobileFee, widgetFields.TEXT_BLOCK_WIDGET.TEXT),
[mobileFeeLineItem],
cartItemType.MOBILE_FEE,
partTypeStrings.MOBILE_FEE
@ -335,6 +363,11 @@ export default {
return this.cartOrder.lineItems.glassParts?.some((glassPart) => {
return glassPart.childParts?.some((childPart) => childPart.partType === partTypeStrings.MOLDING);
});
},
advancedMobileTooltipCustomValueMap() {
return {
feeAmount: this.mobileFeeCartItem ? formatAmountInDollars(this.mobileFeeCartItem.subTotal) : ''
}
}
},
methods: {
@ -402,6 +435,9 @@ export default {
return null;
}
},
switchToInShop() {
this.$emit('switch-to-in-shop');
},
formatAmountInDollars
}
};
@ -418,7 +454,7 @@ export default {
.cart-item {
display: flex;
flex-direction: column;
padding: .5rem 0rem;
padding: .5rem 1rem;
width: 100%;
min-width: fit-content;
&:nth-child(odd) {
@ -431,12 +467,14 @@ export default {
.price-row {
display: flex;
justify-content: space-between;
>:last-child {
margin-left: auto;
}
}
span, a {
white-space: nowrap;
padding: 0rem 1rem;
}
a:hover {

View file

@ -50,6 +50,10 @@ const addToCalendarStub = {
render: () => {}
};
const cartDropdownStub = {
template: '<div></div>'
};
const initialStore = {
order: {
schedule: {
@ -171,7 +175,8 @@ function getMountedComponent(mainInitialState = {}, initialData = {}, methodToRu
mountOptions.global.stubs = {
siteFooter: footerStub,
siteHeader: headerStub,
addToCalendar: addToCalendarStub
addToCalendar: addToCalendarStub,
cartDropdown: cartDropdownStub
};
const testingPinia = createTestingPinia({

View file

@ -34,7 +34,8 @@
:showAsPaid="false"
:readOnly="false"
recyclingModalCmsWidgetName="RecycleModal"
servicePackageTitleWidgetName="ServicePackageTitle" />
servicePackageTitleWidgetName="ServicePackageTitle"
@switchToInShop="handleSwitchToInShop" />
</div>
<alert
v-if="displayPayInAdvanceAlert"
@ -500,6 +501,14 @@ export default {
break;
}
},
handleSwitchToInShop() {
this.mainStore.resetServiceLocationAndDependencies();
this.mainStore.updateAppointmentType(AppointmentTypeStrings.IN_SHOP);
this.$router.navigateWithSpinner(
this.navigationScenarios.EDIT_SCHEDULE,
this.$route
);
},
openContactDetailsModal() {
this.$refs.contactDetailsDrawer.openModal();
},

View file

@ -0,0 +1,122 @@
import { shallowMount, mount, enableAutoUnmount } from "@vue/test-utils";
import toolTip from "@/ux-components/tool-tip/tool-tip.vue";
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => "Mock CMS Content"),
},
};
function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = {
propsData: {
cmsWidgetName: "testWidget",
},
mixins: [mockMixin],
};
return { ...defaultMountOptions, ...mountOptionsMockData };
}
describe("toolTip.vue", () => {
enableAutoUnmount(afterEach);
afterEach(() => {
jest.clearAllMocks();
});
it("should not show tooltip initially", () => {
// Arrange
const wrapper = shallowMount(toolTip, setupMocks());
// Assert
expect(wrapper.vm.isVisible).toBe(false);
expect(wrapper.find(".tooltip").exists()).toBe(false);
});
it("should show tooltip when icon is clicked", async () => {
// Arrange
const wrapper = shallowMount(toolTip, setupMocks());
const icon = wrapper.find(".tooltip-icon");
// Act
await icon.trigger("click");
// Assert
expect(wrapper.vm.isVisible).toBe(true);
expect(wrapper.find(".tooltip").exists()).toBe(true);
});
it("should hide tooltip when clicked outside", async () => {
// Arrange
const wrapper = shallowMount(toolTip, setupMocks());
const icon = wrapper.find(".tooltip-icon");
// Show tooltip first
await icon.trigger("click");
expect(wrapper.vm.isVisible).toBe(true);
// Act - simulate click outside
const clickEvent = new Event("click");
Object.defineProperty(clickEvent, "target", { value: document.body });
document.dispatchEvent(clickEvent);
// Wait for next tick to allow event handling
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.isVisible).toBe(false);
expect(wrapper.find(".tooltip").exists()).toBe(false);
});
it("should toggle tooltip visibility when icon is clicked multiple times", async () => {
// Arrange
const wrapper = shallowMount(toolTip, setupMocks());
const icon = wrapper.find(".tooltip-icon");
// Act & Assert - First click shows
await icon.trigger("click");
expect(wrapper.vm.isVisible).toBe(true);
// Second click hides
await icon.trigger("click");
expect(wrapper.vm.isVisible).toBe(false);
// Third click shows again
await icon.trigger("click");
expect(wrapper.vm.isVisible).toBe(true);
});
it("should not hide tooltip when clicking inside the tooltip container", async () => {
// Arrange
const wrapper = shallowMount(toolTip, setupMocks());
const icon = wrapper.find(".tooltip-icon");
// Show tooltip first
await icon.trigger("click");
expect(wrapper.vm.isVisible).toBe(true);
// Act - simulate click inside container
const container = wrapper.find(".tooltip-container").element;
const clickEvent = new Event("click");
Object.defineProperty(clickEvent, "target", { value: container });
document.dispatchEvent(clickEvent);
// Wait for next tick
await wrapper.vm.$nextTick();
// Assert - tooltip should still be visible
expect(wrapper.vm.isVisible).toBe(true);
});
it("should calculate tooltipHorizontalOffset based on parent element width", () => {
// Arrange
const wrapper = mount(toolTip, setupMocks());
const parentDiv = document.createElement("div");
Object.defineProperty(parentDiv, "offsetWidth", { value: 300, writable: true });
const container = wrapper.vm.$refs.container;
Object.defineProperty(container, "parentElement", { value: parentDiv, writable: true });
// Act & Assert
expect(wrapper.vm.tooltipHorizontalOffset).toBe(-290);
});
});

View file

@ -0,0 +1,145 @@
<template>
<div class="tooltip-container" ref="container">
<span
class="tooltip-icon"
role="button"
aria-label="Show more information"
@click="toggleTooltip"
@keydown.enter.prevent="toggleTooltip"
@keydown.space.prevent="toggleTooltip">
<slot name="icon">
<img src="@/assets/img/icons/info-circle-blue.svg" alt="" />
</slot>
</span>
<span
v-if="isVisible"
class="tooltip"
:class="{ 'tooltip-visible': isVisible }"
:style="{ left: tooltipHorizontalOffset + 'px', top: '1rem' }">
<slot>
<h3 v-html="toolTipHeaderText"></h3>
<p v-html="toolTipBodyText"></p>
</slot>
</span>
</div>
</template>
<script>
import { getStringWithCustomValues } from '@/helpers/cms-content-helper';
export default {
name: "tool-tip",
props: {
cmsWidgetName: {
type: String,
default: 'widgetUndefined'
},
customValueMap: {
type: Object,
default: () => ({}),
}
},
data() {
return {
isVisible: false,
};
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
unmounted() {
document.removeEventListener("click", this.handleClickOutside);
},
computed: {
toolTipHeaderText() {
return getStringWithCustomValues(
this.getCmsContent(this.cmsWidgetName, "HeaderText") || "",
this.customValueMap
);
},
toolTipBodyText() {
return getStringWithCustomValues(
this.getCmsContent(this.cmsWidgetName, "BodyText") || "",
this.customValueMap
);
},
tooltipHorizontalOffset() {
// Get the width of the parent container - used to position the tooltip to the left
if (this.$refs.container && this.$refs.container.parentElement) {
return 10 - this.$refs.container.parentElement.offsetWidth;
}
return 0;
}
},
methods: {
showTooltip() {
this.isVisible = true;
},
hideTooltip() {
this.isVisible = false;
},
toggleTooltip() {
this.isVisible = !this.isVisible;
},
handleClickOutside(event) {
if (this.$refs.container && !this.$refs.container.contains(event.target)) {
this.hideTooltip();
}
},
},
};
</script>
<style lang="scss" scoped>
.tooltip-container {
position: relative;
display: inline-block;
margin-left: 0.5rem;
padding: 0rem;
.tooltip-icon {
cursor: pointer;
display: inline-block;
img {
width: 1rem;
}
}
.tooltip {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
left: 1rem;
padding: 1rem;
border-radius: $border-radius;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s,
visibility 0.2s;
z-index: 500;
width: 18.75rem;
background-color: $white;
box-shadow: $box-shadow;
h3 {
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 600;
color: $black;
}
p {
margin: 0;
font-size: .875rem;
line-height: 1.5rem;
}
}
.tooltip-visible {
opacity: 1;
visibility: visible;
}
}
</style>